mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0260db3e0e | |||
| 8fc94bc687 | |||
| d81d643227 | |||
| da9f132226 | |||
| 2064c1eda6 | |||
| ce243b3622 | |||
| 9aedb88f3c | |||
| 7548911413 | |||
| 59059dfd2c | |||
| 24bd38ab5b |
@@ -89,6 +89,7 @@ jobs:
|
||||
DOTNET_NOLOGO: true
|
||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
||||
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -120,13 +121,24 @@ jobs:
|
||||
- name: Publish win-x64 CLI
|
||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||
|
||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||
if (Test-Path $archivePath) {
|
||||
Remove-Item $archivePath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.PUBLISH_DIR }}
|
||||
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
|
||||
build-posix:
|
||||
name: Build ${{ matrix.rid }}
|
||||
@@ -146,6 +158,7 @@ jobs:
|
||||
DOTNET_NOLOGO: true
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
||||
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
||||
SPIRV_TARGET_ENV: vulkan1.2
|
||||
@@ -210,13 +223,19 @@ jobs:
|
||||
if: matrix.rid == 'osx-x64'
|
||||
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
# tar keeps the executable bit, which zip would drop.
|
||||
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||
-C "$PUBLISH_DIR" .
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.PUBLISH_DIR }}
|
||||
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
@@ -236,28 +255,6 @@ jobs:
|
||||
with:
|
||||
path: release
|
||||
|
||||
- name: Package release assets
|
||||
shell: bash
|
||||
env:
|
||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
|
||||
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
|
||||
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
|
||||
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
|
||||
test -d "${package_dir}"
|
||||
done
|
||||
|
||||
mkdir -p release-assets
|
||||
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
|
||||
|
||||
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
|
||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
@@ -267,9 +264,9 @@ jobs:
|
||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||
if [ "${#assets[@]}" -ne 3 ]; then
|
||||
echo "Expected 3 release assets, found ${#assets[@]}." >&2
|
||||
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||
if [ "${#assets[@]}" -eq 0 ]; then
|
||||
echo "No release assets found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -42,4 +42,3 @@ ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -9,18 +9,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx</_HostRidOSPrefix>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64</_HostRidArch>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64</_HostRidArch>
|
||||
<RuntimeIdentifier Condition="'$(_HostRidOSPrefix)' != ''">$(_HostRidOSPrefix)-$(_HostRidArch)</RuntimeIdentifier>
|
||||
|
||||
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
|
||||
@@ -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
|
||||
```
|
||||
+22
-51
@@ -9,67 +9,38 @@ Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
|
||||
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||
decoder cannot observe or replace those frames.
|
||||
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink decoder is
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||
|
||||
The default path decodes by calling FFmpeg's own C API directly from managed
|
||||
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
|
||||
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
|
||||
bindings) against a custom FFmpeg build
|
||||
(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
|
||||
FFmpeg 7.1.2; see "Supplying the FFmpeg libraries" below for where those
|
||||
libraries come from. No proprietary RAD SDK is needed to build or run
|
||||
SharpEmu, and there is no C/C++ code of SharpEmu's own involved in decoding
|
||||
-- SharpEmu.CLI.csproj only downloads a prebuilt release archive.
|
||||
|
||||
Set `SHARPEMU_BINK_MODE=guest` to leave decoding to the Bink implementation
|
||||
statically linked into the game instead. Set `skip` only when explicitly
|
||||
testing a title whose cinematics are optional.
|
||||
Without an adapter, Bink files remain visible to the guest and the game's
|
||||
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
|
||||
explicitly testing a title whose cinematics are optional.
|
||||
|
||||
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
|
||||
only; it does not decode the movie or alter its game logic.
|
||||
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
|
||||
being explicit about it.
|
||||
only; it does not decode the movie or alter its game logic. Set
|
||||
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||
|
||||
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override is unrelated to the
|
||||
default path above: instead of calling into FFmpeg in-process, it spawns a
|
||||
standalone `ffmpeg` executable and reads raw frames from its stdout
|
||||
(`src/SharpEmu.Libs/Bink/FfmpegBinkFrameSource.cs`). SharpEmu searches
|
||||
`SHARPEMU_FFMPEG_PATH`, the executable directory, its `ffmpeg` subdirectory,
|
||||
and then `PATH` (plus a couple of common Homebrew paths on macOS). That
|
||||
`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
|
||||
only recognizes the Bink container is not sufficient. Most users want the
|
||||
default `native` mode instead, which always has Bink 2 support since it's
|
||||
built against `ffmpeg-core` specifically.
|
||||
## Supplying the adapter
|
||||
|
||||
## Supplying the FFmpeg libraries
|
||||
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||
The adapter deliberately contains only a three-function C ABI so the managed
|
||||
emulator never depends on RAD's private binary ABI.
|
||||
|
||||
`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
|
||||
(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
|
||||
the `FFmpeg.AutoGen` package version in `Directory.Packages.props` -- both
|
||||
need to agree on the same FFmpeg ABI) and copies its dynamically linked
|
||||
libraries into a `plugins` folder next to the published executable. No C
|
||||
toolchain is required to build SharpEmu; publishing just downloads a zip.
|
||||
`plugins` is a loose, unpacked folder rather than something embedded in the
|
||||
single-file bundle, so the OS loader can resolve the libraries' own
|
||||
inter-dependencies (`avcodec` depends on `avutil`, etc.) itself.
|
||||
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||
executable, or point to it explicitly:
|
||||
|
||||
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
||||
machine's own RID (see `Directory.Build.props`), so it fetches the matching
|
||||
`ffmpeg-core` archive and populates `plugins` without any extra flags.
|
||||
Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
||||
Windows) still overrides that default normally.
|
||||
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||
./SharpEmu /path/to/eboot.bin
|
||||
|
||||
To use a different set of FFmpeg libraries, drop them into the published
|
||||
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||
folder and does not otherwise care where the files came from.
|
||||
The expected exports are sharpemu_bink2_open_utf8,
|
||||
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||
frame. The managed side validates dimensions and retains ownership of the
|
||||
destination buffer.
|
||||
|
||||
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
||||
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
|
||||
could not open movie ...") and leaves the guest's own rendering path
|
||||
untouched, rather than crashing.
|
||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||
and retains the existing guest rendering path.
|
||||
|
||||
@@ -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": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "latestFeature"
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*
|
||||
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "bink.h"
|
||||
|
||||
typedef struct sharpemu_bink2_info {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t frames_per_second_numerator;
|
||||
uint32_t frames_per_second_denominator;
|
||||
} sharpemu_bink2_info;
|
||||
|
||||
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||
HBINK bink;
|
||||
if (!path || !movie || !info) return 0;
|
||||
|
||||
*movie = NULL;
|
||||
|
||||
bink = BinkOpen(path, 0);
|
||||
if (!bink) return 0;
|
||||
|
||||
if (bink->Width == 0 || bink->Height == 0) {
|
||||
BinkClose(bink);
|
||||
return 0;
|
||||
}
|
||||
|
||||
*movie = bink;
|
||||
info->width = bink->Width;
|
||||
info->height = bink->Height;
|
||||
info->frames_per_second_numerator = bink->FrameRate;
|
||||
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||
uint32_t stride, uint32_t destination_bytes) {
|
||||
uint64_t needed;
|
||||
uint64_t min_stride;
|
||||
|
||||
if (!movie || !destination) return 0;
|
||||
|
||||
min_stride = (uint64_t)movie->Width * 4;
|
||||
if ((uint64_t)stride < min_stride) return 0;
|
||||
|
||||
needed = (uint64_t)stride * movie->Height;
|
||||
if (needed > destination_bytes) return 0;
|
||||
|
||||
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||
if (BinkWait(movie)) return 0;
|
||||
|
||||
if (!BinkDoFrame(movie)) return 0;
|
||||
|
||||
if (!BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA)) return 0;
|
||||
|
||||
BinkNextFrame(movie);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void sharpemu_bink2_close(HBINK movie) {
|
||||
if (movie) BinkClose(movie);
|
||||
}
|
||||
@@ -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())
|
||||
@@ -64,7 +64,6 @@ internal static partial class Program
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
PreloadGlfw();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
@@ -214,27 +213,6 @@ internal static partial class Program
|
||||
"as libvulkan.1.dylib.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharpEmu.CLI.csproj publishes glfw into a "plugins" subfolder rather
|
||||
/// than flat next to the executable, which falls outside the default OS
|
||||
/// DLL/dlopen search path. Preloading it here by full path first means
|
||||
/// any later bare-name lookup (however Silk.NET/GLFW itself resolves the
|
||||
/// library) finds it already loaded in the process and reuses it -- the
|
||||
/// same technique <see cref="PreloadMacVulkanLoader"/> already relies on
|
||||
/// for the Vulkan loader.
|
||||
/// </summary>
|
||||
private static void PreloadGlfw()
|
||||
{
|
||||
var fileName = OperatingSystem.IsWindows() ? "glfw3.dll"
|
||||
: OperatingSystem.IsMacOS() ? "libglfw.3.dylib"
|
||||
: "libglfw.so.3";
|
||||
var candidate = Path.Combine(AppContext.BaseDirectory, "plugins", fileName);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
NativeLibrary.TryLoad(candidate, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
@@ -629,7 +607,7 @@ internal static partial class Program
|
||||
nint jobHandle = 0;
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
var created = CreateProcessW(
|
||||
null,
|
||||
processPath,
|
||||
cmdLineBuilder,
|
||||
0,
|
||||
0,
|
||||
@@ -1455,7 +1433,7 @@ internal static partial class Program
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(
|
||||
string? applicationName,
|
||||
string applicationName,
|
||||
StringBuilder commandLine,
|
||||
nint processAttributes,
|
||||
nint threadAttributes,
|
||||
|
||||
@@ -20,11 +20,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<!-- A plain "dotnet publish" with no -r defaults $(RuntimeIdentifier) to
|
||||
the host's own RID; see Directory.Build.props, which is where that
|
||||
default actually has to live (PublishDir's RID suffix is decided
|
||||
there, evaluated before this file, so a default set only here would
|
||||
be too late for it). -->
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
@@ -54,6 +49,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
@@ -65,7 +61,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -79,75 +75,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Native libraries (glfw, FFmpeg) publish into a subfolder next to the
|
||||
executable instead of sitting loose beside it, so the publish
|
||||
directory stays uncluttered as more native deps get added. The folder
|
||||
name is a fixed constant, not derived from the RID/architecture: each
|
||||
publish output only ever holds one architecture's binaries anyway, so
|
||||
varying the name added a class of bugs (RID resolution timing, host-OS
|
||||
vs. target-RID mixups) for no benefit. Runtime code (Program.cs's
|
||||
PreloadGlfw, FfmpegNativeBinkFrameSource's RootPath) uses the same
|
||||
literal "plugins" folder name. -->
|
||||
<PropertyGroup>
|
||||
<NativeLibraryFolderName>plugins</NativeLibraryFolderName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file in the native subfolder; every other native
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<RelativePath>$(NativeLibraryFolderName)/%(Filename)%(Extension)</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<FfmpegRuntimeTag>2c92585</FfmpegRuntimeTag>
|
||||
<FfmpegRuntimeDir>
|
||||
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'linux-x64'">ffmpeg-linux-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-x64'">ffmpeg-macos-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">ffmpeg-macos-arm64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimeArchive>$(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)</FfmpegRuntimeArchive>
|
||||
<FfmpegRuntimeExtractDir>$(FfmpegRuntimeDir)/extracted</FfmpegRuntimeExtractDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="FetchFfmpegRuntime"
|
||||
BeforeTargets="Publish"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||
<Unzip
|
||||
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishFfmpegRuntime"
|
||||
AfterTargets="Publish"
|
||||
DependsOnTargets="FetchFfmpegRuntime"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
|
||||
e.g. linux-x64 from a Windows machine is a supported cross-publish,
|
||||
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
|
||||
depends on which platform's ffmpeg-core package was fetched. -->
|
||||
<ItemGroup>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,9 +13,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
{
|
||||
"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.Metal": "[0.0.2-beta.3, )",
|
||||
"SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.3, )",
|
||||
"Silk.NET.Input": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"sharpemu.shadercompiler": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[0.0.2-beta.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.shadercompiler.metal": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.shadercompiler.vulkan": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
|
||||
}
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Input": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Input.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
|
||||
/// (immediate-form) instructions.
|
||||
///
|
||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
|
||||
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
|
||||
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
|
||||
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
|
||||
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
|
||||
/// a different register allocation, a title built with a different compiler version, and so on
|
||||
/// - still aborts the title. This class ported from Kyty's
|
||||
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
|
||||
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
|
||||
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
|
||||
///
|
||||
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
|
||||
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
|
||||
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
|
||||
/// </summary>
|
||||
public static class Sse4aBitFieldEmulator
|
||||
{
|
||||
public static bool IsValidBitField(int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
|
||||
}
|
||||
|
||||
public static ulong ExtractBitField(ulong value, int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
if (!IsValidBitField(length, index))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
||||
return (value >> idx) & mask;
|
||||
}
|
||||
|
||||
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
if (!IsValidBitField(length, index))
|
||||
{
|
||||
return destination;
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
||||
var destinationClearMask = fieldMask << idx;
|
||||
var sourceField = (source & fieldMask) << idx;
|
||||
return (destination & ~destinationClearMask) | sourceField;
|
||||
}
|
||||
}
|
||||
@@ -1,199 +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, and on Linux the bridge copies the mcontext's FXSAVE image into the
|
||||
// Xmm0.. slots and writes them back through sigreturn (_posixXmmContextBridged). On
|
||||
// Darwin the XMM area is still a zeroed scratch buffer - running this there would
|
||||
// silently compute a result from stale bytes and then discard whatever it "wrote", so
|
||||
// the recovery declines until that bridge exists.
|
||||
return (OperatingSystem.IsWindows() || _posixXmmContextBridged) &&
|
||||
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() && !_posixXmmContextBridged ||
|
||||
!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
|
||||
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
|
||||
if (!isExtrq && !isInsertq)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.GetOpKind(0) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destLow = ReadCtxU64(contextRecord, destOffset);
|
||||
if (isExtrq)
|
||||
{
|
||||
var length = (int)instruction.GetImmediate(1);
|
||||
var index = (int)instruction.GetImmediate(2);
|
||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
|
||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (instruction.GetOpKind(1) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = (int)instruction.GetImmediate(2);
|
||||
var index = (int)instruction.GetImmediate(3);
|
||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
|
||||
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
|
||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _sse4aInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
|
||||
// explicit switch (rather than arithmetic on the Register enum) to match the style already
|
||||
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
|
||||
private static bool TryGetXmmOffset(Register register, out int offset)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
|
||||
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
|
||||
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
|
||||
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
|
||||
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
|
||||
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
|
||||
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
|
||||
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
|
||||
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
|
||||
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
|
||||
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
|
||||
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
|
||||
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
|
||||
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
|
||||
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
|
||||
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
|
||||
default:
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,11 +133,6 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverAmdCompatInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
@@ -483,7 +478,7 @@ public sealed partial class DirectExecutionBackend
|
||||
if (count <= 16 || count % 65536 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (default-on; set SHARPEMU_IGNORE_INT41=0 to disable)");
|
||||
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -530,12 +530,9 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
}
|
||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||
{
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||
}
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||
if (dispatchResolved &&
|
||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||
@@ -1329,12 +1326,9 @@ public sealed partial class DirectExecutionBackend
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
}
|
||||
}
|
||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||
{
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||
}
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||
|
||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
@@ -1404,13 +1398,11 @@ public sealed partial class DirectExecutionBackend
|
||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||
"4fgtGfXDrFc" or // sceAmprMeasureCommandSizeWriteAddress_04_00
|
||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||
"gzndltBEzWc" or // sceAmprCommandBufferGetNumCommands
|
||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||
"j0+3uJMxYJY" or // sceAmprCommandBufferWriteAddress_04_00
|
||||
"mPpPxv5CZt4" or // sceSystemServiceGetHdrToneMapLuminance
|
||||
"1FZBKy8HeNU" or // sceVideoOutGetVblankStatus
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
@@ -1446,9 +1438,6 @@ public sealed partial class DirectExecutionBackend
|
||||
var expectedMutexTrylockBusy =
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
var expectedSemaphoreTrywaitAgain =
|
||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
var expectedNetAcceptWouldBlock =
|
||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80410123);
|
||||
@@ -1462,7 +1451,6 @@ public sealed partial class DirectExecutionBackend
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedSemaphoreTrywaitAgain &&
|
||||
!expectedNetAcceptWouldBlock &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter)
|
||||
@@ -1556,13 +1544,11 @@ public sealed partial class DirectExecutionBackend
|
||||
"vWU-odnS+fU" or
|
||||
"sSAUCCU1dv4" or
|
||||
"C+IEj+BsAFM" or
|
||||
"4fgtGfXDrFc" or
|
||||
"tZDDEo2tE5k" or
|
||||
"GnxKOHEawhk" or
|
||||
"gzndltBEzWc" or
|
||||
"H896Pt-yB4I" or
|
||||
"sJXyWHjP-F8" or
|
||||
"j0+3uJMxYJY" or
|
||||
"mPpPxv5CZt4" or
|
||||
"1FZBKy8HeNU" or
|
||||
"ASoW5WE-UPo" or
|
||||
|
||||
@@ -50,19 +50,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsErrOffset = 19 * 8;
|
||||
|
||||
// The kernel's x86-64 sigcontext places the FXSAVE-image pointer right
|
||||
// after the general registers it hands to the handler: err(152)
|
||||
// trapno(160) oldmask(168) cr2(176) fpstate(184), all relative to
|
||||
// GetPosixRegisterBase. glibc and musl both overlay this kernel layout
|
||||
// verbatim (glibc's mcontext_t.fpregs is the same slot), so the offset
|
||||
// is libc-independent. Inside the FXSAVE image the XMM registers start
|
||||
// at +160 (32-byte header + 8 legacy x87/MMX slots x 16 bytes) - the
|
||||
// same relative position they occupy in the Win64 CONTEXT's FltSave
|
||||
// area (Win64ContextXmm0Offset = 256 + 160).
|
||||
private const int LinuxGregsFpstateOffset = 184;
|
||||
private const int FxsaveXmmOffset = 160;
|
||||
private const int XmmBlockSize = 16 * 16;
|
||||
|
||||
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||
@@ -84,15 +71,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
[ThreadStatic]
|
||||
private static int _posixSignalHandlerDepth;
|
||||
|
||||
// True while the current thread's in-flight POSIX fault carries the real
|
||||
// XMM registers in the CONTEXT scratch buffer and writes to them will
|
||||
// reach the mcontext on resume. Gates recovery paths (SSE4a EXTRQ/
|
||||
// INSERTQ) that would otherwise compute results from a zeroed XMM area
|
||||
// and silently discard what they "wrote". Darwin is not bridged yet, so
|
||||
// the flag stays false there.
|
||||
[ThreadStatic]
|
||||
private static bool _posixXmmContextBridged;
|
||||
|
||||
private void SetupPosixExceptionHandler()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
||||
@@ -274,26 +252,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||
}
|
||||
|
||||
// Bridge the XMM registers alongside the GPRs where the layout is
|
||||
// known: on Linux the fpstate pointer and FXSAVE image are kernel
|
||||
// ABI, so recovery paths that read or write XMM state (SSE4a
|
||||
// EXTRQ/INSERTQ) see the live registers and their writes reach the
|
||||
// guest through sigreturn.
|
||||
byte* fpstate = null;
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
fpstate = *(byte**)(registers + LinuxGregsFpstateOffset);
|
||||
if (fpstate != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
fpstate + FxsaveXmmOffset,
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
}
|
||||
_posixXmmContextBridged = fpstate != null;
|
||||
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||
if (signal == PosixSigIll)
|
||||
@@ -359,14 +317,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||
}
|
||||
if (fpstate != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
fpstate + FxsaveXmmOffset,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -712,11 +712,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
||||
|
||||
// Import dispatch is the hottest managed path in UE titles. Most imports do
|
||||
// not have an exception queued, so publish the dictionary population and let
|
||||
// safe points skip _guestThreadGate entirely in the common case.
|
||||
private int _pendingGuestExceptionCount;
|
||||
|
||||
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
||||
|
||||
private int _guestThreadPumpDepth;
|
||||
@@ -1123,9 +1118,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_logStrlenBursts = _logStrlenImports ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STRLEN_BURSTS"), "1", StringComparison.Ordinal);
|
||||
_logGuestContext = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_CONTEXT"), "1", StringComparison.Ordinal);
|
||||
var ignoreGuestInt41Env = Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_INT41");
|
||||
_ignoreGuestInt41 = !string.Equals(ignoreGuestInt41Env, "0", StringComparison.Ordinal) &&
|
||||
!string.Equals(ignoreGuestInt41Env, "false", StringComparison.OrdinalIgnoreCase);
|
||||
_ignoreGuestInt41 = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_INT41"), "1", StringComparison.Ordinal);
|
||||
_ignoredGuestInt41Count = 0;
|
||||
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
||||
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
||||
@@ -3955,10 +3948,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
// unwinding. Unity can begin its next stop-the-world cycle in
|
||||
// that window; treating the new raise as part of the old delivery
|
||||
// strands the collector waiting for an acknowledgement.
|
||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||
handler,
|
||||
exceptionType,
|
||||
external.ExceptionStackBase));
|
||||
external.ExceptionStackBase);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3967,10 +3960,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
// managed thread corrupts the worker's control state. Queue the
|
||||
// request and let that exact executor consume it at its next HLE
|
||||
// boundary, where the original guest thread is safely paused.
|
||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||
handler,
|
||||
exceptionType,
|
||||
external.ExceptionStackBase));
|
||||
external.ExceptionStackBase);
|
||||
if (logGuestExceptions)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -4015,17 +4008,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
if (target.ExceptionDeliveryActive)
|
||||
{
|
||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||
handler,
|
||||
exceptionType,
|
||||
exceptionStackBase));
|
||||
exceptionStackBase);
|
||||
return true;
|
||||
}
|
||||
|
||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||
handler,
|
||||
exceptionType,
|
||||
exceptionStackBase));
|
||||
exceptionStackBase);
|
||||
if (logGuestExceptions)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -4186,7 +4179,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
RestoreInterruptedGuestThread();
|
||||
if (target.State == GuestThreadRunState.Blocked &&
|
||||
!target.ExecutorActive &&
|
||||
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
|
||||
_pendingGuestExceptions.Remove(threadHandle, out var queued))
|
||||
{
|
||||
followUp = queued;
|
||||
}
|
||||
@@ -4272,11 +4265,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
CpuContext currentContext,
|
||||
GuestCpuContinuation interruptedContinuation)
|
||||
{
|
||||
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
@@ -4290,7 +4278,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
|
||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -4352,27 +4340,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
}
|
||||
|
||||
private void QueuePendingGuestExceptionLocked(
|
||||
ulong threadHandle,
|
||||
PendingGuestException pending)
|
||||
{
|
||||
_pendingGuestExceptions[threadHandle] = pending;
|
||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
||||
}
|
||||
|
||||
private bool TryRemovePendingGuestExceptionLocked(
|
||||
ulong threadHandle,
|
||||
out PendingGuestException pending)
|
||||
{
|
||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
@@ -4467,7 +4434,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_guestThreads.Clear();
|
||||
_externalGuestThreads.Clear();
|
||||
_pendingGuestExceptions.Clear();
|
||||
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
||||
_activeGuestExceptionDeliveries.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
|
||||
var pattern = TlsAccessPattern;
|
||||
var end = start + length - pattern.Length;
|
||||
|
||||
for (var ptr = start; ptr <= end; ptr++)
|
||||
for (var ptr = start; ptr < end; ptr++)
|
||||
{
|
||||
if (MatchesPattern(ptr, pattern))
|
||||
{
|
||||
|
||||
@@ -40,9 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
|
||||
_inner.TryCopy(destinationAddress, sourceAddress, length);
|
||||
|
||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||
{
|
||||
if (_inner is IGuestMemoryAllocator allocator)
|
||||
|
||||
@@ -199,32 +199,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
||||
{
|
||||
// Exact allocation failed — the host may have already claimed
|
||||
// part of this range (ASLR, Rosetta 2, or another process).
|
||||
// Try backing the fixed range page by page to claim whatever
|
||||
// free gaps exist. If the whole range is occupied the backfill
|
||||
// returns false and we surface the original failure reason.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Exact allocation at main image base 0x{imageBase:X16} " +
|
||||
$"(size=0x{totalImageSize:X}) failed; attempting fixed-range backfill.");
|
||||
if (!physicalVm.TryBackFixedRange(imageBase, totalImageSize, executable: true))
|
||||
{
|
||||
// TryBackFixedRange may have partially backed pages before
|
||||
// failing. The earlier Clear() already reset all regions, so
|
||||
// this second Clear() is idempotent for everything except the
|
||||
// partial backfill — it frees only those orphaned pages.
|
||||
physicalVm.Clear();
|
||||
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||
throw new InvalidOperationException(
|
||||
$"Could not allocate main image at required base 0x{imageBase:X16} " +
|
||||
$"(size=0x{totalImageSize:X}): {reason}. " +
|
||||
"Try closing other applications, rebooting, or " +
|
||||
(OperatingSystem.IsWindows()
|
||||
? "setting SHARPEMU_DISABLE_MITIGATION_RELAUNCH=1."
|
||||
: "ensuring no other process maps into this address range."));
|
||||
}
|
||||
|
||||
allocatedBase = imageBase;
|
||||
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||
throw new InvalidOperationException(
|
||||
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}): {reason}.");
|
||||
}
|
||||
|
||||
imageBase = allocatedBase;
|
||||
@@ -737,9 +714,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
importedRelocations = BuildImportedRelocations(descriptors);
|
||||
|
||||
var stubEligibleNids = CollectStubEligibleNids(descriptors, moduleManager);
|
||||
var stubImportNids = orderedImportNids
|
||||
.Where(stubEligibleNids.Contains)
|
||||
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||
.ToArray();
|
||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||
@@ -1184,35 +1160,6 @@ public sealed class SelfLoader : ISelfLoader
|
||||
isWeak);
|
||||
}
|
||||
|
||||
// Collects every NID that needs a trap import stub in a single pass over the
|
||||
// descriptors. This mirrors ShouldCreateImportStub applied per NID, but avoids
|
||||
// the O(nids * descriptors) rescan that filtering each unique NID against the
|
||||
// full descriptor list would incur on large modules. A NID qualifies as soon as
|
||||
// one of its descriptors is non-weak, or is weak but resolvable via the module
|
||||
// manager.
|
||||
private static HashSet<string> CollectStubEligibleNids(
|
||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||
IModuleManager? moduleManager)
|
||||
{
|
||||
var eligible = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < descriptors.Count; i++)
|
||||
{
|
||||
var descriptor = descriptors[i];
|
||||
var nid = descriptor.ImportNid;
|
||||
if (nid is null || eligible.Contains(nid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
|
||||
{
|
||||
eligible.Add(nid);
|
||||
}
|
||||
}
|
||||
|
||||
return eligible;
|
||||
}
|
||||
|
||||
private static bool ShouldCreateImportStub(
|
||||
string nid,
|
||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||
@@ -2484,19 +2431,6 @@ public sealed class SelfLoader : ISelfLoader
|
||||
Debug.Assert(
|
||||
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||
"An unresolved weak symbol incorrectly received a trap import stub.");
|
||||
|
||||
var strong = new RelocationDescriptor(
|
||||
TargetAddress: 0x3000,
|
||||
Addend: 0,
|
||||
ImportNid: "strong",
|
||||
SymbolValue: 0,
|
||||
RelocationValueKind.Pointer,
|
||||
IsDataImport: false);
|
||||
var mixed = new List<RelocationDescriptor> { weak, strong };
|
||||
var eligible = CollectStubEligibleNids(mixed, moduleManager: null);
|
||||
Debug.Assert(
|
||||
eligible.Contains("strong") && !eligible.Contains("weak"),
|
||||
"CollectStubEligibleNids disagreed with the per-NID stub eligibility rule.");
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
|
||||
@@ -20,11 +20,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||
private bool _disposed;
|
||||
|
||||
[ThreadStatic]
|
||||
private static CommittedRangeCache? _committedRangeCache;
|
||||
|
||||
private long _mappingGeneration;
|
||||
private const ulong PageSize = 0x1000;
|
||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||
@@ -33,77 +28,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||
private const int CommittedRangeCacheCapacity = 4;
|
||||
|
||||
private sealed class CommittedRangeCache
|
||||
{
|
||||
private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity];
|
||||
private PhysicalVirtualMemory? _owner;
|
||||
private long _generation;
|
||||
private int _count;
|
||||
private int _nextReplacement;
|
||||
|
||||
public bool Contains(
|
||||
PhysicalVirtualMemory owner,
|
||||
long generation,
|
||||
ulong start,
|
||||
ulong end)
|
||||
{
|
||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < _count; index++)
|
||||
{
|
||||
var range = _ranges[index];
|
||||
if (start >= range.Start && end <= range.End)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Add(
|
||||
PhysicalVirtualMemory owner,
|
||||
long generation,
|
||||
ulong start,
|
||||
ulong end)
|
||||
{
|
||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
||||
{
|
||||
_owner = owner;
|
||||
_generation = generation;
|
||||
_count = 0;
|
||||
_nextReplacement = 0;
|
||||
}
|
||||
|
||||
for (var index = 0; index < _count; index++)
|
||||
{
|
||||
var range = _ranges[index];
|
||||
if (start <= range.End && end >= range.Start)
|
||||
{
|
||||
_ranges[index] = new CommittedRange(
|
||||
Math.Min(start, range.Start),
|
||||
Math.Max(end, range.End));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_count < _ranges.Length)
|
||||
{
|
||||
_ranges[_count++] = new CommittedRange(start, end);
|
||||
return;
|
||||
}
|
||||
|
||||
_ranges[_nextReplacement] = new CommittedRange(start, end);
|
||||
_nextReplacement = (_nextReplacement + 1) % _ranges.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct CommittedRange(ulong Start, ulong End);
|
||||
|
||||
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||
@@ -425,111 +349,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return actualAddress;
|
||||
}
|
||||
|
||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var start = AlignDown(address, PageSize);
|
||||
var end = AlignUp(address + size, PageSize);
|
||||
if (end <= start)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
|
||||
// Walk the range page-run by page-run. VirtualQuery reports the largest run
|
||||
// of same-state pages from the queried address, so a single query advances
|
||||
// us over whole free or occupied stretches. Only free stretches get backed;
|
||||
// stretches already reserved or committed by another allocation are left as
|
||||
// they are, which is exactly what a fixed mapping does on hardware.
|
||||
//
|
||||
// Because backing may span several disjoint free runs, allocations are
|
||||
// staged: host pages are reserved/committed first, and the corresponding
|
||||
// MemoryRegions are inserted only once every gap in the range has been
|
||||
// backed. If any gap fails to back, every earlier host allocation is freed
|
||||
// and no region is inserted, so the address space is left untouched.
|
||||
var stagedAllocations = new List<(ulong Address, ulong Size)>();
|
||||
|
||||
var cursor = start;
|
||||
while (cursor < end)
|
||||
{
|
||||
if (!_hostMemory.Query(cursor, out var info))
|
||||
{
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||
? ulong.MaxValue
|
||||
: info.BaseAddress + info.RegionSize;
|
||||
var runEnd = Math.Min(end, queriedEnd);
|
||||
if (runEnd <= cursor)
|
||||
{
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
stagedAllocations.Add((cursor, runSize));
|
||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
||||
}
|
||||
|
||||
cursor = runEnd;
|
||||
}
|
||||
|
||||
if (stagedAllocations.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// All gaps backed successfully — insert regions in one batch.
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var (gapAddress, gapSize) in stagedAllocations)
|
||||
{
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = gapAddress,
|
||||
Size = gapSize,
|
||||
IsExecutable = executable,
|
||||
IsReservedOnly = false,
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Rollback:
|
||||
foreach (var (gapAddress, _) in stagedAllocations)
|
||||
{
|
||||
_hostMemory.Free(gapAddress);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryAllocateAtOrAbove(
|
||||
ulong desiredAddress,
|
||||
ulong size,
|
||||
@@ -621,7 +440,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _mappingGeneration);
|
||||
_hostMemory.Free(address);
|
||||
}
|
||||
|
||||
@@ -793,7 +611,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
Interlocked.Increment(ref _mappingGeneration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1102,7 +919,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
NotifyGuestWriteWatch(virtualAddress, source);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1128,68 +944,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (GuestWriteWatch.Armed)
|
||||
{
|
||||
GuestWriteWatch.Check(virtualAddress, source);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (length > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match TryWrite's managed-write notification before touching an
|
||||
// identity-mapped guest page protected by the image tracker.
|
||||
GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length);
|
||||
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var sourceRegion = FindRegion(sourceAddress, length);
|
||||
var destinationRegion = FindRegion(destinationAddress, length);
|
||||
if (sourceRegion is null || destinationRegion is null ||
|
||||
!TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) ||
|
||||
!TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourcePointer = sourceRegion.VirtualAddress + sourceOffset;
|
||||
var destinationPointer = destinationRegion.VirtualAddress + destinationOffset;
|
||||
if ((sourceRegion.IsReservedOnly &&
|
||||
!EnsureRangeCommitted(sourcePointer, length, sourceRegion)) ||
|
||||
(destinationRegion.IsReservedOnly &&
|
||||
!EnsureRangeCommitted(destinationPointer, length, destinationRegion)) ||
|
||||
!CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) ||
|
||||
!CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Span.CopyTo has memmove overlap semantics, so this allocation-free
|
||||
// path safely serves both libc memcpy and libc memmove.
|
||||
new ReadOnlySpan<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
|
||||
new Span<byte>((void*)destinationPointer, checked((int)length)));
|
||||
NotifyGuestWriteWatch(
|
||||
destinationAddress,
|
||||
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
@@ -1262,7 +1016,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
NotifyGuestWriteWatch(virtualAddress, source);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1287,7 +1040,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
NotifyGuestWriteWatch(virtualAddress, source);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1529,12 +1281,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var startPage = AlignDown(address, PageSize);
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
var mappingGeneration = Volatile.Read(ref _mappingGeneration);
|
||||
var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache();
|
||||
if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var commitProtection = GetCommitProtection(region);
|
||||
|
||||
var pageAddress = startPage;
|
||||
@@ -1556,9 +1302,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
if (info.State == HostRegionState.Committed)
|
||||
{
|
||||
// The host query proved this whole range is committed. Retain
|
||||
// that result instead of caching only the caller's small span.
|
||||
CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration);
|
||||
pageAddress = rangeEnd;
|
||||
continue;
|
||||
}
|
||||
@@ -1574,23 +1317,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
|
||||
pageAddress = rangeEnd;
|
||||
}
|
||||
|
||||
CacheCommittedRange(startPage, endPage, mappingGeneration);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
|
||||
{
|
||||
(_committedRangeCache ??= new CommittedRangeCache()).Add(
|
||||
this,
|
||||
mappingGeneration,
|
||||
startPage,
|
||||
endPage);
|
||||
}
|
||||
|
||||
private bool TryTemporarilyProtectForRead(
|
||||
ulong address,
|
||||
ulong size,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
@@ -94,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
}
|
||||
|
||||
CopyToRegions(virtualAddress, source, regionIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GuestWriteWatch.Armed)
|
||||
{
|
||||
GuestWriteWatch.Check(virtualAddress, source);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryValidateRange(
|
||||
|
||||
@@ -248,7 +248,7 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
{
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
if (!CreateProcessW(
|
||||
null,
|
||||
exePath,
|
||||
commandLine,
|
||||
0,
|
||||
0,
|
||||
@@ -629,7 +629,7 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(string? applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
||||
private static extern bool CreateProcessW(string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||
|
||||
@@ -351,13 +351,6 @@ public sealed class GameSurfaceHost : NativeControlHost
|
||||
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
||||
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
||||
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
||||
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
|
||||
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
|
||||
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
|
||||
}
|
||||
_surface.UpdatePixelSize(width, height);
|
||||
|
||||
if (!sizeChanged)
|
||||
|
||||
@@ -53,9 +53,6 @@ public sealed class GuiSettings
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
||||
public double RenderResolutionScale { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
@@ -74,7 +71,7 @@ public sealed class GuiSettings
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
var json = File.ReadAllText(SettingsPath);
|
||||
return NormalizeFromJson(json);
|
||||
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -85,39 +82,6 @@ public sealed class GuiSettings
|
||||
return new GuiSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes settings and normalizes null references and null or empty list
|
||||
/// entries introduced by JSON. Empty scalar strings remain unchanged.
|
||||
/// </summary>
|
||||
internal static GuiSettings NormalizeFromJson(string json)
|
||||
{
|
||||
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||
|
||||
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
|
||||
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
|
||||
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
|
||||
settings.LogLevel ??= "Info";
|
||||
settings.Language ??= "en";
|
||||
settings.DiscordClientId ??= "1525606762248540221";
|
||||
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
|
||||
{
|
||||
settings.RenderResolutionScale = 1.0;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
// JSON can populate non-nullable lists with null references and entries.
|
||||
private static List<string> FilterNullOrEmpty(List<string>? source)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -400,29 +400,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
||||
|
||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
||||
</ComboBox>
|
||||
</local:SettingRow>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
@@ -92,11 +92,6 @@ public partial class MainWindow : Window
|
||||
// plain window color remains the fallback when the asset fails to load.
|
||||
private Bitmap? _defaultBackdrop;
|
||||
|
||||
// Whether the native loading/closing popup should be showing; it is a
|
||||
// desktop-topmost popup, so it closes while the launcher is in the
|
||||
// background or minimized and reopens from this flag on activation.
|
||||
private bool _sessionLoadingActive;
|
||||
|
||||
// Controller navigation state.
|
||||
private readonly DispatcherTimer _gamepadTimer;
|
||||
private HostGamepadButtons _previousPadButtons;
|
||||
@@ -155,18 +150,8 @@ public partial class MainWindow : Window
|
||||
};
|
||||
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
||||
|
||||
// Native popups float above every window on the desktop; they must
|
||||
// follow the launcher into the background or a minimized state.
|
||||
Activated += (_, _) =>
|
||||
{
|
||||
UpdateSessionBarVisibility();
|
||||
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
|
||||
};
|
||||
Deactivated += (_, _) =>
|
||||
{
|
||||
SessionBarPopup.IsOpen = false;
|
||||
SessionLoadingPopup.IsOpen = false;
|
||||
};
|
||||
Activated += (_, _) => UpdateSessionBarVisibility();
|
||||
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
|
||||
|
||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||
@@ -192,18 +177,6 @@ public partial class MainWindow : Window
|
||||
// it is open already uses the new values.
|
||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
||||
{
|
||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
||||
double.TryParse(
|
||||
tag,
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var scale))
|
||||
{
|
||||
_settings.RenderResolutionScale = scale;
|
||||
}
|
||||
};
|
||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||
@@ -441,15 +414,6 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isRunning || _isStopping)
|
||||
{
|
||||
// The game renders inside the launcher window, so the launcher
|
||||
// stays active while playing. The controller belongs to the game
|
||||
// then: no navigation, and Circle/B must never stop the session.
|
||||
_previousPadButtons = pad.Buttons;
|
||||
return;
|
||||
}
|
||||
|
||||
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||
{
|
||||
@@ -499,6 +463,11 @@ public partial class MainWindow : Window
|
||||
LaunchSelected();
|
||||
}
|
||||
|
||||
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||
{
|
||||
StopEmulator();
|
||||
}
|
||||
|
||||
_previousPadButtons = pad.Buttons;
|
||||
}
|
||||
|
||||
@@ -881,13 +850,6 @@ public partial class MainWindow : Window
|
||||
_ => 2,
|
||||
};
|
||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
|
||||
{
|
||||
>= 0.875 => 0,
|
||||
>= 0.625 => 1,
|
||||
>= 0.375 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
@@ -1664,23 +1626,13 @@ public partial class MainWindow : Window
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == WindowStateProperty)
|
||||
{
|
||||
// The XAML WindowState="Maximized" assignment raises this change
|
||||
// during InitializeComponent, before named controls are wired up.
|
||||
if (WindowState == WindowState.Minimized)
|
||||
{
|
||||
_sndPreview.Pause();
|
||||
if (SessionLoadingPopup is { } popup)
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sndPreview.Resume();
|
||||
if (SessionLoadingPopup is { } popup)
|
||||
{
|
||||
popup.IsOpen = _sessionLoadingActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1807,12 +1759,6 @@ public partial class MainWindow : Window
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_RENDER_SCALE",
|
||||
_settings.RenderResolutionScale.ToString(
|
||||
"0.###",
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||
{
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
@@ -2055,27 +2001,16 @@ public partial class MainWindow : Window
|
||||
RestoreGameViewToFull();
|
||||
GameView.Background = Brushes.Black;
|
||||
GameView.IsHitTestVisible = true;
|
||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||
LibraryPage.IsVisible = false;
|
||||
OptionsPage.IsVisible = false;
|
||||
LibraryToolbar.IsVisible = false;
|
||||
ContentToolbar.IsVisible = false;
|
||||
ConsolePanel.IsVisible = false;
|
||||
LaunchBar.IsVisible = false;
|
||||
HideSessionLoading();
|
||||
SessionLoadingPopup.IsOpen = false;
|
||||
UpdateSessionBarVisibility();
|
||||
|
||||
// Defer so the layout pass from the margin change above settles first.
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (!_isRunning || _isStopping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_gameSurfaceHost?.RefreshSurfaceSize();
|
||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2174,7 +2109,7 @@ public partial class MainWindow : Window
|
||||
GameView.IsVisible = false;
|
||||
GameView.IsHitTestVisible = true;
|
||||
SessionBarPopup.IsOpen = false;
|
||||
HideSessionLoading();
|
||||
SessionLoadingPopup.IsOpen = false;
|
||||
AnimateLibraryBlur(0, clearWhenComplete: true);
|
||||
MainContent.Margin = new Thickness(32, 24, 32, 20);
|
||||
ContentToolbar.IsVisible = true;
|
||||
@@ -2258,14 +2193,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
SessionLoadingTitle.Text = title;
|
||||
SessionLoadingDetail.Text = detail;
|
||||
_sessionLoadingActive = true;
|
||||
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void HideSessionLoading()
|
||||
{
|
||||
_sessionLoadingActive = false;
|
||||
SessionLoadingPopup.IsOpen = false;
|
||||
SessionLoadingPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private void ReturnToLibraryWhileStopping()
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class PerGameSettings
|
||||
var path = PathFor(titleId);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return NormalizeFromJson(File.ReadAllText(path));
|
||||
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -59,18 +59,6 @@ public sealed class PerGameSettings
|
||||
return null;
|
||||
}
|
||||
|
||||
// A null list inherits global settings; only entries in a present list are sanitized.
|
||||
internal static PerGameSettings? NormalizeFromJson(string json)
|
||||
{
|
||||
var settings = JsonSerializer.Deserialize<PerGameSettings>(json, SerializerOptions);
|
||||
if (settings?.EnvironmentToggles is { } toggles)
|
||||
{
|
||||
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public void Save(string titleId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(titleId))
|
||||
|
||||
@@ -24,10 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
|
||||
@@ -32,7 +32,6 @@ public static unsafe class GuestImageWriteTracker
|
||||
public int Armed;
|
||||
public int FirstCpuWriteSeen;
|
||||
public int PendingFirstCpuWrite;
|
||||
public long WriteGeneration;
|
||||
public bool TraceLifetime;
|
||||
public long SourceSequence;
|
||||
public long FirstCpuWriteTraceSequence;
|
||||
@@ -156,21 +155,10 @@ public static unsafe class GuestImageWriteTracker
|
||||
{
|
||||
// Never resize an object that is still reachable from the
|
||||
// signal handler's lock-free snapshot. Retire it and publish
|
||||
// a fresh immutable range, carrying the write generation so
|
||||
// resizes do not hide guest CPU rewrites from cache owners.
|
||||
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
||||
// a fresh immutable range.
|
||||
DisarmLocked(range, "replace-range");
|
||||
_rangesByAddress.Remove(address);
|
||||
range = new TrackedRange
|
||||
{
|
||||
Address = address,
|
||||
ByteCount = byteCount,
|
||||
Start = start,
|
||||
End = start + length,
|
||||
WriteGeneration = writeGeneration,
|
||||
};
|
||||
_rangesByAddress[address] = range;
|
||||
RebuildSnapshotLocked();
|
||||
range = null;
|
||||
}
|
||||
|
||||
if (range is null)
|
||||
@@ -284,31 +272,6 @@ public static unsafe class GuestImageWriteTracker
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the monotonic first-write generation for a tracked allocation.
|
||||
/// Unlike the consuming dirty flag, this remains changed after another
|
||||
/// cache owner consumes and re-arms the range.
|
||||
/// </summary>
|
||||
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
||||
{
|
||||
generation = 0;
|
||||
if (!_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
generation = Volatile.Read(ref range.WriteGeneration);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||
@@ -462,10 +425,6 @@ public static unsafe class GuestImageWriteTracker
|
||||
}
|
||||
|
||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||
if (wasArmed)
|
||||
{
|
||||
Interlocked.Increment(ref range.WriteGeneration);
|
||||
}
|
||||
if (wasArmed &&
|
||||
range.TraceLifetime &&
|
||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||
|
||||
@@ -17,7 +17,7 @@ public static class GuestTlsTemplate
|
||||
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||
// modules can require more than one host page of Variant II static TLS;
|
||||
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||
public const ulong StartupStaticTlsReservation = 0x20000UL; // Was 0x10000UL, but thats too small for GTA V
|
||||
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||
private static readonly object _gate = new();
|
||||
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
/// <summary>
|
||||
/// Backs an entire fixed-address range, matching the guest's
|
||||
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
|
||||
/// reserves the range in one all-or-nothing host call, this walks the range and
|
||||
/// fills only the sub-ranges that are not already backed. That keeps a fixed
|
||||
/// mapping whole when part of the requested window is already occupied — the
|
||||
/// partial-overlap case where the single-call reservation fails outright and
|
||||
/// leaves the remainder unmapped for the guest to fault into.
|
||||
/// </summary>
|
||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
||||
|
||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||
|
||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Acm;
|
||||
|
||||
public static class AcmExports
|
||||
{
|
||||
private static int _nextContextHandle;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZIXln2K3XMk",
|
||||
ExportName = "sceAcmContextCreate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAcm")]
|
||||
public static int AcmContextCreate(CpuContext ctx)
|
||||
{
|
||||
var outContextAddress = ctx[CpuRegister.Rdi];
|
||||
if (outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
Span<byte> handleBytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(handleBytes, handle);
|
||||
return ctx.Memory.TryWrite(outContextAddress, handleBytes)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jBgBjAj02R8",
|
||||
ExportName = "sceAcmContextDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAcm")]
|
||||
public static int AcmContextDestroy(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Bink;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
@@ -49,7 +48,6 @@ public static partial class AgcExports
|
||||
private const uint ItWriteData = 0x37;
|
||||
private const uint ItDispatchDirect = 0x15;
|
||||
private const uint ItDispatchIndirect = 0x16;
|
||||
private const uint ItSetPredication = 0x20;
|
||||
private const uint ItWaitRegMem = 0x3C;
|
||||
private const uint ItIndirectBuffer = 0x3F;
|
||||
private const uint ItEventWrite = 0x46;
|
||||
@@ -149,10 +147,6 @@ public static partial class AgcExports
|
||||
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
||||
private const uint Gen5TextureType1D = 8;
|
||||
private const uint Gen5TextureType2D = 9;
|
||||
private const uint Gen5TextureType3D = 10;
|
||||
private const uint Gen5TextureTypeCube = 11;
|
||||
private const uint Gen5TextureType1DArray = 12;
|
||||
private const uint Gen5TextureType2DArray = 13;
|
||||
private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL;
|
||||
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||
@@ -223,7 +217,6 @@ public static partial class AgcExports
|
||||
(ulong Es, ulong State, ulong AliasAlignment),
|
||||
IGuestCompiledShader> _depthOnlyVertexShaderCache = new();
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _arrayUploadUnsupported = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
"1",
|
||||
@@ -279,7 +272,7 @@ public static partial class AgcExports
|
||||
private static long _labelProducerSequence;
|
||||
private static readonly object _labelProducerGate = new();
|
||||
private static readonly List<LabelProducerTrace> _labelProducers = [];
|
||||
private static readonly HashSet<(object Memory, ulong Address)>
|
||||
private static readonly HashSet<(object Memory, ulong Address, ulong SubmissionId)>
|
||||
_tracedProducerlessWaits = new();
|
||||
private static long _shaderTranslationMissTraceCount;
|
||||
private static long _translatedDrawTraceCount;
|
||||
@@ -445,8 +438,7 @@ public static partial class AgcExports
|
||||
TextureDescriptor Descriptor,
|
||||
bool IsStorage,
|
||||
uint MipLevel,
|
||||
IReadOnlyList<uint> SamplerDescriptor,
|
||||
bool IsArrayed = false);
|
||||
IReadOnlyList<uint> SamplerDescriptor);
|
||||
|
||||
private readonly record struct RenderTargetWriter(
|
||||
ulong Sequence,
|
||||
@@ -545,7 +537,6 @@ public static partial class AgcExports
|
||||
public uint IndexSize { get; set; }
|
||||
public uint InstanceCount { get; set; } = 1;
|
||||
public uint DrawIndexOffset { get; set; }
|
||||
public bool PredicateSkip { get; set; }
|
||||
public string QueueName { get; set; } = "graphics";
|
||||
public ulong ActiveSubmissionId { get; set; }
|
||||
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
||||
@@ -649,21 +640,6 @@ public static partial class AgcExports
|
||||
public static int GetRegisterDefaults2Internal(CpuContext ctx) =>
|
||||
ReturnRegisterDefaults(ctx, internalDefaults: true);
|
||||
|
||||
/// <summary>
|
||||
/// Reports that the GPU is not running in Trinity mode, matching the base
|
||||
/// console this backend emulates.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "BfBDZGbti7A",
|
||||
ExportName = "sceAgcGetIsTrinityMode",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int GetIsTrinityMode(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "f3dg2CSgRKY",
|
||||
ExportName = "sceAgcCreateShader",
|
||||
@@ -1186,12 +1162,12 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
var packetDwords = size == 0 ? 7u : 9u;
|
||||
var packetDwords = size == 0 ? 6u : 9u;
|
||||
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address & (size == 0 ? ~0x3u : ~0x7u)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32) & 0x3FFFFu) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
@@ -1199,9 +1175,8 @@ public static partial class AgcExports
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, EncodeWaitRegMem32Control(compareFunction, 0, cachePolicy)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, EncodeWaitRegMemPoll(pollCycles)))
|
||||
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
@@ -1209,8 +1184,8 @@ public static partial class AgcExports
|
||||
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, EncodeWaitRegMem64Control(compareFunction, 0, cachePolicy)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 32, EncodeWaitRegMemPoll(pollCycles)))
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 32, pollCycles / 40))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
@@ -1830,21 +1805,38 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
var packetDwords = size == 0 ? 7u : 9u;
|
||||
var standardWait = operation is 2 or 3;
|
||||
var packetDwords = standardWait ? 7u : size == 0 ? 6u : 9u;
|
||||
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address & (size == 0 ? ~0x3u : ~0x7u)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32) & 0x3FFFFu) ||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (standardWait)
|
||||
{
|
||||
if (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItWaitRegMem, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, compareFunction | ((operation & 1) << 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)address) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(address >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)mask) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
}
|
||||
else if (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
else if (size == 0)
|
||||
{
|
||||
if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, EncodeWaitRegMem32Control(compareFunction, operation, cachePolicy)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, EncodeWaitRegMemPoll(pollCycles)))
|
||||
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction | (operation << 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
@@ -1852,8 +1844,8 @@ public static partial class AgcExports
|
||||
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, EncodeWaitRegMem64Control(compareFunction, operation, cachePolicy)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 32, EncodeWaitRegMemPoll(pollCycles)))
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction | (operation << 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 32, pollCycles / 40))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
@@ -2292,14 +2284,7 @@ public static partial class AgcExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var wrote = op == ItNop && register is RWaitMem32 or RWaitMem64
|
||||
? TryWriteUInt32(
|
||||
ctx,
|
||||
commandAddress + fieldOffset,
|
||||
(uint)address & (register == RWaitMem32 ? ~0x3u : ~0x7u)) &&
|
||||
TryWriteUInt32(ctx, commandAddress + fieldOffset + 4, (uint)(address >> 32) & 0x3FFFFu)
|
||||
: ctx.TryWriteUInt64(commandAddress + fieldOffset, address);
|
||||
return wrote
|
||||
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -2322,7 +2307,7 @@ public static partial class AgcExports
|
||||
var fieldOffset = op == ItWaitRegMem
|
||||
? 4UL
|
||||
: op == ItNop && register == RWaitMem32
|
||||
? 20UL
|
||||
? 16UL
|
||||
: op == ItNop && register == RWaitMem64
|
||||
? 28UL
|
||||
: 0;
|
||||
@@ -2351,7 +2336,7 @@ public static partial class AgcExports
|
||||
var wrote = op == ItWaitRegMem
|
||||
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
||||
: op == ItNop && register == RWaitMem32
|
||||
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
||||
? TryWriteUInt32(ctx, commandAddress + 20, (uint)reference)
|
||||
: op == ItNop && register == RWaitMem64 &&
|
||||
ctx.TryWriteUInt64(commandAddress + 20, reference);
|
||||
return wrote
|
||||
@@ -3099,26 +3084,6 @@ public static partial class AgcExports
|
||||
CountSubmittedOpcode(op, register);
|
||||
}
|
||||
|
||||
if ((header & 1u) != 0 && state.PredicateSkip)
|
||||
{
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.predicated_skip queue={state.QueueName} " +
|
||||
$"packet=0x{currentAddress:X16} op=0x{op:X2} len={length}");
|
||||
}
|
||||
|
||||
offset += length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (op == ItSetPredication)
|
||||
{
|
||||
ApplySubmittedPredication(ctx, state, currentAddress, length, tracePackets);
|
||||
offset += length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (op == ItNop &&
|
||||
register is RDrawReset or RAcbReset &&
|
||||
length >= 2)
|
||||
@@ -3837,7 +3802,7 @@ public static partial class AgcExports
|
||||
|
||||
if (!stale && producer is null &&
|
||||
!_tracedProducerlessWaits.Add(
|
||||
(memory, waiter.WaitAddress)))
|
||||
(memory, waiter.WaitAddress, waiter.SubmissionId)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -4033,111 +3998,6 @@ public static partial class AgcExports
|
||||
state.DrawIndexOffset = 0;
|
||||
}
|
||||
|
||||
private static void ApplySubmittedPredication(
|
||||
CpuContext ctx,
|
||||
SubmittedDcbState state,
|
||||
ulong packetAddress,
|
||||
uint packetLength,
|
||||
bool tracePacket)
|
||||
{
|
||||
if (packetLength < 3 ||
|
||||
!TryReadUInt32(ctx, packetAddress + 4, out var first) ||
|
||||
!TryReadUInt32(ctx, packetAddress + 8, out var second))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const uint flagsMask = 0x0007_1100u;
|
||||
uint flags;
|
||||
ulong predicateAddress;
|
||||
if (packetLength >= 4 &&
|
||||
(first & ~flagsMask) == 0 &&
|
||||
TryReadUInt32(ctx, packetAddress + 12, out var third) &&
|
||||
third <= 0xFFFFu)
|
||||
{
|
||||
flags = first;
|
||||
predicateAddress = ((ulong)third << 32) | (second & 0xFFFF_FFF0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = second;
|
||||
predicateAddress = (first & 0xFFFF_FFF0u) | ((ulong)(second & 0xFFu) << 32);
|
||||
}
|
||||
|
||||
var operation = (flags >> 16) & 0x7u;
|
||||
if (operation == 0)
|
||||
{
|
||||
state.PredicateSkip = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (operation != 3)
|
||||
{
|
||||
if (tracePacket)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.predication_unsupported packet=0x{packetAddress:X16} " +
|
||||
$"op={operation} addr=0x{predicateAddress:X16}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var waitOperation = (flags >> 12) & 1u;
|
||||
var value = 0UL;
|
||||
var readSucceeded = false;
|
||||
void ReadPredicate() =>
|
||||
readSucceeded = ctx.TryReadUInt64(predicateAddress, out value);
|
||||
|
||||
if (waitOperation != 0)
|
||||
{
|
||||
var sequence = GuestGpu.Current.SubmitOrderedGuestAction(
|
||||
ReadPredicate,
|
||||
$"set_predication read 0x{predicateAddress:X16}");
|
||||
if (sequence == 0)
|
||||
{
|
||||
ReadPredicate();
|
||||
}
|
||||
else if (!GuestGpu.Current.WaitForGuestWork(sequence))
|
||||
{
|
||||
if (tracePacket)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.predication_wait_failed packet=0x{packetAddress:X16} " +
|
||||
$"addr=0x{predicateAddress:X16} sequence={sequence}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadPredicate();
|
||||
}
|
||||
|
||||
if (!readSucceeded)
|
||||
{
|
||||
if (tracePacket)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.predication_read_failed packet=0x{packetAddress:X16} " +
|
||||
$"addr=0x{predicateAddress:X16}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var condition = (flags >> 8) & 1u;
|
||||
state.PredicateSkip = condition == 0 ? value != 0 : value == 0;
|
||||
if (tracePacket)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.dcb.predication packet=0x{packetAddress:X16} " +
|
||||
$"addr=0x{predicateAddress:X16} value=0x{value:X16} " +
|
||||
$"condition={condition} wait={waitOperation} skip={state.PredicateSkip}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RangesOverlap(
|
||||
ulong leftAddress,
|
||||
ulong leftLength,
|
||||
@@ -4680,7 +4540,6 @@ public static partial class AgcExports
|
||||
private static bool TryParseSubmittedWait(
|
||||
CpuContext ctx,
|
||||
ulong packetAddress,
|
||||
uint packetLength,
|
||||
bool is64Bit,
|
||||
bool isStandard,
|
||||
out ulong waitAddress,
|
||||
@@ -4711,10 +4570,8 @@ public static partial class AgcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
var legacyWait32 = !is64Bit && packetLength == 6;
|
||||
var controlOffset = is64Bit ? 28u : legacyWait32 ? 16u : 20u;
|
||||
if (!TryReadUInt64(ctx, packetAddress + 4, out waitAddress) ||
|
||||
!TryReadUInt32(ctx, packetAddress + controlOffset, out var control))
|
||||
!TryReadUInt32(ctx, packetAddress + (is64Bit ? 28u : 16u), out var control))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -4727,9 +4584,8 @@ public static partial class AgcExports
|
||||
TryReadUInt64(ctx, packetAddress + 20, out reference);
|
||||
}
|
||||
|
||||
var referenceOffset = legacyWait32 ? 20u : 16u;
|
||||
if (!TryReadUInt32(ctx, packetAddress + 12, out var mask32) ||
|
||||
!TryReadUInt32(ctx, packetAddress + referenceOffset, out var reference32))
|
||||
!TryReadUInt32(ctx, packetAddress + 20, out var reference32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -4834,7 +4690,7 @@ public static partial class AgcExports
|
||||
bool tracePacket)
|
||||
{
|
||||
if (!TryParseSubmittedWait(
|
||||
ctx, packetAddress, length, is64Bit, isStandard,
|
||||
ctx, packetAddress, is64Bit, isStandard,
|
||||
out var waitAddress, out var reference, out var mask, out var compareFunction,
|
||||
out var controlValue))
|
||||
{
|
||||
@@ -6088,8 +5944,7 @@ public static partial class AgcExports
|
||||
binding,
|
||||
exportEvaluation.ImageBindings),
|
||||
binding.MipLevel ?? 0,
|
||||
binding.SamplerDescriptor,
|
||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
||||
binding.SamplerDescriptor));
|
||||
}
|
||||
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
@@ -6546,8 +6401,7 @@ public static partial class AgcExports
|
||||
texture,
|
||||
isStorage,
|
||||
binding.MipLevel ?? 0,
|
||||
binding.SamplerDescriptor,
|
||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
||||
binding.SamplerDescriptor));
|
||||
}
|
||||
|
||||
error = string.Empty;
|
||||
@@ -7518,7 +7372,6 @@ public static partial class AgcExports
|
||||
binding.IsStorage,
|
||||
binding.MipLevel,
|
||||
binding.SamplerDescriptor,
|
||||
binding.IsArrayed,
|
||||
out var texture))
|
||||
{
|
||||
textures.Add(texture);
|
||||
@@ -7914,10 +7767,7 @@ public static partial class AgcExports
|
||||
TextureDescriptor descriptor,
|
||||
uint sourceWidth,
|
||||
int logicalByteCount,
|
||||
byte[] source,
|
||||
bool baseMipInTail = false,
|
||||
int tailElementX = 0,
|
||||
int tailElementY = 0)
|
||||
byte[] source)
|
||||
{
|
||||
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
|
||||
!TryGetTextureElementLayout(
|
||||
@@ -7930,48 +7780,6 @@ public static partial class AgcExports
|
||||
return null;
|
||||
}
|
||||
|
||||
if (baseMipInTail)
|
||||
{
|
||||
if (!GnmTiling.TryGetBlockElementDimensions(
|
||||
descriptor.TileMode,
|
||||
bytesPerElement,
|
||||
out var blockWidth,
|
||||
out var blockHeight))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockByteCount = (long)blockWidth * blockHeight * bytesPerElement;
|
||||
if (source.Length < blockByteCount ||
|
||||
(long)elementsWide * elementsHigh * bytesPerElement > logicalByteCount)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockLinear = new byte[blockByteCount];
|
||||
if (!GnmTiling.TryDetile(
|
||||
source,
|
||||
blockLinear,
|
||||
descriptor.TileMode,
|
||||
blockWidth,
|
||||
blockHeight,
|
||||
bytesPerElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tailLinear = new byte[logicalByteCount];
|
||||
var rowBytes = elementsWide * bytesPerElement;
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
{
|
||||
var sourceOffset = (((long)tailElementY + y) * blockWidth + tailElementX) * bytesPerElement;
|
||||
blockLinear.AsSpan((int)sourceOffset, rowBytes)
|
||||
.CopyTo(tailLinear.AsSpan(y * rowBytes, rowBytes));
|
||||
}
|
||||
|
||||
return tailLinear;
|
||||
}
|
||||
|
||||
var linear = new byte[logicalByteCount];
|
||||
return GnmTiling.TryDetile(
|
||||
source,
|
||||
@@ -8009,23 +7817,18 @@ public static partial class AgcExports
|
||||
bool isStorage,
|
||||
uint mipLevel,
|
||||
IReadOnlyList<uint> samplerDescriptor,
|
||||
bool isArrayed,
|
||||
out GuestDrawTexture texture)
|
||||
{
|
||||
texture = default!;
|
||||
if ((descriptor.Type != Gen5TextureType1D &&
|
||||
descriptor.Type != Gen5TextureType2D &&
|
||||
descriptor.Type != Gen5TextureType3D &&
|
||||
descriptor.Type != Gen5TextureTypeCube &&
|
||||
descriptor.Type != Gen5TextureType1DArray &&
|
||||
descriptor.Type != Gen5TextureType2DArray) ||
|
||||
descriptor.Type != Gen5TextureType2D) ||
|
||||
descriptor.Width == 0 ||
|
||||
descriptor.Height == 0 ||
|
||||
descriptor.Width > 8192 ||
|
||||
descriptor.Height > 8192)
|
||||
{
|
||||
TraceTextureFallback(descriptor, "invalid-descriptor");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8046,22 +7849,18 @@ public static partial class AgcExports
|
||||
TraceTextureFallback(
|
||||
descriptor,
|
||||
$"invalid-byte-count:{sourceByteCount}");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
var physicalSourceByteCount = sourceByteCount;
|
||||
var elementsWide = 0;
|
||||
var elementsHigh = 0;
|
||||
var bytesPerElement = 0;
|
||||
var hasElementLayout = GnmTiling.NeedsDetile(descriptor.TileMode) &&
|
||||
if (GnmTiling.NeedsDetile(descriptor.TileMode) &&
|
||||
TryGetTextureElementLayout(
|
||||
descriptor,
|
||||
sourceWidth,
|
||||
out elementsWide,
|
||||
out elementsHigh,
|
||||
out bytesPerElement);
|
||||
if (hasElementLayout &&
|
||||
out var elementsWide,
|
||||
out var elementsHigh,
|
||||
out var bytesPerElement) &&
|
||||
GnmTiling.TryGetTiledByteCount(
|
||||
descriptor.TileMode,
|
||||
elementsWide,
|
||||
@@ -8075,51 +7874,13 @@ public static partial class AgcExports
|
||||
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
|
||||
physicalSourceByteCount > int.MaxValue)
|
||||
{
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
var resourceMipLevels = descriptor.HasExtendedDescriptor
|
||||
? descriptor.ResourceMipLevels
|
||||
: 1u;
|
||||
var baseMipByteOffset = 0UL;
|
||||
var baseMipInTail = false;
|
||||
var mipTailElementX = 0;
|
||||
var mipTailElementY = 0;
|
||||
var chainSliceBytes = physicalSourceByteCount;
|
||||
if (hasElementLayout && resourceMipLevels > 1 &&
|
||||
GnmTiling.TryGetBaseMipPlacement(
|
||||
descriptor.TileMode,
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
bytesPerElement,
|
||||
resourceMipLevels,
|
||||
out baseMipByteOffset,
|
||||
out baseMipInTail,
|
||||
out mipTailElementX,
|
||||
out mipTailElementY,
|
||||
out var placedChainSliceBytes))
|
||||
{
|
||||
chainSliceBytes = placedChainSliceBytes;
|
||||
}
|
||||
|
||||
var wantsArrayUpload = isArrayed &&
|
||||
!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
(descriptor.Type == Gen5TextureType2DArray ||
|
||||
descriptor.Type == Gen5TextureType1DArray) &&
|
||||
descriptor.Depth > 1 &&
|
||||
!_arrayUploadUnsupported.ContainsKey(descriptor.Address);
|
||||
var arrayUploadLayers = wantsArrayUpload ? descriptor.Depth : 1u;
|
||||
|
||||
// Upload-known (not plain availability): the presenter's answer goes
|
||||
// generation-stale when the guest CPU rewrites a CPU-backed image
|
||||
// (video planes, streamed font atlases), which routes this draw back
|
||||
// through the texel copy below so the refresh path re-uploads.
|
||||
if (!isStorage &&
|
||||
!wantsArrayUpload &&
|
||||
descriptor.Address != 0 &&
|
||||
GuestGpu.Current.IsGuestImageUploadKnown(
|
||||
GuestGpu.Current.IsGpuGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
@@ -8140,8 +7901,7 @@ public static partial class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToGuestSampler(samplerDescriptor),
|
||||
ArrayedView: isArrayed);
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8164,17 +7924,14 @@ public static partial class AgcExports
|
||||
// and run the same AddrLib-derived detile path used below for
|
||||
// sampled textures before seeding the Vulkan image.
|
||||
var storageSource = new byte[(int)physicalSourceByteCount];
|
||||
if (ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, storageSource))
|
||||
if (ctx.Memory.TryRead(descriptor.Address, storageSource))
|
||||
{
|
||||
readSucceeded = true;
|
||||
var linearStorage = TryDetileTextureSource(
|
||||
descriptor,
|
||||
sourceWidth,
|
||||
checked((int)sourceByteCount),
|
||||
storageSource,
|
||||
baseMipInTail,
|
||||
mipTailElementX,
|
||||
mipTailElementY) ?? storageSource
|
||||
storageSource) ?? storageSource
|
||||
.AsSpan(0, checked((int)sourceByteCount))
|
||||
.ToArray();
|
||||
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
||||
@@ -8230,19 +7987,6 @@ public static partial class AgcExports
|
||||
// (skipping would leave the draw with no pixels and a fallback
|
||||
// texture for the frame — visible flicker on animated textures).
|
||||
var sampler = ToGuestSampler(samplerDescriptor);
|
||||
// Track the guest allocation before reading its texels so a CPU
|
||||
// rewrite landing after the copy still bumps the write generation.
|
||||
// The generation rides on the texture and is recorded by the
|
||||
// presenter after upload, where the upload-known skip compares it
|
||||
// against the tracker to force fresh texels for rewritten memory.
|
||||
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
||||
descriptor.Address,
|
||||
physicalSourceByteCount,
|
||||
source: "agc.decoded-texture");
|
||||
var hasWriteGeneration =
|
||||
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
|
||||
descriptor.Address,
|
||||
out var writeGeneration);
|
||||
if (!_textureCopySkipDisabled &&
|
||||
descriptor.Address != 0 &&
|
||||
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
||||
@@ -8256,9 +8000,7 @@ public static partial class AgcExports
|
||||
descriptor.DstSelect,
|
||||
descriptor.TileMode,
|
||||
sourceWidth,
|
||||
sampler,
|
||||
isArrayed,
|
||||
arrayUploadLayers)))
|
||||
sampler)))
|
||||
{
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
@@ -8276,79 +8018,17 @@ public static partial class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: sampler,
|
||||
ArrayedView: isArrayed,
|
||||
ArrayLayers: arrayUploadLayers);
|
||||
Sampler: sampler);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wantsArrayUpload)
|
||||
{
|
||||
var arrayLayers = arrayUploadLayers;
|
||||
var layerBytes = checked((int)sourceByteCount);
|
||||
var totalBytes = (long)layerBytes * arrayLayers;
|
||||
if (totalBytes <= int.MaxValue)
|
||||
{
|
||||
var layered = new byte[totalBytes];
|
||||
var uploadedLayers = 0u;
|
||||
for (var layer = 0u; layer < arrayLayers; layer++)
|
||||
{
|
||||
var sliceSource = new byte[(int)physicalSourceByteCount];
|
||||
if (!ctx.Memory.TryRead(
|
||||
descriptor.Address + layer * chainSliceBytes + baseMipByteOffset,
|
||||
sliceSource))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var sliceLinear = TryDetileTextureSource(
|
||||
descriptor,
|
||||
sourceWidth,
|
||||
layerBytes,
|
||||
sliceSource,
|
||||
baseMipInTail,
|
||||
mipTailElementX,
|
||||
mipTailElementY) ?? sliceSource.AsSpan(0, layerBytes).ToArray();
|
||||
sliceLinear.AsSpan(0, layerBytes)
|
||||
.CopyTo(layered.AsSpan(checked((int)(layer * layerBytes))));
|
||||
uploadedLayers++;
|
||||
}
|
||||
|
||||
if (uploadedLayers == arrayLayers)
|
||||
{
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType,
|
||||
layered,
|
||||
IsFallback: false,
|
||||
IsStorage: false,
|
||||
MipLevels: descriptor.MipLevels,
|
||||
MipLevel: mipLevel,
|
||||
BaseMipLevel: descriptor.ViewBaseLevel,
|
||||
ResourceMipLevels: descriptor.ResourceMipLevels,
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: sampler,
|
||||
ArrayedView: true,
|
||||
ArrayLayers: arrayLayers);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_arrayUploadUnsupported.TryAdd(descriptor.Address, 0);
|
||||
}
|
||||
|
||||
var source = new byte[(int)physicalSourceByteCount];
|
||||
if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
|
||||
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
||||
{
|
||||
TraceTextureFallback(
|
||||
descriptor,
|
||||
$"guest-read-failed:{sourceByteCount}");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8380,10 +8060,7 @@ public static partial class AgcExports
|
||||
descriptor,
|
||||
sourceWidth,
|
||||
checked((int)sourceByteCount),
|
||||
source,
|
||||
baseMipInTail,
|
||||
mipTailElementX,
|
||||
mipTailElementY) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
|
||||
source) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
|
||||
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
@@ -8401,9 +8078,7 @@ public static partial class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToGuestSampler(samplerDescriptor),
|
||||
WriteGeneration: hasWriteGeneration ? writeGeneration : -1,
|
||||
ArrayedView: isArrayed);
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8682,8 +8357,7 @@ public static partial class AgcExports
|
||||
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
bool isStorage,
|
||||
uint format,
|
||||
uint numberType,
|
||||
bool isArrayed = false)
|
||||
uint numberType)
|
||||
{
|
||||
var fallbackFormat = format == 0 ? 10u : format;
|
||||
var fallbackNumberType = numberType;
|
||||
@@ -8697,8 +8371,7 @@ public static partial class AgcExports
|
||||
IsFallback: true,
|
||||
IsStorage: isStorage,
|
||||
MipLevels: 1,
|
||||
MipLevel: 0,
|
||||
ArrayedView: isArrayed);
|
||||
MipLevel: 0);
|
||||
}
|
||||
|
||||
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
||||
@@ -9074,8 +8747,7 @@ public static partial class AgcExports
|
||||
texture,
|
||||
isStorage,
|
||||
binding.MipLevel ?? 0,
|
||||
binding.SamplerDescriptor,
|
||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
||||
binding.SamplerDescriptor));
|
||||
hasStorageBinding |= isStorage;
|
||||
|
||||
var descriptorState = descriptorValid ? string.Empty : "/invalid-desc";
|
||||
@@ -11033,23 +10705,6 @@ public static partial class AgcExports
|
||||
((op & 0xFFu) << 8) |
|
||||
((register & 0x3Fu) << 2);
|
||||
|
||||
private static uint EncodeWaitRegMemPoll(uint pollCycles) =>
|
||||
Math.Min(pollCycles >> 4, 0xFFFFu);
|
||||
|
||||
private static uint EncodeWaitRegMem32Control(uint compareFunction, uint operation, uint cachePolicy) =>
|
||||
0x10u |
|
||||
(compareFunction & 0x7u) |
|
||||
((operation & 0x3u) << 8) |
|
||||
((operation & 0xCu) << 4) |
|
||||
((cachePolicy & 0x3u) << 25);
|
||||
|
||||
private static uint EncodeWaitRegMem64Control(uint compareFunction, uint operation, uint cachePolicy) =>
|
||||
0x10u |
|
||||
(compareFunction & 0x7u) |
|
||||
((operation & 0x1u) << 8) |
|
||||
((operation & 0x6u) << 5) |
|
||||
((cachePolicy & 0x3u) << 25);
|
||||
|
||||
private static uint Pm4Length(uint header) =>
|
||||
((header >> 16) & 0x3FFFu) + 2u;
|
||||
|
||||
@@ -11504,21 +11159,16 @@ public static partial class AgcExports
|
||||
public static int DcbSetPredication(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var condition = (uint)(ctx[CpuRegister.Rsi] & 1u);
|
||||
var operation = (uint)(ctx[CpuRegister.Rdx] & 0x7u);
|
||||
var waitOperation = (uint)(ctx[CpuRegister.Rcx] & 1u);
|
||||
var address = ctx[CpuRegister.R8];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
var flags = (condition << 8) | (waitOperation << 12) | (operation << 16);
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItSetPredication, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, flags) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)address & 0xFFFF_FFF0u) ||
|
||||
!ctx.TryWriteUInt32(cmd + 12, (uint)(address >> 32)))
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
@@ -11533,17 +11183,9 @@ public static partial class AgcExports
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int SetPacketPredication(CpuContext ctx)
|
||||
{
|
||||
var packetAddress = ctx[CpuRegister.Rdi];
|
||||
var predication = ctx[CpuRegister.Rsi];
|
||||
if (packetAddress == 0 || !TryReadUInt32(ctx, packetAddress, out var header))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
header = (header & ~1u) | (predication == 1 ? 1u : 0u);
|
||||
return !ctx.TryWriteUInt32(packetAddress, header)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
// Global predication toggle on a packet; a no-op is safe for rendering.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
|
||||
@@ -11718,7 +11360,7 @@ public static partial class AgcExports
|
||||
uint owner;
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (state.ResourceRegistrationInitialized &&
|
||||
if (!state.ResourceRegistrationInitialized ||
|
||||
state.ResourceRegistrationMaxOwners != 0 &&
|
||||
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
|
||||
{
|
||||
@@ -11755,69 +11397,6 @@ public static partial class AgcExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int RemoveResourcesForOwner(SubmittedGpuState state, uint owner)
|
||||
{
|
||||
var stale = new List<uint>();
|
||||
foreach (var (handle, resource) in state.RegisteredResources)
|
||||
{
|
||||
if (resource.Owner == owner)
|
||||
{
|
||||
stale.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var handle in stale)
|
||||
{
|
||||
state.RegisteredResources.Remove(handle);
|
||||
}
|
||||
|
||||
return stale.Count;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZLJk9r2+2Aw",
|
||||
ExportName = "sceAgcDriverUnregisterOwnerAndResources",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DriverUnregisterOwnerAndResources(CpuContext ctx)
|
||||
{
|
||||
var owner = (uint)ctx[CpuRegister.Rdi];
|
||||
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
int resources;
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (!state.ResourceOwners.Remove(owner))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
resources = RemoveResourcesForOwner(state, owner);
|
||||
state.ComputeQueues.Remove(owner);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.driver_unregister_owner owner={owner} resources={resources}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "SCoAN5fYlUM",
|
||||
ExportName = "sceAgcDriverUnregisterAllResourcesForOwner",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DriverUnregisterAllResourcesForOwner(CpuContext ctx)
|
||||
{
|
||||
var owner = (uint)ctx[CpuRegister.Rdi];
|
||||
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
int resources;
|
||||
lock (state.Gate)
|
||||
{
|
||||
resources = RemoveResourcesForOwner(state, owner);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.driver_unregister_owner_resources owner={owner} resources={resources}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pWLG7WOpVcw",
|
||||
ExportName = "sceAgcDriverUnregisterResource",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,11 +16,8 @@ namespace SharpEmu.Libs.Agc;
|
||||
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||
/// equations are being ported.
|
||||
/// </summary>
|
||||
internal static unsafe class GnmTiling
|
||||
internal static class GnmTiling
|
||||
{
|
||||
private const int ParallelDetileElementThreshold = 512 * 512;
|
||||
private const int MaxDetileWorkers = 4;
|
||||
|
||||
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
|
||||
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
|
||||
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||
@@ -124,14 +118,6 @@ internal static unsafe class GnmTiling
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly HashSet<uint> _reportedModes = new();
|
||||
private static readonly ConcurrentDictionary<(uint SwizzleMode, int BppLog2), PatternTerms>
|
||||
_patternTermCache = new();
|
||||
private static readonly ConcurrentDictionary<(SwizzleKind Kind, int Width, int Height), int[]>
|
||||
_blockTableCache = new();
|
||||
private static readonly ParallelOptions _parallelDetileOptions = new()
|
||||
{
|
||||
MaxDegreeOfParallelism = Math.Min(MaxDetileWorkers, Environment.ProcessorCount),
|
||||
};
|
||||
|
||||
public static bool Enabled => _enabled || !_disabled;
|
||||
|
||||
@@ -208,164 +194,6 @@ internal static unsafe class GnmTiling
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetBlockElementDimensions(
|
||||
uint swizzleMode,
|
||||
int bytesPerElement,
|
||||
out int blockWidth,
|
||||
out int blockHeight)
|
||||
{
|
||||
blockWidth = 0;
|
||||
blockHeight = 0;
|
||||
if (bytesPerElement <= 0 ||
|
||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||
if (bppLog2 < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
(blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
||||
return blockWidth != 0 && blockHeight != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
|
||||
/// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
|
||||
/// </summary>
|
||||
public static bool TryGetBaseMipPlacement(
|
||||
uint swizzleMode,
|
||||
int elementsWide,
|
||||
int elementsHigh,
|
||||
int bytesPerElement,
|
||||
uint resourceMipLevels,
|
||||
out ulong byteOffset,
|
||||
out bool inMipTail,
|
||||
out int tailElementX,
|
||||
out int tailElementY,
|
||||
out ulong chainSliceBytes)
|
||||
{
|
||||
byteOffset = 0;
|
||||
inMipTail = false;
|
||||
tailElementX = 0;
|
||||
tailElementY = 0;
|
||||
chainSliceBytes = 0;
|
||||
if (resourceMipLevels <= 1 ||
|
||||
!ShouldDetile(swizzleMode) ||
|
||||
elementsWide <= 0 ||
|
||||
elementsHigh <= 0 ||
|
||||
bytesPerElement <= 0 ||
|
||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||
if (bppLog2 < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
||||
var blockSizeLog2 = BitLog2((uint)blockBytes);
|
||||
if (blockWidth == 0 || blockHeight == 0 || blockSizeLog2 < 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mipLevels = (int)Math.Min(resourceMipLevels, 16u);
|
||||
var maxMipsInTail = blockSizeLog2 <= 8 ? 0
|
||||
: blockSizeLog2 <= 11
|
||||
? 1 + (1 << (blockSizeLog2 - 9))
|
||||
: blockSizeLog2 - 4;
|
||||
var tailWidth = (blockSizeLog2 & 1) != 0 ? blockWidth >> 1 : blockWidth;
|
||||
var tailHeight = (blockSizeLog2 & 1) != 0 ? blockHeight : blockHeight >> 1;
|
||||
|
||||
var firstMipInTail = mipLevels;
|
||||
var mipSizes = new ulong[mipLevels];
|
||||
for (var i = 0; i < mipLevels; i++)
|
||||
{
|
||||
var mipWidth = Math.Max(elementsWide >> i, 1);
|
||||
var mipHeight = Math.Max(elementsHigh >> i, 1);
|
||||
if (maxMipsInTail > 0 &&
|
||||
mipWidth <= tailWidth &&
|
||||
mipHeight <= tailHeight &&
|
||||
mipLevels - i <= maxMipsInTail)
|
||||
{
|
||||
firstMipInTail = i;
|
||||
break;
|
||||
}
|
||||
|
||||
var alignedWidth = (ulong)(mipWidth + blockWidth - 1) / (ulong)blockWidth * (ulong)blockWidth;
|
||||
var alignedHeight = (ulong)(mipHeight + blockHeight - 1) / (ulong)blockHeight * (ulong)blockHeight;
|
||||
mipSizes[i] = alignedWidth * alignedHeight * (ulong)bytesPerElement;
|
||||
}
|
||||
|
||||
if (firstMipInTail == 0)
|
||||
{
|
||||
var m = maxMipsInTail - 1;
|
||||
var mipOffset = m > 6 ? 16 << m : m << 8;
|
||||
var mipX = ((mipOffset >> 9) & 1) |
|
||||
((mipOffset >> 10) & 2) |
|
||||
((mipOffset >> 11) & 4) |
|
||||
((mipOffset >> 12) & 8) |
|
||||
((mipOffset >> 13) & 16) |
|
||||
((mipOffset >> 14) & 32);
|
||||
var mipY = ((mipOffset >> 8) & 1) |
|
||||
((mipOffset >> 9) & 2) |
|
||||
((mipOffset >> 10) & 4) |
|
||||
((mipOffset >> 11) & 8) |
|
||||
((mipOffset >> 12) & 16) |
|
||||
((mipOffset >> 13) & 32);
|
||||
if ((blockSizeLog2 & 1) != 0)
|
||||
{
|
||||
(mipX, mipY) = (mipY, mipX);
|
||||
if ((bppLog2 & 1) != 0)
|
||||
{
|
||||
mipY = (mipY << 1) | (mipX & 1);
|
||||
mipX >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
var (microWidth, microHeight) = SquareBlockDimensions(256 >> bppLog2);
|
||||
if (microWidth == 0 || microHeight == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tailElementX = mipX * microWidth;
|
||||
tailElementY = mipY * microHeight;
|
||||
if (tailElementX + elementsWide > blockWidth ||
|
||||
tailElementY + elementsHigh > blockHeight)
|
||||
{
|
||||
tailElementX = 0;
|
||||
tailElementY = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
inMipTail = true;
|
||||
chainSliceBytes = (ulong)blockBytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
byteOffset = firstMipInTail < mipLevels ? (ulong)blockBytes : 0;
|
||||
chainSliceBytes = byteOffset;
|
||||
for (var i = firstMipInTail - 1; i >= 1; i--)
|
||||
{
|
||||
byteOffset += mipSizes[i];
|
||||
}
|
||||
|
||||
for (var i = 0; i < firstMipInTail; i++)
|
||||
{
|
||||
chainSliceBytes += mipSizes[i];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||
@@ -418,83 +246,50 @@ internal static unsafe class GnmTiling
|
||||
return false;
|
||||
}
|
||||
|
||||
// Address tables depend only on the swizzle equation and element size,
|
||||
// so retain them across textures instead of rebuilding them per upload.
|
||||
// Precompute the within-block element offset for each (x, y) inside a
|
||||
// single block. The swizzle equation only depends on the in-block
|
||||
// coordinates, so this table is reused for every block — turning the
|
||||
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
|
||||
// Detiling a 2048x2048 texture is millions of elements; without this the
|
||||
// per-pixel math makes DETILE unusably slow during asset streaming.
|
||||
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||
var patternTerms = hasExactXorPattern
|
||||
? _patternTermCache.GetOrAdd(
|
||||
(swizzleMode, bppLog2),
|
||||
_ => CreatePatternTerms(xorPattern))
|
||||
: default;
|
||||
var blockTable = hasExactXorPattern
|
||||
? []
|
||||
: _blockTableCache.GetOrAdd(
|
||||
(kind, blockWidth, blockHeight),
|
||||
static key => CreateBlockTable(key.Kind, key.Width, key.Height));
|
||||
|
||||
// The XOR equation offset factors cleanly into independent X and Y
|
||||
// fields — each output bit is parity(x & XMask) XOR parity(y & YMask),
|
||||
// and parity distributes over XOR, so offset(x, y) == xTerm(x) ^ yTerm(y).
|
||||
// Exact equations repeat at a small power-of-two period. Cached axis
|
||||
// terms reduce the inner loop to two array loads and one XOR.
|
||||
fixed (byte* tiledPointer = tiled)
|
||||
fixed (byte* linearPointer = linear)
|
||||
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||
{
|
||||
var sourceAddress = (nint)tiledPointer;
|
||||
var destinationAddress = (nint)linearPointer;
|
||||
var sourceLength = tiled.Length;
|
||||
var destinationLength = linear.Length;
|
||||
var blockWidthShift = BitLog2((uint)blockWidth);
|
||||
var blockWidthMask = blockWidth - 1;
|
||||
var detileRow = (int y) =>
|
||||
for (var bx = 0; bx < blockWidth; bx++)
|
||||
{
|
||||
var blockY = y / blockHeight;
|
||||
var inBlockY = y & (blockHeight - 1);
|
||||
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||
var tableRowBase = inBlockY * blockWidth;
|
||||
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||
var yTerm = hasExactXorPattern
|
||||
? patternTerms.Y[y & patternTerms.YMask]
|
||||
: 0;
|
||||
for (var x = 0; x < elementsWide; x++)
|
||||
{
|
||||
var blockX = x >> blockWidthShift;
|
||||
var inBlockX = x & blockWidthMask;
|
||||
var blockIndex = rowBlockBase + blockX;
|
||||
var sourceByte = hasExactXorPattern
|
||||
? blockIndex * blockBytes + (patternTerms.X[x & patternTerms.XMask] ^ yTerm)
|
||||
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||
(long)bytesPerElement;
|
||||
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||
if (sourceByte < 0 ||
|
||||
sourceByte + bytesPerElement > sourceLength ||
|
||||
destByte + bytesPerElement > destinationLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CopyElement(
|
||||
(byte*)sourceAddress + sourceByte,
|
||||
(byte*)destinationAddress + destByte,
|
||||
bytesPerElement);
|
||||
}
|
||||
};
|
||||
|
||||
var elementCount = (long)elementsWide * elementsHigh;
|
||||
if (elementCount >= ParallelDetileElementThreshold && Environment.ProcessorCount > 1)
|
||||
{
|
||||
Parallel.For(
|
||||
0,
|
||||
elementsHigh,
|
||||
_parallelDetileOptions,
|
||||
detileRow);
|
||||
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
{
|
||||
var blockY = y / blockHeight;
|
||||
var inBlockY = y % blockHeight;
|
||||
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||
var tableRowBase = inBlockY * blockWidth;
|
||||
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||
for (var x = 0; x < elementsWide; x++)
|
||||
{
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
var blockX = x / blockWidth;
|
||||
var inBlockX = x % blockWidth;
|
||||
|
||||
var blockIndex = rowBlockBase + blockX;
|
||||
var sourceByte = hasExactXorPattern
|
||||
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
|
||||
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||
(long)bytesPerElement;
|
||||
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||
if (sourceByte + bytesPerElement > tiled.Length ||
|
||||
destByte + bytesPerElement > linear.Length)
|
||||
{
|
||||
detileRow(y);
|
||||
continue;
|
||||
}
|
||||
|
||||
tiled.Slice((int)sourceByte, bytesPerElement)
|
||||
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,80 +302,6 @@ internal static unsafe class GnmTiling
|
||||
ZOrder,
|
||||
}
|
||||
|
||||
private readonly record struct PatternTerms(int[] X, int XMask, int[] Y, int YMask);
|
||||
|
||||
private static PatternTerms CreatePatternTerms(AddressBit[] pattern)
|
||||
{
|
||||
uint xMask = 0;
|
||||
uint yMask = 0;
|
||||
foreach (var bit in pattern)
|
||||
{
|
||||
xMask |= bit.XMask;
|
||||
yMask |= bit.YMask;
|
||||
}
|
||||
|
||||
var xLength = AxisTermPeriod(xMask);
|
||||
var yLength = AxisTermPeriod(yMask);
|
||||
var xTerms = new int[xLength];
|
||||
var yTerms = new int[yLength];
|
||||
for (var x = 0; x < xTerms.Length; x++)
|
||||
{
|
||||
xTerms[x] = (int)PatternAxisTerm((uint)x, pattern, useX: true);
|
||||
}
|
||||
|
||||
for (var y = 0; y < yTerms.Length; y++)
|
||||
{
|
||||
yTerms[y] = (int)PatternAxisTerm((uint)y, pattern, useX: false);
|
||||
}
|
||||
|
||||
return new PatternTerms(xTerms, xLength - 1, yTerms, yLength - 1);
|
||||
}
|
||||
|
||||
private static int AxisTermPeriod(uint mask) =>
|
||||
mask == 0 ? 1 : 1 << (32 - System.Numerics.BitOperations.LeadingZeroCount(mask));
|
||||
|
||||
private static int[] CreateBlockTable(SwizzleKind kind, int blockWidth, int blockHeight)
|
||||
{
|
||||
var table = new int[blockWidth * blockHeight];
|
||||
for (var y = 0; y < blockHeight; y++)
|
||||
{
|
||||
for (var x = 0; x < blockWidth; x++)
|
||||
{
|
||||
table[y * blockWidth + x] = (int)(kind == SwizzleKind.ZOrder
|
||||
? MortonInterleave((uint)x, (uint)y, blockWidth, blockHeight)
|
||||
: StandardSwizzleOffset((uint)x, (uint)y, blockWidth, blockHeight));
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyElement(byte* source, byte* destination, int bytesPerElement)
|
||||
{
|
||||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
*destination = *source;
|
||||
break;
|
||||
case 2:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<ushort>(source));
|
||||
break;
|
||||
case 4:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<uint>(source));
|
||||
break;
|
||||
case 8:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<ulong>(source));
|
||||
break;
|
||||
case 16:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<UInt128>(source));
|
||||
break;
|
||||
default:
|
||||
Unsafe.CopyBlockUnaligned(destination, source, (uint)bytesPerElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly AddressBit Zero = new(0, 0);
|
||||
|
||||
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||
@@ -614,20 +335,14 @@ internal static unsafe class GnmTiling
|
||||
return pattern.Length != 0;
|
||||
}
|
||||
|
||||
// The AddrLib within-block byte offset is a per-bit XOR equation:
|
||||
// offset = OR over bits of ( parity(x & XMask) XOR parity(y & YMask) ) << bit
|
||||
// Because parity distributes over XOR, that whole offset factors into two
|
||||
// independent axis terms: PatternAxisTerm(x, useX: true) ^
|
||||
// PatternAxisTerm(y, useX: false). Splitting the axes lets TryDetile cache
|
||||
// the X term per column and hoist the Y term per row instead of recomputing
|
||||
// the full 16-bit interleave (32 PopCounts) for every element.
|
||||
private static uint PatternAxisTerm(uint coordinate, AddressBit[] pattern, bool useX)
|
||||
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
|
||||
{
|
||||
uint offset = 0;
|
||||
for (var bit = 0; bit < pattern.Length; bit++)
|
||||
{
|
||||
var mask = useX ? pattern[bit].XMask : pattern[bit].YMask;
|
||||
var parity = System.Numerics.BitOperations.PopCount(coordinate & mask) & 1;
|
||||
var equation = pattern[bit];
|
||||
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
|
||||
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
|
||||
offset |= (uint)parity << bit;
|
||||
}
|
||||
|
||||
|
||||
@@ -343,45 +343,6 @@ internal static class GpuWaitRegistry
|
||||
return expired;
|
||||
}
|
||||
|
||||
public static List<WaitingDcb>? CollectAllForMemory(object memory)
|
||||
{
|
||||
List<WaitingDcb>? collected = null;
|
||||
lock (_gate)
|
||||
{
|
||||
List<ulong>? emptied = null;
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
for (var index = list.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (!ReferenceEquals(list[index].Memory, memory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collected ??= new List<WaitingDcb>();
|
||||
collected.Add(list[index]);
|
||||
list.RemoveAt(index);
|
||||
}
|
||||
|
||||
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 collected;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
|
||||
@@ -340,18 +340,6 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4fgtGfXDrFc",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteAddress_04_00",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWriteAddress0400(CpuContext ctx)
|
||||
{
|
||||
TraceAmpr(ctx, "measure_write_address", 0, WriteAddressRecordSize, 0);
|
||||
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "tZDDEo2tE5k",
|
||||
ExportName = "sceAmprCommandBufferGetSize",
|
||||
@@ -521,32 +509,6 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "j0+3uJMxYJY",
|
||||
ExportName = "sceAmprCommandBufferWriteAddress_04_00",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWriteAddress0400(CpuContext ctx)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
var value = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (commandBuffer == 0 || address == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "write_address", commandBuffer, address, value);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
|
||||
{
|
||||
if (commandBuffer == 0)
|
||||
|
||||
@@ -17,7 +17,7 @@ public static class AjmExports
|
||||
private const int OrbisAjmErrorCodecAlreadyRegistered = unchecked((int)0x80930009);
|
||||
private const int OrbisAjmErrorCodecNotRegistered = unchecked((int)0x8093000A);
|
||||
private const int OrbisAjmErrorWrongRevisionFlag = unchecked((int)0x8093000B);
|
||||
private const uint MaxCodecType = 25;
|
||||
private const uint MaxCodecType = 23;
|
||||
private const int MaxInstanceIndex = 0x2FFF;
|
||||
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
|
||||
private static int _nextContextId;
|
||||
|
||||
@@ -25,10 +25,6 @@ internal static class AudioPcmConversion
|
||||
float volume)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
// Volume is constant for the whole submission, so clamp it once here
|
||||
// rather than per sample inside the loop (this runs on every real-time
|
||||
// audio buffer, hundreds of frames at a time).
|
||||
var clampedVolume = Math.Clamp(volume, 0.0f, 1.0f);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
{
|
||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||
@@ -36,8 +32,8 @@ internal static class AudioPcmConversion
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
left = ApplyVolume(left, clampedVolume);
|
||||
right = ApplyVolume(right, clampedVolume);
|
||||
left = ApplyVolume(left, volume);
|
||||
right = ApplyVolume(right, volume);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||
}
|
||||
@@ -71,10 +67,9 @@ internal static class AudioPcmConversion
|
||||
return checked((short)MathF.Round(value * scale));
|
||||
}
|
||||
|
||||
// <paramref name="volume"/> is expected pre-clamped to [0, 1] by the caller.
|
||||
private static short ApplyVolume(short sample, float volume)
|
||||
{
|
||||
var scaled = MathF.Round(sample * volume);
|
||||
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
|
||||
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ public static class AvPlayerExports
|
||||
private const int FrameBufferCount = 3;
|
||||
private const int FrameInfoSize = 40;
|
||||
private const int FrameInfoExSize = 104;
|
||||
// This structure is 32 bytes. A larger write can damage the guest stack.
|
||||
private const int StreamInfoSize = 32;
|
||||
private const int StreamInfoExSize = 32;
|
||||
private const int StreamInfoSize = 40;
|
||||
private const int MaxGuestPathLength = 4096;
|
||||
private static readonly object StateGate = new();
|
||||
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
||||
@@ -406,8 +404,7 @@ public static class AvPlayerExports
|
||||
ExportName = "sceAvPlayerGetStreamInfoEx",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
|
||||
GetStreamInfoCore(ctx, StreamInfoExSize);
|
||||
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XC9wM+xULz8",
|
||||
@@ -564,48 +561,12 @@ public static class AvPlayerExports
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RegisterPlayerForTest(
|
||||
ulong handle,
|
||||
int width,
|
||||
int height,
|
||||
ulong durationMilliseconds)
|
||||
{
|
||||
PlayerState? previous;
|
||||
lock (StateGate)
|
||||
{
|
||||
Players.Remove(handle, out previous);
|
||||
Players[handle] = new PlayerState
|
||||
{
|
||||
Handle = handle,
|
||||
Width = width,
|
||||
Height = height,
|
||||
DurationMilliseconds = durationMilliseconds,
|
||||
};
|
||||
}
|
||||
|
||||
previous?.Dispose();
|
||||
}
|
||||
|
||||
internal static void RemovePlayerForTest(ulong handle)
|
||||
{
|
||||
PlayerState? player;
|
||||
lock (StateGate)
|
||||
{
|
||||
Players.Remove(handle, out player);
|
||||
}
|
||||
|
||||
player?.Dispose();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "d8FcbzfAdQw",
|
||||
ExportName = "sceAvPlayerGetStreamInfo",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
||||
GetStreamInfoCore(ctx, StreamInfoSize);
|
||||
|
||||
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
|
||||
public static int AvPlayerGetStreamInfo(CpuContext ctx)
|
||||
{
|
||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var infoAddress = ctx[CpuRegister.Rdx];
|
||||
@@ -617,7 +578,7 @@ public static class AvPlayerExports
|
||||
return SetReturn(ctx, InvalidParameters);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[infoSize];
|
||||
Span<byte> info = stackalloc byte[StreamInfoSize];
|
||||
info.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
||||
if (streamIndex == 0)
|
||||
@@ -1048,7 +1009,7 @@ public static class AvPlayerExports
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
|
||||
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
|
||||
if (!File.Exists(ffprobe))
|
||||
{
|
||||
return false;
|
||||
@@ -1131,50 +1092,13 @@ public static class AvPlayerExports
|
||||
}
|
||||
}
|
||||
|
||||
internal static string? FindFfmpeg() =>
|
||||
FindFfmpeg(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
||||
Environment.GetEnvironmentVariable("PATH"),
|
||||
OperatingSystem.IsWindows(),
|
||||
AppContext.BaseDirectory);
|
||||
|
||||
internal static string? FindFfmpeg(
|
||||
string? configured,
|
||||
string? searchPath,
|
||||
bool isWindows,
|
||||
string? baseDirectory = null)
|
||||
private static string? FindFfmpeg()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
|
||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||
{
|
||||
return configured;
|
||||
}
|
||||
|
||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
||||
if (!string.IsNullOrWhiteSpace(baseDirectory))
|
||||
{
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, executable),
|
||||
Path.Combine(baseDirectory, "ffmpeg", executable),
|
||||
})
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var directory in (searchPath ?? string.Empty)
|
||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var candidate = Path.Combine(RemovePathQuotes(directory), executable);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
@@ -1185,16 +1109,6 @@ public static class AvPlayerExports
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
|
||||
Path.Combine(
|
||||
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
|
||||
isWindows ? "ffprobe.exe" : "ffprobe");
|
||||
|
||||
private static string RemovePathQuotes(string directory) =>
|
||||
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
|
||||
? directory[1..^1]
|
||||
: directory;
|
||||
|
||||
internal static string? ResolveGuestPath(string guestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(guestPath))
|
||||
@@ -1204,9 +1118,7 @@ public static class AvPlayerExports
|
||||
|
||||
var normalized = guestPath.Replace('\\', '/');
|
||||
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
||||
var unrealProjectRelative =
|
||||
normalized.StartsWith("../", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("./", StringComparison.Ordinal);
|
||||
var unrealProjectRelative = false;
|
||||
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
||||
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
||||
uri.IsFile)
|
||||
@@ -1237,10 +1149,7 @@ public static class AvPlayerExports
|
||||
|
||||
if (unrealProjectRelative)
|
||||
{
|
||||
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
normalized = RemoveUnrealLeadingDotSegments(normalized);
|
||||
}
|
||||
|
||||
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
@@ -1324,20 +1233,15 @@ public static class AvPlayerExports
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryRemoveUnrealLeadingDotSegments(
|
||||
string guestPath,
|
||||
out string normalized)
|
||||
private static string RemoveUnrealLeadingDotSegments(string guestPath)
|
||||
{
|
||||
var removedParent = false;
|
||||
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
||||
guestPath.StartsWith("./", StringComparison.Ordinal))
|
||||
{
|
||||
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
|
||||
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
||||
}
|
||||
|
||||
normalized = guestPath;
|
||||
return !removedParent || guestPath.Contains('/');
|
||||
return guestPath;
|
||||
}
|
||||
|
||||
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
||||
|
||||
@@ -18,43 +18,15 @@ namespace SharpEmu.Libs.Bink;
|
||||
internal static class Bink2MovieBridge
|
||||
{
|
||||
private const uint MaxDimension = 16384;
|
||||
private const uint MaxHostVideoWidth = 1920;
|
||||
private const uint MaxHostVideoHeight = 1080;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static NativeAdapter? _adapter;
|
||||
private static string? _activePath;
|
||||
private static IntPtr _activeMovie;
|
||||
private static Bink2MovieInfo _activeInfo;
|
||||
private static byte[]? _frameBuffer;
|
||||
private static bool _frameBufferPresented;
|
||||
private static BinkFramePlayback? _playback;
|
||||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
|
||||
internal static bool IsHostPlaybackActive
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _playback is not null || _frameBuffer is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetPresentationSize(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_presentationWidth = Math.Min(width, MaxHostVideoWidth);
|
||||
_presentationHeight = Math.Min(height, MaxHostVideoHeight);
|
||||
}
|
||||
}
|
||||
private static bool _usingDummyMovie;
|
||||
private static bool _loadAttempted;
|
||||
private static bool _availabilityReported;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true only when movie skipping was explicitly requested. Without
|
||||
@@ -65,106 +37,109 @@ internal static class Bink2MovieBridge
|
||||
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||
ResolveMode() == MovieMode.Skip;
|
||||
|
||||
/// <summary>
|
||||
/// Starts or queues host decoding. Decoded frames are only exposed as a
|
||||
/// sampled guest texture; presentation and UI composition remain guest-owned.
|
||||
/// </summary>
|
||||
internal static bool ObserveGuestMovie(string hostPath)
|
||||
internal static void ObserveGuestMovie(string hostPath)
|
||||
{
|
||||
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||
!File.Exists(hostPath))
|
||||
{
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _playback is not null || _frameBuffer is not null;
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = ResolveMode();
|
||||
if (mode is MovieMode.Guest or MovieMode.Skip)
|
||||
if (mode == MovieMode.Dummy)
|
||||
{
|
||||
return false;
|
||||
AttachDummyMovieLocked(hostPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playback is not null || _frameBuffer is not null)
|
||||
if (mode != MovieMode.Native)
|
||||
{
|
||||
if (PendingMoviePathSet.Add(hostPath))
|
||||
{
|
||||
PendingMoviePaths.Enqueue(hostPath);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge queued: " +
|
||||
Path.GetFileName(hostPath));
|
||||
}
|
||||
return PendingMoviePathSet.Contains(hostPath);
|
||||
return;
|
||||
}
|
||||
|
||||
AttachMovieLocked(hostPath, mode);
|
||||
return string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) &&
|
||||
(_playback is not null || _frameBuffer is not null);
|
||||
var adapter = GetAdapterLocked();
|
||||
if (adapter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseActiveLocked();
|
||||
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValid(info))
|
||||
{
|
||||
adapter.Close(movie);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activePath = hostPath;
|
||||
_activeMovie = movie;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryDecodeNextFrame(
|
||||
bool advanceClock,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out bool advanced,
|
||||
out long frameSerial,
|
||||
out string hostPath)
|
||||
out uint height)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
height = 0;
|
||||
advanced = false;
|
||||
frameSerial = _frameSerial;
|
||||
hostPath = _activePath ?? string.Empty;
|
||||
|
||||
if (_playback is not null)
|
||||
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||
{
|
||||
if (!_playback.TryGetFrame(advanceClock, out pixels, out advanced))
|
||||
if (_usingDummyMovie && _frameBuffer is not null)
|
||||
{
|
||||
if (_playback.IsFinished)
|
||||
{
|
||||
var completedPath = _activePath;
|
||||
CloseActiveLocked();
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge completed: " +
|
||||
Path.GetFileName(completedPath));
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
return false;
|
||||
pixels = _frameBuffer;
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
return true;
|
||||
}
|
||||
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
if (advanced)
|
||||
{
|
||||
frameSerial = ++_frameSerial;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_frameBuffer is null)
|
||||
unsafe
|
||||
{
|
||||
return false;
|
||||
fixed (byte* destination = _frameBuffer)
|
||||
{
|
||||
if (!_adapter.DecodeNextBgra(
|
||||
_activeMovie,
|
||||
(IntPtr)destination,
|
||||
_activeInfo.Width * 4,
|
||||
(uint)_frameBuffer.Length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels = _frameBuffer;
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
advanced = !_frameBufferPresented;
|
||||
_frameBufferPresented = true;
|
||||
if (advanced)
|
||||
{
|
||||
frameSerial = ++_frameSerial;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -177,52 +152,6 @@ internal static class Bink2MovieBridge
|
||||
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||
checked((int)((ulong)info.Width * info.Height * 4));
|
||||
|
||||
private static void AttachMovieLocked(string hostPath, MovieMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case MovieMode.Dummy:
|
||||
AttachDummyMovieLocked(hostPath);
|
||||
return;
|
||||
case MovieMode.Ffmpeg:
|
||||
AttachFfmpegMovieLocked(hostPath);
|
||||
return;
|
||||
case MovieMode.Native:
|
||||
AttachNativeMovieLocked(hostPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AttachNativeMovieLocked(string hostPath)
|
||||
{
|
||||
if (!FfmpegNativeBinkFrameSource.TryOpen(
|
||||
hostPath, _presentationWidth, _presentationHeight, out var source) ||
|
||||
source is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var info = new Bink2MovieInfo(
|
||||
source.Width, source.Height, source.FramesPerSecondNumerator, source.FramesPerSecondDenominator);
|
||||
if (!IsValid(info))
|
||||
{
|
||||
source.Dispose();
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
|
||||
private static MovieMode ResolveMode()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||
@@ -241,22 +170,15 @@ internal static class Bink2MovieBridge
|
||||
return MovieMode.Skip;
|
||||
}
|
||||
|
||||
if (string.Equals(configured, "guest", StringComparison.OrdinalIgnoreCase))
|
||||
// Prefer the optional host adapter when one is supplied. Otherwise let
|
||||
// the game's statically linked Bink implementation consume the file.
|
||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||
EnumerateAdapterCandidates().Any(File.Exists))
|
||||
{
|
||||
return MovieMode.Guest;
|
||||
return MovieMode.Native;
|
||||
}
|
||||
|
||||
if (string.Equals(configured, "ffmpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Ffmpeg;
|
||||
}
|
||||
|
||||
// Native is the default: FfmpegNativeBinkFrameSource.TryOpen degrades
|
||||
// gracefully (falls back to the guest's own decode, logging one
|
||||
// informational line) if the FFmpeg libraries SharpEmu.CLI.csproj
|
||||
// downloads next to the executable are genuinely unavailable, so
|
||||
// defaulting to Native unconditionally is safe.
|
||||
return MovieMode.Native;
|
||||
return MovieMode.Guest;
|
||||
}
|
||||
|
||||
private static void AttachDummyMovieLocked(string hostPath)
|
||||
@@ -273,61 +195,22 @@ internal static class Bink2MovieBridge
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
_frameBufferPresented = false;
|
||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||
_usingDummyMovie = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + ".");
|
||||
}
|
||||
|
||||
private static void AttachFfmpegMovieLocked(string hostPath)
|
||||
{
|
||||
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink FFmpeg source has an invalid header: " +
|
||||
Path.GetFileName(hostPath));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FfmpegBinkFrameSource.TryOpen(
|
||||
hostPath,
|
||||
info.Width,
|
||||
info.Height,
|
||||
info.FramesPerSecondNumerator,
|
||||
info.FramesPerSecondDenominator,
|
||||
out var source) || source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink FFmpeg source attached: " +
|
||||
Path.GetFileName(hostPath) + " " + info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
|
||||
private static void AttachPlaybackLocked(
|
||||
string hostPath,
|
||||
Bink2MovieInfo info,
|
||||
IBinkFrameDecoder decoder)
|
||||
{
|
||||
CloseActiveLocked();
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_playback = new BinkFramePlayback(decoder);
|
||||
}
|
||||
|
||||
internal static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
{
|
||||
info = default;
|
||||
Span<byte> header = stackalloc byte[36];
|
||||
Span<byte> header = stackalloc byte[32];
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
stream.ReadExactly(header);
|
||||
if (!header[..3].SequenceEqual("KB2"u8))
|
||||
if (stream.Read(header) != header.Length ||
|
||||
!header[..4].SequenceEqual("KB2j"u8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -336,11 +219,10 @@ internal static class Bink2MovieBridge
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
|
||||
return info.FramesPerSecondNumerator != 0 &&
|
||||
info.FramesPerSecondDenominator != 0;
|
||||
1);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or EndOfStreamException)
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -362,23 +244,80 @@ internal static class Bink2MovieBridge
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeAdapter? GetAdapterLocked()
|
||||
{
|
||||
if (_loadAttempted)
|
||||
{
|
||||
return _adapter;
|
||||
}
|
||||
|
||||
_loadAttempted = true;
|
||||
foreach (var candidate in EnumerateAdapterCandidates())
|
||||
{
|
||||
if (!NativeLibrary.TryLoad(candidate, out var library))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (NativeAdapter.TryCreate(library, out var adapter))
|
||||
{
|
||||
_adapter = adapter;
|
||||
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(library);
|
||||
}
|
||||
|
||||
if (!_availabilityReported)
|
||||
{
|
||||
_availabilityReported = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateAdapterCandidates()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
yield return configured;
|
||||
}
|
||||
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseActiveLocked()
|
||||
{
|
||||
_playback?.Dispose();
|
||||
_playback = null;
|
||||
if (_activeMovie != IntPtr.Zero)
|
||||
{
|
||||
_adapter?.Close(_activeMovie);
|
||||
}
|
||||
|
||||
_activePath = null;
|
||||
_activeMovie = IntPtr.Zero;
|
||||
_activeInfo = default;
|
||||
_frameBuffer = null;
|
||||
_frameBufferPresented = false;
|
||||
|
||||
// Wake any guest _read() blocked in WaitForHostPlaybackToFinish: its
|
||||
// movie either just finished or is being pre-empted by a new attach.
|
||||
Monitor.PulseAll(Gate);
|
||||
_usingDummyMovie = false;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal readonly struct Bink2MovieInfo
|
||||
private readonly struct Bink2MovieInfo
|
||||
{
|
||||
public readonly uint Width;
|
||||
public readonly uint Height;
|
||||
@@ -404,222 +343,66 @@ internal static class Bink2MovieBridge
|
||||
Skip,
|
||||
Dummy,
|
||||
Native,
|
||||
Ffmpeg,
|
||||
}
|
||||
|
||||
private static readonly Queue<string> PendingMoviePaths = new();
|
||||
private static readonly HashSet<string> PendingMoviePathSet =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static void AttachNextQueuedMovieLocked()
|
||||
private sealed class NativeAdapter
|
||||
{
|
||||
while (PendingMoviePaths.Count > 0)
|
||||
{
|
||||
var path = PendingMoviePaths.Dequeue();
|
||||
PendingMoviePathSet.Remove(path);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||
|
||||
AttachMovieLocked(path, ResolveMode());
|
||||
if (_playback is not null || _frameBuffer is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void CloseDelegate(IntPtr movie);
|
||||
|
||||
private readonly OpenUtf8Delegate _openUtf8;
|
||||
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
||||
private readonly CloseDelegate _close;
|
||||
|
||||
private NativeAdapter(
|
||||
OpenUtf8Delegate openUtf8,
|
||||
DecodeNextBgraDelegate decodeNextBgra,
|
||||
CloseDelegate close)
|
||||
{
|
||||
_openUtf8 = openUtf8;
|
||||
_decodeNextBgra = decodeNextBgra;
|
||||
_close = close;
|
||||
}
|
||||
}
|
||||
// Longest a guest _read() will block waiting for real host playback to
|
||||
// finish. A safety net, not a target: real movies finish well under
|
||||
// this. Bounds the damage if a movie fails to attach/decode after being
|
||||
// queued, so the guest thread doesn't hang forever.
|
||||
private const long MaxCompletionWaitMilliseconds = 5 * 60 * 1000;
|
||||
/// <summary>
|
||||
/// Blocks the calling (guest I/O) thread until the host has actually
|
||||
/// finished presenting <paramref name="hostPath"/> — either because it
|
||||
/// played through, or because something else took over the timeline.
|
||||
///
|
||||
/// The completion shim tells the guest's own Bink header parse "this
|
||||
/// movie is one frame and already done" so its native decoder never
|
||||
/// blocks the guest on real per-frame work. Without this wait, that lie
|
||||
/// lands the instant the guest reads the header, so guest-side game
|
||||
/// logic races far ahead of whatever the host is still showing on
|
||||
/// screen: pressing a button lands on the (already-advanced) guest
|
||||
/// state, but the video visibly keeps playing, and any real-time-gated
|
||||
/// trigger later in the guest's own flow can fire against a clock that
|
||||
/// no longer matches wall time. Gating the "done" read on real host
|
||||
/// completion keeps guest pacing and on-screen playback in lockstep.
|
||||
/// </summary>
|
||||
internal static void WaitForHostPlaybackToFinish(string hostPath)
|
||||
{
|
||||
var deadline = Environment.TickCount64 + MaxCompletionWaitMilliseconds;
|
||||
lock (Gate)
|
||||
|
||||
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||
{
|
||||
while (IsTrackedLocked(hostPath))
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge completion wait timed out for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Monitor.Wait(Gate, (int)Math.Min(remaining, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTrackedLocked(string hostPath) =>
|
||||
string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
PendingMoviePathSet.Contains(hostPath);
|
||||
|
||||
internal static bool TryTakeOverGuestMovie(
|
||||
string hostPath,
|
||||
out BinkGuestCompletionShim completionShim,
|
||||
out bool observed)
|
||||
{
|
||||
completionShim = default;
|
||||
observed = ObserveGuestMovie(hostPath);
|
||||
|
||||
// Keep the real header visible so the guest creates its movie surface
|
||||
// and draw. Host-decoded pixels replace that sampled image later; a
|
||||
// one-frame completion shim would finish before the descriptor exists.
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static void NotifyGuestMovieClosed(string hostPath)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (PendingMoviePathSet.Remove(hostPath))
|
||||
{
|
||||
var retained = PendingMoviePaths
|
||||
.Where(path => !string.Equals(
|
||||
path,
|
||||
hostPath,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
PendingMoviePaths.Clear();
|
||||
foreach (var path in retained)
|
||||
{
|
||||
PendingMoviePaths.Enqueue(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Monitor.PulseAll(Gate);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge stopped by guest close: " +
|
||||
Path.GetFileName(hostPath));
|
||||
CloseActiveLocked();
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadGuestCompletionShim(
|
||||
string hostPath,
|
||||
out BinkGuestCompletionShim completionShim)
|
||||
{
|
||||
completionShim = default;
|
||||
Span<byte> header = stackalloc byte[48];
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(hostPath);
|
||||
stream.ReadExactly(header);
|
||||
if (!header[..3].SequenceEqual("KB2"u8))
|
||||
adapter = null;
|
||||
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var frameCount = BinaryPrimitives.ReadUInt32LittleEndian(header[8..12]);
|
||||
var audioTrackCount = BinaryPrimitives.ReadUInt32LittleEndian(header[40..44]);
|
||||
if (frameCount < 2 || audioTrackCount > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var revision = header[3];
|
||||
var frameIndexOffset = 44L + checked(12L * audioTrackCount);
|
||||
if (revision == (byte)'m')
|
||||
{
|
||||
frameIndexOffset += 16;
|
||||
}
|
||||
else if (revision is (byte)'i' or (byte)'j' or (byte)'k' or (byte)'n')
|
||||
{
|
||||
frameIndexOffset += 4;
|
||||
}
|
||||
|
||||
Span<byte> frameOffsets = stackalloc byte[8];
|
||||
stream.Position = frameIndexOffset;
|
||||
stream.ReadExactly(frameOffsets);
|
||||
var firstFrameOffset = BinaryPrimitives.ReadUInt32LittleEndian(frameOffsets[..4]) & ~1u;
|
||||
var secondFrameOffset = BinaryPrimitives.ReadUInt32LittleEndian(frameOffsets[4..]) & ~1u;
|
||||
if (firstFrameOffset < frameIndexOffset + 8 ||
|
||||
secondFrameOffset <= firstFrameOffset ||
|
||||
secondFrameOffset > stream.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
completionShim = new BinkGuestCompletionShim(
|
||||
secondFrameOffset - 8,
|
||||
secondFrameOffset - firstFrameOffset);
|
||||
adapter = new NativeAdapter(
|
||||
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException or EndOfStreamException or OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct BinkGuestCompletionShim
|
||||
{
|
||||
private readonly uint _fileSizeMinusHeader;
|
||||
private readonly uint _largestFrameSize;
|
||||
|
||||
internal BinkGuestCompletionShim(uint fileSizeMinusHeader, uint largestFrameSize)
|
||||
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||
{
|
||||
_fileSizeMinusHeader = fileSizeMinusHeader;
|
||||
_largestFrameSize = largestFrameSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites the frame-count/size fields the guest's own Bink header
|
||||
/// parse reads, if this read covers them. Returns true when the
|
||||
/// NumFrames field (the field that tells the guest "this movie is
|
||||
/// done") was in range, so the caller can gate that specific read on
|
||||
/// the host's real playback actually finishing first.
|
||||
/// </summary>
|
||||
internal bool Patch(long fileOffset, Span<byte> bytes)
|
||||
{
|
||||
PatchUInt32(fileOffset, bytes, 4, _fileSizeMinusHeader);
|
||||
var touchedCompletionField = PatchUInt32(fileOffset, bytes, 8, 1);
|
||||
PatchUInt32(fileOffset, bytes, 12, _largestFrameSize);
|
||||
return touchedCompletionField;
|
||||
}
|
||||
|
||||
private static bool PatchUInt32(
|
||||
long fileOffset,
|
||||
Span<byte> bytes,
|
||||
long fieldOffset,
|
||||
uint value)
|
||||
{
|
||||
var relativeOffset = fieldOffset - fileOffset;
|
||||
if (relativeOffset < 0 || relativeOffset + sizeof(uint) > bytes.Length)
|
||||
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||
try
|
||||
{
|
||||
return false;
|
||||
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(utf8);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
bytes.Slice((int)relativeOffset, sizeof(uint)),
|
||||
value);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
||||
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
||||
|
||||
internal void Close(IntPtr movie) => _close(movie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
internal interface IBinkFrameDecoder : IDisposable
|
||||
{
|
||||
uint Width { get; }
|
||||
|
||||
uint Height { get; }
|
||||
|
||||
uint FramesPerSecondNumerator { get; }
|
||||
|
||||
uint FramesPerSecondDenominator { get; }
|
||||
|
||||
bool TryDecodeNextFrame(Span<byte> destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps blocking codec work away from the Vulkan presentation thread and
|
||||
/// releases decoded frames according to the movie time base.
|
||||
/// </summary>
|
||||
internal sealed class BinkFramePlayback : IDisposable
|
||||
{
|
||||
private const int BufferCount = 5;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly IBinkFrameDecoder _decoder;
|
||||
private readonly Queue<byte[]> _freeBuffers = new();
|
||||
private readonly Queue<DecodedFrame> _decodedFrames = new();
|
||||
private readonly Thread _decoderThread;
|
||||
private byte[]? _currentFrame;
|
||||
private byte[]? _retiredFrame;
|
||||
private long _currentFrameIndex = -1;
|
||||
private long _nextDecodedFrameIndex;
|
||||
private long _playbackStartTimestamp;
|
||||
private bool _playbackClockStarted;
|
||||
private bool _decoderCompleted;
|
||||
private bool _stopRequested;
|
||||
private bool _finished;
|
||||
private int _disposed;
|
||||
|
||||
internal BinkFramePlayback(IBinkFrameDecoder decoder)
|
||||
{
|
||||
_decoder = decoder;
|
||||
Width = decoder.Width;
|
||||
Height = decoder.Height;
|
||||
FramesPerSecondNumerator = decoder.FramesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = decoder.FramesPerSecondDenominator;
|
||||
|
||||
var frameBytes = checked((int)((ulong)Width * Height * 4));
|
||||
for (var index = 0; index < BufferCount; index++)
|
||||
{
|
||||
_freeBuffers.Enqueue(GC.AllocateUninitializedArray<byte>(frameBytes));
|
||||
}
|
||||
|
||||
_decoderThread = new Thread(DecodeLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu Bink video decoder",
|
||||
};
|
||||
_decoderThread.Start();
|
||||
}
|
||||
|
||||
internal uint Width { get; }
|
||||
|
||||
internal uint Height { get; }
|
||||
|
||||
internal uint FramesPerSecondNumerator { get; }
|
||||
|
||||
internal uint FramesPerSecondDenominator { get; }
|
||||
|
||||
internal bool IsFinished
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryGetFrame(
|
||||
bool advanceClock,
|
||||
out byte[] pixels,
|
||||
out bool advanced)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
pixels = [];
|
||||
advanced = false;
|
||||
if (_finished)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_currentFrame is null)
|
||||
{
|
||||
if (_decodedFrames.Count == 0)
|
||||
{
|
||||
if (_decoderCompleted)
|
||||
{
|
||||
_finished = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var first = _decodedFrames.Dequeue();
|
||||
_currentFrame = first.Pixels;
|
||||
_currentFrameIndex = first.Index;
|
||||
advanced = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
if (advanceClock && !_playbackClockStarted)
|
||||
{
|
||||
_playbackStartTimestamp = Stopwatch.GetTimestamp();
|
||||
_playbackClockStarted = true;
|
||||
}
|
||||
|
||||
var elapsedSeconds = _playbackClockStarted
|
||||
? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds
|
||||
: 0;
|
||||
var targetFrameIndex = (long)Math.Floor(
|
||||
elapsedSeconds * FramesPerSecondNumerator / FramesPerSecondDenominator);
|
||||
DecodedFrame? replacement = null;
|
||||
while (_decodedFrames.Count > 0 &&
|
||||
_decodedFrames.Peek().Index <= targetFrameIndex)
|
||||
{
|
||||
if (replacement is { } skipped)
|
||||
{
|
||||
_freeBuffers.Enqueue(skipped.Pixels);
|
||||
}
|
||||
replacement = _decodedFrames.Dequeue();
|
||||
}
|
||||
|
||||
if (replacement is { } next)
|
||||
{
|
||||
if (_retiredFrame is not null)
|
||||
{
|
||||
_freeBuffers.Enqueue(_retiredFrame);
|
||||
}
|
||||
_retiredFrame = _currentFrame;
|
||||
_currentFrame = next.Pixels;
|
||||
_currentFrameIndex = next.Index;
|
||||
advanced = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
var frameDurationSeconds =
|
||||
(double)FramesPerSecondDenominator / FramesPerSecondNumerator;
|
||||
if (_playbackClockStarted &&
|
||||
_decoderCompleted &&
|
||||
_decodedFrames.Count == 0 &&
|
||||
elapsedSeconds >= (_currentFrameIndex + 1) * frameDurationSeconds)
|
||||
{
|
||||
_finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
pixels = _currentFrame;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DecodeLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
byte[] destination;
|
||||
lock (_gate)
|
||||
{
|
||||
while (!_stopRequested && _freeBuffers.Count == 0)
|
||||
{
|
||||
Monitor.Wait(_gate);
|
||||
}
|
||||
if (_stopRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
destination = _freeBuffers.Dequeue();
|
||||
}
|
||||
|
||||
if (!_decoder.TryDecodeNextFrame(destination))
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_freeBuffers.Enqueue(destination);
|
||||
_decoderCompleted = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_decodedFrames.Enqueue(new DecodedFrame(
|
||||
_nextDecodedFrameIndex++, destination));
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or
|
||||
InvalidOperationException)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink decoder stopped: {exception.Message}");
|
||||
lock (_gate)
|
||||
{
|
||||
_decoderCompleted = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_stopRequested = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
if (Thread.CurrentThread != _decoderThread &&
|
||||
!_decoderThread.Join(TimeSpan.FromMilliseconds(100)))
|
||||
{
|
||||
_decoder.Dispose();
|
||||
_decoderThread.Join(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
else
|
||||
{
|
||||
_decoder.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct DecodedFrame(long Index, byte[] Pixels);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using SharpEmu.Libs.AvPlayer;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
internal sealed class FfmpegBinkFrameSource : IBinkFrameDecoder
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly Stream _output;
|
||||
private int _errorLines;
|
||||
private int _disposed;
|
||||
|
||||
private FfmpegBinkFrameSource(
|
||||
Process process,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
_process = process;
|
||||
_output = process.StandardOutput.BaseStream;
|
||||
Width = width;
|
||||
Height = height;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
}
|
||||
|
||||
public uint Width { get; }
|
||||
|
||||
public uint Height { get; }
|
||||
|
||||
public uint FramesPerSecondNumerator { get; }
|
||||
|
||||
public uint FramesPerSecondDenominator { get; }
|
||||
|
||||
internal static bool IsAvailable => AvPlayerExports.FindFfmpeg() is not null;
|
||||
|
||||
internal static bool TryOpen(
|
||||
string path,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator,
|
||||
out FfmpegBinkFrameSource? source)
|
||||
{
|
||||
source = null;
|
||||
var ffmpeg = AvPlayerExports.FindFfmpeg();
|
||||
if (ffmpeg is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo(ffmpeg)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
startInfo.ArgumentList.Add("-nostdin");
|
||||
startInfo.ArgumentList.Add("-hide_banner");
|
||||
startInfo.ArgumentList.Add("-loglevel");
|
||||
startInfo.ArgumentList.Add("error");
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(path);
|
||||
startInfo.ArgumentList.Add("-map");
|
||||
startInfo.ArgumentList.Add("0:v:0");
|
||||
startInfo.ArgumentList.Add("-an");
|
||||
startInfo.ArgumentList.Add("-pix_fmt");
|
||||
startInfo.ArgumentList.Add("bgra");
|
||||
startInfo.ArgumentList.Add("-f");
|
||||
startInfo.ArgumentList.Add("rawvideo");
|
||||
startInfo.ArgumentList.Add("pipe:1");
|
||||
|
||||
try
|
||||
{
|
||||
var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source = new FfmpegBinkFrameSource(
|
||||
process,
|
||||
width,
|
||||
height,
|
||||
framesPerSecondNumerator,
|
||||
framesPerSecondDenominator);
|
||||
process.ErrorDataReceived += source.OnErrorData;
|
||||
process.BeginErrorReadLine();
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or
|
||||
InvalidOperationException or
|
||||
System.ComponentModel.Win32Exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink FFmpeg decoder could not start: {exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < destination.Length)
|
||||
{
|
||||
var read = _output.Read(destination[offset..]);
|
||||
if (read == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or ObjectDisposedException)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink FFmpeg stream failed: {exception.Message}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnErrorData(object sender, DataReceivedEventArgs eventArgs)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(eventArgs.Data) ||
|
||||
Interlocked.Increment(ref _errorLines) > 20)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][FFMPEG-BINK] {eventArgs.Data}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_output.Dispose();
|
||||
try
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Kill(entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using FFmpeg.AutoGen;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a .bk2 (or any FFmpeg-readable movie) directly via FFmpeg's C API
|
||||
/// through FFmpeg.AutoGen P/Invoke bindings against the dynamically linked
|
||||
/// libraries published by github.com/sharpemu/ffmpeg-core -- no native C
|
||||
/// bridge of our own to build. See docs/bink2-bridge.md.
|
||||
/// </summary>
|
||||
internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
|
||||
{
|
||||
private AVFormatContext* _formatContext;
|
||||
private AVCodecContext* _codecContext;
|
||||
private SwsContext* _swsContext;
|
||||
private AVFrame* _frame;
|
||||
private AVPacket* _packet;
|
||||
private readonly int _videoStreamIndex;
|
||||
private bool _draining;
|
||||
private int _disposed;
|
||||
|
||||
public uint Width { get; }
|
||||
|
||||
public uint Height { get; }
|
||||
|
||||
public uint FramesPerSecondNumerator { get; }
|
||||
|
||||
public uint FramesPerSecondDenominator { get; }
|
||||
|
||||
private FfmpegNativeBinkFrameSource(
|
||||
AVFormatContext* formatContext,
|
||||
AVCodecContext* codecContext,
|
||||
int videoStreamIndex,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
_formatContext = formatContext;
|
||||
_codecContext = codecContext;
|
||||
_videoStreamIndex = videoStreamIndex;
|
||||
Width = width;
|
||||
Height = height;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
_frame = ffmpeg.av_frame_alloc();
|
||||
_packet = ffmpeg.av_packet_alloc();
|
||||
}
|
||||
|
||||
private static bool _rootPathInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Points FFmpeg.AutoGen at the FFmpeg shared libraries SharpEmu.CLI
|
||||
/// downloads next to the executable (see SharpEmu.CLI.csproj's
|
||||
/// FetchFfmpegRuntime target); kept as loose files rather than embedded
|
||||
/// in the single-file bundle so the OS loader can resolve the normal
|
||||
/// inter-library dependencies (avcodec depends on avutil, etc.) itself.
|
||||
/// </summary>
|
||||
private static void EnsureRootPathInitialized()
|
||||
{
|
||||
if (_rootPathInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rootPathInitialized = true;
|
||||
// SharpEmu.CLI.csproj publishes FFmpeg's shared libraries into a
|
||||
// "plugins" subfolder next to the executable rather than flat beside
|
||||
// it (see NativeLibraryFolderName in SharpEmu.CLI.csproj).
|
||||
ffmpeg.RootPath = Path.Combine(AppContext.BaseDirectory, "plugins");
|
||||
|
||||
// ffmpeg's static constructor runs DynamicallyLoadedBindings.Initialize()
|
||||
// itself, but that constructor fires on first touch of the ffmpeg type --
|
||||
// which is the RootPath assignment above -- so it binds against the
|
||||
// default (empty) RootPath before the assignment's own setter body runs.
|
||||
// Every function resolved during that first pass permanently throws
|
||||
// NotSupportedException. Re-running Initialize() now, with RootPath
|
||||
// actually set, rebinds everything against the real search path.
|
||||
DynamicallyLoadedBindings.Initialize();
|
||||
}
|
||||
|
||||
internal static bool TryOpen(
|
||||
string path,
|
||||
uint maximumWidth,
|
||||
uint maximumHeight,
|
||||
out FfmpegNativeBinkFrameSource? source)
|
||||
{
|
||||
source = null;
|
||||
EnsureRootPathInitialized();
|
||||
|
||||
AVFormatContext* formatContext = null;
|
||||
AVCodecContext* codecContext = null;
|
||||
try
|
||||
{
|
||||
if (ffmpeg.avformat_open_input(&formatContext, path, null, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ffmpeg.avformat_find_stream_info(formatContext, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AVCodec* decoder = null;
|
||||
var videoStreamIndex = ffmpeg.av_find_best_stream(
|
||||
formatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
|
||||
if (videoStreamIndex < 0 || decoder is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stream = formatContext->streams[videoStreamIndex];
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(decoder);
|
||||
if (codecContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ffmpeg.avcodec_parameters_to_context(codecContext, stream->codecpar) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
codecContext->thread_count = 0;
|
||||
codecContext->thread_type = ffmpeg.FF_THREAD_FRAME | ffmpeg.FF_THREAD_SLICE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, decoder, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (codecContext->width <= 0 || codecContext->height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var frameRate = ffmpeg.av_guess_frame_rate(formatContext, stream, null);
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = stream->avg_frame_rate;
|
||||
}
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = stream->r_frame_rate;
|
||||
}
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = new AVRational { num = 30, den = 1 };
|
||||
}
|
||||
|
||||
var outputWidth = (uint)codecContext->width;
|
||||
var outputHeight = (uint)codecContext->height;
|
||||
if (maximumWidth > 0 && maximumHeight > 0 &&
|
||||
(outputWidth > maximumWidth || outputHeight > maximumHeight))
|
||||
{
|
||||
if ((ulong)outputWidth * maximumHeight > (ulong)outputHeight * maximumWidth)
|
||||
{
|
||||
outputHeight = (uint)((ulong)outputHeight * maximumWidth / outputWidth);
|
||||
outputWidth = maximumWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
outputWidth = (uint)((ulong)outputWidth * maximumHeight / outputHeight);
|
||||
outputHeight = maximumHeight;
|
||||
}
|
||||
|
||||
outputWidth = Math.Max(1, outputWidth);
|
||||
outputHeight = Math.Max(1, outputHeight);
|
||||
}
|
||||
|
||||
source = new FfmpegNativeBinkFrameSource(
|
||||
formatContext,
|
||||
codecContext,
|
||||
videoStreamIndex,
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
(uint)frameRate.num,
|
||||
(uint)frameRate.den);
|
||||
formatContext = null;
|
||||
codecContext = null;
|
||||
return true;
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (codecContext is not null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
}
|
||||
|
||||
if (formatContext is not null)
|
||||
{
|
||||
ffmpeg.avformat_close_input(&formatContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
var stride = checked((int)(Width * 4));
|
||||
var required = (long)stride * Height;
|
||||
if (destination.Length < required)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReceiveFrame())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
(int)Width,
|
||||
(int)Height,
|
||||
AVPixelFormat.AV_PIX_FMT_BGRA,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext is null)
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
fixed (byte* destinationPointer = destination)
|
||||
{
|
||||
var destinationPlanes = new byte*[4] { destinationPointer, null, null, null };
|
||||
var destinationStrides = new int[4] { stride, 0, 0, 0 };
|
||||
var convertedRows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
return convertedRows == (int)Height;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReceiveFrame()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (receiveResult != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_draining)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFeedPacket())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFeedPacket()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var readResult = ffmpeg.av_read_frame(_formatContext, _packet);
|
||||
if (readResult < 0)
|
||||
{
|
||||
_draining = true;
|
||||
ffmpeg.avcodec_send_packet(_codecContext, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_packet->stream_index != _videoStreamIndex)
|
||||
{
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
if (sendResult < 0 && sendResult != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_swsContext is not null)
|
||||
{
|
||||
ffmpeg.sws_freeContext(_swsContext);
|
||||
_swsContext = null;
|
||||
}
|
||||
|
||||
if (_packet is not null)
|
||||
{
|
||||
var packet = _packet;
|
||||
ffmpeg.av_packet_free(&packet);
|
||||
_packet = null;
|
||||
}
|
||||
|
||||
if (_frame is not null)
|
||||
{
|
||||
var frame = _frame;
|
||||
ffmpeg.av_frame_free(&frame);
|
||||
_frame = null;
|
||||
}
|
||||
|
||||
if (_codecContext is not null)
|
||||
{
|
||||
var codecContext = _codecContext;
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
_codecContext = null;
|
||||
}
|
||||
|
||||
if (_formatContext is not null)
|
||||
{
|
||||
var formatContext = _formatContext;
|
||||
ffmpeg.avformat_close_input(&formatContext);
|
||||
_formatContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,37 +153,6 @@ public static class FontExports
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3BrWWFU+4ts",
|
||||
ExportName = "sceFontGetVerticalLayout",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int GetVerticalLayout(CpuContext ctx)
|
||||
{
|
||||
var layoutAddress = ctx[CpuRegister.Rsi];
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Baseline (horizontal offset), line advance, decoration extent.
|
||||
// Mirrors the same three-float layout as GetHorizontalLayout, but
|
||||
// interpreted for vertical writing (e.g. CJK text rendered top-to-bottom).
|
||||
var values = new[] { 8.0f, 16.0f, 0.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
ctx,
|
||||
layoutAddress + (ulong)(index * sizeof(float)),
|
||||
BitConverter.SingleToUInt32Bits(values[index])))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cKYtVmeSTcw",
|
||||
ExportName = "sceFontOpenFontSet",
|
||||
|
||||
@@ -27,12 +27,7 @@ internal sealed record GuestDrawTexture(
|
||||
uint Pitch = 0,
|
||||
uint TileMode = 0,
|
||||
uint DstSelect = 0xFAC,
|
||||
GuestSampler Sampler = default,
|
||||
// Guest CPU write-tracker generation of the memory RgbaPixels was read
|
||||
// from; -1 when the range is untracked or the pixels were not read here.
|
||||
long WriteGeneration = -1,
|
||||
bool ArrayedView = false,
|
||||
uint ArrayLayers = 1);
|
||||
GuestSampler Sampler = default);
|
||||
|
||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||
internal readonly record struct GuestSampler(
|
||||
@@ -53,9 +48,7 @@ internal readonly record struct TextureContentIdentity(
|
||||
uint DstSelect,
|
||||
uint TileMode,
|
||||
uint Pitch,
|
||||
GuestSampler Sampler,
|
||||
bool Arrayed = false,
|
||||
uint ArrayLayers = 1);
|
||||
GuestSampler Sampler);
|
||||
|
||||
internal sealed record GuestMemoryBuffer(
|
||||
ulong BaseAddress,
|
||||
|
||||
@@ -119,17 +119,6 @@ public static class JsonExports
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
// Catalog alias NID for the same callback setter.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "00oCq0RwSAY",
|
||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalNullAccessCallbackAlt(CpuContext ctx) =>
|
||||
InitializerSetGlobalNullAccessCallback(ctx);
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||
|
||||
@@ -362,7 +362,7 @@ public static class KernelExports
|
||||
ExportName = "open",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx);
|
||||
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1G3lF1Gg1k8",
|
||||
@@ -376,7 +376,7 @@ public static class KernelExports
|
||||
ExportName = "fstat",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx);
|
||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hcuQgD53UxM",
|
||||
|
||||
@@ -267,11 +267,6 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
var hostPath = ResolveGuestPath(guestPath);
|
||||
if (string.IsNullOrEmpty(hostPath))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
@@ -315,11 +310,6 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
var fromHost = ResolveGuestPath(fromGuest);
|
||||
var toHost = ResolveGuestPath(toGuest);
|
||||
if (string.IsNullOrEmpty(fromHost) || string.IsNullOrEmpty(toHost))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(fromHost))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,87 +41,18 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
private sealed class PthreadMutexState
|
||||
{
|
||||
private long _ownerThreadId;
|
||||
private int _recursionCount;
|
||||
private int _queuedWaiterCount;
|
||||
|
||||
public Lock SyncRoot { get; } = new();
|
||||
public ulong OwnerThreadId
|
||||
{
|
||||
get => unchecked((ulong)Volatile.Read(ref _ownerThreadId));
|
||||
set => Volatile.Write(ref _ownerThreadId, unchecked((long)value));
|
||||
}
|
||||
|
||||
public int RecursionCount
|
||||
{
|
||||
get => Volatile.Read(ref _recursionCount);
|
||||
set => Volatile.Write(ref _recursionCount, value);
|
||||
}
|
||||
|
||||
public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount);
|
||||
public ulong OwnerThreadId { get; set; }
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
public int Protocol { get; set; }
|
||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
||||
|
||||
public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge)
|
||||
{
|
||||
if (!allowWaiterBarge && QueuedWaiterCount != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryAcquireOwner(threadId);
|
||||
}
|
||||
|
||||
public bool TryAcquireOwner(ulong threadId)
|
||||
{
|
||||
if (Interlocked.CompareExchange(
|
||||
ref _ownerThreadId,
|
||||
unchecked((long)threadId),
|
||||
0) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _recursionCount, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReleaseUncontended(ulong threadId)
|
||||
{
|
||||
if (QueuedWaiterCount != 0 || RecursionCount != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _recursionCount, 0);
|
||||
if (Interlocked.CompareExchange(
|
||||
ref _ownerThreadId,
|
||||
0,
|
||||
unchecked((long)threadId)) == unchecked((long)threadId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _recursionCount, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount);
|
||||
|
||||
public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount);
|
||||
|
||||
public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount);
|
||||
|
||||
public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount);
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required string WakeKey { get; init; }
|
||||
public required bool Cooperative { get; set; }
|
||||
public ManualResetEventSlim? HostSignal { get; set; }
|
||||
public required bool Cooperative { get; init; }
|
||||
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||
public int Granted;
|
||||
}
|
||||
@@ -163,10 +94,7 @@ public static class KernelPthreadCompatExports
|
||||
public static int PthreadSelf(CpuContext ctx)
|
||||
{
|
||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
|
||||
{
|
||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||
}
|
||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||
ctx[CpuRegister.Rax] = currentThreadHandle;
|
||||
TracePthreadSelf(ctx, currentThreadHandle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -211,13 +139,6 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "B5GmVDKwpn0",
|
||||
ExportName = "pthread_yield",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GBUY7ywdULE",
|
||||
ExportName = "scePthreadRename",
|
||||
@@ -650,30 +571,6 @@ public static class KernelPthreadCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The POSIX-named alias of <see cref="PthreadOnce"/>. libKernel exports the
|
||||
/// same routine under two NIDs, and shipped middleware links the plain name:
|
||||
/// DOOM's libcohtml, PlayFab and party modules all import this one rather
|
||||
/// than scePthreadOnce.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "Z4QosVuAsA0",
|
||||
ExportName = "pthread_once",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx);
|
||||
|
||||
/// <summary>
|
||||
/// The POSIX-named alias of <see cref="PthreadRename"/>, following the same
|
||||
/// two-NID pattern as <see cref="PthreadOncePOSIX"/>.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "9vyP6Z7bqzc",
|
||||
ExportName = "pthread_rename_np",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx);
|
||||
|
||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
@@ -724,7 +621,7 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
lock (state.SyncRoot)
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||
{
|
||||
@@ -756,63 +653,11 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly))
|
||||
{
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (state.OwnerThreadId == currentThreadId)
|
||||
{
|
||||
if (state.Type == MutexTypeRecursive)
|
||||
{
|
||||
state.IncrementRecursion();
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
if (state.Type == MutexTypeAdaptiveNp)
|
||||
{
|
||||
var adaptiveResult = tryOnly
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult);
|
||||
return adaptiveResult;
|
||||
}
|
||||
|
||||
if (state.Type == MutexTypeNormal)
|
||||
{
|
||||
if (tryOnly)
|
||||
{
|
||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
state.IncrementRecursion();
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var ownedResult = tryOnly
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||
return ownedResult;
|
||||
}
|
||||
|
||||
var canCooperativelyBlock = !tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||
PthreadMutexWaiter? waiter = null;
|
||||
var acquiredWhileQueueing = false;
|
||||
lock (state.SyncRoot)
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId == currentThreadId)
|
||||
{
|
||||
@@ -823,30 +668,7 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
if (state.Type == MutexTypeAdaptiveNp)
|
||||
{
|
||||
if (tryOnly)
|
||||
{
|
||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
// Gen5 runtime wrappers can layer an adaptive lock call over
|
||||
// scePthreadMutexLock for one logical acquisition, followed by
|
||||
// only one unlock. Keep the duplicate acquisition idempotent so
|
||||
// the matching unlock fully releases the HLE mutex.
|
||||
TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (state.Type == MutexTypeNormal)
|
||||
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||
{
|
||||
if (tryOnly)
|
||||
{
|
||||
@@ -855,7 +677,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
||||
// over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here
|
||||
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||
// the compatibility recursion used by the original implementation;
|
||||
@@ -881,10 +703,10 @@ public static class KernelPthreadCompatExports
|
||||
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
||||
// blocked waiters are not starved by a barging locker.
|
||||
if (state.OwnerThreadId == 0 &&
|
||||
(tryOnly || state.Waiters.Count == 0) &&
|
||||
state.TryAcquireOwner(currentThreadId))
|
||||
if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0))
|
||||
{
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -896,14 +718,6 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
||||
acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter);
|
||||
}
|
||||
|
||||
if (acquiredWhileQueueing)
|
||||
{
|
||||
waiter!.HostSignal?.Dispose();
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (canCooperativelyBlock && waiter is not null &&
|
||||
@@ -937,29 +751,8 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
if (state.OwnerThreadId == currentThreadId)
|
||||
{
|
||||
if (state.RecursionCount > 1)
|
||||
{
|
||||
state.DecrementRecursion();
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (state.TryReleaseUncontended(currentThreadId))
|
||||
{
|
||||
if (state.QueuedWaiterCount != 0)
|
||||
{
|
||||
WakeFirstMutexWaiter(state);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
PthreadMutexWaiter? nextWaiter = null;
|
||||
lock (state.SyncRoot)
|
||||
string? nextWakeKey = null;
|
||||
lock (state)
|
||||
{
|
||||
if (state.RecursionCount <= 0)
|
||||
{
|
||||
@@ -977,29 +770,16 @@ public static class KernelPthreadCompatExports
|
||||
if (state.RecursionCount == 0)
|
||||
{
|
||||
state.OwnerThreadId = 0;
|
||||
|
||||
// Hand the mutex directly to the head waiter instead of only
|
||||
// waking it and relying on it to re-acquire. A woken waiter that
|
||||
// fails to self-grant (its wake races or is lost) would leave the
|
||||
// mutex "free with a queued waiter"; the fast-acquire path refuses
|
||||
// such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every
|
||||
// later locker — including the game's main thread — then queues
|
||||
// behind a head that never advances and the process wedges.
|
||||
if (state.Waiters.First is { } headNode &&
|
||||
TryGrantMutexWaiterLocked(state, headNode.Value))
|
||||
{
|
||||
nextWaiter = headNode.Value;
|
||||
if (!nextWaiter.Cooperative)
|
||||
{
|
||||
nextWaiter.HostSignal!.Set();
|
||||
}
|
||||
}
|
||||
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||
? state.Waiters.First.Value.WakeKey
|
||||
: null;
|
||||
Monitor.PulseAll(state);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextWaiter is { Cooperative: true })
|
||||
if (nextWakeKey is not null)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
@@ -1459,7 +1239,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
lock (mutexState.SyncRoot)
|
||||
lock (mutexState)
|
||||
{
|
||||
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
||||
{
|
||||
@@ -1472,10 +1252,10 @@ public static class KernelPthreadCompatExports
|
||||
// mutex held (the unlock below is skipped), wedging every thread
|
||||
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
||||
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
||||
_ = mutexState.TryAcquireOwner(currentThreadId);
|
||||
mutexState.OwnerThreadId = currentThreadId;
|
||||
mutexState.RecursionCount = 1;
|
||||
}
|
||||
|
||||
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||
else if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||
{
|
||||
return mutexState.OwnerThreadId == currentThreadId
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||
@@ -1644,7 +1424,6 @@ public static class KernelPthreadCompatExports
|
||||
if (node.Value.ThreadId == threadId)
|
||||
{
|
||||
state.Waiters.Remove(node);
|
||||
state.WaiterRemovedLocked();
|
||||
node.Value.Node = null;
|
||||
}
|
||||
|
||||
@@ -1659,10 +1438,8 @@ public static class KernelPthreadCompatExports
|
||||
WakeKey = cooperative
|
||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||
: string.Empty,
|
||||
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
|
||||
};
|
||||
waiter.Node = state.Waiters.AddLast(waiter);
|
||||
state.WaiterAddedLocked();
|
||||
return waiter;
|
||||
}
|
||||
|
||||
@@ -1672,7 +1449,7 @@ public static class KernelPthreadCompatExports
|
||||
var mutex = new PthreadMutexState();
|
||||
PthreadMutexWaiter first;
|
||||
PthreadMutexWaiter second;
|
||||
lock (mutex.SyncRoot)
|
||||
lock (mutex)
|
||||
{
|
||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||
@@ -1716,72 +1493,26 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!state.TryAcquireOwner(waiter.ThreadId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
state.Waiters.Remove(waiter.Node);
|
||||
state.WaiterRemovedLocked();
|
||||
waiter.Node = null;
|
||||
state.OwnerThreadId = waiter.ThreadId;
|
||||
state.RecursionCount = 1;
|
||||
Volatile.Write(ref waiter.Granted, 1);
|
||||
Monitor.PulseAll(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void WakeFirstMutexWaiter(PthreadMutexState state)
|
||||
{
|
||||
PthreadMutexWaiter? nextWaiter;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.OwnerThreadId != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nextWaiter = state.Waiters.First?.Value;
|
||||
if (nextWaiter is { Cooperative: false })
|
||||
{
|
||||
nextWaiter.HostSignal!.Set();
|
||||
}
|
||||
}
|
||||
|
||||
if (nextWaiter is { Cooperative: true })
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||
{
|
||||
ManualResetEventSlim? hostSignal = null;
|
||||
try
|
||||
lock (state)
|
||||
{
|
||||
while (true)
|
||||
while (!TryGrantMutexWaiterLocked(state, waiter))
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (waiter.HostSignal is null)
|
||||
{
|
||||
waiter.Cooperative = false;
|
||||
waiter.HostSignal = new ManualResetEventSlim(initialState: false);
|
||||
}
|
||||
|
||||
hostSignal = waiter.HostSignal;
|
||||
if (TryGrantMutexWaiterLocked(state, waiter))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
hostSignal.Reset();
|
||||
}
|
||||
|
||||
hostSignal.Wait();
|
||||
Monitor.Wait(state);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
hostSignal?.Dispose();
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGrantBlockedMutexLock(
|
||||
@@ -1792,7 +1523,7 @@ public static class KernelPthreadCompatExports
|
||||
PthreadMutexWaiter waiter)
|
||||
{
|
||||
var granted = false;
|
||||
lock (state.SyncRoot)
|
||||
lock (state)
|
||||
{
|
||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||
}
|
||||
@@ -1826,10 +1557,6 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
|
||||
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
|
||||
guestOwner == currentThreadId;
|
||||
|
||||
private static bool CompleteCondWaiterLocked(
|
||||
PthreadCondState state,
|
||||
PthreadCondWaiter waiter,
|
||||
@@ -1845,7 +1572,7 @@ public static class KernelPthreadCompatExports
|
||||
waiter.TimeoutTimer?.Dispose();
|
||||
waiter.TimeoutTimer = null;
|
||||
|
||||
lock (waiter.MutexState.SyncRoot)
|
||||
lock (waiter.MutexState)
|
||||
{
|
||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||
waiter.MutexState,
|
||||
@@ -1893,7 +1620,7 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (waiter.MutexState.SyncRoot)
|
||||
lock (waiter.MutexState)
|
||||
{
|
||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||
}
|
||||
|
||||
@@ -860,18 +860,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
|
||||
/// exports the same routine under two NIDs; middleware compiled against the
|
||||
/// plain POSIX headers links this one rather than scePthreadAttrGetschedparam.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "qlk9pSLsUmM",
|
||||
ExportName = "pthread_attr_getschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FXPWHNk8Of0",
|
||||
ExportName = "scePthreadAttrGetschedparam",
|
||||
@@ -1145,90 +1133,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "SFxTMOfuCkE",
|
||||
ExportName = "pthread_rwlock_tryrdlock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) =>
|
||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XhWHn6P5R7U",
|
||||
ExportName = "pthread_rwlock_trywrlock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) =>
|
||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true);
|
||||
|
||||
/// <summary>
|
||||
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
|
||||
/// only if the lock is free right now, otherwise reports BUSY.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists
|
||||
/// for the scheduler resume path and decrements WaitingWriters on success,
|
||||
/// which is correct only for a thread that previously incremented it. A fresh
|
||||
/// try never did, so reusing it would silently consume another thread's
|
||||
/// waiter count and let a queued writer be skipped.
|
||||
/// </remarks>
|
||||
private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
||||
{
|
||||
if (rwlockAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
// Mirrors the blocking path's re-entrant compat-writer grant so the
|
||||
// two agree on what counts as already owning the lock.
|
||||
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
||||
{
|
||||
rwlock.AddCompatWriter(currentThreadId);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId != 0 ||
|
||||
rwlock.ReaderTotalCount != 0 ||
|
||||
rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock");
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId == currentThreadId)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
rwlock.AddReader(currentThreadId);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+L98PIbGttk",
|
||||
ExportName = "scePthreadRwlockUnlock",
|
||||
@@ -1915,94 +1819,4 @@ public static class KernelPthreadExtendedCompatExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
// POSIX-named aliases. libKernel exports each of these routines under two
|
||||
// NIDs -- a scePthread* name and the plain POSIX name -- and middleware
|
||||
// compiled against POSIX headers links the latter. Both take identical
|
||||
// arguments and, per the convention already used by scePthreadOnce's alias,
|
||||
// return the same OrbisGen2Result rather than translating to errno.
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "a2P9wYGeZvc",
|
||||
ExportName = "pthread_setprio",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FIs3-UQT9sg",
|
||||
ExportName = "pthread_getschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vQm4fDEsWi8",
|
||||
ExportName = "pthread_attr_getstack",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ucsu-OK+els",
|
||||
ExportName = "pthread_attr_get_np",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JarMIy8kKEY",
|
||||
ExportName = "pthread_attr_setschedpolicy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "E+tyo3lp5Lw",
|
||||
ExportName = "pthread_attr_setdetachstate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "euKRgm0Vn2M",
|
||||
ExportName = "pthread_attr_setschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "7ZlAakEf0Qg",
|
||||
ExportName = "pthread_attr_setinheritsched",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0qOtCR-ZHck",
|
||||
ExportName = "pthread_attr_getstacksize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VUT1ZSrHT0I",
|
||||
ExportName = "pthread_attr_getdetachstate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JKyG3SWyA10",
|
||||
ExportName = "pthread_attr_setguardsize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JNkVVsVDmOk",
|
||||
ExportName = "pthread_attr_getguardsize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx);
|
||||
}
|
||||
|
||||
@@ -2058,13 +2058,6 @@ public static class KernelRuntimeCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NhpspxdjEKU",
|
||||
ExportName = "_nanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yS8U2TGCe1A",
|
||||
ExportName = "nanosleep",
|
||||
|
||||
@@ -191,27 +191,6 @@ public static class KernelSemaphoreCompatExports
|
||||
WakePredicate,
|
||||
deadline))
|
||||
{
|
||||
// A signal may have arrived between releasing the semaphore gate
|
||||
// (after incrementing WaitingThreads) and the scheduler registering
|
||||
// this block. When that happens WakeBlockedThreads cannot find the
|
||||
// waiter yet and the exit-handler re-check runs later; a re-check
|
||||
// here keeps the thread from yielding to the scheduler at all when
|
||||
// the count is already sufficient.
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
GuestThreadExecution.TryConsumeCurrentThreadBlock(out _);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-recheck handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
@@ -449,22 +428,6 @@ public static class KernelSemaphoreCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GEnUkDZoUwY",
|
||||
ExportName = "scePthreadSemInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemInit(CpuContext ctx)
|
||||
{
|
||||
// scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores
|
||||
if (ctx[CpuRegister.Rsi] != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return PosixSemInit(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YCV5dGGBcCo",
|
||||
ExportName = "sem_wait",
|
||||
@@ -483,13 +446,6 @@ public static class KernelSemaphoreCompatExports
|
||||
return KernelWaitSema(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C36iRE0F5sE",
|
||||
ExportName = "scePthreadSemWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WBWzsRifCEA",
|
||||
ExportName = "sem_trywait",
|
||||
@@ -507,19 +463,6 @@ public static class KernelSemaphoreCompatExports
|
||||
return KernelPollSema(ctx, handle, 1);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "H2a+IN9TP0E",
|
||||
ExportName = "scePthreadSemTrywait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemTryWait(CpuContext ctx)
|
||||
{
|
||||
var result = PosixSemTryWait(ctx);
|
||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN)
|
||||
: result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w5IHyvahg-o",
|
||||
ExportName = "sem_timedwait",
|
||||
@@ -556,13 +499,6 @@ public static class KernelSemaphoreCompatExports
|
||||
return KernelSignalSema(ctx, handle, 1);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aishVAiFaYM",
|
||||
ExportName = "scePthreadSemPost",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Bq+LRV-N6Hk",
|
||||
ExportName = "sem_getvalue",
|
||||
@@ -613,13 +549,6 @@ public static class KernelSemaphoreCompatExports
|
||||
return result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Vwc+L05e6oE",
|
||||
ExportName = "scePthreadSemDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx);
|
||||
|
||||
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||
{
|
||||
handle = 0;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
|
||||
// futex-style wait/wake: a thread parks on a guest address until another thread
|
||||
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
|
||||
// build their own spinlocks/queues on top of it and call the wait in a hot
|
||||
// loop; left unimplemented, every wait returns immediately and the runtime
|
||||
// busy-spins forever (millions of calls, no forward progress).
|
||||
//
|
||||
// This implements wait/wake over the existing cooperative-block scheduler,
|
||||
// keyed on the address. The real primitive takes a compare value so the wait
|
||||
// only sleeps while the address still holds the expected value; that exact
|
||||
// value is not recovered here, so each wait is given a bounded deadline and
|
||||
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
||||
// self-heals when the deadline expires and the guest re-checks its own
|
||||
// condition, which futex callers already tolerate. A matching wake releases
|
||||
// waiters immediately through the same key.
|
||||
public static class KernelSyncOnAddressCompatExports
|
||||
{
|
||||
// Safety-net poll interval. Real releases come from the wake side (generation
|
||||
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
||||
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
||||
// large: a short interval turns every parked waiter into a hot re-poll that
|
||||
// steals scheduler bandwidth from the threads that actually make progress
|
||||
// (including the ones that would issue the wake), so it must be a rare last
|
||||
// resort, not a spin substitute.
|
||||
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
||||
// which cannot use the guest-thread scheduler's block mechanism.
|
||||
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
||||
|
||||
// Per-address wake generation. A wait captures the current generation and
|
||||
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
||||
// wake bumps it. This is what actually holds the thread blocked: a bare
|
||||
// "always satisfied" predicate is treated as an immediate late-arrival by
|
||||
// the dispatcher's race guard and never yields, leaving the guest to
|
||||
// busy-spin. The generation also closes the register-vs-park race for free:
|
||||
// a wake landing in that window bumps the generation, so the predicate is
|
||||
// already satisfied and the guest correctly resumes at once.
|
||||
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
||||
|
||||
private static long CurrentGeneration(ulong address) =>
|
||||
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
||||
|
||||
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Hc4CaR6JBL0",
|
||||
ExportName = "sceKernelSyncOnAddressWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int SyncOnAddressWait(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
if (address == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var observedGeneration = CurrentGeneration(address);
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
||||
|
||||
// Cooperative path: stay parked until a wake bumps this address's
|
||||
// generation (or the deadline expires as a self-heal). The guest
|
||||
// re-evaluates its own condition after resuming.
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelSyncOnAddressWait",
|
||||
WakeKey(address),
|
||||
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
||||
deadline))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Non-cooperative caller (host main thread): bounded host wait so a
|
||||
// missed wake self-heals instead of hanging.
|
||||
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
||||
lock (gate)
|
||||
{
|
||||
if (CurrentGeneration(address) == observedGeneration)
|
||||
{
|
||||
Monitor.Wait(gate, WaitSelfHealTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "q2y-wDIVWZA",
|
||||
ExportName = "sceKernelSyncOnAddressWake",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int SyncOnAddressWake(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
if (address == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// rsi carries the number of waiters to release (1 = wake-one, a large
|
||||
// value = wake-all); default to all if it looks unset.
|
||||
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
||||
|
||||
// Bump the generation first so a wait that has registered but not yet
|
||||
// parked sees the change and resumes instead of missing this wake.
|
||||
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
||||
|
||||
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
||||
|
||||
if (_hostAddressGates.TryGetValue(address, out var gate))
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,7 @@ internal static class KernelVirtualRangeAllocator
|
||||
bool allowSearch,
|
||||
bool allowAllocateAtAlternative,
|
||||
string traceName,
|
||||
out ulong mappedAddress,
|
||||
bool backPartialOverlap = false)
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
@@ -43,18 +42,6 @@ internal static class KernelVirtualRangeAllocator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fixed mappings must cover the whole requested window even when part of
|
||||
// it is already backed by another allocation. The single-call AllocateAt
|
||||
// below is all-or-nothing and fails outright on partial overlap, leaving
|
||||
// the untouched pages unmapped for the guest to fault into. Fill the free
|
||||
// pages directly instead.
|
||||
if (backPartialOverlap &&
|
||||
addressSpace.TryBackFixedRange(desiredAddress, length, executable))
|
||||
{
|
||||
mappedAddress = desiredAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||
if (allocated == 0)
|
||||
{
|
||||
|
||||
@@ -184,212 +184,6 @@ public static class NetExports
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POSIX alias of <see cref="NetSetsockopt"/>; identical
|
||||
/// (fd, level, option, value, length) argument order.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "fFxGkxF2bVo",
|
||||
ExportName = "setsockopt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Reads back the socket options this backend actually tracks: SO_NBIO,
|
||||
/// SO_REUSEADDR and SO_ERROR.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Anything else returns EINVAL rather than a zero-filled buffer. A caller
|
||||
/// that receives success for an option nobody stored would treat whatever
|
||||
/// happens to be in its output buffer as the real setting, which is a harder
|
||||
/// failure to trace than an explicit rejection.
|
||||
/// </remarks>
|
||||
[SysAbiExport(
|
||||
Nid = "6O8EwYOgH9Y",
|
||||
ExportName = "getsockopt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixGetsockopt(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var level = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var option = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var valueAddress = ctx[CpuRegister.Rcx];
|
||||
var lengthAddress = ctx[CpuRegister.R8];
|
||||
if (!_sockets.TryGetValue(id, out var socket))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> lengthBytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(lengthAddress, lengthBytes))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
int value;
|
||||
switch (option)
|
||||
{
|
||||
// ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored.
|
||||
case 0x1200:
|
||||
value = socket.Blocking ? 0 : 1;
|
||||
break;
|
||||
case 0x0004:
|
||||
value = (int)socket.GetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress)! != 0 ? 1 : 0;
|
||||
break;
|
||||
// ORBIS_NET_SO_ERROR: nothing here records per-socket async errors,
|
||||
// so report "no pending error" rather than inventing one.
|
||||
case 0x1007:
|
||||
value = 0;
|
||||
break;
|
||||
default:
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int));
|
||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes) ||
|
||||
!ctx.Memory.TryWrite(lengthAddress, lengthBytes))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fZOeZIOEmLw",
|
||||
ExportName = "send",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixSend(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||
var length = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (!_sockets.TryGetValue(id, out var socket))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
if (length < 0 || (length != 0 && bufferAddress == 0))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var payload = new byte[length];
|
||||
if (!ctx.Memory.TryRead(bufferAddress, payload))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sent = socket.Send(payload, SocketFlags.None);
|
||||
TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0);
|
||||
return ctx.SetReturn(sent);
|
||||
}
|
||||
catch (SocketException exception)
|
||||
when (exception.SocketErrorCode == SocketError.WouldBlock)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a binary address as text. Pure conversion with no socket state,
|
||||
/// so it behaves identically to the console version for AF_INET/AF_INET6.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "5jRCs2axtr4",
|
||||
ExportName = "inet_ntop",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixInetNtop(CpuContext ctx)
|
||||
{
|
||||
var family = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
var destinationAddress = ctx[CpuRegister.Rdx];
|
||||
var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||
if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily.
|
||||
var addressLength = family switch
|
||||
{
|
||||
2 => 4,
|
||||
28 => 16,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if (addressLength == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var rawAddress = new byte[addressLength];
|
||||
if (!ctx.Memory.TryRead(sourceAddress, rawAddress))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var text = new IPAddress(rawAddress).ToString();
|
||||
var encoded = Encoding.ASCII.GetBytes(text);
|
||||
|
||||
// POSIX requires the terminator to fit as well; a truncated address string
|
||||
// is worse than a reported failure because the caller cannot detect it.
|
||||
if (encoded.Length + 1 > destinationSize)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var buffer = new byte[encoded.Length + 1];
|
||||
encoded.CopyTo(buffer, 0);
|
||||
if (!ctx.Memory.TryWrite(destinationAddress, buffer))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
// inet_ntop returns the destination pointer on success.
|
||||
ctx[CpuRegister.Rax] = destinationAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bErx49PgxyY",
|
||||
ExportName = "sceNetBind",
|
||||
|
||||
@@ -69,23 +69,6 @@ public static class NpManagerExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts the reachability callback and never invokes it. Reachability
|
||||
/// transitions only ever fire on a real PSN connection, which an offline
|
||||
/// session does not have, so registering successfully and staying silent is
|
||||
/// the accurate emulation of a signed-out console rather than a stub.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "hw5KNqAAels",
|
||||
ExportName = "sceNpRegisterNpReachabilityStateCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpRegisterNpReachabilityStateCallback(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "qQJfO8HAiaY",
|
||||
ExportName = "sceNpRegisterStateCallbackA",
|
||||
|
||||
@@ -80,25 +80,6 @@ public static class NpTrophy2Exports
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Gen5 ABI: context, handle, trophy id, then SceNpTrophy2Details and
|
||||
/// SceNpTrophy2Data output pointers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reports "no such trophy" rather than succeeding. Succeeding would require
|
||||
/// filling both output structures, and their exact layouts are not confirmed
|
||||
/// here — a title that trusted zeroed details would read an empty name and a
|
||||
/// grade of zero as real data. NOT_FOUND is a documented outcome that callers
|
||||
/// must already handle, so it degrades along a path the game tests.
|
||||
/// </remarks>
|
||||
[SysAbiExport(
|
||||
Nid = "EwNylPdWUTM",
|
||||
ExportName = "sceNpTrophy2GetTrophyInfo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
|
||||
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
|
||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||
{
|
||||
if (outAddress == 0)
|
||||
|
||||
@@ -10,11 +10,6 @@ public static class NpWebApi2Exports
|
||||
private const int NpWebApi2ErrorInvalidArgument = unchecked((int)0x80553402);
|
||||
|
||||
private static int _initialized;
|
||||
private static int _nextLibraryContextHandle;
|
||||
private static int _nextPushEventHandle;
|
||||
private static int _nextUserContextHandle = 1000;
|
||||
private static readonly object _contextGate = new();
|
||||
private static readonly HashSet<int> _libraryContexts = [];
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+o9816YQhqQ",
|
||||
@@ -31,28 +26,9 @@ public static class NpWebApi2Exports
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var libraryContextId = CreateLibraryContextId();
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||
return ctx.SetReturn(libraryContextId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MsaFhR+lPE4",
|
||||
ExportName = "sceNpWebApi2PushEventCreateFilter",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2PushEventCreateFilter(CpuContext ctx)
|
||||
{
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsValidLibraryContextId(libraryContextId))
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var filterHandle = Interlocked.Increment(ref _nextPushEventHandle);
|
||||
TraceNpWebApi2("push-event-create-filter", libraryContextId, (ulong)filterHandle);
|
||||
return ctx.SetReturn(filterHandle);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -62,16 +38,9 @@ public static class NpWebApi2Exports
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
||||
{
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsValidLibraryContextId(libraryContextId))
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var handle = CreatePushEventHandle();
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init-alt", libraryContextId, 0);
|
||||
return ctx.SetReturn(handle);
|
||||
TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -81,23 +50,10 @@ public static class NpWebApi2Exports
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2CreateUserContext(CpuContext ctx)
|
||||
{
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
TraceNpWebApi2(
|
||||
"create-user-context",
|
||||
libraryContextId,
|
||||
unchecked((uint)userId));
|
||||
|
||||
if (Volatile.Read(ref _initialized) == 0 ||
|
||||
!IsValidLibraryContextId(libraryContextId) ||
|
||||
userId == -1)
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var userContextId = Interlocked.Increment(ref _nextUserContextHandle);
|
||||
return ctx.SetReturn(userContextId);
|
||||
// No PSN backend: refuse user-context creation so the title's online
|
||||
// layer backs off instead of driving a half-created context handle.
|
||||
TraceNpWebApi2("create-user-context", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -108,57 +64,11 @@ public static class NpWebApi2Exports
|
||||
public static int NpWebApi2Terminate(CpuContext ctx)
|
||||
{
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsValidLibraryContextId(libraryContextId))
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
RemoveLibraryContextId(libraryContextId);
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
TraceNpWebApi2("term", libraryContextId, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static int CreateLibraryContextId()
|
||||
{
|
||||
var handle = Interlocked.Increment(ref _nextLibraryContextHandle);
|
||||
lock (_contextGate)
|
||||
{
|
||||
_libraryContexts.Add(handle);
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static int CreatePushEventHandle()
|
||||
{
|
||||
return Interlocked.Increment(ref _nextPushEventHandle);
|
||||
}
|
||||
|
||||
private static bool IsValidLibraryContextId(int libraryContextId)
|
||||
{
|
||||
if (libraryContextId <= 0 || libraryContextId >= 0x8000)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_contextGate)
|
||||
{
|
||||
return _libraryContexts.Contains(libraryContextId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveLibraryContextId(int libraryContextId)
|
||||
{
|
||||
lock (_contextGate)
|
||||
{
|
||||
_libraryContexts.Remove(libraryContextId);
|
||||
if (_libraryContexts.Count == 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
@@ -698,22 +698,14 @@ public static class PlayGoExports
|
||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||
if (!hasMetadata)
|
||||
{
|
||||
// No PlayGo sidecar: derive the installed chunk set from the pak files
|
||||
// actually present on disk. A locally dumped title has all of its data
|
||||
// installed, and a package that splits content across chunks names them
|
||||
// pakchunk<N>-<platform>.pak, so those N are exactly the chunks that
|
||||
// exist. Reporting only chunk 0 told such a title its remaining content
|
||||
// was missing: The Invincible (PPSA06426) ships pakchunk0..8 and spun
|
||||
// forever re-querying scePlayGoGetLocus for a chunk that never became
|
||||
// available. Available must stay true or scePlayGoOpen fails with
|
||||
// NotSupportPlayGo (fatal PS5-component init failure for UE titles).
|
||||
// Ids outside the discovered set still return BAD_CHUNK_ID, so
|
||||
// title-side chunk enumeration still terminates.
|
||||
var installedChunkIds = DiscoverInstalledChunkIds(app0Root);
|
||||
TracePlayGo($"metadata_missing; fully-installed chunks=[{string.Join(',', installedChunkIds)}]");
|
||||
// No PlayGo sidecar: report a fully-installed single chunk. Available must
|
||||
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
|
||||
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
installedChunkIds,
|
||||
[(ushort)0],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
@@ -726,41 +718,6 @@ public static class PlayGoExports
|
||||
: PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
|
||||
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
|
||||
// is the base chunk and must resolve even for a title with no pak files at
|
||||
// all (which keeps the single-chunk behaviour for such titles).
|
||||
private static ushort[] DiscoverInstalledChunkIds(string app0Root)
|
||||
{
|
||||
var ids = new SortedSet<ushort> { 0 };
|
||||
try
|
||||
{
|
||||
foreach (var pakFile in Directory.EnumerateFiles(app0Root, "pakchunk*.pak", SearchOption.AllDirectories))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(pakFile);
|
||||
var digits = name.AsSpan("pakchunk".Length);
|
||||
var length = 0;
|
||||
while (length < digits.Length && char.IsAsciiDigit(digits[length]))
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
if (length > 0 && ushort.TryParse(digits[..length], out var chunkId))
|
||||
{
|
||||
ids.Add(chunkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
|
||||
return ids.ToArray();
|
||||
}
|
||||
|
||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||
{
|
||||
if (!File.Exists(chunkDefsXml))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ public static class SaveDataExports
|
||||
private const ulong ResultInfosOffset = 0x20;
|
||||
private const uint SortKeyFreeBlocks = 5;
|
||||
private const uint SortOrderDescent = 1;
|
||||
private const uint MountModeReadOnly = 1u << 0;
|
||||
private const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
@@ -714,43 +713,16 @@ public static class SaveDataExports
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return MountSaveData(
|
||||
ctx,
|
||||
"mount3",
|
||||
userId,
|
||||
ResolveConfiguredTitleId(),
|
||||
dirName,
|
||||
blocks,
|
||||
systemBlocks,
|
||||
mountMode,
|
||||
resource,
|
||||
mode,
|
||||
resultAddress);
|
||||
}
|
||||
|
||||
private static int MountSaveData(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
int userId,
|
||||
string titleId,
|
||||
string dirName,
|
||||
ulong blocks,
|
||||
ulong systemBlocks,
|
||||
uint mountMode,
|
||||
uint resource,
|
||||
uint mode,
|
||||
ulong resultAddress)
|
||||
{
|
||||
if (userId < 0 || string.IsNullOrWhiteSpace(titleId) || string.IsNullOrWhiteSpace(dirName))
|
||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sanitizedTitleId = SanitizePathSegment(titleId.Trim());
|
||||
var titleId = ResolveConfiguredTitleId();
|
||||
var savePath = Path.Combine(
|
||||
ResolveTitleSaveRoot(userId, sanitizedTitleId),
|
||||
ResolveTitleSaveRoot(userId, titleId),
|
||||
SanitizePathSegment(dirName));
|
||||
var existed = Directory.Exists(savePath);
|
||||
var create = (mountMode & MountModeCreate) != 0;
|
||||
@@ -788,7 +760,7 @@ public static class SaveDataExports
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"{operation} user={userId} title={sanitizedTitleId} dir={dirName} blocks={blocks} " +
|
||||
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
||||
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
||||
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
||||
return SetReturn(ctx, 0);
|
||||
@@ -807,52 +779,6 @@ public static class SaveDataExports
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WAzWTZm1H+I",
|
||||
ExportName = "sceSaveDataTransferringMount",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataTransferringMount(CpuContext ctx)
|
||||
{
|
||||
var mountAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (mountAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, mountAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x10, out var dirNameAddress) ||
|
||||
titleIdAddress == 0 ||
|
||||
dirNameAddress == 0 ||
|
||||
!TryReadFixedAscii(ctx, titleIdAddress, SaveDataTitleIdSize, out var titleId) ||
|
||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return MountSaveData(
|
||||
ctx,
|
||||
"transferring_mount",
|
||||
userId,
|
||||
titleId,
|
||||
dirName,
|
||||
0,
|
||||
0,
|
||||
MountModeReadOnly,
|
||||
0,
|
||||
0,
|
||||
resultAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RjMlsR8EXrw",
|
||||
ExportName = "sceSaveDataTransferringMountPs4",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataTransferringMountPs4(CpuContext ctx) => SaveDataTransferringMount(ctx);
|
||||
|
||||
private static int _nextTransactionResource;
|
||||
[SysAbiExport(
|
||||
Nid = "gjRZNnw0JPE",
|
||||
@@ -861,48 +787,33 @@ public static class SaveDataExports
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||
{
|
||||
// Demon's Souls first-run call:
|
||||
// RDI = 0xC0000, RSI = RDX + 8, RDX = resource output.
|
||||
// Writing integer handle 1 makes the title dereference [1 + 8],
|
||||
// causing the repeatable access violation at guest address 0x9.
|
||||
var desWorkSize = ctx[CpuRegister.Rdi];
|
||||
var desWorkAddress = ctx[CpuRegister.Rsi];
|
||||
var desResourceAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (desWorkSize == 0xC0000 &&
|
||||
desResourceAddress != 0 &&
|
||||
desResourceAddress <= ulong.MaxValue - sizeof(ulong) &&
|
||||
desWorkAddress == desResourceAddress + sizeof(ulong))
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(desResourceAddress, 0))
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"create_transaction_resource_des_guard " +
|
||||
$"work_size=0x{desWorkSize:X} " +
|
||||
$"work=0x{desWorkAddress:X} " +
|
||||
$"resource_addr=0x{desResourceAddress:X} resource=0x0");
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var reserved = ctx[CpuRegister.Rsi];
|
||||
|
||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||
|
||||
// A small RDX value is a flag, and RCX contains the output address.
|
||||
// A larger RDX value is the output address for the older ABI.
|
||||
// The resource-out pointer's argument slot varies by SDK revision: some
|
||||
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
|
||||
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
|
||||
// real out-pointer in rcx. Probe the plausible candidates and write the
|
||||
// handle to the first writable one instead of faulting on a bad rdx.
|
||||
// This is a stub-level create (matches shadPS4's return-OK semantics);
|
||||
// never return MEMORY_FAULT for it, or the guest treats savedata init as
|
||||
// failed and never advances.
|
||||
var resourceAddress = 0UL;
|
||||
var selectedAddress = SelectTransactionResourceAddress(
|
||||
ctx[CpuRegister.Rdx],
|
||||
ctx[CpuRegister.Rcx]);
|
||||
if (selectedAddress != 0 && TryWriteUInt32(ctx, selectedAddress, id))
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
ctx[CpuRegister.Rdx],
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
})
|
||||
{
|
||||
resourceAddress = selectedAddress;
|
||||
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
||||
{
|
||||
resourceAddress = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
@@ -911,16 +822,6 @@ public static class SaveDataExports
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
internal static ulong SelectTransactionResourceAddress(ulong rdx, ulong rcx)
|
||||
{
|
||||
if (rdx == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return rdx <= ushort.MaxValue ? rcx : rdx;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lJUQuaKqoKY",
|
||||
ExportName = "sceSaveDataDeleteTransactionResource",
|
||||
|
||||
@@ -12,9 +12,6 @@ public static class ShareExports
|
||||
|
||||
private static int _initialized;
|
||||
private static string _contentParam = string.Empty;
|
||||
private static readonly object _callbackGate = new();
|
||||
private static ulong _contentEventCallback;
|
||||
private static ulong _contentEventCallbackArgument;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nBDD66kiFW8",
|
||||
@@ -65,56 +62,6 @@ public static class ShareExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Sygnk9dr5WQ",
|
||||
ExportName = "sceShareRegisterContentEventCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceShareUtility")]
|
||||
public static int ShareRegisterContentEventCallback(CpuContext ctx)
|
||||
{
|
||||
var callback = ctx[CpuRegister.Rdi];
|
||||
var argument = ctx[CpuRegister.Rsi];
|
||||
if (callback == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (_callbackGate)
|
||||
{
|
||||
_contentEventCallback = callback;
|
||||
_contentEventCallbackArgument = argument;
|
||||
}
|
||||
|
||||
TraceShare($"register_content_event_callback fn=0x{callback:X16} arg=0x{argument:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KnsfHKmZqFA",
|
||||
ExportName = "sceShareUnregisterContentEventCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceShareUtility")]
|
||||
public static int ShareUnregisterContentEventCallback(CpuContext ctx)
|
||||
{
|
||||
var callback = ctx[CpuRegister.Rdi];
|
||||
if (callback == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (_callbackGate)
|
||||
{
|
||||
if (_contentEventCallback == callback)
|
||||
{
|
||||
_contentEventCallback = 0;
|
||||
_contentEventCallbackArgument = 0;
|
||||
}
|
||||
}
|
||||
|
||||
TraceShare($"unregister_content_event_callback fn=0x{callback:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[maxLength];
|
||||
|
||||
@@ -25,7 +25,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFmpeg.AutoGen" />
|
||||
<PackageReference Include="Silk.NET.Input" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
|
||||
@@ -118,7 +118,7 @@ public static class UserServiceExports
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var capacity = ctx[CpuRegister.Rdx];
|
||||
if (userId != PrimaryUserId && userId != 1)
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
@@ -144,16 +144,6 @@ public static class UserServiceExports
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
// Title-captured alias NID for the same username query.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "znaWI0gpuo8",
|
||||
ExportName = "sceUserServiceGetUserName",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetUserNameAlt(CpuContext ctx) => UserServiceGetUserName(ctx);
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -953,22 +953,22 @@ public static partial class Gen5SpirvTranslator
|
||||
case "VPkMulF16":
|
||||
case "VPkMinF16":
|
||||
case "VPkMaxF16":
|
||||
case "VPkFmaF16":
|
||||
if (!TryEmitPackedF16(instruction, out result, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case "VFmaMixF32":
|
||||
case "VFmaMixloF16":
|
||||
case "VFmaMixhiF16":
|
||||
if (!TryEmitFmaMix(instruction, destination, out result, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case "VPkFmaF16":
|
||||
// Deliberately loud: a fused f16 FMA rounds the product+add once,
|
||||
// whereas doing the multiply-add in f32 and rounding to f16 at the
|
||||
// end double-rounds. Concrete miss: fma(0x4100, 0x7522, 0x04EA) is
|
||||
// 0x7A6B fused but 0x7A6A via f32. Exact emulation (round-to-odd
|
||||
// f32 product then RNE pack) is a planned follow-up slice.
|
||||
error =
|
||||
$"unsupported vop3p opcode {instruction.Opcode} " +
|
||||
"(fused f16 FMA requires single-rounding; deferred to a later slice)";
|
||||
return false;
|
||||
default:
|
||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||
return false;
|
||||
@@ -1008,9 +1008,8 @@ public static partial class Gen5SpirvTranslator
|
||||
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
|
||||
// rounds losslessly to f16 by the double-rounding theorem; a f16 product even
|
||||
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
|
||||
// conversions are. v_pk_fma_f16 cannot be reproduced by a plain f32
|
||||
// multiply-add plus a pack (that double-rounds), so it goes through the
|
||||
// round-to-odd sequence in EmitPackedF16FusedMultiplyAdd instead.
|
||||
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
|
||||
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
|
||||
private bool TryEmitPackedF16(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out uint result,
|
||||
@@ -1024,8 +1023,13 @@ public static partial class Gen5SpirvTranslator
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
|
||||
for (var index = 0; index < sourceCount; index++)
|
||||
if (control.Clamp)
|
||||
{
|
||||
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < 2; index++)
|
||||
{
|
||||
var source = instruction.Sources[index];
|
||||
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
||||
@@ -1042,112 +1046,7 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
// V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16 (VOP3P opcodes 0x20 / 0x21 /
|
||||
// 0x22). Unlike the packed v_pk_* ops these compute a single f32
|
||||
// fma(a, b, c): each of the three sources is *independently* read as
|
||||
// either a full f32 register/constant or one f16 half widened to f32,
|
||||
// selected per operand by op_sel_hi (read as f16 when set) and op_sel
|
||||
// (which half feeds the f32). For the mix ops the VOP3P neg_hi field is
|
||||
// the absolute-value modifier and neg negates, applied abs-then-neg to
|
||||
// match the hardware and shadPS4's GetSrcMix. _MIXLO / _MIXHI round the
|
||||
// f32 result back to f16 and write it into the low / high 16 bits of
|
||||
// vdst, leaving the other half intact.
|
||||
private bool TryEmitFmaMix(
|
||||
Gen5ShaderInstruction instruction,
|
||||
uint destination,
|
||||
out uint result,
|
||||
out string error)
|
||||
{
|
||||
result = 0;
|
||||
error = string.Empty;
|
||||
if (instruction.Control is not Gen5Vop3pControl control)
|
||||
{
|
||||
error = $"missing vop3p control for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var product = Bitcast(
|
||||
_uintType,
|
||||
Ext(
|
||||
50,
|
||||
_floatType,
|
||||
EmitFmaMixOperand(instruction, control, 0),
|
||||
EmitFmaMixOperand(instruction, control, 1),
|
||||
EmitFmaMixOperand(instruction, control, 2)));
|
||||
if (control.Clamp)
|
||||
{
|
||||
product = EmitClampToUnitInterval(product);
|
||||
}
|
||||
|
||||
if (instruction.Opcode == "VFmaMixF32")
|
||||
{
|
||||
result = product;
|
||||
return true;
|
||||
}
|
||||
|
||||
// _MIXLO / _MIXHI: narrow to f16 and merge into one half of vdst.
|
||||
var half = EmitFloatToHalf(product);
|
||||
var existing = LoadV(destination);
|
||||
result = instruction.Opcode == "VFmaMixloF16"
|
||||
? BitwiseOr(BitwiseAnd(existing, UInt(0xFFFF_0000)), half)
|
||||
: BitwiseOr(
|
||||
BitwiseAnd(existing, UInt(0x0000_FFFF)),
|
||||
ShiftLeftLogical(half, UInt(16)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
|
||||
// register operand is taken as an f16 (the half picked by op_sel, widened
|
||||
// exactly to f32) or as a full f32; inline constants are always f32. The
|
||||
// per-operand neg_hi bit takes the absolute value and neg negates, in that
|
||||
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
|
||||
// ops define them rather than the packed low/high-lane meaning.
|
||||
private uint EmitFmaMixOperand(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
int index)
|
||||
{
|
||||
var source = instruction.Sources[index];
|
||||
var readAsHalf =
|
||||
((control.OpSelHiMask >> index) & 1) != 0 &&
|
||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
|
||||
|
||||
uint value;
|
||||
if (readAsHalf)
|
||||
{
|
||||
var raw = GetRawSource(instruction, index);
|
||||
var half = ((control.OpSelMask >> index) & 1) != 0
|
||||
? ShiftRightLogical(raw, UInt(16))
|
||||
: raw;
|
||||
value = Bitcast(_floatType, EmitHalfToFloat(half));
|
||||
}
|
||||
else
|
||||
{
|
||||
value = GetFloatSource(instruction, index);
|
||||
}
|
||||
|
||||
if (((control.NegHiMask >> index) & 1) != 0)
|
||||
{
|
||||
value = Ext(4, _floatType, value);
|
||||
}
|
||||
|
||||
if (((control.NegLoMask >> index) & 1) != 0)
|
||||
{
|
||||
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
||||
// The op runs in f32 and its result is narrowed back to f16 exactly (see
|
||||
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
|
||||
// value is saturated to [0, 1] first; because 0.0 and 1.0 are exact in both
|
||||
// f32 and f16 and the clamp is monotonic, clamping before the narrowing
|
||||
// gives the same f16 the hardware produces by clamping the f16 result. For
|
||||
// the fused multiply-add the pre-narrowing value is the round-to-odd f32
|
||||
// from EmitPackedF16FusedMultiplyAdd, and round-to-odd preserves that
|
||||
// equivalence through the final round-to-nearest-even.
|
||||
private uint EmitPackedF16Lane(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
@@ -1155,113 +1054,15 @@ public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
||||
uint value;
|
||||
if (instruction.Opcode == "VPkFmaF16")
|
||||
var value = instruction.Opcode switch
|
||||
{
|
||||
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
|
||||
value = EmitPackedF16FusedMultiplyAdd(left, right, addend);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = Bitcast(_uintType, instruction.Opcode switch
|
||||
{
|
||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
||||
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
||||
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
||||
_ => left,
|
||||
});
|
||||
}
|
||||
|
||||
if (control.Clamp)
|
||||
{
|
||||
value = EmitClampToUnitInterval(value);
|
||||
}
|
||||
|
||||
return EmitFloatToHalf(value);
|
||||
}
|
||||
|
||||
// Saturates an f32 bit pattern to [0, 1] the way the VOP3P clamp modifier
|
||||
// does: below 0 (and NaN, since the ordered compare is false for it) becomes
|
||||
// 0, above 1 becomes 1. Ordered compares match the hardware's NaN-to-zero
|
||||
// behaviour without a separate IsNan test.
|
||||
private uint EmitClampToUnitInterval(uint valueBits)
|
||||
{
|
||||
var value = Bitcast(_floatType, valueBits);
|
||||
var aboveZero = _module.AddInstruction(SpirvOp.FOrdGreaterThan, _boolType, value, Float(0));
|
||||
var lowerBounded = _module.AddInstruction(SpirvOp.Select, _floatType, aboveZero, value, Float(0));
|
||||
var belowOne = _module.AddInstruction(SpirvOp.FOrdLessThan, _boolType, lowerBounded, Float(1));
|
||||
var clamped = _module.AddInstruction(SpirvOp.Select, _floatType, belowOne, lowerBounded, Float(1));
|
||||
return Bitcast(_uintType, clamped);
|
||||
}
|
||||
|
||||
// Fused f16 multiply-add with a single rounding, emulated in f32 without the
|
||||
// Float16 capability. The f32 product of two widened f16 values is exact
|
||||
// (11-bit significands, and the exponent stays inside the f32 normal range:
|
||||
// any non-zero product magnitude is in [2^-48, 2^33]), so only the addition
|
||||
// rounds. An f32 add then an f16 pack would round twice; instead the add is
|
||||
// corrected to round-to-odd, which a following round-to-nearest-even pack
|
||||
// turns into the exactly-once-rounded fused result (innocuous double rounding
|
||||
// holds because f32 carries 24 significand bits >= 11 + 2).
|
||||
//
|
||||
// sum = RN(product + addend); Knuth's 2Sum recovers the exact residual
|
||||
// (product + addend) - sum from four more RN ops. 2Sum is exact for any two
|
||||
// finite f32 inputs; no intermediate here can overflow (|product| < 2^33,
|
||||
// |addend| < 2^16) and none can enter the f32 subnormal range (every finite
|
||||
// value in play is a multiple of 2^-48 by construction), so implementation
|
||||
// f32 denorm-flush modes never see a denormal. If the residual says the sum
|
||||
// was inexact and the sum's significand is even, step one ulp towards the
|
||||
// true value: consecutive floats have consecutive sign-magnitude encodings,
|
||||
// so that neighbour is the enclosing float with the odd significand.
|
||||
//
|
||||
// Inf/NaN inputs make the residual NaN (e.g. sum - addend = Inf - Inf); the
|
||||
// ordered compare below is then false and the IEEE sum passes through
|
||||
// unchanged. A residual of zero also covers the exact-sum case, where the
|
||||
// parity fix must not fire. Returns the round-to-odd f32 bit pattern.
|
||||
private uint EmitPackedF16FusedMultiplyAdd(uint left, uint right, uint addend)
|
||||
{
|
||||
var product = EmitPreciseFloat(SpirvOp.FMul, left, right);
|
||||
var sum = EmitPreciseFloat(SpirvOp.FAdd, product, addend);
|
||||
|
||||
var productPart = EmitPreciseFloat(SpirvOp.FSub, sum, addend);
|
||||
var addendPart = EmitPreciseFloat(SpirvOp.FSub, sum, productPart);
|
||||
var productError = EmitPreciseFloat(SpirvOp.FSub, product, productPart);
|
||||
var addendError = EmitPreciseFloat(SpirvOp.FSub, addend, addendPart);
|
||||
var residual = EmitPreciseFloat(SpirvOp.FAdd, productError, addendError);
|
||||
|
||||
var sumBits = Bitcast(_uintType, sum);
|
||||
var residualBits = Bitcast(_uintType, residual);
|
||||
var inexact = _module.AddInstruction(
|
||||
SpirvOp.FOrdNotEqual, _boolType, residual, Float(0));
|
||||
var evenSignificand = Equal(BitwiseAnd(sumBits, UInt(1)), 0);
|
||||
var adjust = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd, _boolType, inexact, evenSignificand);
|
||||
|
||||
// Residual sign relative to the sum picks the step direction: same sign
|
||||
// means the true value lies away from zero (encoding + 1), opposite sign
|
||||
// means towards zero (encoding - 1). The sum cannot be zero here (any
|
||||
// inexact sum has magnitude >= 2^-48) and cannot be the largest finite
|
||||
// value (its significand is odd), so the step never crosses zero or Inf.
|
||||
var towardZero = IsNotZero(
|
||||
BitwiseAnd(BitwiseXor(sumBits, residualBits), UInt(0x8000_0000)));
|
||||
var stepped = SelectU(
|
||||
towardZero,
|
||||
ISubU(sumBits, UInt(1)),
|
||||
IAdd(sumBits, UInt(1)));
|
||||
return SelectU(adjust, stepped, sumBits);
|
||||
}
|
||||
|
||||
// A float op the driver must evaluate exactly as written. The 2Sum
|
||||
// residual above is error-free only op by op; without NoContraction
|
||||
// driver compilers fold the sequence (e.g. contract product+sum into an
|
||||
// f32 fma and simplify the rebuilt terms), collapsing the residual to
|
||||
// zero. Observed on AMD RDNA3 Windows: the pinned midpoint case decays
|
||||
// to the double-rounded result unless every op in the chain is marked.
|
||||
private uint EmitPreciseFloat(SpirvOp operation, uint left, uint right)
|
||||
{
|
||||
var value = _module.AddInstruction(operation, _floatType, left, right);
|
||||
_module.AddDecoration(value, SpirvDecoration.NoContraction);
|
||||
return value;
|
||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
||||
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
||||
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
||||
_ => left,
|
||||
};
|
||||
return EmitFloatToHalf(Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
|
||||
|
||||
@@ -313,8 +313,7 @@ public static partial class Gen5SpirvTranslator
|
||||
uint ComponentType,
|
||||
uint VectorType,
|
||||
ImageComponentKind ComponentKind,
|
||||
bool IsStorage,
|
||||
bool Arrayed);
|
||||
bool IsStorage);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
uint Variable,
|
||||
@@ -1001,13 +1000,11 @@ public static partial class Gen5SpirvTranslator
|
||||
SpirvCapability.StorageImageExtendedFormats);
|
||||
}
|
||||
|
||||
var isArrayed = !isStorage &&
|
||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding);
|
||||
var imageType = _module.TypeImage(
|
||||
componentType,
|
||||
SpirvImageDim.Dim2D,
|
||||
depth: false,
|
||||
arrayed: isArrayed,
|
||||
arrayed: false,
|
||||
multisampled: false,
|
||||
sampled: isStorage ? 2u : 1u,
|
||||
isStorage ? format : SpirvImageFormat.Unknown);
|
||||
@@ -1034,8 +1031,7 @@ public static partial class Gen5SpirvTranslator
|
||||
componentType,
|
||||
_module.TypeVector(componentType, 4),
|
||||
componentKind,
|
||||
isStorage,
|
||||
isArrayed));
|
||||
isStorage));
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
}
|
||||
@@ -3533,16 +3529,12 @@ public static partial class Gen5SpirvTranslator
|
||||
addressCursor += 4;
|
||||
}
|
||||
|
||||
var coordinates = resource.Arrayed
|
||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
||||
: BuildFloatCoordinates(image, addressCursor);
|
||||
var coordinates = BuildFloatCoordinates(image, addressCursor);
|
||||
var explicitLod = hasGradients || hasZeroLod || hasLod;
|
||||
var lod = hasZeroLod
|
||||
? Float(0)
|
||||
: hasLod
|
||||
? LoadImageFloatAddress(
|
||||
image,
|
||||
addressCursor + (resource.Arrayed ? 3 : 2))
|
||||
? LoadImageFloatAddress(image, addressCursor + 2)
|
||||
: lodOrBias;
|
||||
if (hasOffset)
|
||||
{
|
||||
@@ -3627,9 +3619,7 @@ public static partial class Gen5SpirvTranslator
|
||||
addressCursor += ImageFullAddressSlots(image);
|
||||
}
|
||||
|
||||
var coordinates = resource.Arrayed
|
||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
||||
: BuildFloatCoordinates(image, addressCursor);
|
||||
var coordinates = BuildFloatCoordinates(image, addressCursor);
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
@@ -3833,19 +3823,6 @@ public static partial class Gen5SpirvTranslator
|
||||
y);
|
||||
}
|
||||
|
||||
private uint BuildFloatArrayCoordinates(Gen5ImageControl image, int start)
|
||||
{
|
||||
var x = LoadImageFloatAddress(image, start);
|
||||
var y = LoadImageFloatAddress(image, start + 1);
|
||||
var slice = LoadImageFloatAddress(image, start + 2);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec3Type,
|
||||
x,
|
||||
y,
|
||||
slice);
|
||||
}
|
||||
|
||||
private static int ImageAddressRegister(
|
||||
Gen5ImageControl image,
|
||||
int component) => image.A16 ? component / 2 : component;
|
||||
@@ -4163,20 +4140,9 @@ public static partial class Gen5SpirvTranslator
|
||||
signedLod);
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySizeLod,
|
||||
resource.Arrayed ? _module.TypeVector(_intType, 3) : ivec2,
|
||||
ivec2,
|
||||
image,
|
||||
clampedLod);
|
||||
if (resource.Arrayed)
|
||||
{
|
||||
size = _module.AddInstruction(
|
||||
SpirvOp.VectorShuffle,
|
||||
ivec2,
|
||||
size,
|
||||
size,
|
||||
0u,
|
||||
1u);
|
||||
}
|
||||
|
||||
var sizeFloat = _module.AddInstruction(
|
||||
SpirvOp.ConvertSToF,
|
||||
_vec2Type,
|
||||
@@ -4190,34 +4156,11 @@ public static partial class Gen5SpirvTranslator
|
||||
_vec2Type,
|
||||
offsetFloat,
|
||||
sizeFloat);
|
||||
if (!resource.Arrayed)
|
||||
{
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.FAdd,
|
||||
_vec2Type,
|
||||
coordinates,
|
||||
normalizedOffset);
|
||||
}
|
||||
|
||||
var offsetVec3 = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec3Type,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_floatType,
|
||||
normalizedOffset,
|
||||
0u),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_floatType,
|
||||
normalizedOffset,
|
||||
1u),
|
||||
Float(0));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.FAdd,
|
||||
_vec3Type,
|
||||
_vec2Type,
|
||||
coordinates,
|
||||
offsetVec3);
|
||||
normalizedOffset);
|
||||
}
|
||||
|
||||
private bool TryEmitExport(
|
||||
@@ -5347,20 +5290,10 @@ public static partial class Gen5SpirvTranslator
|
||||
UInt(0x108));
|
||||
}
|
||||
|
||||
// A wave-mask SGPR (VCC/EXEC) consumed as a per-lane predicate — the
|
||||
// condition of VCndmask, a VCC/EXEC branch, or the derived _vcc/_exec
|
||||
// bool — must be tested at the CURRENT lane's bit, exactly as the
|
||||
// hardware does, not as "the 64-bit value is non-zero". The two coincide
|
||||
// for comparison results (only the lane's own bit is ever set), so the
|
||||
// single-lane path historically used a cheaper whole-word non-zero test.
|
||||
// But bitwise-complement wave-mask idioms (S_NOT/S_ORN2/S_ANDN2/S_NAND/
|
||||
// S_NOR on a 64-bit mask) set the unused upper 63 bits; a whole-word test
|
||||
// then reports "lane active" even when this lane's bit is clear. Unity's
|
||||
// PostProcessing NaN killer does exactly this (`anyNaN | ~allFinite`),
|
||||
// which made every valid pixel read as NaN and get replaced with 0 —
|
||||
// zeroing the whole scene before tonemap. Extract the lane bit always.
|
||||
private uint IsWaveMaskActive(uint mask) =>
|
||||
IsCurrentLaneSet(mask);
|
||||
_subgroupInvocationIdInput == 0
|
||||
? IsNotZero64(mask)
|
||||
: IsCurrentLaneSet(mask);
|
||||
|
||||
private uint IsCurrentLaneSet(uint mask) =>
|
||||
IsNotZero64(
|
||||
|
||||
@@ -238,7 +238,6 @@ public enum SpirvDecoration : uint
|
||||
Binding = 33,
|
||||
DescriptorSet = 34,
|
||||
Offset = 35,
|
||||
NoContraction = 42,
|
||||
}
|
||||
|
||||
public enum SpirvBuiltIn : uint
|
||||
|
||||
@@ -80,7 +80,7 @@ public static class Gen5ShaderTranslator
|
||||
public static bool IsScalarConsumed(ulong[] mask, uint register) =>
|
||||
register < 256 && (mask[register >> 6] & (1UL << (int)(register & 63))) != 0;
|
||||
|
||||
private const int MaxInstructions = 16384;
|
||||
private const int MaxInstructions = 4096;
|
||||
private const uint PsUserDataRegister = 0x0C;
|
||||
private const uint VsUserDataRegister = 0x4C;
|
||||
private const uint GsUserDataRegister = 0x8C;
|
||||
@@ -1192,10 +1192,8 @@ public static class Gen5ShaderTranslator
|
||||
|
||||
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
||||
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
||||
// The mix ops (0x20/0x21/0x22) are V_MAD_MIX_* on gfx9 and V_FMA_MIX_*
|
||||
// (fused) on the gfx10 the PS5 targets; both share these opcodes. Any
|
||||
// remaining packed opcode (integer, ...) stays opaque here and fails
|
||||
// loudly at emission rather than being silently mis-emitted.
|
||||
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
|
||||
// fail loudly at emission rather than being silently mis-emitted.
|
||||
name = opcode switch
|
||||
{
|
||||
0x0E => "VPkFmaF16",
|
||||
@@ -1203,9 +1201,6 @@ public static class Gen5ShaderTranslator
|
||||
0x10 => "VPkMulF16",
|
||||
0x11 => "VPkMinF16",
|
||||
0x12 => "VPkMaxF16",
|
||||
0x20 => "VFmaMixF32",
|
||||
0x21 => "VFmaMixloF16",
|
||||
0x22 => "VFmaMixhiF16",
|
||||
_ => $"Vop3pRaw{opcode:X2}",
|
||||
};
|
||||
|
||||
@@ -1612,11 +1607,6 @@ public static class Gen5ShaderTranslator
|
||||
binding.ResourceDescriptor.SequenceEqual(candidate.ResourceDescriptor));
|
||||
}
|
||||
|
||||
public static bool IsArrayedImageBinding(Gen5ImageBinding binding) =>
|
||||
binding.Control.IsArray &&
|
||||
(binding.Opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
||||
binding.Opcode.StartsWith("ImageGather4", StringComparison.Ordinal));
|
||||
|
||||
public static bool IsDataShareAtomic(string name) => name switch
|
||||
{
|
||||
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcPredicationTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x100;
|
||||
private const ulong PacketAddress = BaseAddress + 0x400;
|
||||
private const ulong PredicateAddress = BaseAddress + 0x800;
|
||||
|
||||
[Fact]
|
||||
public void DcbSetPredication_EmitsGen5Packet()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
ctx[CpuRegister.Rdx] = 3;
|
||||
ctx[CpuRegister.Rcx] = 1;
|
||||
ctx[CpuRegister.R8] = PredicateAddress + 7;
|
||||
ctx[CpuRegister.R9] = 2;
|
||||
|
||||
var result = AgcExports.DcbSetPredication(ctx);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0xC002_2000u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0003_1100u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(unchecked((uint)PredicateAddress), ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal((uint)(PredicateAddress >> 32), ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(PacketAddress + 16, ReadUInt64(memory, CommandBufferAddress + 0x10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPacketPredication_TogglesPacketHeaderBit()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
const uint header = 0xC003_1500;
|
||||
WriteUInt32(memory, PacketAddress, header);
|
||||
|
||||
ctx[CpuRegister.Rdi] = PacketAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.SetPacketPredication(ctx));
|
||||
Assert.Equal(header | 1u, ReadUInt32(memory, PacketAddress));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.SetPacketPredication(ctx));
|
||||
Assert.Equal(header, ReadUInt32(memory, PacketAddress));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcResourceOwnerTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong OwnerAddress = BaseAddress + 0x100;
|
||||
private const ulong NameAddress = BaseAddress + 0x200;
|
||||
private const ulong RegistrationMemoryAddress = BaseAddress + 0x400;
|
||||
|
||||
[Fact]
|
||||
public void RegisterOwner_DoesNotRequireOptionalResourceRegistryMemory()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(NameAddress, "GIRender");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress;
|
||||
ctx[CpuRegister.Rsi] = NameAddress;
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
|
||||
Assert.NotEqual(0u, ReadUInt32(memory, OwnerAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterOwner_RespectsExplicitRegistryCapacity()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = RegistrationMemoryAddress;
|
||||
ctx[CpuRegister.Rsi] = 0x1000;
|
||||
ctx[CpuRegister.Rdx] = 1;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.DriverInitResourceRegistration(ctx));
|
||||
|
||||
memory.WriteCString(NameAddress, "First");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress;
|
||||
ctx[CpuRegister.Rsi] = NameAddress;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
|
||||
|
||||
memory.WriteCString(NameAddress, "Second");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress + 4;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
AgcExports.DriverRegisterOwner(ctx));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcWaitRegMemTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x100;
|
||||
private const ulong PacketAddress = BaseAddress + 0x400;
|
||||
private const ulong StackAddress = BaseAddress + 0x800;
|
||||
|
||||
[Fact]
|
||||
public void DcbWaitRegMem32_EmitsGen5PacketLayout()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
var waitAddress = BaseAddress + 0xC03;
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
ctx[CpuRegister.Rdx] = 3;
|
||||
ctx[CpuRegister.Rcx] = 4;
|
||||
ctx[CpuRegister.R8] = 2;
|
||||
ctx[CpuRegister.R9] = waitAddress;
|
||||
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
|
||||
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
|
||||
WriteUInt32(memory, StackAddress + 24, 0x123456);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
|
||||
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0xC005_1028u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 16));
|
||||
Assert.Equal(0x0400_0053u, ReadUInt32(memory, PacketAddress + 20));
|
||||
Assert.Equal(0xFFFFu, ReadUInt32(memory, PacketAddress + 24));
|
||||
Assert.Equal(PacketAddress + 28, ReadUInt64(memory, CommandBufferAddress + 0x10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DcbWaitRegMem64_EmitsGen5PacketLayout()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
var waitAddress = BaseAddress + 0xC07;
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
ctx[CpuRegister.Rdx] = 6;
|
||||
ctx[CpuRegister.Rcx] = 3;
|
||||
ctx[CpuRegister.R8] = 1;
|
||||
ctx[CpuRegister.R9] = waitAddress;
|
||||
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
|
||||
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
|
||||
WriteUInt32(memory, StackAddress + 24, 0x320);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
|
||||
Assert.Equal(0xC007_1058u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(0xAABB_CCDDu, ReadUInt32(memory, PacketAddress + 16));
|
||||
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 20));
|
||||
Assert.Equal(0x1122_3344u, ReadUInt32(memory, PacketAddress + 24));
|
||||
Assert.Equal(0x0200_0156u, ReadUInt32(memory, PacketAddress + 28));
|
||||
Assert.Equal(0x32u, ReadUInt32(memory, PacketAddress + 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitRegMemPatchFunctions_UseGen5Fields()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
WriteUInt32(memory, PacketAddress, 0xC005_1028);
|
||||
WriteUInt32(memory, PacketAddress + 20, 0x0400_0153);
|
||||
|
||||
ctx[CpuRegister.Rdi] = PacketAddress;
|
||||
ctx[CpuRegister.Rsi] = BaseAddress + 0xD07;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchAddress(ctx));
|
||||
Assert.Equal(0x0000_0D04u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 5;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchCompareFunction(ctx));
|
||||
Assert.Equal(0x0400_0155u, ReadUInt32(memory, PacketAddress + 20));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 0xDEAD_BEEF;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchReference(ctx));
|
||||
Assert.Equal(0xDEAD_BEEFu, ReadUInt32(memory, PacketAddress + 16));
|
||||
}
|
||||
|
||||
private static FakeCpuMemory CreateMemory(out CpuContext ctx)
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
ctx = new CpuContext(memory, Generation.Gen5);
|
||||
ctx[CpuRegister.Rsp] = StackAddress;
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Regression tests for the VOP3P mix ops V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16
|
||||
// (opcodes 0x20 / 0x21 / 0x22). The decoder leaves any unlowered VOP3P opcode
|
||||
// opaque (Vop3pRaw20/21/22); before these were lowered they hit the vector-ALU
|
||||
// switch default and failed emission ("unsupported vector opcode"), which drops
|
||||
// the whole shader. Unity HDR / tone-mapping / auto-exposure shaders use
|
||||
// V_FMA_MIX_F32 and so failed to translate entirely.
|
||||
//
|
||||
// Each mix op computes a single f32 fma(a, b, c) where every source is read
|
||||
// *independently* as either a full f32 register or one f16 half widened to f32,
|
||||
// selected per operand by op_sel_hi (f16 when set) and op_sel (which half). The
|
||||
// mix ops also repurpose the VOP3P neg_hi field as an absolute-value modifier.
|
||||
public sealed class Gen5FmaMixSpirvTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
|
||||
// GLSL.std.450 extended-instruction numbers used by the lowering.
|
||||
private const uint GlslFma = 50;
|
||||
private const uint GlslFAbs = 4;
|
||||
|
||||
[Fact]
|
||||
public void FmaMixF32_TranslatesToFmaAndDoesNotDropShader()
|
||||
{
|
||||
// V_FMA_MIX_F32 v3, v0, v1, v2
|
||||
// op_sel_hi = 0b011 -> src0/src1 read as f16, src2 as full f32
|
||||
// op_sel = 0b010 -> src1 takes its high f16 half (src0 low half)
|
||||
// neg_hi = 0b001 -> abs(src0)
|
||||
// neg = 0b100 -> -src2
|
||||
// Reaching TryCompileComputeShader == true already proves the shader is no
|
||||
// longer dropped at the VOP3P default error path.
|
||||
var spirv = Compile([0xCC201103u, 0x9C0A0300u]);
|
||||
|
||||
Assert.True(
|
||||
ContainsExtInst(spirv, GlslFma),
|
||||
"V_FMA_MIX_F32 must lower to a GLSL.std.450 Fma");
|
||||
Assert.True(
|
||||
ContainsExtInst(spirv, GlslFAbs),
|
||||
"the neg_hi modifier on a mix source must lower to an FAbs (abs-then-neg)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FmaMixLoF16_TranslatesWithoutDroppingShader()
|
||||
{
|
||||
// V_FMA_MIXLO_F16 v3, v0, v1, v2 with op_sel_hi = 0b111 (all sources read
|
||||
// as f16 low halves). The f32 fma result is narrowed to f16 and merged
|
||||
// into the low 16 bits of vdst; the fma itself is still emitted.
|
||||
var spirv = Compile([0xCC214003u, 0x1C0A0300u]);
|
||||
|
||||
Assert.True(
|
||||
ContainsExtInst(spirv, GlslFma),
|
||||
"V_FMA_MIXLO_F16 must still lower its multiply-add to a GLSL.std.450 Fma");
|
||||
}
|
||||
|
||||
// True when the module contains an OpExtInst selecting the given GLSL.std.450
|
||||
// instruction number.
|
||||
private static bool ContainsExtInst(byte[] spirv, uint instruction)
|
||||
{
|
||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
||||
{
|
||||
// OpExtInst = 12: (opcode, resultType, resultId, set, instruction, ...).
|
||||
if (op != 12 || wordCount < 5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReadWord(spirv, offset + 16) == instruction)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
||||
byte[] spirv)
|
||||
{
|
||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
||||
{
|
||||
var word = ReadWord(spirv, offset);
|
||||
var wordCount = (int)(word >> 16);
|
||||
if (wordCount <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ((ushort)word, wordCount, offset);
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
||||
|
||||
private static byte[] Compile(uint[] programWords)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
||||
var shaderRegisters = new Dictionary<uint, uint>
|
||||
{
|
||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
||||
};
|
||||
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
0,
|
||||
shaderRegisters,
|
||||
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
||||
out var state,
|
||||
out var error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state, evaluation, 1, 1, 1, out var shader, out error),
|
||||
error);
|
||||
return shader.Spirv;
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,6 @@ using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Gen5ShaderScalarEvaluator.FallbackMemoryReader is a process-global static. This
|
||||
// test swaps it, but the SharpEmu.Libs [ModuleInitializer] (AgcShaderCompilerHooks)
|
||||
// reassigns the same static the first time any Libs type is touched. Under xUnit's
|
||||
// default cross-class parallelism a Libs test running concurrently can fire that
|
||||
// initializer mid-test and clobber the swapped-in reader (observed as all-zero
|
||||
// reads on CI). A DisableParallelization collection runs alone in the non-parallel
|
||||
// phase, so nothing else can mutate the static while this test holds it.
|
||||
[CollectionDefinition(Gen5ScalarEvaluatorStateCollection.Name, DisableParallelization = true)]
|
||||
public sealed class Gen5ScalarEvaluatorStateCollection
|
||||
{
|
||||
public const string Name = "Gen5ScalarEvaluatorState";
|
||||
}
|
||||
|
||||
[Collection(Gen5ScalarEvaluatorStateCollection.Name)]
|
||||
public sealed class Gen5ScalarMemoryFallbackTests
|
||||
{
|
||||
private const ulong ScalarTableAddress = 0x4_4665_4FD0;
|
||||
|
||||
@@ -13,8 +13,7 @@ public sealed class Gen5ShaderDecoderBoundaryTests
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint Export = 0xF8000000;
|
||||
private const uint Nop = 0xBF800000;
|
||||
private const uint EndPgm = 0xBF810000;
|
||||
private const int MaximumInstructionCount = 16384;
|
||||
private const int MaximumInstructionCount = 4096;
|
||||
|
||||
[Fact]
|
||||
public void MissingAddress_IsRejectedWithoutReadingGuestMemory()
|
||||
@@ -100,23 +99,6 @@ public sealed class Gen5ShaderDecoderBoundaryTests
|
||||
memory.Reads[^1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProgramMayEndAfterPreviousDecoderLimit()
|
||||
{
|
||||
const int previousDecoderLimit = 4096;
|
||||
var words = new uint[previousDecoderLimit + 1];
|
||||
Array.Fill(words, Nop);
|
||||
words[^1] = EndPgm;
|
||||
var memory = RecordingCpuMemory.FromWords(ShaderAddress, words);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.True(decoded, error);
|
||||
Assert.Equal(words.Length, program.Instructions.Count);
|
||||
Assert.Equal("SEndpgm", program.Instructions[^1].Opcode);
|
||||
Assert.Equal(words.Length, memory.Reads.Count);
|
||||
}
|
||||
|
||||
private static bool Decode(
|
||||
RecordingCpuMemory memory,
|
||||
ulong address,
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Regression tests for how a VCC/EXEC wave mask consumed as a per-lane predicate
|
||||
// is lowered to SPIR-V. A wave mask must be tested at the current lane's bit
|
||||
// (mask & lane_bit) — exactly as the hardware evaluates the VCndmask condition or
|
||||
// a VCC/EXEC branch — not with a whole-word "the 64-bit value is non-zero" test.
|
||||
//
|
||||
// The two agree for comparison results (only the lane's own bit is ever set), but
|
||||
// diverge for the bitwise-complement wave-mask idioms (S_NOT / S_ORN2 / S_ANDN2 /
|
||||
// S_NAND / S_NOR), which set the unused upper 63 bits. A whole-word test then
|
||||
// reports the lane active even when its bit is clear. Unity's PostProcessing NaN
|
||||
// killer combines its channels as `anyNaN | ~allFinite` (S_ORN2_B64); under the
|
||||
// whole-word test every valid pixel read as NaN and was replaced with 0, zeroing
|
||||
// the whole HDR scene before tone-mapping.
|
||||
public sealed class Gen5WaveMaskSpirvTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
|
||||
[Fact]
|
||||
public void WaveMaskPredicate_IsTestedAtCurrentLaneBit()
|
||||
{
|
||||
// V_CMP_EQ_F32 vcc, v0, v1 writes VCC at run time, which re-materialises
|
||||
// the per-lane _vcc predicate from the wave mask via IsWaveMaskActive.
|
||||
var spirv = Compile([0x7C04_0300u]);
|
||||
|
||||
// The lane's bit in single-lane emulation is the 64-bit constant 1, so the
|
||||
// predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0`
|
||||
// with no such mask. Require the lane-bit AND to be present.
|
||||
Assert.True(
|
||||
ContainsLaneBitMaskedWaveTest(spirv),
|
||||
"wave-mask predicate must be tested at the current lane bit "
|
||||
+ "(mask & lane_bit), not as a whole-word non-zero test");
|
||||
}
|
||||
|
||||
// True when the module contains an OpBitwiseAnd whose operand is a 64-bit
|
||||
// constant of value 1 — the current-lane bit that IsCurrentLaneSet masks the
|
||||
// wave mask with before the non-zero test.
|
||||
private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv)
|
||||
{
|
||||
var laneBitConstIds = new HashSet<uint>();
|
||||
|
||||
// Pass 1: collect 64-bit OpConstant result-ids whose value is 1.
|
||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
||||
{
|
||||
// OpConstant = 43; a 64-bit constant occupies 5 words
|
||||
// (opcode, resultType, resultId, valueLow, valueHigh).
|
||||
if (op != 43 || wordCount != 5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var resultId = ReadWord(spirv, offset + 8);
|
||||
var low = ReadWord(spirv, offset + 12);
|
||||
var high = ReadWord(spirv, offset + 16);
|
||||
if (low == 1 && high == 0)
|
||||
{
|
||||
laneBitConstIds.Add(resultId);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: look for an OpBitwiseAnd that consumes one of those constants.
|
||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
||||
{
|
||||
// OpBitwiseAnd = 199 (opcode, resultType, resultId, operand0, operand1).
|
||||
if (op != 199 || wordCount != 5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var operand0 = ReadWord(spirv, offset + 12);
|
||||
var operand1 = ReadWord(spirv, offset + 16);
|
||||
if (laneBitConstIds.Contains(operand0) || laneBitConstIds.Contains(operand1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
||||
byte[] spirv)
|
||||
{
|
||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
||||
{
|
||||
var word = ReadWord(spirv, offset);
|
||||
var wordCount = (int)(word >> 16);
|
||||
if (wordCount <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ((ushort)word, wordCount, offset);
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
||||
|
||||
private static byte[] Compile(uint[] programWords)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
||||
var shaderRegisters = new Dictionary<uint, uint>
|
||||
{
|
||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
||||
};
|
||||
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
0,
|
||||
shaderRegisters,
|
||||
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
||||
out var state,
|
||||
out var error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state, evaluation, 1, 1, 1, out var shader, out error),
|
||||
error);
|
||||
return shader.Spirv;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// TryDetile's exact-XOR fast path (PS5 swizzle modes 5/9/24/27) factors the
|
||||
// AddrLib bit-interleave into independent per-column X and per-row Y terms so
|
||||
// the inner loop is one array load and one XOR instead of a 16-bit interleave.
|
||||
// These tests pin that the factored output stays byte-identical to the direct
|
||||
// AddrLib address equation.
|
||||
public sealed class GnmTilingDetileTests
|
||||
{
|
||||
// Independent re-derivation of the 64 KiB RB+ R_X equation (swizzle mode 27,
|
||||
// 2 bytes/element) straight from the address-bit table, so the tiled source
|
||||
// layout does not depend on TryDetile's own internal factoring.
|
||||
private static readonly (uint XMask, uint YMask)[] RbPlus64KRenderX2Bpp =
|
||||
[
|
||||
(0, 0), (1u << 0, 0), (1u << 1, 0), (1u << 2, 0),
|
||||
(0, 1u << 0), (0, 1u << 1), (0, 1u << 2), (1u << 3, 0),
|
||||
(1u << 7, (1u << 4) | (1u << 7)), (1u << 4, 1u << 4), (1u << 6, 1u << 5), (1u << 5, 1u << 6),
|
||||
(0, 1u << 3), (1u << 6, 0), (1u << 7, 1u << 7), (1u << 8, 1u << 6),
|
||||
];
|
||||
|
||||
private static uint ReferenceOffset(uint x, uint y, (uint XMask, uint YMask)[] pattern)
|
||||
{
|
||||
uint offset = 0;
|
||||
for (var bit = 0; bit < pattern.Length; bit++)
|
||||
{
|
||||
var parity = (System.Numerics.BitOperations.PopCount(x & pattern[bit].XMask) +
|
||||
System.Numerics.BitOperations.PopCount(y & pattern[bit].YMask)) & 1;
|
||||
offset |= (uint)parity << bit;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(384, 200)]
|
||||
[InlineData(768, 512)]
|
||||
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation(
|
||||
int elementsWide,
|
||||
int elementsHigh)
|
||||
{
|
||||
const uint swizzleMode = 27; // 64 KiB RB+ R_X
|
||||
const int bytesPerElement = 2;
|
||||
const int blockBytes = 65536;
|
||||
// SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored.
|
||||
const int blockWidth = 256;
|
||||
const int blockHeight = 128;
|
||||
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||
var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight;
|
||||
|
||||
// Lay out a tiled source where each element stores its own linear index,
|
||||
// placed at the byte address the AddrLib equation dictates. The tiled
|
||||
// buffer is sized by padded whole blocks (block addressing overshoots the
|
||||
// linear extent). A correct detile must recover ascending linear indices.
|
||||
var tiled = new byte[blocksPerRow * blocksPerColumn * blockBytes];
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
{
|
||||
for (var x = 0; x < elementsWide; x++)
|
||||
{
|
||||
var blockIndex = (long)(y / blockHeight) * blocksPerRow + (x / blockWidth);
|
||||
// The equation yields a byte offset within the block (bit 0 is
|
||||
// Zero at 2bpp, keeping element writes 2-byte aligned).
|
||||
var sourceByte = (int)(blockIndex * blockBytes +
|
||||
ReferenceOffset((uint)x, (uint)y, RbPlus64KRenderX2Bpp));
|
||||
var linearIndex = (ushort)(y * elementsWide + x);
|
||||
tiled[sourceByte] = (byte)linearIndex;
|
||||
tiled[sourceByte + 1] = (byte)(linearIndex >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
var linear = new byte[elementsWide * elementsHigh * bytesPerElement];
|
||||
var ok = GnmTiling.TryDetile(tiled, linear, swizzleMode, elementsWide, elementsHigh, bytesPerElement);
|
||||
|
||||
Assert.True(ok);
|
||||
for (var i = 0; i < elementsWide * elementsHigh; i++)
|
||||
{
|
||||
var value = (ushort)(linear[i * 2] | (linear[i * 2 + 1] << 8));
|
||||
Assert.Equal((ushort)i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
public sealed class AmprWriteAddressTests
|
||||
{
|
||||
[Fact]
|
||||
public void MeasureCommandSizeWriteAddress0400_MatchesOnCompletionVariant()
|
||||
{
|
||||
const string nid = "4fgtGfXDrFc";
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var manager = CreateManagerWithExport(
|
||||
nid,
|
||||
"sceAmprMeasureCommandSizeWriteAddress_04_00");
|
||||
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
|
||||
var measured = context[CpuRegister.Rax];
|
||||
|
||||
Assert.Equal(0, AmprExports.MeasureCommandSizeWriteAddressOnCompletion(context));
|
||||
Assert.Equal(context[CpuRegister.Rax], measured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandBufferWriteAddress0400_WritesValueOnCompletion()
|
||||
{
|
||||
const string nid = "j0+3uJMxYJY";
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong commandBufferAddress = memoryBase + 0x100;
|
||||
const ulong recordBufferAddress = memoryBase + 0x200;
|
||||
const ulong watcherAddress = memoryBase + 0x800;
|
||||
const ulong watcherValue = 1;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var manager = CreateManagerWithExport(
|
||||
nid,
|
||||
"sceAmprCommandBufferWriteAddress_04_00");
|
||||
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = recordBufferAddress;
|
||||
context[CpuRegister.Rdx] = 0x100;
|
||||
|
||||
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
|
||||
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = watcherAddress;
|
||||
context[CpuRegister.Rdx] = watcherValue;
|
||||
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
|
||||
|
||||
Span<byte> watcher = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(watcherAddress, watcher));
|
||||
Assert.Equal(0UL, BinaryPrimitives.ReadUInt64LittleEndian(watcher));
|
||||
|
||||
Assert.Equal(0, AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
|
||||
|
||||
Assert.True(memory.TryRead(watcherAddress, watcher));
|
||||
Assert.Equal(watcherValue, BinaryPrimitives.ReadUInt64LittleEndian(watcher));
|
||||
}
|
||||
|
||||
private static ModuleManager CreateManagerWithExport(string nid, string exportName)
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||
Assert.Equal(exportName, export.Name);
|
||||
Assert.Equal("libSceAmpr", export.LibraryName);
|
||||
Assert.Equal(Generation.Gen5, export.Target);
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@@ -24,25 +24,14 @@ public sealed class AprStreamingContractTests
|
||||
const ulong destinationAddress = memoryBase + 0x2000;
|
||||
const ulong stackAddress = memoryBase + 0x3000;
|
||||
byte[] fileContents = [10, 11, 12, 13, 14, 15, 16, 17];
|
||||
// The kernel FS resolver default-denies raw absolute host paths, so the
|
||||
// guest addresses the file through a registered mount instead of handing
|
||||
// in a bare host temp path.
|
||||
var mountRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-apr-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(mountRoot);
|
||||
var mountPoint = $"/sharpemu_apr_mnt_{Guid.NewGuid():N}";
|
||||
const string fileName = "asset.bin";
|
||||
var hostPath = Path.Combine(mountRoot, fileName);
|
||||
var guestPath = $"{mountPoint}/{fileName}";
|
||||
var hostPath = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(hostPath, fileContents);
|
||||
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, mountRoot);
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(pathAddress, guestPath);
|
||||
memory.WriteCString(pathAddress, hostPath);
|
||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
||||
|
||||
context[CpuRegister.Rdi] = pathListAddress;
|
||||
@@ -97,155 +86,10 @@ public sealed class AprStreamingContractTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
|
||||
if (Directory.Exists(mountRoot))
|
||||
{
|
||||
Directory.Delete(mountRoot, recursive: true);
|
||||
}
|
||||
File.Delete(hostPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathListAddress = memoryBase + 0x100;
|
||||
const ulong pathAddress = memoryBase + 0x200;
|
||||
const ulong idsAddress = memoryBase + 0x800;
|
||||
const ulong sizesAddress = memoryBase + 0x880;
|
||||
const ulong errorIndexAddress = memoryBase + 0x8F0;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var missingHostPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
|
||||
memory.WriteCString(pathAddress, missingHostPath);
|
||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
||||
|
||||
context[CpuRegister.Rdi] = pathListAddress;
|
||||
context[CpuRegister.Rsi] = 1;
|
||||
context[CpuRegister.Rdx] = idsAddress;
|
||||
context[CpuRegister.Rcx] = sizesAddress;
|
||||
context[CpuRegister.R8] = errorIndexAddress;
|
||||
|
||||
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
||||
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress));
|
||||
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress));
|
||||
Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFilepathsToIdsAndFileSizes_InvalidErrorIndex_ReturnsMemoryFault()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathListAddress = memoryBase + 0x100;
|
||||
const ulong pathAddress = memoryBase + 0x200;
|
||||
const ulong idsAddress = memoryBase + 0x800;
|
||||
const ulong sizesAddress = memoryBase + 0x880;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var missingHostPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
|
||||
memory.WriteCString(pathAddress, missingHostPath);
|
||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
||||
|
||||
context[CpuRegister.Rdi] = pathListAddress;
|
||||
context[CpuRegister.Rsi] = 1;
|
||||
context[CpuRegister.Rdx] = idsAddress;
|
||||
context[CpuRegister.Rcx] = sizesAddress;
|
||||
context[CpuRegister.R8] = memoryBase + 0x5000;
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFilepathsToIdsAndFileSizes_MissingMidBatch_StopsAtFailingEntry()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathListAddress = memoryBase + 0x100;
|
||||
const ulong idsAddress = memoryBase + 0x800;
|
||||
const ulong sizesAddress = memoryBase + 0x880;
|
||||
const ulong errorIndexAddress = memoryBase + 0x8F0;
|
||||
byte[] fileContents = [1, 2, 3, 4, 5];
|
||||
// Entries 0 and 2 must resolve to a real file; the kernel FS resolver
|
||||
// default-denies raw absolute host paths, so the present file is reached
|
||||
// through a registered mount. The missing entry stays an unresolvable
|
||||
// path so the batch fails mid-way at index 1.
|
||||
var mountRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-apr-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(mountRoot);
|
||||
var mountPoint = $"/sharpemu_apr_mnt_{Guid.NewGuid():N}";
|
||||
const string fileName = "asset.bin";
|
||||
var hostPath = Path.Combine(mountRoot, fileName);
|
||||
var guestPath = $"{mountPoint}/{fileName}";
|
||||
var missingGuestPath = $"{mountPoint}/missing-{Guid.NewGuid():N}.bin";
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(hostPath, fileContents);
|
||||
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, mountRoot);
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(memoryBase + 0x200, guestPath);
|
||||
memory.WriteCString(memoryBase + 0x400, missingGuestPath);
|
||||
memory.WriteCString(memoryBase + 0x600, guestPath);
|
||||
WriteUInt64(memory, pathListAddress, memoryBase + 0x200);
|
||||
WriteUInt64(memory, pathListAddress + 8, memoryBase + 0x400);
|
||||
WriteUInt64(memory, pathListAddress + 16, memoryBase + 0x600);
|
||||
WriteUInt32(memory, idsAddress + 8, 0x1234_5678); // sentinel: entry 2 untouched
|
||||
WriteUInt64(memory, sizesAddress + 16, 0xDEAD);
|
||||
|
||||
context[CpuRegister.Rdi] = pathListAddress;
|
||||
context[CpuRegister.Rsi] = 3;
|
||||
context[CpuRegister.Rdx] = idsAddress;
|
||||
context[CpuRegister.Rcx] = sizesAddress;
|
||||
context[CpuRegister.R8] = errorIndexAddress;
|
||||
|
||||
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
||||
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
|
||||
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
|
||||
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress + 4));
|
||||
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress + 8));
|
||||
Assert.Equal(1u, ReadUInt32(memory, errorIndexAddress));
|
||||
Assert.Equal(0x1234_5678u, ReadUInt32(memory, idsAddress + 8));
|
||||
Assert.Equal(0xDEADul, ReadUInt64(memory, sizesAddress + 16));
|
||||
}
|
||||
finally
|
||||
{
|
||||
KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
|
||||
if (Directory.Exists(mountRoot))
|
||||
{
|
||||
Directory.Delete(mountRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
|
||||
@@ -80,19 +80,6 @@ public sealed class AjmExportsTests : IDisposable
|
||||
Assert.Equal(InvalidContext, RegisterCodec(contextId + 1, 1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(23u)]
|
||||
[InlineData(24u)]
|
||||
public void Gen5CodecTypesCanRegisterAndCreateInstances(uint codecType)
|
||||
{
|
||||
var contextId = Initialize();
|
||||
|
||||
Assert.Equal(0, RegisterCodec(contextId, codecType));
|
||||
Assert.Equal(
|
||||
0,
|
||||
CreateInstance(contextId, codecType, 0x401, InstanceAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceDestroy_RejectsUnknownContextAndSlot()
|
||||
{
|
||||
|
||||
@@ -40,84 +40,6 @@ public sealed class AvPlayerPathTests : IDisposable
|
||||
AssertPathIsInsideApp0(resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnrealRelativeRawPathAnchorsAtApp0AndResolvesMedia()
|
||||
{
|
||||
var mediaPath = CreateFile("SampleProject/Content/Movies/Startup.mp4");
|
||||
|
||||
var resolved = AvPlayerExports.ResolveGuestPath(
|
||||
"../../../SampleProject/Content/Movies/Startup.mp4");
|
||||
|
||||
Assert.NotNull(resolved);
|
||||
Assert.Equal(File.ReadAllBytes(mediaPath), File.ReadAllBytes(resolved));
|
||||
AssertPathIsInsideApp0(resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnrealRelativeRawPathCannotEscapeApp0()
|
||||
{
|
||||
var outsidePath = Path.Combine(_tempRoot, "outside.mp4");
|
||||
File.WriteAllBytes(outsidePath, [0x7F]);
|
||||
CreateFile("outside.mp4");
|
||||
|
||||
Assert.Null(AvPlayerExports.ResolveGuestPath("../../../outside.mp4"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentDirectoryRawPathResolvesInsideApp0()
|
||||
{
|
||||
var mediaPath = CreateFile("Movies/Intro.mp4");
|
||||
|
||||
var resolved = AvPlayerExports.ResolveGuestPath("./Movies/Intro.mp4");
|
||||
|
||||
Assert.NotNull(resolved);
|
||||
Assert.Equal(Path.GetFullPath(mediaPath), resolved);
|
||||
AssertPathIsInsideApp0(resolved);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "ffmpeg", "ffprobe")]
|
||||
[InlineData(true, "ffmpeg.exe", "ffprobe.exe")]
|
||||
public void MediaToolLookupUsesPlatformNames(
|
||||
bool isWindows,
|
||||
string ffmpegName,
|
||||
string ffprobeName)
|
||||
{
|
||||
var toolDirectory = Path.Combine(_tempRoot, "Media Tools");
|
||||
Directory.CreateDirectory(toolDirectory);
|
||||
var ffmpeg = Path.Combine(toolDirectory, ffmpegName);
|
||||
File.WriteAllBytes(ffmpeg, []);
|
||||
|
||||
var resolved = AvPlayerExports.FindFfmpeg(
|
||||
configured: null,
|
||||
searchPath: $"\"{toolDirectory}\"",
|
||||
isWindows);
|
||||
|
||||
Assert.Equal(ffmpeg, resolved);
|
||||
Assert.Equal(
|
||||
Path.Combine(toolDirectory, ffprobeName),
|
||||
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "ffmpeg")]
|
||||
[InlineData(true, "ffmpeg.exe")]
|
||||
public void MediaToolLookupFindsPackagedBinary(bool isWindows, string executable)
|
||||
{
|
||||
var publishDirectory = Path.Combine(_tempRoot, "publish");
|
||||
Directory.CreateDirectory(Path.Combine(publishDirectory, "ffmpeg"));
|
||||
var ffmpeg = Path.Combine(publishDirectory, "ffmpeg", executable);
|
||||
File.WriteAllBytes(ffmpeg, []);
|
||||
|
||||
Assert.Equal(
|
||||
ffmpeg,
|
||||
AvPlayerExports.FindFfmpeg(
|
||||
configured: null,
|
||||
searchPath: null,
|
||||
isWindows,
|
||||
publishDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RelativeFileUriCannotEscapeApp0()
|
||||
{
|
||||
@@ -221,14 +143,12 @@ public sealed class AvPlayerPathTests : IDisposable
|
||||
|
||||
private void AssertPathIsInsideApp0(string resolved)
|
||||
{
|
||||
var relative = Path.GetRelativePath(
|
||||
Path.GetFullPath(_app0Root),
|
||||
Path.GetFullPath(resolved));
|
||||
Assert.False(Path.IsPathFullyQualified(relative));
|
||||
Assert.NotEqual("..", relative);
|
||||
Assert.False(
|
||||
relative.StartsWith(
|
||||
".." + Path.DirectorySeparatorChar,
|
||||
StringComparison.Ordinal));
|
||||
var rootWithSeparator =
|
||||
Path.TrimEndingDirectorySeparator(Path.GetFullPath(_app0Root)) +
|
||||
Path.DirectorySeparatorChar;
|
||||
Assert.StartsWith(
|
||||
rootWithSeparator,
|
||||
Path.GetFullPath(resolved),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.AvPlayer;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
||||
|
||||
public sealed class AvPlayerStreamInfoTests
|
||||
{
|
||||
private const string StreamInfoExNid = "ctTAcF5DiKQ";
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const int MemorySize = 0x2000;
|
||||
private const ulong InfoAddress = BaseAddress + 0x100;
|
||||
private const ulong Handle = 0xA0_0000_0001;
|
||||
private const ulong DurationMilliseconds = 0x0102_0304_0506_0708;
|
||||
private const byte Sentinel = 0xAB;
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 0u)]
|
||||
[InlineData(true, 0u)]
|
||||
[InlineData(false, 1u)]
|
||||
[InlineData(true, 1u)]
|
||||
public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure(
|
||||
bool useExtendedFunction,
|
||||
uint streamIndex)
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
||||
try
|
||||
{
|
||||
Span<byte> window = stackalloc byte[40];
|
||||
window.Fill(Sentinel);
|
||||
Assert.True(memory.TryWrite(InfoAddress, window));
|
||||
|
||||
context[CpuRegister.Rdi] = Handle;
|
||||
context[CpuRegister.Rsi] = streamIndex;
|
||||
context[CpuRegister.Rdx] = InfoAddress;
|
||||
|
||||
var resultCode = useExtendedFunction
|
||||
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
|
||||
: AvPlayerExports.AvPlayerGetStreamInfo(context);
|
||||
Assert.Equal(0, resultCode);
|
||||
|
||||
Span<byte> result = stackalloc byte[40];
|
||||
Assert.True(memory.TryRead(InfoAddress, result));
|
||||
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
|
||||
if (streamIndex == 0)
|
||||
{
|
||||
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
|
||||
Assert.Equal(720u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(result[8..]));
|
||||
Assert.Equal(48_000u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
|
||||
}
|
||||
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
|
||||
|
||||
for (var index = 32; index < result.Length; index++)
|
||||
{
|
||||
Assert.Equal(Sentinel, result[index]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GetStreamInfoFunctionsRejectInvalidArguments(bool useExtendedFunction)
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
||||
|
||||
try
|
||||
{
|
||||
context[CpuRegister.Rdi] = Handle;
|
||||
context[CpuRegister.Rsi] = 2;
|
||||
context[CpuRegister.Rdx] = InfoAddress;
|
||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
||||
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
||||
|
||||
context[CpuRegister.Rdi] = Handle + 1;
|
||||
context[CpuRegister.Rdx] = InfoAddress;
|
||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamInfoExExportIsRegisteredForGen5Only()
|
||||
{
|
||||
var gen4Manager = new ModuleManager();
|
||||
gen4Manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4));
|
||||
Assert.False(gen4Manager.TryGetExport(StreamInfoExNid, out _));
|
||||
|
||||
var gen5Manager = new ModuleManager();
|
||||
gen5Manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
Assert.True(gen5Manager.TryGetExport(StreamInfoExNid, out var export));
|
||||
Assert.Equal("sceAvPlayerGetStreamInfoEx", export.Name);
|
||||
Assert.Equal("libSceAvPlayer", export.LibraryName);
|
||||
Assert.Equal(Generation.Gen5, export.Target);
|
||||
}
|
||||
|
||||
private static int InvokeGetStreamInfo(CpuContext context, bool useExtendedFunction) =>
|
||||
useExtendedFunction
|
||||
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
|
||||
: AvPlayerExports.AvPlayerGetStreamInfo(context);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Libs.Bink;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Bink;
|
||||
|
||||
public sealed class Bink2MovieBridgeTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-bink-{Guid.NewGuid():N}");
|
||||
|
||||
public Bink2MovieBridgeTests()
|
||||
{
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeaderPreservesFractionalFrameRate()
|
||||
{
|
||||
var path = WriteHeader("KB2j"u8, 3840, 2160, 30_000, 1_001);
|
||||
|
||||
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out var info));
|
||||
Assert.Equal(3840u, info.Width);
|
||||
Assert.Equal(2160u, info.Height);
|
||||
Assert.Equal(30_000u, info.FramesPerSecondNumerator);
|
||||
Assert.Equal(1_001u, info.FramesPerSecondDenominator);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("KB2g")]
|
||||
[InlineData("KB2i")]
|
||||
[InlineData("KB2j")]
|
||||
public void HeaderAcceptsBink2Revisions(string signature)
|
||||
{
|
||||
var path = WriteHeader(
|
||||
System.Text.Encoding.ASCII.GetBytes(signature),
|
||||
1920,
|
||||
1080,
|
||||
60,
|
||||
1);
|
||||
|
||||
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeaderRejectsMissingFrameRateDenominator()
|
||||
{
|
||||
var path = WriteHeader("KB2j"u8, 1920, 1080, 60, 0);
|
||||
|
||||
Assert.False(Bink2MovieBridge.TryReadBinkInfo(path, out _));
|
||||
}
|
||||
|
||||
private string WriteHeader(
|
||||
ReadOnlySpan<byte> signature,
|
||||
uint width,
|
||||
uint height,
|
||||
uint fpsNumerator,
|
||||
uint fpsDenominator)
|
||||
{
|
||||
var header = new byte[36];
|
||||
signature.CopyTo(header);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x14), width);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x18), height);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x1C), fpsNumerator);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x20), fpsDenominator);
|
||||
var path = Path.Combine(_tempDirectory, $"{Guid.NewGuid():N}.bk2");
|
||||
File.WriteAllBytes(path, header);
|
||||
return path;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Bink;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Bink;
|
||||
|
||||
public sealed class BinkFramePlaybackTests
|
||||
{
|
||||
[Fact]
|
||||
public void FramesAdvanceAccordingToMovieClock()
|
||||
{
|
||||
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2, 3));
|
||||
|
||||
Assert.Equal(1, WaitForAdvancedFrame(playback)[0]);
|
||||
Assert.True(playback.TryGetFrame(true, out var heldFrame, out var advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, heldFrame[0]);
|
||||
|
||||
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
|
||||
Assert.Equal(3, WaitForAdvancedFrame(playback)[0]);
|
||||
}
|
||||
|
||||
private static byte[] WaitForAdvancedFrame(BinkFramePlayback playback)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (playback.TryGetFrame(true, out var frame, out var advanced) && advanced)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The decoder did not produce a frame.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstFrameWaitsUntilPresentationStarts()
|
||||
{
|
||||
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2));
|
||||
|
||||
var first = WaitForFrame(playback, advanceClock: false);
|
||||
Assert.Equal(1, first[0]);
|
||||
Thread.Sleep(100);
|
||||
|
||||
Assert.True(playback.TryGetFrame(false, out var held, out var advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, held[0]);
|
||||
|
||||
Assert.True(playback.TryGetFrame(true, out held, out advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, held[0]);
|
||||
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
|
||||
}
|
||||
|
||||
private static byte[] WaitForFrame(
|
||||
BinkFramePlayback playback,
|
||||
bool advanceClock)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (playback.TryGetFrame(advanceClock, out var frame, out _))
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The decoder did not produce a frame.");
|
||||
}
|
||||
|
||||
private sealed class SequenceDecoder(params byte[] values) : IBinkFrameDecoder
|
||||
{
|
||||
private int _index;
|
||||
|
||||
public uint Width => 1;
|
||||
|
||||
public uint Height => 1;
|
||||
|
||||
public uint FramesPerSecondNumerator => 20;
|
||||
|
||||
public uint FramesPerSecondDenominator => 1;
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
if (_index >= values.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination.Fill(values[_index++]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed unsafe class JitStubsTests
|
||||
{
|
||||
[Fact]
|
||||
public void FindTlsAccessPatterns_IncludesLastValidOffset()
|
||||
{
|
||||
var pattern = JitStubs.TlsAccessPattern;
|
||||
var code = new byte[pattern.Length + 3];
|
||||
pattern.CopyTo(code.AsSpan(3));
|
||||
|
||||
fixed (byte* codePointer = code)
|
||||
{
|
||||
var matches = JitStubs.FindTlsAccessPatterns(codePointer, code.Length);
|
||||
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Equal((nint)(codePointer + 3), match);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
// These exercise the pure EXTRQ/INSERTQ bit-field semantics used by the general SSE4a
|
||||
// illegal-instruction software fallback (DirectExecutionBackend.Amd64Compat.cs). Expected values
|
||||
// were computed from the AMD64 Architecture Programmer's Manual definitions and cross-checked
|
||||
// with an independent Python re-implementation before being written here, so a regression in the
|
||||
// ported bit math fails in this file without needing a live guest or a Windows host.
|
||||
public sealed class Sse4aBitFieldEmulatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExtractBitField_ExtractsLowByte()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 8, index: 0);
|
||||
|
||||
Assert.Equal(0xF0UL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractBitField_ExtractsMidFieldAtNonZeroIndex()
|
||||
{
|
||||
// bits [31:16] of 0x1234_5678_9ABC_DEF0 == 0x9ABC
|
||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 16, index: 16);
|
||||
|
||||
Assert.Equal(0x9ABCUL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractBitField_LengthZeroMeansSixtyFour()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0xFFFF_FFFF_FFFF_FFFF, length: 0, index: 0);
|
||||
|
||||
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractBitField_MasksImmediatesToLowSixBits()
|
||||
{
|
||||
// length=0x28 (40) and index=0 is exactly the idiom SharpEmu's load-time
|
||||
// Sse4aExtrqBlendPatch already recognizes; the general emulator must agree with it.
|
||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x0000_0000_0000_00FF, length: 0x28, index: 0);
|
||||
|
||||
Assert.Equal(0xFFUL, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x1234_5678_9ABC_DEF0UL)]
|
||||
[InlineData(0x0000_0000_0000_0000UL)]
|
||||
[InlineData(0xFFFF_FFFF_FFFF_FFFFUL)]
|
||||
[InlineData(0x00FF_00FF_00FF_00FFUL)]
|
||||
public void ExtractBitField_AgreesWithSse4aExtrqBlendPatchsByteFourRule(ulong value)
|
||||
{
|
||||
// Sse4aExtrqBlendPatch's own comment states that after "EXTRQ xmmN, 0x28, 0x00", dword
|
||||
// lane 1 (bits 63:32) of the result equals byte 4 of the source zero-extended. The
|
||||
// general emulator (used for every other EXTRQ occurrence) must produce a result
|
||||
// consistent with that independently-reverse-engineered rule for the one idiom both
|
||||
// paths can be checked against.
|
||||
var extractedLow64 = Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x28, index: 0);
|
||||
var dword1 = (uint)(extractedLow64 >> 32);
|
||||
var byteFourZeroExtended = (uint)((value >> 32) & 0xFF);
|
||||
|
||||
Assert.Equal(byteFourZeroExtended, dword1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractBitField_RejectsUndefinedFieldPastRegisterEnd()
|
||||
{
|
||||
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 8, index: 60));
|
||||
Assert.Equal(0UL, Sse4aBitFieldEmulator.ExtractBitField(
|
||||
0xFFFF_FFFF_FFFF_FFFF,
|
||||
length: 8,
|
||||
index: 60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractBitField_RejectsZeroLengthAtNonZeroIndex()
|
||||
{
|
||||
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 0, index: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertBitField_InsertsFieldAtNonZeroIndexWithoutDisturbingOtherBits()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
||||
destination: 0x0000_0000_0000_0000,
|
||||
source: 0xFFFF_FFFF_FFFF_FFFF,
|
||||
length: 8,
|
||||
index: 8);
|
||||
|
||||
Assert.Equal(0x0000_0000_0000_FF00UL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertBitField_ClearsExactlyTheDestinationWindowBeforeInserting()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
||||
destination: 0xFFFF_FFFF_FFFF_FFFF,
|
||||
source: 0x0000_0000_0000_0000,
|
||||
length: 16,
|
||||
index: 16);
|
||||
|
||||
Assert.Equal(0xFFFF_FFFF_0000_FFFFUL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertBitField_InsertsLowByteAtIndexZero()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
||||
destination: 0x1122_3344_5566_7788,
|
||||
source: 0xAABB_CCDD_EEFF_0011,
|
||||
length: 8,
|
||||
index: 0);
|
||||
|
||||
Assert.Equal(0x1122_3344_5566_7711UL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertBitField_LengthZeroMeansSixtyFourAndOverwritesEverything()
|
||||
{
|
||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
||||
destination: 0x1111_1111_1111_1111,
|
||||
source: 0xFFFF_FFFF_FFFF_FFFF,
|
||||
length: 0,
|
||||
index: 0);
|
||||
|
||||
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertBitField_ZeroSourceFieldClearsOnlyItsOwnWindow()
|
||||
{
|
||||
// A zero-valued 12-bit field inserted at index 20 clears exactly bits [31:20]
|
||||
// (0x234 -> 0x000) and leaves every bit outside that window untouched.
|
||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
||||
destination: 0xABCD_EF01_2345_6789,
|
||||
source: 0,
|
||||
length: 12,
|
||||
index: 20);
|
||||
|
||||
Assert.Equal(0xABCD_EF01_0005_6789UL, result);
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the SSE4a EXTRQ/INSERTQ fault recovery through the POSIX signal bridge on
|
||||
/// Linux. Each test fabricates the exact frame the kernel hands the SIGILL handler - gregs
|
||||
/// whose RIP points at a real EXTRQ/INSERTQ encoding in probe-visible host memory, plus an
|
||||
/// FXSAVE image carrying the XMM registers - and drives the production entry point
|
||||
/// (TryHandlePosixFault) over it. The bridge must capture the XMM state into the CONTEXT
|
||||
/// scratch buffer, the recovery must decode and emulate the instruction, and the write-back
|
||||
/// must land the result in the FXSAVE image and advance RIP, because that is precisely what
|
||||
/// sigreturn restores on a live fault.
|
||||
/// </summary>
|
||||
public sealed unsafe class Sse4aPosixSignalRecoveryTests
|
||||
{
|
||||
private const int PosixSigIll = 4;
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsRipOffset = 16 * 8;
|
||||
private const int LinuxGregsFpstateOffset = 184;
|
||||
private const int FxsaveXmm0Offset = 160;
|
||||
private const int FxsaveXmm1Offset = 176;
|
||||
|
||||
private static readonly MethodInfo TryHandlePosixFault = typeof(DirectExecutionBackend).GetMethod(
|
||||
"TryHandlePosixFault",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
|
||||
private static readonly FieldInfo PosixSignalBackend = typeof(DirectExecutionBackend).GetField(
|
||||
"_posixSignalBackend",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
|
||||
private static readonly FieldInfo EmulatedCounter = typeof(DirectExecutionBackend).GetField(
|
||||
"_sse4aInstructionsEmulated",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
|
||||
private static readonly FieldInfo XmmBridgedFlag = typeof(DirectExecutionBackend).GetField(
|
||||
"_posixXmmContextBridged",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
|
||||
private static readonly MethodInfo TryRecoverAmdCompat = typeof(DirectExecutionBackend).GetMethod(
|
||||
"TryRecoverAmdCompatInstruction",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
[Fact]
|
||||
public void ExtrqSigillRoundTripsXmmThroughTheBridge()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// extrq xmm0, 0x10, 0x08
|
||||
var code = AllocateProbeVisibleCode([0x66, 0x0F, 0x78, 0xC0, 0x10, 0x08]);
|
||||
try
|
||||
{
|
||||
const ulong value = 0x1234_5678_9ABC_DEF0UL;
|
||||
var frame = new FakeSignalFrame((ulong)code);
|
||||
frame.SetXmmLow(FxsaveXmm0Offset, value);
|
||||
var emulatedBefore = (long)EmulatedCounter.GetValue(null)!;
|
||||
|
||||
Assert.True(frame.Dispatch());
|
||||
|
||||
Assert.Equal(
|
||||
Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x10, index: 0x08),
|
||||
frame.XmmLow(FxsaveXmm0Offset));
|
||||
Assert.Equal(0UL, frame.XmmHigh(FxsaveXmm0Offset));
|
||||
Assert.Equal((ulong)code + 6, frame.Rip);
|
||||
Assert.True((long)EmulatedCounter.GetValue(null)! > emulatedBefore);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeProbeVisibleCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertqSigillReadsSourceXmmThroughTheBridge()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// insertq xmm0, xmm1, 0x10, 0x08
|
||||
var code = AllocateProbeVisibleCode([0xF2, 0x0F, 0x78, 0xC1, 0x10, 0x08]);
|
||||
try
|
||||
{
|
||||
const ulong destination = 0x1111_2222_3333_4444UL;
|
||||
const ulong source = 0xAAAA_BBBB_CCCC_DDDDUL;
|
||||
var frame = new FakeSignalFrame((ulong)code);
|
||||
frame.SetXmmLow(FxsaveXmm0Offset, destination);
|
||||
frame.SetXmmLow(FxsaveXmm1Offset, source);
|
||||
|
||||
Assert.True(frame.Dispatch());
|
||||
|
||||
Assert.Equal(
|
||||
Sse4aBitFieldEmulator.InsertBitField(destination, source, length: 0x10, index: 0x08),
|
||||
frame.XmmLow(FxsaveXmm0Offset));
|
||||
Assert.Equal((ulong)code + 6, frame.Rip);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeProbeVisibleCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecoveryDeclinesWhenNoXmmStateWasBridged()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// extrq xmm0, 0x10, 0x08 - valid and recoverable, but without bridged XMM state
|
||||
// (fpstate missing from the frame) the recovery must decline rather than emulate
|
||||
// over the zeroed scratch bytes. Drive the recovery entry directly: earlier tests
|
||||
// on this thread leave the thread-static bridge flag set, so clear it the way a
|
||||
// fpstate-less capture would.
|
||||
var code = AllocateProbeVisibleCode([0x66, 0x0F, 0x78, 0xC0, 0x10, 0x08]);
|
||||
try
|
||||
{
|
||||
XmmBridgedFlag.SetValue(null, false);
|
||||
var backend = RuntimeHelpers.GetUninitializedObject(typeof(DirectExecutionBackend));
|
||||
var contextRecord = stackalloc byte[0x4D0];
|
||||
|
||||
var recovered = (bool)TryRecoverAmdCompat.Invoke(
|
||||
backend,
|
||||
[Pointer.Box(contextRecord, typeof(void*)), (ulong)code])!;
|
||||
|
||||
Assert.False(recovered);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeProbeVisibleCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Linux x86-64 signal frame as TryHandlePosixFault consumes it: a ucontext whose
|
||||
/// mcontext gregs sit at +40 (kernel sigcontext layout) with the fpstate pointer at
|
||||
/// gregs+184 aiming at a 512-byte FXSAVE image.
|
||||
/// </summary>
|
||||
private sealed class FakeSignalFrame
|
||||
{
|
||||
private readonly byte[] _ucontext = new byte[512];
|
||||
private readonly byte[] _fpstate = new byte[512];
|
||||
private readonly bool _wireFpstate;
|
||||
|
||||
public FakeSignalFrame(ulong rip, bool wireFpstate = true)
|
||||
{
|
||||
_wireFpstate = wireFpstate;
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
{
|
||||
*(ulong*)(ucontext + LinuxUcontextGregsOffset + LinuxGregsRipOffset) = rip;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong Rip
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
{
|
||||
return *(ulong*)(ucontext + LinuxUcontextGregsOffset + LinuxGregsRipOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetXmmLow(int fxsaveOffset, ulong value)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
*(ulong*)(fpstate + fxsaveOffset) = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong XmmLow(int fxsaveOffset)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
return *(ulong*)(fpstate + fxsaveOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong XmmHigh(int fxsaveOffset)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
return *(ulong*)(fpstate + fxsaveOffset + 8);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dispatch()
|
||||
{
|
||||
EnsureBridgeBackend();
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
if (_wireFpstate)
|
||||
{
|
||||
*(byte**)(ucontext + LinuxUcontextGregsOffset + LinuxGregsFpstateOffset) = fpstate;
|
||||
}
|
||||
|
||||
return (bool)TryHandlePosixFault.Invoke(
|
||||
null,
|
||||
[PosixSigIll, (nint)0, (nint)ucontext])!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TryHandlePosixFault only runs the recovery chain when a backend instance is
|
||||
/// registered. The tests do not need any of the constructor's state (and must not run
|
||||
/// it: it installs process-wide signal handlers), so register an uninitialized
|
||||
/// instance - the SIGILL recovery path only touches static state.
|
||||
/// </summary>
|
||||
private static void EnsureBridgeBackend()
|
||||
{
|
||||
if (PosixSignalBackend.GetValue(null) == null)
|
||||
{
|
||||
PosixSignalBackend.SetValue(
|
||||
null,
|
||||
RuntimeHelpers.GetUninitializedObject(typeof(DirectExecutionBackend)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The instruction bytes must live in memory the fault-time page probe
|
||||
/// (TryReadHostBytes -> VirtualQuery) can see; on POSIX that is HostMemory's shadow
|
||||
/// region table, the same allocator guest code pages come from. A raw libc mmap or a
|
||||
/// pinned managed array would be invisible and the recovery would decline before
|
||||
/// decoding.
|
||||
/// </summary>
|
||||
private static nint AllocateProbeVisibleCode(ReadOnlySpan<byte> instructions)
|
||||
{
|
||||
var size = checked((nuint)Environment.SystemPageSize);
|
||||
var mapping = (nint)HostMemory.Alloc(
|
||||
null,
|
||||
size,
|
||||
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||
HostMemory.PAGE_READWRITE);
|
||||
Assert.NotEqual((nint)0, mapping);
|
||||
|
||||
instructions.CopyTo(new Span<byte>((void*)mapping, checked((int)size)));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static void FreeProbeVisibleCode(nint mapping)
|
||||
{
|
||||
Assert.True(HostMemory.Free((void*)mapping, 0, HostMemory.MEM_RELEASE));
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Fiber;
|
||||
|
||||
/// <summary>
|
||||
/// Contract tests for the libSceFiber HLE exports. These pin the current
|
||||
/// validation and layout behaviour of <see cref="FiberExports"/>; they do not
|
||||
/// exercise a live guest thread scheduler.
|
||||
/// </summary>
|
||||
public sealed class FiberExportsTests
|
||||
{
|
||||
private const ulong Base = 0x3_0000_0000UL;
|
||||
private const int RegionSize = 0x2000;
|
||||
|
||||
private const int ErrorNull = unchecked((int)0x80590001);
|
||||
private const int ErrorAlignment = unchecked((int)0x80590002);
|
||||
private const int ErrorRange = unchecked((int)0x80590003);
|
||||
private const int ErrorInvalid = unchecked((int)0x80590004);
|
||||
private const int ErrorPermission = unchecked((int)0x80590005);
|
||||
|
||||
private const uint SignatureStart = 0xDEF1649Cu;
|
||||
private const uint SignatureEnd = 0xB37592A0u;
|
||||
private const ulong StackSignature = 0x7149F2CA7149F2CAUL;
|
||||
private const uint StateIdle = 2;
|
||||
|
||||
private const ulong FiberAddress = Base;
|
||||
private const ulong NameAddress = Base + 0x200;
|
||||
private const ulong ContextAddress = Base + 0x400;
|
||||
private const ulong EntryAddress = 0x4_0000_1000UL;
|
||||
private const ulong InfoAddress = Base + 0x800;
|
||||
|
||||
public FiberExportsTests()
|
||||
{
|
||||
FiberExports.ResetRuntimeState();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptParamInitialize_NullParam_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = 0;
|
||||
|
||||
var result = FiberExports.FiberOptParamInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelf_NullOutAddress_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = 0;
|
||||
|
||||
var result = FiberExports.FiberGetSelf(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelf_OutsideFiberContext_ReturnsPermissionError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = Base + 0x100;
|
||||
|
||||
var result = FiberExports.FiberGetSelf(context);
|
||||
|
||||
Assert.Equal(ErrorPermission, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_NullInfo_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
|
||||
var result = FiberExports.FiberGetInfo(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_NullFiber_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = 0;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_NullName_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_NullEntry_ReturnsNullError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorNull, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_MisalignedFiber_ReturnsAlignmentError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress + 4;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorAlignment, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_TooSmallContextSize_ReturnsRangeError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
context[CpuRegister.R8] = ContextAddress;
|
||||
context[CpuRegister.R9] = 256;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorRange, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_ContextAddressWithoutSize_ReturnsInvalidError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
context[CpuRegister.R8] = ContextAddress;
|
||||
context[CpuRegister.R9] = 0;
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(ErrorInvalid, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_Valid_WritesExpectedLayout()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
const string name = "TestFiber";
|
||||
WriteCString(memory, NameAddress, name);
|
||||
const ulong argOnInitialize = 0xDEADUL;
|
||||
const ulong contextSize = 512UL;
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
context[CpuRegister.Rcx] = argOnInitialize;
|
||||
context[CpuRegister.R8] = ContextAddress;
|
||||
context[CpuRegister.R9] = contextSize;
|
||||
// RSP defaults to 0 (unmapped); ReadStackArg64 falls back to 0 ->
|
||||
// optParam = 0, buildVersion = 0. ApplyInitializationFlags(0, 0, false) == 0.
|
||||
|
||||
var result = FiberExports.FiberInitialize(context);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
|
||||
Assert.Equal(SignatureStart, ReadUInt32(memory, FiberAddress + 0));
|
||||
Assert.Equal(StateIdle, ReadUInt32(memory, FiberAddress + 4));
|
||||
Assert.Equal(EntryAddress, ReadUInt64(memory, FiberAddress + 8));
|
||||
Assert.Equal(argOnInitialize, ReadUInt64(memory, FiberAddress + 16));
|
||||
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 24));
|
||||
Assert.Equal(contextSize, ReadUInt64(memory, FiberAddress + 32));
|
||||
AssertInlineName(memory, FiberAddress + 40, name);
|
||||
Assert.Equal(0UL, ReadUInt64(memory, FiberAddress + 72));
|
||||
Assert.Equal(0u, ReadUInt32(memory, FiberAddress + 80));
|
||||
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 88));
|
||||
Assert.Equal(ContextAddress + contextSize, ReadUInt64(memory, FiberAddress + 96));
|
||||
Assert.Equal(SignatureEnd, ReadUInt32(memory, FiberAddress + 104));
|
||||
Assert.Equal(StackSignature, ReadUInt64(memory, ContextAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_AfterInitialize_RoundTripsFields()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
const string name = "RoundTrip";
|
||||
WriteCString(memory, NameAddress, name);
|
||||
const ulong argOnInitialize = 0xCAFEUL;
|
||||
const ulong contextSize = 512UL;
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
context[CpuRegister.Rcx] = argOnInitialize;
|
||||
context[CpuRegister.R8] = ContextAddress;
|
||||
context[CpuRegister.R9] = contextSize;
|
||||
|
||||
Assert.Equal(0, FiberExports.FiberInitialize(context));
|
||||
|
||||
WriteUInt64(memory, InfoAddress, 128);
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = InfoAddress;
|
||||
|
||||
var result = FiberExports.FiberGetInfo(context);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(EntryAddress, ReadUInt64(memory, InfoAddress + 8));
|
||||
Assert.Equal(argOnInitialize, ReadUInt64(memory, InfoAddress + 16));
|
||||
Assert.Equal(ContextAddress, ReadUInt64(memory, InfoAddress + 24));
|
||||
Assert.Equal(contextSize, ReadUInt64(memory, InfoAddress + 32));
|
||||
AssertInlineName(memory, InfoAddress + 40, name);
|
||||
Assert.Equal(ulong.MaxValue, ReadUInt64(memory, InfoAddress + 72));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInfo_WrongSize_ReturnsInvalidError()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteCString(memory, NameAddress, "F");
|
||||
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = EntryAddress;
|
||||
context[CpuRegister.R8] = ContextAddress;
|
||||
context[CpuRegister.R9] = 512;
|
||||
|
||||
Assert.Equal(0, FiberExports.FiberInitialize(context));
|
||||
|
||||
WriteUInt64(memory, InfoAddress, 64);
|
||||
context[CpuRegister.Rdi] = FiberAddress;
|
||||
context[CpuRegister.Rsi] = InfoAddress;
|
||||
|
||||
var result = FiberExports.FiberGetInfo(context);
|
||||
|
||||
Assert.Equal(ErrorInvalid, result);
|
||||
}
|
||||
|
||||
private static void WriteCString(FakeCpuMemory memory, ulong address, string text)
|
||||
{
|
||||
memory.WriteCString(address, text);
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void AssertInlineName(FakeCpuMemory memory, ulong address, string expected)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[32];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
var length = buffer.IndexOf((byte)0);
|
||||
if (length < 0)
|
||||
{
|
||||
length = buffer.Length;
|
||||
}
|
||||
|
||||
Assert.Equal(expected, System.Text.Encoding.UTF8.GetString(buffer[..length]));
|
||||
}
|
||||
}
|
||||
@@ -41,32 +41,4 @@ public sealed class FontExportsTests
|
||||
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[8..]));
|
||||
Assert.Equal(Sentinel, BinaryPrimitives.ReadUInt32LittleEndian(layout[12..]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetVerticalLayout_WritesExactlyThreeFloats()
|
||||
{
|
||||
const uint Sentinel = 0xDEADBEEF;
|
||||
Span<byte> sentinelBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(sentinelBytes, Sentinel);
|
||||
Assert.True(_ctx.Memory.TryWrite(LayoutAddress + 12, sentinelBytes));
|
||||
|
||||
_ctx[CpuRegister.Rsi] = LayoutAddress;
|
||||
Assert.Equal(0, FontExports.GetVerticalLayout(_ctx));
|
||||
|
||||
Span<byte> layout = stackalloc byte[16];
|
||||
Assert.True(_ctx.Memory.TryRead(LayoutAddress, layout));
|
||||
Assert.Equal(8.0f, BinaryPrimitives.ReadSingleLittleEndian(layout));
|
||||
Assert.Equal(16.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[4..]));
|
||||
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[8..]));
|
||||
Assert.Equal(Sentinel, BinaryPrimitives.ReadUInt32LittleEndian(layout[12..]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetVerticalLayout_NullBuffer_ReturnsInvalidArgument()
|
||||
{
|
||||
_ctx[CpuRegister.Rsi] = 0;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
FontExports.GetVerticalLayout(_ctx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.GUI;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.GUI;
|
||||
|
||||
public sealed class GuiSettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void NormalizeFromJson_AllPropertiesNull_FallsBackToDefaults()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"LogLevel": null,
|
||||
"GameFolders": null,
|
||||
"ExcludedGames": null,
|
||||
"EnvironmentToggles": null,
|
||||
"Language": null,
|
||||
"DiscordClientId": null
|
||||
}
|
||||
""";
|
||||
|
||||
var settings = GuiSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.Equal("Info", settings.LogLevel);
|
||||
Assert.Equal("en", settings.Language);
|
||||
Assert.Equal("1525606762248540221", settings.DiscordClientId);
|
||||
Assert.Empty(settings.GameFolders);
|
||||
Assert.Empty(settings.ExcludedGames);
|
||||
Assert.Empty(settings.EnvironmentToggles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromJson_ValidValues_ArePreserved()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"LogLevel": "Debug",
|
||||
"GameFolders": ["C:\\Games"],
|
||||
"ExcludedGames": ["C:\\Games\\skip.bin"],
|
||||
"EnvironmentToggles": ["SHARPEMU_TRACE"],
|
||||
"Language": "pt-BR",
|
||||
"DiscordClientId": "999"
|
||||
}
|
||||
""";
|
||||
|
||||
var settings = GuiSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.Equal("Debug", settings.LogLevel);
|
||||
Assert.Equal("pt-BR", settings.Language);
|
||||
Assert.Equal("999", settings.DiscordClientId);
|
||||
Assert.Equal(["C:\\Games"], settings.GameFolders);
|
||||
Assert.Equal(["C:\\Games\\skip.bin"], settings.ExcludedGames);
|
||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
||||
}
|
||||
|
||||
// An empty Discord client ID intentionally disables Rich Presence.
|
||||
[Fact]
|
||||
public void NormalizeFromJson_EmptyDiscordClientId_IsPreservedNotNormalized()
|
||||
{
|
||||
const string json = """{ "DiscordClientId": "" }""";
|
||||
|
||||
var settings = GuiSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.Equal(string.Empty, settings.DiscordClientId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromJson_NullOrEmptyListEntries_AreFilteredOut()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"GameFolders": ["C:\\Games", null, ""],
|
||||
"ExcludedGames": [null],
|
||||
"EnvironmentToggles": [null, "SHARPEMU_TRACE", ""]
|
||||
}
|
||||
""";
|
||||
|
||||
var settings = GuiSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.Equal(["C:\\Games"], settings.GameFolders);
|
||||
Assert.Empty(settings.ExcludedGames);
|
||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromJson_EmptyObject_UsesConstructorDefaults()
|
||||
{
|
||||
var settings = GuiSettings.NormalizeFromJson("{}");
|
||||
|
||||
Assert.Equal("Info", settings.LogLevel);
|
||||
Assert.Equal("en", settings.Language);
|
||||
Assert.Equal("1525606762248540221", settings.DiscordClientId);
|
||||
Assert.Empty(settings.GameFolders);
|
||||
Assert.Empty(settings.ExcludedGames);
|
||||
Assert.Empty(settings.EnvironmentToggles);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.GUI;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.GUI;
|
||||
|
||||
public sealed class PerGameSettingsTests
|
||||
{
|
||||
// Invalid entries must not reach Environment.SetEnvironmentVariable.
|
||||
[Fact]
|
||||
public void NormalizeFromJson_NullOrEmptyToggleEntries_AreFilteredOut()
|
||||
{
|
||||
const string json = """
|
||||
{ "EnvironmentToggles": [null, "SHARPEMU_TRACE", ""] }
|
||||
""";
|
||||
|
||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.NotNull(settings);
|
||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
||||
}
|
||||
|
||||
// A null list means that the global setting should be inherited.
|
||||
[Fact]
|
||||
public void NormalizeFromJson_NullToggleList_StaysNull()
|
||||
{
|
||||
const string json = """{ "EnvironmentToggles": null }""";
|
||||
|
||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.NotNull(settings);
|
||||
Assert.Null(settings.EnvironmentToggles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromJson_EmptyToggleList_StaysEmpty()
|
||||
{
|
||||
const string json = """{ "EnvironmentToggles": [] }""";
|
||||
|
||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.NotNull(settings);
|
||||
Assert.Empty(Assert.IsType<List<string>>(settings.EnvironmentToggles));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromJson_ValidToggles_ArePreserved()
|
||||
{
|
||||
const string json = """
|
||||
{ "EnvironmentToggles": ["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"] }
|
||||
""";
|
||||
|
||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
||||
|
||||
Assert.NotNull(settings);
|
||||
Assert.Equal(["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"], settings.EnvironmentToggles);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.HLE;
|
||||
|
||||
public sealed class GuestWriteWatchTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0x0000001000000001, "torn")]
|
||||
[InlineData(0x0000FFFF00000001, "torn")]
|
||||
[InlineData(0x0000000008000000, "shift")]
|
||||
[InlineData(0x0000000080015F00, "shift")]
|
||||
[InlineData(0x0000000007FFFFFF, null)]
|
||||
[InlineData(0x0000000009000000, null)]
|
||||
[InlineData(0x000000003F800000, null)]
|
||||
[InlineData(0x0000000080015F01, null)]
|
||||
[InlineData(0x0001000000000001, null)]
|
||||
public void ClassifyBulkValue_RecognizesCorruptionSignatures(ulong value, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, GuestWriteWatch.ClassifyBulkValue(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(1, 7)]
|
||||
[InlineData(3, 5)]
|
||||
[InlineData(7, 1)]
|
||||
[InlineData(8, 0)]
|
||||
public void FirstAlignedOffset_ReturnsTheNextEightByteBoundary(ulong address, int expected)
|
||||
{
|
||||
Assert.Equal(expected, GuestWriteWatch.FirstAlignedOffset(address));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x1000, 8, 0x1000, true)]
|
||||
[InlineData(0x0FFF, 1, 0x1000, false)]
|
||||
[InlineData(0x0FFF, 2, 0x1000, true)]
|
||||
[InlineData(0x1008, 1, 0x1000, false)]
|
||||
[InlineData(ulong.MaxValue - 3, 4, ulong.MaxValue - 1, true)]
|
||||
[InlineData(ulong.MaxValue, 1, ulong.MaxValue, true)]
|
||||
[InlineData(0x1000, 0, 0x1000, false)]
|
||||
public void Overlaps_HandlesBoundariesWithoutOverflow(
|
||||
ulong address,
|
||||
int length,
|
||||
ulong slot,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, GuestWriteWatch.Overlaps(address, length, slot));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x10000, 0xF2, true)]
|
||||
[InlineData(0x10001, 0xF2, false)]
|
||||
[InlineData(0x10000, 0xF1, false)]
|
||||
public void IsPoolMapping_RequiresTheExpectedSizeAndProtection(
|
||||
ulong length,
|
||||
int protection,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, GuestWriteWatch.IsPoolMapping(length, protection));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 0)]
|
||||
[InlineData("", 0)]
|
||||
[InlineData("not-hex", 0)]
|
||||
[InlineData("80", 0x80)]
|
||||
[InlineData("0x80", 0x80)]
|
||||
[InlineData(" 0X801DB3BBB ", 0x801DB3BBB)]
|
||||
public void Parse_HandlesHexadecimalWatchValues(string? text, ulong expected)
|
||||
{
|
||||
Assert.Equal(expected, GuestWriteWatch.Parse(text));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user