mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 13:40:58 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89ee111395 |
@@ -89,6 +89,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -120,13 +121,24 @@ jobs:
|
|||||||
- name: Publish win-x64 CLI
|
- name: Publish win-x64 CLI
|
||||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|
||||||
|
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||||
|
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||||
|
if (Test-Path $archivePath) {
|
||||||
|
Remove-Item $archivePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
build-posix:
|
build-posix:
|
||||||
name: Build ${{ matrix.rid }}
|
name: Build ${{ matrix.rid }}
|
||||||
@@ -146,6 +158,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||||
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
||||||
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
||||||
SPIRV_TARGET_ENV: vulkan1.2
|
SPIRV_TARGET_ENV: vulkan1.2
|
||||||
@@ -210,13 +223,19 @@ jobs:
|
|||||||
if: matrix.rid == 'osx-x64'
|
if: matrix.rid == 'osx-x64'
|
||||||
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
# tar keeps the executable bit, which zip would drop.
|
||||||
|
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||||
|
-C "$PUBLISH_DIR" .
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
@@ -236,28 +255,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Package release assets
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
|
|
||||||
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
|
|
||||||
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
|
|
||||||
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
|
|
||||||
test -d "${package_dir}"
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p release-assets
|
|
||||||
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
|
|
||||||
|
|
||||||
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -267,9 +264,9 @@ jobs:
|
|||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
if [ "${#assets[@]}" -ne 3 ]; then
|
if [ "${#assets[@]}" -eq 0 ]; then
|
||||||
echo "Expected 3 release assets, found ${#assets[@]}." >&2
|
echo "No release assets found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ ehthumbs.db
|
|||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
|
||||||
@@ -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.4</SharpEmuVersion>
|
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ path = [
|
|||||||
"**/packages.lock.json",
|
"**/packages.lock.json",
|
||||||
"scripts/ps5_names.txt",
|
"scripts/ps5_names.txt",
|
||||||
"src/SharpEmu.GUI/Languages/**",
|
"src/SharpEmu.GUI/Languages/**",
|
||||||
"src/SharpEmu.ShaderCompiler.Metal/Templates/**",
|
|
||||||
"tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/**",
|
|
||||||
"_logs/**",
|
"_logs/**",
|
||||||
".github/images/**",
|
".github/images/**",
|
||||||
".github/pull_request_template.md",
|
".github/pull_request_template.md",
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler.Metal/SharpEmu.ShaderCompiler.Metal.csproj" />
|
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||||
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||||
<Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
|
|
||||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Aerolib Catalog
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NID to export name
|
|
||||||
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
|
|
||||||
|
|
||||||
# Export name to NID
|
|
||||||
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
|
|
||||||
|
|
||||||
# Search export names
|
|
||||||
python scripts/aerolib_catalog.py search VideoOut --limit 20
|
|
||||||
|
|
||||||
# Export all NID/name pairs to artifacts/aerolib.txt
|
|
||||||
python scripts/aerolib_catalog.py export
|
|
||||||
```
|
|
||||||
@@ -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 files remain visible to the guest and the game's
|
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||||
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
|
not-found so games that mark cinematics as optional progress to their next
|
||||||
explicitly testing a title whose cinematics are optional.
|
state instead of waiting on an empty Bink GPU texture.
|
||||||
|
|
||||||
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,57 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Guest write watch
|
|
||||||
|
|
||||||
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
|
|
||||||
code and HLE code that damage guest memory. The tool starts only if you set one
|
|
||||||
or more `SHARPEMU_WATCH_*` environment variables.
|
|
||||||
|
|
||||||
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
|
|
||||||
does not monitor stores that native guest code makes directly. Use a platform
|
|
||||||
debugger or a hardware watchpoint to monitor these stores.
|
|
||||||
|
|
||||||
## Watch modes
|
|
||||||
|
|
||||||
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
|
|
||||||
block at the specified guest address.
|
|
||||||
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
|
|
||||||
monitors the first 64 direct mappings that have a size of 64 KiB and
|
|
||||||
protection value `0xF2`.
|
|
||||||
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
|
|
||||||
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
|
|
||||||
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
|
|
||||||
memory range. The tool logs a maximum of 128 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
|
|
||||||
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
|
|
||||||
logs a maximum of 64 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
|
|
||||||
specified upper 32 bits in the destination address.
|
|
||||||
|
|
||||||
For each match, the tool logs the destination address, the data pattern, and the
|
|
||||||
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
|
|
||||||
tag.
|
|
||||||
|
|
||||||
Use these variables together to scan bulk writes in the
|
|
||||||
`0x00000080xxxxxxxx` region.
|
|
||||||
|
|
||||||
macOS and Linux:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
SHARPEMU_WATCH_BULK_TORN=1 \
|
|
||||||
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
|
|
||||||
SharpEmu /path/to/eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:SHARPEMU_WATCH_BULK_TORN = "1"
|
|
||||||
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
|
|
||||||
& .\SharpEmu.exe C:\path\to\game\eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
|
|
||||||
address from a crash dump.
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "disable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
|
|
||||||
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
|
|
||||||
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
|
|
||||||
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def compute_nid(export_name: str) -> str:
|
|
||||||
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
|
|
||||||
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
|
|
||||||
return encoded.rstrip("=").replace("/", "-")
|
|
||||||
|
|
||||||
|
|
||||||
def read_names(path: Path) -> list[str]:
|
|
||||||
try:
|
|
||||||
return [
|
|
||||||
line.strip()
|
|
||||||
for line in path.read_text(encoding="utf-8").splitlines()
|
|
||||||
if line.strip()
|
|
||||||
]
|
|
||||||
except OSError as error:
|
|
||||||
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
|
|
||||||
|
|
||||||
|
|
||||||
def write_pair(nid: str, export_name: str) -> None:
|
|
||||||
print(f"{nid}\t{export_name}")
|
|
||||||
|
|
||||||
|
|
||||||
def lookup(args: argparse.Namespace) -> int:
|
|
||||||
value = args.value.strip()
|
|
||||||
if NID_PATTERN.fullmatch(value):
|
|
||||||
for export_name in read_names(args.names):
|
|
||||||
if compute_nid(export_name) == value:
|
|
||||||
write_pair(value, export_name)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print(f"NID not found in catalog: {value}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
names = set(read_names(args.names))
|
|
||||||
write_pair(compute_nid(value), value)
|
|
||||||
if value not in names:
|
|
||||||
print("Warning: export name is not present in the catalog.", file=sys.stderr)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def search(args: argparse.Namespace) -> int:
|
|
||||||
names = read_names(args.names)
|
|
||||||
if args.regex:
|
|
||||||
try:
|
|
||||||
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
|
|
||||||
except re.error as error:
|
|
||||||
print(f"Invalid regular expression: {error}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
matches = (name for name in names if pattern.search(name))
|
|
||||||
elif args.case_sensitive:
|
|
||||||
matches = (name for name in names if args.query in name)
|
|
||||||
else:
|
|
||||||
query = args.query.casefold()
|
|
||||||
matches = (name for name in names if query in name.casefold())
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for export_name in matches:
|
|
||||||
write_pair(compute_nid(export_name), export_name)
|
|
||||||
count += 1
|
|
||||||
if args.limit and count >= args.limit:
|
|
||||||
break
|
|
||||||
|
|
||||||
if count == 0:
|
|
||||||
print(f"No catalog names matched: {args.query}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def export_catalog(args: argparse.Namespace) -> int:
|
|
||||||
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
|
|
||||||
if args.sort == "nid":
|
|
||||||
pairs.sort(key=lambda pair: (pair[0], pair[1]))
|
|
||||||
elif args.sort == "name":
|
|
||||||
pairs.sort(key=lambda pair: pair[1])
|
|
||||||
|
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
try:
|
|
||||||
with args.output.open("w", encoding="utf-8", newline="\n") as output:
|
|
||||||
output.write("# NID\tExportName\n")
|
|
||||||
for nid, export_name in pairs:
|
|
||||||
output.write(f"{nid}\t{export_name}\n")
|
|
||||||
except OSError as error:
|
|
||||||
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(f"Wrote {len(pairs)} entries to {args.output}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def create_parser() -> argparse.ArgumentParser:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
|
|
||||||
epilog=(
|
|
||||||
"Examples:\n"
|
|
||||||
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
|
|
||||||
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
|
|
||||||
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
|
|
||||||
" python scripts/aerolib_catalog.py export"
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--names",
|
|
||||||
type=Path,
|
|
||||||
default=DEFAULT_NAMES_FILE,
|
|
||||||
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
lookup_parser = subparsers.add_parser(
|
|
||||||
"lookup", help="resolve a NID or calculate the NID for an export name"
|
|
||||||
)
|
|
||||||
lookup_parser.add_argument("value", help="11-character NID or exact export name")
|
|
||||||
lookup_parser.set_defaults(handler=lookup)
|
|
||||||
|
|
||||||
search_parser = subparsers.add_parser(
|
|
||||||
"search", help="find export names and print matching NID/name pairs"
|
|
||||||
)
|
|
||||||
search_parser.add_argument("query", help="name substring or regular expression")
|
|
||||||
search_parser.add_argument(
|
|
||||||
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
|
|
||||||
)
|
|
||||||
search_parser.add_argument(
|
|
||||||
"--case-sensitive", action="store_true", help="match case exactly"
|
|
||||||
)
|
|
||||||
search_parser.add_argument(
|
|
||||||
"--regex", action="store_true", help="treat the query as a regular expression"
|
|
||||||
)
|
|
||||||
search_parser.set_defaults(handler=search)
|
|
||||||
|
|
||||||
export_parser = subparsers.add_parser(
|
|
||||||
"export", help="write every NID/name pair to a tab-separated text file"
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
"output",
|
|
||||||
type=Path,
|
|
||||||
nargs="?",
|
|
||||||
default=DEFAULT_EXPORT_FILE,
|
|
||||||
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
"--sort",
|
|
||||||
choices=("source", "nid", "name"),
|
|
||||||
default="nid",
|
|
||||||
help="output ordering (default: nid)",
|
|
||||||
)
|
|
||||||
export_parser.set_defaults(handler=export_catalog)
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = create_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
return args.handler(args)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -45,6 +45,11 @@ 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);
|
||||||
@@ -607,7 +612,7 @@ internal static partial class Program
|
|||||||
nint jobHandle = 0;
|
nint jobHandle = 0;
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
var created = CreateProcessW(
|
var created = CreateProcessW(
|
||||||
null,
|
processPath,
|
||||||
cmdLineBuilder,
|
cmdLineBuilder,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -1433,7 +1438,7 @@ internal static partial class Program
|
|||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(
|
private static extern bool CreateProcessW(
|
||||||
string? applicationName,
|
string applicationName,
|
||||||
StringBuilder commandLine,
|
StringBuilder commandLine,
|
||||||
nint processAttributes,
|
nint processAttributes,
|
||||||
nint threadAttributes,
|
nint threadAttributes,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ 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)' == ''">
|
||||||
|
|||||||
@@ -13,9 +13,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
</application>
|
</application>
|
||||||
</compatibility>
|
</compatibility>
|
||||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<asmv3:windowsSettings>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@@ -0,0 +1,596 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.3, )",
|
||||||
|
"resolved": "10.0.3",
|
||||||
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
|
},
|
||||||
|
"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.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.vulkan": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia.BuildServices": "11.3.2",
|
||||||
|
"Avalonia.Remote.Protocol": "11.3.18",
|
||||||
|
"MicroCom.Runtime": "0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Desktop": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.Native": "11.3.18",
|
||||||
|
"Avalonia.Skia": "11.3.18",
|
||||||
|
"Avalonia.Win32": "11.3.18",
|
||||||
|
"Avalonia.X11": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Fonts.Inter": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Themes.Fluent": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Iced": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Input": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Input.Common": "2.23.0",
|
||||||
|
"Silk.NET.Input.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tmds.DBus.Protocol": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[0.21.3, )",
|
||||||
|
"resolved": "0.21.3",
|
||||||
|
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/linux-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/osx-arm64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/osx-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/win-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Emulation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
|
|
||||||
/// (immediate-form) instructions.
|
|
||||||
///
|
|
||||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
|
|
||||||
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
|
|
||||||
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
|
|
||||||
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
|
|
||||||
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
|
|
||||||
/// a different register allocation, a title built with a different compiler version, and so on
|
|
||||||
/// - still aborts the title. This class ported from Kyty's
|
|
||||||
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
|
|
||||||
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
|
|
||||||
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
|
|
||||||
///
|
|
||||||
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
|
|
||||||
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
|
|
||||||
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
|
|
||||||
/// </summary>
|
|
||||||
public static class Sse4aBitFieldEmulator
|
|
||||||
{
|
|
||||||
public static bool IsValidBitField(int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong ExtractBitField(ulong value, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
return (value >> idx) & mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
var destinationClearMask = fieldMask << idx;
|
|
||||||
var sourceField = (source & fieldMask) << idx;
|
|
||||||
return (destination & ~destinationClearMask) | sourceField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using Iced.Intel;
|
|
||||||
using SharpEmu.Core.Cpu.Emulation;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
|
||||||
|
|
||||||
// General software fallback for the AMD-only instructions PS5 titles occasionally emit that a
|
|
||||||
// Zen 2-only host implements but Intel hosts (and Rosetta 2 on Apple Silicon) do not:
|
|
||||||
// - SSE4a EXTRQ/INSERTQ, immediate form
|
|
||||||
// - MONITORX/MWAITX
|
|
||||||
//
|
|
||||||
// This is a direct port of Kyty's Loader::X64InstructionEmulator (TryEmulateSse4a /
|
|
||||||
// TryEmulateMonitorxMwaitx). SharpEmu already special-cases exactly one compiled EXTRQ+VPBLENDD
|
|
||||||
// byte sequence at load time (Sse4aExtrqBlendPatch), which only helps the one idiom it was
|
|
||||||
// reverse-engineered from. This file is a general, fault-time fallback that engages for any
|
|
||||||
// immediate-form EXTRQ/INSERTQ or MONITORX/MWAITX the narrower patch (or a title using a
|
|
||||||
// different compiler/register allocation) does not cover, complementing rather than replacing
|
|
||||||
// it: the load-time patch still avoids paying the fault-and-recover cost on the hot path it was
|
|
||||||
// built for, while this method is the safety net for everything else.
|
|
||||||
//
|
|
||||||
// This is deliberately additive: DirectExecutionBackend.IllegalInstruction.cs (the BMI1/BMI2/ABM
|
|
||||||
// fallback) is untouched, and this method is only reached from VectoredHandler after that one
|
|
||||||
// has already declined to handle the fault.
|
|
||||||
public sealed partial class DirectExecutionBackend
|
|
||||||
{
|
|
||||||
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
|
|
||||||
// image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that
|
|
||||||
// area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross-
|
|
||||||
// checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT
|
|
||||||
// layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen
|
|
||||||
// if this offset (or anything before it) were wrong.
|
|
||||||
private const int Win64ContextXmm0Offset = 0x1A0;
|
|
||||||
|
|
||||||
private static int _sse4aSoftwareFallbackAnnounced;
|
|
||||||
private static long _sse4aInstructionsEmulated;
|
|
||||||
private static int _monitorxSoftwareFallbackAnnounced;
|
|
||||||
private static long _monitorxInstructionsEmulated;
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
if (TryRecoverMonitorxMwaitx(contextRecord, rip))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MONITORX/MWAITX above only ever reads guest code memory and rewrites RIP, both of
|
|
||||||
// which the POSIX signal bridge (DirectExecutionBackend.PosixSignals.cs) faithfully
|
|
||||||
// round-trips through the real ucontext, so it works on every supported OS. EXTRQ/
|
|
||||||
// INSERTQ additionally read and write an XMM register: on Windows contextRecord is the
|
|
||||||
// live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible
|
|
||||||
// to the guest, but on POSIX contextRecord is a CONTEXT-shaped scratch buffer that the
|
|
||||||
// bridge only populates with the 17 general-purpose registers - the XMM region is never
|
|
||||||
// read from or written back to the real mcontext/ucontext. Running this on POSIX would
|
|
||||||
// silently compute a result from stale/zeroed XMM bytes and then discard whatever it
|
|
||||||
// "wrote", so keep it Windows-only, matching Kyty's own scope for the identical fix.
|
|
||||||
return OperatingSystem.IsWindows() && TryRecoverSse4aExtractInsert(contextRecord, rip);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
|
|
||||||
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
|
|
||||||
var opcode = new byte[3];
|
|
||||||
if (!TryReadHostBytes(rip, opcode) ||
|
|
||||||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line
|
|
||||||
// and MWAITX blocks until that line is written (or a timeout elapses). Hosts without
|
|
||||||
// the extension raise #UD on either one. We do not model the monitor itself, only its
|
|
||||||
// observable effect on guest forward progress: MONITORX becomes a no-op (arming a
|
|
||||||
// watch we never honour has no side effect of its own) and MWAITX becomes a plain
|
|
||||||
// thread yield, i.e. treat the awaited condition as already satisfied so the guest
|
|
||||||
// loop keeps making progress instead of executing an illegal opcode forever.
|
|
||||||
if (opcode[2] == 0xFB)
|
|
||||||
{
|
|
||||||
Thread.Yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, CTX_RIP, rip + 3);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _monitorxInstructionsEmulated);
|
|
||||||
if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " +
|
|
||||||
"emulating those instructions in software.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows() || !TryReadFaultingInstruction(rip, out var instruction))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
|
|
||||||
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
|
|
||||||
if (!isExtrq && !isInsertq)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.GetOpKind(0) != OpKind.Register ||
|
|
||||||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var destLow = ReadCtxU64(contextRecord, destOffset);
|
|
||||||
if (isExtrq)
|
|
||||||
{
|
|
||||||
var length = (int)instruction.GetImmediate(1);
|
|
||||||
var index = (int)instruction.GetImmediate(2);
|
|
||||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
|
|
||||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (instruction.GetOpKind(1) != OpKind.Register ||
|
|
||||||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var length = (int)instruction.GetImmediate(2);
|
|
||||||
var index = (int)instruction.GetImmediate(3);
|
|
||||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
|
|
||||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _sse4aInstructionsEmulated);
|
|
||||||
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
|
|
||||||
"emulating those instructions in software.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
|
|
||||||
// explicit switch (rather than arithmetic on the Register enum) to match the style already
|
|
||||||
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
|
|
||||||
private static bool TryGetXmmOffset(Register register, out int offset)
|
|
||||||
{
|
|
||||||
switch (register)
|
|
||||||
{
|
|
||||||
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
|
|
||||||
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
|
|
||||||
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
|
|
||||||
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
|
|
||||||
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
|
|
||||||
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
|
|
||||||
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
|
|
||||||
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
|
|
||||||
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
|
|
||||||
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
|
|
||||||
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
|
|
||||||
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
|
|
||||||
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
|
|
||||||
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
|
|
||||||
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
|
|
||||||
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
|
|
||||||
default:
|
|
||||||
offset = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -133,11 +133,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (exceptionCode == StatusIllegalInstruction &&
|
|
||||||
TryRecoverAmdCompatInstruction(contextRecord, rip))
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (IsBenignHostDebugException(exceptionCode))
|
if (IsBenignHostDebugException(exceptionCode))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -530,12 +530,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
if (dispatchResolved &&
|
if (dispatchResolved &&
|
||||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||||
@@ -1329,12 +1326,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
|
|
||||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -1416,8 +1410,6 @@ 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
|
||||||
@@ -1444,9 +1436,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
var expectedSemaphoreTrywaitAgain =
|
|
||||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
||||||
var expectedNetAcceptWouldBlock =
|
var expectedNetAcceptWouldBlock =
|
||||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||||
resultValue == unchecked((int)0x80410123);
|
resultValue == unchecked((int)0x80410123);
|
||||||
@@ -1460,7 +1449,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
!expectedTimedWaitTimeout &&
|
!expectedTimedWaitTimeout &&
|
||||||
!expectedEqueueTimeout &&
|
!expectedEqueueTimeout &&
|
||||||
!expectedMutexTrylockBusy &&
|
!expectedMutexTrylockBusy &&
|
||||||
!expectedSemaphoreTrywaitAgain &&
|
|
||||||
!expectedNetAcceptWouldBlock &&
|
!expectedNetAcceptWouldBlock &&
|
||||||
!expectedUserServiceNoEvent &&
|
!expectedUserServiceNoEvent &&
|
||||||
!expectedPrivacyInvalidParameter)
|
!expectedPrivacyInvalidParameter)
|
||||||
@@ -1583,8 +1571,6 @@ 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
|
||||||
|
|||||||
@@ -712,11 +712,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
||||||
|
|
||||||
// Import dispatch is the hottest managed path in UE titles. Most imports do
|
|
||||||
// not have an exception queued, so publish the dictionary population and let
|
|
||||||
// safe points skip _guestThreadGate entirely in the common case.
|
|
||||||
private int _pendingGuestExceptionCount;
|
|
||||||
|
|
||||||
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
||||||
|
|
||||||
private int _guestThreadPumpDepth;
|
private int _guestThreadPumpDepth;
|
||||||
@@ -1296,12 +1291,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address)
|
private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address)
|
||||||
{
|
{
|
||||||
if (IsHlePreferredNid(nid))
|
|
||||||
{
|
|
||||||
address = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nid == "1jfXLRVzisc" &&
|
if (nid == "1jfXLRVzisc" &&
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -1413,54 +1402,6 @@ 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,
|
||||||
@@ -1525,14 +1466,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
"Q3VBxCXhUHs" =>
|
"Q3VBxCXhUHs" =>
|
||||||
[
|
[
|
||||||
0x48, 0x89, 0xF8,
|
0x48, 0x89, 0xF8,
|
||||||
0x48, 0x85, 0xD2,
|
0x48, 0x89, 0xD1,
|
||||||
0x74, 0x11,
|
0xF3, 0xA4,
|
||||||
0x44, 0x8A, 0x06,
|
|
||||||
0x44, 0x88, 0x07,
|
|
||||||
0x48, 0xFF, 0xC6,
|
|
||||||
0x48, 0xFF, 0xC7,
|
|
||||||
0x48, 0xFF, 0xCA,
|
|
||||||
0x75, 0xEF,
|
|
||||||
0xC3,
|
0xC3,
|
||||||
],
|
],
|
||||||
"8zTFvBIAIN8" =>
|
"8zTFvBIAIN8" =>
|
||||||
@@ -1666,8 +1601,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private static bool IsHlePreferredNid(string nid)
|
private static bool IsHlePreferredNid(string nid)
|
||||||
{
|
{
|
||||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal);
|
||||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLibcLibrary(string libraryName)
|
private static bool IsLibcLibrary(string libraryName)
|
||||||
@@ -2607,22 +2541,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
||||||
{
|
{
|
||||||
// Rosetta does not implement AMD SSE4a EXTRQ. Recognize the compiler's
|
// Rosetta does not implement AMD SSE4a EXTRQ. This exact sequence masks
|
||||||
// EXTRQ+blend idiom (against whichever xmm0-xmm7 it allocated) and rewrite
|
// xmm2 to its low 40 bits, then copies the resulting second dword into
|
||||||
// it into an equivalent SSE4.1 sequence. Match/encode is isolated in
|
// xmm0. PEXTRB/PINSRD provides the same observable result in 12 bytes:
|
||||||
// Sse4aExtrqBlendPatch so it can be unit-tested; here we only patch bytes.
|
// extract source byte 4 and insert the zero-extended value into lane 1.
|
||||||
var window = new ReadOnlySpan<byte>(source, Sse4aExtrqBlendPatch.SequenceLength);
|
ReadOnlySpan<byte> pattern =
|
||||||
if (!Sse4aExtrqBlendPatch.TryMatch(window, out var destRegister, out var srcRegister))
|
[
|
||||||
|
0x66, 0x0F, 0x78, 0xC2, 0x28, 0x00,
|
||||||
|
0xC4, 0xE3, 0x79, 0x02, 0xC2, 0x02,
|
||||||
|
];
|
||||||
|
for (var i = 0; i < pattern.Length; i++)
|
||||||
{
|
{
|
||||||
return false;
|
if (source[i] != pattern[i])
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
Span<byte> replacement = stackalloc byte[Sse4aExtrqBlendPatch.SequenceLength];
|
}
|
||||||
if (!Sse4aExtrqBlendPatch.TryEncode(destRegister, srcRegister, replacement))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> replacement =
|
||||||
|
[
|
||||||
|
0x66, 0x0F, 0x3A, 0x14, 0xD0, 0x04,
|
||||||
|
0x66, 0x0F, 0x3A, 0x22, 0xC0, 0x01,
|
||||||
|
];
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect))
|
if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect))
|
||||||
{
|
{
|
||||||
@@ -3953,10 +3893,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// unwinding. Unity can begin its next stop-the-world cycle in
|
// unwinding. Unity can begin its next stop-the-world cycle in
|
||||||
// that window; treating the new raise as part of the old delivery
|
// that window; treating the new raise as part of the old delivery
|
||||||
// strands the collector waiting for an acknowledgement.
|
// strands the collector waiting for an acknowledgement.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3965,10 +3905,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// managed thread corrupts the worker's control state. Queue the
|
// managed thread corrupts the worker's control state. Queue the
|
||||||
// request and let that exact executor consume it at its next HLE
|
// request and let that exact executor consume it at its next HLE
|
||||||
// boundary, where the original guest thread is safely paused.
|
// boundary, where the original guest thread is safely paused.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4013,17 +3953,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
if (target.ExceptionDeliveryActive)
|
if (target.ExceptionDeliveryActive)
|
||||||
{
|
{
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4184,7 +4124,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
RestoreInterruptedGuestThread();
|
RestoreInterruptedGuestThread();
|
||||||
if (target.State == GuestThreadRunState.Blocked &&
|
if (target.State == GuestThreadRunState.Blocked &&
|
||||||
!target.ExecutorActive &&
|
!target.ExecutorActive &&
|
||||||
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
|
_pendingGuestExceptions.Remove(threadHandle, out var queued))
|
||||||
{
|
{
|
||||||
followUp = queued;
|
followUp = queued;
|
||||||
}
|
}
|
||||||
@@ -4270,11 +4210,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
CpuContext currentContext,
|
CpuContext currentContext,
|
||||||
GuestCpuContinuation interruptedContinuation)
|
GuestCpuContinuation interruptedContinuation)
|
||||||
{
|
{
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
if (threadHandle == 0)
|
if (threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -4288,7 +4223,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
|
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4350,27 +4285,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueuePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
PendingGuestException pending)
|
|
||||||
{
|
|
||||||
_pendingGuestExceptions[threadHandle] = pending;
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryRemovePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
out PendingGuestException pending)
|
|
||||||
{
|
|
||||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteGuestExceptionContext(
|
private static bool TryWriteGuestExceptionContext(
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -4465,7 +4379,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_guestThreads.Clear();
|
_guestThreads.Clear();
|
||||||
_externalGuestThreads.Clear();
|
_externalGuestThreads.Clear();
|
||||||
_pendingGuestExceptions.Clear();
|
_pendingGuestExceptions.Clear();
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
|
||||||
_activeGuestExceptionDeliveries.Clear();
|
_activeGuestExceptionDeliveries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Recognizes Sony's AMD-only SSE4a EXTRQ+blend idiom and rewrites it into an
|
|
||||||
/// equivalent SSE4.1 sequence. SharpEmu executes guest x86-64 natively, but
|
|
||||||
/// Rosetta 2 and Intel hosts do not implement SSE4a, so the original opcode
|
|
||||||
/// raises #UD -> SIGILL. The compiler emits the idiom against whichever XMM
|
|
||||||
/// register it happens to allocate (Dead Cells uses xmm1, others xmm2), so the
|
|
||||||
/// source register is read from the ModRM r/m field rather than hard-coded.
|
|
||||||
///
|
|
||||||
/// The match/encode logic is deliberately free of native page-patching so it
|
|
||||||
/// can be unit-tested against handcrafted byte sequences.
|
|
||||||
/// </summary>
|
|
||||||
public static class Sse4aExtrqBlendPatch
|
|
||||||
{
|
|
||||||
/// <summary>Length in bytes of both the matched idiom and its replacement.</summary>
|
|
||||||
public const int SequenceLength = 12;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Matches the 12-byte idiom, extracting the destination register D and the
|
|
||||||
/// source (scratch) register N:
|
|
||||||
/// <code>
|
|
||||||
/// EXTRQ xmmN, 0x28, 0x00 ; 66 0F 78 /0 28 00 mask xmmN to low 40 bits
|
|
||||||
/// VPBLENDD xmmD, xmmD, xmmN, 2 ; C4 E3 vvvv 02 /r 02 copy dword 1 into xmmD
|
|
||||||
/// </code>
|
|
||||||
/// N lives in the ModRM r/m field of both instructions; D (the blend
|
|
||||||
/// destination and src1) lives in the VPBLENDD ModRM reg field and VEX.vvvv.
|
|
||||||
/// Both are xmm0-xmm7 (the VEX byte1 0xE3 pins R/X/B, so no xmm8-15 extension).
|
|
||||||
/// The compiler allocates whichever registers it likes — Dead Cells builds use
|
|
||||||
/// D=xmm0 and D=xmm3, others differ — so both are read from the encoding.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryMatch(ReadOnlySpan<byte> source, out int destRegister, out int srcRegister)
|
|
||||||
{
|
|
||||||
destRegister = -1;
|
|
||||||
srcRegister = -1;
|
|
||||||
if (source.Length < SequenceLength)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXTRQ xmmN, 0x28, 0x00 : 66 0F 78, ModRM (mod=11 reg=000 rm=N), 28, 00.
|
|
||||||
if (source[0] != 0x66 || source[1] != 0x0F || source[2] != 0x78 ||
|
|
||||||
(source[3] & 0xF8) != 0xC0 || source[4] != 0x28 || source[5] != 0x00)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var n = source[3] & 0x07;
|
|
||||||
|
|
||||||
// VPBLENDD xmmD, xmmD, xmmN, 2 : C4 E3 <W=0 vvvv=~D L=0 pp=01> 02 ModRM 02.
|
|
||||||
// VEX.byte2 fixed bits (W, L, pp) must read 0b*0000*01; vvvv encodes ~D.
|
|
||||||
if (source[6] != 0xC4 || source[7] != 0xE3 || (source[8] & 0x87) != 0x01 ||
|
|
||||||
source[9] != 0x02 || source[11] != 0x02)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = (~(source[8] >> 3)) & 0x0F;
|
|
||||||
if (d > 7)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModRM: mod=11, reg=D (dest = src1), rm=N (src2 = the masked register).
|
|
||||||
if (source[10] != (0xC0 | (d << 3) | n))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destRegister = d;
|
|
||||||
srcRegister = n;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the SSE4.1 equivalent into <paramref name="destination"/>:
|
|
||||||
/// <code>
|
|
||||||
/// PEXTRB eax, xmmN, 4 ; 66 0F 3A 14 /r 04 extract byte 4 (zero-extended)
|
|
||||||
/// PINSRD xmmD, eax, 1 ; 66 0F 3A 22 /r 01 insert into xmmD dword lane 1
|
|
||||||
/// </code>
|
|
||||||
/// After EXTRQ masks xmmN to its low 40 bits, dword 1 is just byte 4
|
|
||||||
/// zero-extended, so the two-instruction extract/insert reproduces the exact
|
|
||||||
/// observable result the AMD idiom left in xmmD. eax is a caller-dead scratch
|
|
||||||
/// at every site the compiler emits this idiom.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryEncode(int destRegister, int srcRegister, Span<byte> destination)
|
|
||||||
{
|
|
||||||
if ((uint)destRegister > 7 || (uint)srcRegister > 7 || destination.Length < SequenceLength)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PEXTRB eax, xmmN, 4 : ModRM (mod=11 reg=N rm=000 -> eax), imm8 = byte index 4.
|
|
||||||
destination[0] = 0x66;
|
|
||||||
destination[1] = 0x0F;
|
|
||||||
destination[2] = 0x3A;
|
|
||||||
destination[3] = 0x14;
|
|
||||||
destination[4] = (byte)(0xC0 | (srcRegister << 3));
|
|
||||||
destination[5] = 0x04;
|
|
||||||
|
|
||||||
// PINSRD xmmD, eax, 1 : ModRM (mod=11 reg=D -> xmmD, rm=000 -> eax), lane 1.
|
|
||||||
destination[6] = 0x66;
|
|
||||||
destination[7] = 0x0F;
|
|
||||||
destination[8] = 0x3A;
|
|
||||||
destination[9] = 0x22;
|
|
||||||
destination[10] = (byte)(0xC0 | (destRegister << 3));
|
|
||||||
destination[11] = 0x01;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,9 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
|
|
||||||
_inner.TryCopy(destinationAddress, sourceAddress, length);
|
|
||||||
|
|
||||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
{
|
{
|
||||||
if (_inner is IGuestMemoryAllocator allocator)
|
if (_inner is IGuestMemoryAllocator allocator)
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static CommittedRangeCache? _committedRangeCache;
|
|
||||||
|
|
||||||
private long _mappingGeneration;
|
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
@@ -33,77 +28,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
private const int CommittedRangeCacheCapacity = 4;
|
|
||||||
|
|
||||||
private sealed class CommittedRangeCache
|
|
||||||
{
|
|
||||||
private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity];
|
|
||||||
private PhysicalVirtualMemory? _owner;
|
|
||||||
private long _generation;
|
|
||||||
private int _count;
|
|
||||||
private int _nextReplacement;
|
|
||||||
|
|
||||||
public bool Contains(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start >= range.Start && end <= range.End)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
_generation = generation;
|
|
||||||
_count = 0;
|
|
||||||
_nextReplacement = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start <= range.End && end >= range.Start)
|
|
||||||
{
|
|
||||||
_ranges[index] = new CommittedRange(
|
|
||||||
Math.Min(start, range.Start),
|
|
||||||
Math.Max(end, range.End));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_count < _ranges.Length)
|
|
||||||
{
|
|
||||||
_ranges[_count++] = new CommittedRange(start, end);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ranges[_nextReplacement] = new CommittedRange(start, end);
|
|
||||||
_nextReplacement = (_nextReplacement + 1) % _ranges.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct CommittedRange(ulong Start, ulong End);
|
|
||||||
|
|
||||||
// Raw Windows PAGE_* values retained for the internal region/protection
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
@@ -425,87 +349,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
|
||||||
{
|
|
||||||
if (size == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = AlignDown(address, PageSize);
|
|
||||||
var end = AlignUp(address + size, PageSize);
|
|
||||||
if (end <= start)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
|
||||||
|
|
||||||
// Walk the range page-run by page-run. VirtualQuery reports the largest run
|
|
||||||
// of same-state pages from the queried address, so a single query advances
|
|
||||||
// us over whole free or occupied stretches. Only free stretches get backed;
|
|
||||||
// stretches already reserved or committed by another allocation are left as
|
|
||||||
// they are, which is exactly what a fixed mapping does on hardware.
|
|
||||||
var cursor = start;
|
|
||||||
var backedAny = false;
|
|
||||||
while (cursor < end)
|
|
||||||
{
|
|
||||||
if (!_hostMemory.Query(cursor, out var info))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
|
||||||
? ulong.MaxValue
|
|
||||||
: info.BaseAddress + info.RegionSize;
|
|
||||||
var runEnd = Math.Min(end, queriedEnd);
|
|
||||||
if (runEnd <= cursor)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.State == HostRegionState.Free)
|
|
||||||
{
|
|
||||||
var runSize = runEnd - cursor;
|
|
||||||
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
|
||||||
if (allocated != cursor)
|
|
||||||
{
|
|
||||||
if (allocated != 0)
|
|
||||||
{
|
|
||||||
_hostMemory.Free(allocated);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
|
||||||
_gate.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InsertRegionSorted(new MemoryRegion
|
|
||||||
{
|
|
||||||
VirtualAddress = cursor,
|
|
||||||
Size = runSize,
|
|
||||||
IsExecutable = executable,
|
|
||||||
IsReservedOnly = false,
|
|
||||||
Protection = protection
|
|
||||||
});
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gate.ExitWriteLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
|
||||||
backedAny = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = runEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return backedAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAllocateAtOrAbove(
|
public bool TryAllocateAtOrAbove(
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
@@ -597,7 +440,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.ExitWriteLock();
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
_hostMemory.Free(address);
|
_hostMemory.Free(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,7 +611,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
_allocationSearchHints.Clear();
|
_allocationSearchHints.Clear();
|
||||||
}
|
}
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1032,15 +873,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
// A managed write into a page the guest-image write tracker has
|
|
||||||
// protected surfaces as a fatal AccessViolation — the runtime turns
|
|
||||||
// SIGSEGV in managed code into an exception before the resumable
|
|
||||||
// signal bridge can restore access (native guest stores recover
|
|
||||||
// there). Pre-visit the span so tracked pages are unprotected and
|
|
||||||
// their owners dirtied before the copy; guest addresses are
|
|
||||||
// host-identical, matching the tracker's fault addresses.
|
|
||||||
GuestImageWriteTracker.NotifyManagedWrite(virtualAddress, (ulong)source.Length);
|
|
||||||
|
|
||||||
var requiresExclusiveAccess = false;
|
var requiresExclusiveAccess = false;
|
||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
@@ -1078,7 +910,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1104,68 +935,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
|
|
||||||
{
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length)
|
|
||||||
{
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (length > int.MaxValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match TryWrite's managed-write notification before touching an
|
|
||||||
// identity-mapped guest page protected by the image tracker.
|
|
||||||
GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length);
|
|
||||||
|
|
||||||
_gate.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sourceRegion = FindRegion(sourceAddress, length);
|
|
||||||
var destinationRegion = FindRegion(destinationAddress, length);
|
|
||||||
if (sourceRegion is null || destinationRegion is null ||
|
|
||||||
!TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) ||
|
|
||||||
!TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourcePointer = sourceRegion.VirtualAddress + sourceOffset;
|
|
||||||
var destinationPointer = destinationRegion.VirtualAddress + destinationOffset;
|
|
||||||
if ((sourceRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(sourcePointer, length, sourceRegion)) ||
|
|
||||||
(destinationRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(destinationPointer, length, destinationRegion)) ||
|
|
||||||
!CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) ||
|
|
||||||
!CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Span.CopyTo has memmove overlap semantics, so this allocation-free
|
|
||||||
// path safely serves both libc memcpy and libc memmove.
|
|
||||||
new ReadOnlySpan<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
|
|
||||||
new Span<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
NotifyGuestWriteWatch(
|
|
||||||
destinationAddress,
|
|
||||||
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gate.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
@@ -1238,7 +1007,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1263,7 +1031,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1505,12 +1272,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var mappingGeneration = Volatile.Read(ref _mappingGeneration);
|
|
||||||
var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache();
|
|
||||||
if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
@@ -1532,9 +1293,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if (info.State == HostRegionState.Committed)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
// The host query proved this whole range is committed. Retain
|
|
||||||
// that result instead of caching only the caller's small span.
|
|
||||||
CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1550,23 +1308,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(startPage, endPage, mappingGeneration);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
|
|
||||||
{
|
|
||||||
(_committedRangeCache ??= new CommittedRangeCache()).Add(
|
|
||||||
this,
|
|
||||||
mappingGeneration,
|
|
||||||
startPage,
|
|
||||||
endPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryTemporarilyProtectForRead(
|
private bool TryTemporarilyProtectForRead(
|
||||||
ulong address,
|
ulong address,
|
||||||
ulong size,
|
ulong size,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
@@ -94,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyToRegions(virtualAddress, source, regionIndex);
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryValidateRange(
|
private bool TryValidateRange(
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
if (!CreateProcessW(
|
if (!CreateProcessW(
|
||||||
null,
|
exePath,
|
||||||
commandLine,
|
commandLine,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -629,7 +629,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(string? applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
private static extern bool CreateProcessW(string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||||
|
|||||||
@@ -351,13 +351,6 @@ public sealed class GameSurfaceHost : NativeControlHost
|
|||||||
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
||||||
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
||||||
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
||||||
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
|
|
||||||
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
|
|
||||||
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
|
|
||||||
}
|
|
||||||
_surface.UpdatePixelSize(width, height);
|
_surface.UpdatePixelSize(width, height);
|
||||||
|
|
||||||
if (!sizeChanged)
|
if (!sizeChanged)
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||||
public List<string> EnvironmentToggles { get; set; } = new();
|
public List<string> EnvironmentToggles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
|
||||||
public double RenderResolutionScale { get; set; } = 1.0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
@@ -74,7 +71,7 @@ public sealed class GuiSettings
|
|||||||
if (File.Exists(SettingsPath))
|
if (File.Exists(SettingsPath))
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(SettingsPath);
|
var json = File.ReadAllText(SettingsPath);
|
||||||
return NormalizeFromJson(json);
|
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -85,39 +82,6 @@ public sealed class GuiSettings
|
|||||||
return new GuiSettings();
|
return new GuiSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserializes settings and normalizes null references and null or empty list
|
|
||||||
/// entries introduced by JSON. Empty scalar strings remain unchanged.
|
|
||||||
/// </summary>
|
|
||||||
internal static GuiSettings NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
|
||||||
|
|
||||||
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
|
|
||||||
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
|
|
||||||
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
|
|
||||||
settings.LogLevel ??= "Info";
|
|
||||||
settings.Language ??= "en";
|
|
||||||
settings.DiscordClientId ??= "1525606762248540221";
|
|
||||||
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
|
|
||||||
{
|
|
||||||
settings.RenderResolutionScale = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON can populate non-nullable lists with null references and entries.
|
|
||||||
private static List<string> FilterNullOrEmpty(List<string>? source)
|
|
||||||
{
|
|
||||||
if (source is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -400,29 +400,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
|
||||||
|
|
||||||
<Border Classes="card">
|
|
||||||
<StackPanel Spacing="14">
|
|
||||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
|
||||||
|
|
||||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
|
||||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
|
||||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
|
||||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
|
||||||
</ComboBox>
|
|
||||||
</local:SettingRow>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|||||||
@@ -92,11 +92,6 @@ 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;
|
||||||
@@ -155,18 +150,8 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
||||||
|
|
||||||
// Native popups float above every window on the desktop; they must
|
Activated += (_, _) => UpdateSessionBarVisibility();
|
||||||
// follow the launcher into the background or a minimized state.
|
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
|
||||||
Activated += (_, _) =>
|
|
||||||
{
|
|
||||||
UpdateSessionBarVisibility();
|
|
||||||
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
|
|
||||||
};
|
|
||||||
Deactivated += (_, _) =>
|
|
||||||
{
|
|
||||||
SessionBarPopup.IsOpen = false;
|
|
||||||
SessionLoadingPopup.IsOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||||
@@ -192,18 +177,6 @@ public partial class MainWindow : Window
|
|||||||
// it is open already uses the new values.
|
// it is open already uses the new values.
|
||||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
|
||||||
double.TryParse(
|
|
||||||
tag,
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
out var scale))
|
|
||||||
{
|
|
||||||
_settings.RenderResolutionScale = scale;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||||
@@ -441,15 +414,6 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -499,6 +463,11 @@ public partial class MainWindow : Window
|
|||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||||
|
{
|
||||||
|
StopEmulator();
|
||||||
|
}
|
||||||
|
|
||||||
_previousPadButtons = pad.Buttons;
|
_previousPadButtons = pad.Buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,13 +850,6 @@ public partial class MainWindow : Window
|
|||||||
_ => 2,
|
_ => 2,
|
||||||
};
|
};
|
||||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||||
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
|
|
||||||
{
|
|
||||||
>= 0.875 => 0,
|
|
||||||
>= 0.625 => 1,
|
|
||||||
>= 0.375 => 2,
|
|
||||||
_ => 3,
|
|
||||||
};
|
|
||||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
@@ -1664,23 +1626,13 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1807,12 +1759,6 @@ public partial class MainWindow : Window
|
|||||||
_appliedEnvironmentVariables.Add(name);
|
_appliedEnvironmentVariables.Add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(
|
|
||||||
"SHARPEMU_RENDER_SCALE",
|
|
||||||
_settings.RenderResolutionScale.ToString(
|
|
||||||
"0.###",
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||||
{
|
{
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
@@ -2055,27 +2001,16 @@ public partial class MainWindow : Window
|
|||||||
RestoreGameViewToFull();
|
RestoreGameViewToFull();
|
||||||
GameView.Background = Brushes.Black;
|
GameView.Background = Brushes.Black;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
|
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||||
|
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||||
LibraryPage.IsVisible = false;
|
LibraryPage.IsVisible = false;
|
||||||
OptionsPage.IsVisible = false;
|
OptionsPage.IsVisible = false;
|
||||||
LibraryToolbar.IsVisible = false;
|
LibraryToolbar.IsVisible = false;
|
||||||
ContentToolbar.IsVisible = false;
|
ContentToolbar.IsVisible = false;
|
||||||
ConsolePanel.IsVisible = false;
|
ConsolePanel.IsVisible = false;
|
||||||
LaunchBar.IsVisible = false;
|
LaunchBar.IsVisible = false;
|
||||||
HideSessionLoading();
|
SessionLoadingPopup.IsOpen = false;
|
||||||
UpdateSessionBarVisibility();
|
UpdateSessionBarVisibility();
|
||||||
|
|
||||||
// Defer so the layout pass from the margin change above settles first.
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (!_isRunning || _isStopping)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameSurfaceHost?.RefreshSurfaceSize();
|
|
||||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
|
||||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2174,7 +2109,7 @@ public partial class MainWindow : Window
|
|||||||
GameView.IsVisible = false;
|
GameView.IsVisible = false;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
SessionBarPopup.IsOpen = false;
|
SessionBarPopup.IsOpen = false;
|
||||||
HideSessionLoading();
|
SessionLoadingPopup.IsOpen = false;
|
||||||
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;
|
||||||
@@ -2258,14 +2193,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
SessionLoadingTitle.Text = title;
|
SessionLoadingTitle.Text = title;
|
||||||
SessionLoadingDetail.Text = detail;
|
SessionLoadingDetail.Text = detail;
|
||||||
_sessionLoadingActive = true;
|
SessionLoadingPopup.IsOpen = true;
|
||||||
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HideSessionLoading()
|
|
||||||
{
|
|
||||||
_sessionLoadingActive = false;
|
|
||||||
SessionLoadingPopup.IsOpen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReturnToLibraryWhileStopping()
|
private void ReturnToLibraryWhileStopping()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class PerGameSettings
|
|||||||
var path = PathFor(titleId);
|
var path = PathFor(titleId);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
return NormalizeFromJson(File.ReadAllText(path));
|
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -59,18 +59,6 @@ public sealed class PerGameSettings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A null list inherits global settings; only entries in a present list are sanitized.
|
|
||||||
internal static PerGameSettings? NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<PerGameSettings>(json, SerializerOptions);
|
|
||||||
if (settings?.EnvironmentToggles is { } toggles)
|
|
||||||
{
|
|
||||||
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(string titleId)
|
public void Save(string titleId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(titleId))
|
if (string.IsNullOrWhiteSpace(titleId))
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
public int Armed;
|
public int Armed;
|
||||||
public int FirstCpuWriteSeen;
|
public int FirstCpuWriteSeen;
|
||||||
public int PendingFirstCpuWrite;
|
public int PendingFirstCpuWrite;
|
||||||
public long WriteGeneration;
|
|
||||||
public bool TraceLifetime;
|
public bool TraceLifetime;
|
||||||
public long SourceSequence;
|
public long SourceSequence;
|
||||||
public long FirstCpuWriteTraceSequence;
|
public long FirstCpuWriteTraceSequence;
|
||||||
@@ -52,33 +51,9 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
||||||
|
|
||||||
/// <summary>Immutable snapshot read lock-free from the signal handler and
|
// Snapshot array read lock-free from the signal handler; rebuilt on every
|
||||||
/// the managed-write pre-visit; rebuilt on every mutation under the gate
|
// mutation under the gate. Signal handlers must not take managed locks.
|
||||||
/// (signal handlers must not take managed locks). Carrying the overall
|
private static TrackedRange[] _rangeSnapshot = [];
|
||||||
/// bounds inside the same object keeps the hot-path intersection test
|
|
||||||
/// consistent with the array it guards.</summary>
|
|
||||||
private sealed class RangeSnapshot
|
|
||||||
{
|
|
||||||
public static readonly RangeSnapshot Empty = new([]);
|
|
||||||
|
|
||||||
public readonly TrackedRange[] Ranges;
|
|
||||||
public readonly ulong Start;
|
|
||||||
public readonly ulong End;
|
|
||||||
|
|
||||||
public RangeSnapshot(TrackedRange[] ranges)
|
|
||||||
{
|
|
||||||
Ranges = ranges;
|
|
||||||
Start = ulong.MaxValue;
|
|
||||||
End = 0;
|
|
||||||
foreach (var range in ranges)
|
|
||||||
{
|
|
||||||
Start = Math.Min(Start, range.Start);
|
|
||||||
End = Math.Max(End, range.End);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
|
|
||||||
|
|
||||||
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||||
@@ -156,21 +131,10 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
{
|
{
|
||||||
// Never resize an object that is still reachable from the
|
// Never resize an object that is still reachable from the
|
||||||
// signal handler's lock-free snapshot. Retire it and publish
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
// a fresh immutable range, carrying the write generation so
|
// a fresh immutable range.
|
||||||
// resizes do not hide guest CPU rewrites from cache owners.
|
|
||||||
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
DisarmLocked(range, "replace-range");
|
DisarmLocked(range, "replace-range");
|
||||||
_rangesByAddress.Remove(address);
|
_rangesByAddress.Remove(address);
|
||||||
range = new TrackedRange
|
range = null;
|
||||||
{
|
|
||||||
Address = address,
|
|
||||||
ByteCount = byteCount,
|
|
||||||
Start = start,
|
|
||||||
End = start + length,
|
|
||||||
WriteGeneration = writeGeneration,
|
|
||||||
};
|
|
||||||
_rangesByAddress[address] = range;
|
|
||||||
RebuildSnapshotLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range is null)
|
if (range is null)
|
||||||
@@ -284,31 +248,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the monotonic first-write generation for a tracked allocation.
|
|
||||||
/// Unlike the consuming dirty flag, this remains changed after another
|
|
||||||
/// cache owner consumes and re-arms the range.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
|
||||||
{
|
|
||||||
generation = 0;
|
|
||||||
if (!_enabled)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generation = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
@@ -327,17 +266,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
var end = address > ulong.MaxValue - byteCount
|
var end = address > ulong.MaxValue - byteCount
|
||||||
? ulong.MaxValue
|
? ulong.MaxValue
|
||||||
: address + byteCount;
|
: address + byteCount;
|
||||||
|
|
||||||
// Fast rejection for the hot path: this runs on every managed guest
|
|
||||||
// write, and almost none of them touch tracked texture pages. The
|
|
||||||
// bounds live inside the snapshot so they are always consistent with
|
|
||||||
// the ranges the per-page visit below would consult.
|
|
||||||
var snapshot = Volatile.Read(ref _rangeSnapshot);
|
|
||||||
if (snapshot.Ranges.Length == 0 || end <= snapshot.Start || address >= snapshot.End)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = address;
|
var candidate = address;
|
||||||
while (candidate < end)
|
while (candidate < end)
|
||||||
{
|
{
|
||||||
@@ -383,7 +311,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranges = Volatile.Read(ref _rangeSnapshot).Ranges;
|
var ranges = Volatile.Read(ref _rangeSnapshot);
|
||||||
var writableStart = ulong.MaxValue;
|
var writableStart = ulong.MaxValue;
|
||||||
var writableEnd = 0UL;
|
var writableEnd = 0UL;
|
||||||
for (var index = 0; index < ranges.Length; index++)
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
@@ -462,10 +390,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
if (wasArmed)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref range.WriteGeneration);
|
|
||||||
}
|
|
||||||
if (wasArmed &&
|
if (wasArmed &&
|
||||||
range.TraceLifetime &&
|
range.TraceLifetime &&
|
||||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
@@ -534,7 +458,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
|
|
||||||
private static void RebuildSnapshotLocked()
|
private static void RebuildSnapshotLocked()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _rangeSnapshot, new RangeSnapshot(_rangesByAddress.Values.ToArray()));
|
_rangeSnapshot = _rangesByAddress.Values.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class GuestTlsTemplate
|
|||||||
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||||
// modules can require more than one host page of Variant II static TLS;
|
// modules can require more than one host page of Variant II static TLS;
|
||||||
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||||
public const ulong StartupStaticTlsReservation = 0x20000UL; // Was 0x10000UL, but thats too small for GTA V
|
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||||
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
|
||||||
|
|
||||||
// This tool monitors guest-memory writes only when a watch mode is active.
|
|
||||||
public static class GuestWriteWatch
|
|
||||||
{
|
|
||||||
private const ulong WatchBytes = 8;
|
|
||||||
private const int MaxBulkReports = 64;
|
|
||||||
|
|
||||||
private static readonly ulong WatchBase = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_WRITE"));
|
|
||||||
|
|
||||||
private static readonly bool WatchPoolHeaders = IsEnabled("SHARPEMU_WATCH_POOL_HEADER");
|
|
||||||
|
|
||||||
private static readonly ulong[] PoolSlots = new ulong[64];
|
|
||||||
private static int _poolSlotCount;
|
|
||||||
|
|
||||||
private static readonly bool WatchValuePattern = IsEnabled("SHARPEMU_WATCH_VALUE_PATTERN");
|
|
||||||
|
|
||||||
private static readonly bool WatchValue1 = IsEnabled("SHARPEMU_WATCH_VALUE1");
|
|
||||||
|
|
||||||
private const ulong DirectBandLow = 0x100_0000_0000;
|
|
||||||
private const ulong DirectBandHigh = 0x1000_0000_0000;
|
|
||||||
private static int _value1Reports;
|
|
||||||
|
|
||||||
private static readonly bool WatchBulkTorn = IsEnabled("SHARPEMU_WATCH_BULK_TORN");
|
|
||||||
|
|
||||||
private static readonly ulong BulkDestHigh = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_BULK_DEST_HI"));
|
|
||||||
private static int _bulkTornReports;
|
|
||||||
private static int _bulkShiftReports;
|
|
||||||
|
|
||||||
public static bool Armed =>
|
|
||||||
WatchBase != 0 || WatchPoolHeaders || WatchValuePattern || WatchValue1 || WatchBulkTorn;
|
|
||||||
|
|
||||||
public static void OnDirectMapping(ulong mappedAddress, ulong length, int protection)
|
|
||||||
{
|
|
||||||
if (!WatchPoolHeaders || !IsPoolMapping(length, protection))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = Interlocked.Increment(ref _poolSlotCount) - 1;
|
|
||||||
if (index < PoolSlots.Length)
|
|
||||||
{
|
|
||||||
Volatile.Write(ref PoolSlots[index], mappedAddress + 0x40);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write armed on pool header slot 0x{mappedAddress + 0x40:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Check(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
if (WatchBulkTorn &&
|
|
||||||
data.Length >= 8 &&
|
|
||||||
(BulkDestHigh != 0
|
|
||||||
? (address >> 32) == BulkDestHigh
|
|
||||||
: address >= DirectBandLow && address < DirectBandHigh))
|
|
||||||
{
|
|
||||||
for (var offset = FirstAlignedOffset(address); offset + 8 <= data.Length; offset += 8)
|
|
||||||
{
|
|
||||||
var qword = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
|
|
||||||
var kind = ClassifyBulkValue(qword);
|
|
||||||
if (kind is not null && ReserveBulkReport(kind))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_bulk_torn HIT ({kind}) " +
|
|
||||||
$"dest=0x{address + (ulong)offset:X16} (base=0x{address:X16}+0x{offset:X}) " +
|
|
||||||
$"len={data.Length} qword=0x{qword:X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValue1 &&
|
|
||||||
address >= DirectBandLow && address < DirectBandHigh &&
|
|
||||||
data.Length is >= 1 and <= 8 &&
|
|
||||||
LittleEndianValue(data) == 1 &&
|
|
||||||
Interlocked.Increment(ref _value1Reports) <= 128)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValuePattern && data.Length == 8)
|
|
||||||
{
|
|
||||||
var value = BinaryPrimitives.ReadUInt64LittleEndian(data);
|
|
||||||
if ((value & 0xFFFFFFFF) == 1 && value >> 32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchBase != 0 && Overlaps(address, data.Length, WatchBase))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var slots = Math.Min(Volatile.Read(ref _poolSlotCount), PoolSlots.Length);
|
|
||||||
for (var i = 0; i < slots; i++)
|
|
||||||
{
|
|
||||||
var slot = Volatile.Read(ref PoolSlots[i]);
|
|
||||||
if (slot != 0 && Overlaps(address, data.Length, slot))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string? ClassifyBulkValue(ulong qword)
|
|
||||||
{
|
|
||||||
var low32 = qword & 0xFFFFFFFF;
|
|
||||||
var high32 = qword >> 32;
|
|
||||||
if (low32 == 1 && high32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
return "torn";
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefix = low32 & 0xFF00_0000;
|
|
||||||
var hasShiftedPointerPrefix = prefix is 0x0800_0000 or 0x8000_0000;
|
|
||||||
return high32 == 0 && hasShiftedPointerPrefix && (low32 & 0xFF) == 0
|
|
||||||
? "shift"
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static int FirstAlignedOffset(ulong address) =>
|
|
||||||
(int)((8 - (address & 7)) & 7);
|
|
||||||
|
|
||||||
internal static bool IsPoolMapping(ulong length, int protection) =>
|
|
||||||
length == 0x10000 && protection == 0xF2;
|
|
||||||
|
|
||||||
internal static bool Overlaps(ulong address, int length, ulong slot)
|
|
||||||
{
|
|
||||||
if (length <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeLength = (ulong)length - 1;
|
|
||||||
var writeEnd = address > ulong.MaxValue - writeLength
|
|
||||||
? ulong.MaxValue
|
|
||||||
: address + writeLength;
|
|
||||||
var slotEnd = slot > ulong.MaxValue - (WatchBytes - 1)
|
|
||||||
? ulong.MaxValue
|
|
||||||
: slot + WatchBytes - 1;
|
|
||||||
return address <= slotEnd && slot <= writeEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong LittleEndianValue(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
ulong value = 0;
|
|
||||||
for (var i = 0; i < data.Length; i++)
|
|
||||||
{
|
|
||||||
value |= (ulong)data[i] << (i * 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Report(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write HIT addr=0x{address:X16} len={data.Length} " +
|
|
||||||
$"first_qword=0x{LittleEndianValue(data):X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEnabled(string name) =>
|
|
||||||
string.Equals(Environment.GetEnvironmentVariable(name), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static bool ReserveBulkReport(string kind) =>
|
|
||||||
kind == "torn"
|
|
||||||
? Interlocked.Increment(ref _bulkTornReports) <= MaxBulkReports
|
|
||||||
: Interlocked.Increment(ref _bulkShiftReports) <= MaxBulkReports;
|
|
||||||
|
|
||||||
internal static ulong Parse(string? text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.Trim();
|
|
||||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
text = text[2..];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
|
|
||||||
? value
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,4 @@ public interface ICpuMemory
|
|||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
|
|
||||||
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,6 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
|||||||
{
|
{
|
||||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Backs an entire fixed-address range, matching the guest's
|
|
||||||
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
|
|
||||||
/// reserves the range in one all-or-nothing host call, this walks the range and
|
|
||||||
/// fills only the sub-ranges that are not already backed. That keeps a fixed
|
|
||||||
/// mapping whole when part of the requested window is already occupied — the
|
|
||||||
/// partial-overlap case where the single-call reservation fails outright and
|
|
||||||
/// leaves the remainder unmapped for the guest to fault into.
|
|
||||||
/// </summary>
|
|
||||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
|
||||||
|
|
||||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
|||||||
+133
-620
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using SharpEmu.Libs.Gpu;
|
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
@@ -27,8 +26,8 @@ internal static class AgcShaderCompilerHooks
|
|||||||
internal static void Install()
|
internal static void Install()
|
||||||
{
|
{
|
||||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||||
KernelMemoryCompatExports.TryReadShaderGuestMemory;
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||||
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||||
GuestDataPool.Shared;
|
VulkanVideoPresenter.GuestDataPool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,164 +194,6 @@ internal static class GnmTiling
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetBlockElementDimensions(
|
|
||||||
uint swizzleMode,
|
|
||||||
int bytesPerElement,
|
|
||||||
out int blockWidth,
|
|
||||||
out int blockHeight)
|
|
||||||
{
|
|
||||||
blockWidth = 0;
|
|
||||||
blockHeight = 0;
|
|
||||||
if (bytesPerElement <= 0 ||
|
|
||||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
|
||||||
if (bppLog2 < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
(blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
|
||||||
return blockWidth != 0 && blockHeight != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
|
|
||||||
/// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetBaseMipPlacement(
|
|
||||||
uint swizzleMode,
|
|
||||||
int elementsWide,
|
|
||||||
int elementsHigh,
|
|
||||||
int bytesPerElement,
|
|
||||||
uint resourceMipLevels,
|
|
||||||
out ulong byteOffset,
|
|
||||||
out bool inMipTail,
|
|
||||||
out int tailElementX,
|
|
||||||
out int tailElementY,
|
|
||||||
out ulong chainSliceBytes)
|
|
||||||
{
|
|
||||||
byteOffset = 0;
|
|
||||||
inMipTail = false;
|
|
||||||
tailElementX = 0;
|
|
||||||
tailElementY = 0;
|
|
||||||
chainSliceBytes = 0;
|
|
||||||
if (resourceMipLevels <= 1 ||
|
|
||||||
!ShouldDetile(swizzleMode) ||
|
|
||||||
elementsWide <= 0 ||
|
|
||||||
elementsHigh <= 0 ||
|
|
||||||
bytesPerElement <= 0 ||
|
|
||||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
|
||||||
if (bppLog2 < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
|
||||||
var blockSizeLog2 = BitLog2((uint)blockBytes);
|
|
||||||
if (blockWidth == 0 || blockHeight == 0 || blockSizeLog2 < 8)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mipLevels = (int)Math.Min(resourceMipLevels, 16u);
|
|
||||||
var maxMipsInTail = blockSizeLog2 <= 8 ? 0
|
|
||||||
: blockSizeLog2 <= 11
|
|
||||||
? 1 + (1 << (blockSizeLog2 - 9))
|
|
||||||
: blockSizeLog2 - 4;
|
|
||||||
var tailWidth = (blockSizeLog2 & 1) != 0 ? blockWidth >> 1 : blockWidth;
|
|
||||||
var tailHeight = (blockSizeLog2 & 1) != 0 ? blockHeight : blockHeight >> 1;
|
|
||||||
|
|
||||||
var firstMipInTail = mipLevels;
|
|
||||||
var mipSizes = new ulong[mipLevels];
|
|
||||||
for (var i = 0; i < mipLevels; i++)
|
|
||||||
{
|
|
||||||
var mipWidth = Math.Max(elementsWide >> i, 1);
|
|
||||||
var mipHeight = Math.Max(elementsHigh >> i, 1);
|
|
||||||
if (maxMipsInTail > 0 &&
|
|
||||||
mipWidth <= tailWidth &&
|
|
||||||
mipHeight <= tailHeight &&
|
|
||||||
mipLevels - i <= maxMipsInTail)
|
|
||||||
{
|
|
||||||
firstMipInTail = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alignedWidth = (ulong)(mipWidth + blockWidth - 1) / (ulong)blockWidth * (ulong)blockWidth;
|
|
||||||
var alignedHeight = (ulong)(mipHeight + blockHeight - 1) / (ulong)blockHeight * (ulong)blockHeight;
|
|
||||||
mipSizes[i] = alignedWidth * alignedHeight * (ulong)bytesPerElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstMipInTail == 0)
|
|
||||||
{
|
|
||||||
var m = maxMipsInTail - 1;
|
|
||||||
var mipOffset = m > 6 ? 16 << m : m << 8;
|
|
||||||
var mipX = ((mipOffset >> 9) & 1) |
|
|
||||||
((mipOffset >> 10) & 2) |
|
|
||||||
((mipOffset >> 11) & 4) |
|
|
||||||
((mipOffset >> 12) & 8) |
|
|
||||||
((mipOffset >> 13) & 16) |
|
|
||||||
((mipOffset >> 14) & 32);
|
|
||||||
var mipY = ((mipOffset >> 8) & 1) |
|
|
||||||
((mipOffset >> 9) & 2) |
|
|
||||||
((mipOffset >> 10) & 4) |
|
|
||||||
((mipOffset >> 11) & 8) |
|
|
||||||
((mipOffset >> 12) & 16) |
|
|
||||||
((mipOffset >> 13) & 32);
|
|
||||||
if ((blockSizeLog2 & 1) != 0)
|
|
||||||
{
|
|
||||||
(mipX, mipY) = (mipY, mipX);
|
|
||||||
if ((bppLog2 & 1) != 0)
|
|
||||||
{
|
|
||||||
mipY = (mipY << 1) | (mipX & 1);
|
|
||||||
mipX >>= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (microWidth, microHeight) = SquareBlockDimensions(256 >> bppLog2);
|
|
||||||
if (microWidth == 0 || microHeight == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
tailElementX = mipX * microWidth;
|
|
||||||
tailElementY = mipY * microHeight;
|
|
||||||
if (tailElementX + elementsWide > blockWidth ||
|
|
||||||
tailElementY + elementsHigh > blockHeight)
|
|
||||||
{
|
|
||||||
tailElementX = 0;
|
|
||||||
tailElementY = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inMipTail = true;
|
|
||||||
chainSliceBytes = (ulong)blockBytes;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
byteOffset = firstMipInTail < mipLevels ? (ulong)blockBytes : 0;
|
|
||||||
chainSliceBytes = byteOffset;
|
|
||||||
for (var i = firstMipInTail - 1; i >= 1; i--)
|
|
||||||
{
|
|
||||||
byteOffset += mipSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < firstMipInTail; i++)
|
|
||||||
{
|
|
||||||
chainSliceBytes += mipSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||||
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||||
|
|||||||
@@ -37,26 +37,10 @@ internal static class GpuWaitRegistry
|
|||||||
public long RegisteredTicks;
|
public long RegisteredTicks;
|
||||||
public bool StaleReported;
|
public bool StaleReported;
|
||||||
public object? State;
|
public object? State;
|
||||||
// Latched by LatchSatisfiedByValue when a producer wrote a value that
|
|
||||||
// satisfies this waiter. The label is frequently reused (reset to 0 for
|
|
||||||
// the next frame) immediately after the producing write, so re-reading
|
|
||||||
// guest memory at wake time can miss the transient satisfied window.
|
|
||||||
// Latching records satisfaction at the moment of the write instead.
|
|
||||||
public bool Latched;
|
|
||||||
// Non-zero for indirect-dispatch dimension retries: a bounded deadline
|
|
||||||
// (Stopwatch ticks) after which the waiter is resumed even if unsatisfied,
|
|
||||||
// so a legitimately empty indirect dispatch can never stall forever.
|
|
||||||
public long RetryDeadlineTicks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||||
// The last value each label producer wrote. Used only by the deadlock
|
|
||||||
// breaker: our serial submission parser cannot model two GPU queues running
|
|
||||||
// concurrently, so a label written -> reset -> re-waited across queues can
|
|
||||||
// cycle forever even though a real producer did signal it. Keyed by (memory,
|
|
||||||
// address) so distinct guest processes never alias.
|
|
||||||
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
|
|
||||||
|
|
||||||
public static int Count
|
public static int Count
|
||||||
{
|
{
|
||||||
@@ -130,14 +114,8 @@ internal static class GpuWaitRegistry
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var satisfied = list[i].Latched;
|
var value = readValue(address, list[i].Is64Bit);
|
||||||
if (!satisfied)
|
if (value is null || !Compare(list[i], value.Value))
|
||||||
{
|
|
||||||
var value = readValue(address, list[i].Is64Bit);
|
|
||||||
satisfied = value is not null && Compare(list[i], value.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!satisfied)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -258,162 +236,6 @@ internal static class GpuWaitRegistry
|
|||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records satisfaction for every waiter at <paramref name="address"/> whose
|
|
||||||
/// condition is met by <paramref name="value"/> — the value a producer just
|
|
||||||
/// wrote to that label. Called from the ordered producer side effect so a
|
|
||||||
/// same-frame label reset cannot lose the wakeup. The waiters stay registered
|
|
||||||
/// (latched) and are drained by the next CollectSatisfied. Returns true when
|
|
||||||
/// at least one waiter latched, so the caller can trigger a wake pass.
|
|
||||||
/// </summary>
|
|
||||||
public static bool LatchSatisfiedByValue(object memory, ulong address, ulong value)
|
|
||||||
{
|
|
||||||
var latchedAny = false;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_waiters.TryGetValue(address, out var list))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < list.Count; i++)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (waiter.Latched ||
|
|
||||||
!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
!Compare(waiter, value))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
waiter.Latched = true;
|
|
||||||
list[i] = waiter;
|
|
||||||
latchedAny = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return latchedAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes and returns waiters carrying a <see cref="WaitingDcb.RetryDeadlineTicks"/>
|
|
||||||
/// that has elapsed. Used for indirect-dispatch dimension retries: the caller
|
|
||||||
/// resumes them so a genuinely empty dispatch (dims that never become non-zero)
|
|
||||||
/// is dropped after a bounded wait instead of stalling the queue forever.
|
|
||||||
/// </summary>
|
|
||||||
public static List<WaitingDcb>? CollectExpiredRetries(object memory, long nowTicks)
|
|
||||||
{
|
|
||||||
List<WaitingDcb>? expired = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
List<ulong>? emptied = null;
|
|
||||||
foreach (var (address, list) in _waiters)
|
|
||||||
{
|
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (waiter.RetryDeadlineTicks == 0 ||
|
|
||||||
!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
nowTicks < waiter.RetryDeadlineTicks)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
expired ??= new List<WaitingDcb>();
|
|
||||||
expired.Add(waiter);
|
|
||||||
list.RemoveAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count == 0)
|
|
||||||
{
|
|
||||||
emptied ??= new List<ulong>();
|
|
||||||
emptied.Add(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptied is not null)
|
|
||||||
{
|
|
||||||
foreach (var address in emptied)
|
|
||||||
{
|
|
||||||
_waiters.Remove(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Records the value a label producer wrote, for the deadlock
|
|
||||||
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
|
||||||
public static bool RecordProduced(object memory, ulong address, ulong value)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_lastProduced.Count >= 8192)
|
|
||||||
{
|
|
||||||
_lastProduced.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastProduced[(memory, address)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return LatchSatisfiedByValue(memory, address, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Breaks cross-queue GPU deadlocks the serial parser cannot avoid: returns
|
|
||||||
/// (and removes) waiters that have been stuck longer than
|
|
||||||
/// <paramref name="minAgeTicks"/> and whose condition is satisfied by the
|
|
||||||
/// last value a real producer wrote to their label — even though guest
|
|
||||||
/// memory has since been reset. Never fabricates a value: a waiter is only
|
|
||||||
/// released when an actual producer signalled it at least once.
|
|
||||||
/// </summary>
|
|
||||||
public static List<WaitingDcb>? CollectDeadlockBroken(
|
|
||||||
object memory,
|
|
||||||
long nowTicks,
|
|
||||||
long minAgeTicks)
|
|
||||||
{
|
|
||||||
List<WaitingDcb>? broken = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
List<ulong>? emptied = null;
|
|
||||||
foreach (var (address, list) in _waiters)
|
|
||||||
{
|
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
nowTicks - waiter.RegisteredTicks < minAgeTicks ||
|
|
||||||
!_lastProduced.TryGetValue((memory, address), out var produced) ||
|
|
||||||
!Compare(waiter, produced))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
broken ??= new List<WaitingDcb>();
|
|
||||||
broken.Add(waiter);
|
|
||||||
list.RemoveAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count == 0)
|
|
||||||
{
|
|
||||||
emptied ??= new List<ulong>();
|
|
||||||
emptied.Add(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptied is not null)
|
|
||||||
{
|
|
||||||
foreach (var address in emptied)
|
|
||||||
{
|
|
||||||
_waiters.Remove(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||||
{
|
{
|
||||||
var masked = value & waiter.Mask;
|
var masked = value & waiter.Mask;
|
||||||
@@ -438,7 +260,6 @@ internal static class GpuWaitRegistry
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_waiters.Clear();
|
_waiters.Clear();
|
||||||
_lastProduced.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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;
|
||||||
|
|
||||||
@@ -44,17 +43,17 @@ public static class AmprExports
|
|||||||
{
|
{
|
||||||
public CachedHostFile(string path)
|
public CachedHostFile(string path)
|
||||||
{
|
{
|
||||||
Handle = File.OpenHandle(
|
Stream = new FileStream(
|
||||||
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 SafeFileHandle Handle { get; }
|
public object Gate { get; } = new();
|
||||||
public long Length { get; }
|
public FileStream Stream { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -736,7 +735,13 @@ public static class AmprExports
|
|||||||
return openResult;
|
return openResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileOffset >= (ulong)cachedFile.Length)
|
long fileLength;
|
||||||
|
lock (cachedFile.Gate)
|
||||||
|
{
|
||||||
|
fileLength = cachedFile.Stream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileOffset >= (ulong)fileLength)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -755,10 +760,12 @@ public static class AmprExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
var read = RandomAccess.Read(
|
int read;
|
||||||
cachedFile.Handle,
|
lock (cachedFile.Gate)
|
||||||
buffer.AsSpan(0, request),
|
{
|
||||||
unchecked((long)absoluteOffset));
|
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
||||||
|
read = cachedFile.Stream.Read(buffer, 0, request);
|
||||||
|
}
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,33 +121,6 @@ public static class AppContentExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download data is not emulated as a real quota; report a comfortable
|
|
||||||
// fixed amount of free space so titles never take the "storage full" path.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Gl6w5i0JokY",
|
|
||||||
ExportName = "sceAppContentDownloadDataGetAvailableSpaceKb",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAppContent")]
|
|
||||||
public static int AppContentDownloadDataGetAvailableSpaceKb(CpuContext ctx)
|
|
||||||
{
|
|
||||||
const ulong availableSpaceKb = 1024UL * 1024UL; // 1 GiB
|
|
||||||
var availableSpaceAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (availableSpaceAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> spaceBytes = stackalloc byte[sizeof(ulong)];
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(spaceBytes, availableSpaceKb);
|
|
||||||
if (!ctx.Memory.TryWrite(availableSpaceAddress, spaceBytes))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
||||||
{
|
{
|
||||||
value = 0;
|
value = 0;
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ public static class AudioOutExports
|
|||||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||||
private static int _nextPortHandle;
|
private static int _nextPortHandle;
|
||||||
|
|
||||||
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
|
||||||
// guest submits real samples or silence. Gated so it costs nothing when off.
|
|
||||||
private static readonly bool _traceOutput = string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT"), "1", StringComparison.Ordinal);
|
|
||||||
private static long _outputCount;
|
|
||||||
|
|
||||||
private sealed class PortState : IDisposable
|
private sealed class PortState : IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _paceGate = new();
|
private readonly object _paceGate = new();
|
||||||
@@ -161,37 +155,6 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "GrQ9s4IrNaQ",
|
|
||||||
ExportName = "sceAudioOutGetPortState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutGetPortState(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (stateAddress == 0 || !Ports.TryGetValue(handle, out var port))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SceAudioOutPortState: report a connected primary output at full volume
|
|
||||||
// so pacing/mixing code sees a live port. We do no host rerouting, so
|
|
||||||
// rerouteCounter and flag stay zero.
|
|
||||||
Span<byte> state = stackalloc byte[16];
|
|
||||||
state.Clear();
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(state, 1);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(
|
|
||||||
state[2..], (ushort)port.Channels);
|
|
||||||
state[7] = 127;
|
|
||||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QOQtbeDqsT4",
|
Nid = "QOQtbeDqsT4",
|
||||||
ExportName = "sceAudioOutOutput",
|
ExportName = "sceAudioOutOutput",
|
||||||
@@ -203,12 +166,7 @@ public static class AudioOutExports
|
|||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!Ports.TryGetValue(handle, out var port))
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
{
|
{
|
||||||
// Host shutdown disposes the ports while guest audio threads are
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
// still draining their last buffers; report success so the guest
|
|
||||||
// winds down without a per-buffer error (and its WARN log flood).
|
|
||||||
return ctx.SetReturn(_shutdown
|
|
||||||
? 0
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceAddress == 0)
|
if (sourceAddress == 0)
|
||||||
@@ -225,17 +183,6 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceOutput)
|
|
||||||
{
|
|
||||||
var n = Interlocked.Increment(ref _outputCount);
|
|
||||||
if (n <= 8 || n % 200 == 0)
|
|
||||||
{
|
|
||||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port.Backend is null)
|
if (port.Backend is null)
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
@@ -319,40 +266,8 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peak normalized amplitude [0,1] of an interleaved PCM buffer, used only by
|
|
||||||
// the SHARPEMU_LOG_AUDIO_OUT diagnostic to distinguish real audio from silence.
|
|
||||||
private static float PeakAmplitude(ReadOnlySpan<byte> source, bool isFloat, int bytesPerSample)
|
|
||||||
{
|
|
||||||
var peak = 0f;
|
|
||||||
if (isFloat && bytesPerSample == 4)
|
|
||||||
{
|
|
||||||
for (var i = 0; i + 4 <= source.Length; i += 4)
|
|
||||||
{
|
|
||||||
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(source.Slice(i, 4)));
|
|
||||||
if (v > peak)
|
|
||||||
{
|
|
||||||
peak = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (bytesPerSample == 2)
|
|
||||||
{
|
|
||||||
for (var i = 0; i + 2 <= source.Length; i += 2)
|
|
||||||
{
|
|
||||||
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(source.Slice(i, 2)) / 32768f);
|
|
||||||
if (v > peak)
|
|
||||||
{
|
|
||||||
peak = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ShutdownAllPorts()
|
public static void ShutdownAllPorts()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _shutdown, true);
|
|
||||||
foreach (var handle in Ports.Keys)
|
foreach (var handle in Ports.Keys)
|
||||||
{
|
{
|
||||||
if (Ports.TryRemove(handle, out var port))
|
if (Ports.TryRemove(handle, out var port))
|
||||||
@@ -362,8 +277,6 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool _shutdown;
|
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
private static bool TryGetFormat(
|
||||||
int rawFormat,
|
int rawFormat,
|
||||||
out int channels,
|
out int channels,
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
|
||||||
|
|
||||||
// PS5 acoustic-propagation (3D-audio ray/portal/room) module. We do not model
|
|
||||||
// acoustic propagation; the geometry-driven reverb/occlusion it produces is a
|
|
||||||
// quality feature, not a correctness gate. Games (e.g. Astro Bot) call it
|
|
||||||
// during audio init and hard-assert if any entry point is missing:
|
|
||||||
// ASSERT ... sceAudioPropagationSystemQueryMemory failed : 0x80020002
|
|
||||||
// The API is placement-style: QueryMemory reports a buffer size, the game
|
|
||||||
// allocates it, and the "system"/objects live inside that caller-owned buffer,
|
|
||||||
// so success-returning stubs let init proceed without us owning any state.
|
|
||||||
public static class AudioPropagationExports
|
|
||||||
{
|
|
||||||
private const int Ok = 0;
|
|
||||||
|
|
||||||
// QueryMemory reports the working-set size the caller must allocate before
|
|
||||||
// SystemCreate. rsi points at the out size/alignment; write a modest,
|
|
||||||
// aligned block so the caller's allocation succeeds.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7xyAxrusLko",
|
|
||||||
ExportName = "sceAudioPropagationSystemQueryMemory",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemQueryMemory(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
// {size, alignment} — 1 MiB / 256 B covers the caller's allocation.
|
|
||||||
ctx.TryWriteUInt64(outAddress, 0x10_0000);
|
|
||||||
ctx.TryWriteUInt64(outAddress + sizeof(ulong), 0x100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "GrA9ke1QT+E", ExportName = "sceAudioPropagationSystemQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "aNEqtSHdUSo", ExportName = "sceAudioPropagationSystemCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "x5VPqg5iyAk", ExportName = "sceAudioPropagationSystemDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ile38Gl-p5M", ExportName = "sceAudioPropagationSystem", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int System(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "cMl3u+7QBBM", ExportName = "sceAudioPropagationSystemMemoryInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemMemoryInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "3B9IabLByyM", ExportName = "sceAudioPropagationSystemOptionInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemOptionInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "B2KI2AachWE", ExportName = "sceAudioPropagationSystemLock", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemLock(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "kIdb+iQUzCs", ExportName = "sceAudioPropagationSystemSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "VlBT16890mA", ExportName = "sceAudioPropagationSystemSetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemSetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ht-QXT3zGxo", ExportName = "sceAudioPropagationSystemGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "CPLV6G-eXmk", ExportName = "sceAudioPropagationSystemRegisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemRegisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "XKCN4gpeYsM", ExportName = "sceAudioPropagationSystemUnregisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemUnregisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "8bI5h8req30", ExportName = "sceAudioPropagationRoomCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RoomCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "S0JwP2AFTTE", ExportName = "sceAudioPropagationRoomDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RoomDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "b-dYXrjSNZU", ExportName = "sceAudioPropagationPortalCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ZQXE-xS6MTE", ExportName = "sceAudioPropagationPortalDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "WXMhENV2NcA", ExportName = "sceAudioPropagationPortalSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "i687TNRF+hw", ExportName = "sceAudioPropagationPortalSettingsInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalSettingsInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "d84otraxt2s", ExportName = "sceAudioPropagationSourceCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "wkseM3LWPuc", ExportName = "sceAudioPropagationSourceDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "-wsUTr31yeg", ExportName = "sceAudioPropagationSourceSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "PBcrVpEqUVY", ExportName = "sceAudioPropagationSourceCalculateAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceCalculateAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "eEeKqFeNI3o", ExportName = "sceAudioPropagationSourceGetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "G+QLTfyLMYk", ExportName = "sceAudioPropagationSourceGetAudioPathCount", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetAudioPathCount(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "aKJZx7wCma8", ExportName = "sceAudioPropagationSourceGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "3aEY9tPXGKc", ExportName = "sceAudioPropagationSourceQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "hhz9pITnC8k", ExportName = "sceAudioPropagationSourceRender", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceRender(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "SoKPzY1-3SU", ExportName = "sceAudioPropagationSourceRenderInfoInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceRenderInfoInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "tKSmk2JsMAA", ExportName = "sceAudioPropagationSourceSetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "5vzOS2pHMFc", ExportName = "sceAudioPropagationSourceSetAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "MNmGapXrYRs", ExportName = "sceAudioPropagationSourceSetAudioPathsParamInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPathsParamInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "i-0aUex3zCE", ExportName = "sceAudioPropagationAudioPathInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int AudioPathInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "JZIkSbmt2BE", ExportName = "sceAudioPropagationAudioPathPointInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int AudioPathPointInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "tL2AEPejVQE", ExportName = "sceAudioPropagationPathGetNumPoints", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PathGetNumPoints(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "2BSFmuKtRss", ExportName = "sceAudioPropagationMaterialInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int MaterialInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "0r2+9UTg1BA", ExportName = "sceAudioPropagationRayInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RayInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "BbOT4vBwAjs", ExportName = "sceAudioPropagationResetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int ResetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "gCmQm6dvMxw", ExportName = "sceAudioPropagationReportApi", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int ReportApi(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,7 @@ public static class AvPlayerExports
|
|||||||
private const int FrameBufferCount = 3;
|
private const int FrameBufferCount = 3;
|
||||||
private const int FrameInfoSize = 40;
|
private const int FrameInfoSize = 40;
|
||||||
private const int FrameInfoExSize = 104;
|
private const int FrameInfoExSize = 104;
|
||||||
// This structure is 32 bytes. A larger write can damage the guest stack.
|
private const int StreamInfoSize = 40;
|
||||||
private const int StreamInfoSize = 32;
|
|
||||||
private const int StreamInfoExSize = 32;
|
|
||||||
private const int MaxGuestPathLength = 4096;
|
private const int MaxGuestPathLength = 4096;
|
||||||
private static readonly object StateGate = new();
|
private static readonly object StateGate = new();
|
||||||
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
||||||
@@ -406,8 +404,7 @@ public static class AvPlayerExports
|
|||||||
ExportName = "sceAvPlayerGetStreamInfoEx",
|
ExportName = "sceAvPlayerGetStreamInfoEx",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
|
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
|
||||||
GetStreamInfoCore(ctx, StreamInfoExSize);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XC9wM+xULz8",
|
Nid = "XC9wM+xULz8",
|
||||||
@@ -564,48 +561,12 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RegisterPlayerForTest(
|
|
||||||
ulong handle,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
ulong durationMilliseconds)
|
|
||||||
{
|
|
||||||
PlayerState? previous;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out previous);
|
|
||||||
Players[handle] = new PlayerState
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
DurationMilliseconds = durationMilliseconds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
previous?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemovePlayerForTest(ulong handle)
|
|
||||||
{
|
|
||||||
PlayerState? player;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out player);
|
|
||||||
}
|
|
||||||
|
|
||||||
player?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "d8FcbzfAdQw",
|
Nid = "d8FcbzfAdQw",
|
||||||
ExportName = "sceAvPlayerGetStreamInfo",
|
ExportName = "sceAvPlayerGetStreamInfo",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
public static int AvPlayerGetStreamInfo(CpuContext ctx)
|
||||||
GetStreamInfoCore(ctx, StreamInfoSize);
|
|
||||||
|
|
||||||
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
|
|
||||||
{
|
{
|
||||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
@@ -617,7 +578,7 @@ public static class AvPlayerExports
|
|||||||
return SetReturn(ctx, InvalidParameters);
|
return SetReturn(ctx, InvalidParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[infoSize];
|
Span<byte> info = stackalloc byte[StreamInfoSize];
|
||||||
info.Clear();
|
info.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
||||||
if (streamIndex == 0)
|
if (streamIndex == 0)
|
||||||
@@ -1048,7 +1009,7 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
|
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
|
||||||
if (!File.Exists(ffprobe))
|
if (!File.Exists(ffprobe))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1131,33 +1092,13 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FindFfmpeg() =>
|
private static string? FindFfmpeg()
|
||||||
FindFfmpeg(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
|
||||||
Environment.GetEnvironmentVariable("PATH"),
|
|
||||||
OperatingSystem.IsWindows());
|
|
||||||
|
|
||||||
internal static string? FindFfmpeg(
|
|
||||||
string? configured,
|
|
||||||
string? searchPath,
|
|
||||||
bool isWindows)
|
|
||||||
{
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
|
||||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||||
{
|
{
|
||||||
return configured;
|
return configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
|
||||||
foreach (var directory in (searchPath ?? string.Empty)
|
|
||||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
var candidate = Path.Combine(RemovePathQuotes(directory), executable);
|
|
||||||
if (File.Exists(candidate))
|
|
||||||
{
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
||||||
{
|
{
|
||||||
if (File.Exists(candidate))
|
if (File.Exists(candidate))
|
||||||
@@ -1168,16 +1109,6 @@ public static class AvPlayerExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
|
|
||||||
Path.Combine(
|
|
||||||
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
|
|
||||||
isWindows ? "ffprobe.exe" : "ffprobe");
|
|
||||||
|
|
||||||
private static string RemovePathQuotes(string directory) =>
|
|
||||||
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
|
|
||||||
? directory[1..^1]
|
|
||||||
: directory;
|
|
||||||
|
|
||||||
internal static string? ResolveGuestPath(string guestPath)
|
internal static string? ResolveGuestPath(string guestPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(guestPath))
|
if (string.IsNullOrWhiteSpace(guestPath))
|
||||||
@@ -1187,9 +1118,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
var normalized = guestPath.Replace('\\', '/');
|
var normalized = guestPath.Replace('\\', '/');
|
||||||
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
||||||
var unrealProjectRelative =
|
var unrealProjectRelative = false;
|
||||||
normalized.StartsWith("../", StringComparison.Ordinal) ||
|
|
||||||
normalized.StartsWith("./", StringComparison.Ordinal);
|
|
||||||
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
||||||
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
||||||
uri.IsFile)
|
uri.IsFile)
|
||||||
@@ -1220,10 +1149,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
if (unrealProjectRelative)
|
if (unrealProjectRelative)
|
||||||
{
|
{
|
||||||
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
|
normalized = RemoveUnrealLeadingDotSegments(normalized);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
@@ -1307,20 +1233,15 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryRemoveUnrealLeadingDotSegments(
|
private static string RemoveUnrealLeadingDotSegments(string guestPath)
|
||||||
string guestPath,
|
|
||||||
out string normalized)
|
|
||||||
{
|
{
|
||||||
var removedParent = false;
|
|
||||||
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
||||||
guestPath.StartsWith("./", StringComparison.Ordinal))
|
guestPath.StartsWith("./", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
|
|
||||||
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized = guestPath;
|
return guestPath;
|
||||||
return !removedParent || guestPath.Contains('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ internal static class Bink2MovieBridge
|
|||||||
private static bool _availabilityReported;
|
private static bool _availabilityReported;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true only when movie skipping was explicitly requested. Without
|
/// Returns true when the guest should receive a normal "file not found"
|
||||||
/// a host adapter the guest must be allowed to run the Bink implementation
|
/// result for a Bink movie. This is the safe default without a decoder:
|
||||||
/// statically linked into its executable.
|
/// games that treat movies as optional fall through to their next state
|
||||||
|
/// 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) &&
|
||||||
@@ -52,18 +53,12 @@ internal static class Bink2MovieBridge
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mode = ResolveMode();
|
if (ResolveMode() == MovieMode.Dummy)
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -170,15 +165,16 @@ internal static class Bink2MovieBridge
|
|||||||
return MovieMode.Skip;
|
return MovieMode.Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the optional host adapter when one is supplied. Otherwise let
|
// With no SDK adapter present, returning "not found" makes optional
|
||||||
// the game's statically linked Bink implementation consume the file.
|
// cinematics advance. Supplying either an explicit path or the normal
|
||||||
|
// side-by-side adapter enables native playback automatically.
|
||||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||||
EnumerateAdapterCandidates().Any(File.Exists))
|
EnumerateAdapterCandidates().Any(File.Exists))
|
||||||
{
|
{
|
||||||
return MovieMode.Native;
|
return MovieMode.Native;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MovieMode.Guest;
|
return MovieMode.Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AttachDummyMovieLocked(string hostPath)
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
@@ -339,7 +335,6 @@ internal static class Bink2MovieBridge
|
|||||||
|
|
||||||
private enum MovieMode
|
private enum MovieMode
|
||||||
{
|
{
|
||||||
Guest,
|
|
||||||
Skip,
|
Skip,
|
||||||
Dummy,
|
Dummy,
|
||||||
Native,
|
Native,
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The pool backing AGC-to-presenter ownership transfers, shared by every backend
|
|
||||||
/// (the AGC layer rents, the presenter returns, so both sides must use one pool).
|
|
||||||
/// Guest draw snapshots churn through a small set of 128 KiB-16 MiB size classes
|
|
||||||
/// thousands of times per second; the process-wide shared pool trims and
|
|
||||||
/// repartitions those large arrays aggressively under GC load, causing hundreds of
|
|
||||||
/// MiB/s of replacement byte[] allocations, so this pool is bounded and non-shared.
|
|
||||||
/// </summary>
|
|
||||||
internal static class GuestDataPool
|
|
||||||
{
|
|
||||||
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
|
|
||||||
maxArrayLength: 16 * 1024 * 1024,
|
|
||||||
maxCachedBytes: 256UL * 1024 * 1024,
|
|
||||||
maxArraysPerBucket: 8);
|
|
||||||
|
|
||||||
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
|
|
||||||
|
|
||||||
private sealed class BoundedByteArrayPool : ArrayPool<byte>
|
|
||||||
{
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private readonly int _maxArrayLength;
|
|
||||||
private readonly ulong _maxCachedBytes;
|
|
||||||
private readonly int _maxArraysPerBucket;
|
|
||||||
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
|
|
||||||
private readonly HashSet<byte[]> _leases =
|
|
||||||
new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
|
|
||||||
private ulong _cachedBytes;
|
|
||||||
|
|
||||||
public BoundedByteArrayPool(
|
|
||||||
int maxArrayLength,
|
|
||||||
ulong maxCachedBytes,
|
|
||||||
int maxArraysPerBucket)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength);
|
|
||||||
ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes);
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket);
|
|
||||||
_maxArrayLength = maxArrayLength;
|
|
||||||
_maxCachedBytes = maxCachedBytes;
|
|
||||||
_maxArraysPerBucket = maxArraysPerBucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override byte[] Rent(int minimumLength)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(minimumLength);
|
|
||||||
var length = GetAllocationLength(minimumLength);
|
|
||||||
byte[]? array = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (length <= _maxArrayLength &&
|
|
||||||
_cachedByBucket.TryGetValue(length, out var bucket) &&
|
|
||||||
bucket.TryPop(out array))
|
|
||||||
{
|
|
||||||
_cachedBytes -= (ulong)array.LongLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
array ??= new byte[length];
|
|
||||||
_leases.Add(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Return(byte[] array, bool clearArray = false)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_leases.Remove(array))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearArray)
|
|
||||||
{
|
|
||||||
Array.Clear(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (array.Length > _maxArrayLength ||
|
|
||||||
!IsBucketLength(array.Length) ||
|
|
||||||
(ulong)array.LongLength > _maxCachedBytes -
|
|
||||||
Math.Min(_cachedBytes, _maxCachedBytes))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_cachedByBucket.TryGetValue(array.Length, out var bucket))
|
|
||||||
{
|
|
||||||
bucket = new Stack<byte[]>();
|
|
||||||
_cachedByBucket.Add(array.Length, bucket);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bucket.Count >= _maxArraysPerBucket)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bucket.Push(array);
|
|
||||||
_cachedBytes += (ulong)array.LongLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Trim()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_cachedByBucket.Clear();
|
|
||||||
_cachedBytes = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetAllocationLength(int minimumLength)
|
|
||||||
{
|
|
||||||
if (minimumLength <= 16)
|
|
||||||
{
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minimumLength > _maxArrayLength)
|
|
||||||
{
|
|
||||||
return minimumLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsBucketLength(int length) =>
|
|
||||||
length >= 16 && (length & (length - 1)) == 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Libs.Gpu.Metal;
|
|
||||||
using SharpEmu.Libs.Gpu.Vulkan;
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
@@ -9,39 +8,11 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
||||||
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
||||||
/// Vulkan is the default everywhere; SHARPEMU_GPU_BACKEND=metal opts into the Metal
|
/// Vulkan is the only backend today; Metal/DX12 slot in here.
|
||||||
/// backend (macOS only) while it is being brought up. macOS flips to Metal by default
|
|
||||||
/// once the presenter reaches parity.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class GuestGpu
|
internal static class GuestGpu
|
||||||
{
|
{
|
||||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(Create);
|
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||||
|
|
||||||
public static IGuestGpuBackend Current => Instance.Value;
|
public static IGuestGpuBackend Current => Instance.Value;
|
||||||
|
|
||||||
private static IGuestGpuBackend Create()
|
|
||||||
{
|
|
||||||
var requested = Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND");
|
|
||||||
if (string.IsNullOrEmpty(requested) || requested.Equals("vulkan", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requested.Equals("metal", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] SHARPEMU_GPU_BACKEND=metal is only available on macOS; using Vulkan.");
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] GPU backend: Metal (SHARPEMU_GPU_BACKEND).");
|
|
||||||
return new MetalGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] Unknown SHARPEMU_GPU_BACKEND value '{requested}'; using Vulkan.");
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ internal sealed record GuestDrawTexture(
|
|||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
GuestSampler Sampler = default,
|
GuestSampler Sampler = default);
|
||||||
// Guest CPU write-tracker generation of the memory RgbaPixels was read
|
|
||||||
// from; -1 when the range is untracked or the pixels were not read here.
|
|
||||||
long WriteGeneration = -1,
|
|
||||||
bool ArrayedView = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||||
internal readonly record struct GuestSampler(
|
internal readonly record struct GuestSampler(
|
||||||
@@ -41,22 +36,6 @@ internal readonly record struct GuestSampler(
|
|||||||
uint Word2,
|
uint Word2,
|
||||||
uint Word3);
|
uint Word3);
|
||||||
|
|
||||||
/// <summary>Identity of a texture's content in a backend texture cache, keyed
|
|
||||||
/// entirely on raw guest descriptor values; the AGC layer uses it to skip texel
|
|
||||||
/// copies for content the backend already holds.</summary>
|
|
||||||
internal readonly record struct TextureContentIdentity(
|
|
||||||
ulong Address,
|
|
||||||
uint Width,
|
|
||||||
uint Height,
|
|
||||||
uint Format,
|
|
||||||
uint NumberType,
|
|
||||||
uint DstSelect,
|
|
||||||
uint TileMode,
|
|
||||||
uint Pitch,
|
|
||||||
GuestSampler Sampler,
|
|
||||||
bool Arrayed = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
internal sealed record GuestMemoryBuffer(
|
internal sealed record GuestMemoryBuffer(
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
byte[] Data,
|
byte[] Data,
|
||||||
@@ -143,22 +122,12 @@ internal readonly record struct GuestBlendState(
|
|||||||
WriteMask: 0xFu);
|
WriteMask: 0xFu);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>CB_BLEND_RED..ALPHA: the constant color referenced by the
|
|
||||||
/// CONSTANT_COLOR / CONSTANT_ALPHA blend factors. One constant serves every
|
|
||||||
/// render target of a draw; the hardware reset value is transparent black.</summary>
|
|
||||||
internal readonly record struct GuestBlendConstant(
|
|
||||||
float Red,
|
|
||||||
float Green,
|
|
||||||
float Blue,
|
|
||||||
float Alpha);
|
|
||||||
|
|
||||||
internal sealed record GuestRenderState(
|
internal sealed record GuestRenderState(
|
||||||
IReadOnlyList<GuestBlendState> Blends,
|
IReadOnlyList<GuestBlendState> Blends,
|
||||||
GuestRect? Scissor,
|
GuestRect? Scissor,
|
||||||
GuestViewport? Viewport,
|
GuestViewport? Viewport,
|
||||||
GuestRasterState Raster,
|
GuestRasterState Raster,
|
||||||
GuestDepthState Depth,
|
GuestDepthState Depth)
|
||||||
GuestBlendConstant BlendConstant = default)
|
|
||||||
{
|
{
|
||||||
public static GuestRenderState Default { get; } = new(
|
public static GuestRenderState Default { get; } = new(
|
||||||
[GuestBlendState.Default],
|
[GuestBlendState.Default],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
@@ -18,10 +17,6 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal interface IGuestGpuBackend
|
internal interface IGuestGpuBackend
|
||||||
{
|
{
|
||||||
/// <summary>Human-readable name of this backend ("Metal", "Vulkan"), shown in
|
|
||||||
/// the window title on macOS where either backend can run.</summary>
|
|
||||||
string BackendName { get; }
|
|
||||||
|
|
||||||
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
||||||
void EnsureStarted(uint width, uint height);
|
void EnsureStarted(uint width, uint height);
|
||||||
|
|
||||||
@@ -193,70 +188,4 @@ internal interface IGuestGpuBackend
|
|||||||
/// the guest codes cross the seam and each backend maps them internally.
|
/// the guest codes cross the seam and each backend maps them internally.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
||||||
|
|
||||||
// Guest work ordering. AGC submissions execute on a single backend consumer in
|
|
||||||
// logical guest-queue order; sequences returned here are backend work tickets.
|
|
||||||
// A backend without a running presenter returns 0 from the Submit* methods and
|
|
||||||
// callers fall back to executing inline.
|
|
||||||
|
|
||||||
/// <summary>Scopes subsequent submissions on this thread to a named guest queue.</summary>
|
|
||||||
IDisposable EnterGuestQueue(string queueName, ulong submissionId);
|
|
||||||
|
|
||||||
/// <summary>Enqueues an action at its exact position in the current guest queue;
|
|
||||||
/// returns its work sequence, or 0 when nothing could be enqueued.</summary>
|
|
||||||
long SubmitOrderedGuestAction(Action action, string debugName);
|
|
||||||
|
|
||||||
/// <summary>Preserves sceAgcDcbWaitUntilSafeForRendering in queue order.</summary>
|
|
||||||
long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex);
|
|
||||||
|
|
||||||
/// <summary>Blocks until the given work sequence completes; false on timeout,
|
|
||||||
/// close, or a non-positive sequence.</summary>
|
|
||||||
bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite);
|
|
||||||
|
|
||||||
/// <summary>Sequence currently executing on the guest-work consumer; diagnostics only.</summary>
|
|
||||||
long CurrentGuestWorkSequenceForDiagnostics { get; }
|
|
||||||
|
|
||||||
// Guest image lifecycle beyond presentation: CPU-visible seeding, writes, and
|
|
||||||
// extent queries the AGC layer uses to keep guest memory and backend images
|
|
||||||
// coherent. Addresses and formats are always raw guest values.
|
|
||||||
|
|
||||||
/// <summary>Whether the image exists on the backend or an already-queued upload
|
|
||||||
/// owns its initialization (a pending image may skip a duplicate upload but is
|
|
||||||
/// not yet a valid flip source).</summary>
|
|
||||||
bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType);
|
|
||||||
|
|
||||||
/// <summary>True when the first draw into this address must seed the backend
|
|
||||||
/// image from guest memory (PS5 render targets alias guest memory, so
|
|
||||||
/// CPU-prefilled pixels are visible before the first draw).</summary>
|
|
||||||
bool GuestImageWantsInitialData(ulong address);
|
|
||||||
|
|
||||||
void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels);
|
|
||||||
|
|
||||||
void SubmitGuestImageFill(ulong address, uint fillValue);
|
|
||||||
|
|
||||||
void SubmitGuestImageWrite(ulong address, byte[] pixels);
|
|
||||||
|
|
||||||
bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount);
|
|
||||||
|
|
||||||
IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents();
|
|
||||||
|
|
||||||
/// <summary>Whether the backend's texture cache already holds this content; lets
|
|
||||||
/// the AGC layer skip copying texels out of guest memory on every draw.</summary>
|
|
||||||
bool IsTextureContentCached(in TextureContentIdentity identity);
|
|
||||||
|
|
||||||
/// <summary>Guest memory handle for backend self-healing (cache misses re-read
|
|
||||||
/// texels directly instead of showing a fallback pattern).</summary>
|
|
||||||
void AttachGuestMemory(ICpuMemory memory);
|
|
||||||
|
|
||||||
/// <summary>Alignment the AGC layer must apply to storage-buffer offsets before
|
|
||||||
/// they cross the seam.</summary>
|
|
||||||
ulong GuestStorageBufferOffsetAlignment { get; }
|
|
||||||
|
|
||||||
/// <summary>Counts a guest shader translation for the perf overlay.</summary>
|
|
||||||
void CountShaderCompilation();
|
|
||||||
|
|
||||||
(long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters();
|
|
||||||
|
|
||||||
/// <summary>Asks a running presenter to close its window.</summary>
|
|
||||||
void RequestClose();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Metal backend's compiled shader: MSL source plus the reflection data
|
|
||||||
/// (<see cref="Gen5MslShader"/>) the presenter needs to create and bind pipeline
|
|
||||||
/// states. The diagnostics payload is the source text — Metal has no portable
|
|
||||||
/// binary form until an MTLBinaryArchive is introduced.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class MetalCompiledGuestShader(Gen5MslShader shader) : IGuestCompiledShader
|
|
||||||
{
|
|
||||||
private byte[]? _payload;
|
|
||||||
|
|
||||||
public Gen5MslShader Shader { get; } = shader;
|
|
||||||
|
|
||||||
/// <summary>MTLLibrary handle cached by the presenter after the first
|
|
||||||
/// runtime compile; the render loop is its only reader and writer.</summary>
|
|
||||||
internal nint CachedLibrary;
|
|
||||||
|
|
||||||
public byte[] Payload => _payload ??= Encoding.UTF8.GetBytes(Shader.Source);
|
|
||||||
|
|
||||||
public string PayloadFileExtension => "msl";
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MTLPixelFormat raw values — only the formats the backend maps. Declared here
|
|
||||||
/// rather than pulled from a binding package: the Metal backend talks to the OS
|
|
||||||
/// exclusively through objc_msgSend, so ABI constants are owned locally.
|
|
||||||
/// </summary>
|
|
||||||
internal enum MtlPixelFormat : uint
|
|
||||||
{
|
|
||||||
Invalid = 0,
|
|
||||||
R8Unorm = 10,
|
|
||||||
R8Snorm = 12,
|
|
||||||
R8Uint = 13,
|
|
||||||
R8Sint = 14,
|
|
||||||
R16Unorm = 20,
|
|
||||||
R16Snorm = 22,
|
|
||||||
R16Uint = 23,
|
|
||||||
R16Sint = 24,
|
|
||||||
R16Float = 25,
|
|
||||||
Rg8Unorm = 30,
|
|
||||||
Rg8Snorm = 32,
|
|
||||||
Rg8Uint = 33,
|
|
||||||
Rg8Sint = 34,
|
|
||||||
B5G6R5Unorm = 40,
|
|
||||||
R32Uint = 53,
|
|
||||||
R32Sint = 54,
|
|
||||||
R32Float = 55,
|
|
||||||
Rg16Unorm = 60,
|
|
||||||
Rg16Uint = 63,
|
|
||||||
Rg16Sint = 64,
|
|
||||||
Rg16Float = 65,
|
|
||||||
Rgba8Unorm = 70,
|
|
||||||
Rgba8UnormSrgb = 71,
|
|
||||||
Rgba8Uint = 73,
|
|
||||||
Rgba8Sint = 74,
|
|
||||||
Bgra8Unorm = 80,
|
|
||||||
Bgra8UnormSrgb = 81,
|
|
||||||
Rgb10A2Unorm = 90,
|
|
||||||
Rg11B10Float = 92,
|
|
||||||
Rgb9E5Float = 93,
|
|
||||||
Bgr10A2Unorm = 94,
|
|
||||||
Rg32Uint = 103,
|
|
||||||
Rg32Sint = 104,
|
|
||||||
Rg32Float = 105,
|
|
||||||
Rgba16Unorm = 110,
|
|
||||||
Rgba16Uint = 113,
|
|
||||||
Rgba16Sint = 114,
|
|
||||||
Rgba16Float = 115,
|
|
||||||
Rgba32Uint = 123,
|
|
||||||
Rgba32Sint = 124,
|
|
||||||
Rgba32Float = 125,
|
|
||||||
Bc1Rgba = 130,
|
|
||||||
Bc1RgbaSrgb = 131,
|
|
||||||
Bc2Rgba = 132,
|
|
||||||
Bc2RgbaSrgb = 133,
|
|
||||||
Bc3Rgba = 134,
|
|
||||||
Bc3RgbaSrgb = 135,
|
|
||||||
Bc4RUnorm = 140,
|
|
||||||
Bc4RSnorm = 141,
|
|
||||||
Bc5RgUnorm = 142,
|
|
||||||
Bc5RgSnorm = 143,
|
|
||||||
Bc6HRgbFloat = 150,
|
|
||||||
Bc6HRgbUfloat = 151,
|
|
||||||
Bc7RgbaUnorm = 152,
|
|
||||||
Bc7RgbaUnormSrgb = 153,
|
|
||||||
Depth32Float = 252,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>A sampled-texture format: the Metal pixel format plus the byte
|
|
||||||
/// layout the upload path needs. <see cref="BlockBytes"/> is nonzero for
|
|
||||||
/// block-compressed formats (bytes per 4x4 block); otherwise
|
|
||||||
/// <see cref="BytesPerPixel"/> applies.</summary>
|
|
||||||
internal readonly record struct MetalTextureFormat(
|
|
||||||
MtlPixelFormat Format,
|
|
||||||
uint BytesPerPixel,
|
|
||||||
uint BlockBytes)
|
|
||||||
{
|
|
||||||
public bool IsBlockCompressed => BlockBytes != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct MetalRenderTargetFormat(
|
|
||||||
MtlPixelFormat Format,
|
|
||||||
Gen5PixelOutputKind OutputKind)
|
|
||||||
{
|
|
||||||
public static uint GetBytesPerPixel(MtlPixelFormat format) =>
|
|
||||||
format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Unorm or MtlPixelFormat.R8Uint => 1,
|
|
||||||
MtlPixelFormat.Rg8Unorm => 2,
|
|
||||||
MtlPixelFormat.Rg32Float => 8,
|
|
||||||
MtlPixelFormat.Rgba16Unorm or MtlPixelFormat.Rgba16Uint or
|
|
||||||
MtlPixelFormat.Rgba16Sint or MtlPixelFormat.Rgba16Float => 8,
|
|
||||||
MtlPixelFormat.Rgba32Float => 16,
|
|
||||||
_ => 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Guest texture-descriptor codes to Metal formats, mirroring the Vulkan
|
|
||||||
/// backend's table case for case so both backends accept the same guest
|
|
||||||
/// formats. Guest format 9 (2:10:10:10) maps to BGR10A2 — the bit layout that
|
|
||||||
/// matches Vulkan's A2R10G10B10 pack.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MetalGuestFormats
|
|
||||||
{
|
|
||||||
/// <summary>Guest sampled-texture format to Metal, mirroring the Vulkan
|
|
||||||
/// backend's GetTextureFormat case for case (including its RGBA8 fallback
|
|
||||||
/// for unmapped codes, so unknown formats render something rather than
|
|
||||||
/// nothing). BC formats upload raw blocks — Mac-family GPUs decode them
|
|
||||||
/// natively.</summary>
|
|
||||||
public static MetalTextureFormat DecodeTextureFormat(uint dataFormat, uint numberType)
|
|
||||||
{
|
|
||||||
var format = (dataFormat, numberType) switch
|
|
||||||
{
|
|
||||||
(1, 0) => MtlPixelFormat.R8Unorm,
|
|
||||||
(1, 1) => MtlPixelFormat.R8Snorm,
|
|
||||||
(1, 4) => MtlPixelFormat.R8Uint,
|
|
||||||
(1, 5) => MtlPixelFormat.R8Sint,
|
|
||||||
(2, 0) => MtlPixelFormat.R16Unorm,
|
|
||||||
(2, 1) => MtlPixelFormat.R16Snorm,
|
|
||||||
(2, 4) => MtlPixelFormat.R16Uint,
|
|
||||||
(2, 5) => MtlPixelFormat.R16Sint,
|
|
||||||
(2, 7) => MtlPixelFormat.R16Float,
|
|
||||||
(3, 0) => MtlPixelFormat.Rg8Unorm,
|
|
||||||
(3, 1) => MtlPixelFormat.Rg8Snorm,
|
|
||||||
(3, 4) => MtlPixelFormat.Rg8Uint,
|
|
||||||
(3, 5) => MtlPixelFormat.Rg8Sint,
|
|
||||||
(4, 4) => MtlPixelFormat.R32Uint,
|
|
||||||
(4, 5) => MtlPixelFormat.R32Sint,
|
|
||||||
(4, 7) => MtlPixelFormat.R32Float,
|
|
||||||
(5, 0) => MtlPixelFormat.Rg16Unorm,
|
|
||||||
(5, 4) => MtlPixelFormat.Rg16Uint,
|
|
||||||
(5, 5) => MtlPixelFormat.Rg16Sint,
|
|
||||||
(5, 7) => MtlPixelFormat.Rg16Float,
|
|
||||||
(6, 7) or (7, 7) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(8, _) or (9, _) => MtlPixelFormat.Bgr10A2Unorm,
|
|
||||||
(10, 4) => MtlPixelFormat.Rgba8Uint,
|
|
||||||
(10, 5) => MtlPixelFormat.Rgba8Sint,
|
|
||||||
(10, 9) => MtlPixelFormat.Rgba8UnormSrgb,
|
|
||||||
(11, 4) => MtlPixelFormat.Rg32Uint,
|
|
||||||
(11, 5) => MtlPixelFormat.Rg32Sint,
|
|
||||||
(11, 7) => MtlPixelFormat.Rg32Float,
|
|
||||||
(12, 0) => MtlPixelFormat.Rgba16Unorm,
|
|
||||||
(12, 4) => MtlPixelFormat.Rgba16Uint,
|
|
||||||
(12, 5) => MtlPixelFormat.Rgba16Sint,
|
|
||||||
(12, 7) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(13, 4) or (14, 4) => MtlPixelFormat.Rgba32Uint,
|
|
||||||
(13, 5) or (14, 5) => MtlPixelFormat.Rgba32Sint,
|
|
||||||
(13, _) or (14, _) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(16, 0) => MtlPixelFormat.B5G6R5Unorm,
|
|
||||||
(34, 7) => MtlPixelFormat.Rgb9E5Float,
|
|
||||||
(169, _) => MtlPixelFormat.Bc1Rgba,
|
|
||||||
(170, _) => MtlPixelFormat.Bc1RgbaSrgb,
|
|
||||||
(171, _) => MtlPixelFormat.Bc2Rgba,
|
|
||||||
(172, _) => MtlPixelFormat.Bc2RgbaSrgb,
|
|
||||||
(173, _) => MtlPixelFormat.Bc3Rgba,
|
|
||||||
(174, _) => MtlPixelFormat.Bc3RgbaSrgb,
|
|
||||||
(175, 1) or (176, _) => MtlPixelFormat.Bc4RSnorm,
|
|
||||||
(175, _) => MtlPixelFormat.Bc4RUnorm,
|
|
||||||
(177, 1) or (178, _) => MtlPixelFormat.Bc5RgSnorm,
|
|
||||||
(177, _) => MtlPixelFormat.Bc5RgUnorm,
|
|
||||||
(179, _) => MtlPixelFormat.Bc6HRgbUfloat,
|
|
||||||
(180, _) => MtlPixelFormat.Bc6HRgbFloat,
|
|
||||||
(181, _) => MtlPixelFormat.Bc7RgbaUnorm,
|
|
||||||
(182, _) => MtlPixelFormat.Bc7RgbaUnormSrgb,
|
|
||||||
_ => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
};
|
|
||||||
|
|
||||||
var blockBytes = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.Bc1Rgba or MtlPixelFormat.Bc1RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc4RUnorm or MtlPixelFormat.Bc4RSnorm => 8u,
|
|
||||||
MtlPixelFormat.Bc2Rgba or MtlPixelFormat.Bc2RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc3Rgba or MtlPixelFormat.Bc3RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc5RgUnorm or MtlPixelFormat.Bc5RgSnorm or
|
|
||||||
MtlPixelFormat.Bc6HRgbFloat or MtlPixelFormat.Bc6HRgbUfloat or
|
|
||||||
MtlPixelFormat.Bc7RgbaUnorm or MtlPixelFormat.Bc7RgbaUnormSrgb => 16u,
|
|
||||||
_ => 0u,
|
|
||||||
};
|
|
||||||
|
|
||||||
var bytesPerPixel = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Unorm or MtlPixelFormat.R8Snorm or
|
|
||||||
MtlPixelFormat.R8Uint or MtlPixelFormat.R8Sint => 1u,
|
|
||||||
MtlPixelFormat.R16Unorm or MtlPixelFormat.R16Snorm or
|
|
||||||
MtlPixelFormat.R16Uint or MtlPixelFormat.R16Sint or
|
|
||||||
MtlPixelFormat.R16Float or MtlPixelFormat.Rg8Unorm or
|
|
||||||
MtlPixelFormat.Rg8Snorm or MtlPixelFormat.Rg8Uint or
|
|
||||||
MtlPixelFormat.Rg8Sint or MtlPixelFormat.B5G6R5Unorm => 2u,
|
|
||||||
MtlPixelFormat.Rg32Uint or MtlPixelFormat.Rg32Sint or
|
|
||||||
MtlPixelFormat.Rg32Float or MtlPixelFormat.Rgba16Unorm or
|
|
||||||
MtlPixelFormat.Rgba16Uint or MtlPixelFormat.Rgba16Sint or
|
|
||||||
MtlPixelFormat.Rgba16Float => 8u,
|
|
||||||
MtlPixelFormat.Rgba32Uint or MtlPixelFormat.Rgba32Sint or
|
|
||||||
MtlPixelFormat.Rgba32Float => 16u,
|
|
||||||
_ => 4u,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new MetalTextureFormat(format, bytesPerPixel, blockBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Source byte footprint of a sampled texture, block-aware —
|
|
||||||
/// the same math the AGC layer uses to size the texel copy it ships.</summary>
|
|
||||||
public static ulong GetTextureByteCount(in MetalTextureFormat format, uint width, uint height) =>
|
|
||||||
format.IsBlockCompressed
|
|
||||||
? checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * format.BlockBytes)
|
|
||||||
: checked((ulong)width * height * format.BytesPerPixel);
|
|
||||||
|
|
||||||
public static bool TryDecodeRenderTargetFormat(
|
|
||||||
uint dataFormat,
|
|
||||||
uint numberType,
|
|
||||||
out MetalRenderTargetFormat result)
|
|
||||||
{
|
|
||||||
var format = (dataFormat, numberType) switch
|
|
||||||
{
|
|
||||||
(4, 4) => MtlPixelFormat.R32Uint,
|
|
||||||
(4, 5) => MtlPixelFormat.R32Sint,
|
|
||||||
(4, 7) => MtlPixelFormat.R32Float,
|
|
||||||
(5, 4) => MtlPixelFormat.Rg16Uint,
|
|
||||||
(5, 5) => MtlPixelFormat.Rg16Sint,
|
|
||||||
(5, 7) => MtlPixelFormat.Rg16Float,
|
|
||||||
(6, 7) or (7, 7) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(9, _) => MtlPixelFormat.Bgr10A2Unorm,
|
|
||||||
(10, 4) => MtlPixelFormat.Rgba8Uint,
|
|
||||||
(10, 5) => MtlPixelFormat.Rgba8Sint,
|
|
||||||
(10, 9) => MtlPixelFormat.Rgba8UnormSrgb,
|
|
||||||
(10, _) => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
(11, 7) => MtlPixelFormat.Rg32Float,
|
|
||||||
(12, 4) => MtlPixelFormat.Rgba16Uint,
|
|
||||||
(12, 5) => MtlPixelFormat.Rgba16Sint,
|
|
||||||
(12, 7) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(13, 7) or (14, 7) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(20, 0) => MtlPixelFormat.R32Uint,
|
|
||||||
(29, 0) or (4, 0) => MtlPixelFormat.R32Float,
|
|
||||||
(1, 0) or (36, 0) => MtlPixelFormat.R8Unorm,
|
|
||||||
(49, 0) => MtlPixelFormat.R8Uint,
|
|
||||||
(3, 0) => MtlPixelFormat.Rg8Unorm,
|
|
||||||
(5, 0) => MtlPixelFormat.Rg16Unorm,
|
|
||||||
(7, 0) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(12, 0) => MtlPixelFormat.Rgba16Unorm,
|
|
||||||
(13, 0) or (14, 0) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(22, 0) or (71, 0) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(56, 0) or (62, 0) or (64, 0) => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
(75, 0) => MtlPixelFormat.Rg32Float,
|
|
||||||
_ => MtlPixelFormat.Invalid,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (format == MtlPixelFormat.Invalid)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputKind = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Uint or MtlPixelFormat.R32Uint or MtlPixelFormat.Rg16Uint or
|
|
||||||
MtlPixelFormat.Rgba8Uint or MtlPixelFormat.Rgba16Uint => Gen5PixelOutputKind.Uint,
|
|
||||||
MtlPixelFormat.R32Sint or MtlPixelFormat.Rg16Sint or MtlPixelFormat.Rgba8Sint or
|
|
||||||
MtlPixelFormat.Rgba16Sint => Gen5PixelOutputKind.Sint,
|
|
||||||
_ => Gen5PixelOutputKind.Float,
|
|
||||||
};
|
|
||||||
result = new MetalRenderTargetFormat(format, outputKind);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
using SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Metal backend for the guest-GPU seam: MSL codegen via
|
|
||||||
/// SharpEmu.ShaderCompiler.Metal, rendering via the Metal presenter — the full
|
|
||||||
/// surface (presentation, guest images, ordered flips, translated draws, and
|
|
||||||
/// compute) with no Vulkan, MoltenVK, or windowing-library dependency.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
|
|
||||||
{
|
|
||||||
public string BackendName => "Metal";
|
|
||||||
|
|
||||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
|
||||||
new MetalCompiledGuestShader(new Gen5MslShader(
|
|
||||||
MslFixedShaders.CreateDepthOnlyFragment(),
|
|
||||||
"depth_only_fs",
|
|
||||||
Gen5MslStage.Pixel,
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
AttributeCount: 0,
|
|
||||||
[]));
|
|
||||||
|
|
||||||
public bool TryCompileVertexShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int globalBufferBase = 0,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int imageBindingBase = 0,
|
|
||||||
int scalarRegisterBufferIndex = -1,
|
|
||||||
int requiredVertexOutputCount = 0,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
if (!Gen5MslTranslator.TryCompileVertexShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
globalBufferBase,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
imageBindingBase,
|
|
||||||
scalarRegisterBufferIndex,
|
|
||||||
requiredVertexOutputCount,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompilePixelShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int globalBufferBase = 0,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int imageBindingBase = 0,
|
|
||||||
int scalarRegisterBufferIndex = -1,
|
|
||||||
uint pixelInputEnable = 0,
|
|
||||||
uint pixelInputAddress = 0,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
if (!Gen5MslTranslator.TryCompilePixelShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
outputs,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
globalBufferBase,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
imageBindingBase,
|
|
||||||
scalarRegisterBufferIndex,
|
|
||||||
pixelInputEnable,
|
|
||||||
pixelInputAddress,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompileComputeShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
uint localSizeX,
|
|
||||||
uint localSizeY,
|
|
||||||
uint localSizeZ,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int initialScalarBufferIndex = -1,
|
|
||||||
uint waveLaneCount = 32,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
// Wave64 compute is emulated by the translator: cross-lane ops bridge
|
|
||||||
// the two 32-wide Apple simdgroups of a guest wave through threadgroup
|
|
||||||
// scratch, and wave-agnostic kernels run per-thread unchanged.
|
|
||||||
if (!Gen5MslTranslator.TryCompileComputeShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
localSizeX,
|
|
||||||
localSizeY,
|
|
||||||
localSizeZ,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
initialScalarBufferIndex,
|
|
||||||
waveLaneCount,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
|
|
||||||
DepthOnlyFragmentShader;
|
|
||||||
|
|
||||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
|
||||||
{
|
|
||||||
if (MetalGuestFormats.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
|
|
||||||
{
|
|
||||||
outputKind = format.OutputKind;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputKind = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnsureStarted(uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.EnsureStarted(width, height);
|
|
||||||
|
|
||||||
public void HideSplashScreen() =>
|
|
||||||
MetalVideoPresenter.HideSplashScreen();
|
|
||||||
|
|
||||||
public void Submit(byte[] bgraFrame, uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.Submit(bgraFrame, width, height);
|
|
||||||
|
|
||||||
public bool TrySubmitGuestImage(
|
|
||||||
ulong address,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint pitchInPixel) =>
|
|
||||||
MetalVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
|
||||||
|
|
||||||
public bool TrySubmitOrderedGuestImageFlip(
|
|
||||||
int videoOutHandle,
|
|
||||||
int displayBufferIndex,
|
|
||||||
ulong address,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint pitchInPixel) =>
|
|
||||||
MetalVideoPresenter.TrySubmitOrderedGuestImageFlip(
|
|
||||||
videoOutHandle,
|
|
||||||
displayBufferIndex,
|
|
||||||
address,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
pitchInPixel);
|
|
||||||
|
|
||||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
|
||||||
MetalVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
|
||||||
|
|
||||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
|
|
||||||
MetalVideoPresenter.IsGuestImageAvailable(address, format, numberType);
|
|
||||||
|
|
||||||
public bool TrySubmitGuestImageBlit(
|
|
||||||
ulong sourceAddress,
|
|
||||||
uint sourceWidth,
|
|
||||||
uint sourceHeight,
|
|
||||||
uint sourceFormat,
|
|
||||||
uint sourceNumberType,
|
|
||||||
ulong destinationAddress,
|
|
||||||
uint destinationWidth,
|
|
||||||
uint destinationHeight,
|
|
||||||
uint destinationFormat,
|
|
||||||
uint destinationNumberType) =>
|
|
||||||
MetalVideoPresenter.TrySubmitGuestImageBlit(
|
|
||||||
sourceAddress,
|
|
||||||
sourceWidth,
|
|
||||||
sourceHeight,
|
|
||||||
sourceFormat,
|
|
||||||
sourceNumberType,
|
|
||||||
destinationAddress,
|
|
||||||
destinationWidth,
|
|
||||||
destinationHeight,
|
|
||||||
destinationFormat,
|
|
||||||
destinationNumberType);
|
|
||||||
|
|
||||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestDraw(drawKind, width, height);
|
|
||||||
|
|
||||||
public void SubmitTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint attributeCount,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null) =>
|
|
||||||
MetalVideoPresenter.SubmitTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
attributeCount,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState);
|
|
||||||
|
|
||||||
public void SubmitDepthOnlyTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
GuestDepthTarget depthTarget,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitDepthOnlyTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
depthTarget,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
public void SubmitOffscreenTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
IReadOnlyList<GuestRenderTarget> targets,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null,
|
|
||||||
GuestDepthTarget? depthTarget = null,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitOffscreenTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
targets,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState,
|
|
||||||
depthTarget,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
public void SubmitStorageTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitStorageTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
private static MetalCompiledGuestShader Msl(IGuestCompiledShader shader) =>
|
|
||||||
shader as MetalCompiledGuestShader ??
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"shader handle of type {shader.GetType().Name} was not compiled by the Metal backend");
|
|
||||||
|
|
||||||
public long SubmitComputeDispatch(
|
|
||||||
ulong shaderAddress,
|
|
||||||
IGuestCompiledShader computeShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint groupCountX,
|
|
||||||
uint groupCountY,
|
|
||||||
uint groupCountZ,
|
|
||||||
uint baseGroupX,
|
|
||||||
uint baseGroupY,
|
|
||||||
uint baseGroupZ,
|
|
||||||
uint localSizeX,
|
|
||||||
uint localSizeY,
|
|
||||||
uint localSizeZ,
|
|
||||||
bool isIndirect,
|
|
||||||
bool writesGlobalMemory,
|
|
||||||
uint threadCountX = uint.MaxValue,
|
|
||||||
uint threadCountY = uint.MaxValue,
|
|
||||||
uint threadCountZ = uint.MaxValue)
|
|
||||||
{
|
|
||||||
// The translated kernel bakes its threadgroup size; localSize and
|
|
||||||
// isIndirect are already folded in by the AGC layer before submission.
|
|
||||||
_ = localSizeX;
|
|
||||||
_ = localSizeY;
|
|
||||||
_ = localSizeZ;
|
|
||||||
_ = isIndirect;
|
|
||||||
return MetalVideoPresenter.SubmitComputeDispatch(
|
|
||||||
shaderAddress,
|
|
||||||
Msl(computeShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
groupCountX,
|
|
||||||
groupCountY,
|
|
||||||
groupCountZ,
|
|
||||||
baseGroupX,
|
|
||||||
baseGroupY,
|
|
||||||
baseGroupZ,
|
|
||||||
writesGlobalMemory,
|
|
||||||
threadCountX,
|
|
||||||
threadCountY,
|
|
||||||
threadCountZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long _perfShaderCompilations;
|
|
||||||
|
|
||||||
public IDisposable EnterGuestQueue(string queueName, ulong submissionId) =>
|
|
||||||
MetalVideoPresenter.EnterGuestQueue(queueName, submissionId);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestAction(Action action, string debugName) =>
|
|
||||||
MetalVideoPresenter.SubmitOrderedGuestAction(action, debugName);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex) =>
|
|
||||||
MetalVideoPresenter.SubmitOrderedGuestFlipWait(videoOutHandle, displayBufferIndex);
|
|
||||||
|
|
||||||
public bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite) =>
|
|
||||||
MetalVideoPresenter.WaitForGuestWork(workSequence, timeoutMilliseconds);
|
|
||||||
|
|
||||||
public long CurrentGuestWorkSequenceForDiagnostics =>
|
|
||||||
MetalVideoPresenter.CurrentGuestWorkSequenceForDiagnostics;
|
|
||||||
|
|
||||||
public bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType) =>
|
|
||||||
MetalVideoPresenter.IsGuestImageUploadKnown(address, format, numberType);
|
|
||||||
|
|
||||||
public bool GuestImageWantsInitialData(ulong address) =>
|
|
||||||
MetalVideoPresenter.GuestImageWantsInitialData(address);
|
|
||||||
|
|
||||||
public void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels) =>
|
|
||||||
MetalVideoPresenter.ProvideGuestImageInitialData(address, rgbaPixels);
|
|
||||||
|
|
||||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
|
||||||
|
|
||||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
|
||||||
|
|
||||||
public bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount) =>
|
|
||||||
MetalVideoPresenter.TryGetGuestImageExtent(address, out width, out height, out byteCount);
|
|
||||||
|
|
||||||
public IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents() =>
|
|
||||||
MetalVideoPresenter.GetGuestImageExtents();
|
|
||||||
|
|
||||||
public bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
MetalVideoPresenter.IsTextureContentCached(identity);
|
|
||||||
|
|
||||||
public void AttachGuestMemory(SharpEmu.HLE.ICpuMemory memory) =>
|
|
||||||
MetalVideoPresenter.AttachGuestMemory(memory);
|
|
||||||
|
|
||||||
// Over-alignment is always valid, and 256 covers every Metal buffer-offset
|
|
||||||
// requirement (Intel Macs need 256 for constant buffers; Apple GPUs less).
|
|
||||||
public ulong GuestStorageBufferOffsetAlignment => 256;
|
|
||||||
|
|
||||||
public void CountShaderCompilation() =>
|
|
||||||
Interlocked.Increment(ref _perfShaderCompilations);
|
|
||||||
|
|
||||||
public (long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters()
|
|
||||||
{
|
|
||||||
var (draws, drawMs, pipelines) = MetalVideoPresenter.ReadAndResetDrawPerfCounters();
|
|
||||||
return (draws, drawMs, pipelines, Interlocked.Exchange(ref _perfShaderCompilations, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RequestClose() =>
|
|
||||||
MetalVideoPresenter.RequestClose();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.HLE.Host.Posix;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keyboard state sampled from the Metal presenter's window, feeding the POSIX
|
|
||||||
/// host input seam so pad emulation works like the Vulkan presenter's
|
|
||||||
/// HostWindowInput. Key events arrive on the AppKit main thread as macOS
|
|
||||||
/// virtual key codes; pad reads happen on guest threads, so state is guarded.
|
|
||||||
/// Window gamepads are not surfaced by AppKit — controller support would go
|
|
||||||
/// through GameController.framework and is out of scope here.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MetalHostInput
|
|
||||||
{
|
|
||||||
private static readonly object Gate = new();
|
|
||||||
private static readonly HashSet<ushort> Pressed = new();
|
|
||||||
private static volatile bool _connected;
|
|
||||||
|
|
||||||
/// <summary>Registers this window's keyboard as the host input source.</summary>
|
|
||||||
public static void Attach()
|
|
||||||
{
|
|
||||||
_connected = true;
|
|
||||||
PosixHostInput.SetSource(new MetalWindowInputSource());
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Window keyboard input attached for pad emulation.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug automation: SHARPEMU_METAL_AUTOKEY="12:0x24,15:0x24" presses the
|
|
||||||
// macOS key code at each elapsed-seconds mark for a few frames, letting
|
|
||||||
// headless test runs navigate menus without a human at the keyboard.
|
|
||||||
private static readonly List<(double At, ushort Key, bool[] State)> _autoKeys = ParseAutoKeys();
|
|
||||||
private static readonly System.Diagnostics.Stopwatch _autoKeyClock =
|
|
||||||
System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
|
|
||||||
private static List<(double, ushort, bool[])> ParseAutoKeys()
|
|
||||||
{
|
|
||||||
var keys = new List<(double, ushort, bool[])>();
|
|
||||||
var spec = Environment.GetEnvironmentVariable("SHARPEMU_METAL_AUTOKEY");
|
|
||||||
if (string.IsNullOrWhiteSpace(spec))
|
|
||||||
{
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in spec.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
var parts = entry.Split(':');
|
|
||||||
if (parts.Length == 2 &&
|
|
||||||
double.TryParse(parts[0], out var at) &&
|
|
||||||
TryParseKeyCode(parts[1], out var key))
|
|
||||||
{
|
|
||||||
keys.Add((at, key, new bool[2]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseKeyCode(string text, out ushort key)
|
|
||||||
{
|
|
||||||
return text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? ushort.TryParse(text[2..], System.Globalization.NumberStyles.HexNumber, null, out key)
|
|
||||||
: ushort.TryParse(text, out key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called once per render frame; fires and releases scripted keys.</summary>
|
|
||||||
public static void PumpAutoKeys()
|
|
||||||
{
|
|
||||||
if (_autoKeys.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var elapsed = _autoKeyClock.Elapsed.TotalSeconds;
|
|
||||||
foreach (var (at, key, state) in _autoKeys)
|
|
||||||
{
|
|
||||||
if (!state[0] && elapsed >= at)
|
|
||||||
{
|
|
||||||
state[0] = true;
|
|
||||||
KeyDown(key, isRepeat: false);
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Metal autokey press 0x{key:X} at {elapsed:F1}s");
|
|
||||||
}
|
|
||||||
else if (state[0] && !state[1] && elapsed >= at + 0.2)
|
|
||||||
{
|
|
||||||
state[1] = true;
|
|
||||||
KeyUp(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void KeyDown(ushort keyCode, bool isRepeat)
|
|
||||||
{
|
|
||||||
// kVK_F1: parity with the Vulkan window's perf-overlay toggle.
|
|
||||||
if (keyCode == 0x7A && !isRepeat)
|
|
||||||
{
|
|
||||||
VideoOut.PerfOverlay.Toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
Pressed.Add(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void KeyUp(ushort keyCode)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
Pressed.Remove(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsKeyCodeDown(ushort keyCode)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
return Pressed.Contains(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MetalWindowInputSource : IPosixWindowInputSource
|
|
||||||
{
|
|
||||||
public bool HasKeyboardFocus => _connected;
|
|
||||||
|
|
||||||
public bool IsKeyDown(int virtualKey) =>
|
|
||||||
TryMapVirtualKey(virtualKey, out var keyCode) && IsKeyCodeDown(keyCode);
|
|
||||||
|
|
||||||
public int GetGamepadStates(Span<HostGamepadState> destination) => 0;
|
|
||||||
|
|
||||||
public string? DescribeConnectedGamepad() => null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Windows virtual-key semantics (the seam's contract) to macOS
|
|
||||||
/// kVK virtual key codes, covering the keys pad emulation polls.</summary>
|
|
||||||
private static bool TryMapVirtualKey(int vk, out ushort keyCode)
|
|
||||||
{
|
|
||||||
keyCode = vk switch
|
|
||||||
{
|
|
||||||
0x08 => 0x33, // Backspace -> kVK_Delete
|
|
||||||
0x09 => 0x30, // Tab
|
|
||||||
0x0D => 0x24, // Enter -> kVK_Return
|
|
||||||
0x1B => 0x35, // Escape
|
|
||||||
0x20 => 0x31, // Space
|
|
||||||
0x25 => 0x7B, // Left
|
|
||||||
0x26 => 0x7E, // Up
|
|
||||||
0x27 => 0x7C, // Right
|
|
||||||
0x28 => 0x7D, // Down
|
|
||||||
// Letters: macOS ANSI key codes are layout-position based and
|
|
||||||
// non-contiguous, so map each polled letter explicitly.
|
|
||||||
0x41 => 0x00, // A
|
|
||||||
0x42 => 0x0B, // B
|
|
||||||
0x43 => 0x08, // C
|
|
||||||
0x44 => 0x02, // D
|
|
||||||
0x45 => 0x0E, // E
|
|
||||||
0x46 => 0x03, // F
|
|
||||||
0x47 => 0x05, // G
|
|
||||||
0x48 => 0x04, // H
|
|
||||||
0x49 => 0x22, // I
|
|
||||||
0x4A => 0x26, // J
|
|
||||||
0x4B => 0x28, // K
|
|
||||||
0x4C => 0x25, // L
|
|
||||||
0x4D => 0x2E, // M
|
|
||||||
0x4E => 0x2D, // N
|
|
||||||
0x4F => 0x1F, // O
|
|
||||||
0x50 => 0x23, // P
|
|
||||||
0x51 => 0x0C, // Q
|
|
||||||
0x52 => 0x0F, // R
|
|
||||||
0x53 => 0x01, // S
|
|
||||||
0x54 => 0x11, // T
|
|
||||||
0x55 => 0x20, // U
|
|
||||||
0x56 => 0x09, // V
|
|
||||||
0x57 => 0x0D, // W
|
|
||||||
0x58 => 0x07, // X
|
|
||||||
0x59 => 0x10, // Y
|
|
||||||
0x5A => 0x06, // Z
|
|
||||||
_ => ushort.MaxValue,
|
|
||||||
};
|
|
||||||
return keyCode != ushort.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Core Graphics / Metal ABI structs passed by value through objc_msgSend. Struct
|
|
||||||
// *returns* are deliberately never used: on x86-64 (this process runs under Rosetta
|
|
||||||
// on Apple silicon) large struct returns switch to objc_msgSend_stret, and avoiding
|
|
||||||
// them entirely keeps one calling convention everywhere.
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct CGRect
|
|
||||||
{
|
|
||||||
public double X;
|
|
||||||
public double Y;
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct CGSize
|
|
||||||
{
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlClearColor
|
|
||||||
{
|
|
||||||
public double Red;
|
|
||||||
public double Green;
|
|
||||||
public double Blue;
|
|
||||||
public double Alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>MTLTextureSwizzleChannels: one MTLTextureSwizzle byte per output
|
|
||||||
/// channel (Zero=0, One=1, Red=2, Green=3, Blue=4, Alpha=5).</summary>
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlTextureSwizzleChannels
|
|
||||||
{
|
|
||||||
public byte Red;
|
|
||||||
public byte Green;
|
|
||||||
public byte Blue;
|
|
||||||
public byte Alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlRegion
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Z;
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
public nuint Depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlSize
|
|
||||||
{
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
public nuint Depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlOrigin
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlScissorRect
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlViewport
|
|
||||||
{
|
|
||||||
public double OriginX;
|
|
||||||
public double OriginY;
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
public double ZNear;
|
|
||||||
public double ZFar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Objective-C runtime access for the Metal presenter: AppKit, QuartzCore, and Metal
|
|
||||||
/// through objc_msgSend, with one LibraryImport overload per distinct native
|
|
||||||
/// signature. Dependency-free by design — this plus the OS frameworks is the entire
|
|
||||||
/// Metal path, which is what keeps it NativeAOT-clean.
|
|
||||||
/// </summary>
|
|
||||||
internal static partial class MetalNative
|
|
||||||
{
|
|
||||||
private const string CoreFoundation =
|
|
||||||
"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
|
|
||||||
|
|
||||||
[LibraryImport(CoreFoundation)]
|
|
||||||
public static partial nint CFRunLoopGetMain();
|
|
||||||
|
|
||||||
[LibraryImport(CoreFoundation)]
|
|
||||||
public static partial void CFRunLoopStop(nint runLoop);
|
|
||||||
|
|
||||||
private const string ObjCLibrary = "/usr/lib/libobjc.A.dylib";
|
|
||||||
private const string MetalFramework = "/System/Library/Frameworks/Metal.framework/Metal";
|
|
||||||
private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit";
|
|
||||||
private const string QuartzCoreFramework = "/System/Library/Frameworks/QuartzCore.framework/QuartzCore";
|
|
||||||
|
|
||||||
private static bool _frameworksLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Makes the AppKit and QuartzCore classes visible to objc_getClass; Metal is
|
|
||||||
/// pulled in by its own LibraryImport. Call once before any Class() lookup.
|
|
||||||
/// </summary>
|
|
||||||
public static void EnsureFrameworksLoaded()
|
|
||||||
{
|
|
||||||
if (_frameworksLoaded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.Load(AppKitFramework);
|
|
||||||
NativeLibrary.Load(QuartzCoreFramework);
|
|
||||||
_frameworksLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[LibraryImport(MetalFramework)]
|
|
||||||
public static partial nint MTLCreateSystemDefaultDevice();
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
private static partial nint objc_getClass(string name);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
private static partial nint sel_registerName(string name);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
public static partial nint objc_allocateClassPair(nint superclass, string name, nuint extraBytes);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial void objc_registerClassPair(nint cls);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool class_addMethod(nint cls, nint name, nint imp, string types);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial nint objc_autoreleasePoolPush();
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial void objc_autoreleasePoolPop(nint pool);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector);
|
|
||||||
|
|
||||||
/// <summary>objc_msgSend for -gpuResourceID. MTLResourceID is a one-field
|
|
||||||
/// 8-byte struct, returned in a register on the x86-64 ABI, so it maps to a
|
|
||||||
/// ulong return — the value written into a Tier 2 argument buffer slot.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial ulong SendGpuResourceId(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>objc_msgSend for a CGRect-returning selector (e.g. -bounds).
|
|
||||||
/// A 32-byte struct is returned via the x86-64 stret ABI — a hidden
|
|
||||||
/// pointer to caller storage passed ahead of self/_cmd — so this must not
|
|
||||||
/// be folded into the plain objc_msgSend overloads.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend_stret")]
|
|
||||||
public static partial void SendStretRect(out CGRect result, nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument, ref nint error);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument0, nint argument1, ref nint error);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendAtIndex(nint receiver, nint selector, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool SendBool(nint receiver, nint selector);
|
|
||||||
|
|
||||||
/// <summary>One-argument BOOL sends, e.g. respondsToSelector:.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool SendBool(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial double SendDouble(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
/// <summary>Two-object-argument void sends, e.g. setObject:forKey:.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector, nint argument0, nint argument1);
|
|
||||||
|
|
||||||
/// <summary>performSelectorOnMainThread:withObject:waitUntilDone: — the SEL
|
|
||||||
/// to perform is itself an argument, followed by the object and the wait
|
|
||||||
/// flag.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidPerformSelector(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint performedSelector,
|
|
||||||
nint argument,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool waitUntilDone);
|
|
||||||
|
|
||||||
/// <summary>setSwizzle: on MTLTextureDescriptor. Four one-byte
|
|
||||||
/// MTLTextureSwizzle values, passed packed like the framework expects.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidSwizzle(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlTextureSwizzleChannels channels);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidBool(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool argument);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidDouble(nint receiver, nint selector, double argument);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidSize(nint receiver, nint selector, CGSize size);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidRect(nint receiver, nint selector, CGRect rect);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidClearColor(nint receiver, nint selector, MtlClearColor color);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidBlendColor(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
float red,
|
|
||||||
float green,
|
|
||||||
float blue,
|
|
||||||
float alpha);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidViewport(nint receiver, nint selector, MtlViewport viewport);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendSetAtIndex(nint receiver, nint selector, nint value, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidCopyTexture(nint receiver, nint selector, nint source, nint destination);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendBuffer(nint receiver, nint selector, nint bytes, nuint length, nuint options);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendNewBuffer(nint receiver, nint selector, nuint length, nuint options);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendCopyTextureToBuffer(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint sourceTexture,
|
|
||||||
nuint sourceSlice,
|
|
||||||
nuint sourceLevel,
|
|
||||||
MtlOrigin sourceOrigin,
|
|
||||||
MtlSize sourceSize,
|
|
||||||
nint destinationBuffer,
|
|
||||||
nuint destinationOffset,
|
|
||||||
nuint destinationBytesPerRow,
|
|
||||||
nuint destinationBytesPerImage);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendCopyBufferToTexture(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint sourceBuffer,
|
|
||||||
nuint sourceOffset,
|
|
||||||
nuint sourceBytesPerRow,
|
|
||||||
nuint sourceBytesPerImage,
|
|
||||||
MtlSize sourceSize,
|
|
||||||
nint destinationTexture,
|
|
||||||
nuint destinationSlice,
|
|
||||||
nuint destinationLevel,
|
|
||||||
MtlOrigin destinationOrigin);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDispatch(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlSize threadgroups,
|
|
||||||
MtlSize threadsPerThreadgroup);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendSetBuffer(nint receiver, nint selector, nint buffer, nuint offset, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidScissor(nint receiver, nint selector, MtlScissorRect rect);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawPrimitivesInstanced(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint vertexStart,
|
|
||||||
nuint vertexCount,
|
|
||||||
nuint instanceCount);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawIndexedPrimitives(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint indexCount,
|
|
||||||
nuint indexType,
|
|
||||||
nint indexBuffer,
|
|
||||||
nuint indexBufferOffset,
|
|
||||||
nuint instanceCount);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendTimer(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
double interval,
|
|
||||||
nint target,
|
|
||||||
nint timerSelector,
|
|
||||||
nint userInfo,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool repeats);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendInitFrame(nint receiver, nint selector, CGRect frame);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendInitWindow(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
CGRect contentRect,
|
|
||||||
nuint styleMask,
|
|
||||||
nuint backing,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool defer);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendNextEvent(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
ulong eventMask,
|
|
||||||
nint untilDate,
|
|
||||||
nint inMode,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool dequeue);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendTextureDescriptor(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint pixelFormat,
|
|
||||||
nuint width,
|
|
||||||
nuint height,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool mipmapped);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendReplaceRegion(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlRegion region,
|
|
||||||
nuint mipmapLevel,
|
|
||||||
nint bytes,
|
|
||||||
nuint bytesPerRow);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawPrimitives(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint vertexStart,
|
|
||||||
nuint vertexCount);
|
|
||||||
|
|
||||||
public static nint Class(string name) => objc_getClass(name);
|
|
||||||
|
|
||||||
public static nint Selector(string name) => sel_registerName(name);
|
|
||||||
|
|
||||||
/// <summary>Autoreleased NSString — only valid inside an autorelease pool
|
|
||||||
/// unless the caller retains it.</summary>
|
|
||||||
public static nint NsString(string value)
|
|
||||||
{
|
|
||||||
var utf8 = Marshal.StringToCoTaskMemUTF8(value);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Send(Class("NSString"), Selector("stringWithUTF8String:"), utf8);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(utf8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Reads an NSString's UTF-8 contents, or null if the handle is nil.</summary>
|
|
||||||
public static string? ReadNsString(nint nsString)
|
|
||||||
{
|
|
||||||
if (nsString == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var utf8 = Send(nsString, Selector("UTF8String"));
|
|
||||||
return utf8 == 0 ? null : Marshal.PtrToStringUTF8(utf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribeError(nint error)
|
|
||||||
{
|
|
||||||
if (error == 0)
|
|
||||||
{
|
|
||||||
return "unknown error";
|
|
||||||
}
|
|
||||||
|
|
||||||
var description = Send(error, Selector("localizedDescription"));
|
|
||||||
var utf8 = Send(description, Selector("UTF8String"));
|
|
||||||
return Marshal.PtrToStringUTF8(utf8) ?? "unknown error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Guest draws and compute dispatches batch into one command buffer per drain
|
|
||||||
// instead of one per work item, mirroring the Vulkan presenter's batched guest
|
|
||||||
// commands: commit overhead dominated CPU time for scenes with dozens of draws
|
|
||||||
// per frame. Ordering inside the batch is by encoder sequence (snapshot blits
|
|
||||||
// for a draw's feedback reads are encoded before its render pass opens), and
|
|
||||||
// everything that must observe batched work on the serial queue — flips, image
|
|
||||||
// writes/blits, CPU-visible write-backs, the present pass — flushes first.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private static nint _batchCommandBuffer;
|
|
||||||
private static bool _batchOpen;
|
|
||||||
|
|
||||||
/// <summary>Returns the open batch command buffer, opening one on first
|
|
||||||
/// use. Render thread only, like the drain it serves.</summary>
|
|
||||||
private static nint BeginBatchedGuestCommands(nint queue)
|
|
||||||
{
|
|
||||||
if (_batchOpen)
|
|
||||||
{
|
|
||||||
return _batchCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_batchCommandBuffer = MetalNative.Send(queue, MetalNative.Selector("commandBuffer"));
|
|
||||||
_batchOpen = _batchCommandBuffer != 0;
|
|
||||||
return _batchCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Commits the open batch (if any), tagging the upload pages and
|
|
||||||
/// snapshot resources it consumed. Returns the committed command buffer so
|
|
||||||
/// write-back sites can wait on it, or 0 when nothing was open.</summary>
|
|
||||||
private static nint FlushBatchedGuestCommands()
|
|
||||||
{
|
|
||||||
if (!_batchOpen)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_batchOpen = false;
|
|
||||||
var commandBuffer = _batchCommandBuffer;
|
|
||||||
_batchCommandBuffer = 0;
|
|
||||||
MetalNative.SendVoid(commandBuffer, MetalNative.Selector("commit"));
|
|
||||||
TagUploadPages(commandBuffer);
|
|
||||||
TagSnapshotResources(commandBuffer);
|
|
||||||
return commandBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Guest compute dispatches: ordered guest work like draws, with two contracts to
|
|
||||||
// honor. Storage images are shared live through the guest-image registry so a
|
|
||||||
// dispatch's writes are visible to later draws, blits, and flips of the same
|
|
||||||
// address; and CPU-visible buffer writes land back in guest memory before the
|
|
||||||
// work item completes, which is the ordering point WaitForGuestWork promises.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private static readonly bool _skipAllCompute =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_SKIP_ALL_COMPUTE") == "1";
|
|
||||||
private static bool _tracedDispatchBase;
|
|
||||||
|
|
||||||
private sealed record ComputeGuestDispatch(
|
|
||||||
ulong ShaderAddress,
|
|
||||||
MetalCompiledGuestShader Shader,
|
|
||||||
GuestDrawTexture[] Textures,
|
|
||||||
GuestMemoryBuffer[] GlobalMemoryBuffers,
|
|
||||||
uint GroupCountX,
|
|
||||||
uint GroupCountY,
|
|
||||||
uint GroupCountZ,
|
|
||||||
uint BaseGroupX,
|
|
||||||
uint BaseGroupY,
|
|
||||||
uint BaseGroupZ,
|
|
||||||
uint ThreadCountX,
|
|
||||||
uint ThreadCountY,
|
|
||||||
uint ThreadCountZ);
|
|
||||||
|
|
||||||
private static readonly Dictionary<MetalCompiledGuestShader, nint> _computePipelineCache = new();
|
|
||||||
|
|
||||||
public static long SubmitComputeDispatch(
|
|
||||||
ulong shaderAddress,
|
|
||||||
MetalCompiledGuestShader computeShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint groupCountX,
|
|
||||||
uint groupCountY,
|
|
||||||
uint groupCountZ,
|
|
||||||
uint baseGroupX,
|
|
||||||
uint baseGroupY,
|
|
||||||
uint baseGroupZ,
|
|
||||||
bool writesGlobalMemory,
|
|
||||||
uint threadCountX,
|
|
||||||
uint threadCountY,
|
|
||||||
uint threadCountZ)
|
|
||||||
{
|
|
||||||
var hasStorage = false;
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
hasStorage |= texture.IsStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupCountX == 0 ||
|
|
||||||
groupCountY == 0 ||
|
|
||||||
groupCountZ == 0 ||
|
|
||||||
(!hasStorage && !writesGlobalMemory))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_closed || _thread is null)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage images a dispatch writes become flip sources and sampled
|
|
||||||
// inputs for later work, exactly like published render targets.
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
if (!texture.IsStorage || texture.Address == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
|
|
||||||
if (guestFormat != 0)
|
|
||||||
{
|
|
||||||
_availableGuestImages[texture.Address] = guestFormat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sequence = EnqueueGuestWorkLocked(
|
|
||||||
new ComputeGuestDispatch(
|
|
||||||
shaderAddress,
|
|
||||||
computeShader,
|
|
||||||
ToArray(textures),
|
|
||||||
ToArray(globalMemoryBuffers),
|
|
||||||
groupCountX,
|
|
||||||
groupCountY,
|
|
||||||
groupCountZ,
|
|
||||||
baseGroupX,
|
|
||||||
baseGroupY,
|
|
||||||
baseGroupZ,
|
|
||||||
threadCountX,
|
|
||||||
threadCountY,
|
|
||||||
threadCountZ));
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
if (texture.IsStorage && texture.Address != 0)
|
|
||||||
{
|
|
||||||
_guestImageWorkSequences[texture.Address] = sequence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ExecuteComputeDispatch(nint device, nint queue, ComputeGuestDispatch dispatch)
|
|
||||||
{
|
|
||||||
if (_skipAllCompute)
|
|
||||||
{
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoOut.PerfOverlay.RecordDraw();
|
|
||||||
|
|
||||||
if ((dispatch.BaseGroupX | dispatch.BaseGroupY | dispatch.BaseGroupZ) != 0 &&
|
|
||||||
!_tracedDispatchBase)
|
|
||||||
{
|
|
||||||
// Metal has no dispatch-base; the translated kernel derives its ids
|
|
||||||
// from the raw grid position, so a nonzero base computes offset-zero
|
|
||||||
// work until base support lands in the emitted kernel.
|
|
||||||
_tracedDispatchBase = true;
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] Metal compute dispatch with nonzero base group " +
|
|
||||||
$"({dispatch.BaseGroupX},{dispatch.BaseGroupY},{dispatch.BaseGroupZ}); " +
|
|
||||||
"executing without the base offset.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryGetComputePipeline(device, dispatch.Shader, out var pipeline))
|
|
||||||
{
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandBuffer = BeginBatchedGuestCommands(queue);
|
|
||||||
|
|
||||||
// Pre-resolve textures before the compute encoder opens: snapshot
|
|
||||||
// blits for feedback reads encode into the batch and encoder order
|
|
||||||
// must place them ahead of this dispatch.
|
|
||||||
Span<nint> textureHandles = stackalloc nint[dispatch.Textures.Length];
|
|
||||||
Span<bool> textureOwned = stackalloc bool[dispatch.Textures.Length];
|
|
||||||
for (var index = 0; index < dispatch.Textures.Length; index++)
|
|
||||||
{
|
|
||||||
var descriptor = dispatch.Textures[index];
|
|
||||||
if (descriptor.IsStorage && descriptor.Address != 0)
|
|
||||||
{
|
|
||||||
textureHandles[index] = EnsureStorageImage(device, descriptor)?.Texture ?? 0;
|
|
||||||
textureOwned[index] = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
textureHandles[index] = CreateDrawTexture(
|
|
||||||
device, commandBuffer, descriptor, out var ownedTexture);
|
|
||||||
textureOwned[index] = ownedTexture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoder = MetalNative.Send(commandBuffer, MetalNative.Selector("computeCommandEncoder"));
|
|
||||||
MetalNative.SendVoid(encoder, MetalNative.Selector("setComputePipelineState:"), pipeline);
|
|
||||||
|
|
||||||
var writeBackBuffers = new List<(nint Pointer, GuestMemoryBuffer Guest)>();
|
|
||||||
var selSetBuffer = MetalNative.Selector("setBuffer:offset:atIndex:");
|
|
||||||
var bufferCount = dispatch.GlobalMemoryBuffers.Length;
|
|
||||||
Span<uint> boundBytes = stackalloc uint[Math.Max(bufferCount, 1)];
|
|
||||||
for (var index = 0; index < bufferCount; index++)
|
|
||||||
{
|
|
||||||
var guest = dispatch.GlobalMemoryBuffers[index];
|
|
||||||
var pointer = UploadGlobalBuffer(
|
|
||||||
device, guest, out var buffer, out var offset, out boundBytes[index]);
|
|
||||||
MetalNative.SendSetBuffer(encoder, selSetBuffer, buffer, (nuint)offset, (nuint)index);
|
|
||||||
if (guest.Writable && guest.WriteBackToGuest)
|
|
||||||
{
|
|
||||||
writeBackBuffers.Add((pointer, guest));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SharpEmuUniforms: the dispatch limit clamps the overshoot threads of the
|
|
||||||
// last threadgroup row, then each bound buffer's byte length follows
|
|
||||||
// (including the alignment-bias prefix the shader indexes past).
|
|
||||||
var shader = dispatch.Shader.Shader;
|
|
||||||
var uniforms = AllocateUpload(
|
|
||||||
device,
|
|
||||||
16 + (Math.Max(bufferCount, 1) * sizeof(uint)),
|
|
||||||
out var uniformsBuffer,
|
|
||||||
out var uniformsOffset);
|
|
||||||
WriteDispatchLimit(uniforms, 0, dispatch.ThreadCountX, dispatch.GroupCountX, shader.ThreadgroupSizeX);
|
|
||||||
WriteDispatchLimit(uniforms, 4, dispatch.ThreadCountY, dispatch.GroupCountY, shader.ThreadgroupSizeY);
|
|
||||||
WriteDispatchLimit(uniforms, 8, dispatch.ThreadCountZ, dispatch.GroupCountZ, shader.ThreadgroupSizeZ);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(uniforms[12..], 0);
|
|
||||||
for (var index = 0; index < bufferCount; index++)
|
|
||||||
{
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
uniforms[(16 + (index * sizeof(uint)))..],
|
|
||||||
boundBytes[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind at the stage's declared SharpEmuUniforms slot (see the draw path:
|
|
||||||
// stages compute their own index from globalBufferBase + total count).
|
|
||||||
var uniformsIndex = shader.UniformsBufferIndex;
|
|
||||||
MetalNative.SendSetBuffer(
|
|
||||||
encoder,
|
|
||||||
selSetBuffer,
|
|
||||||
uniformsBuffer,
|
|
||||||
(nuint)uniformsOffset,
|
|
||||||
(nuint)(uniformsIndex >= 0 ? uniformsIndex : bufferCount));
|
|
||||||
|
|
||||||
var selSetTexture = MetalNative.Selector("setTexture:atIndex:");
|
|
||||||
for (var index = 0; index < dispatch.Textures.Length; index++)
|
|
||||||
{
|
|
||||||
var texture = textureHandles[index];
|
|
||||||
if (texture != 0)
|
|
||||||
{
|
|
||||||
MetalNative.SendSetAtIndex(encoder, selSetTexture, texture, (nuint)index);
|
|
||||||
if (textureOwned[index])
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(texture, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Samplers travel in an argument buffer bound at setBuffer (see the draw
|
|
||||||
// path), sidestepping Metal's 16-sampler-per-stage cap.
|
|
||||||
BindSamplerArgumentBuffer(device, encoder, selSetBuffer, dispatch.Shader, dispatch.Textures);
|
|
||||||
|
|
||||||
MetalNative.SendDispatch(
|
|
||||||
encoder,
|
|
||||||
MetalNative.Selector("dispatchThreadgroups:threadsPerThreadgroup:"),
|
|
||||||
new MtlSize
|
|
||||||
{
|
|
||||||
Width = dispatch.GroupCountX,
|
|
||||||
Height = dispatch.GroupCountY,
|
|
||||||
Depth = dispatch.GroupCountZ,
|
|
||||||
},
|
|
||||||
new MtlSize
|
|
||||||
{
|
|
||||||
Width = Math.Max(shader.ThreadgroupSizeX, 1),
|
|
||||||
Height = Math.Max(shader.ThreadgroupSizeY, 1),
|
|
||||||
Depth = Math.Max(shader.ThreadgroupSizeZ, 1),
|
|
||||||
});
|
|
||||||
MetalNative.SendVoid(encoder, MetalNative.Selector("endEncoding"));
|
|
||||||
|
|
||||||
// CPU-visible writes are ordering points (see the draw path): flush
|
|
||||||
// the batch and wait so the write-back lands before this work item
|
|
||||||
// completes. Pure-GPU dispatches stay in the open batch.
|
|
||||||
if (writeBackBuffers.Count > 0)
|
|
||||||
{
|
|
||||||
var committed = FlushBatchedGuestCommands();
|
|
||||||
MetalNative.SendVoid(committed, MetalNative.Selector("waitUntilCompleted"));
|
|
||||||
WriteBuffersBackToGuest(writeBackBuffers);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var descriptor in dispatch.Textures)
|
|
||||||
{
|
|
||||||
if (!descriptor.IsStorage || descriptor.Address == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestImage? image;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_guestImages.TryGetValue(descriptor.Address, out image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image is not null)
|
|
||||||
{
|
|
||||||
image.MarkContentChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The live, shared storage image for a guest address: dispatches,
|
|
||||||
/// draws, blits, and flips of the same address all see one texture.</summary>
|
|
||||||
private static GuestImage? EnsureStorageImage(nint device, GuestDrawTexture descriptor)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_guestImages.TryGetValue(descriptor.Address, out var existing))
|
|
||||||
{
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.Width == 0 || descriptor.Height == 0 ||
|
|
||||||
descriptor.Width > 16384 || descriptor.Height > 16384)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var format = MetalGuestFormats.TryDecodeRenderTargetFormat(
|
|
||||||
descriptor.Format, descriptor.NumberType, out var decoded)
|
|
||||||
? decoded.Format
|
|
||||||
: MtlPixelFormat.Rgba8Unorm;
|
|
||||||
var textureDescriptor = MetalNative.SendTextureDescriptor(
|
|
||||||
MetalNative.Class("MTLTextureDescriptor"),
|
|
||||||
MetalNative.Selector("texture2DDescriptorWithPixelFormat:width:height:mipmapped:"),
|
|
||||||
(nuint)format,
|
|
||||||
descriptor.Width,
|
|
||||||
descriptor.Height,
|
|
||||||
mipmapped: false);
|
|
||||||
MetalNative.Send(
|
|
||||||
textureDescriptor,
|
|
||||||
MetalNative.Selector("setUsage:"),
|
|
||||||
(nint)(UsageShaderRead | UsageShaderWrite | UsageRenderTarget));
|
|
||||||
var image = new GuestImage
|
|
||||||
{
|
|
||||||
Texture = MetalNative.Send(
|
|
||||||
device, MetalNative.Selector("newTextureWithDescriptor:"), textureDescriptor),
|
|
||||||
Width = descriptor.Width,
|
|
||||||
Height = descriptor.Height,
|
|
||||||
Format = format,
|
|
||||||
};
|
|
||||||
if (image.Texture == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytesPerPixel = MetalRenderTargetFormat.GetBytesPerPixel(format);
|
|
||||||
// Snapshot copies arrive in the image's native texel layout; only
|
|
||||||
// 4-byte texels can be RGBA8 verbatim, wider ones carry native bytes.
|
|
||||||
if ((ulong)descriptor.RgbaPixels.Length >= (ulong)descriptor.Width * bytesPerPixel)
|
|
||||||
{
|
|
||||||
var pitch = descriptor.Pitch != 0
|
|
||||||
? Math.Max(descriptor.Pitch, descriptor.Width)
|
|
||||||
: descriptor.Width;
|
|
||||||
ReplaceTextureContents(
|
|
||||||
image.Texture, descriptor.Width, descriptor.Height, descriptor.RgbaPixels, pitch, bytesPerPixel);
|
|
||||||
image.MarkContentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_guestImages.TryGetValue(descriptor.Address, out var raced))
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(image.Texture, MetalNative.Selector("release"));
|
|
||||||
return raced;
|
|
||||||
}
|
|
||||||
|
|
||||||
_guestImages[descriptor.Address] = image;
|
|
||||||
_guestImageExtents[descriptor.Address] =
|
|
||||||
(descriptor.Width, descriptor.Height, (ulong)descriptor.Width * descriptor.Height * bytesPerPixel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetComputePipeline(nint device, MetalCompiledGuestShader shader, out nint pipeline)
|
|
||||||
{
|
|
||||||
lock (_computePipelineCache)
|
|
||||||
{
|
|
||||||
if (_computePipelineCache.TryGetValue(shader, out pipeline))
|
|
||||||
{
|
|
||||||
return pipeline != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var function = GetShaderFunction(device, shader);
|
|
||||||
if (function != 0)
|
|
||||||
{
|
|
||||||
nint error = 0;
|
|
||||||
pipeline = MetalNative.Send(
|
|
||||||
device,
|
|
||||||
MetalNative.Selector("newComputePipelineStateWithFunction:error:"),
|
|
||||||
function,
|
|
||||||
ref error);
|
|
||||||
if (pipeline == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] Metal compute pipeline creation failed: {MetalNative.DescribeError(error)}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _perfPipelineCreations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pipeline = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_computePipelineCache)
|
|
||||||
{
|
|
||||||
_computePipelineCache[shader] = pipeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pipeline != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteDispatchLimit(
|
|
||||||
Span<byte> uniforms,
|
|
||||||
int offset,
|
|
||||||
uint threadCount,
|
|
||||||
uint groupCount,
|
|
||||||
uint threadgroupSize)
|
|
||||||
{
|
|
||||||
var limit = threadCount != uint.MaxValue
|
|
||||||
? threadCount
|
|
||||||
: groupCount * Math.Max(threadgroupSize, 1);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
uniforms[offset..],
|
|
||||||
limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReturnPooledComputeData(ComputeGuestDispatch dispatch)
|
|
||||||
{
|
|
||||||
foreach (var buffer in dispatch.GlobalMemoryBuffers)
|
|
||||||
{
|
|
||||||
if (buffer.Pooled)
|
|
||||||
{
|
|
||||||
GuestDataPool.Shared.Return(buffer.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Feedback reads (draws sampling a live guest render target or depth image)
|
|
||||||
// need a fresh ordered snapshot per draw. Creating and destroying an MTLTexture
|
|
||||||
// — and for depth reads a private staging MTLBuffer — per draw is measurable
|
|
||||||
// CPU and allocator churn at hundreds of feedback draws per second, so both
|
|
||||||
// recycle through a pool with the same lifecycle as the upload arena pages:
|
|
||||||
// acquired snapshots are tagged with the command buffer that samples them at
|
|
||||||
// commit, and return to the free list once that command buffer completes (the
|
|
||||||
// command queue is serial, so the earlier snapshot-blit command buffer is
|
|
||||||
// necessarily complete by then too). Everything here runs on the render thread.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int MaxFreeSnapshotResources = 16;
|
|
||||||
|
|
||||||
private sealed class PooledSnapshotResource
|
|
||||||
{
|
|
||||||
public nint Handle;
|
|
||||||
public bool IsBuffer;
|
|
||||||
|
|
||||||
/// <summary>Texture identity (unused for buffers).</summary>
|
|
||||||
public uint Format;
|
|
||||||
public uint Width;
|
|
||||||
public uint Height;
|
|
||||||
public nint Usage;
|
|
||||||
|
|
||||||
/// <summary>Buffer capacity in bytes (unused for textures).</summary>
|
|
||||||
public nuint Capacity;
|
|
||||||
|
|
||||||
/// <summary>Retained handle of the command buffer that samples this
|
|
||||||
/// snapshot; the resource is reusable once it completes.</summary>
|
|
||||||
public nint LastCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly List<PooledSnapshotResource> _retiredSnapshotResources = [];
|
|
||||||
private static readonly List<PooledSnapshotResource> _pendingSnapshotResources = [];
|
|
||||||
private static readonly List<PooledSnapshotResource> _freeSnapshotResources = [];
|
|
||||||
|
|
||||||
/// <summary>Returns completed snapshot resources to the free list; called
|
|
||||||
/// once per render-loop drain, next to the upload-page recycler.</summary>
|
|
||||||
private static void RecycleCompletedSnapshotResources()
|
|
||||||
{
|
|
||||||
for (var index = _retiredSnapshotResources.Count - 1; index >= 0; index--)
|
|
||||||
{
|
|
||||||
var resource = _retiredSnapshotResources[index];
|
|
||||||
if (resource.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
// MTLCommandBufferStatus: Completed = 4, Error = 5.
|
|
||||||
var status = MetalNative.Send(
|
|
||||||
resource.LastCommandBuffer, MetalNative.Selector("status"));
|
|
||||||
if (status < 4)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MetalNative.SendVoid(resource.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
resource.LastCommandBuffer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_retiredSnapshotResources.RemoveAt(index);
|
|
||||||
if (_freeSnapshotResources.Count < MaxFreeSnapshotResources)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.Add(resource);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(resource.Handle, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pops a pooled snapshot texture matching the exact identity, or
|
|
||||||
/// creates one. The returned handle is owned by the pool — callers must not
|
|
||||||
/// release it, and it must be tagged at the next commit.</summary>
|
|
||||||
private static nint AcquireSnapshotTexture(
|
|
||||||
nint device,
|
|
||||||
MtlPixelFormat format,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
nint usage)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _freeSnapshotResources.Count; index++)
|
|
||||||
{
|
|
||||||
var candidate = _freeSnapshotResources[index];
|
|
||||||
if (!candidate.IsBuffer &&
|
|
||||||
candidate.Format == (uint)format &&
|
|
||||||
candidate.Width == width &&
|
|
||||||
candidate.Height == height &&
|
|
||||||
candidate.Usage == usage)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.RemoveAt(index);
|
|
||||||
_pendingSnapshotResources.Add(candidate);
|
|
||||||
return candidate.Handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var descriptor = MetalNative.SendTextureDescriptor(
|
|
||||||
MetalNative.Class("MTLTextureDescriptor"),
|
|
||||||
MetalNative.Selector("texture2DDescriptorWithPixelFormat:width:height:mipmapped:"),
|
|
||||||
(nuint)format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mipmapped: false);
|
|
||||||
MetalNative.Send(descriptor, MetalNative.Selector("setUsage:"), usage);
|
|
||||||
var handle = MetalNative.Send(
|
|
||||||
device, MetalNative.Selector("newTextureWithDescriptor:"), descriptor);
|
|
||||||
if (handle == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Add(new PooledSnapshotResource
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
Format = (uint)format,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
Usage = usage,
|
|
||||||
});
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pops a pooled private-storage staging buffer of at least
|
|
||||||
/// <paramref name="minimumBytes"/>, or creates one. Pool-owned like
|
|
||||||
/// <see cref="AcquireSnapshotTexture"/>.</summary>
|
|
||||||
private static nint AcquireSnapshotBuffer(nint device, nuint minimumBytes)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _freeSnapshotResources.Count; index++)
|
|
||||||
{
|
|
||||||
var candidate = _freeSnapshotResources[index];
|
|
||||||
if (candidate.IsBuffer && candidate.Capacity >= minimumBytes)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.RemoveAt(index);
|
|
||||||
_pendingSnapshotResources.Add(candidate);
|
|
||||||
return candidate.Handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MTLResourceStorageModePrivate = 32: staging never touches the CPU.
|
|
||||||
var handle = MetalNative.SendNewBuffer(
|
|
||||||
device, MetalNative.Selector("newBufferWithLength:options:"), minimumBytes, 32);
|
|
||||||
if (handle == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Add(new PooledSnapshotResource
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
IsBuffer = true,
|
|
||||||
Capacity = minimumBytes,
|
|
||||||
});
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Marks every snapshot resource acquired since the previous tag
|
|
||||||
/// as owing its lifetime to <paramref name="commandBuffer"/>. Called at the
|
|
||||||
/// same commit sites as <see cref="TagUploadPages"/>; a resource acquired
|
|
||||||
/// for a draw that never committed is tagged by the next commit, which is
|
|
||||||
/// conservative but safe.</summary>
|
|
||||||
private static void TagSnapshotResources(nint commandBuffer)
|
|
||||||
{
|
|
||||||
if (_pendingSnapshotResources.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var resource in _pendingSnapshotResources)
|
|
||||||
{
|
|
||||||
resource.LastCommandBuffer = MetalNative.Send(
|
|
||||||
commandBuffer, MetalNative.Selector("retain"));
|
|
||||||
_retiredSnapshotResources.Add(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Draw textures decoded from guest memory are cached across draws keyed by
|
|
||||||
// their full descriptor identity, mirroring the Vulkan presenter's texture
|
|
||||||
// cache: once an identity is marked cached, the AGC submit thread skips the
|
|
||||||
// guest-memory read/detile/copy entirely (shipping empty texels) and the
|
|
||||||
// render thread serves the cached MTLTexture — for scenes that sample large
|
|
||||||
// textures every draw, that per-draw copy dominated both allocation churn
|
|
||||||
// and CPU time. GuestImageWriteTracker write-protects the source pages, so
|
|
||||||
// a guest CPU write dirties the address and the entry is evicted at the next
|
|
||||||
// drain; the following draw ships fresh texels and re-populates the cache.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int MaxCachedDrawTextures = 2048;
|
|
||||||
|
|
||||||
/// <summary>Render-thread-only cache of decoded draw textures; each value
|
|
||||||
/// holds one retain. Committed command buffers retain the textures they
|
|
||||||
/// reference, so eviction releases immediately without a GPU drain.</summary>
|
|
||||||
private static readonly Dictionary<TextureContentIdentity, nint> _drawTextureCache = new();
|
|
||||||
|
|
||||||
/// <summary>Identities the AGC submit thread may skip texel copies for.
|
|
||||||
/// Read from the submit thread, written by the render thread.</summary>
|
|
||||||
private static readonly ConcurrentDictionary<TextureContentIdentity, byte> _cachedDrawTextureIdentities = new();
|
|
||||||
|
|
||||||
internal static bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
_cachedDrawTextureIdentities.ContainsKey(identity);
|
|
||||||
|
|
||||||
/// <summary>Builds the same identity the AGC layer checks before skipping
|
|
||||||
/// a texel copy; the two must agree field-for-field or skips and cache
|
|
||||||
/// entries would never line up.</summary>
|
|
||||||
private static TextureContentIdentity GetDrawTextureIdentity(GuestDrawTexture texture) => new(
|
|
||||||
texture.Address,
|
|
||||||
texture.Width,
|
|
||||||
texture.Height,
|
|
||||||
texture.Format,
|
|
||||||
texture.NumberType,
|
|
||||||
texture.DstSelect,
|
|
||||||
texture.TileMode,
|
|
||||||
texture.Pitch,
|
|
||||||
texture.Sampler);
|
|
||||||
|
|
||||||
/// <summary>Caching requires the write tracker: without page protection a
|
|
||||||
/// guest CPU write would never evict the entry and draws would sample
|
|
||||||
/// stale texels forever. Storage textures are shader-writable on the GPU,
|
|
||||||
/// so their content identity is not stable either.</summary>
|
|
||||||
private static bool IsCacheableDrawTexture(GuestDrawTexture texture) =>
|
|
||||||
GuestImageWriteTracker.Enabled &&
|
|
||||||
texture.Address != 0 &&
|
|
||||||
!texture.IsStorage &&
|
|
||||||
!texture.IsFallback;
|
|
||||||
|
|
||||||
private static bool TryGetCachedDrawTexture(GuestDrawTexture texture, out nint handle) =>
|
|
||||||
_drawTextureCache.TryGetValue(GetDrawTextureIdentity(texture), out handle);
|
|
||||||
|
|
||||||
private static void CacheDrawTexture(GuestDrawTexture texture, nint handle)
|
|
||||||
{
|
|
||||||
var key = GetDrawTextureIdentity(texture);
|
|
||||||
if (_drawTextureCache.Remove(key, out var previous))
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(previous, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = MetalNative.Send(handle, MetalNative.Selector("retain"));
|
|
||||||
_drawTextureCache[key] = handle;
|
|
||||||
_cachedDrawTextureIdentities[key] = 0;
|
|
||||||
GuestImageWriteTracker.Track(
|
|
||||||
texture.Address,
|
|
||||||
(ulong)texture.RgbaPixels.Length,
|
|
||||||
Volatile.Read(ref _executingGuestWorkSequence),
|
|
||||||
"metal.texture-cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Runs once per drain, before any queued draw executes: a draw
|
|
||||||
/// whose texels the submit thread skipped must never resolve to an entry
|
|
||||||
/// the guest has since rewritten.</summary>
|
|
||||||
private static void EvictDirtyCachedDrawTextures()
|
|
||||||
{
|
|
||||||
if (_drawTextureCache.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict by address rather than by identity: several identities can
|
|
||||||
// share one source address (same texels, different samplers), and
|
|
||||||
// ConsumeDirty clears the flag on first read — evicting only the
|
|
||||||
// first identity would leave the others sampling stale texels.
|
|
||||||
HashSet<ulong>? dirtyAddresses = null;
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
if (dirtyAddresses is not null && dirtyAddresses.Contains(entry.Key.Address))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GuestImageWriteTracker.ConsumeDirty(entry.Key.Address))
|
|
||||||
{
|
|
||||||
(dirtyAddresses ??= []).Add(entry.Key.Address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirtyAddresses is null && _drawTextureCache.Count <= MaxCachedDrawTextures)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_drawTextureCache.Count > MaxCachedDrawTextures)
|
|
||||||
{
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(entry.Value, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawTextureCache.Clear();
|
|
||||||
_cachedDrawTextureIdentities.Clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TextureContentIdentity>? evicted = null;
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
if (dirtyAddresses!.Contains(entry.Key.Address))
|
|
||||||
{
|
|
||||||
(evicted ??= []).Add(entry.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evicted is not null)
|
|
||||||
{
|
|
||||||
foreach (var key in evicted)
|
|
||||||
{
|
|
||||||
if (_drawTextureCache.Remove(key, out var handle))
|
|
||||||
{
|
|
||||||
_cachedDrawTextureIdentities.TryRemove(key, out _);
|
|
||||||
MetalNative.SendVoid(handle, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var address in dirtyAddresses!)
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Rearm(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Self-heal for the skip/eviction race: the submit thread saw a
|
|
||||||
/// cached identity and skipped the copy, but the entry was evicted before
|
|
||||||
/// this draw executed. Read the texels directly rather than rendering a
|
|
||||||
/// fallback texture for the frame, sized with the same block-aware math
|
|
||||||
/// the draw path expects.</summary>
|
|
||||||
private static byte[]? TryReadGuestDrawTexturePixels(GuestDrawTexture texture)
|
|
||||||
{
|
|
||||||
var memory = _guestMemory;
|
|
||||||
if (memory is null || texture.Address == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = Math.Max(texture.Width, 1u);
|
|
||||||
var height = Math.Max(texture.Height, 1u);
|
|
||||||
var rowLength = texture.TileMode == 0
|
|
||||||
? Math.Max(texture.Pitch, width)
|
|
||||||
: width;
|
|
||||||
var format = MetalGuestFormats.DecodeTextureFormat(texture.Format, texture.NumberType);
|
|
||||||
var byteCount = MetalGuestFormats.GetTextureByteCount(format, rowLength, height);
|
|
||||||
if (byteCount == 0 || byteCount > int.MaxValue)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pixels = new byte[(int)byteCount];
|
|
||||||
return memory.TryRead(texture.Address, pixels) ? pixels : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Per-draw upload data (guest global buffers, uniforms, vertex and index
|
|
||||||
// bytes) bump-allocates from shared-storage arena pages bound by offset,
|
|
||||||
// instead of creating one MTLBuffer and one managed copy per binding per
|
|
||||||
// draw — which dominated allocation churn (hundreds of MB/s) and held the
|
|
||||||
// guest flip rate well under the display rate. Pages recycle once the last
|
|
||||||
// command buffer that referenced them reports completion; everything here
|
|
||||||
// runs on the render thread, so no state is locked.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int UploadPageBytes = 8 * 1024 * 1024;
|
|
||||||
|
|
||||||
// Superset of every Metal bind-offset alignment rule (constant address
|
|
||||||
// space on Intel Macs is the strictest at 256), and conveniently the
|
|
||||||
// guest storage-buffer alignment the shader bias contract assumes.
|
|
||||||
private const int UploadAlignment = 256;
|
|
||||||
|
|
||||||
private sealed class UploadPage
|
|
||||||
{
|
|
||||||
public nint Buffer;
|
|
||||||
public nint Contents;
|
|
||||||
public int Capacity;
|
|
||||||
public int Offset;
|
|
||||||
|
|
||||||
/// <summary>Retained handle of the last command buffer that consumed
|
|
||||||
/// data from this page; the page is reusable once it completes.</summary>
|
|
||||||
public nint LastCommandBuffer;
|
|
||||||
|
|
||||||
/// <summary>Stamp of the last TagUploadPages call that saw this page,
|
|
||||||
/// so a commit only re-tags pages it actually touched.</summary>
|
|
||||||
public int TouchStamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly List<UploadPage> _retiredUploadPages = [];
|
|
||||||
private static readonly Stack<UploadPage> _freeUploadPages = new();
|
|
||||||
private static readonly List<UploadPage> _touchedUploadPages = [];
|
|
||||||
private static UploadPage? _currentUploadPage;
|
|
||||||
private static int _uploadTouchStamp;
|
|
||||||
|
|
||||||
/// <summary>Returns completed pages to the free stack. Called once per
|
|
||||||
/// render-loop drain; completion is polled (command buffer status) rather
|
|
||||||
/// than block-based so the ObjC interop stays block-free.</summary>
|
|
||||||
private static void RecycleCompletedUploadPages()
|
|
||||||
{
|
|
||||||
for (var index = _retiredUploadPages.Count - 1; index >= 0; index--)
|
|
||||||
{
|
|
||||||
var page = _retiredUploadPages[index];
|
|
||||||
if (page.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
// MTLCommandBufferStatus: Completed = 4, Error = 5.
|
|
||||||
var status = MetalNative.Send(
|
|
||||||
page.LastCommandBuffer, MetalNative.Selector("status"));
|
|
||||||
if (status < 4)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MetalNative.SendVoid(page.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
page.LastCommandBuffer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_retiredUploadPages.RemoveAt(index);
|
|
||||||
if (page.Capacity == UploadPageBytes)
|
|
||||||
{
|
|
||||||
page.Offset = 0;
|
|
||||||
_freeUploadPages.Push(page);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Oversized one-off allocation; not worth pooling.
|
|
||||||
MetalNative.SendVoid(page.Buffer, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Bump-allocates an aligned slice for CPU-written upload data.
|
|
||||||
/// The returned span is the slice's shared-storage memory; bind the
|
|
||||||
/// buffer at the returned offset.</summary>
|
|
||||||
private static unsafe Span<byte> AllocateUpload(
|
|
||||||
nint device,
|
|
||||||
int length,
|
|
||||||
out nint buffer,
|
|
||||||
out int offset)
|
|
||||||
{
|
|
||||||
var page = _currentUploadPage;
|
|
||||||
var aligned = page is null
|
|
||||||
? 0
|
|
||||||
: (page.Offset + UploadAlignment - 1) & ~(UploadAlignment - 1);
|
|
||||||
if (page is null || aligned + length > page.Capacity)
|
|
||||||
{
|
|
||||||
if (page is not null)
|
|
||||||
{
|
|
||||||
_retiredUploadPages.Add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
page = AcquireUploadPage(device, length);
|
|
||||||
_currentUploadPage = page;
|
|
||||||
aligned = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.TouchStamp != _uploadTouchStamp)
|
|
||||||
{
|
|
||||||
page.TouchStamp = _uploadTouchStamp;
|
|
||||||
_touchedUploadPages.Add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = page.Buffer;
|
|
||||||
offset = aligned;
|
|
||||||
page.Offset = aligned + length;
|
|
||||||
return new Span<byte>((void*)(page.Contents + aligned), length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UploadPage AcquireUploadPage(nint device, int minimumBytes)
|
|
||||||
{
|
|
||||||
if (minimumBytes <= UploadPageBytes && _freeUploadPages.Count > 0)
|
|
||||||
{
|
|
||||||
return _freeUploadPages.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
var capacity = Math.Max(minimumBytes, UploadPageBytes);
|
|
||||||
// Options 0 = MTLResourceStorageModeShared: CPU writes are coherent
|
|
||||||
// and write-backs read the GPU's stores after waitUntilCompleted.
|
|
||||||
var handle = MetalNative.SendNewBuffer(
|
|
||||||
device, MetalNative.Selector("newBufferWithLength:options:"), (nuint)capacity, 0);
|
|
||||||
return new UploadPage
|
|
||||||
{
|
|
||||||
Buffer = handle,
|
|
||||||
Contents = MetalNative.Send(handle, MetalNative.Selector("contents")),
|
|
||||||
Capacity = capacity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Marks every page touched since the previous tag as owing its
|
|
||||||
/// lifetime to <paramref name="commandBuffer"/>. Called after each commit
|
|
||||||
/// that consumed arena data.</summary>
|
|
||||||
private static void TagUploadPages(nint commandBuffer)
|
|
||||||
{
|
|
||||||
if (_touchedUploadPages.Count == 0)
|
|
||||||
{
|
|
||||||
_uploadTouchStamp++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var page in _touchedUploadPages)
|
|
||||||
{
|
|
||||||
if (page.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(page.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
page.LastCommandBuffer = MetalNative.Send(
|
|
||||||
commandBuffer, MetalNative.Selector("retain"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_touchedUploadPages.Clear();
|
|
||||||
_uploadTouchStamp++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,6 @@ namespace SharpEmu.Libs.Gpu.Vulkan;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||||
{
|
{
|
||||||
public string BackendName => "Vulkan";
|
|
||||||
|
|
||||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||||
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||||
|
|
||||||
@@ -345,60 +343,6 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable EnterGuestQueue(string queueName, ulong submissionId) =>
|
|
||||||
VulkanVideoPresenter.EnterGuestQueue(queueName, submissionId);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestAction(Action action, string debugName) =>
|
|
||||||
VulkanVideoPresenter.SubmitOrderedGuestAction(action, debugName);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex) =>
|
|
||||||
VulkanVideoPresenter.SubmitOrderedGuestFlipWait(videoOutHandle, displayBufferIndex);
|
|
||||||
|
|
||||||
public bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite) =>
|
|
||||||
VulkanVideoPresenter.WaitForGuestWork(workSequence, timeoutMilliseconds);
|
|
||||||
|
|
||||||
public long CurrentGuestWorkSequenceForDiagnostics =>
|
|
||||||
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics;
|
|
||||||
|
|
||||||
public bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType) =>
|
|
||||||
VulkanVideoPresenter.IsGuestImageUploadKnown(address, format, numberType);
|
|
||||||
|
|
||||||
public bool GuestImageWantsInitialData(ulong address) =>
|
|
||||||
VulkanVideoPresenter.GuestImageWantsInitialData(address);
|
|
||||||
|
|
||||||
public void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels) =>
|
|
||||||
VulkanVideoPresenter.ProvideGuestImageInitialData(address, rgbaPixels);
|
|
||||||
|
|
||||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
|
||||||
VulkanVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
|
||||||
|
|
||||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
|
||||||
VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
|
||||||
|
|
||||||
public bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount) =>
|
|
||||||
VulkanVideoPresenter.TryGetGuestImageExtent(address, out width, out height, out byteCount);
|
|
||||||
|
|
||||||
public IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents() =>
|
|
||||||
VulkanVideoPresenter.GetGuestImageExtents();
|
|
||||||
|
|
||||||
public bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
VulkanVideoPresenter.IsTextureContentCached(identity);
|
|
||||||
|
|
||||||
public void AttachGuestMemory(SharpEmu.HLE.ICpuMemory memory) =>
|
|
||||||
VulkanVideoPresenter.AttachGuestMemory(memory);
|
|
||||||
|
|
||||||
public ulong GuestStorageBufferOffsetAlignment =>
|
|
||||||
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment;
|
|
||||||
|
|
||||||
public void CountShaderCompilation() =>
|
|
||||||
VulkanVideoPresenter.CountSpirvCompilation();
|
|
||||||
|
|
||||||
public (long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters() =>
|
|
||||||
VulkanVideoPresenter.ReadAndResetPerfCounters();
|
|
||||||
|
|
||||||
public void RequestClose() =>
|
|
||||||
VulkanVideoPresenter.RequestClose();
|
|
||||||
|
|
||||||
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
||||||
shader is VulkanCompiledGuestShader vulkanShader
|
shader is VulkanCompiledGuestShader vulkanShader
|
||||||
? vulkanShader.Spirv
|
? vulkanShader.Spirv
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ public static class KernelExports
|
|||||||
ExportName = "open",
|
ExportName = "open",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx);
|
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "1G3lF1Gg1k8",
|
Nid = "1G3lF1Gg1k8",
|
||||||
@@ -376,7 +376,7 @@ public static class KernelExports
|
|||||||
ExportName = "fstat",
|
ExportName = "fstat",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx);
|
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -65,10 +65,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
private const uint HostPageExecuteReadWrite = 0x40;
|
private const uint HostPageExecuteReadWrite = 0x40;
|
||||||
private const uint HostPageExecuteWriteCopy = 0x80;
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
||||||
private const uint HostPageGuard = 0x100;
|
private const uint HostPageGuard = 0x100;
|
||||||
private const int Enoent = 2;
|
|
||||||
private const int Ebadf = 9;
|
|
||||||
private const int Enomem = 12;
|
private const int Enomem = 12;
|
||||||
private const int Eacces = 13;
|
|
||||||
private const int Efault = 14;
|
private const int Efault = 14;
|
||||||
private const int Einval = 22;
|
private const int Einval = 22;
|
||||||
private const int Erange = 34;
|
private const int Erange = 34;
|
||||||
@@ -227,21 +224,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Removes a guest mount registered by <see cref="RegisterGuestPathMount"/>.</summary>
|
|
||||||
public static bool UnregisterGuestPathMount(string guestMountPoint)
|
|
||||||
{
|
|
||||||
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
|
||||||
if (normalizedMountPoint is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_guestMountGate)
|
|
||||||
{
|
|
||||||
return _guestMounts.Remove(normalizedMountPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryAllocateHleData(
|
internal static bool TryAllocateHleData(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong length,
|
ulong length,
|
||||||
@@ -1108,16 +1090,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count > 0 && !ctx.Memory.TryCopy(destination, source, (ulong)count))
|
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||||
|
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||||
{
|
{
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
if (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1548,13 +1526,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "close",
|
ExportName = "close",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixClose(CpuContext ctx)
|
public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
||||||
{
|
|
||||||
var result = KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "UK2Tl2DWUns",
|
Nid = "UK2Tl2DWUns",
|
||||||
@@ -1611,29 +1583,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translates a failed raw Orbis kernel result into the libc/POSIX ABI:
|
|
||||||
// return -1 with errno set. The raw sceKernel* implementations report the
|
|
||||||
// 0x8002xxxx sentinel through the return value, but the POSIX-named exports
|
|
||||||
// (open/fstat/close/read/write/stat) are called by libc code that expects a
|
|
||||||
// negative result on failure. Leaking the raw sentinel makes callers store
|
|
||||||
// it as a "valid" fd or handle and later dereference it - the null-pointer
|
|
||||||
// access violation seen when Unity's IL2CPP file layer probes an absent
|
|
||||||
// il2cpp.usym. fd-based calls pass notFoundErrno=Ebadf; path-based calls
|
|
||||||
// leave the Enoent default.
|
|
||||||
private static int PosixFailure(CpuContext ctx, int orbisResult, int notFoundErrno = Enoent)
|
|
||||||
{
|
|
||||||
var errno = orbisResult switch
|
|
||||||
{
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => Eacces,
|
|
||||||
_ => notFoundErrno,
|
|
||||||
};
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
|
||||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "E6ao34wPw+U",
|
Nid = "E6ao34wPw+U",
|
||||||
ExportName = "stat",
|
ExportName = "stat",
|
||||||
@@ -1642,29 +1591,23 @@ public static partial class KernelMemoryCompatExports
|
|||||||
public static int PosixStat(CpuContext ctx)
|
public static int PosixStat(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var result = KernelStat(ctx);
|
var result = KernelStat(ctx);
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
? 0
|
{
|
||||||
: PosixFailure(ctx, result);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
// stat(2) follows the libc/POSIX ABI: failures return -1 and expose
|
||||||
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
|
// the reason through errno. Returning the raw Orbis kernel code here
|
||||||
// prefers a written RAX over the return value), so returning 0 is correct.
|
// makes callers treat a missing file as a non-negative success value.
|
||||||
public static int PosixOpen(CpuContext ctx)
|
var errno = result switch
|
||||||
{
|
{
|
||||||
var result = KernelOpenUnderscore(ctx);
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
||||||
? 0
|
_ => 2, // ENOENT
|
||||||
: PosixFailure(ctx, result);
|
};
|
||||||
}
|
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
||||||
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
// POSIX fstat(2): a bad fd maps to EBADF rather than the path-oriented ENOENT.
|
return -1;
|
||||||
public static int PosixFstat(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var result = KernelFstat(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1678,7 +1621,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
var count = ctx[CpuRegister.Rsi];
|
var count = ctx[CpuRegister.Rsi];
|
||||||
var idsAddress = ctx[CpuRegister.Rdx];
|
var idsAddress = ctx[CpuRegister.Rdx];
|
||||||
var sizesAddress = ctx[CpuRegister.Rcx];
|
var sizesAddress = ctx[CpuRegister.Rcx];
|
||||||
var errorIndexAddress = ctx[CpuRegister.R8];
|
|
||||||
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
|
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
@@ -1703,8 +1645,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
var hostPath = ResolveGuestPath(guestPath);
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
if (!TryGetAprFileSize(hostPath, out var fileSize))
|
if (!TryGetAprFileSize(hostPath, out var fileSize))
|
||||||
{
|
{
|
||||||
// Stop at the first miss and report its index.
|
// Per-file resolve: a missing entry gets an invalid id
|
||||||
// The caller can then use its normal file-open fallback.
|
// (0xFFFFFFFF, already written above) and size 0, and the batch
|
||||||
|
// CONTINUES. Aborting the whole batch on the first miss left the
|
||||||
|
// remaining paths unresolved and could stall the guest's asset
|
||||||
|
// streaming when a batch happens to include an absent (e.g.
|
||||||
|
// patch/DLC) file; the caller checks per-file id/size.
|
||||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||||
if (sizesAddress != 0 &&
|
if (sizesAddress != 0 &&
|
||||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||||
@@ -1713,16 +1659,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorIndexAddress != 0 &&
|
continue;
|
||||||
!TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i))
|
|
||||||
{
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT
|
|
||||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
||||||
@@ -2140,15 +2077,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "read",
|
ExportName = "read",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixRead(CpuContext ctx)
|
public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx);
|
||||||
{
|
|
||||||
// On success KernelReadUnderscore writes the byte count into RAX, which
|
|
||||||
// the import bridge prefers over this return value.
|
|
||||||
var result = KernelReadUnderscore(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Cg4srZ6TKbU",
|
Nid = "Cg4srZ6TKbU",
|
||||||
@@ -2347,15 +2276,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "write",
|
ExportName = "write",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixWrite(CpuContext ctx)
|
public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx);
|
||||||
{
|
|
||||||
// On success KernelWriteUnderscore writes the byte count into RAX, which
|
|
||||||
// the import bridge prefers over this return value.
|
|
||||||
var result = KernelWriteUnderscore(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "4wSze92BhLI",
|
Nid = "4wSze92BhLI",
|
||||||
@@ -2390,37 +2311,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "smIj7eqzZE8",
|
|
||||||
ExportName = "clock_getres",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int ClockGetres(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
|
||||||
|
|
||||||
// POSIX allows a null resolution pointer: the call then only validates
|
|
||||||
// the clock id, which every id a title passes here does.
|
|
||||||
if (timespecAddress == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clock_gettime above is backed by DateTimeOffset.UtcNow, whose tick is
|
|
||||||
// 100 ns, so that is the honest resolution to report rather than the 1 ns
|
|
||||||
// a caller might otherwise assume it can rely on.
|
|
||||||
const ulong ResolutionNanoseconds = 100;
|
|
||||||
if (!ctx.TryWriteUInt64(timespecAddress, 0) ||
|
|
||||||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), ResolutionNanoseconds))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "vNe1w4diLCs",
|
Nid = "vNe1w4diLCs",
|
||||||
ExportName = "__tls_get_addr",
|
ExportName = "__tls_get_addr",
|
||||||
@@ -2921,51 +2811,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelMapDirectMemory(CpuContext ctx)
|
public static int KernelMapDirectMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return MapDirectMemoryCore(
|
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
||||||
ctx,
|
var length = ctx[CpuRegister.Rsi];
|
||||||
inOutAddressPointer: ctx[CpuRegister.Rdi],
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
length: ctx[CpuRegister.Rsi],
|
var flags = ctx[CpuRegister.Rcx];
|
||||||
protection: unchecked((int)ctx[CpuRegister.Rdx]),
|
var directMemoryStart = ctx[CpuRegister.R8];
|
||||||
flags: ctx[CpuRegister.Rcx],
|
var alignment = ctx[CpuRegister.R9];
|
||||||
directMemoryStart: ctx[CpuRegister.R8],
|
|
||||||
alignment: ctx[CpuRegister.R9]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "BQQniolj9tQ",
|
|
||||||
ExportName = "sceKernelMapDirectMemory2",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelMapDirectMemory2(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// The "2" variant inserts a memoryType argument (rdx) ahead of v1's
|
|
||||||
// protection, shifting protection/flags/directMemoryStart down one
|
|
||||||
// register each and pushing alignment onto the stack (the 7th argument,
|
|
||||||
// at [rsp + 8], above the return address). The memoryType only selects
|
|
||||||
// cache/GPU access attributes, which this HLE does not model per
|
|
||||||
// mapping, so it is accepted but does not affect placement.
|
|
||||||
ulong alignment = 0;
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out alignment);
|
|
||||||
|
|
||||||
return MapDirectMemoryCore(
|
|
||||||
ctx,
|
|
||||||
inOutAddressPointer: ctx[CpuRegister.Rdi],
|
|
||||||
length: ctx[CpuRegister.Rsi],
|
|
||||||
protection: unchecked((int)ctx[CpuRegister.Rcx]),
|
|
||||||
flags: ctx[CpuRegister.R8],
|
|
||||||
directMemoryStart: ctx[CpuRegister.R9],
|
|
||||||
alignment: alignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int MapDirectMemoryCore(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong inOutAddressPointer,
|
|
||||||
ulong length,
|
|
||||||
int protection,
|
|
||||||
ulong flags,
|
|
||||||
ulong directMemoryStart,
|
|
||||||
ulong alignment)
|
|
||||||
{
|
|
||||||
if (ShouldTraceDirectMemory())
|
if (ShouldTraceDirectMemory())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -3078,7 +2929,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestWriteWatch.OnDirectMapping(mappedAddress, length, protection);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3461,7 +3311,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
public static int KernelDirectMemoryQuery(CpuContext ctx)
|
public static int KernelDirectMemoryQuery(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var offset = ctx[CpuRegister.Rdi];
|
var offset = ctx[CpuRegister.Rdi];
|
||||||
var flags = ctx[CpuRegister.Rsi];
|
_ = ctx[CpuRegister.Rsi]; // flags
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
var infoSize = ctx[CpuRegister.Rcx];
|
var infoSize = ctx[CpuRegister.Rcx];
|
||||||
if (infoAddress == 0 || infoSize < 24)
|
if (infoAddress == 0 || infoSize < 24)
|
||||||
@@ -3469,90 +3319,27 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset >= DirectMemorySizeBytes)
|
|
||||||
{
|
|
||||||
// Real hardware returns EACCES here (0x8002000D), not ENOENT.
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
|
||||||
}
|
|
||||||
|
|
||||||
var findNext = (flags & 1) != 0;
|
|
||||||
var found = false;
|
|
||||||
var matchStart = 0UL;
|
|
||||||
var matchEnd = 0UL;
|
|
||||||
var matchMemoryType = 0;
|
|
||||||
|
|
||||||
lock (_memoryGate)
|
lock (_memoryGate)
|
||||||
{
|
{
|
||||||
var candidates = _directAllocations.Values
|
foreach (var block in _directAllocations.Values)
|
||||||
.Where(block => findNext
|
|
||||||
? block.Start + block.Length > offset
|
|
||||||
: offset >= block.Start && offset < block.Start + block.Length)
|
|
||||||
.OrderBy(block => block.Start);
|
|
||||||
|
|
||||||
foreach (var block in candidates)
|
|
||||||
{
|
{
|
||||||
found = true;
|
if (offset < block.Start || offset >= block.Start + block.Length)
|
||||||
matchStart = block.Start;
|
{
|
||||||
matchEnd = block.Start + block.Length;
|
continue;
|
||||||
matchMemoryType = block.MemoryType;
|
}
|
||||||
break;
|
|
||||||
|
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||||
|
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
||||||
|
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found)
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(infoAddress, matchStart) ||
|
|
||||||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), matchEnd) ||
|
|
||||||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), matchMemoryType))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="KernelMprotect"/>; identical (addr, len, prot)
|
|
||||||
/// argument order. Imported by libcohtml, whose embedded V8 changes page
|
|
||||||
/// permissions through this name when moving JIT pages between writable and
|
|
||||||
/// executable.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "YQOfxL4QfeU",
|
|
||||||
ExportName = "mprotect",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixMprotect(CpuContext ctx) => KernelMprotect(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="KernelMunmap"/>; identical (addr, len) argument
|
|
||||||
/// order.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "UqDGjXA5yUM",
|
|
||||||
ExportName = "munmap",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixMunmap(CpuContext ctx) => KernelMunmap(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reports the 16 KiB page granularity this backend maps and aligns against
|
|
||||||
/// (<see cref="OrbisPageSize"/>), not the host's 4 KiB. An allocator that
|
|
||||||
/// rounded to the host value would hand back sub-page offsets that every
|
|
||||||
/// mapping call here then rejects for misalignment.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "k+AXqu2-eBc",
|
|
||||||
ExportName = "getpagesize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixGetPageSize(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = OrbisPageSize;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -4685,8 +4472,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
allowSearch: false,
|
allowSearch: false,
|
||||||
allowAllocateAtAlternative: false,
|
allowAllocateAtAlternative: false,
|
||||||
"reserve fixed range",
|
"reserve fixed range",
|
||||||
out _,
|
out _);
|
||||||
backPartialOverlap: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
||||||
@@ -4887,7 +4673,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
|
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
|
||||||
!guestPath.StartsWith("\\", StringComparison.Ordinal))
|
!guestPath.StartsWith("\\", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var relative = NormalizeMountRelativePath(guestPath);
|
var relative = guestPath.Replace('/', Path.DirectorySeparatorChar);
|
||||||
return Path.Combine(app0Root, relative);
|
return Path.Combine(app0Root, relative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4962,40 +4748,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return _cachedApp0Root;
|
return _cachedApp0Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves "." and ".." inside a mount-relative guest path and clamps the
|
|
||||||
// result at the mount root, so a guest path can never escape into the host
|
|
||||||
// filesystem. Unreal Engine titles depend on this: their base directory is
|
|
||||||
// <app>/binaries/<platform>, so they address content with "../../../"
|
|
||||||
// prefixes that land back inside /app0 on real hardware. Combining those
|
|
||||||
// raw against the app0 root walked out of the game folder entirely, so the
|
|
||||||
// title enumerated an unrelated host directory and never found its .pak files.
|
|
||||||
private static string NormalizeMountRelativePath(string relativePath)
|
private static string NormalizeMountRelativePath(string relativePath)
|
||||||
{
|
{
|
||||||
var segments = relativePath.Split(
|
return relativePath
|
||||||
new[] { '/', '\\' },
|
.TrimStart('/', '\\')
|
||||||
StringSplitOptions.RemoveEmptyEntries);
|
.Replace('/', Path.DirectorySeparatorChar)
|
||||||
var resolved = new List<string>(segments.Length);
|
.Replace('\\', Path.DirectorySeparatorChar);
|
||||||
foreach (var segment in segments)
|
|
||||||
{
|
|
||||||
if (segment == ".")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment == "..")
|
|
||||||
{
|
|
||||||
if (resolved.Count > 0)
|
|
||||||
{
|
|
||||||
resolved.RemoveAt(resolved.Count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved.Add(segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join(Path.DirectorySeparatorChar, resolved);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveDevlogAppRoot()
|
private static string ResolveDevlogAppRoot()
|
||||||
@@ -5818,74 +5576,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts <paramref name="replacement"/> into the tracked-region table,
|
|
||||||
/// carving it out of any regions it overlaps and preserving the parts of
|
|
||||||
/// those regions that fall outside it.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The table is a SortedList keyed by start address, so assigning directly
|
|
||||||
/// destroys any existing entry that happens to share a start address. That
|
|
||||||
/// silently discarded enclosing reservations: a title reserves a large range
|
|
||||||
/// and then commits a small mapping at the same base, and the record of
|
|
||||||
/// everything past the small mapping disappears — leaving sceKernelVirtualQuery
|
|
||||||
/// unable to find memory the guest legitimately owns.
|
|
||||||
///
|
|
||||||
/// Carving also keeps the table non-overlapping. Previously a new region
|
|
||||||
/// starting inside an existing one produced two overlapping entries, which
|
|
||||||
/// the ordered scan in TryFindVirtualQueryRegionLocked is not written to
|
|
||||||
/// expect.
|
|
||||||
/// </remarks>
|
|
||||||
private static void ReplaceMappedRegionRangeLocked(MappedRegion replacement)
|
|
||||||
{
|
|
||||||
if (replacement.Length == 0 ||
|
|
||||||
!TryAddU64(replacement.Address, replacement.Length, out var replacementEnd))
|
|
||||||
{
|
|
||||||
_mappedRegions[replacement.Address] = replacement;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = replacement.Address;
|
|
||||||
List<MappedRegion>? overlapping = null;
|
|
||||||
foreach (var region in _mappedRegions.Values)
|
|
||||||
{
|
|
||||||
if (region.Length == 0 ||
|
|
||||||
!TryAddU64(region.Address, region.Length, out var regionEnd))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region.Address < replacementEnd && regionEnd > start)
|
|
||||||
{
|
|
||||||
(overlapping ??= []).Add(region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlapping is not null)
|
|
||||||
{
|
|
||||||
foreach (var region in overlapping)
|
|
||||||
{
|
|
||||||
_mappedRegions.Remove(region.Address);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var region in overlapping)
|
|
||||||
{
|
|
||||||
var regionEnd = region.Address + region.Length;
|
|
||||||
if (region.Address < start)
|
|
||||||
{
|
|
||||||
AddMappedRegionSliceLocked(region, region.Address, start, region.Protection);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regionEnd > replacementEnd)
|
|
||||||
{
|
|
||||||
AddMappedRegionSliceLocked(region, replacementEnd, regionEnd, region.Protection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mappedRegions[start] = replacement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddMappedRegionSliceLocked(
|
private static void AddMappedRegionSliceLocked(
|
||||||
MappedRegion source,
|
MappedRegion source,
|
||||||
ulong start,
|
ulong start,
|
||||||
@@ -6304,7 +5994,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return highWaterMark;
|
return highWaterMark;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe bool TryReadHostMemory(ulong address, Span<byte> destination)
|
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
|
||||||
{
|
{
|
||||||
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
|
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
|
||||||
{
|
{
|
||||||
@@ -6313,7 +6003,9 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
|
var temporary = new byte[destination.Length];
|
||||||
|
Marshal.Copy((nint)address, temporary, 0, temporary.Length);
|
||||||
|
temporary.AsSpan().CopyTo(destination);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6353,41 +6045,6 @@ 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)
|
||||||
@@ -6667,7 +6324,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return value != 0 && (value & (value - 1)) == 0;
|
return value != 0 && (value & (value - 1)) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
|
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
|
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
|
||||||
{
|
{
|
||||||
@@ -6676,7 +6333,8 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
source.CopyTo(new Span<byte>((void*)address, source.Length));
|
var temporary = source.ToArray();
|
||||||
|
Marshal.Copy(temporary, 0, (nint)address, temporary.Length);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6703,37 +6361,20 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var endAddress = address + length - 1;
|
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
||||||
var currentAddress = address;
|
|
||||||
while (currentAddress <= endAddress)
|
|
||||||
{
|
{
|
||||||
if (!TryQueryHostPage(currentAddress, out var info) ||
|
return false;
|
||||||
!HasRequiredProtection(info.Protect, writeAccess))
|
}
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var regionBase = unchecked((ulong)info.BaseAddress);
|
var endAddress = address + length - 1;
|
||||||
var regionSize = (ulong)info.RegionSize;
|
if (endAddress == address)
|
||||||
if (regionSize == 0 ||
|
{
|
||||||
regionBase > currentAddress ||
|
return true;
|
||||||
ulong.MaxValue - regionBase < regionSize)
|
}
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var regionEnd = regionBase + regionSize;
|
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
||||||
if (regionEnd <= currentAddress)
|
{
|
||||||
{
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regionEnd > endAddress)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAddress = regionEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -41,87 +41,18 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
private long _ownerThreadId;
|
public ulong OwnerThreadId { get; set; }
|
||||||
private int _recursionCount;
|
public int RecursionCount { get; set; }
|
||||||
private int _queuedWaiterCount;
|
|
||||||
|
|
||||||
public Lock SyncRoot { get; } = new();
|
|
||||||
public ulong OwnerThreadId
|
|
||||||
{
|
|
||||||
get => unchecked((ulong)Volatile.Read(ref _ownerThreadId));
|
|
||||||
set => Volatile.Write(ref _ownerThreadId, unchecked((long)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RecursionCount
|
|
||||||
{
|
|
||||||
get => Volatile.Read(ref _recursionCount);
|
|
||||||
set => Volatile.Write(ref _recursionCount, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount);
|
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
||||||
|
|
||||||
public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge)
|
|
||||||
{
|
|
||||||
if (!allowWaiterBarge && QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryAcquireOwner(threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAcquireOwner(ulong threadId)
|
|
||||||
{
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
unchecked((long)threadId),
|
|
||||||
0) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryReleaseUncontended(ulong threadId)
|
|
||||||
{
|
|
||||||
if (QueuedWaiterCount != 0 || RecursionCount != 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 0);
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
0,
|
|
||||||
unchecked((long)threadId)) == unchecked((long)threadId))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount);
|
|
||||||
|
|
||||||
public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount);
|
|
||||||
|
|
||||||
public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount);
|
|
||||||
|
|
||||||
public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
public required string WakeKey { get; init; }
|
public required string WakeKey { get; init; }
|
||||||
public required bool Cooperative { get; set; }
|
public required bool Cooperative { get; init; }
|
||||||
public ManualResetEventSlim? HostSignal { get; set; }
|
|
||||||
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||||
public int Granted;
|
public int Granted;
|
||||||
}
|
}
|
||||||
@@ -163,10 +94,7 @@ public static class KernelPthreadCompatExports
|
|||||||
public static int PthreadSelf(CpuContext ctx)
|
public static int PthreadSelf(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
|
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||||
{
|
|
||||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
|
||||||
}
|
|
||||||
ctx[CpuRegister.Rax] = currentThreadHandle;
|
ctx[CpuRegister.Rax] = currentThreadHandle;
|
||||||
TracePthreadSelf(ctx, currentThreadHandle);
|
TracePthreadSelf(ctx, currentThreadHandle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -211,13 +139,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "B5GmVDKwpn0",
|
|
||||||
ExportName = "pthread_yield",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "GBUY7ywdULE",
|
Nid = "GBUY7ywdULE",
|
||||||
ExportName = "scePthreadRename",
|
ExportName = "scePthreadRename",
|
||||||
@@ -650,30 +571,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadOnce"/>. libKernel exports the
|
|
||||||
/// same routine under two NIDs, and shipped middleware links the plain name:
|
|
||||||
/// DOOM's libcohtml, PlayFab and party modules all import this one rather
|
|
||||||
/// than scePthreadOnce.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Z4QosVuAsA0",
|
|
||||||
ExportName = "pthread_once",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadRename"/>, following the same
|
|
||||||
/// two-NID pattern as <see cref="PthreadOncePOSIX"/>.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "9vyP6Z7bqzc",
|
|
||||||
ExportName = "pthread_rename_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx);
|
|
||||||
|
|
||||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
{
|
{
|
||||||
if (mutexAddress == 0)
|
if (mutexAddress == 0)
|
||||||
@@ -724,7 +621,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -756,63 +653,11 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
if (state.Type == MutexTypeRecursive)
|
|
||||||
{
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
var adaptiveResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult);
|
|
||||||
return adaptiveResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
|
||||||
return ownedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var canCooperativelyBlock = !tryOnly &&
|
var canCooperativelyBlock = !tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||||
PthreadMutexWaiter? waiter = null;
|
PthreadMutexWaiter? waiter = null;
|
||||||
var acquiredWhileQueueing = false;
|
lock (state)
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
@@ -823,30 +668,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gen5 runtime wrappers can layer an adaptive lock call over
|
|
||||||
// scePthreadMutexLock for one logical acquisition, followed by
|
|
||||||
// only one unlock. Keep the duplicate acquisition idempotent so
|
|
||||||
// the matching unlock fully releases the HLE mutex.
|
|
||||||
TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
{
|
||||||
if (tryOnly)
|
if (tryOnly)
|
||||||
{
|
{
|
||||||
@@ -855,7 +677,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
||||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
// over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here
|
||||||
// leaves that guest bookkeeping out of sync with the HLE owner and
|
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||||
// the compatibility recursion used by the original implementation;
|
// the compatibility recursion used by the original implementation;
|
||||||
@@ -874,17 +696,10 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pthread_mutex_trylock succeeds whenever the mutex is not currently
|
if (state.OwnerThreadId == 0 && state.Waiters.Count == 0)
|
||||||
// held; unlike the blocking lock it does not queue behind waiters
|
|
||||||
// (POSIX gives it no fairness obligation). Gating trylock on an empty
|
|
||||||
// wait queue is wrong and, worse, lets a single stale/undrainable
|
|
||||||
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
|
||||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
|
||||||
// blocked waiters are not starved by a barging locker.
|
|
||||||
if (state.OwnerThreadId == 0 &&
|
|
||||||
(tryOnly || state.Waiters.Count == 0) &&
|
|
||||||
state.TryAcquireOwner(currentThreadId))
|
|
||||||
{
|
{
|
||||||
|
state.OwnerThreadId = currentThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -896,14 +711,6 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
||||||
acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acquiredWhileQueueing)
|
|
||||||
{
|
|
||||||
waiter!.HostSignal?.Dispose();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCooperativelyBlock && waiter is not null &&
|
if (canCooperativelyBlock && waiter is not null &&
|
||||||
@@ -937,29 +744,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
string? nextWakeKey = null;
|
||||||
{
|
lock (state)
|
||||||
if (state.RecursionCount > 1)
|
|
||||||
{
|
|
||||||
state.DecrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.TryReleaseUncontended(currentThreadId))
|
|
||||||
{
|
|
||||||
if (state.QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
WakeFirstMutexWaiter(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PthreadMutexWaiter? nextWaiter = null;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
@@ -977,29 +763,16 @@ public static class KernelPthreadCompatExports
|
|||||||
if (state.RecursionCount == 0)
|
if (state.RecursionCount == 0)
|
||||||
{
|
{
|
||||||
state.OwnerThreadId = 0;
|
state.OwnerThreadId = 0;
|
||||||
|
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||||
// Hand the mutex directly to the head waiter instead of only
|
? state.Waiters.First.Value.WakeKey
|
||||||
// waking it and relying on it to re-acquire. A woken waiter that
|
: null;
|
||||||
// fails to self-grant (its wake races or is lost) would leave the
|
Monitor.PulseAll(state);
|
||||||
// mutex "free with a queued waiter"; the fast-acquire path refuses
|
|
||||||
// such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every
|
|
||||||
// later locker — including the game's main thread — then queues
|
|
||||||
// behind a head that never advances and the process wedges.
|
|
||||||
if (state.Waiters.First is { } headNode &&
|
|
||||||
TryGrantMutexWaiterLocked(state, headNode.Value))
|
|
||||||
{
|
|
||||||
nextWaiter = headNode.Value;
|
|
||||||
if (!nextWaiter.Cooperative)
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
if (nextWakeKey is not null)
|
||||||
{
|
{
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
@@ -1459,22 +1232,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
lock (mutexState.SyncRoot)
|
lock (mutexState)
|
||||||
{
|
{
|
||||||
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
|
||||||
{
|
|
||||||
// The guest holds the mutex through a path our host-side tracking
|
|
||||||
// never observed — most commonly libkernel's uncontended userspace
|
|
||||||
// fast-path, which locks the mutex word directly without an HLE
|
|
||||||
// call. Real pthread_cond_wait requires the caller to own the
|
|
||||||
// mutex and does not verify it for normal mutexes, so returning
|
|
||||||
// EPERM here is wrong: it spins the guest and, worse, leaves the
|
|
||||||
// mutex held (the unlock below is skipped), wedging every thread
|
|
||||||
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
|
||||||
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
|
||||||
_ = mutexState.TryAcquireOwner(currentThreadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||||
{
|
{
|
||||||
return mutexState.OwnerThreadId == currentThreadId
|
return mutexState.OwnerThreadId == currentThreadId
|
||||||
@@ -1626,32 +1385,6 @@ public static class KernelPthreadCompatExports
|
|||||||
bool cooperative,
|
bool cooperative,
|
||||||
string? wakeKey = null)
|
string? wakeKey = null)
|
||||||
{
|
{
|
||||||
// A guest thread can have at most one pending acquisition on a mutex —
|
|
||||||
// it is either running or blocked on exactly one wait. If a waiter for
|
|
||||||
// this thread is still queued when it comes back for a fresh
|
|
||||||
// acquisition, that entry is a stale leftover the thread abandoned
|
|
||||||
// (most often a cond_timedwait timeout whose re-acquire hand-off was
|
|
||||||
// lost). Stale entries clog the FIFO head with waiters no thread is
|
|
||||||
// blocked on, so the unlock hand-off wakes a dead wake-key and the
|
|
||||||
// mutex wedges permanently (observed deadlocking Hades: several
|
|
||||||
// re-acquire waiters from one thread piled ahead of a live locker).
|
|
||||||
// Prune any prior entry for this thread before enqueueing the new one.
|
|
||||||
if (threadId != 0)
|
|
||||||
{
|
|
||||||
for (var node = state.Waiters.First; node is not null;)
|
|
||||||
{
|
|
||||||
var next = node.Next;
|
|
||||||
if (node.Value.ThreadId == threadId)
|
|
||||||
{
|
|
||||||
state.Waiters.Remove(node);
|
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
node.Value.Node = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var waiter = new PthreadMutexWaiter
|
var waiter = new PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
ThreadId = threadId,
|
ThreadId = threadId,
|
||||||
@@ -1659,10 +1392,8 @@ public static class KernelPthreadCompatExports
|
|||||||
WakeKey = cooperative
|
WakeKey = cooperative
|
||||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
|
|
||||||
};
|
};
|
||||||
waiter.Node = state.Waiters.AddLast(waiter);
|
waiter.Node = state.Waiters.AddLast(waiter);
|
||||||
state.WaiterAddedLocked();
|
|
||||||
return waiter;
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1403,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var mutex = new PthreadMutexState();
|
var mutex = new PthreadMutexState();
|
||||||
PthreadMutexWaiter first;
|
PthreadMutexWaiter first;
|
||||||
PthreadMutexWaiter second;
|
PthreadMutexWaiter second;
|
||||||
lock (mutex.SyncRoot)
|
lock (mutex)
|
||||||
{
|
{
|
||||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||||
@@ -1716,72 +1447,26 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.TryAcquireOwner(waiter.ThreadId))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Waiters.Remove(waiter.Node);
|
state.Waiters.Remove(waiter.Node);
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
waiter.Node = null;
|
waiter.Node = null;
|
||||||
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
Volatile.Write(ref waiter.Granted, 1);
|
Volatile.Write(ref waiter.Granted, 1);
|
||||||
|
Monitor.PulseAll(state);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WakeFirstMutexWaiter(PthreadMutexState state)
|
|
||||||
{
|
|
||||||
PthreadMutexWaiter? nextWaiter;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
|
||||||
if (state.OwnerThreadId != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWaiter = state.Waiters.First?.Value;
|
|
||||||
if (nextWaiter is { Cooperative: false })
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
|
||||||
{
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
ManualResetEventSlim? hostSignal = null;
|
lock (state)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while (true)
|
while (!TryGrantMutexWaiterLocked(state, waiter))
|
||||||
{
|
{
|
||||||
lock (state.SyncRoot)
|
Monitor.Wait(state);
|
||||||
{
|
|
||||||
if (waiter.HostSignal is null)
|
|
||||||
{
|
|
||||||
waiter.Cooperative = false;
|
|
||||||
waiter.HostSignal = new ManualResetEventSlim(initialState: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal = waiter.HostSignal;
|
|
||||||
if (TryGrantMutexWaiterLocked(state, waiter))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
hostSignal?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGrantBlockedMutexLock(
|
private static bool TryGrantBlockedMutexLock(
|
||||||
@@ -1792,7 +1477,7 @@ public static class KernelPthreadCompatExports
|
|||||||
PthreadMutexWaiter waiter)
|
PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
var granted = false;
|
var granted = false;
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||||
}
|
}
|
||||||
@@ -1826,10 +1511,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
|
|
||||||
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
|
|
||||||
guestOwner == currentThreadId;
|
|
||||||
|
|
||||||
private static bool CompleteCondWaiterLocked(
|
private static bool CompleteCondWaiterLocked(
|
||||||
PthreadCondState state,
|
PthreadCondState state,
|
||||||
PthreadCondWaiter waiter,
|
PthreadCondWaiter waiter,
|
||||||
@@ -1845,7 +1526,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waiter.TimeoutTimer?.Dispose();
|
waiter.TimeoutTimer?.Dispose();
|
||||||
waiter.TimeoutTimer = null;
|
waiter.TimeoutTimer = null;
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||||
waiter.MutexState,
|
waiter.MutexState,
|
||||||
@@ -1893,7 +1574,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -860,18 +860,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
|
|
||||||
/// exports the same routine under two NIDs; middleware compiled against the
|
|
||||||
/// plain POSIX headers links this one rather than scePthreadAttrGetschedparam.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "qlk9pSLsUmM",
|
|
||||||
ExportName = "pthread_attr_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "FXPWHNk8Of0",
|
Nid = "FXPWHNk8Of0",
|
||||||
ExportName = "scePthreadAttrGetschedparam",
|
ExportName = "scePthreadAttrGetschedparam",
|
||||||
@@ -1145,90 +1133,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "SFxTMOfuCkE",
|
|
||||||
ExportName = "pthread_rwlock_tryrdlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "XhWHn6P5R7U",
|
|
||||||
ExportName = "pthread_rwlock_trywrlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
|
|
||||||
/// only if the lock is free right now, otherwise reports BUSY.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists
|
|
||||||
/// for the scheduler resume path and decrements WaitingWriters on success,
|
|
||||||
/// which is correct only for a thread that previously incremented it. A fresh
|
|
||||||
/// try never did, so reusing it would silently consume another thread's
|
|
||||||
/// waiter count and let a queued writer be skipped.
|
|
||||||
/// </remarks>
|
|
||||||
private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
|
||||||
{
|
|
||||||
if (rwlockAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
|
||||||
lock (rwlock.SyncRoot)
|
|
||||||
{
|
|
||||||
if (write)
|
|
||||||
{
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors the blocking path's re-entrant compat-writer grant so the
|
|
||||||
// two agree on what counts as already owning the lock.
|
|
||||||
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
rwlock.AddCompatWriter(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId != 0 ||
|
|
||||||
rwlock.ReaderTotalCount != 0 ||
|
|
||||||
rwlock.CompatWriterTotalCount != 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock");
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
rwlock.AddReader(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "+L98PIbGttk",
|
Nid = "+L98PIbGttk",
|
||||||
ExportName = "scePthreadRwlockUnlock",
|
ExportName = "scePthreadRwlockUnlock",
|
||||||
@@ -1915,94 +1819,4 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX-named aliases. libKernel exports each of these routines under two
|
|
||||||
// NIDs -- a scePthread* name and the plain POSIX name -- and middleware
|
|
||||||
// compiled against POSIX headers links the latter. Both take identical
|
|
||||||
// arguments and, per the convention already used by scePthreadOnce's alias,
|
|
||||||
// return the same OrbisGen2Result rather than translating to errno.
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "a2P9wYGeZvc",
|
|
||||||
ExportName = "pthread_setprio",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "FIs3-UQT9sg",
|
|
||||||
ExportName = "pthread_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "vQm4fDEsWi8",
|
|
||||||
ExportName = "pthread_attr_getstack",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Ucsu-OK+els",
|
|
||||||
ExportName = "pthread_attr_get_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JarMIy8kKEY",
|
|
||||||
ExportName = "pthread_attr_setschedpolicy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "E+tyo3lp5Lw",
|
|
||||||
ExportName = "pthread_attr_setdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "euKRgm0Vn2M",
|
|
||||||
ExportName = "pthread_attr_setschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7ZlAakEf0Qg",
|
|
||||||
ExportName = "pthread_attr_setinheritsched",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "0qOtCR-ZHck",
|
|
||||||
ExportName = "pthread_attr_getstacksize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "VUT1ZSrHT0I",
|
|
||||||
ExportName = "pthread_attr_getdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JKyG3SWyA10",
|
|
||||||
ExportName = "pthread_attr_setguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JNkVVsVDmOk",
|
|
||||||
ExportName = "pthread_attr_getguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2058,13 +2058,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "NhpspxdjEKU",
|
|
||||||
ExportName = "_nanosleep",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yS8U2TGCe1A",
|
Nid = "yS8U2TGCe1A",
|
||||||
ExportName = "nanosleep",
|
ExportName = "nanosleep",
|
||||||
|
|||||||
@@ -428,22 +428,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "GEnUkDZoUwY",
|
|
||||||
ExportName = "scePthreadSemInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores
|
|
||||||
if (ctx[CpuRegister.Rsi] != 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PosixSemInit(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YCV5dGGBcCo",
|
Nid = "YCV5dGGBcCo",
|
||||||
ExportName = "sem_wait",
|
ExportName = "sem_wait",
|
||||||
@@ -462,13 +446,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelWaitSema(ctx);
|
return KernelWaitSema(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "C36iRE0F5sE",
|
|
||||||
ExportName = "scePthreadSemWait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WBWzsRifCEA",
|
Nid = "WBWzsRifCEA",
|
||||||
ExportName = "sem_trywait",
|
ExportName = "sem_trywait",
|
||||||
@@ -486,19 +463,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelPollSema(ctx, handle, 1);
|
return KernelPollSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "H2a+IN9TP0E",
|
|
||||||
ExportName = "scePthreadSemTrywait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemTryWait(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var result = PosixSemTryWait(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN)
|
|
||||||
: result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "w5IHyvahg-o",
|
Nid = "w5IHyvahg-o",
|
||||||
ExportName = "sem_timedwait",
|
ExportName = "sem_timedwait",
|
||||||
@@ -535,13 +499,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelSignalSema(ctx, handle, 1);
|
return KernelSignalSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "aishVAiFaYM",
|
|
||||||
ExportName = "scePthreadSemPost",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Bq+LRV-N6Hk",
|
Nid = "Bq+LRV-N6Hk",
|
||||||
ExportName = "sem_getvalue",
|
ExportName = "sem_getvalue",
|
||||||
@@ -592,13 +549,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Vwc+L05e6oE",
|
|
||||||
ExportName = "scePthreadSemDestroy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx);
|
|
||||||
|
|
||||||
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||||
{
|
{
|
||||||
handle = 0;
|
handle = 0;
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
|
||||||
|
|
||||||
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
|
|
||||||
// futex-style wait/wake: a thread parks on a guest address until another thread
|
|
||||||
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
|
|
||||||
// build their own spinlocks/queues on top of it and call the wait in a hot
|
|
||||||
// loop; left unimplemented, every wait returns immediately and the runtime
|
|
||||||
// busy-spins forever (millions of calls, no forward progress).
|
|
||||||
//
|
|
||||||
// This implements wait/wake over the existing cooperative-block scheduler,
|
|
||||||
// keyed on the address. The real primitive takes a compare value so the wait
|
|
||||||
// only sleeps while the address still holds the expected value; that exact
|
|
||||||
// value is not recovered here, so each wait is given a bounded deadline and
|
|
||||||
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
|
||||||
// self-heals when the deadline expires and the guest re-checks its own
|
|
||||||
// condition, which futex callers already tolerate. A matching wake releases
|
|
||||||
// waiters immediately through the same key.
|
|
||||||
public static class KernelSyncOnAddressCompatExports
|
|
||||||
{
|
|
||||||
// Safety-net poll interval. Real releases come from the wake side (generation
|
|
||||||
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
|
||||||
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
|
||||||
// large: a short interval turns every parked waiter into a hot re-poll that
|
|
||||||
// steals scheduler bandwidth from the threads that actually make progress
|
|
||||||
// (including the ones that would issue the wake), so it must be a rare last
|
|
||||||
// resort, not a spin substitute.
|
|
||||||
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
|
||||||
|
|
||||||
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
|
||||||
// which cannot use the guest-thread scheduler's block mechanism.
|
|
||||||
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
|
||||||
|
|
||||||
// Per-address wake generation. A wait captures the current generation and
|
|
||||||
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
|
||||||
// wake bumps it. This is what actually holds the thread blocked: a bare
|
|
||||||
// "always satisfied" predicate is treated as an immediate late-arrival by
|
|
||||||
// the dispatcher's race guard and never yields, leaving the guest to
|
|
||||||
// busy-spin. The generation also closes the register-vs-park race for free:
|
|
||||||
// a wake landing in that window bumps the generation, so the predicate is
|
|
||||||
// already satisfied and the guest correctly resumes at once.
|
|
||||||
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
|
||||||
|
|
||||||
private static long CurrentGeneration(ulong address) =>
|
|
||||||
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
|
||||||
|
|
||||||
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Hc4CaR6JBL0",
|
|
||||||
ExportName = "sceKernelSyncOnAddressWait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int SyncOnAddressWait(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var address = ctx[CpuRegister.Rdi];
|
|
||||||
if (address == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var observedGeneration = CurrentGeneration(address);
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
|
||||||
|
|
||||||
// Cooperative path: stay parked until a wake bumps this address's
|
|
||||||
// generation (or the deadline expires as a self-heal). The guest
|
|
||||||
// re-evaluates its own condition after resuming.
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelSyncOnAddressWait",
|
|
||||||
WakeKey(address),
|
|
||||||
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
||||||
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
|
||||||
deadline))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-cooperative caller (host main thread): bounded host wait so a
|
|
||||||
// missed wake self-heals instead of hanging.
|
|
||||||
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (CurrentGeneration(address) == observedGeneration)
|
|
||||||
{
|
|
||||||
Monitor.Wait(gate, WaitSelfHealTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "q2y-wDIVWZA",
|
|
||||||
ExportName = "sceKernelSyncOnAddressWake",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int SyncOnAddressWake(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var address = ctx[CpuRegister.Rdi];
|
|
||||||
if (address == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsi carries the number of waiters to release (1 = wake-one, a large
|
|
||||||
// value = wake-all); default to all if it looks unset.
|
|
||||||
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
|
||||||
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
|
||||||
|
|
||||||
// Bump the generation first so a wait that has registered but not yet
|
|
||||||
// parked sees the change and resumes instead of missing this wake.
|
|
||||||
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
|
||||||
|
|
||||||
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
|
||||||
|
|
||||||
if (_hostAddressGates.TryGetValue(address, out var gate))
|
|
||||||
{
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
Monitor.PulseAll(gate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
var value = (int)result;
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,7 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
bool allowAllocateAtAlternative,
|
bool allowAllocateAtAlternative,
|
||||||
string traceName,
|
string traceName,
|
||||||
out ulong mappedAddress,
|
out ulong mappedAddress)
|
||||||
bool backPartialOverlap = false)
|
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
mappedAddress = 0;
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -43,18 +42,6 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed mappings must cover the whole requested window even when part of
|
|
||||||
// it is already backed by another allocation. The single-call AllocateAt
|
|
||||||
// below is all-or-nothing and fails outright on partial overlap, leaving
|
|
||||||
// the untouched pages unmapped for the guest to fault into. Fill the free
|
|
||||||
// pages directly instead.
|
|
||||||
if (backPartialOverlap &&
|
|
||||||
addressSpace.TryBackFixedRange(desiredAddress, length, executable))
|
|
||||||
{
|
|
||||||
mappedAddress = desiredAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
if (allocated == 0)
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -184,212 +184,6 @@ public static class NetExports
|
|||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="NetSetsockopt"/>; identical
|
|
||||||
/// (fd, level, option, value, length) argument order.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "fFxGkxF2bVo",
|
|
||||||
ExportName = "setsockopt",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads back the socket options this backend actually tracks: SO_NBIO,
|
|
||||||
/// SO_REUSEADDR and SO_ERROR.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Anything else returns EINVAL rather than a zero-filled buffer. A caller
|
|
||||||
/// that receives success for an option nobody stored would treat whatever
|
|
||||||
/// happens to be in its output buffer as the real setting, which is a harder
|
|
||||||
/// failure to trace than an explicit rejection.
|
|
||||||
/// </remarks>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "6O8EwYOgH9Y",
|
|
||||||
ExportName = "getsockopt",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixGetsockopt(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var level = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
||||||
var option = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
var valueAddress = ctx[CpuRegister.Rcx];
|
|
||||||
var lengthAddress = ctx[CpuRegister.R8];
|
|
||||||
if (!_sockets.TryGetValue(id, out var socket))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> lengthBytes = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryRead(lengthAddress, lengthBytes))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
int value;
|
|
||||||
switch (option)
|
|
||||||
{
|
|
||||||
// ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored.
|
|
||||||
case 0x1200:
|
|
||||||
value = socket.Blocking ? 0 : 1;
|
|
||||||
break;
|
|
||||||
case 0x0004:
|
|
||||||
value = (int)socket.GetSocketOption(
|
|
||||||
SocketOptionLevel.Socket,
|
|
||||||
SocketOptionName.ReuseAddress)! != 0 ? 1 : 0;
|
|
||||||
break;
|
|
||||||
// ORBIS_NET_SO_ERROR: nothing here records per-socket async errors,
|
|
||||||
// so report "no pending error" rather than inventing one.
|
|
||||||
case 0x1007:
|
|
||||||
value = 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int));
|
|
||||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes) ||
|
|
||||||
!ctx.Memory.TryWrite(lengthAddress, lengthBytes))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0);
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "fZOeZIOEmLw",
|
|
||||||
ExportName = "send",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixSend(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var bufferAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var length = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
if (!_sockets.TryGetValue(id, out var socket))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length < 0 || (length != 0 && bufferAddress == 0))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = new byte[length];
|
|
||||||
if (!ctx.Memory.TryRead(bufferAddress, payload))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sent = socket.Send(payload, SocketFlags.None);
|
|
||||||
TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0);
|
|
||||||
return ctx.SetReturn(sent);
|
|
||||||
}
|
|
||||||
catch (SocketException exception)
|
|
||||||
when (exception.SocketErrorCode == SocketError.WouldBlock)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
|
||||||
}
|
|
||||||
catch (SocketException)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Formats a binary address as text. Pure conversion with no socket state,
|
|
||||||
/// so it behaves identically to the console version for AF_INET/AF_INET6.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "5jRCs2axtr4",
|
|
||||||
ExportName = "inet_ntop",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixInetNtop(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var family = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var destinationAddress = ctx[CpuRegister.Rdx];
|
|
||||||
var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]);
|
|
||||||
if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily.
|
|
||||||
var addressLength = family switch
|
|
||||||
{
|
|
||||||
2 => 4,
|
|
||||||
28 => 16,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (addressLength == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawAddress = new byte[addressLength];
|
|
||||||
if (!ctx.Memory.TryRead(sourceAddress, rawAddress))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = new IPAddress(rawAddress).ToString();
|
|
||||||
var encoded = Encoding.ASCII.GetBytes(text);
|
|
||||||
|
|
||||||
// POSIX requires the terminator to fit as well; a truncated address string
|
|
||||||
// is worse than a reported failure because the caller cannot detect it.
|
|
||||||
if (encoded.Length + 1 > destinationSize)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new byte[encoded.Length + 1];
|
|
||||||
encoded.CopyTo(buffer, 0);
|
|
||||||
if (!ctx.Memory.TryWrite(destinationAddress, buffer))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inet_ntop returns the destination pointer on success.
|
|
||||||
ctx[CpuRegister.Rax] = destinationAddress;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bErx49PgxyY",
|
Nid = "bErx49PgxyY",
|
||||||
ExportName = "sceNetBind",
|
ExportName = "sceNetBind",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using System.Buffers;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -26,44 +25,24 @@ public static class Ngs2Exports
|
|||||||
private static long _nextUid;
|
private static long _nextUid;
|
||||||
private static long _renderCount;
|
private static long _renderCount;
|
||||||
|
|
||||||
// NGS2 renders one grain of interleaved float32 per sceNgs2SystemRender.
|
private sealed record SystemState(uint Uid);
|
||||||
// The grain length defaults to 256 frames (matching the 8192-byte AudioOut
|
|
||||||
// buffers games copy it into) until the title overrides it.
|
|
||||||
private const int DefaultGrainSamples = 256;
|
|
||||||
private const double OutputSampleRate = 48000.0;
|
|
||||||
|
|
||||||
private sealed class SystemState
|
|
||||||
{
|
|
||||||
public SystemState(uint uid) => Uid = uid;
|
|
||||||
|
|
||||||
public uint Uid { get; }
|
|
||||||
public int GrainSamples { get; set; } = DefaultGrainSamples;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record RackState(ulong SystemHandle, uint RackId);
|
private sealed record RackState(ulong SystemHandle, uint RackId);
|
||||||
|
private sealed record VoiceState(ulong RackHandle, uint VoiceIndex);
|
||||||
|
|
||||||
private sealed class VoiceState
|
[SysAbiExport(
|
||||||
|
Nid = "koBbCMvOKWw",
|
||||||
|
ExportName = "sceNgs2SystemCreate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemCreate(CpuContext ctx)
|
||||||
{
|
{
|
||||||
public VoiceState(ulong rackHandle, uint voiceIndex)
|
var bufferInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadContextBuffer(ctx, bufferInfoAddress, out var hostBuffer))
|
||||||
{
|
{
|
||||||
RackHandle = rackHandle;
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
VoiceIndex = voiceIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong RackHandle { get; }
|
return CreateSystem(ctx, ctx[CpuRegister.Rdx], hostBuffer);
|
||||||
public uint VoiceIndex { get; }
|
|
||||||
|
|
||||||
// Software-mixer playback state. Pcm is the fully decoded mono waveform;
|
|
||||||
// Position is a fractional read cursor advanced at the source/output rate
|
|
||||||
// ratio each output frame.
|
|
||||||
public short[]? Pcm { get; set; }
|
|
||||||
public ulong SourceAddr { get; set; }
|
|
||||||
public int SourceRate { get; set; }
|
|
||||||
public double Position { get; set; }
|
|
||||||
public bool Playing { get; set; }
|
|
||||||
public int LoopStart { get; set; } = -1;
|
|
||||||
public int LoopEnd { get; set; }
|
|
||||||
public float Gain { get; set; } = 1f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -79,34 +58,14 @@ public static class Ngs2Exports
|
|||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle))
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
return CreateSystem(ctx, outHandleAddress, handle);
|
||||||
{
|
|
||||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-allocator create: identical to the WithAllocator form for our purposes.
|
|
||||||
// The only signature difference is the caller-supplied buffer info in rsi
|
|
||||||
// (vs an allocator callback); the system option (rdi) and out-handle (rdx)
|
|
||||||
// sit at the same argument positions, so we reuse the same implementation.
|
|
||||||
// Dead Cells uses these variants — leaving sceNgs2SystemCreate unresolved
|
|
||||||
// gave the game a garbage system handle, so every later rack/voice call
|
|
||||||
// failed and it polled sceNgs2VoiceGetState forever, freezing at FLIP 0.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "koBbCMvOKWw",
|
|
||||||
ExportName = "sceNgs2SystemCreate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2SystemCreate(CpuContext ctx) => Ngs2SystemCreateWithAllocator(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "u-WrYDaJA3k",
|
Nid = "u-WrYDaJA3k",
|
||||||
ExportName = "sceNgs2SystemDestroy",
|
ExportName = "sceNgs2SystemDestroy",
|
||||||
@@ -135,6 +94,27 @@ public static class Ngs2Exports
|
|||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cLV4aiT9JpA",
|
||||||
|
ExportName = "sceNgs2RackCreate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var bufferInfoAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (!TryReadContextBuffer(ctx, bufferInfoAddress, out var hostBuffer))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateRack(
|
||||||
|
ctx,
|
||||||
|
ctx[CpuRegister.Rdi],
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
hostBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "U546k6orxQo",
|
Nid = "U546k6orxQo",
|
||||||
ExportName = "sceNgs2RackCreateWithAllocator",
|
ExportName = "sceNgs2RackCreateWithAllocator",
|
||||||
@@ -158,29 +138,14 @@ public static class Ngs2Exports
|
|||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle))
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
return CreateRack(ctx, systemHandle, rackId, outHandleAddress, handle);
|
||||||
{
|
|
||||||
Racks[handle] = new RackState(systemHandle, rackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-allocator rack create: system handle (rdi), rack id (rsi) and the
|
|
||||||
// out-handle (r8) share the WithAllocator argument layout, so reuse it.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "cLV4aiT9JpA",
|
|
||||||
ExportName = "sceNgs2RackCreate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2RackCreate(CpuContext ctx) => Ngs2RackCreateWithAllocator(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "lCqD7oycmIM",
|
Nid = "lCqD7oycmIM",
|
||||||
ExportName = "sceNgs2RackDestroy",
|
ExportName = "sceNgs2RackDestroy",
|
||||||
@@ -255,217 +220,14 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2VoiceControl(CpuContext ctx)
|
public static int Ngs2VoiceControl(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var voiceHandle = ctx[CpuRegister.Rdi];
|
|
||||||
var paramList = ctx[CpuRegister.Rsi];
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
if (!Voices.ContainsKey(voiceHandle))
|
return SetReturn(
|
||||||
{
|
ctx,
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
|
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldTrace())
|
|
||||||
{
|
|
||||||
TraceVoiceParamList(ctx, voiceHandle, paramList);
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleVoiceParams(ctx, voiceHandle, paramList);
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the SceNgs2VoiceParamHead command list (header = u32 size, u32 id;
|
|
||||||
// params are laid out contiguously) and apply the ones the mixer needs:
|
|
||||||
// the waveform-blocks param arms a voice with decoded PCM, and the port
|
|
||||||
// matrix param carries its output gain.
|
|
||||||
private static void HandleVoiceParams(CpuContext ctx, ulong voiceHandle, ulong paramList)
|
|
||||||
{
|
|
||||||
if (paramList == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset = paramList;
|
|
||||||
for (var guard = 0; guard < 32; guard++)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt32(offset, out var size) ||
|
|
||||||
!ctx.TryReadUInt32(offset + 4, out var id))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (id)
|
|
||||||
{
|
|
||||||
case 0x10000001:
|
|
||||||
ApplyWaveformParam(ctx, voiceHandle, offset);
|
|
||||||
break;
|
|
||||||
case 0x20010001:
|
|
||||||
ApplyPortMatrixParam(ctx, voiceHandle, offset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance to the next contiguous block; the game normally sends one
|
|
||||||
// param per call (size==whole block), so stop when size is degenerate.
|
|
||||||
if (size < 8 || size > 0x1000)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += (size + 7) & ~7u;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Waveform-blocks param: the guest pointer at +8 references a "VAGp"
|
|
||||||
// (PS-ADPCM) container. Decode it once and arm the voice for playback.
|
|
||||||
private static void ApplyWaveformParam(CpuContext ctx, ulong voiceHandle, ulong paramOffset)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt64(paramOffset + 8, out var dataAddr) || dataAddr <= 0x10000)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (Voices.TryGetValue(voiceHandle, out var existing) &&
|
|
||||||
existing.SourceAddr == dataAddr && existing.Pcm is not null)
|
|
||||||
{
|
|
||||||
// Same waveform already armed — don't restart it every frame.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> header = stackalloc byte[Ngs2VagDecoder.VagHeaderSize];
|
|
||||||
if (!ctx.Memory.TryRead(dataAddr, header) || !Ngs2VagDecoder.IsVag(header))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var declaredSize = (int)BinaryPrimitives.ReadUInt32BigEndian(header[0x0C..]);
|
|
||||||
var totalBytes = Ngs2VagDecoder.VagHeaderSize + Math.Clamp(declaredSize, 0, 8 * 1024 * 1024);
|
|
||||||
var raw = System.Buffers.ArrayPool<byte>.Shared.Rent(totalBytes);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!ctx.Memory.TryRead(dataAddr, raw.AsSpan(0, totalBytes)) ||
|
|
||||||
!Ngs2VagDecoder.TryDecode(raw.AsSpan(0, totalBytes), out var waveform))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (!Voices.TryGetValue(voiceHandle, out var voice))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
voice.Pcm = waveform.Samples;
|
|
||||||
voice.SourceAddr = dataAddr;
|
|
||||||
voice.SourceRate = waveform.SampleRate;
|
|
||||||
voice.LoopStart = waveform.LoopStart;
|
|
||||||
voice.LoopEnd = waveform.LoopEnd > 0 ? waveform.LoopEnd : waveform.Samples.Length;
|
|
||||||
voice.Position = 0;
|
|
||||||
voice.Playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldTrace())
|
|
||||||
{
|
|
||||||
var peak = 0;
|
|
||||||
for (var i = 0; i < waveform.Samples.Length; i++)
|
|
||||||
{
|
|
||||||
peak = Math.Max(peak, Math.Abs((int)waveform.Samples[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] ngs2.arm voice=0x{voiceHandle:X16} addr=0x{dataAddr:X} rate={waveform.SampleRate} samples={waveform.Samples.Length} loop={waveform.LoopStart} peak={peak}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
System.Buffers.ArrayPool<byte>.Shared.Return(raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port matrix param: the first float level is a reasonable proxy for the
|
|
||||||
// voice's output gain until per-channel panning is implemented.
|
|
||||||
private static void ApplyPortMatrixParam(CpuContext ctx, ulong voiceHandle, ulong paramOffset)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt32(paramOffset + 12, out var levelBits))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var level = BitConverter.UInt32BitsToSingle(levelBits);
|
|
||||||
if (!float.IsFinite(level) || level < 0f || level > 8f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (Voices.TryGetValue(voiceHandle, out var voice))
|
|
||||||
{
|
|
||||||
voice.Gain = level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empirically dump the SceNgs2VoiceParamHead-chained command list so we can
|
|
||||||
// confirm the real struct layout (size/next/id) against public NGS2 sources
|
|
||||||
// before building the software mixer. Assumed header: u16 size, s16 next
|
|
||||||
// (byte offset to the next block, 0 = end), u32 id.
|
|
||||||
private static void TraceVoiceParamList(CpuContext ctx, ulong voiceHandle, ulong paramList)
|
|
||||||
{
|
|
||||||
if (paramList == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> peek = stackalloc byte[32];
|
|
||||||
var offset = paramList;
|
|
||||||
for (int guard = 0; guard < 32; guard++)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt16(offset, out var size) ||
|
|
||||||
!ctx.TryReadUInt16(offset + 2, out var next) ||
|
|
||||||
!ctx.TryReadUInt32(offset + 4, out var id))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] ngs2.voiceparam voice=0x{voiceHandle:X16} @0x{offset:X}: unreadable header");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
peek.Clear();
|
|
||||||
var readable = Math.Min((int)Math.Max((ushort)8, size), peek.Length);
|
|
||||||
ctx.Memory.TryRead(offset, peek[..readable]);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] ngs2.voiceparam voice=0x{voiceHandle:X16} id=0x{id:X} size={size} next={unchecked((short)next)} bytes={Convert.ToHexString(peek[..readable])}");
|
|
||||||
|
|
||||||
// For the waveform-blocks param, follow the embedded pointers and
|
|
||||||
// dump the pointed-to bytes so we can tell PCM16 from ATRAC9.
|
|
||||||
if (id == 0x10000001 && Interlocked.Increment(ref _waveformDumps) <= 8)
|
|
||||||
{
|
|
||||||
for (int po = 8; po + 8 <= readable; po += 8)
|
|
||||||
{
|
|
||||||
if (ctx.TryReadUInt64(offset + (ulong)po, out var ptr) && ptr > 0x10000 &&
|
|
||||||
ctx.Memory.TryRead(ptr, peek))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] ngs2.waveform @+{po} ptr=0x{ptr:X} head={Convert.ToHexString(peek)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var advance = unchecked((short)next);
|
|
||||||
if (advance <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += (ulong)advance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long _waveformDumps;
|
|
||||||
private static long _renderInfoDumps;
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "AbYvTOZ8Pts",
|
Nid = "AbYvTOZ8Pts",
|
||||||
ExportName = "sceNgs2VoiceRunCommands",
|
ExportName = "sceNgs2VoiceRunCommands",
|
||||||
@@ -511,32 +273,11 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SceNgs2RenderBufferInfo: {ptr@0, size@8, waveformType@16,
|
|
||||||
// channelsCount@20}. Mix the armed voices into the leading grain
|
|
||||||
// as interleaved float32 — this is what the game copies to
|
|
||||||
// sceAudioOutOutput, so it is where NGS2 audio must appear.
|
|
||||||
var channels = 2;
|
|
||||||
if (ctx.TryReadUInt32(entryAddress + 20, out var declaredChannels) &&
|
|
||||||
declaredChannels is > 0 and <= 8)
|
|
||||||
{
|
|
||||||
channels = (int)declaredChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
MixVoicesIntoGrain(ctx, systemHandle, bufferAddress, bufferSize, channels);
|
|
||||||
|
|
||||||
if (ShouldTrace() && Interlocked.Increment(ref _renderInfoDumps) <= 4)
|
|
||||||
{
|
|
||||||
Span<byte> rbi = stackalloc byte[RenderBufferInfoSize];
|
|
||||||
ctx.Memory.TryRead(entryAddress, rbi);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] ngs2.renderbufinfo addr=0x{bufferAddress:X} size={bufferSize} ch={channels} raw={Convert.ToHexString(rbi)}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = Interlocked.Increment(ref _renderCount);
|
var count = Interlocked.Increment(ref _renderCount);
|
||||||
if (ShouldTrace() && (count <= 4 || count % 200 == 0))
|
if (ShouldTrace() && (count <= 4 || count % 10_000 == 0))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||||
@@ -545,135 +286,6 @@ public static class Ngs2Exports
|
|||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum every armed voice belonging to this system into the leading grain of
|
|
||||||
// the render buffer as interleaved float32. The buffer was just zeroed, so
|
|
||||||
// this is a plain additive mix; silence stays silence when nothing plays.
|
|
||||||
private static void MixVoicesIntoGrain(
|
|
||||||
CpuContext ctx, ulong systemHandle, ulong bufferAddress, ulong bufferSize, int channels)
|
|
||||||
{
|
|
||||||
int grain;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (!Systems.TryGetValue(systemHandle, out var system))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grain = system.GrainSamples;
|
|
||||||
}
|
|
||||||
|
|
||||||
var capacityFrames = (int)Math.Min((ulong)grain, bufferSize / (ulong)(channels * sizeof(float)));
|
|
||||||
if (capacityFrames <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var floatCount = capacityFrames * channels;
|
|
||||||
var accum = ArrayPool<float>.Shared.Rent(floatCount);
|
|
||||||
var mixedAnything = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Array.Clear(accum, 0, floatCount);
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
foreach (var pair in Voices)
|
|
||||||
{
|
|
||||||
var voice = pair.Value;
|
|
||||||
if (!voice.Playing || voice.Pcm is null || voice.Pcm.Length == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Racks.TryGetValue(voice.RackHandle, out var rack) ||
|
|
||||||
rack.SystemHandle != systemHandle)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MixOneVoice(accum, capacityFrames, channels, voice);
|
|
||||||
mixedAnything = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mixedAnything)
|
|
||||||
{
|
|
||||||
WriteGrain(ctx, bufferAddress, accum, floatCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<float>.Shared.Return(accum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resample one voice from its source rate to 48 kHz (nearest-sample) and add
|
|
||||||
// it to the front stereo pair. Advances the voice cursor and handles loop /
|
|
||||||
// one-shot end. Must be called under StateGate.
|
|
||||||
private static void MixOneVoice(float[] accum, int frames, int channels, VoiceState voice)
|
|
||||||
{
|
|
||||||
var pcm = voice.Pcm!;
|
|
||||||
var loopEnd = voice.LoopEnd > 0 && voice.LoopEnd <= pcm.Length ? voice.LoopEnd : pcm.Length;
|
|
||||||
var loopStart = voice.LoopStart;
|
|
||||||
var step = voice.SourceRate / OutputSampleRate;
|
|
||||||
var gain = voice.Gain / 32768f;
|
|
||||||
var pos = voice.Position;
|
|
||||||
for (var f = 0; f < frames; f++)
|
|
||||||
{
|
|
||||||
var idx = (int)pos;
|
|
||||||
if (idx >= loopEnd)
|
|
||||||
{
|
|
||||||
if (loopStart >= 0 && loopStart < loopEnd)
|
|
||||||
{
|
|
||||||
pos = loopStart;
|
|
||||||
idx = loopStart;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
voice.Playing = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idx < 0 || idx >= pcm.Length)
|
|
||||||
{
|
|
||||||
voice.Playing = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sample = pcm[idx] * gain;
|
|
||||||
var baseIndex = f * channels;
|
|
||||||
accum[baseIndex] += sample;
|
|
||||||
if (channels > 1)
|
|
||||||
{
|
|
||||||
accum[baseIndex + 1] += sample;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos += step;
|
|
||||||
}
|
|
||||||
|
|
||||||
voice.Position = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteGrain(CpuContext ctx, ulong address, float[] accum, int count)
|
|
||||||
{
|
|
||||||
var bytes = ArrayPool<byte>.Shared.Rent(count * sizeof(float));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var span = bytes.AsSpan(0, count * sizeof(float));
|
|
||||||
for (var i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var value = Math.Clamp(accum[i], -1f, 1f);
|
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(span.Slice(i * sizeof(float), sizeof(float)), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Memory.TryWrite(address, span);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "pgFAiLR5qT4",
|
Nid = "pgFAiLR5qT4",
|
||||||
ExportName = "sceNgs2SystemQueryBufferSize",
|
ExportName = "sceNgs2SystemQueryBufferSize",
|
||||||
@@ -711,25 +323,7 @@ public static class Ngs2Exports
|
|||||||
ExportName = "sceNgs2SystemSetGrainSamples",
|
ExportName = "sceNgs2SystemSetGrainSamples",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2SystemSetGrainSamples(CpuContext ctx)
|
public static int Ngs2SystemSetGrainSamples(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
{
|
|
||||||
var systemHandle = ctx[CpuRegister.Rdi];
|
|
||||||
var grain = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (!Systems.TryGetValue(systemHandle, out var system))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grain > 0 && grain <= 8192)
|
|
||||||
{
|
|
||||||
system.GrainSamples = grain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-tbc2SxQD60",
|
Nid = "-tbc2SxQD60",
|
||||||
@@ -818,6 +412,67 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int CreateSystem(CpuContext ctx, ulong outHandleAddress, ulong handle)
|
||||||
|
{
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle == 0 || !ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CreateRack(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong systemHandle,
|
||||||
|
uint rackId,
|
||||||
|
ulong outHandleAddress,
|
||||||
|
ulong handle)
|
||||||
|
{
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle == 0 || !ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Racks[handle] = new RackState(systemHandle, rackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadContextBuffer(CpuContext ctx, ulong address, out ulong hostBuffer)
|
||||||
|
{
|
||||||
|
hostBuffer = 0;
|
||||||
|
return address != 0 &&
|
||||||
|
ctx.TryReadUInt64(address, out hostBuffer) &&
|
||||||
|
hostBuffer != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||||
{
|
{
|
||||||
handle = 0;
|
handle = 0;
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Ngs2;
|
|
||||||
|
|
||||||
// Clean-room PS-ADPCM ("VAG") decoder. NGS2 sampler voices point at waveforms
|
|
||||||
// wrapped in the classic Sony "VAGp" container: a 48-byte big-endian header
|
|
||||||
// followed by 16-byte ADPCM frames (2-byte predictor/shift + flags, then 14
|
|
||||||
// bytes = 28 nibbles = 28 samples). The predictor coefficient table and the
|
|
||||||
// nibble decode are the publicly documented PSX SPU ADPCM algorithm.
|
|
||||||
public static class Ngs2VagDecoder
|
|
||||||
{
|
|
||||||
// Standard PS-ADPCM predictor filters (scaled by 1/64).
|
|
||||||
private static readonly int[] Coeff0 = { 0, 60, 115, 98, 122 };
|
|
||||||
private static readonly int[] Coeff1 = { 0, 0, -52, -55, -60 };
|
|
||||||
|
|
||||||
public const int VagHeaderSize = 0x30;
|
|
||||||
private const uint VagMagic = 0x56414770; // "VAGp"
|
|
||||||
|
|
||||||
public readonly struct Waveform
|
|
||||||
{
|
|
||||||
public Waveform(short[] samples, int sampleRate, int loopStart, int loopEnd)
|
|
||||||
{
|
|
||||||
Samples = samples;
|
|
||||||
SampleRate = sampleRate;
|
|
||||||
LoopStart = loopStart;
|
|
||||||
LoopEnd = loopEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short[] Samples { get; }
|
|
||||||
public int SampleRate { get; }
|
|
||||||
public int LoopStart { get; } // -1 when the waveform does not loop
|
|
||||||
public int LoopEnd { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when the buffer begins with a recognizable "VAGp" container header.
|
|
||||||
public static bool IsVag(ReadOnlySpan<byte> data) =>
|
|
||||||
data.Length >= VagHeaderSize &&
|
|
||||||
BinaryPrimitives.ReadUInt32BigEndian(data) == VagMagic;
|
|
||||||
|
|
||||||
// Decode a full "VAGp" container into mono PCM16. Returns false when the
|
|
||||||
// header is missing/short so callers can skip unsupported formats safely.
|
|
||||||
public static bool TryDecode(ReadOnlySpan<byte> data, out Waveform waveform)
|
|
||||||
{
|
|
||||||
waveform = default;
|
|
||||||
if (!IsVag(data))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header (big-endian): +0x0C dataSize, +0x10 sampleRate.
|
|
||||||
var declaredSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[0x0C..]);
|
|
||||||
var sampleRate = (int)BinaryPrimitives.ReadUInt32BigEndian(data[0x10..]);
|
|
||||||
if (sampleRate <= 0)
|
|
||||||
{
|
|
||||||
sampleRate = 48000;
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = data[VagHeaderSize..];
|
|
||||||
// Trust the declared payload size when it fits; otherwise decode what we
|
|
||||||
// actually have (some tools pad or under-report).
|
|
||||||
var available = body.Length - (body.Length % 16);
|
|
||||||
var frameBytes = declaredSize > 0 && declaredSize <= available ? declaredSize - (declaredSize % 16) : available;
|
|
||||||
if (frameBytes <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
waveform = Decode(body[..frameBytes], sampleRate);
|
|
||||||
return waveform.Samples.Length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode raw 16-byte-framed PS-ADPCM (no container header) into PCM16 and
|
|
||||||
// resolve loop points from the per-frame flag bytes.
|
|
||||||
public static Waveform Decode(ReadOnlySpan<byte> frames, int sampleRate)
|
|
||||||
{
|
|
||||||
var frameCount = frames.Length / 16;
|
|
||||||
var samples = new short[frameCount * 28];
|
|
||||||
var loopStart = -1;
|
|
||||||
var loopEnd = -1;
|
|
||||||
|
|
||||||
var hist1 = 0;
|
|
||||||
var hist2 = 0;
|
|
||||||
var outIndex = 0;
|
|
||||||
var ended = false;
|
|
||||||
for (var frame = 0; frame < frameCount && !ended; frame++)
|
|
||||||
{
|
|
||||||
var offset = frame * 16;
|
|
||||||
var header = frames[offset];
|
|
||||||
var shift = header & 0x0F;
|
|
||||||
var filter = (header >> 4) & 0x0F;
|
|
||||||
if (filter > 4)
|
|
||||||
{
|
|
||||||
filter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-frame loop marker (exact PS-ADPCM values, not bit masks):
|
|
||||||
// 3 = loop start, 6 = loop end + jump back, 1/7 = one-shot end.
|
|
||||||
var flags = frames[offset + 1];
|
|
||||||
var blockStart = outIndex;
|
|
||||||
if (flags == 0x03)
|
|
||||||
{
|
|
||||||
loopStart = blockStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
var f0 = Coeff0[filter];
|
|
||||||
var f1 = Coeff1[filter];
|
|
||||||
for (var i = 0; i < 14; i++)
|
|
||||||
{
|
|
||||||
var d = frames[offset + 2 + i];
|
|
||||||
for (var nibble = 0; nibble < 2; nibble++)
|
|
||||||
{
|
|
||||||
var raw = nibble == 0 ? d & 0x0F : d >> 4;
|
|
||||||
// Sign-extend the 4-bit sample into the top nibble, then scale.
|
|
||||||
var s = (short)(raw << 12) >> shift;
|
|
||||||
var predicted = (hist1 * f0 + hist2 * f1) >> 6;
|
|
||||||
var sample = Math.Clamp(s + predicted, short.MinValue, short.MaxValue);
|
|
||||||
samples[outIndex++] = (short)sample;
|
|
||||||
hist2 = hist1;
|
|
||||||
hist1 = sample;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags == 0x06)
|
|
||||||
{
|
|
||||||
loopEnd = outIndex;
|
|
||||||
}
|
|
||||||
else if (flags == 0x01 || flags == 0x07)
|
|
||||||
{
|
|
||||||
ended = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim to the samples we actually decoded (a one-shot end marker can stop
|
|
||||||
// us before the declared frame count).
|
|
||||||
if (outIndex != samples.Length)
|
|
||||||
{
|
|
||||||
Array.Resize(ref samples, outIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loopStart >= 0 && loopEnd <= loopStart)
|
|
||||||
{
|
|
||||||
loopEnd = outIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Waveform(samples, sampleRate, loopStart, loopEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,36 +58,6 @@ public static class NpEntitlementAccessExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int EmptyAddcontInfoSize = 0x30;
|
|
||||||
|
|
||||||
// Singular lookup of one add-on-content entitlement (rdx = info out). We own
|
|
||||||
// no DLC, so report an empty/zeroed info and success — matching the list
|
|
||||||
// variant's "no entitlements" answer. Dead Cells calls this while loading a
|
|
||||||
// level; leaving it unresolved left the info struct uninitialized.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "xddD23+8TfQ",
|
|
||||||
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfo",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpEntitlementAccess")]
|
|
||||||
public static int NpEntitlementAccessGetAddcontEntitlementInfo(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
|
||||||
if (infoAddress != 0)
|
|
||||||
{
|
|
||||||
Span<byte> info = stackalloc byte[EmptyAddcontInfoSize];
|
|
||||||
info.Clear();
|
|
||||||
if (!ctx.Memory.TryWrite(infoAddress, info))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceNpEntitlementAccess(
|
|
||||||
$"get_addcont_info service=0x{ctx[CpuRegister.Rdi]:X16} label=0x{ctx[CpuRegister.Rsi]:X16} " +
|
|
||||||
$"info=0x{infoAddress:X16} -> empty");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceNpEntitlementAccess(string message)
|
private static void TraceNpEntitlementAccess(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -69,23 +69,6 @@ public static class NpManagerExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Accepts the reachability callback and never invokes it. Reachability
|
|
||||||
/// transitions only ever fire on a real PSN connection, which an offline
|
|
||||||
/// session does not have, so registering successfully and staying silent is
|
|
||||||
/// the accurate emulation of a signed-out console rather than a stub.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "hw5KNqAAels",
|
|
||||||
ExportName = "sceNpRegisterNpReachabilityStateCallback",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpRegisterNpReachabilityStateCallback(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "qQJfO8HAiaY",
|
Nid = "qQJfO8HAiaY",
|
||||||
ExportName = "sceNpRegisterStateCallbackA",
|
ExportName = "sceNpRegisterStateCallbackA",
|
||||||
|
|||||||
@@ -80,25 +80,6 @@ public static class NpTrophy2Exports
|
|||||||
LibraryName = "libSceNpTrophy2")]
|
LibraryName = "libSceNpTrophy2")]
|
||||||
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gen5 ABI: context, handle, trophy id, then SceNpTrophy2Details and
|
|
||||||
/// SceNpTrophy2Data output pointers.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Reports "no such trophy" rather than succeeding. Succeeding would require
|
|
||||||
/// filling both output structures, and their exact layouts are not confirmed
|
|
||||||
/// here — a title that trusted zeroed details would read an empty name and a
|
|
||||||
/// grade of zero as real data. NOT_FOUND is a documented outcome that callers
|
|
||||||
/// must already handle, so it degrades along a path the game tests.
|
|
||||||
/// </remarks>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "EwNylPdWUTM",
|
|
||||||
ExportName = "sceNpTrophy2GetTrophyInfo",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpTrophy2")]
|
|
||||||
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
|
|
||||||
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
|
||||||
|
|
||||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||||
{
|
{
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
|
|||||||
@@ -197,16 +197,4 @@ public static class NpUniversalDataSystemExports
|
|||||||
{
|
{
|
||||||
return ctx.SetReturn(0, typeof(long));
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telemetry property setter (event property array, string value). We do not
|
|
||||||
// upload analytics, so accept and drop it — matching the other Set* stubs.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "4llLk7YJRTE",
|
|
||||||
ExportName = "sceNpUniversalDataSystemEventPropertyArraySetString",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpUniversalDataSystem")]
|
|
||||||
public static int NpUniversalDataSystemEventPropertyArraySetString(CpuContext ctx)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(0, typeof(long));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,35 +65,6 @@ public static class PadExports
|
|||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||||
|
|
||||||
// scePadGetHandle(userId, type, index): returns the handle of an already-open
|
|
||||||
// pad without opening a new one. Dead Cells calls it every frame to poll
|
|
||||||
// input; leaving it unresolved returned a garbage handle so the input path
|
|
||||||
// (and the game loop that drives it) misbehaved. Same validation as
|
|
||||||
// scePadOpen — the one primary pad — returning its handle or a not-connected
|
|
||||||
// error, never opening or logging.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "u1GRHp+oWoY",
|
|
||||||
ExportName = "scePadGetHandle",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libScePad")]
|
|
||||||
public static int PadGetHandle(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
||||||
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
if (!_initialized)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||||
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||||
private static int PadOpenCore(CpuContext ctx, bool extended)
|
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||||
@@ -245,38 +216,6 @@ public static class PadExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "AcslpN1jHR8",
|
|
||||||
ExportName = "scePadDeviceClassGetExtendedInformation",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libScePad")]
|
|
||||||
public static int PadDeviceClassGetExtendedInformation(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var informationAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (!IsPrimaryPadHandle(handle))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (informationAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScePadDeviceClassExtendedInformation: deviceClass 0 = standard pad
|
|
||||||
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
|
|
||||||
// the class-data union stays zeroed — the guest treats it as a plain
|
|
||||||
// controller with no extended capabilities.
|
|
||||||
Span<byte> information = stackalloc byte[0x20];
|
|
||||||
information.Clear();
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
|
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(informationAddress, information)
|
|
||||||
? ctx.SetReturn(0)
|
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YndgXqQVV7c",
|
Nid = "YndgXqQVV7c",
|
||||||
ExportName = "scePadReadState",
|
ExportName = "scePadReadState",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -698,22 +698,14 @@ public static class PlayGoExports
|
|||||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||||
if (!hasMetadata)
|
if (!hasMetadata)
|
||||||
{
|
{
|
||||||
// No PlayGo sidecar: derive the installed chunk set from the pak files
|
// No PlayGo sidecar: report a fully-installed single chunk. Available must
|
||||||
// actually present on disk. A locally dumped title has all of its data
|
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
|
||||||
// installed, and a package that splits content across chunks names them
|
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||||
// pakchunk<N>-<platform>.pak, so those N are exactly the chunks that
|
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||||
// exist. Reporting only chunk 0 told such a title its remaining content
|
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||||
// was missing: The Invincible (PPSA06426) ships pakchunk0..8 and spun
|
|
||||||
// forever re-querying scePlayGoGetLocus for a chunk that never became
|
|
||||||
// available. Available must stay true or scePlayGoOpen fails with
|
|
||||||
// NotSupportPlayGo (fatal PS5-component init failure for UE titles).
|
|
||||||
// Ids outside the discovered set still return BAD_CHUNK_ID, so
|
|
||||||
// title-side chunk enumeration still terminates.
|
|
||||||
var installedChunkIds = DiscoverInstalledChunkIds(app0Root);
|
|
||||||
TracePlayGo($"metadata_missing; fully-installed chunks=[{string.Join(',', installedChunkIds)}]");
|
|
||||||
return new PlayGoMetadata(
|
return new PlayGoMetadata(
|
||||||
true,
|
true,
|
||||||
installedChunkIds,
|
[(ushort)0],
|
||||||
PlayGoChunkIdKnowledge.Authoritative);
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,41 +718,6 @@ public static class PlayGoExports
|
|||||||
: PlayGoChunkIdKnowledge.Authoritative);
|
: PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
|
|
||||||
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
|
|
||||||
// is the base chunk and must resolve even for a title with no pak files at
|
|
||||||
// all (which keeps the single-chunk behaviour for such titles).
|
|
||||||
private static ushort[] DiscoverInstalledChunkIds(string app0Root)
|
|
||||||
{
|
|
||||||
var ids = new SortedSet<ushort> { 0 };
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var pakFile in Directory.EnumerateFiles(app0Root, "pakchunk*.pak", SearchOption.AllDirectories))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileNameWithoutExtension(pakFile);
|
|
||||||
var digits = name.AsSpan("pakchunk".Length);
|
|
||||||
var length = 0;
|
|
||||||
while (length < digits.Length && char.IsAsciiDigit(digits[length]))
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length > 0 && ushort.TryParse(digits[..length], out var chunkId))
|
|
||||||
{
|
|
||||||
ids.Add(chunkId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||||
{
|
{
|
||||||
if (!File.Exists(chunkDefsXml))
|
if (!File.Exists(chunkDefsXml))
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,530 +45,6 @@ public static class SaveDataExports
|
|||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
_preparedTransactionResources.Clear();
|
_preparedTransactionResources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_eventGate)
|
|
||||||
{
|
|
||||||
_events.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional error codes and the async-event model (see sceSaveDataGetEventResult).
|
|
||||||
private const int OrbisSaveDataErrorBusy = unchecked((int)0x809F0006);
|
|
||||||
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
|
|
||||||
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
|
|
||||||
// SceSaveDataEventType
|
|
||||||
private const uint EventTypeUmountBackupEnd = 1;
|
|
||||||
private const uint EventTypeBackupEnd = 2;
|
|
||||||
private const uint EventTypeSaveDataMemorySyncEnd = 3;
|
|
||||||
private const int SaveDataEventSize = 0x60;
|
|
||||||
private const int MountInfoSize = 0x40;
|
|
||||||
private const uint DefaultBlockSize = 32768;
|
|
||||||
private const ulong DefaultTotalBlocks = 0x8000; // 1 GiB of 32 KiB blocks
|
|
||||||
|
|
||||||
private static readonly object _eventGate = new();
|
|
||||||
private static readonly Queue<SaveDataEvent> _events = new();
|
|
||||||
private static readonly object _mountGate = new();
|
|
||||||
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
|
|
||||||
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
|
|
||||||
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
|
||||||
|
|
||||||
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
|
|
||||||
{
|
|
||||||
lock (_eventGate)
|
|
||||||
{
|
|
||||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
|
|
||||||
}
|
|
||||||
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "j8xKtiFj0SY",
|
|
||||||
ExportName = "sceSaveDataGetEventResult",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetEventResult(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// rdi: SceSaveDataEventParam* (filter, ignored). rsi: SceSaveDataEvent* out.
|
|
||||||
var eventAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (eventAddress == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataEvent pending;
|
|
||||||
lock (_eventGate)
|
|
||||||
{
|
|
||||||
if (_events.Count == 0)
|
|
||||||
{
|
|
||||||
// No queued completion. Games poll this from a worker; report the
|
|
||||||
// defined "no event" status so the loop keeps polling instead of
|
|
||||||
// acting on an uninitialized event struct.
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorNoEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = _events.Dequeue();
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> ev = stackalloc byte[SaveDataEventSize];
|
|
||||||
ev.Clear();
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
|
||||||
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
|
|
||||||
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "hsKd5c21sQc",
|
|
||||||
ExportName = "sceSaveDataRegisterEventCallback",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataRegisterEventCallback(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "v-AK1AxQhS0",
|
|
||||||
ExportName = "sceSaveDataUnregisterEventCallback",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataUnregisterEventCallback(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
// ---- lifecycle ----
|
|
||||||
[SysAbiExport(Nid = "ZkZhskCPXFw", ExportName = "sceSaveDataInitialize", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataInitialize(CpuContext ctx) => SaveDataInitializeCommon(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "l1NmDeDpNGU", ExportName = "sceSaveDataInitialize2", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataInitialize2(CpuContext ctx) => SaveDataInitializeCommon(ctx);
|
|
||||||
|
|
||||||
private static int SaveDataInitializeCommon(CpuContext ctx)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "yKDy8S5yLA0", ExportName = "sceSaveDataTerminate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataTerminate(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
// ---- mount variants (all share the SceSaveDataMount layout) ----
|
|
||||||
[SysAbiExport(Nid = "32HQAQdwM2o", ExportName = "sceSaveDataMount", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataMount(CpuContext ctx) => SaveDataMount3(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "0z45PIH+SNI", ExportName = "sceSaveDataMount2", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataMount2(CpuContext ctx) => SaveDataMount3(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "xz0YMi6BfNk", ExportName = "sceSaveDataMount5", Target = Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataMount5(CpuContext ctx) => SaveDataMount3(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "BMR4F-Uek3E", ExportName = "sceSaveDataUmount", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataUmount(CpuContext ctx) => SaveDataUmount2(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ieP6jP138Qo", ExportName = "sceSaveDataIsMounted", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataIsMounted(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
int mountCount;
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
mountCount = _mounts.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
TryWriteUInt32(ctx, outAddress, mountCount > 0 ? 1u : 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "65VH0Qaaz6s", ExportName = "sceSaveDataGetMountInfo", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetMountInfo(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var infoAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (mountPointAddress == 0 || infoAddress == 0 ||
|
|
||||||
!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
MountEntry? entry;
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts.TryGetValue(mountPoint, out entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry is null)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorBadMounted);
|
|
||||||
}
|
|
||||||
|
|
||||||
var used = SafeDirectorySize(entry.SlotDir);
|
|
||||||
var usedBlocks = (ulong)((used + DefaultBlockSize - 1) / DefaultBlockSize);
|
|
||||||
Span<byte> info = stackalloc byte[MountInfoSize];
|
|
||||||
info.Clear();
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], DefaultTotalBlocks); // blocks
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], usedBlocks); // freeBlocks slot reused as used
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info)
|
|
||||||
? SetReturn(ctx, 0)
|
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- delete ----
|
|
||||||
[SysAbiExport(Nid = "S1GkePI17zQ", ExportName = "sceSaveDataDelete", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataDelete(CpuContext ctx) => SaveDataDeleteCommon(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "SQWusLoK8Pw", ExportName = "sceSaveDataDelete5", Target = Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataDelete5(CpuContext ctx) => SaveDataDeleteCommon(ctx);
|
|
||||||
|
|
||||||
private static int SaveDataDeleteCommon(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// SceSaveDataDelete: +0x00 userId, +0x08 dirName*, ... (dirName drives the slot).
|
|
||||||
var deleteAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (deleteAddress == 0 ||
|
|
||||||
!TryReadInt32(ctx, deleteAddress, out var userId) ||
|
|
||||||
!ctx.TryReadUInt64(deleteAddress + 0x08, out var dirNameAddress) ||
|
|
||||||
dirNameAddress == 0 ||
|
|
||||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName) ||
|
|
||||||
string.IsNullOrWhiteSpace(dirName))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var slotDir = SaveDataStorage.SlotDir(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()), dirName);
|
|
||||||
if (!Directory.Exists(slotDir))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Delete(slotDir, recursive: true);
|
|
||||||
TraceSaveData($"delete user={userId} dir='{dirName}' path='{slotDir}'");
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- params (metadata shown in the save UI) ----
|
|
||||||
[SysAbiExport(Nid = "XgvSuIdnMlw", ExportName = "sceSaveDataGetParam", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetParam(CpuContext ctx) => TransferParam(ctx, write: false);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "85zul--eGXs", ExportName = "sceSaveDataSetParam", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSetParam(CpuContext ctx) => TransferParam(ctx, write: true);
|
|
||||||
|
|
||||||
private static int TransferParam(CpuContext ctx, bool write)
|
|
||||||
{
|
|
||||||
// rdi: mount-point string (16 bytes). rsi: paramType. rdx: SceSaveDataParam*. rcx: size.
|
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var paramAddress = ctx[CpuRegister.Rdx];
|
|
||||||
if (mountPointAddress == 0 || paramAddress == 0 ||
|
|
||||||
!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
MountEntry? entry;
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts.TryGetValue(mountPoint, out entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry is null)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorBadMounted);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (write)
|
|
||||||
{
|
|
||||||
Span<byte> raw = stackalloc byte[SaveDataParamSize];
|
|
||||||
if (!ctx.Memory.TryRead(paramAddress, raw))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = new SaveDataMetadata
|
|
||||||
{
|
|
||||||
Title = ReadAsciiField(raw.Slice(0x00, 128)),
|
|
||||||
SubTitle = ReadAsciiField(raw.Slice(0x80, 128)),
|
|
||||||
Detail = ReadAsciiField(raw.Slice(0x100, 1024)),
|
|
||||||
UserParam = BinaryPrimitives.ReadUInt32LittleEndian(raw[0x500..]),
|
|
||||||
};
|
|
||||||
SaveDataStorage.WriteMetadata(entry.SlotDir, metadata);
|
|
||||||
TraceSaveData($"set_param mount='{mountPoint}' title='{metadata.Title}'");
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var loaded = SaveDataStorage.ReadMetadata(entry.SlotDir);
|
|
||||||
var param = new byte[SaveDataParamSize];
|
|
||||||
WriteAscii(param.AsSpan(0x00, 128), loaded.Title);
|
|
||||||
WriteAscii(param.AsSpan(0x80, 128), loaded.SubTitle);
|
|
||||||
WriteAscii(param.AsSpan(0x100, 1024), loaded.Detail);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(param.AsSpan(0x500), loaded.UserParam);
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(
|
|
||||||
param.AsSpan(0x508),
|
|
||||||
new DateTimeOffset(SafeLastWriteUtc(entry.SlotDir)).ToUnixTimeSeconds());
|
|
||||||
return ctx.Memory.TryWrite(paramAddress, param)
|
|
||||||
? SetReturn(ctx, 0)
|
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- icons ----
|
|
||||||
[SysAbiExport(Nid = "c88Yy54Mx0w", ExportName = "sceSaveDataSaveIcon", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSaveIcon(CpuContext ctx) => TransferIconForMount(ctx, write: true);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "cGjO3wM3V28", ExportName = "sceSaveDataLoadIcon", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataLoadIcon(CpuContext ctx) => TransferIconForMount(ctx, write: false);
|
|
||||||
|
|
||||||
private static int TransferIconForMount(CpuContext ctx, bool write)
|
|
||||||
{
|
|
||||||
// rdi: mount-point string. rsi: SceSaveDataIcon* {buf@+0x00, bufSize@+0x08, dataSize@+0x10}.
|
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var iconAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (mountPointAddress == 0 || iconAddress == 0 ||
|
|
||||||
!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint) ||
|
|
||||||
!ctx.TryReadUInt64(iconAddress + 0x00, out var bufferAddress) ||
|
|
||||||
!ctx.TryReadUInt64(iconAddress + 0x08, out var bufferSize))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
MountEntry? entry;
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts.TryGetValue(mountPoint, out entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry is null)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorBadMounted);
|
|
||||||
}
|
|
||||||
|
|
||||||
var iconPath = SaveDataStorage.IconPath(entry.SlotDir);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (write)
|
|
||||||
{
|
|
||||||
var length = checked((int)Math.Min(bufferSize, (ulong)16 * 1024 * 1024));
|
|
||||||
var bytes = ArrayPool<byte>.Shared.Rent(length);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!ctx.Memory.TryRead(bufferAddress, bytes.AsSpan(0, length)))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(iconPath)!);
|
|
||||||
File.WriteAllBytes(iconPath, bytes.AsSpan(0, length).ToArray());
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(iconPath))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = File.ReadAllBytes(iconPath);
|
|
||||||
var copy = (int)Math.Min((ulong)data.Length, bufferSize);
|
|
||||||
if (bufferAddress != 0 && copy > 0 && !ctx.Memory.TryWrite(bufferAddress, data.AsSpan(0, copy)))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
TryWriteUInt32(ctx, iconAddress + 0x10, (uint)data.Length); // dataSize
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- size / progress / abort ----
|
|
||||||
[SysAbiExport(Nid = "A1ThglSGUwA", ExportName = "sceSaveDataGetAllSize", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetAllSize(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
long total = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var titleRoot = ResolveTitleSaveRoot(0, ResolveConfiguredTitleId());
|
|
||||||
if (Directory.Exists(titleRoot))
|
|
||||||
{
|
|
||||||
total = SafeDirectorySize(titleRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
// Report zero on an unreadable tree rather than fail the query.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
var kib = (ulong)((total + 1023) / 1024);
|
|
||||||
ctx.TryWriteUInt64(outAddress, kib);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ANmSWUiyyGQ", ExportName = "sceSaveDataGetProgress", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetProgress(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// Our operations complete synchronously, so any in-flight progress is 100%.
|
|
||||||
var outAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
Span<byte> progress = stackalloc byte[8];
|
|
||||||
progress.Clear();
|
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(progress, 1.0f);
|
|
||||||
ctx.Memory.TryWrite(outAddress, progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "Wz-4JZfeO9g", ExportName = "sceSaveDataClearProgress", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataClearProgress(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "dQ2GohUHXzk", ExportName = "sceSaveDataAbort", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataAbort(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "eBSSNIG6hMk", ExportName = "sceSaveDataGetEventInfo", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetEventInfo(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "52pL2GKkdjA", ExportName = "sceSaveDataSetEventInfo", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSetEventInfo(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "Z7z6HXWORJY", ExportName = "sceSaveDataSaveIconByPath", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSaveIconByPath(CpuContext ctx) => SetReturn(ctx, 0);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "SN7rTPHS+Cg", ExportName = "sceSaveDataGetSaveDataCount", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetSaveDataCount(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var count = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var titleRoot = ResolveTitleSaveRoot(0, ResolveConfiguredTitleId());
|
|
||||||
if (Directory.Exists(titleRoot))
|
|
||||||
{
|
|
||||||
foreach (var dir in Directory.EnumerateDirectories(titleRoot))
|
|
||||||
{
|
|
||||||
if (!string.Equals(Path.GetFileName(dir), "sce_sdmemory", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
// Report zero on an unreadable tree.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
TryWriteUInt32(ctx, outAddress, (uint)count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "pc4guaUPVqA", ExportName = "sceSaveDataGetMountedSaveDataCount", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetMountedSaveDataCount(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
int mounted;
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
mounted = _mounts.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
TryWriteUInt32(ctx, outAddress, (uint)mounted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- SaveDataMemory v1 aliases (identical arg layout to the v2 forms) ----
|
|
||||||
[SysAbiExport(Nid = "v7AAAMo0Lz4", ExportName = "sceSaveDataSetupSaveDataMemory", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSetupSaveDataMemory(CpuContext ctx) => SaveDataSetupSaveDataMemory2(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "7Bt5pBC-Aco", ExportName = "sceSaveDataGetSaveDataMemory", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataGetSaveDataMemory(CpuContext ctx) => SaveDataGetSaveDataMemory2(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "h3YURzXGSVQ", ExportName = "sceSaveDataSetSaveDataMemory", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataSetSaveDataMemory(CpuContext ctx) => SaveDataSetSaveDataMemory2(ctx);
|
|
||||||
|
|
||||||
private static string ReadAsciiField(ReadOnlySpan<byte> field)
|
|
||||||
{
|
|
||||||
var length = field.IndexOf((byte)0);
|
|
||||||
if (length < 0)
|
|
||||||
{
|
|
||||||
length = field.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Encoding.ASCII.GetString(field[..length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long SafeDirectorySize(string root)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Directory.Exists(root) ? GetDirectorySize(root) : 0;
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DateTime SafeLastWriteUtc(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Directory.Exists(path) ? Directory.GetLastWriteTimeUtc(path) : DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
return DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -745,10 +221,6 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
const string mountPoint = "/savedata0";
|
const string mountPoint = "/savedata0";
|
||||||
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath);
|
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath);
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts[mountPoint] = new MountEntry(savePath, dirName, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[MountResultSize];
|
Span<byte> result = stackalloc byte[MountResultSize];
|
||||||
result.Clear();
|
result.Clear();
|
||||||
@@ -792,15 +264,28 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
// A small RDX value is a flag, and RCX contains the output address.
|
// The resource-out pointer's argument slot varies by SDK revision: some
|
||||||
// A larger RDX value is the output address for the older ABI.
|
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
|
||||||
|
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
|
||||||
|
// real out-pointer in rcx. Probe the plausible candidates and write the
|
||||||
|
// handle to the first writable one instead of faulting on a bad rdx.
|
||||||
|
// This is a stub-level create (matches shadPS4's return-OK semantics);
|
||||||
|
// never return MEMORY_FAULT for it, or the guest treats savedata init as
|
||||||
|
// failed and never advances.
|
||||||
var resourceAddress = 0UL;
|
var resourceAddress = 0UL;
|
||||||
var selectedAddress = SelectTransactionResourceAddress(
|
foreach (var candidate in new[]
|
||||||
ctx[CpuRegister.Rdx],
|
{
|
||||||
ctx[CpuRegister.Rcx]);
|
ctx[CpuRegister.Rdx],
|
||||||
if (selectedAddress != 0 && TryWriteUInt32(ctx, selectedAddress, id))
|
ctx[CpuRegister.Rcx],
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
ctx[CpuRegister.R9],
|
||||||
|
})
|
||||||
{
|
{
|
||||||
resourceAddress = selectedAddress;
|
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
||||||
|
{
|
||||||
|
resourceAddress = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
@@ -809,16 +294,6 @@ public static class SaveDataExports
|
|||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ulong SelectTransactionResourceAddress(ulong rdx, ulong rcx)
|
|
||||||
{
|
|
||||||
if (rdx == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rdx <= ushort.MaxValue ? rcx : rdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "lJUQuaKqoKY",
|
Nid = "lJUQuaKqoKY",
|
||||||
ExportName = "sceSaveDataDeleteTransactionResource",
|
ExportName = "sceSaveDataDeleteTransactionResource",
|
||||||
@@ -843,20 +318,10 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataUmount2(CpuContext ctx)
|
public static int SaveDataUmount2(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// rdi: SceSaveDataMountPoint* (16-byte mount point string) for umount2.
|
// Unmounting a save directory always succeeds in the stub filesystem;
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
// returning an error here makes the game's save flow stall before it
|
||||||
if (mountPointAddress != 0 && TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint) &&
|
// hands control to the title/gameplay state.
|
||||||
!string.IsNullOrEmpty(mountPoint))
|
TraceSaveData($"umount2 user={unchecked((int)ctx[CpuRegister.Rdi])}");
|
||||||
{
|
|
||||||
lock (_mountGate)
|
|
||||||
{
|
|
||||||
_mounts.Remove(mountPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
|
|
||||||
TraceSaveData($"umount2 mount='{mountPoint}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,12 +405,9 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
|
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
|
||||||
{
|
{
|
||||||
var metadata = SaveDataStorage.ReadMetadata(entry.Path);
|
|
||||||
var param = new byte[SaveDataParamSize];
|
var param = new byte[SaveDataParamSize];
|
||||||
WriteAscii(param.AsSpan(0x00, 128), metadata.Title);
|
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
|
||||||
WriteAscii(param.AsSpan(0x80, 128), metadata.SubTitle);
|
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
|
||||||
WriteAscii(param.AsSpan(0x100, 1024), string.IsNullOrEmpty(metadata.Detail) ? entry.Name : metadata.Detail);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(param.AsSpan(0x500), metadata.UserParam);
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(
|
BinaryPrimitives.WriteInt64LittleEndian(
|
||||||
param.AsSpan(0x508, sizeof(long)),
|
param.AsSpan(0x508, sizeof(long)),
|
||||||
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
|
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
|
||||||
@@ -1012,14 +474,11 @@ public static class SaveDataExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves are keyed by title id only (single-user emulation) under
|
|
||||||
// ~/SharpEmu/Saves/<titleId>/; userId is accepted for API fidelity but not
|
|
||||||
// part of the host path.
|
|
||||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||||
SaveDataStorage.TitleRoot(ResolveSaveDataRoot(), titleId);
|
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||||
|
|
||||||
private static string ResolveSaveDataMemoryPath(int userId) =>
|
private static string ResolveSaveDataMemoryPath(int userId) =>
|
||||||
SaveDataStorage.MemoryPath(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()));
|
Path.Combine(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()), "sce_sdmemory", "memory.dat");
|
||||||
|
|
||||||
private static bool TryReadMemoryData(
|
private static bool TryReadMemoryData(
|
||||||
CpuContext ctx, ulong address, out ulong buffer, out ulong size, out ulong offset)
|
CpuContext ctx, ulong address, out ulong buffer, out ulong size, out ulong offset)
|
||||||
@@ -1031,7 +490,14 @@ public static class SaveDataExports
|
|||||||
ctx.TryReadUInt64(address + 0x10, out offset);
|
ctx.TryReadUInt64(address + 0x10, out offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveSaveDataRoot() => SaveDataStorage.Root();
|
private static string ResolveSaveDataRoot()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||||
|
var root = string.IsNullOrWhiteSpace(configured)
|
||||||
|
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
|
||||||
|
: configured;
|
||||||
|
return Path.GetFullPath(root);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveConfiguredTitleId()
|
private static string ResolveConfiguredTitleId()
|
||||||
{
|
{
|
||||||
@@ -1059,7 +525,12 @@ public static class SaveDataExports
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SanitizePathSegment(string value) => SaveDataStorage.Sanitize(value);
|
private static string SanitizePathSegment(string value)
|
||||||
|
{
|
||||||
|
var invalid = Path.GetInvalidFileNameChars();
|
||||||
|
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
|
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
|
||||||
{
|
{
|
||||||
@@ -1319,17 +790,8 @@ public static class SaveDataExports
|
|||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(ResolveSaveDataMemoryPath(userId)))
|
return ctx.SetReturn(
|
||||||
{
|
File.Exists(ResolveSaveDataMemoryPath(userId)) ? 0 : OrbisSaveDataErrorMemoryNotReady);
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorMemoryNotReady);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The write already reached disk synchronously, but the guest treats
|
|
||||||
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
|
|
||||||
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
|
|
||||||
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
|
|
||||||
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.SaveData;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Host-side layout and metadata for PS5 save data. Saves live under
|
|
||||||
/// <c>~/SharpEmu/Saves/<titleId>/<dirName>/</c> (overridable via
|
|
||||||
/// <c>SHARPEMU_SAVEDATA_DIR</c>); the game's files are written directly inside a
|
|
||||||
/// slot through the mounted <c>/savedata0</c> filesystem, and the PS5 UI
|
|
||||||
/// metadata (title/subtitle/detail/userParam) plus icon live under
|
|
||||||
/// <c><slot>/sce_sys/</c>. This type is pure filesystem logic with no guest
|
|
||||||
/// interop so the path and metadata handling can be unit-tested.
|
|
||||||
/// </summary>
|
|
||||||
public static class SaveDataStorage
|
|
||||||
{
|
|
||||||
/// <summary>Root of all saves: the env override, else <c>~/SharpEmu/Saves</c>.</summary>
|
|
||||||
public static string Root(string? overrideDir = null)
|
|
||||||
{
|
|
||||||
var configured = overrideDir ?? Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
|
||||||
var root = string.IsNullOrWhiteSpace(configured)
|
|
||||||
? Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
"SharpEmu",
|
|
||||||
"Saves")
|
|
||||||
: configured;
|
|
||||||
return Path.GetFullPath(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Per-title directory: <c><root>/<titleId></c>.</summary>
|
|
||||||
public static string TitleRoot(string root, string titleId) =>
|
|
||||||
Path.Combine(root, Sanitize(titleId));
|
|
||||||
|
|
||||||
/// <summary>A single save slot: <c><titleRoot>/<dirName></c>.</summary>
|
|
||||||
public static string SlotDir(string titleRoot, string dirName) =>
|
|
||||||
Path.Combine(titleRoot, Sanitize(dirName));
|
|
||||||
|
|
||||||
/// <summary>The SaveDataMemory blob shared by a title.</summary>
|
|
||||||
public static string MemoryPath(string titleRoot) =>
|
|
||||||
Path.Combine(titleRoot, "sce_sdmemory", "memory.dat");
|
|
||||||
|
|
||||||
public static string ParamPath(string slotDir) =>
|
|
||||||
Path.Combine(slotDir, "sce_sys", "param.json");
|
|
||||||
|
|
||||||
public static string IconPath(string slotDir) =>
|
|
||||||
Path.Combine(slotDir, "sce_sys", "icon0.png");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replaces characters that are invalid in a host path segment. Empty or
|
|
||||||
/// all-invalid input collapses to "default" so a bad guest name can never
|
|
||||||
/// escape the save root or produce an empty segment.
|
|
||||||
/// </summary>
|
|
||||||
public static string Sanitize(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
return "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
var invalid = Path.GetInvalidFileNameChars();
|
|
||||||
Span<char> buffer = value.Length <= 128 ? stackalloc char[value.Length] : new char[value.Length];
|
|
||||||
for (var i = 0; i < value.Length; i++)
|
|
||||||
{
|
|
||||||
var ch = value[i];
|
|
||||||
buffer[i] = Array.IndexOf(invalid, ch) >= 0 ? '_' : ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sanitized = new string(buffer).Trim();
|
|
||||||
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Reads a slot's metadata, or defaults if none has been written.</summary>
|
|
||||||
public static SaveDataMetadata ReadMetadata(string slotDir)
|
|
||||||
{
|
|
||||||
var path = ParamPath(slotDir);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parsed = JsonSerializer.Deserialize(File.ReadAllText(path), SaveDataMetadataContext.Default.SaveDataMetadata);
|
|
||||||
if (parsed is not null)
|
|
||||||
{
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is JsonException or IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
// Fall through to defaults on a corrupt or unreadable metadata file.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SaveDataMetadata.CreateDefault(Path.GetFileName(slotDir.TrimEnd(Path.DirectorySeparatorChar)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Writes a slot's metadata, creating <c>sce_sys/</c> as needed.</summary>
|
|
||||||
public static void WriteMetadata(string slotDir, SaveDataMetadata metadata)
|
|
||||||
{
|
|
||||||
var path = ParamPath(slotDir);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
||||||
File.WriteAllText(path, JsonSerializer.Serialize(metadata, SaveDataMetadataContext.Default.SaveDataMetadata));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>PS5 save-slot metadata surfaced by sceSaveDataGetParam / the save UI.</summary>
|
|
||||||
public sealed record SaveDataMetadata
|
|
||||||
{
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string Title { get; init; } = "Saved Data";
|
|
||||||
|
|
||||||
[JsonPropertyName("subTitle")]
|
|
||||||
public string SubTitle { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("detail")]
|
|
||||||
public string Detail { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("userParam")]
|
|
||||||
public uint UserParam { get; init; }
|
|
||||||
|
|
||||||
public static SaveDataMetadata CreateDefault(string dirName) =>
|
|
||||||
new() { Title = string.IsNullOrWhiteSpace(dirName) ? "Saved Data" : dirName };
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(SaveDataMetadata))]
|
|
||||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
|
||||||
internal sealed partial class SaveDataMetadataContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler.Metal\SharpEmu.ShaderCompiler.Metal.csproj" />
|
|
||||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
<ProjectReference Include="..\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||||
<!-- SysAbi export generator + analyzers (compile-time registry, NID validation). -->
|
<!-- SysAbi export generator + analyzers (compile-time registry, NID validation). -->
|
||||||
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
}
|
||||||
@@ -36,7 +36,6 @@ 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();
|
||||||
@@ -75,8 +74,11 @@ 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;
|
||||||
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
if (milliseconds < 1000.0)
|
||||||
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
{
|
||||||
|
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
||||||
|
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +88,6 @@ 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.
|
||||||
@@ -166,7 +165,7 @@ public static class PerfOverlay
|
|||||||
Environment.ProcessorCount;
|
Environment.ProcessorCount;
|
||||||
_lastCpuTime = cpuTime;
|
_lastCpuTime = cpuTime;
|
||||||
|
|
||||||
var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0;
|
var drawsPerFrame = _fps > 0.5 ? _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
|
||||||
@@ -177,9 +176,7 @@ 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}";
|
||||||
var heapMb = GC.GetTotalMemory(false) / (1024 * 1024);
|
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
|
||||||
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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,14 +131,9 @@ public static class VideoOutExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS can run either backend (Vulkan through MoltenVK, or Metal), so
|
|
||||||
// name the active one in the title to make which is in use unambiguous.
|
|
||||||
var backendSuffix = OperatingSystem.IsMacOS()
|
|
||||||
? $" ({GuestGpu.Current.BackendName})"
|
|
||||||
: string.Empty;
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_windowTitle = $"{_windowTitle} · {gpuName.Trim()}{backendSuffix}";
|
_windowTitle = $"{_windowTitle} · {gpuName.Trim()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +166,11 @@ public static class VideoOutExports
|
|||||||
HostSessionControl.RequestShutdown(reason);
|
HostSessionControl.RequestShutdown(reason);
|
||||||
|
|
||||||
// A hosted game can still be issuing AGC work after it requests its
|
// A hosted game can still be issuing AGC work after it requests its
|
||||||
// own shutdown. Keep the presenter's resources alive until the GUI
|
// own shutdown. Keep the Vulkan resources alive until the GUI session
|
||||||
// session reaches its guest-safe exit path and disposes the host
|
// reaches its guest-safe exit path and disposes the host surface.
|
||||||
// surface.
|
|
||||||
if (!embedded)
|
if (!embedded)
|
||||||
{
|
{
|
||||||
GuestGpu.Current.RequestClose();
|
VulkanVideoPresenter.RequestClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The embedded GUI owns the process lifetime. A guest shutdown should
|
// The embedded GUI owns the process lifetime. A guest shutdown should
|
||||||
@@ -1076,12 +1070,6 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SceVideoOutBufferCategory is a 32-bit enum passed on the stack; the
|
|
||||||
// upper 32 bits of the slot are stale (games leave GNM magic there), so
|
|
||||||
// mask before validating. UNCOMPRESSED (0) and COMPRESSED (1) are both
|
|
||||||
// valid — we present either identically, so accept both.
|
|
||||||
var category = (uint)categoryRaw;
|
|
||||||
|
|
||||||
if (!TryGetPort(handle, out var port))
|
if (!TryGetPort(handle, out var port))
|
||||||
{
|
{
|
||||||
return OrbisVideoOutErrorInvalidHandle;
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
@@ -1102,7 +1090,7 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidValue;
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category > 1 || option != 0)
|
if (categoryRaw != 0 || option != 0)
|
||||||
{
|
{
|
||||||
return OrbisVideoOutErrorInvalidValue;
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
}
|
}
|
||||||
@@ -1221,7 +1209,7 @@ public static class VideoOutExports
|
|||||||
{
|
{
|
||||||
TriggerFlipEvents();
|
TriggerFlipEvents();
|
||||||
}
|
}
|
||||||
else if (GuestGpu.Current.SubmitOrderedGuestAction(
|
else if (VulkanVideoPresenter.SubmitOrderedGuestAction(
|
||||||
TriggerFlipEvents,
|
TriggerFlipEvents,
|
||||||
$"videoout flip complete handle={handle} index={bufferIndex}") == 0)
|
$"videoout flip complete handle={handle} index={bufferIndex}") == 0)
|
||||||
{
|
{
|
||||||
@@ -1275,7 +1263,7 @@ public static class VideoOutExports
|
|||||||
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||||
var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters();
|
var (draws, drawMs, pipelines, spirvCompiles) = VulkanVideoPresenter.ReadAndResetPerfCounters();
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||||
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
||||||
@@ -1714,7 +1702,7 @@ public static class VideoOutExports
|
|||||||
SceVideoOutPixelFormat2B10G10R10A2Bt2100Pq;
|
SceVideoOutPixelFormat2B10G10R10A2Bt2100Pq;
|
||||||
|
|
||||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||||
// the backend keys its guest-image registry on (see the presenter's
|
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
|
||||||
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||||
// Unknown formats default to 56 (8-bit RGBA) with a logged warning so games
|
// Unknown formats default to 56 (8-bit RGBA) with a logged warning so games
|
||||||
// display something rather than silently failing the flip pipeline.
|
// display something rather than silently failing the flip pipeline.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
|
|
||||||
namespace SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
// MSL-specific shader artifact types. These stay beside the MSL emitter (not in the
|
|
||||||
// backend-neutral SharpEmu.ShaderCompiler project): each codegen owns its own
|
|
||||||
// compiled-shader shape, mirroring Gen5SpirvShader on the Vulkan side.
|
|
||||||
public enum Gen5MslStage
|
|
||||||
{
|
|
||||||
Vertex,
|
|
||||||
Pixel,
|
|
||||||
Compute,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A translated Metal shader: MSL source text plus the reflection data the Metal
|
|
||||||
/// backend needs to bind it. Buffer argument indices follow the translation
|
|
||||||
/// contract documented on <see cref="Gen5MslTranslator"/>: global memory buffers
|
|
||||||
/// occupy [[buffer(globalBufferBase + i)]] in <see cref="GlobalMemoryBindings"/>
|
|
||||||
/// order, and compute shaders reserve one trailing slot for the dispatch-limit
|
|
||||||
/// uniform. Unlike SPIR-V, Metal fixes the threadgroup size at dispatch time, so
|
|
||||||
/// the size the shader was translated for is carried here.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para><see cref="UniformsBufferIndex"/> is the [[buffer(N)]] slot this stage's
|
|
||||||
/// SharpEmuUniforms argument was emitted at (globalBufferBase +
|
|
||||||
/// totalGlobalBufferCount, both translation-time inputs). Stages sharing a draw
|
|
||||||
/// can disagree — a vertex stage whose guest buffers sit after the pixel
|
|
||||||
/// stage's has a higher base — so the presenter must bind the uniforms buffer
|
|
||||||
/// per stage at this exact index rather than assuming one shared slot.</para>
|
|
||||||
/// <para>Texture slots are global across a draw's stages ([[texture(
|
|
||||||
/// ImageBindingBase + i)]]). Samplers live in a per-stage argument buffer
|
|
||||||
/// bound at <see cref="SamplerArgBufferIndex"/> (Metal caps direct
|
|
||||||
/// [[sampler(N)]] slots at 16 per stage, but shaders sample more), holding one
|
|
||||||
/// sampler per sampled image. <see cref="SamplerSlots"/> maps this stage's
|
|
||||||
/// image binding index to its [[id(N)]] entry in that argument buffer, -1 for
|
|
||||||
/// storage images that take none; <see cref="SamplerCount"/> is the entry
|
|
||||||
/// count.</para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed record Gen5MslShader(
|
|
||||||
string Source,
|
|
||||||
string EntryPoint,
|
|
||||||
Gen5MslStage Stage,
|
|
||||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
|
||||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
|
||||||
uint AttributeCount,
|
|
||||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs,
|
|
||||||
uint ThreadgroupSizeX = 1,
|
|
||||||
uint ThreadgroupSizeY = 1,
|
|
||||||
uint ThreadgroupSizeZ = 1,
|
|
||||||
int UniformsBufferIndex = -1,
|
|
||||||
int ImageBindingBase = 0,
|
|
||||||
IReadOnlyList<int>? SamplerSlots = null,
|
|
||||||
int SamplerCount = 0,
|
|
||||||
int SamplerArgBufferIndex = -1);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,888 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
|
|
||||||
namespace SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
public static partial class Gen5MslTranslator
|
|
||||||
{
|
|
||||||
private sealed partial class CompilationContext
|
|
||||||
{
|
|
||||||
private const uint ImageDescriptorDwords = 8;
|
|
||||||
private const uint SamplerDescriptorDwords = 4;
|
|
||||||
|
|
||||||
// ---- image resources ----
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Classifies every image binding (storage vs sampled, component kind
|
|
||||||
/// from the descriptor's unified format) and seeds the PC lookup,
|
|
||||||
/// mirroring DeclareImages on the SPIR-V side. MSL needs no format on
|
|
||||||
/// the texture type — only the component type and access.
|
|
||||||
/// </summary>
|
|
||||||
private void DeclareImageKinds()
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _evaluation.ImageBindings.Count; index++)
|
|
||||||
{
|
|
||||||
var binding = _evaluation.ImageBindings[index];
|
|
||||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
|
||||||
var isStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
|
||||||
_imageKinds.Add((isStorage, DecodeImageComponentKind(binding.ResourceDescriptor)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed each binding's access from the opcode that defined it; the body
|
|
||||||
// emission (TryEmitImage) then ORs in the access of every instruction
|
|
||||||
// that resolves to the same binding, so a load and a store sharing one
|
|
||||||
// binding correctly become read_write.
|
|
||||||
_imageBindingReads = new bool[_imageKinds.Count];
|
|
||||||
_imageBindingWrites = new bool[_imageKinds.Count];
|
|
||||||
for (var index = 0; index < _evaluation.ImageBindings.Count; index++)
|
|
||||||
{
|
|
||||||
MarkImageBindingAccess(index, _evaluation.ImageBindings[index].Opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign one sampler per sampled image (storage images take none).
|
|
||||||
// Computed before body emission because the sample calls reference
|
|
||||||
// the slots. Samplers live in an argument buffer (see
|
|
||||||
// EmitImageArguments), so there is no 16-slot cap to dedup against —
|
|
||||||
// each image keeps its own sampler, matching the SPIR-V/Vulkan path.
|
|
||||||
_samplerSlots = new int[_imageKinds.Count];
|
|
||||||
_samplerCount = 0;
|
|
||||||
for (var index = 0; index < _imageKinds.Count; index++)
|
|
||||||
{
|
|
||||||
_samplerSlots[index] = _imageKinds[index].IsStorage ? -1 : _samplerCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Records that <paramref name="opcode"/> reads and/or writes the
|
|
||||||
/// storage image at <paramref name="bindingIndex"/>, so EmitImageArguments
|
|
||||||
/// can pick the minimal Metal access qualifier.</summary>
|
|
||||||
private void MarkImageBindingAccess(int bindingIndex, string opcode)
|
|
||||||
{
|
|
||||||
if ((uint)bindingIndex >= (uint)_imageBindingReads.Length)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opcode.StartsWith("ImageStore", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_imageBindingWrites[bindingIndex] = true;
|
|
||||||
}
|
|
||||||
else if (opcode.StartsWith("ImageAtomic", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_imageBindingReads[bindingIndex] = true;
|
|
||||||
_imageBindingWrites[bindingIndex] = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ImageLoad/ImageLoadMip and ImageGetResinfo read the texture;
|
|
||||||
// sampled ops are non-storage and ignore these flags.
|
|
||||||
_imageBindingReads[bindingIndex] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>"float", "int", or "uint" from the descriptor's unified format.</summary>
|
|
||||||
private static string DecodeImageComponentKind(IReadOnlyList<uint> descriptor)
|
|
||||||
{
|
|
||||||
if (descriptor.Count < 2)
|
|
||||||
{
|
|
||||||
return "float";
|
|
||||||
}
|
|
||||||
|
|
||||||
var unifiedFormat = (descriptor[1] >> 20) & 0x1FFu;
|
|
||||||
if (!Gfx10UnifiedFormat.TryDecode(unifiedFormat, out _, out var numberType))
|
|
||||||
{
|
|
||||||
return "float";
|
|
||||||
}
|
|
||||||
|
|
||||||
return numberType switch
|
|
||||||
{
|
|
||||||
4 => "uint",
|
|
||||||
5 => "int",
|
|
||||||
_ => "float",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Per image binding: its sampler's [[id(N)]] inside the sampler
|
|
||||||
/// argument buffer, or -1 for storage images. Set by DeclareImageKinds.</summary>
|
|
||||||
private int[] _samplerSlots = [];
|
|
||||||
|
|
||||||
/// <summary>Number of sampled images (= sampler argument-buffer entries).</summary>
|
|
||||||
private int _samplerCount;
|
|
||||||
|
|
||||||
/// <summary>Buffer slot the sampler argument buffer binds to, past this
|
|
||||||
/// stage's global buffers, uniforms, and scalar-state buffer.</summary>
|
|
||||||
private int SamplerArgBufferIndex =>
|
|
||||||
Math.Max(UniformsBufferIndex, _initialScalarBufferIndex) + 1;
|
|
||||||
|
|
||||||
/// <summary>Emits the texture arguments (direct [[texture(N)]] slots, which
|
|
||||||
/// run to 31 — enough) plus, when the stage samples anything, the sampler
|
|
||||||
/// argument buffer. Samplers go through an argument buffer rather than
|
|
||||||
/// [[sampler(N)]] slots because Metal caps those at 16 per stage while
|
|
||||||
/// real shaders sample more (void Terrarium's scene shader: 17); argument
|
|
||||||
/// buffers have no such limit on Apple Silicon.</summary>
|
|
||||||
private void EmitImageArguments(StringBuilder source)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _imageKinds.Count; index++)
|
|
||||||
{
|
|
||||||
var (isStorage, kind) = _imageKinds[index];
|
|
||||||
var textureSlot = _imageBindingBase + index;
|
|
||||||
if (isStorage)
|
|
||||||
{
|
|
||||||
// Minimal access keeps read_write textures under Metal's cap
|
|
||||||
// of 8 per function: only images that are both read and
|
|
||||||
// written (or resolve a load and a store to one binding) need
|
|
||||||
// read_write; the rest are read-only or write-only.
|
|
||||||
var access = _imageBindingWrites[index]
|
|
||||||
? (_imageBindingReads[index] ? "read_write" : "write")
|
|
||||||
: "read";
|
|
||||||
source.AppendLine(
|
|
||||||
$" texture2d<{kind}, access::{access}> tex{index} [[texture({textureSlot})]],");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
source.AppendLine($" texture2d<{kind}> tex{index} [[texture({textureSlot})]],");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_samplerCount > 0)
|
|
||||||
{
|
|
||||||
source.AppendLine(
|
|
||||||
$" constant Gen5Samplers& sharpemu_samplers [[buffer({SamplerArgBufferIndex})]],");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Declares the sampler argument-buffer struct at file scope (one
|
|
||||||
/// sampler per sampled image). Empty when the stage samples nothing.</summary>
|
|
||||||
private void EmitSamplerArgumentBufferStruct(StringBuilder source)
|
|
||||||
{
|
|
||||||
if (_samplerCount == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
source.AppendLine("struct Gen5Samplers");
|
|
||||||
source.AppendLine("{");
|
|
||||||
for (var slot = 0; slot < _samplerCount; slot++)
|
|
||||||
{
|
|
||||||
source.AppendLine($" sampler smp{slot} [[id({slot})]];");
|
|
||||||
}
|
|
||||||
|
|
||||||
source.AppendLine("};");
|
|
||||||
source.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryResolveDominatingImageBinding(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ImageControl control,
|
|
||||||
out int bindingIndex)
|
|
||||||
{
|
|
||||||
if (_imageBindingByPc.TryGetValue(instruction.Pc, out bindingIndex) &&
|
|
||||||
bindingIndex < _imageKinds.Count)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var storage = Gen5ShaderTranslator.IsStorageImageOperation(instruction.Opcode);
|
|
||||||
for (var index = 0; index < _evaluation.ImageBindings.Count; index++)
|
|
||||||
{
|
|
||||||
var candidate = _evaluation.ImageBindings[index];
|
|
||||||
if (candidate.Control.ScalarResource != control.ScalarResource ||
|
|
||||||
candidate.Control.ScalarSampler != control.ScalarSampler ||
|
|
||||||
Gen5ShaderTranslator.IsStorageImageOperation(candidate.Opcode) != storage ||
|
|
||||||
!HasSameScalarDefinitions(
|
|
||||||
candidate.Pc,
|
|
||||||
instruction.Pc,
|
|
||||||
control.ScalarResource,
|
|
||||||
ImageDescriptorDwords) ||
|
|
||||||
(UsesSampler(instruction.Opcode) &&
|
|
||||||
!HasSameScalarDefinitions(
|
|
||||||
candidate.Pc,
|
|
||||||
instruction.Pc,
|
|
||||||
control.ScalarSampler,
|
|
||||||
SamplerDescriptorDwords)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindingIndex = index;
|
|
||||||
_imageBindingByPc.Add(instruction.Pc, index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindingIndex = -1;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasSameScalarDefinitions(
|
|
||||||
uint candidatePc,
|
|
||||||
uint targetPc,
|
|
||||||
uint firstRegister,
|
|
||||||
uint registerCount)
|
|
||||||
{
|
|
||||||
if (firstRegister + registerCount > ScalarRegisterFileCount ||
|
|
||||||
!_scalarDefinitionsBeforePc.TryGetValue(candidatePc, out var candidate) ||
|
|
||||||
!_scalarDefinitionsBeforePc.TryGetValue(targetPc, out var target))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var register = firstRegister;
|
|
||||||
register < firstRegister + registerCount;
|
|
||||||
register++)
|
|
||||||
{
|
|
||||||
var definition = candidate[register];
|
|
||||||
if (definition is ConflictingScalarDefinition or UnreachableScalarDefinition ||
|
|
||||||
target[register] != definition)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool UsesSampler(string opcode) =>
|
|
||||||
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
|
||||||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
// ---- image instruction emission ----
|
|
||||||
|
|
||||||
private bool TryEmitImage(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ImageControl image,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
error = string.Empty;
|
|
||||||
if (!TryResolveDominatingImageBinding(instruction, image, out var bindingIndex))
|
|
||||||
{
|
|
||||||
error = $"unresolved image binding t=s{image.ScalarResource} s=s{image.ScalarSampler}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The resolving instruction may differ from the one that defined the
|
|
||||||
// binding (a store can dominate a load's binding); fold its access in.
|
|
||||||
MarkImageBindingAccess(bindingIndex, instruction.Opcode);
|
|
||||||
var (isStorage, kind) = _imageKinds[bindingIndex];
|
|
||||||
var texture = $"tex{bindingIndex}";
|
|
||||||
|
|
||||||
if (instruction.Opcode == "ImageGetResinfo")
|
|
||||||
{
|
|
||||||
var width = Temp("uint", isStorage
|
|
||||||
? $"{texture}.get_width()"
|
|
||||||
: $"{texture}.get_width(0)");
|
|
||||||
var height = Temp("uint", isStorage
|
|
||||||
? $"{texture}.get_height()"
|
|
||||||
: $"{texture}.get_height(0)");
|
|
||||||
uint outputIndex = 0;
|
|
||||||
for (var component = 0; component < 4; component++)
|
|
||||||
{
|
|
||||||
if ((image.Dmask & (1u << component)) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
StoreVector(
|
|
||||||
image.VectorData + outputIndex++,
|
|
||||||
component switch
|
|
||||||
{
|
|
||||||
0 => width,
|
|
||||||
1 => height,
|
|
||||||
_ => "1u",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.Opcode is "ImageStore" or "ImageStoreMip")
|
|
||||||
{
|
|
||||||
if (!isStorage)
|
|
||||||
{
|
|
||||||
error = "image store is not bound as storage";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = Temp("int", $"as_type<int>({ImageIntegerAddress(image, 0)})");
|
|
||||||
var y = Temp("int", $"as_type<int>({ImageIntegerAddress(image, 1)})");
|
|
||||||
var components = new string[4];
|
|
||||||
uint sourceIndex = 0;
|
|
||||||
for (var component = 0; component < 4; component++)
|
|
||||||
{
|
|
||||||
components[component] = (image.Dmask & (1u << component)) != 0
|
|
||||||
? ImageTexelComponent(kind, ImageStoreComponent(image, kind, sourceIndex++))
|
|
||||||
: kind == "float" ? "0.0f" : "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds-checked, EXEC-guarded write.
|
|
||||||
Line($"if (exec && {x} >= 0 && {y} >= 0 && {x} < (int){texture}.get_width() && {y} < (int){texture}.get_height())");
|
|
||||||
Line("{");
|
|
||||||
_indent++;
|
|
||||||
Line($"{texture}.write({VectorLiteral(kind)}({components[0]}, {components[1]}, {components[2]}, {components[3]}), uint2((uint){x}, (uint){y}));");
|
|
||||||
_indent--;
|
|
||||||
Line("}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStorage && instruction.Opcode is not ("ImageLoad" or "ImageLoadMip"))
|
|
||||||
{
|
|
||||||
error = $"unsupported storage image opcode {instruction.Opcode}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string sampled;
|
|
||||||
var writeAllComponents = false;
|
|
||||||
if (instruction.Opcode is "ImageLoad" or "ImageLoadMip")
|
|
||||||
{
|
|
||||||
var mip = _evaluation.ImageBindings[bindingIndex].MipLevel ?? 0;
|
|
||||||
var widthQuery = isStorage ? $"{texture}.get_width()" : $"{texture}.get_width({mip}u)";
|
|
||||||
var heightQuery = isStorage ? $"{texture}.get_height()" : $"{texture}.get_height({mip}u)";
|
|
||||||
var x = Temp(
|
|
||||||
"uint",
|
|
||||||
$"(uint)clamp(as_type<int>({ImageIntegerAddress(image, 0)}), 0, (int){widthQuery} - 1)");
|
|
||||||
var y = Temp(
|
|
||||||
"uint",
|
|
||||||
$"(uint)clamp(as_type<int>({ImageIntegerAddress(image, 1)}), 0, (int){heightQuery} - 1)");
|
|
||||||
sampled = Temp(
|
|
||||||
$"vec<{kind}, 4>",
|
|
||||||
isStorage
|
|
||||||
? $"{texture}.read(uint2({x}, {y}))"
|
|
||||||
: $"{texture}.read(uint2({x}, {y}), {mip}u)");
|
|
||||||
}
|
|
||||||
else if (instruction.Opcode.StartsWith("ImageSample", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (!TryEmitImageSample(instruction, image, bindingIndex, kind, out sampled, out error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (instruction.Opcode.StartsWith("ImageGather4", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (!TryEmitImageGather(instruction, image, bindingIndex, kind, out sampled, out error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeAllComponents = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
error = $"unsupported image opcode {instruction.Opcode}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputValues = new List<string>(4);
|
|
||||||
for (var component = 0; component < 4; component++)
|
|
||||||
{
|
|
||||||
if (!writeAllComponents && (image.Dmask & (1u << component)) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = $"{sampled}[{component}]";
|
|
||||||
outputValues.Add(kind == "uint" ? value : AsUInt(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image.D16)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < outputValues.Count; index += 2)
|
|
||||||
{
|
|
||||||
var low = outputValues[index];
|
|
||||||
var high = index + 1 < outputValues.Count ? outputValues[index + 1] : "0u";
|
|
||||||
StoreVector(
|
|
||||||
image.VectorData + (uint)(index / 2),
|
|
||||||
PackImageD16(kind, low, high));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var index = 0; index < outputValues.Count; index++)
|
|
||||||
{
|
|
||||||
StoreVector(image.VectorData + (uint)index, outputValues[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryEmitImageSample(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ImageControl image,
|
|
||||||
int bindingIndex,
|
|
||||||
string kind,
|
|
||||||
out string sampled,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
sampled = string.Empty;
|
|
||||||
error = string.Empty;
|
|
||||||
var opcode = instruction.Opcode;
|
|
||||||
var texture = $"tex{bindingIndex}";
|
|
||||||
var samplerName = $"sharpemu_samplers.smp{_samplerSlots[bindingIndex]}";
|
|
||||||
var hasOffset = opcode.EndsWith("O", StringComparison.Ordinal);
|
|
||||||
var hasCompare = opcode.Contains("SampleC", StringComparison.Ordinal);
|
|
||||||
var hasGradients = opcode.Contains("SampleD", StringComparison.Ordinal);
|
|
||||||
var hasZeroLod = opcode.Contains("Lz", StringComparison.Ordinal);
|
|
||||||
var hasLod = !hasZeroLod && opcode.Contains("SampleL", StringComparison.Ordinal);
|
|
||||||
var hasBias = opcode.Contains("SampleB", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
// RDNA MIMG address operands are ordered
|
|
||||||
// {offset}{bias}{z-compare}{derivatives}{body}; SAMPLE_L carries LOD
|
|
||||||
// as the final body component instead.
|
|
||||||
var addressCursor = 0;
|
|
||||||
var offsetX = "0";
|
|
||||||
var offsetY = "0";
|
|
||||||
if (hasOffset)
|
|
||||||
{
|
|
||||||
addressCursor = AlignFullImageAddress(image, addressCursor);
|
|
||||||
var packed = Temp(
|
|
||||||
"int",
|
|
||||||
$"as_type<int>(v[{image.GetAddressRegister(ImageAddressRegister(image, addressCursor))}])");
|
|
||||||
offsetX = Temp("int", $"extract_bits({packed}, 0u, 6u)");
|
|
||||||
offsetY = Temp("int", $"extract_bits({packed}, 8u, 6u)");
|
|
||||||
addressCursor += ImageFullAddressSlots(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bias = hasBias ? Temp("float", ImageFloatAddress(image, addressCursor++)) : "0.0f";
|
|
||||||
var reference = "0.0f";
|
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
addressCursor = AlignFullImageAddress(image, addressCursor);
|
|
||||||
reference = Temp(
|
|
||||||
"float",
|
|
||||||
$"as_type<float>(v[{image.GetAddressRegister(ImageAddressRegister(image, addressCursor))}])");
|
|
||||||
addressCursor += ImageFullAddressSlots(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
var gradientX = "float2(0.0f)";
|
|
||||||
var gradientY = "float2(0.0f)";
|
|
||||||
if (hasGradients)
|
|
||||||
{
|
|
||||||
gradientX = Temp(
|
|
||||||
"float2",
|
|
||||||
$"float2({ImageFloatAddress(image, addressCursor)}, {ImageFloatAddress(image, addressCursor + 1)})");
|
|
||||||
gradientY = Temp(
|
|
||||||
"float2",
|
|
||||||
$"float2({ImageFloatAddress(image, addressCursor + 2)}, {ImageFloatAddress(image, addressCursor + 3)})");
|
|
||||||
addressCursor += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
var coordinates = Temp(
|
|
||||||
"float2",
|
|
||||||
$"float2({ImageFloatAddress(image, addressCursor)}, {ImageFloatAddress(image, addressCursor + 1)})");
|
|
||||||
var lod = hasZeroLod
|
|
||||||
? "0.0f"
|
|
||||||
: hasLod
|
|
||||||
? Temp("float", ImageFloatAddress(image, addressCursor + 2))
|
|
||||||
: bias;
|
|
||||||
if (hasOffset)
|
|
||||||
{
|
|
||||||
// Per-lane texel offsets fold into normalized coordinates using
|
|
||||||
// the selected mip extent, mirroring the SPIR-V translator
|
|
||||||
// (Metal sample offsets must be compile-time constants).
|
|
||||||
var explicitLod = hasGradients || hasZeroLod || hasLod;
|
|
||||||
var offsetLod = explicitLod && !hasGradients ? lod : "0.0f";
|
|
||||||
var mipLevel = Temp("uint", $"(uint)max((int)({offsetLod}), 0)");
|
|
||||||
coordinates = Temp(
|
|
||||||
"float2",
|
|
||||||
$"{coordinates} + float2((float){offsetX} / (float){texture}.get_width({mipLevel}), " +
|
|
||||||
$"(float){offsetY} / (float){texture}.get_height({mipLevel}))");
|
|
||||||
}
|
|
||||||
|
|
||||||
var samplerArguments = hasGradients
|
|
||||||
? $", gradient2d({gradientX}, {gradientY})"
|
|
||||||
: hasZeroLod || hasLod
|
|
||||||
? $", level({lod})"
|
|
||||||
: hasBias
|
|
||||||
? $", bias({bias})"
|
|
||||||
: string.Empty;
|
|
||||||
sampled = Temp(
|
|
||||||
$"vec<{kind}, 4>",
|
|
||||||
$"{texture}.sample({samplerName}, {coordinates}{samplerArguments})");
|
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
// Manual PCF: reference passes when <= texel, broadcast (r,r,r,1).
|
|
||||||
var passes = Temp("bool", $"{reference} <= (float){sampled}[0]");
|
|
||||||
var one = kind == "float" ? "1.0f" : "1";
|
|
||||||
var zero = kind == "float" ? "0.0f" : "0";
|
|
||||||
sampled = Temp(
|
|
||||||
$"vec<{kind}, 4>",
|
|
||||||
$"{VectorLiteral(kind)}({passes} ? {one} : {zero}, {passes} ? {one} : {zero}, {passes} ? {one} : {zero}, {one})");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryEmitImageGather(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ImageControl image,
|
|
||||||
int bindingIndex,
|
|
||||||
string kind,
|
|
||||||
out string sampled,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
sampled = string.Empty;
|
|
||||||
error = string.Empty;
|
|
||||||
var opcode = instruction.Opcode;
|
|
||||||
var texture = $"tex{bindingIndex}";
|
|
||||||
var samplerName = $"sharpemu_samplers.smp{_samplerSlots[bindingIndex]}";
|
|
||||||
var hasOffset = opcode.EndsWith("O", StringComparison.Ordinal);
|
|
||||||
var hasCompare = opcode.Contains("Gather4C", StringComparison.Ordinal);
|
|
||||||
var addressCursor = 0;
|
|
||||||
var offset = "int2(0)";
|
|
||||||
if (hasOffset)
|
|
||||||
{
|
|
||||||
var packed = Temp(
|
|
||||||
"int",
|
|
||||||
$"as_type<int>(v[{image.GetAddressRegister(ImageAddressRegister(image, addressCursor))}])");
|
|
||||||
offset = Temp(
|
|
||||||
"int2",
|
|
||||||
$"int2(extract_bits({packed}, 0u, 6u), extract_bits({packed}, 8u, 6u))");
|
|
||||||
addressCursor += ImageFullAddressSlots(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
var reference = "0.0f";
|
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
addressCursor = AlignFullImageAddress(image, addressCursor);
|
|
||||||
reference = Temp(
|
|
||||||
"float",
|
|
||||||
$"as_type<float>(v[{image.GetAddressRegister(ImageAddressRegister(image, addressCursor))}])");
|
|
||||||
addressCursor += ImageFullAddressSlots(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
var coordinates = Temp(
|
|
||||||
"float2",
|
|
||||||
$"float2({ImageFloatAddress(image, addressCursor)}, {ImageFloatAddress(image, addressCursor + 1)})");
|
|
||||||
|
|
||||||
// The gathered component is selected from the first dmask bit.
|
|
||||||
uint component = 0;
|
|
||||||
while (component < 3 && (image.Dmask & (1u << (int)component)) == 0)
|
|
||||||
{
|
|
||||||
component++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var componentName = hasCompare ? "x" : component switch
|
|
||||||
{
|
|
||||||
0 => "x",
|
|
||||||
1 => "y",
|
|
||||||
2 => "z",
|
|
||||||
_ => "w",
|
|
||||||
};
|
|
||||||
sampled = Temp(
|
|
||||||
$"vec<{kind}, 4>",
|
|
||||||
$"{texture}.gather({samplerName}, {coordinates}, {offset}, component::{componentName})");
|
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
var one = kind == "float" ? "1.0f" : "1";
|
|
||||||
var zero = kind == "float" ? "0.0f" : "0";
|
|
||||||
var compared = Temp(
|
|
||||||
$"vec<{kind}, 4>",
|
|
||||||
$"{VectorLiteral(kind)}(" +
|
|
||||||
$"{reference} <= (float){sampled}[0] ? {one} : {zero}, " +
|
|
||||||
$"{reference} <= (float){sampled}[1] ? {one} : {zero}, " +
|
|
||||||
$"{reference} <= (float){sampled}[2] ? {one} : {zero}, " +
|
|
||||||
$"{reference} <= (float){sampled}[3] ? {one} : {zero})");
|
|
||||||
sampled = compared;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string VectorLiteral(string kind) => $"vec<{kind}, 4>";
|
|
||||||
|
|
||||||
private static int ImageAddressRegister(Gen5ImageControl image, int component) =>
|
|
||||||
image.A16 ? component / 2 : component;
|
|
||||||
|
|
||||||
private static int ImageFullAddressSlots(Gen5ImageControl image) =>
|
|
||||||
image.A16 ? 2 : 1;
|
|
||||||
|
|
||||||
private static int AlignFullImageAddress(Gen5ImageControl image, int component) =>
|
|
||||||
image.A16 ? (component + 1) & ~1 : component;
|
|
||||||
|
|
||||||
/// <summary>Float address component, unpacking A16 half pairs.</summary>
|
|
||||||
private string ImageFloatAddress(Gen5ImageControl image, int component)
|
|
||||||
{
|
|
||||||
var register = image.GetAddressRegister(ImageAddressRegister(image, component));
|
|
||||||
return image.A16
|
|
||||||
? $"(float)as_type<half2>(v[{register}])[{component & 1}]"
|
|
||||||
: $"as_type<float>(v[{register}])";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Integer address component, unpacking A16 16-bit pairs.</summary>
|
|
||||||
private string ImageIntegerAddress(Gen5ImageControl image, int component)
|
|
||||||
{
|
|
||||||
var register = image.GetAddressRegister(ImageAddressRegister(image, component));
|
|
||||||
return image.A16
|
|
||||||
? $"((v[{register}] >> {(component & 1) * 16}) & 0xFFFFu)"
|
|
||||||
: $"v[{register}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>One store-source component, unpacking D16 halves.</summary>
|
|
||||||
private string ImageStoreComponent(Gen5ImageControl image, string kind, uint component)
|
|
||||||
{
|
|
||||||
if (!image.D16)
|
|
||||||
{
|
|
||||||
return $"v[{image.VectorData + component}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
var packed = $"v[{image.VectorData + (component / 2)}]";
|
|
||||||
if (kind == "float")
|
|
||||||
{
|
|
||||||
return AsUInt($"(float)as_type<half2>({packed})[{component & 1}]");
|
|
||||||
}
|
|
||||||
|
|
||||||
var low = $"(({packed} >> {(component & 1) * 16}) & 0xFFFFu)";
|
|
||||||
return kind == "int"
|
|
||||||
? $"(uint)extract_bits(as_type<int>({low}), 0u, 16u)"
|
|
||||||
: low;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ImageTexelComponent(string kind, string raw) => kind switch
|
|
||||||
{
|
|
||||||
"int" => $"as_type<int>({raw})",
|
|
||||||
"uint" => raw,
|
|
||||||
_ => $"as_type<float>({raw})",
|
|
||||||
};
|
|
||||||
|
|
||||||
private string PackImageD16(string kind, string low, string high)
|
|
||||||
{
|
|
||||||
if (kind == "float")
|
|
||||||
{
|
|
||||||
return $"(((uint)as_type<ushort>(half(as_type<float>({low})))) | (((uint)as_type<ushort>(half(as_type<float>({high})))) << 16))";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"((({low}) & 0xFFFFu) | ((({high}) & 0xFFFFu) << 16))";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- exports ----
|
|
||||||
|
|
||||||
private bool TryEmitExport(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ExportControl export,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
error = string.Empty;
|
|
||||||
if (instruction.Sources.Count < 4)
|
|
||||||
{
|
|
||||||
error = "missing export sources";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_stage == Gen5MslStage.Vertex)
|
|
||||||
{
|
|
||||||
return TryEmitVertexExport(instruction, export);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_stage != Gen5MslStage.Pixel)
|
|
||||||
{
|
|
||||||
// Compute programs have no export interface.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gen5PixelOutputBinding? binding = null;
|
|
||||||
foreach (var candidate in _pixelOutputBindings)
|
|
||||||
{
|
|
||||||
if (candidate.GuestSlot == export.Target)
|
|
||||||
{
|
|
||||||
binding = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding is null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = $"sharpemu_out.mrt{binding.Value.GuestSlot}";
|
|
||||||
var componentType = binding.Value.Kind switch
|
|
||||||
{
|
|
||||||
Gen5PixelOutputKind.Uint => "uint",
|
|
||||||
Gen5PixelOutputKind.Sint => "int",
|
|
||||||
_ => "float",
|
|
||||||
};
|
|
||||||
var values = new string[4];
|
|
||||||
for (var component = 0; component < 4; component++)
|
|
||||||
{
|
|
||||||
if ((export.EnableMask & (1u << component)) == 0)
|
|
||||||
{
|
|
||||||
values[component] = $"{field}[{component}]";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (export.Compressed)
|
|
||||||
{
|
|
||||||
var packed = $"v[{instruction.Sources[component >> 1].Value}]";
|
|
||||||
var half = $"(float)as_type<half2>({packed})[{component & 1}]";
|
|
||||||
values[component] = binding.Value.Kind switch
|
|
||||||
{
|
|
||||||
Gen5PixelOutputKind.Uint => $"(uint)({half})",
|
|
||||||
Gen5PixelOutputKind.Sint => $"(int)({half})",
|
|
||||||
_ => half,
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var raw = $"v[{instruction.Sources[component].Value}]";
|
|
||||||
values[component] = binding.Value.Kind switch
|
|
||||||
{
|
|
||||||
Gen5PixelOutputKind.Uint => raw,
|
|
||||||
Gen5PixelOutputKind.Sint => $"as_type<int>({raw})",
|
|
||||||
_ => $"as_type<float>({raw})",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A lane removed from EXEC keeps the previous output value; killed
|
|
||||||
// fragments are discarded in the epilogue.
|
|
||||||
Line($"{field} = exec ? vec<{componentType}, 4>({values[0]}, {values[1]}, {values[2]}, {values[3]}) : {field};");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryEmitVertexExport(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5ExportControl export)
|
|
||||||
{
|
|
||||||
// Target 12 is POS0; 32..63 are the param outputs. Everything else
|
|
||||||
// (other position slots, MRTZ) is ignored like the SPIR-V side.
|
|
||||||
string field;
|
|
||||||
if (export.Target == 12)
|
|
||||||
{
|
|
||||||
field = "sharpemu_out.sharpemu_position";
|
|
||||||
}
|
|
||||||
else if (export.Target is >= 32 and < 64 &&
|
|
||||||
_vertexOutputs.Contains(export.Target - 32))
|
|
||||||
{
|
|
||||||
field = $"sharpemu_out.param{export.Target - 32}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var values = new string[4];
|
|
||||||
for (var component = 0; component < 4; component++)
|
|
||||||
{
|
|
||||||
if ((export.EnableMask & (1u << component)) == 0)
|
|
||||||
{
|
|
||||||
values[component] = component == 3 ? "1.0f" : "0.0f";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (export.Compressed)
|
|
||||||
{
|
|
||||||
var packed = $"v[{instruction.Sources[component >> 1].Value}]";
|
|
||||||
values[component] = $"(float)as_type<half2>({packed})[{component & 1}]";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
values[component] = $"as_type<float>(v[{instruction.Sources[component].Value}])";
|
|
||||||
}
|
|
||||||
|
|
||||||
Line($"{field} = exec ? float4({values[0]}, {values[1]}, {values[2]}, {values[3]}) : {field};");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vertex attribute fetch: the evaluator captured this buffer load as a
|
|
||||||
/// fixed-function vertex input, so read the stage_in field instead of
|
|
||||||
/// guest memory (bound via MTLVertexDescriptor by the backend).
|
|
||||||
/// </summary>
|
|
||||||
private bool TryEmitVertexInputFetch(
|
|
||||||
Gen5BufferMemoryControl control,
|
|
||||||
Gen5VertexInputBinding input,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
error = string.Empty;
|
|
||||||
if (control.DwordCount == 0 || control.DwordCount > input.ComponentCount)
|
|
||||||
{
|
|
||||||
error =
|
|
||||||
$"invalid vertex input fetch components={control.DwordCount} " +
|
|
||||||
$"input={input.ComponentCount}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint component = 0; component < control.DwordCount; component++)
|
|
||||||
{
|
|
||||||
var value = input.ComponentCount == 1
|
|
||||||
? $"sharpemu_vin.in{input.Location}"
|
|
||||||
: $"sharpemu_vin.in{input.Location}[{component}]";
|
|
||||||
StoreVector(control.VectorData + component, AsUInt(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- interpolation / pixel inputs ----
|
|
||||||
|
|
||||||
private bool TryEmitInterpolation(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5InterpolationControl interpolation,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
error = string.Empty;
|
|
||||||
if (_stage != Gen5MslStage.Pixel ||
|
|
||||||
!_pixelAttributes.Contains(interpolation.Attribute) ||
|
|
||||||
instruction.Destinations.Count == 0 ||
|
|
||||||
instruction.Destinations[0].Kind != Gen5OperandKind.VectorRegister)
|
|
||||||
{
|
|
||||||
error = "invalid interpolated attribute";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
StoreVector(
|
|
||||||
instruction.Destinations[0].Value,
|
|
||||||
AsUInt($"sharpemu_in.attr{interpolation.Attribute}[{interpolation.Channel}]"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Seeds pixel input VGPRs in SPI_PS_INPUT_ADDR compact order: the
|
|
||||||
/// interpolation slots reserve registers even though V_INTERP reads MSL
|
|
||||||
/// varyings directly, and the position inputs land in the
|
|
||||||
/// hardware-selected VGPRs from the fragment coordinate.
|
|
||||||
/// </summary>
|
|
||||||
private void EmitPixelInputState(StringBuilder source)
|
|
||||||
{
|
|
||||||
uint vgpr = 0;
|
|
||||||
|
|
||||||
void Advance(int bit, uint dwordCount)
|
|
||||||
{
|
|
||||||
if ((_pixelInputAddress & (1u << bit)) != 0)
|
|
||||||
{
|
|
||||||
vgpr += dwordCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Position(int bit, string component)
|
|
||||||
{
|
|
||||||
var mask = 1u << bit;
|
|
||||||
if ((_pixelInputAddress & mask) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((_pixelInputEnable & mask) != 0)
|
|
||||||
{
|
|
||||||
source.AppendLine(
|
|
||||||
$" v[{vgpr}] = as_type<uint>(sharpemu_in.sharpemu_frag_coord.{component});");
|
|
||||||
}
|
|
||||||
|
|
||||||
vgpr++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Advance(0, 2); // PERSP_SAMPLE
|
|
||||||
Advance(1, 2); // PERSP_CENTER
|
|
||||||
Advance(2, 2); // PERSP_CENTROID
|
|
||||||
Advance(3, 3); // PERSP_PULL_MODEL
|
|
||||||
Advance(4, 2); // LINEAR_SAMPLE
|
|
||||||
Advance(5, 2); // LINEAR_CENTER
|
|
||||||
Advance(6, 2); // LINEAR_CENTROID
|
|
||||||
Advance(7, 1); // LINE_STIPPLE
|
|
||||||
Position(8, "x");
|
|
||||||
Position(9, "y");
|
|
||||||
Position(10, "z");
|
|
||||||
Position(11, "w");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The fixed presenter shaders, mirroring SpirvFixedShaders semantically. The
|
|
||||||
/// MSL lives in Templates/*.msl (authored as real Metal source); this class
|
|
||||||
/// only substitutes the per-call parameters. Entry point names are stable
|
|
||||||
/// (Metal forbids "main"); textures and samplers bind at index 0; attributes
|
|
||||||
/// use the same user(locn) convention as the translated stages.
|
|
||||||
/// </summary>
|
|
||||||
public static class MslFixedShaders
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Fullscreen triangle from the vertex index; every attribute location in
|
|
||||||
/// 0..attributeCount-1 carries (x, y, 0, 1) so paired fragment stages can
|
|
||||||
/// read a screen-space UV from any location.
|
|
||||||
/// </summary>
|
|
||||||
public static string CreateFullscreenVertex(uint attributeCount)
|
|
||||||
{
|
|
||||||
var fields = new StringBuilder();
|
|
||||||
var stores = new StringBuilder();
|
|
||||||
for (uint index = 0; index < attributeCount; index++)
|
|
||||||
{
|
|
||||||
if (index != 0)
|
|
||||||
{
|
|
||||||
fields.AppendLine();
|
|
||||||
stores.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
fields.Append($" float4 attr{index} [[user(locn{index})]];");
|
|
||||||
stores.Append($" out.attr{index} = float4(x, y, 0.0f, 1.0f);");
|
|
||||||
}
|
|
||||||
|
|
||||||
return MslTemplates.Render(
|
|
||||||
"fullscreen_vertex",
|
|
||||||
("attribute_fields", fields.ToString()),
|
|
||||||
("attribute_stores", stores.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Samples texture 0 at the interpolated location-0 UV.</summary>
|
|
||||||
public static string CreateCopyFragment() => MslTemplates.Render("copy_fragment");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The presenter's blit stage: samples texture 0 with V flipped, because
|
|
||||||
/// pairing the shared fullscreen triangle with Metal's y-up NDC puts UV
|
|
||||||
/// (0,0) at the bottom of the screen while textures keep v=0 at the top.
|
|
||||||
/// </summary>
|
|
||||||
public static string CreatePresentFragment() => MslTemplates.Render("present_fragment");
|
|
||||||
|
|
||||||
public static string CreateSolidFragment(float red, float green, float blue, float alpha) =>
|
|
||||||
MslTemplates.Render(
|
|
||||||
"solid_fragment",
|
|
||||||
("red", Format(red)),
|
|
||||||
("green", Format(green)),
|
|
||||||
("blue", Format(blue)),
|
|
||||||
("alpha", Format(alpha)));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Diagnostic fragment stage exposing one interpolated vertex output
|
|
||||||
/// directly as color, isolating fragment translation from interface data.
|
|
||||||
/// </summary>
|
|
||||||
public static string CreateAttributeFragment(uint location) =>
|
|
||||||
MslTemplates.Render(
|
|
||||||
"attribute_fragment",
|
|
||||||
("location", location.ToString(CultureInfo.InvariantCulture)));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Output-free fragment stage for fixed-function depth-only passes: the
|
|
||||||
/// guest has no pixel shader, so no color may be written while depth
|
|
||||||
/// testing still runs for the translated vertex shader.
|
|
||||||
/// </summary>
|
|
||||||
public static string CreateDepthOnlyFragment() => MslTemplates.Render("depth_only_fragment");
|
|
||||||
|
|
||||||
private static string Format(float value) =>
|
|
||||||
value.ToString("0.0######", CultureInfo.InvariantCulture) + "f";
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads the static MSL blocks from embedded Templates/*.msl resources and
|
|
||||||
/// substitutes {{placeholder}} tokens. The static prelude and fixed shaders
|
|
||||||
/// are authored as real Metal source files; only the per-instruction body
|
|
||||||
/// emission stays programmatic in the translator.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MslTemplates
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<string, string> _cache = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
public static string Render(string name, params (string Key, string Value)[] substitutions)
|
|
||||||
{
|
|
||||||
var template = _cache.GetOrAdd(name, Load);
|
|
||||||
if (substitutions.Length == 0)
|
|
||||||
{
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = new StringBuilder(template);
|
|
||||||
foreach (var (key, value) in substitutions)
|
|
||||||
{
|
|
||||||
builder.Replace("{{" + key + "}}", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rendered = builder.ToString();
|
|
||||||
var marker = rendered.IndexOf("{{", StringComparison.Ordinal);
|
|
||||||
if (marker >= 0)
|
|
||||||
{
|
|
||||||
var end = rendered.IndexOf("}}", marker, StringComparison.Ordinal);
|
|
||||||
var token = end > marker ? rendered[marker..(end + 2)] : "{{...";
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"template '{name}' has an unsubstituted placeholder {token}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rendered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Load(string name)
|
|
||||||
{
|
|
||||||
var assembly = typeof(MslTemplates).Assembly;
|
|
||||||
var resourceName = $"SharpEmu.ShaderCompiler.Metal.Templates.{name}.msl";
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
|
||||||
?? throw new InvalidOperationException($"missing embedded MSL template {resourceName}");
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
||||||
return reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<!-- The Metal codegen backend: consumes the backend-neutral shader IR from
|
|
||||||
SharpEmu.ShaderCompiler and emits Metal Shading Language source text.
|
|
||||||
Deliberately has no dependency on Metal bindings — emitters produce text;
|
|
||||||
renderers own APIs (the Metal backend compiles the source via MTLLibrary). -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- Static MSL blocks (prelude helpers, fixed shaders) authored as real
|
|
||||||
Metal source; the emitter renders them with placeholder substitution. -->
|
|
||||||
<EmbeddedResource Include="Templates\**\*.msl" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
struct AttributeIn
|
|
||||||
{
|
|
||||||
float4 attr{{location}} [[user(locn{{location}})]];
|
|
||||||
};
|
|
||||||
|
|
||||||
fragment float4 attribute_fs(AttributeIn in [[stage_in]])
|
|
||||||
{
|
|
||||||
return in.attr{{location}};
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
struct CopyIn
|
|
||||||
{
|
|
||||||
float4 attr0 [[user(locn0)]];
|
|
||||||
};
|
|
||||||
|
|
||||||
fragment float4 copy_fs(
|
|
||||||
CopyIn in [[stage_in]],
|
|
||||||
texture2d<float> tex0 [[texture(0)]],
|
|
||||||
sampler smp0 [[sampler(0)]])
|
|
||||||
{
|
|
||||||
return tex0.sample(smp0, in.attr0.xy);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
fragment void depth_only_fs()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user