mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 05:30:57 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f63204df75 |
@@ -89,6 +89,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -120,13 +121,24 @@ jobs:
|
|||||||
- name: Publish win-x64 CLI
|
- name: Publish win-x64 CLI
|
||||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|
||||||
|
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||||
|
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||||
|
if (Test-Path $archivePath) {
|
||||||
|
Remove-Item $archivePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
build-posix:
|
build-posix:
|
||||||
name: Build ${{ matrix.rid }}
|
name: Build ${{ matrix.rid }}
|
||||||
@@ -146,6 +158,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||||
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
||||||
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
||||||
SPIRV_TARGET_ENV: vulkan1.2
|
SPIRV_TARGET_ENV: vulkan1.2
|
||||||
@@ -210,13 +223,19 @@ jobs:
|
|||||||
if: matrix.rid == 'osx-x64'
|
if: matrix.rid == 'osx-x64'
|
||||||
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
# tar keeps the executable bit, which zip would drop.
|
||||||
|
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||||
|
-C "$PUBLISH_DIR" .
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
@@ -236,28 +255,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Package release assets
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
|
|
||||||
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
|
|
||||||
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
|
|
||||||
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
|
|
||||||
test -d "${package_dir}"
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p release-assets
|
|
||||||
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
|
|
||||||
|
|
||||||
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -267,9 +264,9 @@ jobs:
|
|||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
if [ "${#assets[@]}" -ne 3 ]; then
|
if [ "${#assets[@]}" -eq 0 ]; then
|
||||||
echo "Expected 3 release assets, found ${#assets[@]}." >&2
|
echo "No release assets found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ ehthumbs.db
|
|||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
|
||||||
@@ -9,18 +9,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|
||||||
<_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>
|
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
<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.Desktop" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" 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="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ path = [
|
|||||||
"**/packages.lock.json",
|
"**/packages.lock.json",
|
||||||
"scripts/ps5_names.txt",
|
"scripts/ps5_names.txt",
|
||||||
"src/SharpEmu.GUI/Languages/**",
|
"src/SharpEmu.GUI/Languages/**",
|
||||||
"src/SharpEmu.ShaderCompiler.Metal/Templates/**",
|
|
||||||
"tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/**",
|
|
||||||
"_logs/**",
|
"_logs/**",
|
||||||
".github/images/**",
|
".github/images/**",
|
||||||
".github/pull_request_template.md",
|
".github/pull_request_template.md",
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler.Metal/SharpEmu.ShaderCompiler.Metal.csproj" />
|
|
||||||
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||||
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||||
<Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
|
|
||||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB |
@@ -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
|
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||||
decoder cannot observe or replace those frames.
|
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.
|
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||||
This preserves the game's own timing and lets the host Vulkan presenter display
|
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||||
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||||
|
|
||||||
The default path decodes by calling FFmpeg's own C API directly from managed
|
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||||
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
|
not-found so games that mark cinematics as optional progress to their next
|
||||||
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
|
state instead of waiting on an empty Bink GPU texture.
|
||||||
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.
|
|
||||||
|
|
||||||
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||||
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||||
only; it does not decode the movie or alter its game logic.
|
only; it does not decode the movie or alter its game logic. Set
|
||||||
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
|
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||||
being explicit about it.
|
|
||||||
|
|
||||||
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override is unrelated to the
|
## Supplying the adapter
|
||||||
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 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`
|
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||||
(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
|
executable, or point to it explicitly:
|
||||||
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.
|
|
||||||
|
|
||||||
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||||
machine's own RID (see `Directory.Build.props`), so it fetches the matching
|
./SharpEmu /path/to/eboot.bin
|
||||||
`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.
|
|
||||||
|
|
||||||
To use a different set of FFmpeg libraries, drop them into the published
|
The expected exports are sharpemu_bink2_open_utf8,
|
||||||
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||||
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||||
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
frame. The managed side validates dimensions and retains ownership of the
|
||||||
folder and does not otherwise care where the files came from.
|
destination buffer.
|
||||||
|
|
||||||
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||||
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
|
and retains the existing guest rendering path.
|
||||||
could not open movie ...") and leaves the guest's own rendering path
|
|
||||||
untouched, rather than crashing.
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Guest write watch
|
|
||||||
|
|
||||||
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
|
|
||||||
code and HLE code that damage guest memory. The tool starts only if you set one
|
|
||||||
or more `SHARPEMU_WATCH_*` environment variables.
|
|
||||||
|
|
||||||
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
|
|
||||||
does not monitor stores that native guest code makes directly. Use a platform
|
|
||||||
debugger or a hardware watchpoint to monitor these stores.
|
|
||||||
|
|
||||||
## Watch modes
|
|
||||||
|
|
||||||
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
|
|
||||||
block at the specified guest address.
|
|
||||||
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
|
|
||||||
monitors the first 64 direct mappings that have a size of 64 KiB and
|
|
||||||
protection value `0xF2`.
|
|
||||||
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
|
|
||||||
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
|
|
||||||
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
|
|
||||||
memory range. The tool logs a maximum of 128 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
|
|
||||||
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
|
|
||||||
logs a maximum of 64 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
|
|
||||||
specified upper 32 bits in the destination address.
|
|
||||||
|
|
||||||
For each match, the tool logs the destination address, the data pattern, and the
|
|
||||||
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
|
|
||||||
tag.
|
|
||||||
|
|
||||||
Use these variables together to scan bulk writes in the
|
|
||||||
`0x00000080xxxxxxxx` region.
|
|
||||||
|
|
||||||
macOS and Linux:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
SHARPEMU_WATCH_BULK_TORN=1 \
|
|
||||||
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
|
|
||||||
SharpEmu /path/to/eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:SHARPEMU_WATCH_BULK_TORN = "1"
|
|
||||||
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
|
|
||||||
& .\SharpEmu.exe C:\path\to\game\eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
|
|
||||||
address from a crash dump.
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "disable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -45,6 +45,11 @@ internal static partial class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// Avoid blocking full collections while guest and render threads are
|
||||||
|
// running, and establish the GC mode before the runtime reserves the
|
||||||
|
// fixed guest address-space window.
|
||||||
|
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Run(args);
|
return Run(args);
|
||||||
@@ -64,7 +69,6 @@ internal static partial class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
PreloadGlfw();
|
|
||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -214,27 +218,6 @@ internal static partial class Program
|
|||||||
"as libvulkan.1.dylib.");
|
"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)
|
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
@@ -629,7 +612,7 @@ internal static partial class Program
|
|||||||
nint jobHandle = 0;
|
nint jobHandle = 0;
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
var created = CreateProcessW(
|
var created = CreateProcessW(
|
||||||
null,
|
processPath,
|
||||||
cmdLineBuilder,
|
cmdLineBuilder,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -1455,7 +1438,7 @@ internal static partial class Program
|
|||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(
|
private static extern bool CreateProcessW(
|
||||||
string? applicationName,
|
string applicationName,
|
||||||
StringBuilder commandLine,
|
StringBuilder commandLine,
|
||||||
nint processAttributes,
|
nint processAttributes,
|
||||||
nint threadAttributes,
|
nint threadAttributes,
|
||||||
|
|||||||
@@ -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
|
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||||
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
<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>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
@@ -54,6 +49,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||||
@@ -79,75 +75,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Native libraries (glfw, FFmpeg) publish into a subfolder next to the
|
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||||
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
|
|
||||||
library is embedded into the single-file bundle. -->
|
library is embedded into the single-file bundle. -->
|
||||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
<_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 Remove="@(_GlfwPublishFiles)" />
|
||||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
<RelativePath>$(NativeLibraryFolderName)/%(Filename)%(Extension)</RelativePath>
|
|
||||||
</ResolvedFileToPublish>
|
</ResolvedFileToPublish>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</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>
|
</Project>
|
||||||
|
|||||||
@@ -13,9 +13,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
</application>
|
</application>
|
||||||
</compatibility>
|
</compatibility>
|
||||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<asmv3:windowsSettings>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@@ -0,0 +1,596 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.3, )",
|
||||||
|
"resolved": "10.0.3",
|
||||||
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
|
},
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.BuildServices": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.2",
|
||||||
|
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||||
|
},
|
||||||
|
"Avalonia.FreeDesktop": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Tmds.DBus.Protocol": "0.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Remote.Protocol": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||||
|
},
|
||||||
|
"Avalonia.Skia": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"HarfBuzzSharp": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||||
|
"SkiaSharp": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.X11": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.FreeDesktop": "11.3.18",
|
||||||
|
"Avalonia.Skia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"MicroCom.Runtime": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "0.11.0",
|
||||||
|
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.6",
|
||||||
|
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "9.0.9",
|
||||||
|
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||||
|
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Ultz.Native.GLFW": "3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Input.Common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Input.Glfw": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Input.Common": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Maths": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Maths": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing.Glfw": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.GLFW": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
|
"sharpemu.core": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Iced": "[1.21.0, )",
|
||||||
|
"SharpEmu.HLE": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.Libs": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.Logging": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.debugger": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Core": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.HLE": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.Logging": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.gui": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "[11.3.18, )",
|
||||||
|
"Avalonia.Desktop": "[11.3.18, )",
|
||||||
|
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||||
|
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||||
|
"SharpEmu.Core": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.Libs": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.Logging": "[0.0.2-beta.3, )",
|
||||||
|
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.libs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )",
|
||||||
|
"SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.3, )",
|
||||||
|
"Silk.NET.Input": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"sharpemu.shadercompiler": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.shadercompiler.vulkan": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia.BuildServices": "11.3.2",
|
||||||
|
"Avalonia.Remote.Protocol": "11.3.18",
|
||||||
|
"MicroCom.Runtime": "0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Desktop": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.Native": "11.3.18",
|
||||||
|
"Avalonia.Skia": "11.3.18",
|
||||||
|
"Avalonia.Win32": "11.3.18",
|
||||||
|
"Avalonia.X11": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Fonts.Inter": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Themes.Fluent": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Iced": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
|
"Silk.NET.Input": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Input.Common": "2.23.0",
|
||||||
|
"Silk.NET.Input.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Windowing": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tmds.DBus.Protocol": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[0.21.3, )",
|
||||||
|
"resolved": "0.21.3",
|
||||||
|
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/linux-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/osx-arm64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/osx-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/win-x64": {
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Emulation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
|
|
||||||
/// (immediate-form) instructions.
|
|
||||||
///
|
|
||||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
|
|
||||||
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
|
|
||||||
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
|
|
||||||
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
|
|
||||||
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
|
|
||||||
/// a different register allocation, a title built with a different compiler version, and so on
|
|
||||||
/// - still aborts the title. This class ported from Kyty's
|
|
||||||
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
|
|
||||||
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
|
|
||||||
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
|
|
||||||
///
|
|
||||||
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
|
|
||||||
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
|
|
||||||
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
|
|
||||||
/// </summary>
|
|
||||||
public static class Sse4aBitFieldEmulator
|
|
||||||
{
|
|
||||||
public static bool IsValidBitField(int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong ExtractBitField(ulong value, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
return (value >> idx) & mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
var destinationClearMask = fieldMask << idx;
|
|
||||||
var sourceField = (source & fieldMask) << idx;
|
|
||||||
return (destination & ~destinationClearMask) | sourceField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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;
|
return -1;
|
||||||
}
|
}
|
||||||
if (exceptionCode == StatusIllegalInstruction &&
|
|
||||||
TryRecoverAmdCompatInstruction(contextRecord, rip))
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (IsBenignHostDebugException(exceptionCode))
|
if (IsBenignHostDebugException(exceptionCode))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
@@ -483,7 +478,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (count <= 16 || count % 65536 == 0)
|
if (count <= 16 || count % 65536 == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
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();
|
Console.Error.Flush();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -530,12 +530,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
if (dispatchResolved &&
|
if (dispatchResolved &&
|
||||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||||
@@ -1329,12 +1326,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
|
|
||||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -1404,13 +1398,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||||
"4fgtGfXDrFc" or // sceAmprMeasureCommandSizeWriteAddress_04_00
|
|
||||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||||
"gzndltBEzWc" or // sceAmprCommandBufferGetNumCommands
|
"gzndltBEzWc" or // sceAmprCommandBufferGetNumCommands
|
||||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||||
"j0+3uJMxYJY" or // sceAmprCommandBufferWriteAddress_04_00
|
|
||||||
"mPpPxv5CZt4" or // sceSystemServiceGetHdrToneMapLuminance
|
"mPpPxv5CZt4" or // sceSystemServiceGetHdrToneMapLuminance
|
||||||
"1FZBKy8HeNU" or // sceVideoOutGetVblankStatus
|
"1FZBKy8HeNU" or // sceVideoOutGetVblankStatus
|
||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
@@ -1418,8 +1410,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Q2V+iqvjgC0" or // vsnprintf
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
"AV6ipCNa4Rw" or // strcasecmp
|
|
||||||
"viiwFMaNamA" or // strstr
|
|
||||||
"q1cHNfGycLI" or // scePadRead
|
"q1cHNfGycLI" or // scePadRead
|
||||||
"xk0AcarP3V4" or // scePadOpen
|
"xk0AcarP3V4" or // scePadOpen
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||||
@@ -1446,9 +1436,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
var expectedSemaphoreTrywaitAgain =
|
|
||||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
||||||
var expectedNetAcceptWouldBlock =
|
var expectedNetAcceptWouldBlock =
|
||||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||||
resultValue == unchecked((int)0x80410123);
|
resultValue == unchecked((int)0x80410123);
|
||||||
@@ -1462,7 +1449,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
!expectedTimedWaitTimeout &&
|
!expectedTimedWaitTimeout &&
|
||||||
!expectedEqueueTimeout &&
|
!expectedEqueueTimeout &&
|
||||||
!expectedMutexTrylockBusy &&
|
!expectedMutexTrylockBusy &&
|
||||||
!expectedSemaphoreTrywaitAgain &&
|
|
||||||
!expectedNetAcceptWouldBlock &&
|
!expectedNetAcceptWouldBlock &&
|
||||||
!expectedUserServiceNoEvent &&
|
!expectedUserServiceNoEvent &&
|
||||||
!expectedPrivacyInvalidParameter)
|
!expectedPrivacyInvalidParameter)
|
||||||
@@ -1556,13 +1542,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"vWU-odnS+fU" or
|
"vWU-odnS+fU" or
|
||||||
"sSAUCCU1dv4" or
|
"sSAUCCU1dv4" or
|
||||||
"C+IEj+BsAFM" or
|
"C+IEj+BsAFM" or
|
||||||
"4fgtGfXDrFc" or
|
|
||||||
"tZDDEo2tE5k" or
|
"tZDDEo2tE5k" or
|
||||||
"GnxKOHEawhk" or
|
"GnxKOHEawhk" or
|
||||||
"gzndltBEzWc" or
|
"gzndltBEzWc" or
|
||||||
"H896Pt-yB4I" or
|
"H896Pt-yB4I" or
|
||||||
"sJXyWHjP-F8" or
|
"sJXyWHjP-F8" or
|
||||||
"j0+3uJMxYJY" or
|
|
||||||
"mPpPxv5CZt4" or
|
"mPpPxv5CZt4" or
|
||||||
"1FZBKy8HeNU" or
|
"1FZBKy8HeNU" or
|
||||||
"ASoW5WE-UPo" or
|
"ASoW5WE-UPo" or
|
||||||
@@ -1587,8 +1571,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
"AV6ipCNa4Rw" or // strcasecmp
|
|
||||||
"viiwFMaNamA" or // strstr
|
|
||||||
"pNtJdE3x49E" or // wcscmp
|
"pNtJdE3x49E" or // wcscmp
|
||||||
"fV2xHER+bKE" or // wcscoll
|
"fV2xHER+bKE" or // wcscoll
|
||||||
"E8wCoUEbfzk" or // wcsncmp
|
"E8wCoUEbfzk" or // wcsncmp
|
||||||
|
|||||||
@@ -50,19 +50,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
private const int LinuxUcontextGregsOffset = 40;
|
private const int LinuxUcontextGregsOffset = 40;
|
||||||
private const int LinuxGregsErrOffset = 19 * 8;
|
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,
|
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||||
@@ -84,15 +71,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static int _posixSignalHandlerDepth;
|
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()
|
private void SetupPosixExceptionHandler()
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
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]));
|
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;
|
EXCEPTION_RECORD record = default;
|
||||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
if (signal == PosixSigIll)
|
if (signal == PosixSigIll)
|
||||||
@@ -359,14 +317,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||||
}
|
}
|
||||||
if (fpstate != null)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(
|
|
||||||
contextRecord + Win64ContextXmm0Offset,
|
|
||||||
fpstate + FxsaveXmmOffset,
|
|
||||||
XmmBlockSize,
|
|
||||||
XmmBlockSize);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -712,11 +712,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
||||||
|
|
||||||
// Import dispatch is the hottest managed path in UE titles. Most imports do
|
|
||||||
// not have an exception queued, so publish the dictionary population and let
|
|
||||||
// safe points skip _guestThreadGate entirely in the common case.
|
|
||||||
private int _pendingGuestExceptionCount;
|
|
||||||
|
|
||||||
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
||||||
|
|
||||||
private int _guestThreadPumpDepth;
|
private int _guestThreadPumpDepth;
|
||||||
@@ -1123,9 +1118,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_logStrlenBursts = _logStrlenImports ||
|
_logStrlenBursts = _logStrlenImports ||
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STRLEN_BURSTS"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STRLEN_BURSTS"), "1", StringComparison.Ordinal);
|
||||||
_logGuestContext = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_CONTEXT"), "1", StringComparison.Ordinal);
|
_logGuestContext = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_CONTEXT"), "1", StringComparison.Ordinal);
|
||||||
var ignoreGuestInt41Env = Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_INT41");
|
_ignoreGuestInt41 = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_INT41"), "1", StringComparison.Ordinal);
|
||||||
_ignoreGuestInt41 = !string.Equals(ignoreGuestInt41Env, "0", StringComparison.Ordinal) &&
|
|
||||||
!string.Equals(ignoreGuestInt41Env, "false", StringComparison.OrdinalIgnoreCase);
|
|
||||||
_ignoredGuestInt41Count = 0;
|
_ignoredGuestInt41Count = 0;
|
||||||
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
||||||
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
||||||
@@ -1298,12 +1291,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address)
|
private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address)
|
||||||
{
|
{
|
||||||
if (IsHlePreferredNid(nid))
|
|
||||||
{
|
|
||||||
address = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nid == "1jfXLRVzisc" &&
|
if (nid == "1jfXLRVzisc" &&
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -1415,54 +1402,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
0x75, 0xE7,
|
0x75, 0xE7,
|
||||||
0xC3,
|
0xC3,
|
||||||
],
|
],
|
||||||
"AV6ipCNa4Rw" =>
|
|
||||||
[
|
|
||||||
0x0F, 0xB6, 0x07,
|
|
||||||
0x0F, 0xB6, 0x16,
|
|
||||||
0x8D, 0x48, 0xBF,
|
|
||||||
0x83, 0xF9, 0x19,
|
|
||||||
0x77, 0x03,
|
|
||||||
0x83, 0xC0, 0x20,
|
|
||||||
0x8D, 0x4A, 0xBF,
|
|
||||||
0x83, 0xF9, 0x19,
|
|
||||||
0x77, 0x03,
|
|
||||||
0x83, 0xC2, 0x20,
|
|
||||||
0x29, 0xD0,
|
|
||||||
0x75, 0x0C,
|
|
||||||
0x85, 0xD2,
|
|
||||||
0x74, 0x08,
|
|
||||||
0x48, 0xFF, 0xC7,
|
|
||||||
0x48, 0xFF, 0xC6,
|
|
||||||
0xEB, 0xD4,
|
|
||||||
0xC3,
|
|
||||||
],
|
|
||||||
"viiwFMaNamA" =>
|
|
||||||
[
|
|
||||||
0x0F, 0xB6, 0x16,
|
|
||||||
0x84, 0xD2,
|
|
||||||
0x74, 0x2D,
|
|
||||||
0x0F, 0xB6, 0x07,
|
|
||||||
0x84, 0xC0,
|
|
||||||
0x74, 0x2A,
|
|
||||||
0x38, 0xD0,
|
|
||||||
0x75, 0x1D,
|
|
||||||
0x4C, 0x8D, 0x47, 0x01,
|
|
||||||
0x4C, 0x8D, 0x4E, 0x01,
|
|
||||||
0x41, 0x0F, 0xB6, 0x09,
|
|
||||||
0x84, 0xC9,
|
|
||||||
0x74, 0x12,
|
|
||||||
0x41, 0x38, 0x08,
|
|
||||||
0x75, 0x08,
|
|
||||||
0x49, 0xFF, 0xC0,
|
|
||||||
0x49, 0xFF, 0xC1,
|
|
||||||
0xEB, 0xEB,
|
|
||||||
0x48, 0xFF, 0xC7,
|
|
||||||
0xEB, 0xD3,
|
|
||||||
0x48, 0x89, 0xF8,
|
|
||||||
0xC3,
|
|
||||||
0x31, 0xC0,
|
|
||||||
0xC3,
|
|
||||||
],
|
|
||||||
"pNtJdE3x49E" or "fV2xHER+bKE" =>
|
"pNtJdE3x49E" or "fV2xHER+bKE" =>
|
||||||
[
|
[
|
||||||
0x0F, 0xB7, 0x07,
|
0x0F, 0xB7, 0x07,
|
||||||
@@ -1527,14 +1466,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
"Q3VBxCXhUHs" =>
|
"Q3VBxCXhUHs" =>
|
||||||
[
|
[
|
||||||
0x48, 0x89, 0xF8,
|
0x48, 0x89, 0xF8,
|
||||||
0x48, 0x85, 0xD2,
|
0x48, 0x89, 0xD1,
|
||||||
0x74, 0x11,
|
0xF3, 0xA4,
|
||||||
0x44, 0x8A, 0x06,
|
|
||||||
0x44, 0x88, 0x07,
|
|
||||||
0x48, 0xFF, 0xC6,
|
|
||||||
0x48, 0xFF, 0xC7,
|
|
||||||
0x48, 0xFF, 0xCA,
|
|
||||||
0x75, 0xEF,
|
|
||||||
0xC3,
|
0xC3,
|
||||||
],
|
],
|
||||||
"8zTFvBIAIN8" =>
|
"8zTFvBIAIN8" =>
|
||||||
@@ -1668,8 +1601,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private static bool IsHlePreferredNid(string nid)
|
private static bool IsHlePreferredNid(string nid)
|
||||||
{
|
{
|
||||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal);
|
||||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLibcLibrary(string libraryName)
|
private static bool IsLibcLibrary(string libraryName)
|
||||||
@@ -2609,22 +2541,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
||||||
{
|
{
|
||||||
// Rosetta does not implement AMD SSE4a EXTRQ. Recognize the compiler's
|
// Rosetta does not implement AMD SSE4a EXTRQ. This exact sequence masks
|
||||||
// EXTRQ+blend idiom (against whichever xmm0-xmm7 it allocated) and rewrite
|
// xmm2 to its low 40 bits, then copies the resulting second dword into
|
||||||
// it into an equivalent SSE4.1 sequence. Match/encode is isolated in
|
// xmm0. PEXTRB/PINSRD provides the same observable result in 12 bytes:
|
||||||
// Sse4aExtrqBlendPatch so it can be unit-tested; here we only patch bytes.
|
// extract source byte 4 and insert the zero-extended value into lane 1.
|
||||||
var window = new ReadOnlySpan<byte>(source, Sse4aExtrqBlendPatch.SequenceLength);
|
ReadOnlySpan<byte> pattern =
|
||||||
if (!Sse4aExtrqBlendPatch.TryMatch(window, out var destRegister, out var srcRegister))
|
[
|
||||||
|
0x66, 0x0F, 0x78, 0xC2, 0x28, 0x00,
|
||||||
|
0xC4, 0xE3, 0x79, 0x02, 0xC2, 0x02,
|
||||||
|
];
|
||||||
|
for (var i = 0; i < pattern.Length; i++)
|
||||||
{
|
{
|
||||||
return false;
|
if (source[i] != pattern[i])
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
Span<byte> replacement = stackalloc byte[Sse4aExtrqBlendPatch.SequenceLength];
|
}
|
||||||
if (!Sse4aExtrqBlendPatch.TryEncode(destRegister, srcRegister, replacement))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> replacement =
|
||||||
|
[
|
||||||
|
0x66, 0x0F, 0x3A, 0x14, 0xD0, 0x04,
|
||||||
|
0x66, 0x0F, 0x3A, 0x22, 0xC0, 0x01,
|
||||||
|
];
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect))
|
if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect))
|
||||||
{
|
{
|
||||||
@@ -3955,10 +3893,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// unwinding. Unity can begin its next stop-the-world cycle in
|
// unwinding. Unity can begin its next stop-the-world cycle in
|
||||||
// that window; treating the new raise as part of the old delivery
|
// that window; treating the new raise as part of the old delivery
|
||||||
// strands the collector waiting for an acknowledgement.
|
// strands the collector waiting for an acknowledgement.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3967,10 +3905,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// managed thread corrupts the worker's control state. Queue the
|
// managed thread corrupts the worker's control state. Queue the
|
||||||
// request and let that exact executor consume it at its next HLE
|
// request and let that exact executor consume it at its next HLE
|
||||||
// boundary, where the original guest thread is safely paused.
|
// boundary, where the original guest thread is safely paused.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4015,17 +3953,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
if (target.ExceptionDeliveryActive)
|
if (target.ExceptionDeliveryActive)
|
||||||
{
|
{
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4186,7 +4124,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
RestoreInterruptedGuestThread();
|
RestoreInterruptedGuestThread();
|
||||||
if (target.State == GuestThreadRunState.Blocked &&
|
if (target.State == GuestThreadRunState.Blocked &&
|
||||||
!target.ExecutorActive &&
|
!target.ExecutorActive &&
|
||||||
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
|
_pendingGuestExceptions.Remove(threadHandle, out var queued))
|
||||||
{
|
{
|
||||||
followUp = queued;
|
followUp = queued;
|
||||||
}
|
}
|
||||||
@@ -4272,11 +4210,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
CpuContext currentContext,
|
CpuContext currentContext,
|
||||||
GuestCpuContinuation interruptedContinuation)
|
GuestCpuContinuation interruptedContinuation)
|
||||||
{
|
{
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
if (threadHandle == 0)
|
if (threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -4290,7 +4223,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
|
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4352,27 +4285,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueuePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
PendingGuestException pending)
|
|
||||||
{
|
|
||||||
_pendingGuestExceptions[threadHandle] = pending;
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryRemovePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
out PendingGuestException pending)
|
|
||||||
{
|
|
||||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteGuestExceptionContext(
|
private static bool TryWriteGuestExceptionContext(
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -4467,7 +4379,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_guestThreads.Clear();
|
_guestThreads.Clear();
|
||||||
_externalGuestThreads.Clear();
|
_externalGuestThreads.Clear();
|
||||||
_pendingGuestExceptions.Clear();
|
_pendingGuestExceptions.Clear();
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
|
||||||
_activeGuestExceptionDeliveries.Clear();
|
_activeGuestExceptionDeliveries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
|
|||||||
var pattern = TlsAccessPattern;
|
var pattern = TlsAccessPattern;
|
||||||
var end = start + length - pattern.Length;
|
var end = start + length - pattern.Length;
|
||||||
|
|
||||||
for (var ptr = start; ptr <= end; ptr++)
|
for (var ptr = start; ptr < end; ptr++)
|
||||||
{
|
{
|
||||||
if (MatchesPattern(ptr, pattern))
|
if (MatchesPattern(ptr, pattern))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Recognizes Sony's AMD-only SSE4a EXTRQ+blend idiom and rewrites it into an
|
|
||||||
/// equivalent SSE4.1 sequence. SharpEmu executes guest x86-64 natively, but
|
|
||||||
/// Rosetta 2 and Intel hosts do not implement SSE4a, so the original opcode
|
|
||||||
/// raises #UD -> SIGILL. The compiler emits the idiom against whichever XMM
|
|
||||||
/// register it happens to allocate (Dead Cells uses xmm1, others xmm2), so the
|
|
||||||
/// source register is read from the ModRM r/m field rather than hard-coded.
|
|
||||||
///
|
|
||||||
/// The match/encode logic is deliberately free of native page-patching so it
|
|
||||||
/// can be unit-tested against handcrafted byte sequences.
|
|
||||||
/// </summary>
|
|
||||||
public static class Sse4aExtrqBlendPatch
|
|
||||||
{
|
|
||||||
/// <summary>Length in bytes of both the matched idiom and its replacement.</summary>
|
|
||||||
public const int SequenceLength = 12;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Matches the 12-byte idiom, extracting the destination register D and the
|
|
||||||
/// source (scratch) register N:
|
|
||||||
/// <code>
|
|
||||||
/// EXTRQ xmmN, 0x28, 0x00 ; 66 0F 78 /0 28 00 mask xmmN to low 40 bits
|
|
||||||
/// VPBLENDD xmmD, xmmD, xmmN, 2 ; C4 E3 vvvv 02 /r 02 copy dword 1 into xmmD
|
|
||||||
/// </code>
|
|
||||||
/// N lives in the ModRM r/m field of both instructions; D (the blend
|
|
||||||
/// destination and src1) lives in the VPBLENDD ModRM reg field and VEX.vvvv.
|
|
||||||
/// Both are xmm0-xmm7 (the VEX byte1 0xE3 pins R/X/B, so no xmm8-15 extension).
|
|
||||||
/// The compiler allocates whichever registers it likes — Dead Cells builds use
|
|
||||||
/// D=xmm0 and D=xmm3, others differ — so both are read from the encoding.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryMatch(ReadOnlySpan<byte> source, out int destRegister, out int srcRegister)
|
|
||||||
{
|
|
||||||
destRegister = -1;
|
|
||||||
srcRegister = -1;
|
|
||||||
if (source.Length < SequenceLength)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXTRQ xmmN, 0x28, 0x00 : 66 0F 78, ModRM (mod=11 reg=000 rm=N), 28, 00.
|
|
||||||
if (source[0] != 0x66 || source[1] != 0x0F || source[2] != 0x78 ||
|
|
||||||
(source[3] & 0xF8) != 0xC0 || source[4] != 0x28 || source[5] != 0x00)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var n = source[3] & 0x07;
|
|
||||||
|
|
||||||
// VPBLENDD xmmD, xmmD, xmmN, 2 : C4 E3 <W=0 vvvv=~D L=0 pp=01> 02 ModRM 02.
|
|
||||||
// VEX.byte2 fixed bits (W, L, pp) must read 0b*0000*01; vvvv encodes ~D.
|
|
||||||
if (source[6] != 0xC4 || source[7] != 0xE3 || (source[8] & 0x87) != 0x01 ||
|
|
||||||
source[9] != 0x02 || source[11] != 0x02)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = (~(source[8] >> 3)) & 0x0F;
|
|
||||||
if (d > 7)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModRM: mod=11, reg=D (dest = src1), rm=N (src2 = the masked register).
|
|
||||||
if (source[10] != (0xC0 | (d << 3) | n))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destRegister = d;
|
|
||||||
srcRegister = n;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the SSE4.1 equivalent into <paramref name="destination"/>:
|
|
||||||
/// <code>
|
|
||||||
/// PEXTRB eax, xmmN, 4 ; 66 0F 3A 14 /r 04 extract byte 4 (zero-extended)
|
|
||||||
/// PINSRD xmmD, eax, 1 ; 66 0F 3A 22 /r 01 insert into xmmD dword lane 1
|
|
||||||
/// </code>
|
|
||||||
/// After EXTRQ masks xmmN to its low 40 bits, dword 1 is just byte 4
|
|
||||||
/// zero-extended, so the two-instruction extract/insert reproduces the exact
|
|
||||||
/// observable result the AMD idiom left in xmmD. eax is a caller-dead scratch
|
|
||||||
/// at every site the compiler emits this idiom.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryEncode(int destRegister, int srcRegister, Span<byte> destination)
|
|
||||||
{
|
|
||||||
if ((uint)destRegister > 7 || (uint)srcRegister > 7 || destination.Length < SequenceLength)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PEXTRB eax, xmmN, 4 : ModRM (mod=11 reg=N rm=000 -> eax), imm8 = byte index 4.
|
|
||||||
destination[0] = 0x66;
|
|
||||||
destination[1] = 0x0F;
|
|
||||||
destination[2] = 0x3A;
|
|
||||||
destination[3] = 0x14;
|
|
||||||
destination[4] = (byte)(0xC0 | (srcRegister << 3));
|
|
||||||
destination[5] = 0x04;
|
|
||||||
|
|
||||||
// PINSRD xmmD, eax, 1 : ModRM (mod=11 reg=D -> xmmD, rm=000 -> eax), lane 1.
|
|
||||||
destination[6] = 0x66;
|
|
||||||
destination[7] = 0x0F;
|
|
||||||
destination[8] = 0x3A;
|
|
||||||
destination[9] = 0x22;
|
|
||||||
destination[10] = (byte)(0xC0 | (destRegister << 3));
|
|
||||||
destination[11] = 0x01;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,9 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
|
|
||||||
_inner.TryCopy(destinationAddress, sourceAddress, length);
|
|
||||||
|
|
||||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
{
|
{
|
||||||
if (_inner is IGuestMemoryAllocator allocator)
|
if (_inner is IGuestMemoryAllocator allocator)
|
||||||
|
|||||||
@@ -199,32 +199,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
||||||
{
|
{
|
||||||
// Exact allocation failed — the host may have already claimed
|
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||||
// part of this range (ASLR, Rosetta 2, or another process).
|
throw new InvalidOperationException(
|
||||||
// Try backing the fixed range page by page to claim whatever
|
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}): {reason}.");
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageBase = allocatedBase;
|
imageBase = allocatedBase;
|
||||||
@@ -737,9 +714,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
importedRelocations = BuildImportedRelocations(descriptors);
|
importedRelocations = BuildImportedRelocations(descriptors);
|
||||||
|
|
||||||
var stubEligibleNids = CollectStubEligibleNids(descriptors, moduleManager);
|
|
||||||
var stubImportNids = orderedImportNids
|
var stubImportNids = orderedImportNids
|
||||||
.Where(stubEligibleNids.Contains)
|
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||||
@@ -1184,35 +1160,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
isWeak);
|
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(
|
private static bool ShouldCreateImportStub(
|
||||||
string nid,
|
string nid,
|
||||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||||
@@ -2484,19 +2431,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
Debug.Assert(
|
Debug.Assert(
|
||||||
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||||
"An unresolved weak symbol incorrectly received a trap import stub.");
|
"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)
|
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 DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static CommittedRangeCache? _committedRangeCache;
|
|
||||||
|
|
||||||
private long _mappingGeneration;
|
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
@@ -33,77 +28,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
private const int CommittedRangeCacheCapacity = 4;
|
|
||||||
|
|
||||||
private sealed class CommittedRangeCache
|
|
||||||
{
|
|
||||||
private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity];
|
|
||||||
private PhysicalVirtualMemory? _owner;
|
|
||||||
private long _generation;
|
|
||||||
private int _count;
|
|
||||||
private int _nextReplacement;
|
|
||||||
|
|
||||||
public bool Contains(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start >= range.Start && end <= range.End)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
_generation = generation;
|
|
||||||
_count = 0;
|
|
||||||
_nextReplacement = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start <= range.End && end >= range.Start)
|
|
||||||
{
|
|
||||||
_ranges[index] = new CommittedRange(
|
|
||||||
Math.Min(start, range.Start),
|
|
||||||
Math.Max(end, range.End));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_count < _ranges.Length)
|
|
||||||
{
|
|
||||||
_ranges[_count++] = new CommittedRange(start, end);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ranges[_nextReplacement] = new CommittedRange(start, end);
|
|
||||||
_nextReplacement = (_nextReplacement + 1) % _ranges.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct CommittedRange(ulong Start, ulong End);
|
|
||||||
|
|
||||||
// Raw Windows PAGE_* values retained for the internal region/protection
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
@@ -425,111 +349,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
|
||||||
{
|
|
||||||
if (size == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = AlignDown(address, PageSize);
|
|
||||||
var end = AlignUp(address + size, PageSize);
|
|
||||||
if (end <= start)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
|
||||||
|
|
||||||
// Walk the range page-run by page-run. VirtualQuery reports the largest run
|
|
||||||
// of same-state pages from the queried address, so a single query advances
|
|
||||||
// us over whole free or occupied stretches. Only free stretches get backed;
|
|
||||||
// stretches already reserved or committed by another allocation are left as
|
|
||||||
// they are, which is exactly what a fixed mapping does on hardware.
|
|
||||||
//
|
|
||||||
// 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(
|
public bool TryAllocateAtOrAbove(
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
@@ -621,7 +440,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.ExitWriteLock();
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
_hostMemory.Free(address);
|
_hostMemory.Free(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,7 +611,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
_allocationSearchHints.Clear();
|
_allocationSearchHints.Clear();
|
||||||
}
|
}
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1056,15 +873,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
// A managed write into a page the guest-image write tracker has
|
|
||||||
// protected surfaces as a fatal AccessViolation — the runtime turns
|
|
||||||
// SIGSEGV in managed code into an exception before the resumable
|
|
||||||
// signal bridge can restore access (native guest stores recover
|
|
||||||
// there). Pre-visit the span so tracked pages are unprotected and
|
|
||||||
// their owners dirtied before the copy; guest addresses are
|
|
||||||
// host-identical, matching the tracker's fault addresses.
|
|
||||||
GuestImageWriteTracker.NotifyManagedWrite(virtualAddress, (ulong)source.Length);
|
|
||||||
|
|
||||||
var requiresExclusiveAccess = false;
|
var requiresExclusiveAccess = false;
|
||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
@@ -1102,7 +910,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1128,68 +935,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
|
|
||||||
{
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length)
|
|
||||||
{
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (length > int.MaxValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match TryWrite's managed-write notification before touching an
|
|
||||||
// identity-mapped guest page protected by the image tracker.
|
|
||||||
GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length);
|
|
||||||
|
|
||||||
_gate.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sourceRegion = FindRegion(sourceAddress, length);
|
|
||||||
var destinationRegion = FindRegion(destinationAddress, length);
|
|
||||||
if (sourceRegion is null || destinationRegion is null ||
|
|
||||||
!TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) ||
|
|
||||||
!TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourcePointer = sourceRegion.VirtualAddress + sourceOffset;
|
|
||||||
var destinationPointer = destinationRegion.VirtualAddress + destinationOffset;
|
|
||||||
if ((sourceRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(sourcePointer, length, sourceRegion)) ||
|
|
||||||
(destinationRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(destinationPointer, length, destinationRegion)) ||
|
|
||||||
!CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) ||
|
|
||||||
!CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Span.CopyTo has memmove overlap semantics, so this allocation-free
|
|
||||||
// path safely serves both libc memcpy and libc memmove.
|
|
||||||
new ReadOnlySpan<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
|
|
||||||
new Span<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
NotifyGuestWriteWatch(
|
|
||||||
destinationAddress,
|
|
||||||
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gate.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
@@ -1262,7 +1007,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1287,7 +1031,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1529,12 +1272,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var mappingGeneration = Volatile.Read(ref _mappingGeneration);
|
|
||||||
var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache();
|
|
||||||
if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
@@ -1556,9 +1293,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if (info.State == HostRegionState.Committed)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
// The host query proved this whole range is committed. Retain
|
|
||||||
// that result instead of caching only the caller's small span.
|
|
||||||
CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1574,23 +1308,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(startPage, endPage, mappingGeneration);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
|
|
||||||
{
|
|
||||||
(_committedRangeCache ??= new CommittedRangeCache()).Add(
|
|
||||||
this,
|
|
||||||
mappingGeneration,
|
|
||||||
startPage,
|
|
||||||
endPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryTemporarilyProtectForRead(
|
private bool TryTemporarilyProtectForRead(
|
||||||
ulong address,
|
ulong address,
|
||||||
ulong size,
|
ulong size,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
@@ -94,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyToRegions(virtualAddress, source, regionIndex);
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryValidateRange(
|
private bool TryValidateRange(
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
if (!CreateProcessW(
|
if (!CreateProcessW(
|
||||||
null,
|
exePath,
|
||||||
commandLine,
|
commandLine,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -629,7 +629,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(string? applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
private static extern bool CreateProcessW(string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||||
|
|||||||
@@ -351,13 +351,6 @@ public sealed class GameSurfaceHost : NativeControlHost
|
|||||||
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
||||||
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
||||||
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
||||||
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
|
|
||||||
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
|
|
||||||
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
|
|
||||||
}
|
|
||||||
_surface.UpdatePixelSize(width, height);
|
_surface.UpdatePixelSize(width, height);
|
||||||
|
|
||||||
if (!sizeChanged)
|
if (!sizeChanged)
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||||
public List<string> EnvironmentToggles { get; set; } = new();
|
public List<string> EnvironmentToggles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
|
||||||
public double RenderResolutionScale { get; set; } = 1.0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
@@ -74,7 +71,7 @@ public sealed class GuiSettings
|
|||||||
if (File.Exists(SettingsPath))
|
if (File.Exists(SettingsPath))
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(SettingsPath);
|
var json = File.ReadAllText(SettingsPath);
|
||||||
return NormalizeFromJson(json);
|
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -85,39 +82,6 @@ public sealed class GuiSettings
|
|||||||
return new GuiSettings();
|
return new GuiSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserializes settings and normalizes null references and null or empty list
|
|
||||||
/// entries introduced by JSON. Empty scalar strings remain unchanged.
|
|
||||||
/// </summary>
|
|
||||||
internal static GuiSettings NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
|
||||||
|
|
||||||
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
|
|
||||||
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
|
|
||||||
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
|
|
||||||
settings.LogLevel ??= "Info";
|
|
||||||
settings.Language ??= "en";
|
|
||||||
settings.DiscordClientId ??= "1525606762248540221";
|
|
||||||
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
|
|
||||||
{
|
|
||||||
settings.RenderResolutionScale = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON can populate non-nullable lists with null references and entries.
|
|
||||||
private static List<string> FilterNullOrEmpty(List<string>? source)
|
|
||||||
{
|
|
||||||
if (source is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "ملفات PS التنفيذية",
|
"Dialog.PsExecutables": "ملفات PS التنفيذية",
|
||||||
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
|
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
|
||||||
"Dialog.PlainTextFiles": "ملفات نصية عادية",
|
"Dialog.PlainTextFiles": "ملفات نصية عادية",
|
||||||
"Dialog.LogFiles": "ملفات السجل",
|
"Dialog.LogFiles": "ملفات السجل"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "إعدادات اللعبة…",
|
|
||||||
"Options.Env.Tab": "البيئة",
|
|
||||||
"Options.Section.Environment": "متغيرات البيئة",
|
|
||||||
"Options.Env.Desc": "خيارات تُمرر إلى المحاكي كمتغيرات بيئة عند التشغيل.",
|
|
||||||
"Options.Env.Bthid.Desc": "الإبلاغ عن أن Bluetooth HID غير متاح للألعاب التي تنتظر برمجيات عجلة القيادة/FFB فيها إلى ما لا نهاية.\nاتركه معطلاً عادةً. بعض الألعاب تتجمد عند فشل التهيئة.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "عدم إجبار الألعاب التي تكرر النداء نفسه لفترة طويلة على الإغلاق.\nجرّب هذا عندما تُغلق لعبة نفسها أثناء التحميل.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "السماح للألعاب بإنشاء الملفات والكتابة داخل مجلد التثبيت الخاص بها.\nمطلوب للنسخ غير المحزومة التي تكتب بيانات الحفظ أو الإعدادات تحت /app0.",
|
|
||||||
"Options.Env.VkValidation.Desc": "تفعيل طبقات التحقق في Vulkan لتصحيح أخطاء وحدة معالجة الرسوميات.\nبطيء. يتطلب تثبيت Vulkan SDK.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "تفريغ شيدرات AGC وترجماتها إلى SPIR-V في مجلد shader-dumps.\nاستخدمه عند الإبلاغ عن أخطاء الشيدرات أو العرض.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "تسجيل تخصيصات الذاكرة المباشرة وإخفاقاتها في وحدة التحكم.\nاستخدمه عندما تنهار لعبة أو تُغلق أثناء الإقلاع.",
|
|
||||||
"Options.Env.LogIo.Desc": "تسجيل فتح الملفات وقراءتها وحلّ المسارات في وحدة التحكم.\nاستخدمه عندما لا تجد لعبة ملفات بياناتها أثناء الإقلاع.",
|
|
||||||
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
|
|
||||||
"Common.Save": "حفظ",
|
|
||||||
"Common.Cancel": "إلغاء",
|
|
||||||
"PerGame.Title": "إعدادات خاصة باللعبة — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "الصفوف غير المحددة ترث الإعدادات الافتراضية العامة.",
|
|
||||||
"PerGame.EnvToggles.Label": "مفاتيح البيئة",
|
|
||||||
"PerGame.EnvToggles.Desc": "تجاوز المجموعة العامة من مفاتيح SHARPEMU_* لهذه اللعبة.",
|
|
||||||
"Options.About": "حول",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "الكود المصدري والمشكلات وتطوير المشروع.",
|
|
||||||
"About.Github.LatestCommitLabel": "أحدث Commit",
|
|
||||||
"About.Github.LatestCommitDescription": "أحدث commit على الفرع main",
|
|
||||||
"About.Discord.Label": "دسكورد",
|
|
||||||
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
|
|
||||||
"About.GithubButton": "ساهم على GitHub!",
|
|
||||||
"About.DiscordButton": "انضم إلى دسكوردنا!",
|
|
||||||
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
|
|
||||||
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
|
|
||||||
"Updater.Label": "التحديثات",
|
|
||||||
"Updater.Check": "التحقق من التحديثات",
|
|
||||||
"Updater.DownloadRestart": "تنزيل وإعادة التشغيل",
|
|
||||||
"Updater.Status.Ready": "الإصدار الحالي: {0}",
|
|
||||||
"Updater.Status.Checking": "جارٍ التحقق من التحديثات…",
|
|
||||||
"Updater.Status.Current": "أنت على أحدث إصدار ({0}).",
|
|
||||||
"Updater.Status.Available": "يتوفر إصدار جديد: {0}",
|
|
||||||
"Updater.Status.Downloading": "جارٍ تنزيل التحديث… {0}%",
|
|
||||||
"Updater.Status.Installing": "جارٍ تثبيت التحديث…",
|
|
||||||
"Updater.Status.Timeout": "انتهت مهلة التحقق من التحديثات بعد 10 ثوانٍ.",
|
|
||||||
"Updater.Status.Failed": "تعذر التحقق من التحديثات.",
|
|
||||||
"Updater.Status.ChecksumFailed": "فشل التحديث المنزَّل في اجتياز تحقق SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "يتطلب التحديث التلقائي إصدار x64 لنظام Windows أو Linux أو macOS."
|
|
||||||
}
|
}
|
||||||
@@ -142,32 +142,5 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||||
"About.GithubButton": "Contribua no GitHub!",
|
"About.GithubButton": "Contribua no GitHub!",
|
||||||
"About.DiscordButton": "Entre no nosso Discord!",
|
"About.DiscordButton": "Entre no nosso Discord!"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Configurações do jogo…",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Permite que os jogos criem e gravem arquivos dentro da própria pasta de instalação.\nNecessário para dumps não empacotados que gravam seus saves ou configurações em /app0.",
|
|
||||||
"Options.Env.LogIo.Desc": "Registra no console a abertura e leitura de arquivos e a resolução de caminhos.\nUse quando um jogo não encontrar seus arquivos de dados durante a inicialização.",
|
|
||||||
"Common.Save": "Salvar",
|
|
||||||
"Common.Cancel": "Cancelar",
|
|
||||||
"PerGame.Title": "Configurações por jogo — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "As linhas desmarcadas herdam os padrões globais.",
|
|
||||||
"PerGame.EnvToggles.Label": "Variáveis de ambiente",
|
|
||||||
"PerGame.EnvToggles.Desc": "Substitui o conjunto global de opções SHARPEMU_* para este jogo.",
|
|
||||||
"About.Github.LatestCommitLabel": "Último commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Último commit na branch main",
|
|
||||||
"Updater.Auto.Label": "Verificar atualizações ao iniciar",
|
|
||||||
"Updater.Auto.Desc": "Consulta o GitHub sem atrasar a inicialização.",
|
|
||||||
"Updater.Label": "Atualizações",
|
|
||||||
"Updater.Check": "Verificar atualizações",
|
|
||||||
"Updater.DownloadRestart": "Baixar e reiniciar",
|
|
||||||
"Updater.Status.Ready": "Build atual: {0}",
|
|
||||||
"Updater.Status.Checking": "Verificando atualizações…",
|
|
||||||
"Updater.Status.Current": "Você está atualizado ({0}).",
|
|
||||||
"Updater.Status.Available": "Um novo build está disponível: {0}",
|
|
||||||
"Updater.Status.Downloading": "Baixando atualização… {0}%",
|
|
||||||
"Updater.Status.Installing": "Instalando atualização…",
|
|
||||||
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
|
||||||
"Updater.Status.Failed": "Não foi possível verificar as atualizações.",
|
|
||||||
"Updater.Status.ChecksumFailed": "A atualização baixada falhou na verificação SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
|
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
|
||||||
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
|
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
|
||||||
"Dialog.PlainTextFiles": "Textdateien",
|
"Dialog.PlainTextFiles": "Textdateien",
|
||||||
"Dialog.LogFiles": "Protokolldateien",
|
"Dialog.LogFiles": "Protokolldateien"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Spieleinstellungen…",
|
|
||||||
"Options.Env.Tab": "Umgebung",
|
|
||||||
"Options.Section.Environment": "UMGEBUNGSVARIABLEN",
|
|
||||||
"Options.Env.Desc": "Schalter, die dem Emulator beim Start als Umgebungsvariablen übergeben werden.",
|
|
||||||
"Options.Env.Bthid.Desc": "Bluetooth-HID als nicht verfügbar melden, wenn die Lenkrad-/FFB-Middleware eines Titels endlos wartet.\nNormalerweise ausgeschaltet lassen. Manche Titel frieren ein, wenn die Initialisierung fehlschlägt.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "Titel nicht zwangsweise beenden, wenn sie denselben Aufruf zu lange wiederholen.\nAusprobieren, wenn ein Spiel sich beim Laden von selbst beendet.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Titeln erlauben, Dateien in ihrem Installationsordner anzulegen und zu schreiben.\nNötig für entpackte Dumps, die ihre Spielstände oder Konfiguration unter /app0 speichern.",
|
|
||||||
"Options.Env.VkValidation.Desc": "Vulkan-Validierungsschichten für GPU-Debugging aktivieren.\nLangsam. Erfordert ein installiertes Vulkan SDK.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "AGC-Shader und ihre SPIR-V-Übersetzungen im Ordner shader-dumps ablegen.\nBeim Melden von Shader- oder Grafikfehlern verwenden.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Direkte Speicherzuweisungen und Fehler in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start abbricht oder sich beendet.",
|
|
||||||
"Options.Env.LogIo.Desc": "Datei-Öffnen, -Lesen und Pfadauflösung in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start seine Datendateien nicht findet.",
|
|
||||||
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
|
|
||||||
"Common.Save": "Speichern",
|
|
||||||
"Common.Cancel": "Abbrechen",
|
|
||||||
"PerGame.Title": "Spielspezifische Einstellungen — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Nicht angehakte Zeilen übernehmen die globalen Standardwerte.",
|
|
||||||
"PerGame.EnvToggles.Label": "Umgebungsschalter",
|
|
||||||
"PerGame.EnvToggles.Desc": "Die globalen SHARPEMU_*-Schalter für dieses Spiel überschreiben.",
|
|
||||||
"Options.About": "Über",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "Quellcode, Issues und Projektentwicklung.",
|
|
||||||
"About.Github.LatestCommitLabel": "Neuester Commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Neuester Commit auf dem main-Branch",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "Tritt der Community bei, erhalte Support und verfolge die Entwicklung.",
|
|
||||||
"About.GithubButton": "Auf GitHub mitwirken!",
|
|
||||||
"About.DiscordButton": "Tritt unserem Discord bei!",
|
|
||||||
"Updater.Auto.Label": "Beim Start nach Updates suchen",
|
|
||||||
"Updater.Auto.Desc": "Fragt GitHub ab, ohne den Start zu verzögern.",
|
|
||||||
"Updater.Label": "Updates",
|
|
||||||
"Updater.Check": "Nach Updates suchen",
|
|
||||||
"Updater.DownloadRestart": "Herunterladen und neu starten",
|
|
||||||
"Updater.Status.Ready": "Aktueller Build: {0}",
|
|
||||||
"Updater.Status.Checking": "Suche nach Updates…",
|
|
||||||
"Updater.Status.Current": "Du bist auf dem neuesten Stand ({0}).",
|
|
||||||
"Updater.Status.Available": "Ein neuer Build ist verfügbar: {0}",
|
|
||||||
"Updater.Status.Downloading": "Update wird heruntergeladen… {0}%",
|
|
||||||
"Updater.Status.Installing": "Update wird installiert…",
|
|
||||||
"Updater.Status.Timeout": "Die Updateprüfung ist nach 10 Sekunden abgelaufen.",
|
|
||||||
"Updater.Status.Failed": "Updates konnten nicht geprüft werden.",
|
|
||||||
"Updater.Status.ChecksumFailed": "Das heruntergeladene Update hat die SHA-256-Prüfung nicht bestanden.",
|
|
||||||
"Updater.Status.Unsupported": "Automatische Updates erfordern einen x64-Build für Windows, Linux oder macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "PS-programmer",
|
"Dialog.PsExecutables": "PS-programmer",
|
||||||
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
|
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
|
||||||
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
|
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
|
||||||
"Dialog.LogFiles": "Logfiler",
|
"Dialog.LogFiles": "Logfiler"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Spilindstillinger…",
|
|
||||||
"Options.Env.Tab": "Miljø",
|
|
||||||
"Options.Section.Environment": "MILJØVARIABLER",
|
|
||||||
"Options.Env.Desc": "Kontakter, der gives videre til emulatoren som miljøvariabler ved start.",
|
|
||||||
"Options.Env.Bthid.Desc": "Rapportér Bluetooth HID som utilgængelig for titler, hvis rat-/FFB-middleware venter i det uendelige.\nLad den normalt være slået fra. Nogle titler fryser, når initialiseringen fejler.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "Tving ikke titler til at lukke, når de gentager det samme kald for længe.\nPrøv dette, når et spil lukker af sig selv under indlæsning.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Tillad titler at oprette og skrive filer i deres installationsmappe.\nKræves af upakkede dumps, der skriver deres gemte data eller konfiguration under /app0.",
|
|
||||||
"Options.Env.VkValidation.Desc": "Aktivér Vulkan-valideringslag til GPU-fejlfinding.\nLangsomt. Kræver at Vulkan SDK er installeret.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "Gem AGC-shadere og deres SPIR-V-oversættelser i mappen shader-dumps.\nBrug dette, når du rapporterer shader- eller grafikfejl.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Log direkte hukommelsestildelinger og fejl til konsollen.\nBrug dette, når et spil afbryder eller lukker under opstart.",
|
|
||||||
"Options.Env.LogIo.Desc": "Log åbning og læsning af filer samt stiopslag til konsollen.\nBrug dette, når et spil ikke kan finde sine datafiler under opstart.",
|
|
||||||
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
|
|
||||||
"Common.Save": "Gem",
|
|
||||||
"Common.Cancel": "Annuller",
|
|
||||||
"PerGame.Title": "Indstillinger pr. spil — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Umarkerede rækker arver de globale standardværdier.",
|
|
||||||
"PerGame.EnvToggles.Label": "Miljøkontakter",
|
|
||||||
"PerGame.EnvToggles.Desc": "Tilsidesæt det globale sæt SHARPEMU_*-kontakter for dette spil.",
|
|
||||||
"Options.About": "Om",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "Kildekode, issues og projektudvikling.",
|
|
||||||
"About.Github.LatestCommitLabel": "Seneste commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Seneste commit på main-branchen",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "Bliv en del af fællesskabet, få hjælp og følg udviklingen.",
|
|
||||||
"About.GithubButton": "Bidrag på GitHub!",
|
|
||||||
"About.DiscordButton": "Bliv medlem af vores Discord!",
|
|
||||||
"Updater.Auto.Label": "Søg efter opdateringer ved start",
|
|
||||||
"Updater.Auto.Desc": "Tjekker GitHub uden at forsinke opstarten.",
|
|
||||||
"Updater.Label": "Opdateringer",
|
|
||||||
"Updater.Check": "Søg efter opdateringer",
|
|
||||||
"Updater.DownloadRestart": "Download og genstart",
|
|
||||||
"Updater.Status.Ready": "Nuværende build: {0}",
|
|
||||||
"Updater.Status.Checking": "Søger efter opdateringer…",
|
|
||||||
"Updater.Status.Current": "Du er opdateret ({0}).",
|
|
||||||
"Updater.Status.Available": "Et nyt build er tilgængeligt: {0}",
|
|
||||||
"Updater.Status.Downloading": "Downloader opdatering… {0}%",
|
|
||||||
"Updater.Status.Installing": "Installerer opdatering…",
|
|
||||||
"Updater.Status.Timeout": "Opdateringstjekket fik timeout efter 10 sekunder.",
|
|
||||||
"Updater.Status.Failed": "Kunne ikke søge efter opdateringer.",
|
|
||||||
"Updater.Status.ChecksumFailed": "Den downloadede opdatering bestod ikke SHA-256-verifikationen.",
|
|
||||||
"Updater.Status.Unsupported": "Automatisk opdatering kræver et x64-build til Windows, Linux eller macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,39 +135,5 @@
|
|||||||
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
||||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||||
"About.GithubButton": "Contribuye en GitHub!",
|
"About.GithubButton": "Contribuye en GitHub!",
|
||||||
"About.DiscordButton": "Únete a nuestro Discord!",
|
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Ajustes del juego…",
|
|
||||||
"Options.Env.Tab": "Entorno",
|
|
||||||
"Options.Section.Environment": "VARIABLES DE ENTORNO",
|
|
||||||
"Options.Env.Desc": "Opciones que se pasan al emulador como variables de entorno al iniciar.",
|
|
||||||
"Options.Env.Bthid.Desc": "Indicar que Bluetooth HID no está disponible para títulos cuyo middleware de volante/FFB espera indefinidamente.\nDéjalo desactivado normalmente. Algunos títulos se congelan cuando la inicialización falla.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "No forzar el cierre de títulos que repiten la misma llamada durante demasiado tiempo.\nPruébalo cuando un juego se cierre solo durante la carga.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Permitir que los títulos creen y escriban archivos dentro de su carpeta de instalación.\nNecesario para dumps sin empaquetar que guardan sus datos o configuración en /app0.",
|
|
||||||
"Options.Env.VkValidation.Desc": "Activar las capas de validación de Vulkan para depurar la GPU.\nLento. Requiere tener instalado el SDK de Vulkan.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "Volcar los shaders AGC y sus traducciones SPIR-V a la carpeta shader-dumps.\nÚsalo al informar de errores de shaders o de renderizado.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Registrar en la consola las asignaciones de memoria directa y sus fallos.\nÚsalo cuando un juego se aborte o se cierre durante el arranque.",
|
|
||||||
"Options.Env.LogIo.Desc": "Registrar en la consola la apertura y lectura de archivos y la resolución de rutas.\nÚsalo cuando un juego no encuentre sus archivos de datos durante el arranque.",
|
|
||||||
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
|
|
||||||
"Common.Save": "Guardar",
|
|
||||||
"Common.Cancel": "Cancelar",
|
|
||||||
"PerGame.Title": "Ajustes por juego — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Las filas sin marcar heredan los valores globales.",
|
|
||||||
"PerGame.EnvToggles.Label": "Variables de entorno",
|
|
||||||
"PerGame.EnvToggles.Desc": "Sustituir el conjunto global de opciones SHARPEMU_* para este juego.",
|
|
||||||
"Updater.Auto.Label": "Buscar actualizaciones al iniciar",
|
|
||||||
"Updater.Auto.Desc": "Consulta GitHub sin retrasar el arranque.",
|
|
||||||
"Updater.Label": "Actualizaciones",
|
|
||||||
"Updater.Check": "Buscar actualizaciones",
|
|
||||||
"Updater.DownloadRestart": "Descargar y reiniciar",
|
|
||||||
"Updater.Status.Ready": "Build actual: {0}",
|
|
||||||
"Updater.Status.Checking": "Buscando actualizaciones…",
|
|
||||||
"Updater.Status.Current": "Estás al día ({0}).",
|
|
||||||
"Updater.Status.Available": "Hay un nuevo build disponible: {0}",
|
|
||||||
"Updater.Status.Downloading": "Descargando actualización… {0}%",
|
|
||||||
"Updater.Status.Installing": "Instalando actualización…",
|
|
||||||
"Updater.Status.Timeout": "La comprobación de actualizaciones caducó tras 10 segundos.",
|
|
||||||
"Updater.Status.Failed": "No se pudieron comprobar las actualizaciones.",
|
|
||||||
"Updater.Status.ChecksumFailed": "La actualización descargada no superó la verificación SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "La actualización automática requiere un build x64 de Windows, Linux o macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,32 +142,5 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
||||||
"About.GithubButton": "Contribuer sur GitHub !",
|
"About.GithubButton": "Contribuer sur GitHub !",
|
||||||
"About.DiscordButton": "Rejoindre notre Discord !",
|
"About.DiscordButton": "Rejoindre notre Discord !"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Paramètres du jeu…",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Autoriser les jeux à créer et écrire des fichiers dans leur dossier d’installation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.",
|
|
||||||
"Options.Env.LogIo.Desc": "Journaliser l’ouverture et la lecture des fichiers ainsi que la résolution des chemins dans la console.\nÀ utiliser quand un jeu ne trouve pas ses fichiers de données au démarrage.",
|
|
||||||
"Common.Save": "Enregistrer",
|
|
||||||
"Common.Cancel": "Annuler",
|
|
||||||
"PerGame.Title": "Paramètres par jeu — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Les lignes non cochées héritent des valeurs globales par défaut.",
|
|
||||||
"PerGame.EnvToggles.Label": "Variables d’environnement",
|
|
||||||
"PerGame.EnvToggles.Desc": "Remplacer l’ensemble global des options SHARPEMU_* pour ce jeu.",
|
|
||||||
"About.Github.LatestCommitLabel": "Dernier commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Dernier commit sur la branche main",
|
|
||||||
"Updater.Auto.Label": "Vérifier les mises à jour au démarrage",
|
|
||||||
"Updater.Auto.Desc": "Interroge GitHub sans retarder le démarrage.",
|
|
||||||
"Updater.Label": "Mises à jour",
|
|
||||||
"Updater.Check": "Vérifier les mises à jour",
|
|
||||||
"Updater.DownloadRestart": "Télécharger et redémarrer",
|
|
||||||
"Updater.Status.Ready": "Build actuel : {0}",
|
|
||||||
"Updater.Status.Checking": "Recherche de mises à jour…",
|
|
||||||
"Updater.Status.Current": "Vous êtes à jour ({0}).",
|
|
||||||
"Updater.Status.Available": "Un nouveau build est disponible : {0}",
|
|
||||||
"Updater.Status.Downloading": "Téléchargement de la mise à jour… {0}%",
|
|
||||||
"Updater.Status.Installing": "Installation de la mise à jour…",
|
|
||||||
"Updater.Status.Timeout": "La vérification des mises à jour a expiré après 10 secondes.",
|
|
||||||
"Updater.Status.Failed": "Impossible de vérifier les mises à jour.",
|
|
||||||
"Updater.Status.ChecksumFailed": "La mise à jour téléchargée a échoué à la vérification SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "La mise à jour automatique nécessite un build x64 pour Windows, Linux ou macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,32 +142,5 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
|
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
|
||||||
"About.GithubButton": "Járulj hozzá GitHubon!",
|
"About.GithubButton": "Járulj hozzá GitHubon!",
|
||||||
"About.DiscordButton": "Csatlakozz a Discordunhoz!",
|
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Játékbeállítások…",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Engedélyezi, hogy a játékok fájlokat hozzanak létre és írjanak a telepítési mappájukban.\nA kicsomagolt dumpokhoz szükséges, amelyek a mentéseiket vagy beállításaikat az /app0 alá írják.",
|
|
||||||
"Options.Env.LogIo.Desc": "A fájlmegnyitások, olvasások és útvonal-feloldások naplózása a konzolra.\nAkkor használd, ha egy játék indításkor nem találja az adatfájljait.",
|
|
||||||
"Common.Save": "Mentés",
|
|
||||||
"Common.Cancel": "Mégse",
|
|
||||||
"PerGame.Title": "Játékonkénti beállítások — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "A be nem jelölt sorok a globális alapértelmezéseket öröklik.",
|
|
||||||
"PerGame.EnvToggles.Label": "Környezeti kapcsolók",
|
|
||||||
"PerGame.EnvToggles.Desc": "A globális SHARPEMU_* kapcsolókészlet felülírása ennél a játéknál.",
|
|
||||||
"About.Github.LatestCommitLabel": "Legutóbbi commit",
|
|
||||||
"About.Github.LatestCommitDescription": "A main ág legutóbbi commitja",
|
|
||||||
"Updater.Auto.Label": "Frissítések keresése indításkor",
|
|
||||||
"Updater.Auto.Desc": "A GitHubot az indítás késleltetése nélkül ellenőrzi.",
|
|
||||||
"Updater.Label": "Frissítések",
|
|
||||||
"Updater.Check": "Frissítések keresése",
|
|
||||||
"Updater.DownloadRestart": "Letöltés és újraindítás",
|
|
||||||
"Updater.Status.Ready": "Jelenlegi build: {0}",
|
|
||||||
"Updater.Status.Checking": "Frissítések keresése…",
|
|
||||||
"Updater.Status.Current": "Naprakész vagy ({0}).",
|
|
||||||
"Updater.Status.Available": "Új build érhető el: {0}",
|
|
||||||
"Updater.Status.Downloading": "Frissítés letöltése… {0}%",
|
|
||||||
"Updater.Status.Installing": "Frissítés telepítése…",
|
|
||||||
"Updater.Status.Timeout": "A frissítés-ellenőrzés 10 másodperc után túllépte az időkorlátot.",
|
|
||||||
"Updater.Status.Failed": "Nem sikerült frissítéseket keresni.",
|
|
||||||
"Updater.Status.ChecksumFailed": "A letöltött frissítés nem ment át az SHA-256-ellenőrzésen.",
|
|
||||||
"Updater.Status.Unsupported": "Az automatikus frissítéshez Windows, Linux vagy macOS x64 build szükséges."
|
|
||||||
}
|
}
|
||||||
@@ -130,48 +130,5 @@
|
|||||||
"Dialog.PsExecutables": "Eseguibili PS",
|
"Dialog.PsExecutables": "Eseguibili PS",
|
||||||
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
||||||
"Dialog.PlainTextFiles": "File di testo semplice",
|
"Dialog.PlainTextFiles": "File di testo semplice",
|
||||||
"Dialog.LogFiles": "File di log",
|
"Dialog.LogFiles": "File di log"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Impostazioni del gioco…",
|
|
||||||
"Options.Env.Tab": "Ambiente",
|
|
||||||
"Options.Section.Environment": "VARIABILI D'AMBIENTE",
|
|
||||||
"Options.Env.Desc": "Opzioni passate all'emulatore come variabili d'ambiente all'avvio.",
|
|
||||||
"Options.Env.Bthid.Desc": "Segnala il Bluetooth HID come non disponibile per i titoli il cui middleware volante/FFB attende all'infinito.\nNormalmente lascialo disattivato. Alcuni titoli si bloccano quando l'inizializzazione fallisce.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "Non forzare la chiusura dei titoli che ripetono la stessa chiamata troppo a lungo.\nProvalo quando un gioco si chiude da solo durante il caricamento.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Consenti ai titoli di creare e scrivere file nella propria cartella di installazione.\nNecessario per i dump non pacchettizzati che scrivono salvataggi o configurazioni in /app0.",
|
|
||||||
"Options.Env.VkValidation.Desc": "Abilita i validation layer di Vulkan per il debug della GPU.\nLento. Richiede l'SDK di Vulkan installato.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "Esporta gli shader AGC e le loro traduzioni SPIR-V nella cartella shader-dumps.\nUsalo quando segnali bug di shader o di rendering.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Registra in console le allocazioni di memoria diretta e i relativi errori.\nUsalo quando un gioco si interrompe o si chiude durante l'avvio.",
|
|
||||||
"Options.Env.LogIo.Desc": "Registra in console l'apertura e la lettura dei file e la risoluzione dei percorsi.\nUsalo quando un gioco non trova i propri file di dati durante l'avvio.",
|
|
||||||
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
|
|
||||||
"Common.Save": "Salva",
|
|
||||||
"Common.Cancel": "Annulla",
|
|
||||||
"PerGame.Title": "Impostazioni per gioco — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Le righe non selezionate ereditano i valori globali.",
|
|
||||||
"PerGame.EnvToggles.Label": "Variabili d'ambiente",
|
|
||||||
"PerGame.EnvToggles.Desc": "Sovrascrivi l'insieme globale delle opzioni SHARPEMU_* per questo gioco.",
|
|
||||||
"Options.About": "Informazioni",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "Codice sorgente, issue e sviluppo del progetto.",
|
|
||||||
"About.Github.LatestCommitLabel": "Ultimo commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Ultimo commit sul branch main",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "Unisciti alla community, ricevi supporto e segui lo sviluppo.",
|
|
||||||
"About.GithubButton": "Contribuisci su GitHub!",
|
|
||||||
"About.DiscordButton": "Unisciti al nostro Discord!",
|
|
||||||
"Updater.Auto.Label": "Controlla aggiornamenti all'avvio",
|
|
||||||
"Updater.Auto.Desc": "Interroga GitHub senza rallentare l'avvio.",
|
|
||||||
"Updater.Label": "Aggiornamenti",
|
|
||||||
"Updater.Check": "Controlla aggiornamenti",
|
|
||||||
"Updater.DownloadRestart": "Scarica e riavvia",
|
|
||||||
"Updater.Status.Ready": "Build attuale: {0}",
|
|
||||||
"Updater.Status.Checking": "Ricerca aggiornamenti…",
|
|
||||||
"Updater.Status.Current": "Sei aggiornato ({0}).",
|
|
||||||
"Updater.Status.Available": "È disponibile un nuovo build: {0}",
|
|
||||||
"Updater.Status.Downloading": "Download dell'aggiornamento… {0}%",
|
|
||||||
"Updater.Status.Installing": "Installazione dell'aggiornamento…",
|
|
||||||
"Updater.Status.Timeout": "Il controllo degli aggiornamenti è scaduto dopo 10 secondi.",
|
|
||||||
"Updater.Status.Failed": "Impossibile controllare gli aggiornamenti.",
|
|
||||||
"Updater.Status.ChecksumFailed": "L'aggiornamento scaricato non ha superato la verifica SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "L'aggiornamento automatico richiede un build x64 per Windows, Linux o macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "PlayStation 実行ファイル",
|
"Dialog.PsExecutables": "PlayStation 実行ファイル",
|
||||||
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||||
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||||
"Dialog.LogFiles": "ログファイル",
|
"Dialog.LogFiles": "ログファイル"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "ゲーム設定…",
|
|
||||||
"Options.Env.Tab": "環境",
|
|
||||||
"Options.Section.Environment": "環境変数",
|
|
||||||
"Options.Env.Desc": "起動時に環境変数としてエミュレータへ渡されるスイッチです。",
|
|
||||||
"Options.Env.Bthid.Desc": "ハンドル/FFBミドルウェアが永久に待機するタイトル向けに、Bluetooth HIDを利用不可として報告します。\n通常はオフのままにしてください。初期化に失敗するとフリーズするタイトルもあります。",
|
|
||||||
"Options.Env.LoopGuard.Desc": "同じ呼び出しを長時間繰り返すタイトルを強制終了しません。\nロード中にゲームが勝手に終了する場合に試してください。",
|
|
||||||
"Options.Env.WritableApp0.Desc": "タイトルがインストールフォルダー内にファイルを作成・書き込みできるようにします。\nセーブや設定データを/app0以下に書き込む未パッケージのダンプに必要です。",
|
|
||||||
"Options.Env.VkValidation.Desc": "GPUデバッグ用のVulkan検証レイヤーを有効にします。\n低速です。Vulkan SDKのインストールが必要です。",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "AGCシェーダーとそのSPIR-V変換をshader-dumpsフォルダーに出力します。\nシェーダーや描画のバグを報告する際に使用してください。",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "ダイレクトメモリの割り当てと失敗をコンソールに記録します。\nゲームが起動中に中断・終了する場合に使用してください。",
|
|
||||||
"Options.Env.LogIo.Desc": "ファイルのオープン・読み込み・パス解決の動作をコンソールに記録します。\nゲームが起動中にデータファイルを見つけられない場合に使用してください。",
|
|
||||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
|
|
||||||
"Common.Save": "保存",
|
|
||||||
"Common.Cancel": "キャンセル",
|
|
||||||
"PerGame.Title": "ゲームごとの設定 — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "チェックされていない行はグローバルの既定値を継承します。",
|
|
||||||
"PerGame.EnvToggles.Label": "環境スイッチ",
|
|
||||||
"PerGame.EnvToggles.Desc": "このゲームに対してグローバルのSHARPEMU_*スイッチを上書きします。",
|
|
||||||
"Options.About": "情報",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "ソースコード、Issue、プロジェクトの開発。",
|
|
||||||
"About.Github.LatestCommitLabel": "最新コミット",
|
|
||||||
"About.Github.LatestCommitDescription": "mainブランチの最新コミット",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
|
|
||||||
"About.GithubButton": "GitHubで貢献しよう!",
|
|
||||||
"About.DiscordButton": "Discordに参加しよう!",
|
|
||||||
"Updater.Auto.Label": "起動時にアップデートを確認",
|
|
||||||
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
|
|
||||||
"Updater.Label": "アップデート",
|
|
||||||
"Updater.Check": "アップデートを確認",
|
|
||||||
"Updater.DownloadRestart": "ダウンロードして再起動",
|
|
||||||
"Updater.Status.Ready": "現在のビルド: {0}",
|
|
||||||
"Updater.Status.Checking": "アップデートを確認しています…",
|
|
||||||
"Updater.Status.Current": "最新の状態です({0})。",
|
|
||||||
"Updater.Status.Available": "新しいビルドがあります: {0}",
|
|
||||||
"Updater.Status.Downloading": "アップデートをダウンロード中… {0}%",
|
|
||||||
"Updater.Status.Installing": "アップデートをインストール中…",
|
|
||||||
"Updater.Status.Timeout": "アップデートの確認が10秒でタイムアウトしました。",
|
|
||||||
"Updater.Status.Failed": "アップデートを確認できませんでした。",
|
|
||||||
"Updater.Status.ChecksumFailed": "ダウンロードしたアップデートはSHA-256検証に失敗しました。",
|
|
||||||
"Updater.Status.Unsupported": "自動アップデートにはWindows、Linux、またはmacOSのx64ビルドが必要です。"
|
|
||||||
}
|
}
|
||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "PlayStation 실행 파일",
|
"Dialog.PsExecutables": "PlayStation 실행 파일",
|
||||||
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||||
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||||
"Dialog.LogFiles": "로그 파일",
|
"Dialog.LogFiles": "로그 파일"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "게임 설정…",
|
|
||||||
"Options.Env.Tab": "환경",
|
|
||||||
"Options.Section.Environment": "환경 변수",
|
|
||||||
"Options.Env.Desc": "실행 시 환경 변수로 에뮬레이터에 전달되는 스위치입니다.",
|
|
||||||
"Options.Env.Bthid.Desc": "휠/FFB 미들웨어가 무한 대기하는 타이틀을 위해 블루투스 HID를 사용 불가로 보고합니다.\n평소에는 꺼 두세요. 초기화에 실패하면 멈추는 타이틀도 있습니다.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "같은 호출을 너무 오래 반복하는 타이틀을 강제 종료하지 않습니다.\n게임이 로딩 중 저절로 종료될 때 시도해 보세요.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "타이틀이 설치 폴더 안에 파일을 만들고 쓸 수 있도록 허용합니다.\n세이브나 설정 데이터를 /app0 아래에 쓰는 비패키지 덤프에 필요합니다.",
|
|
||||||
"Options.Env.VkValidation.Desc": "GPU 디버깅을 위한 Vulkan 검증 레이어를 활성화합니다.\n느립니다. Vulkan SDK가 설치되어 있어야 합니다.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "AGC 셰이더와 SPIR-V 변환 결과를 shader-dumps 폴더에 저장합니다.\n셰이더나 렌더링 버그를 보고할 때 사용하세요.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "다이렉트 메모리 할당과 실패를 콘솔에 기록합니다.\n게임이 부팅 중 중단되거나 종료될 때 사용하세요.",
|
|
||||||
"Options.Env.LogIo.Desc": "파일 열기, 읽기, 경로 확인 동작을 콘솔에 기록합니다.\n게임이 부팅 중 데이터 파일을 찾지 못할 때 사용하세요.",
|
|
||||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
|
|
||||||
"Common.Save": "저장",
|
|
||||||
"Common.Cancel": "취소",
|
|
||||||
"PerGame.Title": "게임별 설정 — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "선택하지 않은 항목은 전역 기본값을 따릅니다.",
|
|
||||||
"PerGame.EnvToggles.Label": "환경 스위치",
|
|
||||||
"PerGame.EnvToggles.Desc": "이 게임에 대해 전역 SHARPEMU_* 스위치 설정을 재정의합니다.",
|
|
||||||
"Options.About": "정보",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "소스 코드, 이슈, 프로젝트 개발.",
|
|
||||||
"About.Github.LatestCommitLabel": "최신 커밋",
|
|
||||||
"About.Github.LatestCommitDescription": "main 브랜치의 최신 커밋",
|
|
||||||
"About.Discord.Label": "디스코드",
|
|
||||||
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
|
|
||||||
"About.GithubButton": "GitHub에서 기여하기!",
|
|
||||||
"About.DiscordButton": "디스코드 참여하기!",
|
|
||||||
"Updater.Auto.Label": "시작 시 업데이트 확인",
|
|
||||||
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
|
|
||||||
"Updater.Label": "업데이트",
|
|
||||||
"Updater.Check": "업데이트 확인",
|
|
||||||
"Updater.DownloadRestart": "다운로드 후 재시작",
|
|
||||||
"Updater.Status.Ready": "현재 빌드: {0}",
|
|
||||||
"Updater.Status.Checking": "업데이트 확인 중…",
|
|
||||||
"Updater.Status.Current": "최신 상태입니다 ({0}).",
|
|
||||||
"Updater.Status.Available": "새 빌드가 있습니다: {0}",
|
|
||||||
"Updater.Status.Downloading": "업데이트 다운로드 중… {0}%",
|
|
||||||
"Updater.Status.Installing": "업데이트 설치 중…",
|
|
||||||
"Updater.Status.Timeout": "업데이트 확인이 10초 후 시간 초과되었습니다.",
|
|
||||||
"Updater.Status.Failed": "업데이트를 확인할 수 없습니다.",
|
|
||||||
"Updater.Status.ChecksumFailed": "다운로드한 업데이트가 SHA-256 검증에 실패했습니다.",
|
|
||||||
"Updater.Status.Unsupported": "자동 업데이트에는 Windows, Linux 또는 macOS x64 빌드가 필요합니다."
|
|
||||||
}
|
}
|
||||||
@@ -125,48 +125,5 @@
|
|||||||
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
|
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
|
||||||
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
|
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
|
||||||
"Dialog.PlainTextFiles": "Platte tekstbestanden",
|
"Dialog.PlainTextFiles": "Platte tekstbestanden",
|
||||||
"Dialog.LogFiles": "Logbestanden",
|
"Dialog.LogFiles": "Logbestanden"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Game-instellingen…",
|
|
||||||
"Options.Env.Tab": "Omgeving",
|
|
||||||
"Options.Section.Environment": "OMGEVINGSVARIABELEN",
|
|
||||||
"Options.Env.Desc": "Schakelaars die bij het starten als omgevingsvariabelen aan de emulator worden doorgegeven.",
|
|
||||||
"Options.Env.Bthid.Desc": "Meld Bluetooth HID als niet beschikbaar voor titels waarvan de stuur-/FFB-middleware eindeloos blijft wachten.\nLaat dit normaal uit. Sommige titels bevriezen wanneer de initialisatie mislukt.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "Titels die dezelfde aanroep te lang herhalen niet geforceerd afsluiten.\nProbeer dit wanneer een game zichzelf tijdens het laden afsluit.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Sta titels toe bestanden aan te maken en te schrijven in hun installatiemap.\nNodig voor uitgepakte dumps die hun save- of configuratiegegevens onder /app0 wegschrijven.",
|
|
||||||
"Options.Env.VkValidation.Desc": "Schakel Vulkan-validatielagen in voor GPU-debugging.\nTraag. Vereist een geïnstalleerde Vulkan SDK.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "Sla AGC-shaders en hun SPIR-V-vertalingen op in de map shader-dumps.\nGebruik dit bij het melden van shader- of renderfouten.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Log directe geheugentoewijzingen en fouten naar de console.\nGebruik dit wanneer een game tijdens het opstarten afbreekt of afsluit.",
|
|
||||||
"Options.Env.LogIo.Desc": "Log het openen en lezen van bestanden en het oplossen van paden naar de console.\nGebruik dit wanneer een game zijn databestanden niet kan vinden tijdens het opstarten.",
|
|
||||||
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
|
|
||||||
"Common.Save": "Opslaan",
|
|
||||||
"Common.Cancel": "Annuleren",
|
|
||||||
"PerGame.Title": "Instellingen per game — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "Niet-aangevinkte rijen erven de globale standaardwaarden.",
|
|
||||||
"PerGame.EnvToggles.Label": "Omgevingsschakelaars",
|
|
||||||
"PerGame.EnvToggles.Desc": "Overschrijf de globale set SHARPEMU_*-schakelaars voor deze game.",
|
|
||||||
"Options.About": "Over",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "Broncode, issues en projectontwikkeling.",
|
|
||||||
"About.Github.LatestCommitLabel": "Nieuwste commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Nieuwste commit op de main-branch",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "Word lid van de community, krijg ondersteuning en volg de ontwikkeling.",
|
|
||||||
"About.GithubButton": "Draag bij op GitHub!",
|
|
||||||
"About.DiscordButton": "Word lid van onze Discord!",
|
|
||||||
"Updater.Auto.Label": "Bij het opstarten controleren op updates",
|
|
||||||
"Updater.Auto.Desc": "Controleert GitHub zonder het opstarten te vertragen.",
|
|
||||||
"Updater.Label": "Updates",
|
|
||||||
"Updater.Check": "Controleren op updates",
|
|
||||||
"Updater.DownloadRestart": "Downloaden en opnieuw starten",
|
|
||||||
"Updater.Status.Ready": "Huidige build: {0}",
|
|
||||||
"Updater.Status.Checking": "Controleren op updates…",
|
|
||||||
"Updater.Status.Current": "Je bent up-to-date ({0}).",
|
|
||||||
"Updater.Status.Available": "Er is een nieuwe build beschikbaar: {0}",
|
|
||||||
"Updater.Status.Downloading": "Update downloaden… {0}%",
|
|
||||||
"Updater.Status.Installing": "Update installeren…",
|
|
||||||
"Updater.Status.Timeout": "De updatecontrole is na 10 seconden verlopen.",
|
|
||||||
"Updater.Status.Failed": "Kon niet controleren op updates.",
|
|
||||||
"Updater.Status.ChecksumFailed": "De gedownloade update is niet door de SHA-256-verificatie gekomen.",
|
|
||||||
"Updater.Status.Unsupported": "Automatisch updaten vereist een x64-build voor Windows, Linux of macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,32 +142,5 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||||
"About.GithubButton": "Contribua no GitHub!",
|
"About.GithubButton": "Contribua no GitHub!",
|
||||||
"About.DiscordButton": "Junte-se ao nosso Discord!",
|
"About.DiscordButton": "Junte-se ao nosso Discord!"
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Definições do jogo…",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Permitir que os jogos criem e escrevam ficheiros dentro da sua pasta de instalação.\nNecessário para dumps não empacotados que escrevem os seus dados guardados ou configurações em /app0.",
|
|
||||||
"Options.Env.LogIo.Desc": "Registar na consola a abertura e leitura de ficheiros e a resolução de caminhos.\nUtilize quando um jogo não encontrar os seus ficheiros de dados durante o arranque.",
|
|
||||||
"Common.Save": "Guardar",
|
|
||||||
"Common.Cancel": "Cancelar",
|
|
||||||
"PerGame.Title": "Definições por jogo — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "As linhas não assinaladas herdam as predefinições globais.",
|
|
||||||
"PerGame.EnvToggles.Label": "Variáveis de ambiente",
|
|
||||||
"PerGame.EnvToggles.Desc": "Substituir o conjunto global de opções SHARPEMU_* para este jogo.",
|
|
||||||
"About.Github.LatestCommitLabel": "Último commit",
|
|
||||||
"About.Github.LatestCommitDescription": "Último commit no ramo main",
|
|
||||||
"Updater.Auto.Label": "Procurar atualizações no arranque",
|
|
||||||
"Updater.Auto.Desc": "Consulta o GitHub sem atrasar o arranque.",
|
|
||||||
"Updater.Label": "Atualizações",
|
|
||||||
"Updater.Check": "Procurar atualizações",
|
|
||||||
"Updater.DownloadRestart": "Transferir e reiniciar",
|
|
||||||
"Updater.Status.Ready": "Build atual: {0}",
|
|
||||||
"Updater.Status.Checking": "A procurar atualizações…",
|
|
||||||
"Updater.Status.Current": "Está atualizado ({0}).",
|
|
||||||
"Updater.Status.Available": "Está disponível um novo build: {0}",
|
|
||||||
"Updater.Status.Downloading": "A transferir a atualização… {0}%",
|
|
||||||
"Updater.Status.Installing": "A instalar a atualização…",
|
|
||||||
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
|
||||||
"Updater.Status.Failed": "Não foi possível procurar atualizações.",
|
|
||||||
"Updater.Status.ChecksumFailed": "A atualização transferida falhou a verificação SHA-256.",
|
|
||||||
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,5 @@
|
|||||||
"Updater.Status.Installing": "Установка обновления…",
|
"Updater.Status.Installing": "Установка обновления…",
|
||||||
"Updater.Status.Timeout": "Проверка обновлений превысила лимит времени в 10 секунд.",
|
"Updater.Status.Timeout": "Проверка обновлений превысила лимит времени в 10 секунд.",
|
||||||
"Updater.Status.Failed": "Не удалось проверить наличие обновлений.",
|
"Updater.Status.Failed": "Не удалось проверить наличие обновлений.",
|
||||||
"Updater.Status.Unsupported": "Автоматическое обновление требует сборку Windows, Linux или macOS x64.",
|
"Updater.Status.Unsupported": "Автоматическое обновление требует сборку Windows, Linux или macOS x64."
|
||||||
|
|
||||||
"Updater.Status.ChecksumFailed": "Скачанное обновление не прошло проверку SHA-256."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,33 +141,5 @@
|
|||||||
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
||||||
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
||||||
"Updater.Status.ChecksumFailed": "İndirilen güncelleme SHA-256 doğrulamasını geçemedi.",
|
"Updater.Status.ChecksumFailed": "İndirilen güncelleme SHA-256 doğrulamasını geçemedi.",
|
||||||
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir.",
|
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Oyun ayarları…",
|
|
||||||
"Options.Env.Tab": "Ortam",
|
|
||||||
"Options.Section.Environment": "ORTAM DEĞİŞKENLERİ",
|
|
||||||
"Options.Env.Desc": "Başlatma sırasında emülatöre ortam değişkeni olarak geçirilen anahtarlar.",
|
|
||||||
"Options.Env.Bthid.Desc": "Direksiyon/FFB katmanı sonsuza kadar bekleyen oyunlar için Bluetooth HID'i kullanılamıyor olarak bildir.\nNormalde kapalı bırakın. Bazı oyunlar başlatma başarısız olduğunda donar.",
|
|
||||||
"Options.Env.LoopGuard.Desc": "Aynı çağrıyı uzun süre tekrarlayan oyunları zorla kapatma.\nBir oyun yükleme sırasında kendiliğinden kapanıyorsa bunu deneyin.",
|
|
||||||
"Options.Env.WritableApp0.Desc": "Oyunların kurulum klasörlerinde dosya oluşturup yazmasına izin ver.\nKayıt veya yapılandırma verisini /app0 altına yazan paketlenmemiş dump'lar için gereklidir.",
|
|
||||||
"Options.Env.VkValidation.Desc": "GPU hata ayıklaması için Vulkan doğrulama katmanlarını etkinleştir.\nYavaştır. Vulkan SDK'nın kurulu olması gerekir.",
|
|
||||||
"Options.Env.DumpSpirv.Desc": "AGC shader'larını ve SPIR-V çevirilerini shader-dumps klasörüne kaydet.\nShader veya görüntü hatalarını bildirirken kullanın.",
|
|
||||||
"Options.Env.LogDirectMemory.Desc": "Doğrudan bellek tahsislerini ve hatalarını konsola günlükle.\nBir oyun açılış sırasında çöküyor veya kapanıyorsa kullanın.",
|
|
||||||
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
|
|
||||||
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
|
|
||||||
"Common.Save": "Kaydet",
|
|
||||||
"Common.Cancel": "İptal",
|
|
||||||
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
|
|
||||||
"PerGame.InheritNote": "İşaretlenmemiş satırlar genel varsayılanları kullanır.",
|
|
||||||
"PerGame.EnvToggles.Label": "Ortam anahtarları",
|
|
||||||
"PerGame.EnvToggles.Desc": "Bu oyun için genel SHARPEMU_* anahtar kümesini geçersiz kıl.",
|
|
||||||
"Options.About": "Hakkında",
|
|
||||||
"About.Github.Label": "GitHub",
|
|
||||||
"About.Github.Desc": "Kaynak kodu, hata kayıtları ve proje geliştirme.",
|
|
||||||
"About.Github.LatestCommitLabel": "Son Commit",
|
|
||||||
"About.Github.LatestCommitDescription": "main dalındaki son commit",
|
|
||||||
"About.Discord.Label": "Discord",
|
|
||||||
"About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.",
|
|
||||||
"About.GithubButton": "GitHub'da katkıda bulun!",
|
|
||||||
"About.DiscordButton": "Discord'umuza katıl!"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Panel Grid.Row="1" x:Name="PagesHost">
|
<Panel Grid.Row="1">
|
||||||
|
|
||||||
<!-- Library page. The tile row gets extra top margin so it sits
|
<!-- Library page. The tile row gets extra top margin so it sits
|
||||||
closer to eye level (PS5 home-screen style) instead of hugging
|
closer to eye level (PS5 home-screen style) instead of hugging
|
||||||
@@ -400,29 +400,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
|
||||||
|
|
||||||
<Border Classes="card">
|
|
||||||
<StackPanel Spacing="14">
|
|
||||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
|
||||||
|
|
||||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
|
||||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
|
||||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
|
||||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
|
||||||
</ComboBox>
|
|
||||||
</local:SettingRow>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
@@ -627,12 +604,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<!-- This is a native popup rather than an Avalonia overlay because the
|
<!-- This is a native popup rather than an Avalonia overlay because the
|
||||||
emulated Vulkan surface is a native child window. -->
|
emulated Vulkan surface is a native child window. -->
|
||||||
<!-- Anchored to MainContent, not GameView: the surface host is parked in
|
|
||||||
a 1x1 corner while loading/closing, which would pull a GameView-
|
|
||||||
anchored popup into the corner with it. -->
|
|
||||||
<primitives:Popup x:Name="SessionLoadingPopup"
|
<primitives:Popup x:Name="SessionLoadingPopup"
|
||||||
IsOpen="False"
|
IsOpen="False"
|
||||||
PlacementTarget="{Binding #MainContent}"
|
PlacementTarget="{Binding #GameView}"
|
||||||
Placement="Center"
|
Placement="Center"
|
||||||
Topmost="True"
|
Topmost="True"
|
||||||
ShouldUseOverlayLayer="False"
|
ShouldUseOverlayLayer="False"
|
||||||
|
|||||||
@@ -88,15 +88,6 @@ public partial class MainWindow : Window
|
|||||||
private int _detailLoadGeneration;
|
private int _detailLoadGeneration;
|
||||||
private int _backdropGeneration;
|
private int _backdropGeneration;
|
||||||
|
|
||||||
// Bundled key art shown whenever no game-specific backdrop applies; the
|
|
||||||
// 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.
|
// Controller navigation state.
|
||||||
private readonly DispatcherTimer _gamepadTimer;
|
private readonly DispatcherTimer _gamepadTimer;
|
||||||
private HostGamepadButtons _previousPadButtons;
|
private HostGamepadButtons _previousPadButtons;
|
||||||
@@ -120,18 +111,6 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_defaultBackdrop = new Bitmap(
|
|
||||||
AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/pic0.png")));
|
|
||||||
BackdropImage.Source = _defaultBackdrop;
|
|
||||||
BackdropImage.Opacity = 1.0;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_defaultBackdrop = null; // color background remains the fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
GameList.ItemsSource = _visibleGames;
|
GameList.ItemsSource = _visibleGames;
|
||||||
ConsoleList.ItemsSource = _consoleLines;
|
ConsoleList.ItemsSource = _consoleLines;
|
||||||
_consoleMirror = GuiConsoleMirror.Install((line, isError) =>
|
_consoleMirror = GuiConsoleMirror.Install((line, isError) =>
|
||||||
@@ -155,18 +134,8 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
|
||||||
|
|
||||||
// Native popups float above every window on the desktop; they must
|
Activated += (_, _) => UpdateSessionBarVisibility();
|
||||||
// follow the launcher into the background or a minimized state.
|
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
|
||||||
Activated += (_, _) =>
|
|
||||||
{
|
|
||||||
UpdateSessionBarVisibility();
|
|
||||||
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
|
|
||||||
};
|
|
||||||
Deactivated += (_, _) =>
|
|
||||||
{
|
|
||||||
SessionBarPopup.IsOpen = false;
|
|
||||||
SessionLoadingPopup.IsOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||||
@@ -192,18 +161,6 @@ public partial class MainWindow : Window
|
|||||||
// it is open already uses the new values.
|
// it is open already uses the new values.
|
||||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
|
||||||
double.TryParse(
|
|
||||||
tag,
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
out var scale))
|
|
||||||
{
|
|
||||||
_settings.RenderResolutionScale = scale;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||||
@@ -441,15 +398,6 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isRunning || _isStopping)
|
|
||||||
{
|
|
||||||
// The game renders inside the launcher window, so the launcher
|
|
||||||
// stays active while playing. The controller belongs to the game
|
|
||||||
// then: no navigation, and Circle/B must never stop the session.
|
|
||||||
_previousPadButtons = pad.Buttons;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||||
{
|
{
|
||||||
@@ -499,6 +447,11 @@ public partial class MainWindow : Window
|
|||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||||
|
{
|
||||||
|
StopEmulator();
|
||||||
|
}
|
||||||
|
|
||||||
_previousPadButtons = pad.Buttons;
|
_previousPadButtons = pad.Buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,13 +834,6 @@ public partial class MainWindow : Window
|
|||||||
_ => 2,
|
_ => 2,
|
||||||
};
|
};
|
||||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||||
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
|
|
||||||
{
|
|
||||||
>= 0.875 => 0,
|
|
||||||
>= 0.625 => 1,
|
|
||||||
>= 0.375 => 2,
|
|
||||||
_ => 3,
|
|
||||||
};
|
|
||||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
@@ -1664,23 +1610,13 @@ public partial class MainWindow : Window
|
|||||||
base.OnPropertyChanged(change);
|
base.OnPropertyChanged(change);
|
||||||
if (change.Property == WindowStateProperty)
|
if (change.Property == WindowStateProperty)
|
||||||
{
|
{
|
||||||
// The XAML WindowState="Maximized" assignment raises this change
|
|
||||||
// during InitializeComponent, before named controls are wired up.
|
|
||||||
if (WindowState == WindowState.Minimized)
|
if (WindowState == WindowState.Minimized)
|
||||||
{
|
{
|
||||||
_sndPreview.Pause();
|
_sndPreview.Pause();
|
||||||
if (SessionLoadingPopup is { } popup)
|
|
||||||
{
|
|
||||||
popup.IsOpen = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_sndPreview.Resume();
|
_sndPreview.Resume();
|
||||||
if (SessionLoadingPopup is { } popup)
|
|
||||||
{
|
|
||||||
popup.IsOpen = _sessionLoadingActive;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1695,20 +1631,8 @@ public partial class MainWindow : Window
|
|||||||
var generation = ++_backdropGeneration;
|
var generation = ++_backdropGeneration;
|
||||||
BackdropImage.Opacity = 0;
|
BackdropImage.Opacity = 0;
|
||||||
|
|
||||||
// The bundled key art is the primary backdrop whenever the selection
|
|
||||||
// has no art of its own; the window color stays as the last fallback.
|
|
||||||
void ShowDefaultBackdrop()
|
|
||||||
{
|
|
||||||
if (generation == _backdropGeneration && _defaultBackdrop is not null)
|
|
||||||
{
|
|
||||||
BackdropImage.Source = _defaultBackdrop;
|
|
||||||
BackdropImage.Opacity = 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game?.BackgroundPath is null)
|
if (game?.BackgroundPath is null)
|
||||||
{
|
{
|
||||||
ShowDefaultBackdrop();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1725,8 +1649,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
ShowDefaultBackdrop(); // undecodable key art
|
return; // undecodable key art: keep the plain background
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1807,12 +1730,6 @@ public partial class MainWindow : Window
|
|||||||
_appliedEnvironmentVariables.Add(name);
|
_appliedEnvironmentVariables.Add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(
|
|
||||||
"SHARPEMU_RENDER_SCALE",
|
|
||||||
_settings.RenderResolutionScale.ToString(
|
|
||||||
"0.###",
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||||
{
|
{
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
@@ -2052,30 +1969,18 @@ public partial class MainWindow : Window
|
|||||||
_awaitingFirstFrame = false;
|
_awaitingFirstFrame = false;
|
||||||
ClearLibraryBlur();
|
ClearLibraryBlur();
|
||||||
MainContent.Margin = new Thickness(0);
|
MainContent.Margin = new Thickness(0);
|
||||||
RestoreGameViewToFull();
|
|
||||||
GameView.Background = Brushes.Black;
|
GameView.Background = Brushes.Black;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
|
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||||
|
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||||
LibraryPage.IsVisible = false;
|
LibraryPage.IsVisible = false;
|
||||||
OptionsPage.IsVisible = false;
|
OptionsPage.IsVisible = false;
|
||||||
LibraryToolbar.IsVisible = false;
|
LibraryToolbar.IsVisible = false;
|
||||||
ContentToolbar.IsVisible = false;
|
ContentToolbar.IsVisible = false;
|
||||||
ConsolePanel.IsVisible = false;
|
ConsolePanel.IsVisible = false;
|
||||||
LaunchBar.IsVisible = false;
|
LaunchBar.IsVisible = false;
|
||||||
HideSessionLoading();
|
SessionLoadingPopup.IsOpen = false;
|
||||||
UpdateSessionBarVisibility();
|
UpdateSessionBarVisibility();
|
||||||
|
|
||||||
// Defer so the layout pass from the margin change above settles first.
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (!_isRunning || _isStopping)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameSurfaceHost?.RefreshSurfaceSize();
|
|
||||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
|
||||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2125,31 +2030,11 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The native host attachment is a real child window: it sits above every
|
|
||||||
/// Avalonia control it covers and swallows their mouse input regardless of
|
|
||||||
/// hit-test settings. While the library must stay interactive (loading,
|
|
||||||
/// closing), the surface is parked offscreen AT FULL SIZE via a negative
|
|
||||||
/// margin. It must not be shrunk instead: the emulator child polls the
|
|
||||||
/// HWND client size and its presenter defers swapchain creation while the
|
|
||||||
/// surface is 1px, which would deadlock the loading handshake.
|
|
||||||
/// </summary>
|
|
||||||
private void ParkGameViewOffscreen()
|
|
||||||
{
|
|
||||||
GameView.Margin = new Thickness(-20000, 0, 20000, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestoreGameViewToFull()
|
|
||||||
{
|
|
||||||
GameView.Margin = new Thickness(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowGameView()
|
private void ShowGameView()
|
||||||
{
|
{
|
||||||
_isStopping = false;
|
_isStopping = false;
|
||||||
_awaitingFirstFrame = true;
|
_awaitingFirstFrame = true;
|
||||||
var host = EnsureGameSurfaceHost();
|
var host = EnsureGameSurfaceHost();
|
||||||
ParkGameViewOffscreen();
|
|
||||||
GameView.IsVisible = true;
|
GameView.IsVisible = true;
|
||||||
GameView.Background = Brushes.Transparent;
|
GameView.Background = Brushes.Transparent;
|
||||||
GameView.IsHitTestVisible = false;
|
GameView.IsHitTestVisible = false;
|
||||||
@@ -2174,7 +2059,7 @@ public partial class MainWindow : Window
|
|||||||
GameView.IsVisible = false;
|
GameView.IsVisible = false;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
SessionBarPopup.IsOpen = false;
|
SessionBarPopup.IsOpen = false;
|
||||||
HideSessionLoading();
|
SessionLoadingPopup.IsOpen = false;
|
||||||
AnimateLibraryBlur(0, clearWhenComplete: true);
|
AnimateLibraryBlur(0, clearWhenComplete: true);
|
||||||
MainContent.Margin = new Thickness(32, 24, 32, 20);
|
MainContent.Margin = new Thickness(32, 24, 32, 20);
|
||||||
ContentToolbar.IsVisible = true;
|
ContentToolbar.IsVisible = true;
|
||||||
@@ -2183,15 +2068,16 @@ public partial class MainWindow : Window
|
|||||||
LibraryPage.IsVisible = _activePageIndex == 0;
|
LibraryPage.IsVisible = _activePageIndex == 0;
|
||||||
LibraryToolbar.IsVisible = _activePageIndex == 0;
|
LibraryToolbar.IsVisible = _activePageIndex == 0;
|
||||||
OptionsPage.IsVisible = _activePageIndex == 1;
|
OptionsPage.IsVisible = _activePageIndex == 1;
|
||||||
// Game art when the source still holds it, otherwise the bundled
|
if (GameList.SelectedItem is GameEntry game && game.Background is not null)
|
||||||
// default; a bare color only when neither is available.
|
{
|
||||||
BackdropImage.Opacity = BackdropImage.Source is not null ? 1 : 0;
|
BackdropImage.Opacity = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AnimateLibraryBlur(double targetRadius, bool clearWhenComplete = false)
|
private void AnimateLibraryBlur(double targetRadius, bool clearWhenComplete = false)
|
||||||
{
|
{
|
||||||
_libraryBlur ??= new BlurEffect();
|
_libraryBlur ??= new BlurEffect();
|
||||||
PagesHost.Effect = _libraryBlur;
|
MainContent.Effect = _libraryBlur;
|
||||||
|
|
||||||
_libraryBlurStartRadius = _libraryBlur.Radius;
|
_libraryBlurStartRadius = _libraryBlur.Radius;
|
||||||
_libraryBlurTargetRadius = Math.Max(0, targetRadius);
|
_libraryBlurTargetRadius = Math.Max(0, targetRadius);
|
||||||
@@ -2240,7 +2126,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if (_clearLibraryBlurWhenComplete)
|
if (_clearLibraryBlurWhenComplete)
|
||||||
{
|
{
|
||||||
PagesHost.Effect = null;
|
MainContent.Effect = null;
|
||||||
_libraryBlur = null;
|
_libraryBlur = null;
|
||||||
_clearLibraryBlurWhenComplete = false;
|
_clearLibraryBlurWhenComplete = false;
|
||||||
}
|
}
|
||||||
@@ -2251,21 +2137,14 @@ public partial class MainWindow : Window
|
|||||||
_libraryBlurTimer.Stop();
|
_libraryBlurTimer.Stop();
|
||||||
_libraryBlur = null;
|
_libraryBlur = null;
|
||||||
_clearLibraryBlurWhenComplete = false;
|
_clearLibraryBlurWhenComplete = false;
|
||||||
PagesHost.Effect = null;
|
MainContent.Effect = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowSessionLoading(string title, string detail)
|
private void ShowSessionLoading(string title, string detail)
|
||||||
{
|
{
|
||||||
SessionLoadingTitle.Text = title;
|
SessionLoadingTitle.Text = title;
|
||||||
SessionLoadingDetail.Text = detail;
|
SessionLoadingDetail.Text = detail;
|
||||||
_sessionLoadingActive = true;
|
SessionLoadingPopup.IsOpen = true;
|
||||||
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HideSessionLoading()
|
|
||||||
{
|
|
||||||
_sessionLoadingActive = false;
|
|
||||||
SessionLoadingPopup.IsOpen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReturnToLibraryWhileStopping()
|
private void ReturnToLibraryWhileStopping()
|
||||||
@@ -2277,12 +2156,10 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
// Keep the native child alive until the session exits, but hide it
|
// Keep the native child alive until the session exits, but hide it
|
||||||
// immediately. Destroying it while Vulkan still owns the surface can
|
// immediately. Destroying it while Vulkan still owns the surface can
|
||||||
// crash the GUI; parking it in the 1x1 corner lets the library
|
// crash the GUI; leaving it transparent lets the library recover
|
||||||
// recover — and stay clickable — while the native closing popup
|
// while the native closing popup reports teardown progress.
|
||||||
// reports teardown progress.
|
|
||||||
_gameSurfaceHost?.SetPresentationVisible(false);
|
_gameSurfaceHost?.SetPresentationVisible(false);
|
||||||
_awaitingFirstFrame = false;
|
_awaitingFirstFrame = false;
|
||||||
ParkGameViewOffscreen();
|
|
||||||
GameView.Background = Brushes.Transparent;
|
GameView.Background = Brushes.Transparent;
|
||||||
GameView.IsHitTestVisible = false;
|
GameView.IsHitTestVisible = false;
|
||||||
SessionBarPopup.IsOpen = false;
|
SessionBarPopup.IsOpen = false;
|
||||||
@@ -2294,7 +2171,7 @@ public partial class MainWindow : Window
|
|||||||
LibraryPage.IsVisible = _activePageIndex == 0;
|
LibraryPage.IsVisible = _activePageIndex == 0;
|
||||||
LibraryToolbar.IsVisible = _activePageIndex == 0;
|
LibraryToolbar.IsVisible = _activePageIndex == 0;
|
||||||
OptionsPage.IsVisible = _activePageIndex == 1;
|
OptionsPage.IsVisible = _activePageIndex == 1;
|
||||||
BackdropImage.Opacity = BackdropImage.Source is not null ? 1 : 0;
|
BackdropImage.Opacity = GameList.SelectedItem is GameEntry { Background: not null } ? 1 : 0;
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
Console.Error.WriteLine("[GUI][INFO] Library restored while embedded session is closing.");
|
Console.Error.WriteLine("[GUI][INFO] Library restored while embedded session is closing.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class PerGameSettings
|
|||||||
var path = PathFor(titleId);
|
var path = PathFor(titleId);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
return NormalizeFromJson(File.ReadAllText(path));
|
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -59,18 +59,6 @@ public sealed class PerGameSettings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A null list inherits global settings; only entries in a present list are sanitized.
|
|
||||||
internal static PerGameSettings? NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<PerGameSettings>(json, SerializerOptions);
|
|
||||||
if (settings?.EnvironmentToggles is { } toggles)
|
|
||||||
{
|
|
||||||
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(string titleId)
|
public void Save(string titleId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(titleId))
|
if (string.IsNullOrWhiteSpace(titleId))
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
@@ -42,7 +38,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\pic0.png" Link="Assets/pic0.png" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
public int Armed;
|
public int Armed;
|
||||||
public int FirstCpuWriteSeen;
|
public int FirstCpuWriteSeen;
|
||||||
public int PendingFirstCpuWrite;
|
public int PendingFirstCpuWrite;
|
||||||
public long WriteGeneration;
|
|
||||||
public bool TraceLifetime;
|
public bool TraceLifetime;
|
||||||
public long SourceSequence;
|
public long SourceSequence;
|
||||||
public long FirstCpuWriteTraceSequence;
|
public long FirstCpuWriteTraceSequence;
|
||||||
@@ -52,33 +51,9 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
||||||
|
|
||||||
/// <summary>Immutable snapshot read lock-free from the signal handler and
|
// Snapshot array read lock-free from the signal handler; rebuilt on every
|
||||||
/// the managed-write pre-visit; rebuilt on every mutation under the gate
|
// mutation under the gate. Signal handlers must not take managed locks.
|
||||||
/// (signal handlers must not take managed locks). Carrying the overall
|
private static TrackedRange[] _rangeSnapshot = [];
|
||||||
/// bounds inside the same object keeps the hot-path intersection test
|
|
||||||
/// consistent with the array it guards.</summary>
|
|
||||||
private sealed class RangeSnapshot
|
|
||||||
{
|
|
||||||
public static readonly RangeSnapshot Empty = new([]);
|
|
||||||
|
|
||||||
public readonly TrackedRange[] Ranges;
|
|
||||||
public readonly ulong Start;
|
|
||||||
public readonly ulong End;
|
|
||||||
|
|
||||||
public RangeSnapshot(TrackedRange[] ranges)
|
|
||||||
{
|
|
||||||
Ranges = ranges;
|
|
||||||
Start = ulong.MaxValue;
|
|
||||||
End = 0;
|
|
||||||
foreach (var range in ranges)
|
|
||||||
{
|
|
||||||
Start = Math.Min(Start, range.Start);
|
|
||||||
End = Math.Max(End, range.End);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
|
|
||||||
|
|
||||||
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||||
@@ -156,21 +131,10 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
{
|
{
|
||||||
// Never resize an object that is still reachable from the
|
// Never resize an object that is still reachable from the
|
||||||
// signal handler's lock-free snapshot. Retire it and publish
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
// a fresh immutable range, carrying the write generation so
|
// a fresh immutable range.
|
||||||
// resizes do not hide guest CPU rewrites from cache owners.
|
|
||||||
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
DisarmLocked(range, "replace-range");
|
DisarmLocked(range, "replace-range");
|
||||||
_rangesByAddress.Remove(address);
|
_rangesByAddress.Remove(address);
|
||||||
range = new TrackedRange
|
range = null;
|
||||||
{
|
|
||||||
Address = address,
|
|
||||||
ByteCount = byteCount,
|
|
||||||
Start = start,
|
|
||||||
End = start + length,
|
|
||||||
WriteGeneration = writeGeneration,
|
|
||||||
};
|
|
||||||
_rangesByAddress[address] = range;
|
|
||||||
RebuildSnapshotLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range is null)
|
if (range is null)
|
||||||
@@ -284,31 +248,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the monotonic first-write generation for a tracked allocation.
|
|
||||||
/// Unlike the consuming dirty flag, this remains changed after another
|
|
||||||
/// cache owner consumes and re-arms the range.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
|
||||||
{
|
|
||||||
generation = 0;
|
|
||||||
if (!_enabled)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generation = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
@@ -327,17 +266,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
var end = address > ulong.MaxValue - byteCount
|
var end = address > ulong.MaxValue - byteCount
|
||||||
? ulong.MaxValue
|
? ulong.MaxValue
|
||||||
: address + byteCount;
|
: address + byteCount;
|
||||||
|
|
||||||
// Fast rejection for the hot path: this runs on every managed guest
|
|
||||||
// write, and almost none of them touch tracked texture pages. The
|
|
||||||
// bounds live inside the snapshot so they are always consistent with
|
|
||||||
// the ranges the per-page visit below would consult.
|
|
||||||
var snapshot = Volatile.Read(ref _rangeSnapshot);
|
|
||||||
if (snapshot.Ranges.Length == 0 || end <= snapshot.Start || address >= snapshot.End)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = address;
|
var candidate = address;
|
||||||
while (candidate < end)
|
while (candidate < end)
|
||||||
{
|
{
|
||||||
@@ -383,7 +311,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranges = Volatile.Read(ref _rangeSnapshot).Ranges;
|
var ranges = Volatile.Read(ref _rangeSnapshot);
|
||||||
var writableStart = ulong.MaxValue;
|
var writableStart = ulong.MaxValue;
|
||||||
var writableEnd = 0UL;
|
var writableEnd = 0UL;
|
||||||
for (var index = 0; index < ranges.Length; index++)
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
@@ -462,10 +390,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
if (wasArmed)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref range.WriteGeneration);
|
|
||||||
}
|
|
||||||
if (wasArmed &&
|
if (wasArmed &&
|
||||||
range.TraceLifetime &&
|
range.TraceLifetime &&
|
||||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
@@ -534,7 +458,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
|
|
||||||
private static void RebuildSnapshotLocked()
|
private static void RebuildSnapshotLocked()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _rangeSnapshot, new RangeSnapshot(_rangesByAddress.Values.ToArray()));
|
_rangeSnapshot = _rangesByAddress.Values.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class GuestTlsTemplate
|
|||||||
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||||
// modules can require more than one host page of Variant II static TLS;
|
// modules can require more than one host page of Variant II static TLS;
|
||||||
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||||
public const ulong StartupStaticTlsReservation = 0x20000UL; // Was 0x10000UL, but thats too small for GTA V
|
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||||
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
|
||||||
|
|
||||||
// This tool monitors guest-memory writes only when a watch mode is active.
|
|
||||||
public static class GuestWriteWatch
|
|
||||||
{
|
|
||||||
private const ulong WatchBytes = 8;
|
|
||||||
private const int MaxBulkReports = 64;
|
|
||||||
|
|
||||||
private static readonly ulong WatchBase = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_WRITE"));
|
|
||||||
|
|
||||||
private static readonly bool WatchPoolHeaders = IsEnabled("SHARPEMU_WATCH_POOL_HEADER");
|
|
||||||
|
|
||||||
private static readonly ulong[] PoolSlots = new ulong[64];
|
|
||||||
private static int _poolSlotCount;
|
|
||||||
|
|
||||||
private static readonly bool WatchValuePattern = IsEnabled("SHARPEMU_WATCH_VALUE_PATTERN");
|
|
||||||
|
|
||||||
private static readonly bool WatchValue1 = IsEnabled("SHARPEMU_WATCH_VALUE1");
|
|
||||||
|
|
||||||
private const ulong DirectBandLow = 0x100_0000_0000;
|
|
||||||
private const ulong DirectBandHigh = 0x1000_0000_0000;
|
|
||||||
private static int _value1Reports;
|
|
||||||
|
|
||||||
private static readonly bool WatchBulkTorn = IsEnabled("SHARPEMU_WATCH_BULK_TORN");
|
|
||||||
|
|
||||||
private static readonly ulong BulkDestHigh = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_BULK_DEST_HI"));
|
|
||||||
private static int _bulkTornReports;
|
|
||||||
private static int _bulkShiftReports;
|
|
||||||
|
|
||||||
public static bool Armed =>
|
|
||||||
WatchBase != 0 || WatchPoolHeaders || WatchValuePattern || WatchValue1 || WatchBulkTorn;
|
|
||||||
|
|
||||||
public static void OnDirectMapping(ulong mappedAddress, ulong length, int protection)
|
|
||||||
{
|
|
||||||
if (!WatchPoolHeaders || !IsPoolMapping(length, protection))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = Interlocked.Increment(ref _poolSlotCount) - 1;
|
|
||||||
if (index < PoolSlots.Length)
|
|
||||||
{
|
|
||||||
Volatile.Write(ref PoolSlots[index], mappedAddress + 0x40);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write armed on pool header slot 0x{mappedAddress + 0x40:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Check(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
if (WatchBulkTorn &&
|
|
||||||
data.Length >= 8 &&
|
|
||||||
(BulkDestHigh != 0
|
|
||||||
? (address >> 32) == BulkDestHigh
|
|
||||||
: address >= DirectBandLow && address < DirectBandHigh))
|
|
||||||
{
|
|
||||||
for (var offset = FirstAlignedOffset(address); offset + 8 <= data.Length; offset += 8)
|
|
||||||
{
|
|
||||||
var qword = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
|
|
||||||
var kind = ClassifyBulkValue(qword);
|
|
||||||
if (kind is not null && ReserveBulkReport(kind))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_bulk_torn HIT ({kind}) " +
|
|
||||||
$"dest=0x{address + (ulong)offset:X16} (base=0x{address:X16}+0x{offset:X}) " +
|
|
||||||
$"len={data.Length} qword=0x{qword:X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValue1 &&
|
|
||||||
address >= DirectBandLow && address < DirectBandHigh &&
|
|
||||||
data.Length is >= 1 and <= 8 &&
|
|
||||||
LittleEndianValue(data) == 1 &&
|
|
||||||
Interlocked.Increment(ref _value1Reports) <= 128)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValuePattern && data.Length == 8)
|
|
||||||
{
|
|
||||||
var value = BinaryPrimitives.ReadUInt64LittleEndian(data);
|
|
||||||
if ((value & 0xFFFFFFFF) == 1 && value >> 32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchBase != 0 && Overlaps(address, data.Length, WatchBase))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var slots = Math.Min(Volatile.Read(ref _poolSlotCount), PoolSlots.Length);
|
|
||||||
for (var i = 0; i < slots; i++)
|
|
||||||
{
|
|
||||||
var slot = Volatile.Read(ref PoolSlots[i]);
|
|
||||||
if (slot != 0 && Overlaps(address, data.Length, slot))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string? ClassifyBulkValue(ulong qword)
|
|
||||||
{
|
|
||||||
var low32 = qword & 0xFFFFFFFF;
|
|
||||||
var high32 = qword >> 32;
|
|
||||||
if (low32 == 1 && high32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
return "torn";
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefix = low32 & 0xFF00_0000;
|
|
||||||
var hasShiftedPointerPrefix = prefix is 0x0800_0000 or 0x8000_0000;
|
|
||||||
return high32 == 0 && hasShiftedPointerPrefix && (low32 & 0xFF) == 0
|
|
||||||
? "shift"
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static int FirstAlignedOffset(ulong address) =>
|
|
||||||
(int)((8 - (address & 7)) & 7);
|
|
||||||
|
|
||||||
internal static bool IsPoolMapping(ulong length, int protection) =>
|
|
||||||
length == 0x10000 && protection == 0xF2;
|
|
||||||
|
|
||||||
internal static bool Overlaps(ulong address, int length, ulong slot)
|
|
||||||
{
|
|
||||||
if (length <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeLength = (ulong)length - 1;
|
|
||||||
var writeEnd = address > ulong.MaxValue - writeLength
|
|
||||||
? ulong.MaxValue
|
|
||||||
: address + writeLength;
|
|
||||||
var slotEnd = slot > ulong.MaxValue - (WatchBytes - 1)
|
|
||||||
? ulong.MaxValue
|
|
||||||
: slot + WatchBytes - 1;
|
|
||||||
return address <= slotEnd && slot <= writeEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong LittleEndianValue(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
ulong value = 0;
|
|
||||||
for (var i = 0; i < data.Length; i++)
|
|
||||||
{
|
|
||||||
value |= (ulong)data[i] << (i * 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Report(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write HIT addr=0x{address:X16} len={data.Length} " +
|
|
||||||
$"first_qword=0x{LittleEndianValue(data):X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEnabled(string name) =>
|
|
||||||
string.Equals(Environment.GetEnvironmentVariable(name), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static bool ReserveBulkReport(string kind) =>
|
|
||||||
kind == "torn"
|
|
||||||
? Interlocked.Increment(ref _bulkTornReports) <= MaxBulkReports
|
|
||||||
: Interlocked.Increment(ref _bulkShiftReports) <= MaxBulkReports;
|
|
||||||
|
|
||||||
internal static ulong Parse(string? text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.Trim();
|
|
||||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
text = text[2..];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
|
|
||||||
? value
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,4 @@ public interface ICpuMemory
|
|||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
|
|
||||||
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,6 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
|||||||
{
|
{
|
||||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Backs an entire fixed-address range, matching the guest's
|
|
||||||
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
|
|
||||||
/// reserves the range in one all-or-nothing host call, this walks the range and
|
|
||||||
/// fills only the sub-ranges that are not already backed. That keeps a fixed
|
|
||||||
/// mapping whole when part of the requested window is already occupied — the
|
|
||||||
/// partial-overlap case where the single-call reservation fails outright and
|
|
||||||
/// leaves the remainder unmapped for the guest to fault into.
|
|
||||||
/// </summary>
|
|
||||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
|
||||||
|
|
||||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+184
-828
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using SharpEmu.Libs.Gpu;
|
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
@@ -27,8 +26,8 @@ internal static class AgcShaderCompilerHooks
|
|||||||
internal static void Install()
|
internal static void Install()
|
||||||
{
|
{
|
||||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||||
KernelMemoryCompatExports.TryReadShaderGuestMemory;
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||||
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||||
GuestDataPool.Shared;
|
VulkanVideoPresenter.GuestDataPool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -19,11 +16,8 @@ namespace SharpEmu.Libs.Agc;
|
|||||||
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||||
/// equations are being ported.
|
/// equations are being ported.
|
||||||
/// </summary>
|
/// </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
|
// 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
|
// 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.
|
// 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);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
private static readonly HashSet<uint> _reportedModes = new();
|
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;
|
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>
|
/// <summary>
|
||||||
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||||
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||||
@@ -418,83 +246,50 @@ internal static unsafe class GnmTiling
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address tables depend only on the swizzle equation and element size,
|
// Precompute the within-block element offset for each (x, y) inside a
|
||||||
// so retain them across textures instead of rebuilding them per upload.
|
// 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 hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||||
var patternTerms = hasExactXorPattern
|
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||||
? _patternTermCache.GetOrAdd(
|
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||||
(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 sourceAddress = (nint)tiledPointer;
|
for (var bx = 0; bx < blockWidth; bx++)
|
||||||
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) =>
|
|
||||||
{
|
{
|
||||||
var blockY = y / blockHeight;
|
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||||
var inBlockY = y & (blockHeight - 1);
|
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||||
var rowBlockBase = (long)blockY * blocksPerRow;
|
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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,
|
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 readonly AddressBit Zero = new(0, 0);
|
||||||
|
|
||||||
private static AddressBit X(int bit) => new(1u << bit, 0);
|
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||||
@@ -614,20 +335,14 @@ internal static unsafe class GnmTiling
|
|||||||
return pattern.Length != 0;
|
return pattern.Length != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The AddrLib within-block byte offset is a per-bit XOR equation:
|
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
uint offset = 0;
|
uint offset = 0;
|
||||||
for (var bit = 0; bit < pattern.Length; bit++)
|
for (var bit = 0; bit < pattern.Length; bit++)
|
||||||
{
|
{
|
||||||
var mask = useX ? pattern[bit].XMask : pattern[bit].YMask;
|
var equation = pattern[bit];
|
||||||
var parity = System.Numerics.BitOperations.PopCount(coordinate & mask) & 1;
|
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
|
||||||
|
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
|
||||||
offset |= (uint)parity << bit;
|
offset |= (uint)parity << bit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,26 +37,10 @@ internal static class GpuWaitRegistry
|
|||||||
public long RegisteredTicks;
|
public long RegisteredTicks;
|
||||||
public bool StaleReported;
|
public bool StaleReported;
|
||||||
public object? State;
|
public object? State;
|
||||||
// Latched by LatchSatisfiedByValue when a producer wrote a value that
|
|
||||||
// satisfies this waiter. The label is frequently reused (reset to 0 for
|
|
||||||
// the next frame) immediately after the producing write, so re-reading
|
|
||||||
// guest memory at wake time can miss the transient satisfied window.
|
|
||||||
// Latching records satisfaction at the moment of the write instead.
|
|
||||||
public bool Latched;
|
|
||||||
// Non-zero for indirect-dispatch dimension retries: a bounded deadline
|
|
||||||
// (Stopwatch ticks) after which the waiter is resumed even if unsatisfied,
|
|
||||||
// so a legitimately empty indirect dispatch can never stall forever.
|
|
||||||
public long RetryDeadlineTicks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||||
// The last value each label producer wrote. Used only by the deadlock
|
|
||||||
// breaker: our serial submission parser cannot model two GPU queues running
|
|
||||||
// concurrently, so a label written -> reset -> re-waited across queues can
|
|
||||||
// cycle forever even though a real producer did signal it. Keyed by (memory,
|
|
||||||
// address) so distinct guest processes never alias.
|
|
||||||
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
|
|
||||||
|
|
||||||
public static int Count
|
public static int Count
|
||||||
{
|
{
|
||||||
@@ -130,14 +114,8 @@ internal static class GpuWaitRegistry
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var satisfied = list[i].Latched;
|
var value = readValue(address, list[i].Is64Bit);
|
||||||
if (!satisfied)
|
if (value is null || !Compare(list[i], value.Value))
|
||||||
{
|
|
||||||
var value = readValue(address, list[i].Is64Bit);
|
|
||||||
satisfied = value is not null && Compare(list[i], value.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!satisfied)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -258,201 +236,6 @@ internal static class GpuWaitRegistry
|
|||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records satisfaction for every waiter at <paramref name="address"/> whose
|
|
||||||
/// condition is met by <paramref name="value"/> — the value a producer just
|
|
||||||
/// wrote to that label. Called from the ordered producer side effect so a
|
|
||||||
/// same-frame label reset cannot lose the wakeup. The waiters stay registered
|
|
||||||
/// (latched) and are drained by the next CollectSatisfied. Returns true when
|
|
||||||
/// at least one waiter latched, so the caller can trigger a wake pass.
|
|
||||||
/// </summary>
|
|
||||||
public static bool LatchSatisfiedByValue(object memory, ulong address, ulong value)
|
|
||||||
{
|
|
||||||
var latchedAny = false;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_waiters.TryGetValue(address, out var list))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < list.Count; i++)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (waiter.Latched ||
|
|
||||||
!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
!Compare(waiter, value))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
waiter.Latched = true;
|
|
||||||
list[i] = waiter;
|
|
||||||
latchedAny = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return latchedAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes and returns waiters carrying a <see cref="WaitingDcb.RetryDeadlineTicks"/>
|
|
||||||
/// that has elapsed. Used for indirect-dispatch dimension retries: the caller
|
|
||||||
/// resumes them so a genuinely empty dispatch (dims that never become non-zero)
|
|
||||||
/// is dropped after a bounded wait instead of stalling the queue forever.
|
|
||||||
/// </summary>
|
|
||||||
public static List<WaitingDcb>? CollectExpiredRetries(object memory, long nowTicks)
|
|
||||||
{
|
|
||||||
List<WaitingDcb>? expired = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
List<ulong>? emptied = null;
|
|
||||||
foreach (var (address, list) in _waiters)
|
|
||||||
{
|
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (waiter.RetryDeadlineTicks == 0 ||
|
|
||||||
!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
nowTicks < waiter.RetryDeadlineTicks)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
expired ??= new List<WaitingDcb>();
|
|
||||||
expired.Add(waiter);
|
|
||||||
list.RemoveAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count == 0)
|
|
||||||
{
|
|
||||||
emptied ??= new List<ulong>();
|
|
||||||
emptied.Add(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptied is not null)
|
|
||||||
{
|
|
||||||
foreach (var address in emptied)
|
|
||||||
{
|
|
||||||
_waiters.Remove(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_lastProduced.Count >= 8192)
|
|
||||||
{
|
|
||||||
_lastProduced.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastProduced[(memory, address)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return LatchSatisfiedByValue(memory, address, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Breaks cross-queue GPU deadlocks the serial parser cannot avoid: returns
|
|
||||||
/// (and removes) waiters that have been stuck longer than
|
|
||||||
/// <paramref name="minAgeTicks"/> and whose condition is satisfied by the
|
|
||||||
/// last value a real producer wrote to their label — even though guest
|
|
||||||
/// memory has since been reset. Never fabricates a value: a waiter is only
|
|
||||||
/// released when an actual producer signalled it at least once.
|
|
||||||
/// </summary>
|
|
||||||
public static List<WaitingDcb>? CollectDeadlockBroken(
|
|
||||||
object memory,
|
|
||||||
long nowTicks,
|
|
||||||
long minAgeTicks)
|
|
||||||
{
|
|
||||||
List<WaitingDcb>? broken = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
List<ulong>? emptied = null;
|
|
||||||
foreach (var (address, list) in _waiters)
|
|
||||||
{
|
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var waiter = list[i];
|
|
||||||
if (!ReferenceEquals(waiter.Memory, memory) ||
|
|
||||||
nowTicks - waiter.RegisteredTicks < minAgeTicks ||
|
|
||||||
!_lastProduced.TryGetValue((memory, address), out var produced) ||
|
|
||||||
!Compare(waiter, produced))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
broken ??= new List<WaitingDcb>();
|
|
||||||
broken.Add(waiter);
|
|
||||||
list.RemoveAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count == 0)
|
|
||||||
{
|
|
||||||
emptied ??= new List<ulong>();
|
|
||||||
emptied.Add(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptied is not null)
|
|
||||||
{
|
|
||||||
foreach (var address in emptied)
|
|
||||||
{
|
|
||||||
_waiters.Remove(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||||
{
|
{
|
||||||
var masked = value & waiter.Mask;
|
var masked = value & waiter.Mask;
|
||||||
@@ -477,7 +260,6 @@ internal static class GpuWaitRegistry
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_waiters.Clear();
|
_waiters.Clear();
|
||||||
_lastProduced.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using SharpEmu.Libs.Kernel;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Ampr;
|
namespace SharpEmu.Libs.Ampr;
|
||||||
|
|
||||||
@@ -44,17 +43,17 @@ public static class AmprExports
|
|||||||
{
|
{
|
||||||
public CachedHostFile(string path)
|
public CachedHostFile(string path)
|
||||||
{
|
{
|
||||||
Handle = File.OpenHandle(
|
Stream = new FileStream(
|
||||||
path,
|
path,
|
||||||
FileMode.Open,
|
FileMode.Open,
|
||||||
FileAccess.Read,
|
FileAccess.Read,
|
||||||
FileShare.ReadWrite | FileShare.Delete,
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
|
bufferSize: 1024 * 1024,
|
||||||
FileOptions.RandomAccess);
|
FileOptions.RandomAccess);
|
||||||
Length = RandomAccess.GetLength(Handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SafeFileHandle Handle { get; }
|
public object Gate { get; } = new();
|
||||||
public long Length { get; }
|
public FileStream Stream { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -340,18 +339,6 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "tZDDEo2tE5k",
|
Nid = "tZDDEo2tE5k",
|
||||||
ExportName = "sceAmprCommandBufferGetSize",
|
ExportName = "sceAmprCommandBufferGetSize",
|
||||||
@@ -521,32 +508,6 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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)
|
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
|
||||||
{
|
{
|
||||||
if (commandBuffer == 0)
|
if (commandBuffer == 0)
|
||||||
@@ -774,7 +735,13 @@ public static class AmprExports
|
|||||||
return openResult;
|
return openResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileOffset >= (ulong)cachedFile.Length)
|
long fileLength;
|
||||||
|
lock (cachedFile.Gate)
|
||||||
|
{
|
||||||
|
fileLength = cachedFile.Stream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileOffset >= (ulong)fileLength)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -793,10 +760,12 @@ public static class AmprExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
var read = RandomAccess.Read(
|
int read;
|
||||||
cachedFile.Handle,
|
lock (cachedFile.Gate)
|
||||||
buffer.AsSpan(0, request),
|
{
|
||||||
unchecked((long)absoluteOffset));
|
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
||||||
|
read = cachedFile.Stream.Read(buffer, 0, request);
|
||||||
|
}
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,33 +121,6 @@ public static class AppContentExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download data is not emulated as a real quota; report a comfortable
|
|
||||||
// fixed amount of free space so titles never take the "storage full" path.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Gl6w5i0JokY",
|
|
||||||
ExportName = "sceAppContentDownloadDataGetAvailableSpaceKb",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAppContent")]
|
|
||||||
public static int AppContentDownloadDataGetAvailableSpaceKb(CpuContext ctx)
|
|
||||||
{
|
|
||||||
const ulong availableSpaceKb = 1024UL * 1024UL; // 1 GiB
|
|
||||||
var availableSpaceAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (availableSpaceAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> spaceBytes = stackalloc byte[sizeof(ulong)];
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(spaceBytes, availableSpaceKb);
|
|
||||||
if (!ctx.Memory.TryWrite(availableSpaceAddress, spaceBytes))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
||||||
{
|
{
|
||||||
value = 0;
|
value = 0;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class AjmExports
|
|||||||
private const int OrbisAjmErrorCodecAlreadyRegistered = unchecked((int)0x80930009);
|
private const int OrbisAjmErrorCodecAlreadyRegistered = unchecked((int)0x80930009);
|
||||||
private const int OrbisAjmErrorCodecNotRegistered = unchecked((int)0x8093000A);
|
private const int OrbisAjmErrorCodecNotRegistered = unchecked((int)0x8093000A);
|
||||||
private const int OrbisAjmErrorWrongRevisionFlag = unchecked((int)0x8093000B);
|
private const int OrbisAjmErrorWrongRevisionFlag = unchecked((int)0x8093000B);
|
||||||
private const uint MaxCodecType = 25;
|
private const uint MaxCodecType = 23;
|
||||||
private const int MaxInstanceIndex = 0x2FFF;
|
private const int MaxInstanceIndex = 0x2FFF;
|
||||||
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
|
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
|
||||||
private static int _nextContextId;
|
private static int _nextContextId;
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ public static class AudioOutExports
|
|||||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||||
private static int _nextPortHandle;
|
private static int _nextPortHandle;
|
||||||
|
|
||||||
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
|
||||||
// guest submits real samples or silence. Gated so it costs nothing when off.
|
|
||||||
private static readonly bool _traceOutput = string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT"), "1", StringComparison.Ordinal);
|
|
||||||
private static long _outputCount;
|
|
||||||
|
|
||||||
private sealed class PortState : IDisposable
|
private sealed class PortState : IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _paceGate = new();
|
private readonly object _paceGate = new();
|
||||||
@@ -161,37 +155,6 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "GrQ9s4IrNaQ",
|
|
||||||
ExportName = "sceAudioOutGetPortState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutGetPortState(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (stateAddress == 0 || !Ports.TryGetValue(handle, out var port))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SceAudioOutPortState: report a connected primary output at full volume
|
|
||||||
// so pacing/mixing code sees a live port. We do no host rerouting, so
|
|
||||||
// rerouteCounter and flag stay zero.
|
|
||||||
Span<byte> state = stackalloc byte[16];
|
|
||||||
state.Clear();
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(state, 1);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(
|
|
||||||
state[2..], (ushort)port.Channels);
|
|
||||||
state[7] = 127;
|
|
||||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QOQtbeDqsT4",
|
Nid = "QOQtbeDqsT4",
|
||||||
ExportName = "sceAudioOutOutput",
|
ExportName = "sceAudioOutOutput",
|
||||||
@@ -203,12 +166,7 @@ public static class AudioOutExports
|
|||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!Ports.TryGetValue(handle, out var port))
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
{
|
{
|
||||||
// Host shutdown disposes the ports while guest audio threads are
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
// still draining their last buffers; report success so the guest
|
|
||||||
// winds down without a per-buffer error (and its WARN log flood).
|
|
||||||
return ctx.SetReturn(_shutdown
|
|
||||||
? 0
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceAddress == 0)
|
if (sourceAddress == 0)
|
||||||
@@ -225,17 +183,6 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceOutput)
|
|
||||||
{
|
|
||||||
var n = Interlocked.Increment(ref _outputCount);
|
|
||||||
if (n <= 8 || n % 200 == 0)
|
|
||||||
{
|
|
||||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port.Backend is null)
|
if (port.Backend is null)
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
@@ -319,40 +266,8 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peak normalized amplitude [0,1] of an interleaved PCM buffer, used only by
|
|
||||||
// the SHARPEMU_LOG_AUDIO_OUT diagnostic to distinguish real audio from silence.
|
|
||||||
private static float PeakAmplitude(ReadOnlySpan<byte> source, bool isFloat, int bytesPerSample)
|
|
||||||
{
|
|
||||||
var peak = 0f;
|
|
||||||
if (isFloat && bytesPerSample == 4)
|
|
||||||
{
|
|
||||||
for (var i = 0; i + 4 <= source.Length; i += 4)
|
|
||||||
{
|
|
||||||
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(source.Slice(i, 4)));
|
|
||||||
if (v > peak)
|
|
||||||
{
|
|
||||||
peak = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (bytesPerSample == 2)
|
|
||||||
{
|
|
||||||
for (var i = 0; i + 2 <= source.Length; i += 2)
|
|
||||||
{
|
|
||||||
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(source.Slice(i, 2)) / 32768f);
|
|
||||||
if (v > peak)
|
|
||||||
{
|
|
||||||
peak = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ShutdownAllPorts()
|
public static void ShutdownAllPorts()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _shutdown, true);
|
|
||||||
foreach (var handle in Ports.Keys)
|
foreach (var handle in Ports.Keys)
|
||||||
{
|
{
|
||||||
if (Ports.TryRemove(handle, out var port))
|
if (Ports.TryRemove(handle, out var port))
|
||||||
@@ -362,8 +277,6 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool _shutdown;
|
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
private static bool TryGetFormat(
|
||||||
int rawFormat,
|
int rawFormat,
|
||||||
out int channels,
|
out int channels,
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ internal static class AudioPcmConversion
|
|||||||
float volume)
|
float volume)
|
||||||
{
|
{
|
||||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
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++)
|
for (var frame = 0; frame < frames; frame++)
|
||||||
{
|
{
|
||||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||||
@@ -36,8 +32,8 @@ internal static class AudioPcmConversion
|
|||||||
var right = channels == 1
|
var right = channels == 1
|
||||||
? left
|
? left
|
||||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
left = ApplyVolume(left, clampedVolume);
|
left = ApplyVolume(left, volume);
|
||||||
right = ApplyVolume(right, clampedVolume);
|
right = ApplyVolume(right, volume);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||||
}
|
}
|
||||||
@@ -71,10 +67,9 @@ internal static class AudioPcmConversion
|
|||||||
return checked((short)MathF.Round(value * scale));
|
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)
|
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);
|
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
|
||||||
|
|
||||||
// PS5 acoustic-propagation (3D-audio ray/portal/room) module. We do not model
|
|
||||||
// acoustic propagation; the geometry-driven reverb/occlusion it produces is a
|
|
||||||
// quality feature, not a correctness gate. Games (e.g. Astro Bot) call it
|
|
||||||
// during audio init and hard-assert if any entry point is missing:
|
|
||||||
// ASSERT ... sceAudioPropagationSystemQueryMemory failed : 0x80020002
|
|
||||||
// The API is placement-style: QueryMemory reports a buffer size, the game
|
|
||||||
// allocates it, and the "system"/objects live inside that caller-owned buffer,
|
|
||||||
// so success-returning stubs let init proceed without us owning any state.
|
|
||||||
public static class AudioPropagationExports
|
|
||||||
{
|
|
||||||
private const int Ok = 0;
|
|
||||||
|
|
||||||
// QueryMemory reports the working-set size the caller must allocate before
|
|
||||||
// SystemCreate. rsi points at the out size/alignment; write a modest,
|
|
||||||
// aligned block so the caller's allocation succeeds.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7xyAxrusLko",
|
|
||||||
ExportName = "sceAudioPropagationSystemQueryMemory",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemQueryMemory(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var outAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (outAddress != 0)
|
|
||||||
{
|
|
||||||
// {size, alignment} — 1 MiB / 256 B covers the caller's allocation.
|
|
||||||
ctx.TryWriteUInt64(outAddress, 0x10_0000);
|
|
||||||
ctx.TryWriteUInt64(outAddress + sizeof(ulong), 0x100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "GrA9ke1QT+E", ExportName = "sceAudioPropagationSystemQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "aNEqtSHdUSo", ExportName = "sceAudioPropagationSystemCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "x5VPqg5iyAk", ExportName = "sceAudioPropagationSystemDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ile38Gl-p5M", ExportName = "sceAudioPropagationSystem", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int System(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "cMl3u+7QBBM", ExportName = "sceAudioPropagationSystemMemoryInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemMemoryInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "3B9IabLByyM", ExportName = "sceAudioPropagationSystemOptionInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemOptionInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "B2KI2AachWE", ExportName = "sceAudioPropagationSystemLock", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemLock(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "kIdb+iQUzCs", ExportName = "sceAudioPropagationSystemSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "VlBT16890mA", ExportName = "sceAudioPropagationSystemSetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemSetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ht-QXT3zGxo", ExportName = "sceAudioPropagationSystemGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "CPLV6G-eXmk", ExportName = "sceAudioPropagationSystemRegisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemRegisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "XKCN4gpeYsM", ExportName = "sceAudioPropagationSystemUnregisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SystemUnregisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "8bI5h8req30", ExportName = "sceAudioPropagationRoomCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RoomCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "S0JwP2AFTTE", ExportName = "sceAudioPropagationRoomDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RoomDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "b-dYXrjSNZU", ExportName = "sceAudioPropagationPortalCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ZQXE-xS6MTE", ExportName = "sceAudioPropagationPortalDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "WXMhENV2NcA", ExportName = "sceAudioPropagationPortalSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "i687TNRF+hw", ExportName = "sceAudioPropagationPortalSettingsInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PortalSettingsInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "d84otraxt2s", ExportName = "sceAudioPropagationSourceCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceCreate(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "wkseM3LWPuc", ExportName = "sceAudioPropagationSourceDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "-wsUTr31yeg", ExportName = "sceAudioPropagationSourceSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "PBcrVpEqUVY", ExportName = "sceAudioPropagationSourceCalculateAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceCalculateAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "eEeKqFeNI3o", ExportName = "sceAudioPropagationSourceGetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "G+QLTfyLMYk", ExportName = "sceAudioPropagationSourceGetAudioPathCount", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetAudioPathCount(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "aKJZx7wCma8", ExportName = "sceAudioPropagationSourceGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "3aEY9tPXGKc", ExportName = "sceAudioPropagationSourceQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "hhz9pITnC8k", ExportName = "sceAudioPropagationSourceRender", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceRender(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "SoKPzY1-3SU", ExportName = "sceAudioPropagationSourceRenderInfoInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceRenderInfoInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "tKSmk2JsMAA", ExportName = "sceAudioPropagationSourceSetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "5vzOS2pHMFc", ExportName = "sceAudioPropagationSourceSetAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "MNmGapXrYRs", ExportName = "sceAudioPropagationSourceSetAudioPathsParamInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int SourceSetAudioPathsParamInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "i-0aUex3zCE", ExportName = "sceAudioPropagationAudioPathInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int AudioPathInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "JZIkSbmt2BE", ExportName = "sceAudioPropagationAudioPathPointInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int AudioPathPointInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "tL2AEPejVQE", ExportName = "sceAudioPropagationPathGetNumPoints", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int PathGetNumPoints(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "2BSFmuKtRss", ExportName = "sceAudioPropagationMaterialInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int MaterialInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "0r2+9UTg1BA", ExportName = "sceAudioPropagationRayInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int RayInit(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "BbOT4vBwAjs", ExportName = "sceAudioPropagationResetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int ResetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "gCmQm6dvMxw", ExportName = "sceAudioPropagationReportApi", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
|
|
||||||
public static int ReportApi(CpuContext ctx) => ctx.SetReturn(Ok);
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,7 @@ public static class AvPlayerExports
|
|||||||
private const int FrameBufferCount = 3;
|
private const int FrameBufferCount = 3;
|
||||||
private const int FrameInfoSize = 40;
|
private const int FrameInfoSize = 40;
|
||||||
private const int FrameInfoExSize = 104;
|
private const int FrameInfoExSize = 104;
|
||||||
// This structure is 32 bytes. A larger write can damage the guest stack.
|
private const int StreamInfoSize = 40;
|
||||||
private const int StreamInfoSize = 32;
|
|
||||||
private const int StreamInfoExSize = 32;
|
|
||||||
private const int MaxGuestPathLength = 4096;
|
private const int MaxGuestPathLength = 4096;
|
||||||
private static readonly object StateGate = new();
|
private static readonly object StateGate = new();
|
||||||
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
||||||
@@ -406,8 +404,7 @@ public static class AvPlayerExports
|
|||||||
ExportName = "sceAvPlayerGetStreamInfoEx",
|
ExportName = "sceAvPlayerGetStreamInfoEx",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
|
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
|
||||||
GetStreamInfoCore(ctx, StreamInfoExSize);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XC9wM+xULz8",
|
Nid = "XC9wM+xULz8",
|
||||||
@@ -564,48 +561,12 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RegisterPlayerForTest(
|
|
||||||
ulong handle,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
ulong durationMilliseconds)
|
|
||||||
{
|
|
||||||
PlayerState? previous;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out previous);
|
|
||||||
Players[handle] = new PlayerState
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
DurationMilliseconds = durationMilliseconds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
previous?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemovePlayerForTest(ulong handle)
|
|
||||||
{
|
|
||||||
PlayerState? player;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out player);
|
|
||||||
}
|
|
||||||
|
|
||||||
player?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "d8FcbzfAdQw",
|
Nid = "d8FcbzfAdQw",
|
||||||
ExportName = "sceAvPlayerGetStreamInfo",
|
ExportName = "sceAvPlayerGetStreamInfo",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
public static int AvPlayerGetStreamInfo(CpuContext ctx)
|
||||||
GetStreamInfoCore(ctx, StreamInfoSize);
|
|
||||||
|
|
||||||
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
|
|
||||||
{
|
{
|
||||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
@@ -617,7 +578,7 @@ public static class AvPlayerExports
|
|||||||
return SetReturn(ctx, InvalidParameters);
|
return SetReturn(ctx, InvalidParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[infoSize];
|
Span<byte> info = stackalloc byte[StreamInfoSize];
|
||||||
info.Clear();
|
info.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
||||||
if (streamIndex == 0)
|
if (streamIndex == 0)
|
||||||
@@ -1048,7 +1009,7 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
|
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
|
||||||
if (!File.Exists(ffprobe))
|
if (!File.Exists(ffprobe))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1131,50 +1092,13 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string? FindFfmpeg() =>
|
private 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)
|
|
||||||
{
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
|
||||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||||
{
|
{
|
||||||
return configured;
|
return configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
|
||||||
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" })
|
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
||||||
{
|
{
|
||||||
if (File.Exists(candidate))
|
if (File.Exists(candidate))
|
||||||
@@ -1185,16 +1109,6 @@ public static class AvPlayerExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
|
|
||||||
Path.Combine(
|
|
||||||
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
|
|
||||||
isWindows ? "ffprobe.exe" : "ffprobe");
|
|
||||||
|
|
||||||
private static string RemovePathQuotes(string directory) =>
|
|
||||||
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
|
|
||||||
? directory[1..^1]
|
|
||||||
: directory;
|
|
||||||
|
|
||||||
internal static string? ResolveGuestPath(string guestPath)
|
internal static string? ResolveGuestPath(string guestPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(guestPath))
|
if (string.IsNullOrWhiteSpace(guestPath))
|
||||||
@@ -1204,9 +1118,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
var normalized = guestPath.Replace('\\', '/');
|
var normalized = guestPath.Replace('\\', '/');
|
||||||
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
||||||
var unrealProjectRelative =
|
var unrealProjectRelative = false;
|
||||||
normalized.StartsWith("../", StringComparison.Ordinal) ||
|
|
||||||
normalized.StartsWith("./", StringComparison.Ordinal);
|
|
||||||
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
||||||
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
||||||
uri.IsFile)
|
uri.IsFile)
|
||||||
@@ -1237,10 +1149,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
if (unrealProjectRelative)
|
if (unrealProjectRelative)
|
||||||
{
|
{
|
||||||
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
|
normalized = RemoveUnrealLeadingDotSegments(normalized);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
@@ -1324,20 +1233,15 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryRemoveUnrealLeadingDotSegments(
|
private static string RemoveUnrealLeadingDotSegments(string guestPath)
|
||||||
string guestPath,
|
|
||||||
out string normalized)
|
|
||||||
{
|
{
|
||||||
var removedParent = false;
|
|
||||||
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
||||||
guestPath.StartsWith("./", StringComparison.Ordinal))
|
guestPath.StartsWith("./", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
|
|
||||||
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized = guestPath;
|
return guestPath;
|
||||||
return !removedParent || guestPath.Contains('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
||||||
|
|||||||
@@ -18,153 +18,123 @@ namespace SharpEmu.Libs.Bink;
|
|||||||
internal static class Bink2MovieBridge
|
internal static class Bink2MovieBridge
|
||||||
{
|
{
|
||||||
private const uint MaxDimension = 16384;
|
private const uint MaxDimension = 16384;
|
||||||
private const uint MaxHostVideoWidth = 1920;
|
|
||||||
private const uint MaxHostVideoHeight = 1080;
|
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
|
private static NativeAdapter? _adapter;
|
||||||
private static string? _activePath;
|
private static string? _activePath;
|
||||||
|
private static IntPtr _activeMovie;
|
||||||
private static Bink2MovieInfo _activeInfo;
|
private static Bink2MovieInfo _activeInfo;
|
||||||
private static byte[]? _frameBuffer;
|
private static byte[]? _frameBuffer;
|
||||||
private static bool _frameBufferPresented;
|
private static bool _usingDummyMovie;
|
||||||
private static BinkFramePlayback? _playback;
|
private static bool _loadAttempted;
|
||||||
private static long _frameSerial;
|
private static bool _availabilityReported;
|
||||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
|
||||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
|
||||||
|
|
||||||
internal static bool IsHostPlaybackActive
|
/// <summary>
|
||||||
{
|
/// Returns true when the guest should receive a normal "file not found"
|
||||||
get
|
/// result for a Bink movie. This is the safe default without a decoder:
|
||||||
{
|
/// games that treat movies as optional fall through to their next state
|
||||||
lock (Gate)
|
/// rather than submitting an empty Bink GPU texture forever.
|
||||||
{
|
/// </summary>
|
||||||
return _playback is not null || _frameBuffer is not null;
|
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
||||||
}
|
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||||
}
|
ResolveMode() == MovieMode.Skip;
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetPresentationSize(uint width, uint height)
|
internal static void ObserveGuestMovie(string hostPath)
|
||||||
{
|
{
|
||||||
if (width == 0 || height == 0)
|
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!File.Exists(hostPath))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_presentationWidth = Math.Min(width, MaxHostVideoWidth);
|
|
||||||
_presentationHeight = Math.Min(height, MaxHostVideoHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true only when movie skipping was explicitly requested. Without
|
|
||||||
/// a host adapter the guest must be allowed to run the Bink implementation
|
|
||||||
/// statically linked into its executable.
|
|
||||||
/// </summary>
|
|
||||||
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
!File.Exists(hostPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return _playback is not null || _frameBuffer is not null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mode = ResolveMode();
|
if (ResolveMode() == MovieMode.Dummy)
|
||||||
if (mode is MovieMode.Guest or MovieMode.Skip)
|
|
||||||
{
|
{
|
||||||
return false;
|
AttachDummyMovieLocked(hostPath);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_playback is not null || _frameBuffer is not null)
|
var adapter = GetAdapterLocked();
|
||||||
|
if (adapter is null)
|
||||||
{
|
{
|
||||||
if (PendingMoviePathSet.Add(hostPath))
|
return;
|
||||||
{
|
|
||||||
PendingMoviePaths.Enqueue(hostPath);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Bink2 bridge queued: " +
|
|
||||||
Path.GetFileName(hostPath));
|
|
||||||
}
|
|
||||||
return PendingMoviePathSet.Contains(hostPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachMovieLocked(hostPath, mode);
|
CloseActiveLocked();
|
||||||
return string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) &&
|
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||||
(_playback is not null || _frameBuffer is not null);
|
{
|
||||||
|
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(
|
internal static bool TryDecodeNextFrame(
|
||||||
bool advanceClock,
|
|
||||||
out byte[] pixels,
|
out byte[] pixels,
|
||||||
out uint width,
|
out uint width,
|
||||||
out uint height,
|
out uint height)
|
||||||
out bool advanced,
|
|
||||||
out long frameSerial,
|
|
||||||
out string hostPath)
|
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
pixels = [];
|
pixels = [];
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
advanced = false;
|
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||||
frameSerial = _frameSerial;
|
|
||||||
hostPath = _activePath ?? string.Empty;
|
|
||||||
|
|
||||||
if (_playback is not null)
|
|
||||||
{
|
{
|
||||||
if (!_playback.TryGetFrame(advanceClock, out pixels, out advanced))
|
if (_usingDummyMovie && _frameBuffer is not null)
|
||||||
{
|
{
|
||||||
if (_playback.IsFinished)
|
pixels = _frameBuffer;
|
||||||
{
|
width = _activeInfo.Width;
|
||||||
var completedPath = _activePath;
|
height = _activeInfo.Height;
|
||||||
CloseActiveLocked();
|
return true;
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Bink2 bridge completed: " +
|
|
||||||
Path.GetFileName(completedPath));
|
|
||||||
AttachNextQueuedMovieLocked();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
width = _activeInfo.Width;
|
return false;
|
||||||
height = _activeInfo.Height;
|
|
||||||
if (advanced)
|
|
||||||
{
|
|
||||||
frameSerial = ++_frameSerial;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
pixels = _frameBuffer;
|
||||||
width = _activeInfo.Width;
|
width = _activeInfo.Width;
|
||||||
height = _activeInfo.Height;
|
height = _activeInfo.Height;
|
||||||
advanced = !_frameBufferPresented;
|
|
||||||
_frameBufferPresented = true;
|
|
||||||
if (advanced)
|
|
||||||
{
|
|
||||||
frameSerial = ++_frameSerial;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,52 +147,6 @@ internal static class Bink2MovieBridge
|
|||||||
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||||
checked((int)((ulong)info.Width * info.Height * 4));
|
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()
|
private static MovieMode ResolveMode()
|
||||||
{
|
{
|
||||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||||
@@ -241,22 +165,16 @@ internal static class Bink2MovieBridge
|
|||||||
return MovieMode.Skip;
|
return MovieMode.Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(configured, "guest", StringComparison.OrdinalIgnoreCase))
|
// With no SDK adapter present, returning "not found" makes optional
|
||||||
|
// cinematics advance. Supplying either an explicit path or the normal
|
||||||
|
// side-by-side adapter enables native playback automatically.
|
||||||
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||||
|
EnumerateAdapterCandidates().Any(File.Exists))
|
||||||
{
|
{
|
||||||
return MovieMode.Guest;
|
return MovieMode.Native;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(configured, "ffmpeg", StringComparison.OrdinalIgnoreCase))
|
return MovieMode.Skip;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AttachDummyMovieLocked(string hostPath)
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
@@ -273,61 +191,22 @@ internal static class Bink2MovieBridge
|
|||||||
_activePath = hostPath;
|
_activePath = hostPath;
|
||||||
_activeInfo = info;
|
_activeInfo = info;
|
||||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
_frameBufferPresented = false;
|
|
||||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||||
|
_usingDummyMovie = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
info.Width + "x" + info.Height + ".");
|
info.Width + "x" + info.Height + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AttachFfmpegMovieLocked(string hostPath)
|
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
info = default;
|
info = default;
|
||||||
Span<byte> header = stackalloc byte[36];
|
Span<byte> header = stackalloc byte[32];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(path);
|
using var stream = File.OpenRead(path);
|
||||||
stream.ReadExactly(header);
|
if (stream.Read(header) != header.Length ||
|
||||||
if (!header[..3].SequenceEqual("KB2"u8))
|
!header[..4].SequenceEqual("KB2j"u8))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -336,11 +215,10 @@ internal static class Bink2MovieBridge
|
|||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
|
1);
|
||||||
return info.FramesPerSecondNumerator != 0 &&
|
return true;
|
||||||
info.FramesPerSecondDenominator != 0;
|
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or EndOfStreamException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -362,23 +240,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()
|
private static void CloseActiveLocked()
|
||||||
{
|
{
|
||||||
_playback?.Dispose();
|
if (_activeMovie != IntPtr.Zero)
|
||||||
_playback = null;
|
{
|
||||||
|
_adapter?.Close(_activeMovie);
|
||||||
|
}
|
||||||
|
|
||||||
_activePath = null;
|
_activePath = null;
|
||||||
|
_activeMovie = IntPtr.Zero;
|
||||||
_activeInfo = default;
|
_activeInfo = default;
|
||||||
_frameBuffer = null;
|
_frameBuffer = null;
|
||||||
_frameBufferPresented = false;
|
_usingDummyMovie = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
internal readonly struct Bink2MovieInfo
|
private readonly struct Bink2MovieInfo
|
||||||
{
|
{
|
||||||
public readonly uint Width;
|
public readonly uint Width;
|
||||||
public readonly uint Height;
|
public readonly uint Height;
|
||||||
@@ -400,226 +335,69 @@ internal static class Bink2MovieBridge
|
|||||||
|
|
||||||
private enum MovieMode
|
private enum MovieMode
|
||||||
{
|
{
|
||||||
Guest,
|
|
||||||
Skip,
|
Skip,
|
||||||
Dummy,
|
Dummy,
|
||||||
Native,
|
Native,
|
||||||
Ffmpeg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Queue<string> PendingMoviePaths = new();
|
private sealed class NativeAdapter
|
||||||
private static readonly HashSet<string> PendingMoviePathSet =
|
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private static void AttachNextQueuedMovieLocked()
|
|
||||||
{
|
{
|
||||||
while (PendingMoviePaths.Count > 0)
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
{
|
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||||
var path = PendingMoviePaths.Dequeue();
|
|
||||||
PendingMoviePathSet.Remove(path);
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AttachMovieLocked(path, ResolveMode());
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
if (_playback is not null || _frameBuffer is not null)
|
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||||
{
|
|
||||||
return;
|
[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
|
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
while (IsTrackedLocked(hostPath))
|
adapter = null;
|
||||||
{
|
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||||
var remaining = deadline - Environment.TickCount64;
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||||
if (remaining <= 0)
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var frameCount = BinaryPrimitives.ReadUInt32LittleEndian(header[8..12]);
|
adapter = new NativeAdapter(
|
||||||
var audioTrackCount = BinaryPrimitives.ReadUInt32LittleEndian(header[40..44]);
|
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||||
if (frameCount < 2 || audioTrackCount > 256)
|
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||||
{
|
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||||
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);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (
|
|
||||||
exception is IOException or EndOfStreamException or OverflowException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly struct BinkGuestCompletionShim
|
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||||
{
|
|
||||||
private readonly uint _fileSizeMinusHeader;
|
|
||||||
private readonly uint _largestFrameSize;
|
|
||||||
|
|
||||||
internal BinkGuestCompletionShim(uint fileSizeMinusHeader, uint largestFrameSize)
|
|
||||||
{
|
{
|
||||||
_fileSizeMinusHeader = fileSizeMinusHeader;
|
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||||
_largestFrameSize = largestFrameSize;
|
try
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
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);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "cKYtVmeSTcw",
|
Nid = "cKYtVmeSTcw",
|
||||||
ExportName = "sceFontOpenFontSet",
|
ExportName = "sceFontOpenFontSet",
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The pool backing AGC-to-presenter ownership transfers, shared by every backend
|
|
||||||
/// (the AGC layer rents, the presenter returns, so both sides must use one pool).
|
|
||||||
/// Guest draw snapshots churn through a small set of 128 KiB-16 MiB size classes
|
|
||||||
/// thousands of times per second; the process-wide shared pool trims and
|
|
||||||
/// repartitions those large arrays aggressively under GC load, causing hundreds of
|
|
||||||
/// MiB/s of replacement byte[] allocations, so this pool is bounded and non-shared.
|
|
||||||
/// </summary>
|
|
||||||
internal static class GuestDataPool
|
|
||||||
{
|
|
||||||
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
|
|
||||||
maxArrayLength: 16 * 1024 * 1024,
|
|
||||||
maxCachedBytes: 256UL * 1024 * 1024,
|
|
||||||
maxArraysPerBucket: 8);
|
|
||||||
|
|
||||||
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
|
|
||||||
|
|
||||||
private sealed class BoundedByteArrayPool : ArrayPool<byte>
|
|
||||||
{
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private readonly int _maxArrayLength;
|
|
||||||
private readonly ulong _maxCachedBytes;
|
|
||||||
private readonly int _maxArraysPerBucket;
|
|
||||||
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
|
|
||||||
private readonly HashSet<byte[]> _leases =
|
|
||||||
new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
|
|
||||||
private ulong _cachedBytes;
|
|
||||||
|
|
||||||
public BoundedByteArrayPool(
|
|
||||||
int maxArrayLength,
|
|
||||||
ulong maxCachedBytes,
|
|
||||||
int maxArraysPerBucket)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength);
|
|
||||||
ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes);
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket);
|
|
||||||
_maxArrayLength = maxArrayLength;
|
|
||||||
_maxCachedBytes = maxCachedBytes;
|
|
||||||
_maxArraysPerBucket = maxArraysPerBucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override byte[] Rent(int minimumLength)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(minimumLength);
|
|
||||||
var length = GetAllocationLength(minimumLength);
|
|
||||||
byte[]? array = null;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (length <= _maxArrayLength &&
|
|
||||||
_cachedByBucket.TryGetValue(length, out var bucket) &&
|
|
||||||
bucket.TryPop(out array))
|
|
||||||
{
|
|
||||||
_cachedBytes -= (ulong)array.LongLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
array ??= new byte[length];
|
|
||||||
_leases.Add(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Return(byte[] array, bool clearArray = false)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_leases.Remove(array))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearArray)
|
|
||||||
{
|
|
||||||
Array.Clear(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (array.Length > _maxArrayLength ||
|
|
||||||
!IsBucketLength(array.Length) ||
|
|
||||||
(ulong)array.LongLength > _maxCachedBytes -
|
|
||||||
Math.Min(_cachedBytes, _maxCachedBytes))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_cachedByBucket.TryGetValue(array.Length, out var bucket))
|
|
||||||
{
|
|
||||||
bucket = new Stack<byte[]>();
|
|
||||||
_cachedByBucket.Add(array.Length, bucket);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bucket.Count >= _maxArraysPerBucket)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bucket.Push(array);
|
|
||||||
_cachedBytes += (ulong)array.LongLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Trim()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_cachedByBucket.Clear();
|
|
||||||
_cachedBytes = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetAllocationLength(int minimumLength)
|
|
||||||
{
|
|
||||||
if (minimumLength <= 16)
|
|
||||||
{
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minimumLength > _maxArrayLength)
|
|
||||||
{
|
|
||||||
return minimumLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsBucketLength(int length) =>
|
|
||||||
length >= 16 && (length & (length - 1)) == 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Libs.Gpu.Metal;
|
|
||||||
using SharpEmu.Libs.Gpu.Vulkan;
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
@@ -9,39 +8,11 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
||||||
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
||||||
/// Vulkan is the default everywhere; SHARPEMU_GPU_BACKEND=metal opts into the Metal
|
/// Vulkan is the only backend today; Metal/DX12 slot in here.
|
||||||
/// backend (macOS only) while it is being brought up. macOS flips to Metal by default
|
|
||||||
/// once the presenter reaches parity.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class GuestGpu
|
internal static class GuestGpu
|
||||||
{
|
{
|
||||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(Create);
|
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||||
|
|
||||||
public static IGuestGpuBackend Current => Instance.Value;
|
public static IGuestGpuBackend Current => Instance.Value;
|
||||||
|
|
||||||
private static IGuestGpuBackend Create()
|
|
||||||
{
|
|
||||||
var requested = Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND");
|
|
||||||
if (string.IsNullOrEmpty(requested) || requested.Equals("vulkan", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requested.Equals("metal", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] SHARPEMU_GPU_BACKEND=metal is only available on macOS; using Vulkan.");
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] GPU backend: Metal (SHARPEMU_GPU_BACKEND).");
|
|
||||||
return new MetalGuestGpuBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] Unknown SHARPEMU_GPU_BACKEND value '{requested}'; using Vulkan.");
|
|
||||||
return new VulkanGuestGpuBackend();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ internal sealed record GuestDrawTexture(
|
|||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
GuestSampler Sampler = default,
|
GuestSampler Sampler = default);
|
||||||
// Guest CPU write-tracker generation of the memory RgbaPixels was read
|
|
||||||
// from; -1 when the range is untracked or the pixels were not read here.
|
|
||||||
long WriteGeneration = -1,
|
|
||||||
bool ArrayedView = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||||
internal readonly record struct GuestSampler(
|
internal readonly record struct GuestSampler(
|
||||||
@@ -41,22 +36,6 @@ internal readonly record struct GuestSampler(
|
|||||||
uint Word2,
|
uint Word2,
|
||||||
uint Word3);
|
uint Word3);
|
||||||
|
|
||||||
/// <summary>Identity of a texture's content in a backend texture cache, keyed
|
|
||||||
/// entirely on raw guest descriptor values; the AGC layer uses it to skip texel
|
|
||||||
/// copies for content the backend already holds.</summary>
|
|
||||||
internal readonly record struct TextureContentIdentity(
|
|
||||||
ulong Address,
|
|
||||||
uint Width,
|
|
||||||
uint Height,
|
|
||||||
uint Format,
|
|
||||||
uint NumberType,
|
|
||||||
uint DstSelect,
|
|
||||||
uint TileMode,
|
|
||||||
uint Pitch,
|
|
||||||
GuestSampler Sampler,
|
|
||||||
bool Arrayed = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
internal sealed record GuestMemoryBuffer(
|
internal sealed record GuestMemoryBuffer(
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
byte[] Data,
|
byte[] Data,
|
||||||
@@ -143,22 +122,12 @@ internal readonly record struct GuestBlendState(
|
|||||||
WriteMask: 0xFu);
|
WriteMask: 0xFu);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>CB_BLEND_RED..ALPHA: the constant color referenced by the
|
|
||||||
/// CONSTANT_COLOR / CONSTANT_ALPHA blend factors. One constant serves every
|
|
||||||
/// render target of a draw; the hardware reset value is transparent black.</summary>
|
|
||||||
internal readonly record struct GuestBlendConstant(
|
|
||||||
float Red,
|
|
||||||
float Green,
|
|
||||||
float Blue,
|
|
||||||
float Alpha);
|
|
||||||
|
|
||||||
internal sealed record GuestRenderState(
|
internal sealed record GuestRenderState(
|
||||||
IReadOnlyList<GuestBlendState> Blends,
|
IReadOnlyList<GuestBlendState> Blends,
|
||||||
GuestRect? Scissor,
|
GuestRect? Scissor,
|
||||||
GuestViewport? Viewport,
|
GuestViewport? Viewport,
|
||||||
GuestRasterState Raster,
|
GuestRasterState Raster,
|
||||||
GuestDepthState Depth,
|
GuestDepthState Depth)
|
||||||
GuestBlendConstant BlendConstant = default)
|
|
||||||
{
|
{
|
||||||
public static GuestRenderState Default { get; } = new(
|
public static GuestRenderState Default { get; } = new(
|
||||||
[GuestBlendState.Default],
|
[GuestBlendState.Default],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
@@ -18,10 +17,6 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal interface IGuestGpuBackend
|
internal interface IGuestGpuBackend
|
||||||
{
|
{
|
||||||
/// <summary>Human-readable name of this backend ("Metal", "Vulkan"), shown in
|
|
||||||
/// the window title on macOS where either backend can run.</summary>
|
|
||||||
string BackendName { get; }
|
|
||||||
|
|
||||||
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
||||||
void EnsureStarted(uint width, uint height);
|
void EnsureStarted(uint width, uint height);
|
||||||
|
|
||||||
@@ -193,70 +188,4 @@ internal interface IGuestGpuBackend
|
|||||||
/// the guest codes cross the seam and each backend maps them internally.
|
/// the guest codes cross the seam and each backend maps them internally.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
||||||
|
|
||||||
// Guest work ordering. AGC submissions execute on a single backend consumer in
|
|
||||||
// logical guest-queue order; sequences returned here are backend work tickets.
|
|
||||||
// A backend without a running presenter returns 0 from the Submit* methods and
|
|
||||||
// callers fall back to executing inline.
|
|
||||||
|
|
||||||
/// <summary>Scopes subsequent submissions on this thread to a named guest queue.</summary>
|
|
||||||
IDisposable EnterGuestQueue(string queueName, ulong submissionId);
|
|
||||||
|
|
||||||
/// <summary>Enqueues an action at its exact position in the current guest queue;
|
|
||||||
/// returns its work sequence, or 0 when nothing could be enqueued.</summary>
|
|
||||||
long SubmitOrderedGuestAction(Action action, string debugName);
|
|
||||||
|
|
||||||
/// <summary>Preserves sceAgcDcbWaitUntilSafeForRendering in queue order.</summary>
|
|
||||||
long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex);
|
|
||||||
|
|
||||||
/// <summary>Blocks until the given work sequence completes; false on timeout,
|
|
||||||
/// close, or a non-positive sequence.</summary>
|
|
||||||
bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite);
|
|
||||||
|
|
||||||
/// <summary>Sequence currently executing on the guest-work consumer; diagnostics only.</summary>
|
|
||||||
long CurrentGuestWorkSequenceForDiagnostics { get; }
|
|
||||||
|
|
||||||
// Guest image lifecycle beyond presentation: CPU-visible seeding, writes, and
|
|
||||||
// extent queries the AGC layer uses to keep guest memory and backend images
|
|
||||||
// coherent. Addresses and formats are always raw guest values.
|
|
||||||
|
|
||||||
/// <summary>Whether the image exists on the backend or an already-queued upload
|
|
||||||
/// owns its initialization (a pending image may skip a duplicate upload but is
|
|
||||||
/// not yet a valid flip source).</summary>
|
|
||||||
bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType);
|
|
||||||
|
|
||||||
/// <summary>True when the first draw into this address must seed the backend
|
|
||||||
/// image from guest memory (PS5 render targets alias guest memory, so
|
|
||||||
/// CPU-prefilled pixels are visible before the first draw).</summary>
|
|
||||||
bool GuestImageWantsInitialData(ulong address);
|
|
||||||
|
|
||||||
void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels);
|
|
||||||
|
|
||||||
void SubmitGuestImageFill(ulong address, uint fillValue);
|
|
||||||
|
|
||||||
void SubmitGuestImageWrite(ulong address, byte[] pixels);
|
|
||||||
|
|
||||||
bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount);
|
|
||||||
|
|
||||||
IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents();
|
|
||||||
|
|
||||||
/// <summary>Whether the backend's texture cache already holds this content; lets
|
|
||||||
/// the AGC layer skip copying texels out of guest memory on every draw.</summary>
|
|
||||||
bool IsTextureContentCached(in TextureContentIdentity identity);
|
|
||||||
|
|
||||||
/// <summary>Guest memory handle for backend self-healing (cache misses re-read
|
|
||||||
/// texels directly instead of showing a fallback pattern).</summary>
|
|
||||||
void AttachGuestMemory(ICpuMemory memory);
|
|
||||||
|
|
||||||
/// <summary>Alignment the AGC layer must apply to storage-buffer offsets before
|
|
||||||
/// they cross the seam.</summary>
|
|
||||||
ulong GuestStorageBufferOffsetAlignment { get; }
|
|
||||||
|
|
||||||
/// <summary>Counts a guest shader translation for the perf overlay.</summary>
|
|
||||||
void CountShaderCompilation();
|
|
||||||
|
|
||||||
(long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters();
|
|
||||||
|
|
||||||
/// <summary>Asks a running presenter to close its window.</summary>
|
|
||||||
void RequestClose();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Metal backend's compiled shader: MSL source plus the reflection data
|
|
||||||
/// (<see cref="Gen5MslShader"/>) the presenter needs to create and bind pipeline
|
|
||||||
/// states. The diagnostics payload is the source text — Metal has no portable
|
|
||||||
/// binary form until an MTLBinaryArchive is introduced.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class MetalCompiledGuestShader(Gen5MslShader shader) : IGuestCompiledShader
|
|
||||||
{
|
|
||||||
private byte[]? _payload;
|
|
||||||
|
|
||||||
public Gen5MslShader Shader { get; } = shader;
|
|
||||||
|
|
||||||
/// <summary>MTLLibrary handle cached by the presenter after the first
|
|
||||||
/// runtime compile; the render loop is its only reader and writer.</summary>
|
|
||||||
internal nint CachedLibrary;
|
|
||||||
|
|
||||||
public byte[] Payload => _payload ??= Encoding.UTF8.GetBytes(Shader.Source);
|
|
||||||
|
|
||||||
public string PayloadFileExtension => "msl";
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MTLPixelFormat raw values — only the formats the backend maps. Declared here
|
|
||||||
/// rather than pulled from a binding package: the Metal backend talks to the OS
|
|
||||||
/// exclusively through objc_msgSend, so ABI constants are owned locally.
|
|
||||||
/// </summary>
|
|
||||||
internal enum MtlPixelFormat : uint
|
|
||||||
{
|
|
||||||
Invalid = 0,
|
|
||||||
R8Unorm = 10,
|
|
||||||
R8Snorm = 12,
|
|
||||||
R8Uint = 13,
|
|
||||||
R8Sint = 14,
|
|
||||||
R16Unorm = 20,
|
|
||||||
R16Snorm = 22,
|
|
||||||
R16Uint = 23,
|
|
||||||
R16Sint = 24,
|
|
||||||
R16Float = 25,
|
|
||||||
Rg8Unorm = 30,
|
|
||||||
Rg8Snorm = 32,
|
|
||||||
Rg8Uint = 33,
|
|
||||||
Rg8Sint = 34,
|
|
||||||
B5G6R5Unorm = 40,
|
|
||||||
R32Uint = 53,
|
|
||||||
R32Sint = 54,
|
|
||||||
R32Float = 55,
|
|
||||||
Rg16Unorm = 60,
|
|
||||||
Rg16Uint = 63,
|
|
||||||
Rg16Sint = 64,
|
|
||||||
Rg16Float = 65,
|
|
||||||
Rgba8Unorm = 70,
|
|
||||||
Rgba8UnormSrgb = 71,
|
|
||||||
Rgba8Uint = 73,
|
|
||||||
Rgba8Sint = 74,
|
|
||||||
Bgra8Unorm = 80,
|
|
||||||
Bgra8UnormSrgb = 81,
|
|
||||||
Rgb10A2Unorm = 90,
|
|
||||||
Rg11B10Float = 92,
|
|
||||||
Rgb9E5Float = 93,
|
|
||||||
Bgr10A2Unorm = 94,
|
|
||||||
Rg32Uint = 103,
|
|
||||||
Rg32Sint = 104,
|
|
||||||
Rg32Float = 105,
|
|
||||||
Rgba16Unorm = 110,
|
|
||||||
Rgba16Uint = 113,
|
|
||||||
Rgba16Sint = 114,
|
|
||||||
Rgba16Float = 115,
|
|
||||||
Rgba32Uint = 123,
|
|
||||||
Rgba32Sint = 124,
|
|
||||||
Rgba32Float = 125,
|
|
||||||
Bc1Rgba = 130,
|
|
||||||
Bc1RgbaSrgb = 131,
|
|
||||||
Bc2Rgba = 132,
|
|
||||||
Bc2RgbaSrgb = 133,
|
|
||||||
Bc3Rgba = 134,
|
|
||||||
Bc3RgbaSrgb = 135,
|
|
||||||
Bc4RUnorm = 140,
|
|
||||||
Bc4RSnorm = 141,
|
|
||||||
Bc5RgUnorm = 142,
|
|
||||||
Bc5RgSnorm = 143,
|
|
||||||
Bc6HRgbFloat = 150,
|
|
||||||
Bc6HRgbUfloat = 151,
|
|
||||||
Bc7RgbaUnorm = 152,
|
|
||||||
Bc7RgbaUnormSrgb = 153,
|
|
||||||
Depth32Float = 252,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>A sampled-texture format: the Metal pixel format plus the byte
|
|
||||||
/// layout the upload path needs. <see cref="BlockBytes"/> is nonzero for
|
|
||||||
/// block-compressed formats (bytes per 4x4 block); otherwise
|
|
||||||
/// <see cref="BytesPerPixel"/> applies.</summary>
|
|
||||||
internal readonly record struct MetalTextureFormat(
|
|
||||||
MtlPixelFormat Format,
|
|
||||||
uint BytesPerPixel,
|
|
||||||
uint BlockBytes)
|
|
||||||
{
|
|
||||||
public bool IsBlockCompressed => BlockBytes != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct MetalRenderTargetFormat(
|
|
||||||
MtlPixelFormat Format,
|
|
||||||
Gen5PixelOutputKind OutputKind)
|
|
||||||
{
|
|
||||||
public static uint GetBytesPerPixel(MtlPixelFormat format) =>
|
|
||||||
format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Unorm or MtlPixelFormat.R8Uint => 1,
|
|
||||||
MtlPixelFormat.Rg8Unorm => 2,
|
|
||||||
MtlPixelFormat.Rg32Float => 8,
|
|
||||||
MtlPixelFormat.Rgba16Unorm or MtlPixelFormat.Rgba16Uint or
|
|
||||||
MtlPixelFormat.Rgba16Sint or MtlPixelFormat.Rgba16Float => 8,
|
|
||||||
MtlPixelFormat.Rgba32Float => 16,
|
|
||||||
_ => 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Guest texture-descriptor codes to Metal formats, mirroring the Vulkan
|
|
||||||
/// backend's table case for case so both backends accept the same guest
|
|
||||||
/// formats. Guest format 9 (2:10:10:10) maps to BGR10A2 — the bit layout that
|
|
||||||
/// matches Vulkan's A2R10G10B10 pack.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MetalGuestFormats
|
|
||||||
{
|
|
||||||
/// <summary>Guest sampled-texture format to Metal, mirroring the Vulkan
|
|
||||||
/// backend's GetTextureFormat case for case (including its RGBA8 fallback
|
|
||||||
/// for unmapped codes, so unknown formats render something rather than
|
|
||||||
/// nothing). BC formats upload raw blocks — Mac-family GPUs decode them
|
|
||||||
/// natively.</summary>
|
|
||||||
public static MetalTextureFormat DecodeTextureFormat(uint dataFormat, uint numberType)
|
|
||||||
{
|
|
||||||
var format = (dataFormat, numberType) switch
|
|
||||||
{
|
|
||||||
(1, 0) => MtlPixelFormat.R8Unorm,
|
|
||||||
(1, 1) => MtlPixelFormat.R8Snorm,
|
|
||||||
(1, 4) => MtlPixelFormat.R8Uint,
|
|
||||||
(1, 5) => MtlPixelFormat.R8Sint,
|
|
||||||
(2, 0) => MtlPixelFormat.R16Unorm,
|
|
||||||
(2, 1) => MtlPixelFormat.R16Snorm,
|
|
||||||
(2, 4) => MtlPixelFormat.R16Uint,
|
|
||||||
(2, 5) => MtlPixelFormat.R16Sint,
|
|
||||||
(2, 7) => MtlPixelFormat.R16Float,
|
|
||||||
(3, 0) => MtlPixelFormat.Rg8Unorm,
|
|
||||||
(3, 1) => MtlPixelFormat.Rg8Snorm,
|
|
||||||
(3, 4) => MtlPixelFormat.Rg8Uint,
|
|
||||||
(3, 5) => MtlPixelFormat.Rg8Sint,
|
|
||||||
(4, 4) => MtlPixelFormat.R32Uint,
|
|
||||||
(4, 5) => MtlPixelFormat.R32Sint,
|
|
||||||
(4, 7) => MtlPixelFormat.R32Float,
|
|
||||||
(5, 0) => MtlPixelFormat.Rg16Unorm,
|
|
||||||
(5, 4) => MtlPixelFormat.Rg16Uint,
|
|
||||||
(5, 5) => MtlPixelFormat.Rg16Sint,
|
|
||||||
(5, 7) => MtlPixelFormat.Rg16Float,
|
|
||||||
(6, 7) or (7, 7) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(8, _) or (9, _) => MtlPixelFormat.Bgr10A2Unorm,
|
|
||||||
(10, 4) => MtlPixelFormat.Rgba8Uint,
|
|
||||||
(10, 5) => MtlPixelFormat.Rgba8Sint,
|
|
||||||
(10, 9) => MtlPixelFormat.Rgba8UnormSrgb,
|
|
||||||
(11, 4) => MtlPixelFormat.Rg32Uint,
|
|
||||||
(11, 5) => MtlPixelFormat.Rg32Sint,
|
|
||||||
(11, 7) => MtlPixelFormat.Rg32Float,
|
|
||||||
(12, 0) => MtlPixelFormat.Rgba16Unorm,
|
|
||||||
(12, 4) => MtlPixelFormat.Rgba16Uint,
|
|
||||||
(12, 5) => MtlPixelFormat.Rgba16Sint,
|
|
||||||
(12, 7) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(13, 4) or (14, 4) => MtlPixelFormat.Rgba32Uint,
|
|
||||||
(13, 5) or (14, 5) => MtlPixelFormat.Rgba32Sint,
|
|
||||||
(13, _) or (14, _) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(16, 0) => MtlPixelFormat.B5G6R5Unorm,
|
|
||||||
(34, 7) => MtlPixelFormat.Rgb9E5Float,
|
|
||||||
(169, _) => MtlPixelFormat.Bc1Rgba,
|
|
||||||
(170, _) => MtlPixelFormat.Bc1RgbaSrgb,
|
|
||||||
(171, _) => MtlPixelFormat.Bc2Rgba,
|
|
||||||
(172, _) => MtlPixelFormat.Bc2RgbaSrgb,
|
|
||||||
(173, _) => MtlPixelFormat.Bc3Rgba,
|
|
||||||
(174, _) => MtlPixelFormat.Bc3RgbaSrgb,
|
|
||||||
(175, 1) or (176, _) => MtlPixelFormat.Bc4RSnorm,
|
|
||||||
(175, _) => MtlPixelFormat.Bc4RUnorm,
|
|
||||||
(177, 1) or (178, _) => MtlPixelFormat.Bc5RgSnorm,
|
|
||||||
(177, _) => MtlPixelFormat.Bc5RgUnorm,
|
|
||||||
(179, _) => MtlPixelFormat.Bc6HRgbUfloat,
|
|
||||||
(180, _) => MtlPixelFormat.Bc6HRgbFloat,
|
|
||||||
(181, _) => MtlPixelFormat.Bc7RgbaUnorm,
|
|
||||||
(182, _) => MtlPixelFormat.Bc7RgbaUnormSrgb,
|
|
||||||
_ => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
};
|
|
||||||
|
|
||||||
var blockBytes = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.Bc1Rgba or MtlPixelFormat.Bc1RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc4RUnorm or MtlPixelFormat.Bc4RSnorm => 8u,
|
|
||||||
MtlPixelFormat.Bc2Rgba or MtlPixelFormat.Bc2RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc3Rgba or MtlPixelFormat.Bc3RgbaSrgb or
|
|
||||||
MtlPixelFormat.Bc5RgUnorm or MtlPixelFormat.Bc5RgSnorm or
|
|
||||||
MtlPixelFormat.Bc6HRgbFloat or MtlPixelFormat.Bc6HRgbUfloat or
|
|
||||||
MtlPixelFormat.Bc7RgbaUnorm or MtlPixelFormat.Bc7RgbaUnormSrgb => 16u,
|
|
||||||
_ => 0u,
|
|
||||||
};
|
|
||||||
|
|
||||||
var bytesPerPixel = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Unorm or MtlPixelFormat.R8Snorm or
|
|
||||||
MtlPixelFormat.R8Uint or MtlPixelFormat.R8Sint => 1u,
|
|
||||||
MtlPixelFormat.R16Unorm or MtlPixelFormat.R16Snorm or
|
|
||||||
MtlPixelFormat.R16Uint or MtlPixelFormat.R16Sint or
|
|
||||||
MtlPixelFormat.R16Float or MtlPixelFormat.Rg8Unorm or
|
|
||||||
MtlPixelFormat.Rg8Snorm or MtlPixelFormat.Rg8Uint or
|
|
||||||
MtlPixelFormat.Rg8Sint or MtlPixelFormat.B5G6R5Unorm => 2u,
|
|
||||||
MtlPixelFormat.Rg32Uint or MtlPixelFormat.Rg32Sint or
|
|
||||||
MtlPixelFormat.Rg32Float or MtlPixelFormat.Rgba16Unorm or
|
|
||||||
MtlPixelFormat.Rgba16Uint or MtlPixelFormat.Rgba16Sint or
|
|
||||||
MtlPixelFormat.Rgba16Float => 8u,
|
|
||||||
MtlPixelFormat.Rgba32Uint or MtlPixelFormat.Rgba32Sint or
|
|
||||||
MtlPixelFormat.Rgba32Float => 16u,
|
|
||||||
_ => 4u,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new MetalTextureFormat(format, bytesPerPixel, blockBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Source byte footprint of a sampled texture, block-aware —
|
|
||||||
/// the same math the AGC layer uses to size the texel copy it ships.</summary>
|
|
||||||
public static ulong GetTextureByteCount(in MetalTextureFormat format, uint width, uint height) =>
|
|
||||||
format.IsBlockCompressed
|
|
||||||
? checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * format.BlockBytes)
|
|
||||||
: checked((ulong)width * height * format.BytesPerPixel);
|
|
||||||
|
|
||||||
public static bool TryDecodeRenderTargetFormat(
|
|
||||||
uint dataFormat,
|
|
||||||
uint numberType,
|
|
||||||
out MetalRenderTargetFormat result)
|
|
||||||
{
|
|
||||||
var format = (dataFormat, numberType) switch
|
|
||||||
{
|
|
||||||
(4, 4) => MtlPixelFormat.R32Uint,
|
|
||||||
(4, 5) => MtlPixelFormat.R32Sint,
|
|
||||||
(4, 7) => MtlPixelFormat.R32Float,
|
|
||||||
(5, 4) => MtlPixelFormat.Rg16Uint,
|
|
||||||
(5, 5) => MtlPixelFormat.Rg16Sint,
|
|
||||||
(5, 7) => MtlPixelFormat.Rg16Float,
|
|
||||||
(6, 7) or (7, 7) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(9, _) => MtlPixelFormat.Bgr10A2Unorm,
|
|
||||||
(10, 4) => MtlPixelFormat.Rgba8Uint,
|
|
||||||
(10, 5) => MtlPixelFormat.Rgba8Sint,
|
|
||||||
(10, 9) => MtlPixelFormat.Rgba8UnormSrgb,
|
|
||||||
(10, _) => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
(11, 7) => MtlPixelFormat.Rg32Float,
|
|
||||||
(12, 4) => MtlPixelFormat.Rgba16Uint,
|
|
||||||
(12, 5) => MtlPixelFormat.Rgba16Sint,
|
|
||||||
(12, 7) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(13, 7) or (14, 7) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(20, 0) => MtlPixelFormat.R32Uint,
|
|
||||||
(29, 0) or (4, 0) => MtlPixelFormat.R32Float,
|
|
||||||
(1, 0) or (36, 0) => MtlPixelFormat.R8Unorm,
|
|
||||||
(49, 0) => MtlPixelFormat.R8Uint,
|
|
||||||
(3, 0) => MtlPixelFormat.Rg8Unorm,
|
|
||||||
(5, 0) => MtlPixelFormat.Rg16Unorm,
|
|
||||||
(7, 0) => MtlPixelFormat.Rg11B10Float,
|
|
||||||
(12, 0) => MtlPixelFormat.Rgba16Unorm,
|
|
||||||
(13, 0) or (14, 0) => MtlPixelFormat.Rgba32Float,
|
|
||||||
(22, 0) or (71, 0) => MtlPixelFormat.Rgba16Float,
|
|
||||||
(56, 0) or (62, 0) or (64, 0) => MtlPixelFormat.Rgba8Unorm,
|
|
||||||
(75, 0) => MtlPixelFormat.Rg32Float,
|
|
||||||
_ => MtlPixelFormat.Invalid,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (format == MtlPixelFormat.Invalid)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputKind = format switch
|
|
||||||
{
|
|
||||||
MtlPixelFormat.R8Uint or MtlPixelFormat.R32Uint or MtlPixelFormat.Rg16Uint or
|
|
||||||
MtlPixelFormat.Rgba8Uint or MtlPixelFormat.Rgba16Uint => Gen5PixelOutputKind.Uint,
|
|
||||||
MtlPixelFormat.R32Sint or MtlPixelFormat.Rg16Sint or MtlPixelFormat.Rgba8Sint or
|
|
||||||
MtlPixelFormat.Rgba16Sint => Gen5PixelOutputKind.Sint,
|
|
||||||
_ => Gen5PixelOutputKind.Float,
|
|
||||||
};
|
|
||||||
result = new MetalRenderTargetFormat(format, outputKind);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
using SharpEmu.ShaderCompiler.Metal;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Metal backend for the guest-GPU seam: MSL codegen via
|
|
||||||
/// SharpEmu.ShaderCompiler.Metal, rendering via the Metal presenter — the full
|
|
||||||
/// surface (presentation, guest images, ordered flips, translated draws, and
|
|
||||||
/// compute) with no Vulkan, MoltenVK, or windowing-library dependency.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
|
|
||||||
{
|
|
||||||
public string BackendName => "Metal";
|
|
||||||
|
|
||||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
|
||||||
new MetalCompiledGuestShader(new Gen5MslShader(
|
|
||||||
MslFixedShaders.CreateDepthOnlyFragment(),
|
|
||||||
"depth_only_fs",
|
|
||||||
Gen5MslStage.Pixel,
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
AttributeCount: 0,
|
|
||||||
[]));
|
|
||||||
|
|
||||||
public bool TryCompileVertexShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int globalBufferBase = 0,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int imageBindingBase = 0,
|
|
||||||
int scalarRegisterBufferIndex = -1,
|
|
||||||
int requiredVertexOutputCount = 0,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
if (!Gen5MslTranslator.TryCompileVertexShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
globalBufferBase,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
imageBindingBase,
|
|
||||||
scalarRegisterBufferIndex,
|
|
||||||
requiredVertexOutputCount,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompilePixelShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int globalBufferBase = 0,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int imageBindingBase = 0,
|
|
||||||
int scalarRegisterBufferIndex = -1,
|
|
||||||
uint pixelInputEnable = 0,
|
|
||||||
uint pixelInputAddress = 0,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
if (!Gen5MslTranslator.TryCompilePixelShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
outputs,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
globalBufferBase,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
imageBindingBase,
|
|
||||||
scalarRegisterBufferIndex,
|
|
||||||
pixelInputEnable,
|
|
||||||
pixelInputAddress,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompileComputeShader(
|
|
||||||
Gen5ShaderState state,
|
|
||||||
Gen5ShaderEvaluation evaluation,
|
|
||||||
uint localSizeX,
|
|
||||||
uint localSizeY,
|
|
||||||
uint localSizeZ,
|
|
||||||
out IGuestCompiledShader? shader,
|
|
||||||
out string error,
|
|
||||||
int totalGlobalBufferCount = -1,
|
|
||||||
int initialScalarBufferIndex = -1,
|
|
||||||
uint waveLaneCount = 32,
|
|
||||||
ulong storageBufferOffsetAlignment = 1)
|
|
||||||
{
|
|
||||||
shader = null;
|
|
||||||
// Wave64 compute is emulated by the translator: cross-lane ops bridge
|
|
||||||
// the two 32-wide Apple simdgroups of a guest wave through threadgroup
|
|
||||||
// scratch, and wave-agnostic kernels run per-thread unchanged.
|
|
||||||
if (!Gen5MslTranslator.TryCompileComputeShader(
|
|
||||||
state,
|
|
||||||
evaluation,
|
|
||||||
localSizeX,
|
|
||||||
localSizeY,
|
|
||||||
localSizeZ,
|
|
||||||
out var compiled,
|
|
||||||
out error,
|
|
||||||
totalGlobalBufferCount,
|
|
||||||
initialScalarBufferIndex,
|
|
||||||
waveLaneCount,
|
|
||||||
storageBufferOffsetAlignment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader = new MetalCompiledGuestShader(compiled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
|
|
||||||
DepthOnlyFragmentShader;
|
|
||||||
|
|
||||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
|
||||||
{
|
|
||||||
if (MetalGuestFormats.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
|
|
||||||
{
|
|
||||||
outputKind = format.OutputKind;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputKind = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnsureStarted(uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.EnsureStarted(width, height);
|
|
||||||
|
|
||||||
public void HideSplashScreen() =>
|
|
||||||
MetalVideoPresenter.HideSplashScreen();
|
|
||||||
|
|
||||||
public void Submit(byte[] bgraFrame, uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.Submit(bgraFrame, width, height);
|
|
||||||
|
|
||||||
public bool TrySubmitGuestImage(
|
|
||||||
ulong address,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint pitchInPixel) =>
|
|
||||||
MetalVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
|
||||||
|
|
||||||
public bool TrySubmitOrderedGuestImageFlip(
|
|
||||||
int videoOutHandle,
|
|
||||||
int displayBufferIndex,
|
|
||||||
ulong address,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint pitchInPixel) =>
|
|
||||||
MetalVideoPresenter.TrySubmitOrderedGuestImageFlip(
|
|
||||||
videoOutHandle,
|
|
||||||
displayBufferIndex,
|
|
||||||
address,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
pitchInPixel);
|
|
||||||
|
|
||||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
|
||||||
MetalVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
|
||||||
|
|
||||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
|
|
||||||
MetalVideoPresenter.IsGuestImageAvailable(address, format, numberType);
|
|
||||||
|
|
||||||
public bool TrySubmitGuestImageBlit(
|
|
||||||
ulong sourceAddress,
|
|
||||||
uint sourceWidth,
|
|
||||||
uint sourceHeight,
|
|
||||||
uint sourceFormat,
|
|
||||||
uint sourceNumberType,
|
|
||||||
ulong destinationAddress,
|
|
||||||
uint destinationWidth,
|
|
||||||
uint destinationHeight,
|
|
||||||
uint destinationFormat,
|
|
||||||
uint destinationNumberType) =>
|
|
||||||
MetalVideoPresenter.TrySubmitGuestImageBlit(
|
|
||||||
sourceAddress,
|
|
||||||
sourceWidth,
|
|
||||||
sourceHeight,
|
|
||||||
sourceFormat,
|
|
||||||
sourceNumberType,
|
|
||||||
destinationAddress,
|
|
||||||
destinationWidth,
|
|
||||||
destinationHeight,
|
|
||||||
destinationFormat,
|
|
||||||
destinationNumberType);
|
|
||||||
|
|
||||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestDraw(drawKind, width, height);
|
|
||||||
|
|
||||||
public void SubmitTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint attributeCount,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null) =>
|
|
||||||
MetalVideoPresenter.SubmitTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
attributeCount,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState);
|
|
||||||
|
|
||||||
public void SubmitDepthOnlyTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
GuestDepthTarget depthTarget,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitDepthOnlyTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
depthTarget,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
public void SubmitOffscreenTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
IReadOnlyList<GuestRenderTarget> targets,
|
|
||||||
IGuestCompiledShader? vertexShader = null,
|
|
||||||
uint vertexCount = 3,
|
|
||||||
uint instanceCount = 1,
|
|
||||||
uint primitiveType = 4,
|
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
|
||||||
GuestRenderState? renderState = null,
|
|
||||||
GuestDepthTarget? depthTarget = null,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitOffscreenTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
targets,
|
|
||||||
vertexShader is null ? null : Msl(vertexShader),
|
|
||||||
vertexCount,
|
|
||||||
instanceCount,
|
|
||||||
primitiveType,
|
|
||||||
indexBuffer,
|
|
||||||
vertexBuffers,
|
|
||||||
renderState,
|
|
||||||
depthTarget,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
public void SubmitStorageTranslatedDraw(
|
|
||||||
IGuestCompiledShader pixelShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint attributeCount,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
ulong shaderAddress = 0) =>
|
|
||||||
MetalVideoPresenter.SubmitStorageTranslatedDraw(
|
|
||||||
Msl(pixelShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
attributeCount,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
shaderAddress);
|
|
||||||
|
|
||||||
private static MetalCompiledGuestShader Msl(IGuestCompiledShader shader) =>
|
|
||||||
shader as MetalCompiledGuestShader ??
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"shader handle of type {shader.GetType().Name} was not compiled by the Metal backend");
|
|
||||||
|
|
||||||
public long SubmitComputeDispatch(
|
|
||||||
ulong shaderAddress,
|
|
||||||
IGuestCompiledShader computeShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint groupCountX,
|
|
||||||
uint groupCountY,
|
|
||||||
uint groupCountZ,
|
|
||||||
uint baseGroupX,
|
|
||||||
uint baseGroupY,
|
|
||||||
uint baseGroupZ,
|
|
||||||
uint localSizeX,
|
|
||||||
uint localSizeY,
|
|
||||||
uint localSizeZ,
|
|
||||||
bool isIndirect,
|
|
||||||
bool writesGlobalMemory,
|
|
||||||
uint threadCountX = uint.MaxValue,
|
|
||||||
uint threadCountY = uint.MaxValue,
|
|
||||||
uint threadCountZ = uint.MaxValue)
|
|
||||||
{
|
|
||||||
// The translated kernel bakes its threadgroup size; localSize and
|
|
||||||
// isIndirect are already folded in by the AGC layer before submission.
|
|
||||||
_ = localSizeX;
|
|
||||||
_ = localSizeY;
|
|
||||||
_ = localSizeZ;
|
|
||||||
_ = isIndirect;
|
|
||||||
return MetalVideoPresenter.SubmitComputeDispatch(
|
|
||||||
shaderAddress,
|
|
||||||
Msl(computeShader),
|
|
||||||
textures,
|
|
||||||
globalMemoryBuffers,
|
|
||||||
groupCountX,
|
|
||||||
groupCountY,
|
|
||||||
groupCountZ,
|
|
||||||
baseGroupX,
|
|
||||||
baseGroupY,
|
|
||||||
baseGroupZ,
|
|
||||||
writesGlobalMemory,
|
|
||||||
threadCountX,
|
|
||||||
threadCountY,
|
|
||||||
threadCountZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long _perfShaderCompilations;
|
|
||||||
|
|
||||||
public IDisposable EnterGuestQueue(string queueName, ulong submissionId) =>
|
|
||||||
MetalVideoPresenter.EnterGuestQueue(queueName, submissionId);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestAction(Action action, string debugName) =>
|
|
||||||
MetalVideoPresenter.SubmitOrderedGuestAction(action, debugName);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex) =>
|
|
||||||
MetalVideoPresenter.SubmitOrderedGuestFlipWait(videoOutHandle, displayBufferIndex);
|
|
||||||
|
|
||||||
public bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite) =>
|
|
||||||
MetalVideoPresenter.WaitForGuestWork(workSequence, timeoutMilliseconds);
|
|
||||||
|
|
||||||
public long CurrentGuestWorkSequenceForDiagnostics =>
|
|
||||||
MetalVideoPresenter.CurrentGuestWorkSequenceForDiagnostics;
|
|
||||||
|
|
||||||
public bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType) =>
|
|
||||||
MetalVideoPresenter.IsGuestImageUploadKnown(address, format, numberType);
|
|
||||||
|
|
||||||
public bool GuestImageWantsInitialData(ulong address) =>
|
|
||||||
MetalVideoPresenter.GuestImageWantsInitialData(address);
|
|
||||||
|
|
||||||
public void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels) =>
|
|
||||||
MetalVideoPresenter.ProvideGuestImageInitialData(address, rgbaPixels);
|
|
||||||
|
|
||||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
|
||||||
|
|
||||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
|
||||||
MetalVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
|
||||||
|
|
||||||
public bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount) =>
|
|
||||||
MetalVideoPresenter.TryGetGuestImageExtent(address, out width, out height, out byteCount);
|
|
||||||
|
|
||||||
public IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents() =>
|
|
||||||
MetalVideoPresenter.GetGuestImageExtents();
|
|
||||||
|
|
||||||
public bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
MetalVideoPresenter.IsTextureContentCached(identity);
|
|
||||||
|
|
||||||
public void AttachGuestMemory(SharpEmu.HLE.ICpuMemory memory) =>
|
|
||||||
MetalVideoPresenter.AttachGuestMemory(memory);
|
|
||||||
|
|
||||||
// Over-alignment is always valid, and 256 covers every Metal buffer-offset
|
|
||||||
// requirement (Intel Macs need 256 for constant buffers; Apple GPUs less).
|
|
||||||
public ulong GuestStorageBufferOffsetAlignment => 256;
|
|
||||||
|
|
||||||
public void CountShaderCompilation() =>
|
|
||||||
Interlocked.Increment(ref _perfShaderCompilations);
|
|
||||||
|
|
||||||
public (long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters()
|
|
||||||
{
|
|
||||||
var (draws, drawMs, pipelines) = MetalVideoPresenter.ReadAndResetDrawPerfCounters();
|
|
||||||
return (draws, drawMs, pipelines, Interlocked.Exchange(ref _perfShaderCompilations, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RequestClose() =>
|
|
||||||
MetalVideoPresenter.RequestClose();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.HLE.Host.Posix;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keyboard state sampled from the Metal presenter's window, feeding the POSIX
|
|
||||||
/// host input seam so pad emulation works like the Vulkan presenter's
|
|
||||||
/// HostWindowInput. Key events arrive on the AppKit main thread as macOS
|
|
||||||
/// virtual key codes; pad reads happen on guest threads, so state is guarded.
|
|
||||||
/// Window gamepads are not surfaced by AppKit — controller support would go
|
|
||||||
/// through GameController.framework and is out of scope here.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MetalHostInput
|
|
||||||
{
|
|
||||||
private static readonly object Gate = new();
|
|
||||||
private static readonly HashSet<ushort> Pressed = new();
|
|
||||||
private static volatile bool _connected;
|
|
||||||
|
|
||||||
/// <summary>Registers this window's keyboard as the host input source.</summary>
|
|
||||||
public static void Attach()
|
|
||||||
{
|
|
||||||
_connected = true;
|
|
||||||
PosixHostInput.SetSource(new MetalWindowInputSource());
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Window keyboard input attached for pad emulation.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug automation: SHARPEMU_METAL_AUTOKEY="12:0x24,15:0x24" presses the
|
|
||||||
// macOS key code at each elapsed-seconds mark for a few frames, letting
|
|
||||||
// headless test runs navigate menus without a human at the keyboard.
|
|
||||||
private static readonly List<(double At, ushort Key, bool[] State)> _autoKeys = ParseAutoKeys();
|
|
||||||
private static readonly System.Diagnostics.Stopwatch _autoKeyClock =
|
|
||||||
System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
|
|
||||||
private static List<(double, ushort, bool[])> ParseAutoKeys()
|
|
||||||
{
|
|
||||||
var keys = new List<(double, ushort, bool[])>();
|
|
||||||
var spec = Environment.GetEnvironmentVariable("SHARPEMU_METAL_AUTOKEY");
|
|
||||||
if (string.IsNullOrWhiteSpace(spec))
|
|
||||||
{
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in spec.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
var parts = entry.Split(':');
|
|
||||||
if (parts.Length == 2 &&
|
|
||||||
double.TryParse(parts[0], out var at) &&
|
|
||||||
TryParseKeyCode(parts[1], out var key))
|
|
||||||
{
|
|
||||||
keys.Add((at, key, new bool[2]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseKeyCode(string text, out ushort key)
|
|
||||||
{
|
|
||||||
return text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? ushort.TryParse(text[2..], System.Globalization.NumberStyles.HexNumber, null, out key)
|
|
||||||
: ushort.TryParse(text, out key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called once per render frame; fires and releases scripted keys.</summary>
|
|
||||||
public static void PumpAutoKeys()
|
|
||||||
{
|
|
||||||
if (_autoKeys.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var elapsed = _autoKeyClock.Elapsed.TotalSeconds;
|
|
||||||
foreach (var (at, key, state) in _autoKeys)
|
|
||||||
{
|
|
||||||
if (!state[0] && elapsed >= at)
|
|
||||||
{
|
|
||||||
state[0] = true;
|
|
||||||
KeyDown(key, isRepeat: false);
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Metal autokey press 0x{key:X} at {elapsed:F1}s");
|
|
||||||
}
|
|
||||||
else if (state[0] && !state[1] && elapsed >= at + 0.2)
|
|
||||||
{
|
|
||||||
state[1] = true;
|
|
||||||
KeyUp(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void KeyDown(ushort keyCode, bool isRepeat)
|
|
||||||
{
|
|
||||||
// kVK_F1: parity with the Vulkan window's perf-overlay toggle.
|
|
||||||
if (keyCode == 0x7A && !isRepeat)
|
|
||||||
{
|
|
||||||
VideoOut.PerfOverlay.Toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
Pressed.Add(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void KeyUp(ushort keyCode)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
Pressed.Remove(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsKeyCodeDown(ushort keyCode)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
return Pressed.Contains(keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MetalWindowInputSource : IPosixWindowInputSource
|
|
||||||
{
|
|
||||||
public bool HasKeyboardFocus => _connected;
|
|
||||||
|
|
||||||
public bool IsKeyDown(int virtualKey) =>
|
|
||||||
TryMapVirtualKey(virtualKey, out var keyCode) && IsKeyCodeDown(keyCode);
|
|
||||||
|
|
||||||
public int GetGamepadStates(Span<HostGamepadState> destination) => 0;
|
|
||||||
|
|
||||||
public string? DescribeConnectedGamepad() => null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Windows virtual-key semantics (the seam's contract) to macOS
|
|
||||||
/// kVK virtual key codes, covering the keys pad emulation polls.</summary>
|
|
||||||
private static bool TryMapVirtualKey(int vk, out ushort keyCode)
|
|
||||||
{
|
|
||||||
keyCode = vk switch
|
|
||||||
{
|
|
||||||
0x08 => 0x33, // Backspace -> kVK_Delete
|
|
||||||
0x09 => 0x30, // Tab
|
|
||||||
0x0D => 0x24, // Enter -> kVK_Return
|
|
||||||
0x1B => 0x35, // Escape
|
|
||||||
0x20 => 0x31, // Space
|
|
||||||
0x25 => 0x7B, // Left
|
|
||||||
0x26 => 0x7E, // Up
|
|
||||||
0x27 => 0x7C, // Right
|
|
||||||
0x28 => 0x7D, // Down
|
|
||||||
// Letters: macOS ANSI key codes are layout-position based and
|
|
||||||
// non-contiguous, so map each polled letter explicitly.
|
|
||||||
0x41 => 0x00, // A
|
|
||||||
0x42 => 0x0B, // B
|
|
||||||
0x43 => 0x08, // C
|
|
||||||
0x44 => 0x02, // D
|
|
||||||
0x45 => 0x0E, // E
|
|
||||||
0x46 => 0x03, // F
|
|
||||||
0x47 => 0x05, // G
|
|
||||||
0x48 => 0x04, // H
|
|
||||||
0x49 => 0x22, // I
|
|
||||||
0x4A => 0x26, // J
|
|
||||||
0x4B => 0x28, // K
|
|
||||||
0x4C => 0x25, // L
|
|
||||||
0x4D => 0x2E, // M
|
|
||||||
0x4E => 0x2D, // N
|
|
||||||
0x4F => 0x1F, // O
|
|
||||||
0x50 => 0x23, // P
|
|
||||||
0x51 => 0x0C, // Q
|
|
||||||
0x52 => 0x0F, // R
|
|
||||||
0x53 => 0x01, // S
|
|
||||||
0x54 => 0x11, // T
|
|
||||||
0x55 => 0x20, // U
|
|
||||||
0x56 => 0x09, // V
|
|
||||||
0x57 => 0x0D, // W
|
|
||||||
0x58 => 0x07, // X
|
|
||||||
0x59 => 0x10, // Y
|
|
||||||
0x5A => 0x06, // Z
|
|
||||||
_ => ushort.MaxValue,
|
|
||||||
};
|
|
||||||
return keyCode != ushort.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Core Graphics / Metal ABI structs passed by value through objc_msgSend. Struct
|
|
||||||
// *returns* are deliberately never used: on x86-64 (this process runs under Rosetta
|
|
||||||
// on Apple silicon) large struct returns switch to objc_msgSend_stret, and avoiding
|
|
||||||
// them entirely keeps one calling convention everywhere.
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct CGRect
|
|
||||||
{
|
|
||||||
public double X;
|
|
||||||
public double Y;
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct CGSize
|
|
||||||
{
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlClearColor
|
|
||||||
{
|
|
||||||
public double Red;
|
|
||||||
public double Green;
|
|
||||||
public double Blue;
|
|
||||||
public double Alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>MTLTextureSwizzleChannels: one MTLTextureSwizzle byte per output
|
|
||||||
/// channel (Zero=0, One=1, Red=2, Green=3, Blue=4, Alpha=5).</summary>
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlTextureSwizzleChannels
|
|
||||||
{
|
|
||||||
public byte Red;
|
|
||||||
public byte Green;
|
|
||||||
public byte Blue;
|
|
||||||
public byte Alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlRegion
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Z;
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
public nuint Depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlSize
|
|
||||||
{
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
public nuint Depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlOrigin
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlScissorRect
|
|
||||||
{
|
|
||||||
public nuint X;
|
|
||||||
public nuint Y;
|
|
||||||
public nuint Width;
|
|
||||||
public nuint Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct MtlViewport
|
|
||||||
{
|
|
||||||
public double OriginX;
|
|
||||||
public double OriginY;
|
|
||||||
public double Width;
|
|
||||||
public double Height;
|
|
||||||
public double ZNear;
|
|
||||||
public double ZFar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Objective-C runtime access for the Metal presenter: AppKit, QuartzCore, and Metal
|
|
||||||
/// through objc_msgSend, with one LibraryImport overload per distinct native
|
|
||||||
/// signature. Dependency-free by design — this plus the OS frameworks is the entire
|
|
||||||
/// Metal path, which is what keeps it NativeAOT-clean.
|
|
||||||
/// </summary>
|
|
||||||
internal static partial class MetalNative
|
|
||||||
{
|
|
||||||
private const string CoreFoundation =
|
|
||||||
"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
|
|
||||||
|
|
||||||
[LibraryImport(CoreFoundation)]
|
|
||||||
public static partial nint CFRunLoopGetMain();
|
|
||||||
|
|
||||||
[LibraryImport(CoreFoundation)]
|
|
||||||
public static partial void CFRunLoopStop(nint runLoop);
|
|
||||||
|
|
||||||
private const string ObjCLibrary = "/usr/lib/libobjc.A.dylib";
|
|
||||||
private const string MetalFramework = "/System/Library/Frameworks/Metal.framework/Metal";
|
|
||||||
private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit";
|
|
||||||
private const string QuartzCoreFramework = "/System/Library/Frameworks/QuartzCore.framework/QuartzCore";
|
|
||||||
|
|
||||||
private static bool _frameworksLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Makes the AppKit and QuartzCore classes visible to objc_getClass; Metal is
|
|
||||||
/// pulled in by its own LibraryImport. Call once before any Class() lookup.
|
|
||||||
/// </summary>
|
|
||||||
public static void EnsureFrameworksLoaded()
|
|
||||||
{
|
|
||||||
if (_frameworksLoaded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.Load(AppKitFramework);
|
|
||||||
NativeLibrary.Load(QuartzCoreFramework);
|
|
||||||
_frameworksLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[LibraryImport(MetalFramework)]
|
|
||||||
public static partial nint MTLCreateSystemDefaultDevice();
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
private static partial nint objc_getClass(string name);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
private static partial nint sel_registerName(string name);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
public static partial nint objc_allocateClassPair(nint superclass, string name, nuint extraBytes);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial void objc_registerClassPair(nint cls);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool class_addMethod(nint cls, nint name, nint imp, string types);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial nint objc_autoreleasePoolPush();
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary)]
|
|
||||||
public static partial void objc_autoreleasePoolPop(nint pool);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector);
|
|
||||||
|
|
||||||
/// <summary>objc_msgSend for -gpuResourceID. MTLResourceID is a one-field
|
|
||||||
/// 8-byte struct, returned in a register on the x86-64 ABI, so it maps to a
|
|
||||||
/// ulong return — the value written into a Tier 2 argument buffer slot.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial ulong SendGpuResourceId(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>objc_msgSend for a CGRect-returning selector (e.g. -bounds).
|
|
||||||
/// A 32-byte struct is returned via the x86-64 stret ABI — a hidden
|
|
||||||
/// pointer to caller storage passed ahead of self/_cmd — so this must not
|
|
||||||
/// be folded into the plain objc_msgSend overloads.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend_stret")]
|
|
||||||
public static partial void SendStretRect(out CGRect result, nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument, ref nint error);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint Send(nint receiver, nint selector, nint argument0, nint argument1, ref nint error);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendAtIndex(nint receiver, nint selector, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool SendBool(nint receiver, nint selector);
|
|
||||||
|
|
||||||
/// <summary>One-argument BOOL sends, e.g. respondsToSelector:.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
|
||||||
public static partial bool SendBool(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial double SendDouble(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector, nint argument);
|
|
||||||
|
|
||||||
/// <summary>Two-object-argument void sends, e.g. setObject:forKey:.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoid(nint receiver, nint selector, nint argument0, nint argument1);
|
|
||||||
|
|
||||||
/// <summary>performSelectorOnMainThread:withObject:waitUntilDone: — the SEL
|
|
||||||
/// to perform is itself an argument, followed by the object and the wait
|
|
||||||
/// flag.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidPerformSelector(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint performedSelector,
|
|
||||||
nint argument,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool waitUntilDone);
|
|
||||||
|
|
||||||
/// <summary>setSwizzle: on MTLTextureDescriptor. Four one-byte
|
|
||||||
/// MTLTextureSwizzle values, passed packed like the framework expects.</summary>
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidSwizzle(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlTextureSwizzleChannels channels);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidBool(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool argument);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidDouble(nint receiver, nint selector, double argument);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidSize(nint receiver, nint selector, CGSize size);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidRect(nint receiver, nint selector, CGRect rect);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidClearColor(nint receiver, nint selector, MtlClearColor color);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidBlendColor(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
float red,
|
|
||||||
float green,
|
|
||||||
float blue,
|
|
||||||
float alpha);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidViewport(nint receiver, nint selector, MtlViewport viewport);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendSetAtIndex(nint receiver, nint selector, nint value, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidCopyTexture(nint receiver, nint selector, nint source, nint destination);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendBuffer(nint receiver, nint selector, nint bytes, nuint length, nuint options);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendNewBuffer(nint receiver, nint selector, nuint length, nuint options);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendCopyTextureToBuffer(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint sourceTexture,
|
|
||||||
nuint sourceSlice,
|
|
||||||
nuint sourceLevel,
|
|
||||||
MtlOrigin sourceOrigin,
|
|
||||||
MtlSize sourceSize,
|
|
||||||
nint destinationBuffer,
|
|
||||||
nuint destinationOffset,
|
|
||||||
nuint destinationBytesPerRow,
|
|
||||||
nuint destinationBytesPerImage);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendCopyBufferToTexture(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nint sourceBuffer,
|
|
||||||
nuint sourceOffset,
|
|
||||||
nuint sourceBytesPerRow,
|
|
||||||
nuint sourceBytesPerImage,
|
|
||||||
MtlSize sourceSize,
|
|
||||||
nint destinationTexture,
|
|
||||||
nuint destinationSlice,
|
|
||||||
nuint destinationLevel,
|
|
||||||
MtlOrigin destinationOrigin);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDispatch(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlSize threadgroups,
|
|
||||||
MtlSize threadsPerThreadgroup);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendSetBuffer(nint receiver, nint selector, nint buffer, nuint offset, nuint index);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendVoidScissor(nint receiver, nint selector, MtlScissorRect rect);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawPrimitivesInstanced(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint vertexStart,
|
|
||||||
nuint vertexCount,
|
|
||||||
nuint instanceCount);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawIndexedPrimitives(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint indexCount,
|
|
||||||
nuint indexType,
|
|
||||||
nint indexBuffer,
|
|
||||||
nuint indexBufferOffset,
|
|
||||||
nuint instanceCount);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendTimer(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
double interval,
|
|
||||||
nint target,
|
|
||||||
nint timerSelector,
|
|
||||||
nint userInfo,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool repeats);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendInitFrame(nint receiver, nint selector, CGRect frame);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendInitWindow(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
CGRect contentRect,
|
|
||||||
nuint styleMask,
|
|
||||||
nuint backing,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool defer);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendNextEvent(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
ulong eventMask,
|
|
||||||
nint untilDate,
|
|
||||||
nint inMode,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool dequeue);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial nint SendTextureDescriptor(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint pixelFormat,
|
|
||||||
nuint width,
|
|
||||||
nuint height,
|
|
||||||
[MarshalAs(UnmanagedType.I1)] bool mipmapped);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendReplaceRegion(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
MtlRegion region,
|
|
||||||
nuint mipmapLevel,
|
|
||||||
nint bytes,
|
|
||||||
nuint bytesPerRow);
|
|
||||||
|
|
||||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
|
||||||
public static partial void SendDrawPrimitives(
|
|
||||||
nint receiver,
|
|
||||||
nint selector,
|
|
||||||
nuint primitiveType,
|
|
||||||
nuint vertexStart,
|
|
||||||
nuint vertexCount);
|
|
||||||
|
|
||||||
public static nint Class(string name) => objc_getClass(name);
|
|
||||||
|
|
||||||
public static nint Selector(string name) => sel_registerName(name);
|
|
||||||
|
|
||||||
/// <summary>Autoreleased NSString — only valid inside an autorelease pool
|
|
||||||
/// unless the caller retains it.</summary>
|
|
||||||
public static nint NsString(string value)
|
|
||||||
{
|
|
||||||
var utf8 = Marshal.StringToCoTaskMemUTF8(value);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Send(Class("NSString"), Selector("stringWithUTF8String:"), utf8);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(utf8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Reads an NSString's UTF-8 contents, or null if the handle is nil.</summary>
|
|
||||||
public static string? ReadNsString(nint nsString)
|
|
||||||
{
|
|
||||||
if (nsString == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var utf8 = Send(nsString, Selector("UTF8String"));
|
|
||||||
return utf8 == 0 ? null : Marshal.PtrToStringUTF8(utf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribeError(nint error)
|
|
||||||
{
|
|
||||||
if (error == 0)
|
|
||||||
{
|
|
||||||
return "unknown error";
|
|
||||||
}
|
|
||||||
|
|
||||||
var description = Send(error, Selector("localizedDescription"));
|
|
||||||
var utf8 = Send(description, Selector("UTF8String"));
|
|
||||||
return Marshal.PtrToStringUTF8(utf8) ?? "unknown error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Guest draws and compute dispatches batch into one command buffer per drain
|
|
||||||
// instead of one per work item, mirroring the Vulkan presenter's batched guest
|
|
||||||
// commands: commit overhead dominated CPU time for scenes with dozens of draws
|
|
||||||
// per frame. Ordering inside the batch is by encoder sequence (snapshot blits
|
|
||||||
// for a draw's feedback reads are encoded before its render pass opens), and
|
|
||||||
// everything that must observe batched work on the serial queue — flips, image
|
|
||||||
// writes/blits, CPU-visible write-backs, the present pass — flushes first.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private static nint _batchCommandBuffer;
|
|
||||||
private static bool _batchOpen;
|
|
||||||
|
|
||||||
/// <summary>Returns the open batch command buffer, opening one on first
|
|
||||||
/// use. Render thread only, like the drain it serves.</summary>
|
|
||||||
private static nint BeginBatchedGuestCommands(nint queue)
|
|
||||||
{
|
|
||||||
if (_batchOpen)
|
|
||||||
{
|
|
||||||
return _batchCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_batchCommandBuffer = MetalNative.Send(queue, MetalNative.Selector("commandBuffer"));
|
|
||||||
_batchOpen = _batchCommandBuffer != 0;
|
|
||||||
return _batchCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Commits the open batch (if any), tagging the upload pages and
|
|
||||||
/// snapshot resources it consumed. Returns the committed command buffer so
|
|
||||||
/// write-back sites can wait on it, or 0 when nothing was open.</summary>
|
|
||||||
private static nint FlushBatchedGuestCommands()
|
|
||||||
{
|
|
||||||
if (!_batchOpen)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_batchOpen = false;
|
|
||||||
var commandBuffer = _batchCommandBuffer;
|
|
||||||
_batchCommandBuffer = 0;
|
|
||||||
MetalNative.SendVoid(commandBuffer, MetalNative.Selector("commit"));
|
|
||||||
TagUploadPages(commandBuffer);
|
|
||||||
TagSnapshotResources(commandBuffer);
|
|
||||||
return commandBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Guest compute dispatches: ordered guest work like draws, with two contracts to
|
|
||||||
// honor. Storage images are shared live through the guest-image registry so a
|
|
||||||
// dispatch's writes are visible to later draws, blits, and flips of the same
|
|
||||||
// address; and CPU-visible buffer writes land back in guest memory before the
|
|
||||||
// work item completes, which is the ordering point WaitForGuestWork promises.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private static readonly bool _skipAllCompute =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_SKIP_ALL_COMPUTE") == "1";
|
|
||||||
private static bool _tracedDispatchBase;
|
|
||||||
|
|
||||||
private sealed record ComputeGuestDispatch(
|
|
||||||
ulong ShaderAddress,
|
|
||||||
MetalCompiledGuestShader Shader,
|
|
||||||
GuestDrawTexture[] Textures,
|
|
||||||
GuestMemoryBuffer[] GlobalMemoryBuffers,
|
|
||||||
uint GroupCountX,
|
|
||||||
uint GroupCountY,
|
|
||||||
uint GroupCountZ,
|
|
||||||
uint BaseGroupX,
|
|
||||||
uint BaseGroupY,
|
|
||||||
uint BaseGroupZ,
|
|
||||||
uint ThreadCountX,
|
|
||||||
uint ThreadCountY,
|
|
||||||
uint ThreadCountZ);
|
|
||||||
|
|
||||||
private static readonly Dictionary<MetalCompiledGuestShader, nint> _computePipelineCache = new();
|
|
||||||
|
|
||||||
public static long SubmitComputeDispatch(
|
|
||||||
ulong shaderAddress,
|
|
||||||
MetalCompiledGuestShader computeShader,
|
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
|
||||||
uint groupCountX,
|
|
||||||
uint groupCountY,
|
|
||||||
uint groupCountZ,
|
|
||||||
uint baseGroupX,
|
|
||||||
uint baseGroupY,
|
|
||||||
uint baseGroupZ,
|
|
||||||
bool writesGlobalMemory,
|
|
||||||
uint threadCountX,
|
|
||||||
uint threadCountY,
|
|
||||||
uint threadCountZ)
|
|
||||||
{
|
|
||||||
var hasStorage = false;
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
hasStorage |= texture.IsStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupCountX == 0 ||
|
|
||||||
groupCountY == 0 ||
|
|
||||||
groupCountZ == 0 ||
|
|
||||||
(!hasStorage && !writesGlobalMemory))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_closed || _thread is null)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage images a dispatch writes become flip sources and sampled
|
|
||||||
// inputs for later work, exactly like published render targets.
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
if (!texture.IsStorage || texture.Address == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
|
|
||||||
if (guestFormat != 0)
|
|
||||||
{
|
|
||||||
_availableGuestImages[texture.Address] = guestFormat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sequence = EnqueueGuestWorkLocked(
|
|
||||||
new ComputeGuestDispatch(
|
|
||||||
shaderAddress,
|
|
||||||
computeShader,
|
|
||||||
ToArray(textures),
|
|
||||||
ToArray(globalMemoryBuffers),
|
|
||||||
groupCountX,
|
|
||||||
groupCountY,
|
|
||||||
groupCountZ,
|
|
||||||
baseGroupX,
|
|
||||||
baseGroupY,
|
|
||||||
baseGroupZ,
|
|
||||||
threadCountX,
|
|
||||||
threadCountY,
|
|
||||||
threadCountZ));
|
|
||||||
foreach (var texture in textures)
|
|
||||||
{
|
|
||||||
if (texture.IsStorage && texture.Address != 0)
|
|
||||||
{
|
|
||||||
_guestImageWorkSequences[texture.Address] = sequence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ExecuteComputeDispatch(nint device, nint queue, ComputeGuestDispatch dispatch)
|
|
||||||
{
|
|
||||||
if (_skipAllCompute)
|
|
||||||
{
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoOut.PerfOverlay.RecordDraw();
|
|
||||||
|
|
||||||
if ((dispatch.BaseGroupX | dispatch.BaseGroupY | dispatch.BaseGroupZ) != 0 &&
|
|
||||||
!_tracedDispatchBase)
|
|
||||||
{
|
|
||||||
// Metal has no dispatch-base; the translated kernel derives its ids
|
|
||||||
// from the raw grid position, so a nonzero base computes offset-zero
|
|
||||||
// work until base support lands in the emitted kernel.
|
|
||||||
_tracedDispatchBase = true;
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] Metal compute dispatch with nonzero base group " +
|
|
||||||
$"({dispatch.BaseGroupX},{dispatch.BaseGroupY},{dispatch.BaseGroupZ}); " +
|
|
||||||
"executing without the base offset.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryGetComputePipeline(device, dispatch.Shader, out var pipeline))
|
|
||||||
{
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandBuffer = BeginBatchedGuestCommands(queue);
|
|
||||||
|
|
||||||
// Pre-resolve textures before the compute encoder opens: snapshot
|
|
||||||
// blits for feedback reads encode into the batch and encoder order
|
|
||||||
// must place them ahead of this dispatch.
|
|
||||||
Span<nint> textureHandles = stackalloc nint[dispatch.Textures.Length];
|
|
||||||
Span<bool> textureOwned = stackalloc bool[dispatch.Textures.Length];
|
|
||||||
for (var index = 0; index < dispatch.Textures.Length; index++)
|
|
||||||
{
|
|
||||||
var descriptor = dispatch.Textures[index];
|
|
||||||
if (descriptor.IsStorage && descriptor.Address != 0)
|
|
||||||
{
|
|
||||||
textureHandles[index] = EnsureStorageImage(device, descriptor)?.Texture ?? 0;
|
|
||||||
textureOwned[index] = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
textureHandles[index] = CreateDrawTexture(
|
|
||||||
device, commandBuffer, descriptor, out var ownedTexture);
|
|
||||||
textureOwned[index] = ownedTexture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoder = MetalNative.Send(commandBuffer, MetalNative.Selector("computeCommandEncoder"));
|
|
||||||
MetalNative.SendVoid(encoder, MetalNative.Selector("setComputePipelineState:"), pipeline);
|
|
||||||
|
|
||||||
var writeBackBuffers = new List<(nint Pointer, GuestMemoryBuffer Guest)>();
|
|
||||||
var selSetBuffer = MetalNative.Selector("setBuffer:offset:atIndex:");
|
|
||||||
var bufferCount = dispatch.GlobalMemoryBuffers.Length;
|
|
||||||
Span<uint> boundBytes = stackalloc uint[Math.Max(bufferCount, 1)];
|
|
||||||
for (var index = 0; index < bufferCount; index++)
|
|
||||||
{
|
|
||||||
var guest = dispatch.GlobalMemoryBuffers[index];
|
|
||||||
var pointer = UploadGlobalBuffer(
|
|
||||||
device, guest, out var buffer, out var offset, out boundBytes[index]);
|
|
||||||
MetalNative.SendSetBuffer(encoder, selSetBuffer, buffer, (nuint)offset, (nuint)index);
|
|
||||||
if (guest.Writable && guest.WriteBackToGuest)
|
|
||||||
{
|
|
||||||
writeBackBuffers.Add((pointer, guest));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SharpEmuUniforms: the dispatch limit clamps the overshoot threads of the
|
|
||||||
// last threadgroup row, then each bound buffer's byte length follows
|
|
||||||
// (including the alignment-bias prefix the shader indexes past).
|
|
||||||
var shader = dispatch.Shader.Shader;
|
|
||||||
var uniforms = AllocateUpload(
|
|
||||||
device,
|
|
||||||
16 + (Math.Max(bufferCount, 1) * sizeof(uint)),
|
|
||||||
out var uniformsBuffer,
|
|
||||||
out var uniformsOffset);
|
|
||||||
WriteDispatchLimit(uniforms, 0, dispatch.ThreadCountX, dispatch.GroupCountX, shader.ThreadgroupSizeX);
|
|
||||||
WriteDispatchLimit(uniforms, 4, dispatch.ThreadCountY, dispatch.GroupCountY, shader.ThreadgroupSizeY);
|
|
||||||
WriteDispatchLimit(uniforms, 8, dispatch.ThreadCountZ, dispatch.GroupCountZ, shader.ThreadgroupSizeZ);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(uniforms[12..], 0);
|
|
||||||
for (var index = 0; index < bufferCount; index++)
|
|
||||||
{
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
uniforms[(16 + (index * sizeof(uint)))..],
|
|
||||||
boundBytes[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind at the stage's declared SharpEmuUniforms slot (see the draw path:
|
|
||||||
// stages compute their own index from globalBufferBase + total count).
|
|
||||||
var uniformsIndex = shader.UniformsBufferIndex;
|
|
||||||
MetalNative.SendSetBuffer(
|
|
||||||
encoder,
|
|
||||||
selSetBuffer,
|
|
||||||
uniformsBuffer,
|
|
||||||
(nuint)uniformsOffset,
|
|
||||||
(nuint)(uniformsIndex >= 0 ? uniformsIndex : bufferCount));
|
|
||||||
|
|
||||||
var selSetTexture = MetalNative.Selector("setTexture:atIndex:");
|
|
||||||
for (var index = 0; index < dispatch.Textures.Length; index++)
|
|
||||||
{
|
|
||||||
var texture = textureHandles[index];
|
|
||||||
if (texture != 0)
|
|
||||||
{
|
|
||||||
MetalNative.SendSetAtIndex(encoder, selSetTexture, texture, (nuint)index);
|
|
||||||
if (textureOwned[index])
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(texture, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Samplers travel in an argument buffer bound at setBuffer (see the draw
|
|
||||||
// path), sidestepping Metal's 16-sampler-per-stage cap.
|
|
||||||
BindSamplerArgumentBuffer(device, encoder, selSetBuffer, dispatch.Shader, dispatch.Textures);
|
|
||||||
|
|
||||||
MetalNative.SendDispatch(
|
|
||||||
encoder,
|
|
||||||
MetalNative.Selector("dispatchThreadgroups:threadsPerThreadgroup:"),
|
|
||||||
new MtlSize
|
|
||||||
{
|
|
||||||
Width = dispatch.GroupCountX,
|
|
||||||
Height = dispatch.GroupCountY,
|
|
||||||
Depth = dispatch.GroupCountZ,
|
|
||||||
},
|
|
||||||
new MtlSize
|
|
||||||
{
|
|
||||||
Width = Math.Max(shader.ThreadgroupSizeX, 1),
|
|
||||||
Height = Math.Max(shader.ThreadgroupSizeY, 1),
|
|
||||||
Depth = Math.Max(shader.ThreadgroupSizeZ, 1),
|
|
||||||
});
|
|
||||||
MetalNative.SendVoid(encoder, MetalNative.Selector("endEncoding"));
|
|
||||||
|
|
||||||
// CPU-visible writes are ordering points (see the draw path): flush
|
|
||||||
// the batch and wait so the write-back lands before this work item
|
|
||||||
// completes. Pure-GPU dispatches stay in the open batch.
|
|
||||||
if (writeBackBuffers.Count > 0)
|
|
||||||
{
|
|
||||||
var committed = FlushBatchedGuestCommands();
|
|
||||||
MetalNative.SendVoid(committed, MetalNative.Selector("waitUntilCompleted"));
|
|
||||||
WriteBuffersBackToGuest(writeBackBuffers);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var descriptor in dispatch.Textures)
|
|
||||||
{
|
|
||||||
if (!descriptor.IsStorage || descriptor.Address == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestImage? image;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_guestImages.TryGetValue(descriptor.Address, out image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image is not null)
|
|
||||||
{
|
|
||||||
image.MarkContentChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnPooledComputeData(dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The live, shared storage image for a guest address: dispatches,
|
|
||||||
/// draws, blits, and flips of the same address all see one texture.</summary>
|
|
||||||
private static GuestImage? EnsureStorageImage(nint device, GuestDrawTexture descriptor)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_guestImages.TryGetValue(descriptor.Address, out var existing))
|
|
||||||
{
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.Width == 0 || descriptor.Height == 0 ||
|
|
||||||
descriptor.Width > 16384 || descriptor.Height > 16384)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var format = MetalGuestFormats.TryDecodeRenderTargetFormat(
|
|
||||||
descriptor.Format, descriptor.NumberType, out var decoded)
|
|
||||||
? decoded.Format
|
|
||||||
: MtlPixelFormat.Rgba8Unorm;
|
|
||||||
var textureDescriptor = MetalNative.SendTextureDescriptor(
|
|
||||||
MetalNative.Class("MTLTextureDescriptor"),
|
|
||||||
MetalNative.Selector("texture2DDescriptorWithPixelFormat:width:height:mipmapped:"),
|
|
||||||
(nuint)format,
|
|
||||||
descriptor.Width,
|
|
||||||
descriptor.Height,
|
|
||||||
mipmapped: false);
|
|
||||||
MetalNative.Send(
|
|
||||||
textureDescriptor,
|
|
||||||
MetalNative.Selector("setUsage:"),
|
|
||||||
(nint)(UsageShaderRead | UsageShaderWrite | UsageRenderTarget));
|
|
||||||
var image = new GuestImage
|
|
||||||
{
|
|
||||||
Texture = MetalNative.Send(
|
|
||||||
device, MetalNative.Selector("newTextureWithDescriptor:"), textureDescriptor),
|
|
||||||
Width = descriptor.Width,
|
|
||||||
Height = descriptor.Height,
|
|
||||||
Format = format,
|
|
||||||
};
|
|
||||||
if (image.Texture == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytesPerPixel = MetalRenderTargetFormat.GetBytesPerPixel(format);
|
|
||||||
// Snapshot copies arrive in the image's native texel layout; only
|
|
||||||
// 4-byte texels can be RGBA8 verbatim, wider ones carry native bytes.
|
|
||||||
if ((ulong)descriptor.RgbaPixels.Length >= (ulong)descriptor.Width * bytesPerPixel)
|
|
||||||
{
|
|
||||||
var pitch = descriptor.Pitch != 0
|
|
||||||
? Math.Max(descriptor.Pitch, descriptor.Width)
|
|
||||||
: descriptor.Width;
|
|
||||||
ReplaceTextureContents(
|
|
||||||
image.Texture, descriptor.Width, descriptor.Height, descriptor.RgbaPixels, pitch, bytesPerPixel);
|
|
||||||
image.MarkContentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_guestImages.TryGetValue(descriptor.Address, out var raced))
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(image.Texture, MetalNative.Selector("release"));
|
|
||||||
return raced;
|
|
||||||
}
|
|
||||||
|
|
||||||
_guestImages[descriptor.Address] = image;
|
|
||||||
_guestImageExtents[descriptor.Address] =
|
|
||||||
(descriptor.Width, descriptor.Height, (ulong)descriptor.Width * descriptor.Height * bytesPerPixel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetComputePipeline(nint device, MetalCompiledGuestShader shader, out nint pipeline)
|
|
||||||
{
|
|
||||||
lock (_computePipelineCache)
|
|
||||||
{
|
|
||||||
if (_computePipelineCache.TryGetValue(shader, out pipeline))
|
|
||||||
{
|
|
||||||
return pipeline != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var function = GetShaderFunction(device, shader);
|
|
||||||
if (function != 0)
|
|
||||||
{
|
|
||||||
nint error = 0;
|
|
||||||
pipeline = MetalNative.Send(
|
|
||||||
device,
|
|
||||||
MetalNative.Selector("newComputePipelineStateWithFunction:error:"),
|
|
||||||
function,
|
|
||||||
ref error);
|
|
||||||
if (pipeline == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] Metal compute pipeline creation failed: {MetalNative.DescribeError(error)}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _perfPipelineCreations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pipeline = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_computePipelineCache)
|
|
||||||
{
|
|
||||||
_computePipelineCache[shader] = pipeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pipeline != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteDispatchLimit(
|
|
||||||
Span<byte> uniforms,
|
|
||||||
int offset,
|
|
||||||
uint threadCount,
|
|
||||||
uint groupCount,
|
|
||||||
uint threadgroupSize)
|
|
||||||
{
|
|
||||||
var limit = threadCount != uint.MaxValue
|
|
||||||
? threadCount
|
|
||||||
: groupCount * Math.Max(threadgroupSize, 1);
|
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
uniforms[offset..],
|
|
||||||
limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReturnPooledComputeData(ComputeGuestDispatch dispatch)
|
|
||||||
{
|
|
||||||
foreach (var buffer in dispatch.GlobalMemoryBuffers)
|
|
||||||
{
|
|
||||||
if (buffer.Pooled)
|
|
||||||
{
|
|
||||||
GuestDataPool.Shared.Return(buffer.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Feedback reads (draws sampling a live guest render target or depth image)
|
|
||||||
// need a fresh ordered snapshot per draw. Creating and destroying an MTLTexture
|
|
||||||
// — and for depth reads a private staging MTLBuffer — per draw is measurable
|
|
||||||
// CPU and allocator churn at hundreds of feedback draws per second, so both
|
|
||||||
// recycle through a pool with the same lifecycle as the upload arena pages:
|
|
||||||
// acquired snapshots are tagged with the command buffer that samples them at
|
|
||||||
// commit, and return to the free list once that command buffer completes (the
|
|
||||||
// command queue is serial, so the earlier snapshot-blit command buffer is
|
|
||||||
// necessarily complete by then too). Everything here runs on the render thread.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int MaxFreeSnapshotResources = 16;
|
|
||||||
|
|
||||||
private sealed class PooledSnapshotResource
|
|
||||||
{
|
|
||||||
public nint Handle;
|
|
||||||
public bool IsBuffer;
|
|
||||||
|
|
||||||
/// <summary>Texture identity (unused for buffers).</summary>
|
|
||||||
public uint Format;
|
|
||||||
public uint Width;
|
|
||||||
public uint Height;
|
|
||||||
public nint Usage;
|
|
||||||
|
|
||||||
/// <summary>Buffer capacity in bytes (unused for textures).</summary>
|
|
||||||
public nuint Capacity;
|
|
||||||
|
|
||||||
/// <summary>Retained handle of the command buffer that samples this
|
|
||||||
/// snapshot; the resource is reusable once it completes.</summary>
|
|
||||||
public nint LastCommandBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly List<PooledSnapshotResource> _retiredSnapshotResources = [];
|
|
||||||
private static readonly List<PooledSnapshotResource> _pendingSnapshotResources = [];
|
|
||||||
private static readonly List<PooledSnapshotResource> _freeSnapshotResources = [];
|
|
||||||
|
|
||||||
/// <summary>Returns completed snapshot resources to the free list; called
|
|
||||||
/// once per render-loop drain, next to the upload-page recycler.</summary>
|
|
||||||
private static void RecycleCompletedSnapshotResources()
|
|
||||||
{
|
|
||||||
for (var index = _retiredSnapshotResources.Count - 1; index >= 0; index--)
|
|
||||||
{
|
|
||||||
var resource = _retiredSnapshotResources[index];
|
|
||||||
if (resource.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
// MTLCommandBufferStatus: Completed = 4, Error = 5.
|
|
||||||
var status = MetalNative.Send(
|
|
||||||
resource.LastCommandBuffer, MetalNative.Selector("status"));
|
|
||||||
if (status < 4)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MetalNative.SendVoid(resource.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
resource.LastCommandBuffer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_retiredSnapshotResources.RemoveAt(index);
|
|
||||||
if (_freeSnapshotResources.Count < MaxFreeSnapshotResources)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.Add(resource);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(resource.Handle, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pops a pooled snapshot texture matching the exact identity, or
|
|
||||||
/// creates one. The returned handle is owned by the pool — callers must not
|
|
||||||
/// release it, and it must be tagged at the next commit.</summary>
|
|
||||||
private static nint AcquireSnapshotTexture(
|
|
||||||
nint device,
|
|
||||||
MtlPixelFormat format,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
nint usage)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _freeSnapshotResources.Count; index++)
|
|
||||||
{
|
|
||||||
var candidate = _freeSnapshotResources[index];
|
|
||||||
if (!candidate.IsBuffer &&
|
|
||||||
candidate.Format == (uint)format &&
|
|
||||||
candidate.Width == width &&
|
|
||||||
candidate.Height == height &&
|
|
||||||
candidate.Usage == usage)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.RemoveAt(index);
|
|
||||||
_pendingSnapshotResources.Add(candidate);
|
|
||||||
return candidate.Handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var descriptor = MetalNative.SendTextureDescriptor(
|
|
||||||
MetalNative.Class("MTLTextureDescriptor"),
|
|
||||||
MetalNative.Selector("texture2DDescriptorWithPixelFormat:width:height:mipmapped:"),
|
|
||||||
(nuint)format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mipmapped: false);
|
|
||||||
MetalNative.Send(descriptor, MetalNative.Selector("setUsage:"), usage);
|
|
||||||
var handle = MetalNative.Send(
|
|
||||||
device, MetalNative.Selector("newTextureWithDescriptor:"), descriptor);
|
|
||||||
if (handle == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Add(new PooledSnapshotResource
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
Format = (uint)format,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
Usage = usage,
|
|
||||||
});
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pops a pooled private-storage staging buffer of at least
|
|
||||||
/// <paramref name="minimumBytes"/>, or creates one. Pool-owned like
|
|
||||||
/// <see cref="AcquireSnapshotTexture"/>.</summary>
|
|
||||||
private static nint AcquireSnapshotBuffer(nint device, nuint minimumBytes)
|
|
||||||
{
|
|
||||||
for (var index = 0; index < _freeSnapshotResources.Count; index++)
|
|
||||||
{
|
|
||||||
var candidate = _freeSnapshotResources[index];
|
|
||||||
if (candidate.IsBuffer && candidate.Capacity >= minimumBytes)
|
|
||||||
{
|
|
||||||
_freeSnapshotResources.RemoveAt(index);
|
|
||||||
_pendingSnapshotResources.Add(candidate);
|
|
||||||
return candidate.Handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MTLResourceStorageModePrivate = 32: staging never touches the CPU.
|
|
||||||
var handle = MetalNative.SendNewBuffer(
|
|
||||||
device, MetalNative.Selector("newBufferWithLength:options:"), minimumBytes, 32);
|
|
||||||
if (handle == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Add(new PooledSnapshotResource
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
IsBuffer = true,
|
|
||||||
Capacity = minimumBytes,
|
|
||||||
});
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Marks every snapshot resource acquired since the previous tag
|
|
||||||
/// as owing its lifetime to <paramref name="commandBuffer"/>. Called at the
|
|
||||||
/// same commit sites as <see cref="TagUploadPages"/>; a resource acquired
|
|
||||||
/// for a draw that never committed is tagged by the next commit, which is
|
|
||||||
/// conservative but safe.</summary>
|
|
||||||
private static void TagSnapshotResources(nint commandBuffer)
|
|
||||||
{
|
|
||||||
if (_pendingSnapshotResources.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var resource in _pendingSnapshotResources)
|
|
||||||
{
|
|
||||||
resource.LastCommandBuffer = MetalNative.Send(
|
|
||||||
commandBuffer, MetalNative.Selector("retain"));
|
|
||||||
_retiredSnapshotResources.Add(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingSnapshotResources.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Draw textures decoded from guest memory are cached across draws keyed by
|
|
||||||
// their full descriptor identity, mirroring the Vulkan presenter's texture
|
|
||||||
// cache: once an identity is marked cached, the AGC submit thread skips the
|
|
||||||
// guest-memory read/detile/copy entirely (shipping empty texels) and the
|
|
||||||
// render thread serves the cached MTLTexture — for scenes that sample large
|
|
||||||
// textures every draw, that per-draw copy dominated both allocation churn
|
|
||||||
// and CPU time. GuestImageWriteTracker write-protects the source pages, so
|
|
||||||
// a guest CPU write dirties the address and the entry is evicted at the next
|
|
||||||
// drain; the following draw ships fresh texels and re-populates the cache.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int MaxCachedDrawTextures = 2048;
|
|
||||||
|
|
||||||
/// <summary>Render-thread-only cache of decoded draw textures; each value
|
|
||||||
/// holds one retain. Committed command buffers retain the textures they
|
|
||||||
/// reference, so eviction releases immediately without a GPU drain.</summary>
|
|
||||||
private static readonly Dictionary<TextureContentIdentity, nint> _drawTextureCache = new();
|
|
||||||
|
|
||||||
/// <summary>Identities the AGC submit thread may skip texel copies for.
|
|
||||||
/// Read from the submit thread, written by the render thread.</summary>
|
|
||||||
private static readonly ConcurrentDictionary<TextureContentIdentity, byte> _cachedDrawTextureIdentities = new();
|
|
||||||
|
|
||||||
internal static bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
_cachedDrawTextureIdentities.ContainsKey(identity);
|
|
||||||
|
|
||||||
/// <summary>Builds the same identity the AGC layer checks before skipping
|
|
||||||
/// a texel copy; the two must agree field-for-field or skips and cache
|
|
||||||
/// entries would never line up.</summary>
|
|
||||||
private static TextureContentIdentity GetDrawTextureIdentity(GuestDrawTexture texture) => new(
|
|
||||||
texture.Address,
|
|
||||||
texture.Width,
|
|
||||||
texture.Height,
|
|
||||||
texture.Format,
|
|
||||||
texture.NumberType,
|
|
||||||
texture.DstSelect,
|
|
||||||
texture.TileMode,
|
|
||||||
texture.Pitch,
|
|
||||||
texture.Sampler);
|
|
||||||
|
|
||||||
/// <summary>Caching requires the write tracker: without page protection a
|
|
||||||
/// guest CPU write would never evict the entry and draws would sample
|
|
||||||
/// stale texels forever. Storage textures are shader-writable on the GPU,
|
|
||||||
/// so their content identity is not stable either.</summary>
|
|
||||||
private static bool IsCacheableDrawTexture(GuestDrawTexture texture) =>
|
|
||||||
GuestImageWriteTracker.Enabled &&
|
|
||||||
texture.Address != 0 &&
|
|
||||||
!texture.IsStorage &&
|
|
||||||
!texture.IsFallback;
|
|
||||||
|
|
||||||
private static bool TryGetCachedDrawTexture(GuestDrawTexture texture, out nint handle) =>
|
|
||||||
_drawTextureCache.TryGetValue(GetDrawTextureIdentity(texture), out handle);
|
|
||||||
|
|
||||||
private static void CacheDrawTexture(GuestDrawTexture texture, nint handle)
|
|
||||||
{
|
|
||||||
var key = GetDrawTextureIdentity(texture);
|
|
||||||
if (_drawTextureCache.Remove(key, out var previous))
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(previous, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = MetalNative.Send(handle, MetalNative.Selector("retain"));
|
|
||||||
_drawTextureCache[key] = handle;
|
|
||||||
_cachedDrawTextureIdentities[key] = 0;
|
|
||||||
GuestImageWriteTracker.Track(
|
|
||||||
texture.Address,
|
|
||||||
(ulong)texture.RgbaPixels.Length,
|
|
||||||
Volatile.Read(ref _executingGuestWorkSequence),
|
|
||||||
"metal.texture-cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Runs once per drain, before any queued draw executes: a draw
|
|
||||||
/// whose texels the submit thread skipped must never resolve to an entry
|
|
||||||
/// the guest has since rewritten.</summary>
|
|
||||||
private static void EvictDirtyCachedDrawTextures()
|
|
||||||
{
|
|
||||||
if (_drawTextureCache.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict by address rather than by identity: several identities can
|
|
||||||
// share one source address (same texels, different samplers), and
|
|
||||||
// ConsumeDirty clears the flag on first read — evicting only the
|
|
||||||
// first identity would leave the others sampling stale texels.
|
|
||||||
HashSet<ulong>? dirtyAddresses = null;
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
if (dirtyAddresses is not null && dirtyAddresses.Contains(entry.Key.Address))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GuestImageWriteTracker.ConsumeDirty(entry.Key.Address))
|
|
||||||
{
|
|
||||||
(dirtyAddresses ??= []).Add(entry.Key.Address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirtyAddresses is null && _drawTextureCache.Count <= MaxCachedDrawTextures)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_drawTextureCache.Count > MaxCachedDrawTextures)
|
|
||||||
{
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(entry.Value, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawTextureCache.Clear();
|
|
||||||
_cachedDrawTextureIdentities.Clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TextureContentIdentity>? evicted = null;
|
|
||||||
foreach (var entry in _drawTextureCache)
|
|
||||||
{
|
|
||||||
if (dirtyAddresses!.Contains(entry.Key.Address))
|
|
||||||
{
|
|
||||||
(evicted ??= []).Add(entry.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evicted is not null)
|
|
||||||
{
|
|
||||||
foreach (var key in evicted)
|
|
||||||
{
|
|
||||||
if (_drawTextureCache.Remove(key, out var handle))
|
|
||||||
{
|
|
||||||
_cachedDrawTextureIdentities.TryRemove(key, out _);
|
|
||||||
MetalNative.SendVoid(handle, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var address in dirtyAddresses!)
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Rearm(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Self-heal for the skip/eviction race: the submit thread saw a
|
|
||||||
/// cached identity and skipped the copy, but the entry was evicted before
|
|
||||||
/// this draw executed. Read the texels directly rather than rendering a
|
|
||||||
/// fallback texture for the frame, sized with the same block-aware math
|
|
||||||
/// the draw path expects.</summary>
|
|
||||||
private static byte[]? TryReadGuestDrawTexturePixels(GuestDrawTexture texture)
|
|
||||||
{
|
|
||||||
var memory = _guestMemory;
|
|
||||||
if (memory is null || texture.Address == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = Math.Max(texture.Width, 1u);
|
|
||||||
var height = Math.Max(texture.Height, 1u);
|
|
||||||
var rowLength = texture.TileMode == 0
|
|
||||||
? Math.Max(texture.Pitch, width)
|
|
||||||
: width;
|
|
||||||
var format = MetalGuestFormats.DecodeTextureFormat(texture.Format, texture.NumberType);
|
|
||||||
var byteCount = MetalGuestFormats.GetTextureByteCount(format, rowLength, height);
|
|
||||||
if (byteCount == 0 || byteCount > int.MaxValue)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pixels = new byte[(int)byteCount];
|
|
||||||
return memory.TryRead(texture.Address, pixels) ? pixels : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu.Metal;
|
|
||||||
|
|
||||||
// Per-draw upload data (guest global buffers, uniforms, vertex and index
|
|
||||||
// bytes) bump-allocates from shared-storage arena pages bound by offset,
|
|
||||||
// instead of creating one MTLBuffer and one managed copy per binding per
|
|
||||||
// draw — which dominated allocation churn (hundreds of MB/s) and held the
|
|
||||||
// guest flip rate well under the display rate. Pages recycle once the last
|
|
||||||
// command buffer that referenced them reports completion; everything here
|
|
||||||
// runs on the render thread, so no state is locked.
|
|
||||||
internal static partial class MetalVideoPresenter
|
|
||||||
{
|
|
||||||
private const int UploadPageBytes = 8 * 1024 * 1024;
|
|
||||||
|
|
||||||
// Superset of every Metal bind-offset alignment rule (constant address
|
|
||||||
// space on Intel Macs is the strictest at 256), and conveniently the
|
|
||||||
// guest storage-buffer alignment the shader bias contract assumes.
|
|
||||||
private const int UploadAlignment = 256;
|
|
||||||
|
|
||||||
private sealed class UploadPage
|
|
||||||
{
|
|
||||||
public nint Buffer;
|
|
||||||
public nint Contents;
|
|
||||||
public int Capacity;
|
|
||||||
public int Offset;
|
|
||||||
|
|
||||||
/// <summary>Retained handle of the last command buffer that consumed
|
|
||||||
/// data from this page; the page is reusable once it completes.</summary>
|
|
||||||
public nint LastCommandBuffer;
|
|
||||||
|
|
||||||
/// <summary>Stamp of the last TagUploadPages call that saw this page,
|
|
||||||
/// so a commit only re-tags pages it actually touched.</summary>
|
|
||||||
public int TouchStamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly List<UploadPage> _retiredUploadPages = [];
|
|
||||||
private static readonly Stack<UploadPage> _freeUploadPages = new();
|
|
||||||
private static readonly List<UploadPage> _touchedUploadPages = [];
|
|
||||||
private static UploadPage? _currentUploadPage;
|
|
||||||
private static int _uploadTouchStamp;
|
|
||||||
|
|
||||||
/// <summary>Returns completed pages to the free stack. Called once per
|
|
||||||
/// render-loop drain; completion is polled (command buffer status) rather
|
|
||||||
/// than block-based so the ObjC interop stays block-free.</summary>
|
|
||||||
private static void RecycleCompletedUploadPages()
|
|
||||||
{
|
|
||||||
for (var index = _retiredUploadPages.Count - 1; index >= 0; index--)
|
|
||||||
{
|
|
||||||
var page = _retiredUploadPages[index];
|
|
||||||
if (page.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
// MTLCommandBufferStatus: Completed = 4, Error = 5.
|
|
||||||
var status = MetalNative.Send(
|
|
||||||
page.LastCommandBuffer, MetalNative.Selector("status"));
|
|
||||||
if (status < 4)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MetalNative.SendVoid(page.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
page.LastCommandBuffer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_retiredUploadPages.RemoveAt(index);
|
|
||||||
if (page.Capacity == UploadPageBytes)
|
|
||||||
{
|
|
||||||
page.Offset = 0;
|
|
||||||
_freeUploadPages.Push(page);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Oversized one-off allocation; not worth pooling.
|
|
||||||
MetalNative.SendVoid(page.Buffer, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Bump-allocates an aligned slice for CPU-written upload data.
|
|
||||||
/// The returned span is the slice's shared-storage memory; bind the
|
|
||||||
/// buffer at the returned offset.</summary>
|
|
||||||
private static unsafe Span<byte> AllocateUpload(
|
|
||||||
nint device,
|
|
||||||
int length,
|
|
||||||
out nint buffer,
|
|
||||||
out int offset)
|
|
||||||
{
|
|
||||||
var page = _currentUploadPage;
|
|
||||||
var aligned = page is null
|
|
||||||
? 0
|
|
||||||
: (page.Offset + UploadAlignment - 1) & ~(UploadAlignment - 1);
|
|
||||||
if (page is null || aligned + length > page.Capacity)
|
|
||||||
{
|
|
||||||
if (page is not null)
|
|
||||||
{
|
|
||||||
_retiredUploadPages.Add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
page = AcquireUploadPage(device, length);
|
|
||||||
_currentUploadPage = page;
|
|
||||||
aligned = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.TouchStamp != _uploadTouchStamp)
|
|
||||||
{
|
|
||||||
page.TouchStamp = _uploadTouchStamp;
|
|
||||||
_touchedUploadPages.Add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = page.Buffer;
|
|
||||||
offset = aligned;
|
|
||||||
page.Offset = aligned + length;
|
|
||||||
return new Span<byte>((void*)(page.Contents + aligned), length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UploadPage AcquireUploadPage(nint device, int minimumBytes)
|
|
||||||
{
|
|
||||||
if (minimumBytes <= UploadPageBytes && _freeUploadPages.Count > 0)
|
|
||||||
{
|
|
||||||
return _freeUploadPages.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
var capacity = Math.Max(minimumBytes, UploadPageBytes);
|
|
||||||
// Options 0 = MTLResourceStorageModeShared: CPU writes are coherent
|
|
||||||
// and write-backs read the GPU's stores after waitUntilCompleted.
|
|
||||||
var handle = MetalNative.SendNewBuffer(
|
|
||||||
device, MetalNative.Selector("newBufferWithLength:options:"), (nuint)capacity, 0);
|
|
||||||
return new UploadPage
|
|
||||||
{
|
|
||||||
Buffer = handle,
|
|
||||||
Contents = MetalNative.Send(handle, MetalNative.Selector("contents")),
|
|
||||||
Capacity = capacity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Marks every page touched since the previous tag as owing its
|
|
||||||
/// lifetime to <paramref name="commandBuffer"/>. Called after each commit
|
|
||||||
/// that consumed arena data.</summary>
|
|
||||||
private static void TagUploadPages(nint commandBuffer)
|
|
||||||
{
|
|
||||||
if (_touchedUploadPages.Count == 0)
|
|
||||||
{
|
|
||||||
_uploadTouchStamp++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var page in _touchedUploadPages)
|
|
||||||
{
|
|
||||||
if (page.LastCommandBuffer != 0)
|
|
||||||
{
|
|
||||||
MetalNative.SendVoid(page.LastCommandBuffer, MetalNative.Selector("release"));
|
|
||||||
}
|
|
||||||
|
|
||||||
page.LastCommandBuffer = MetalNative.Send(
|
|
||||||
commandBuffer, MetalNative.Selector("retain"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_touchedUploadPages.Clear();
|
|
||||||
_uploadTouchStamp++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,6 @@ namespace SharpEmu.Libs.Gpu.Vulkan;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||||
{
|
{
|
||||||
public string BackendName => "Vulkan";
|
|
||||||
|
|
||||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||||
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||||
|
|
||||||
@@ -345,60 +343,6 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable EnterGuestQueue(string queueName, ulong submissionId) =>
|
|
||||||
VulkanVideoPresenter.EnterGuestQueue(queueName, submissionId);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestAction(Action action, string debugName) =>
|
|
||||||
VulkanVideoPresenter.SubmitOrderedGuestAction(action, debugName);
|
|
||||||
|
|
||||||
public long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex) =>
|
|
||||||
VulkanVideoPresenter.SubmitOrderedGuestFlipWait(videoOutHandle, displayBufferIndex);
|
|
||||||
|
|
||||||
public bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite) =>
|
|
||||||
VulkanVideoPresenter.WaitForGuestWork(workSequence, timeoutMilliseconds);
|
|
||||||
|
|
||||||
public long CurrentGuestWorkSequenceForDiagnostics =>
|
|
||||||
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics;
|
|
||||||
|
|
||||||
public bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType) =>
|
|
||||||
VulkanVideoPresenter.IsGuestImageUploadKnown(address, format, numberType);
|
|
||||||
|
|
||||||
public bool GuestImageWantsInitialData(ulong address) =>
|
|
||||||
VulkanVideoPresenter.GuestImageWantsInitialData(address);
|
|
||||||
|
|
||||||
public void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels) =>
|
|
||||||
VulkanVideoPresenter.ProvideGuestImageInitialData(address, rgbaPixels);
|
|
||||||
|
|
||||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
|
||||||
VulkanVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
|
||||||
|
|
||||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
|
||||||
VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
|
||||||
|
|
||||||
public bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount) =>
|
|
||||||
VulkanVideoPresenter.TryGetGuestImageExtent(address, out width, out height, out byteCount);
|
|
||||||
|
|
||||||
public IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents() =>
|
|
||||||
VulkanVideoPresenter.GetGuestImageExtents();
|
|
||||||
|
|
||||||
public bool IsTextureContentCached(in TextureContentIdentity identity) =>
|
|
||||||
VulkanVideoPresenter.IsTextureContentCached(identity);
|
|
||||||
|
|
||||||
public void AttachGuestMemory(SharpEmu.HLE.ICpuMemory memory) =>
|
|
||||||
VulkanVideoPresenter.AttachGuestMemory(memory);
|
|
||||||
|
|
||||||
public ulong GuestStorageBufferOffsetAlignment =>
|
|
||||||
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment;
|
|
||||||
|
|
||||||
public void CountShaderCompilation() =>
|
|
||||||
VulkanVideoPresenter.CountSpirvCompilation();
|
|
||||||
|
|
||||||
public (long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters() =>
|
|
||||||
VulkanVideoPresenter.ReadAndResetPerfCounters();
|
|
||||||
|
|
||||||
public void RequestClose() =>
|
|
||||||
VulkanVideoPresenter.RequestClose();
|
|
||||||
|
|
||||||
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
||||||
shader is VulkanCompiledGuestShader vulkanShader
|
shader is VulkanCompiledGuestShader vulkanShader
|
||||||
? vulkanShader.Spirv
|
? vulkanShader.Spirv
|
||||||
|
|||||||
@@ -119,17 +119,6 @@ public static class JsonExports
|
|||||||
return SetReturn(ctx, 0);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "WSOuge5IsCg",
|
Nid = "WSOuge5IsCg",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ public static class KernelExports
|
|||||||
ExportName = "open",
|
ExportName = "open",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx);
|
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "1G3lF1Gg1k8",
|
Nid = "1G3lF1Gg1k8",
|
||||||
@@ -376,7 +376,7 @@ public static class KernelExports
|
|||||||
ExportName = "fstat",
|
ExportName = "fstat",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx);
|
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
|
|||||||
@@ -267,11 +267,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hostPath = ResolveGuestPath(guestPath);
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
if (string.IsNullOrEmpty(hostPath))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
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 fromHost = ResolveGuestPath(fromGuest);
|
||||||
var toHost = ResolveGuestPath(toGuest);
|
var toHost = ResolveGuestPath(toGuest);
|
||||||
if (string.IsNullOrEmpty(fromHost) || string.IsNullOrEmpty(toHost))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Directory.Exists(fromHost))
|
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 sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
private long _ownerThreadId;
|
public ulong OwnerThreadId { get; set; }
|
||||||
private int _recursionCount;
|
public int RecursionCount { get; set; }
|
||||||
private int _queuedWaiterCount;
|
|
||||||
|
|
||||||
public Lock SyncRoot { get; } = new();
|
|
||||||
public ulong OwnerThreadId
|
|
||||||
{
|
|
||||||
get => unchecked((ulong)Volatile.Read(ref _ownerThreadId));
|
|
||||||
set => Volatile.Write(ref _ownerThreadId, unchecked((long)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RecursionCount
|
|
||||||
{
|
|
||||||
get => Volatile.Read(ref _recursionCount);
|
|
||||||
set => Volatile.Write(ref _recursionCount, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount);
|
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
||||||
|
|
||||||
public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge)
|
|
||||||
{
|
|
||||||
if (!allowWaiterBarge && QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryAcquireOwner(threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAcquireOwner(ulong threadId)
|
|
||||||
{
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
unchecked((long)threadId),
|
|
||||||
0) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryReleaseUncontended(ulong threadId)
|
|
||||||
{
|
|
||||||
if (QueuedWaiterCount != 0 || RecursionCount != 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 0);
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
0,
|
|
||||||
unchecked((long)threadId)) == unchecked((long)threadId))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount);
|
|
||||||
|
|
||||||
public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount);
|
|
||||||
|
|
||||||
public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount);
|
|
||||||
|
|
||||||
public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
public required string WakeKey { get; init; }
|
public required string WakeKey { get; init; }
|
||||||
public required bool Cooperative { get; set; }
|
public required bool Cooperative { get; init; }
|
||||||
public ManualResetEventSlim? HostSignal { get; set; }
|
|
||||||
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||||
public int Granted;
|
public int Granted;
|
||||||
}
|
}
|
||||||
@@ -163,10 +94,7 @@ public static class KernelPthreadCompatExports
|
|||||||
public static int PthreadSelf(CpuContext ctx)
|
public static int PthreadSelf(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
|
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||||
{
|
|
||||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
|
||||||
}
|
|
||||||
ctx[CpuRegister.Rax] = currentThreadHandle;
|
ctx[CpuRegister.Rax] = currentThreadHandle;
|
||||||
TracePthreadSelf(ctx, currentThreadHandle);
|
TracePthreadSelf(ctx, currentThreadHandle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -211,13 +139,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "B5GmVDKwpn0",
|
|
||||||
ExportName = "pthread_yield",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "GBUY7ywdULE",
|
Nid = "GBUY7ywdULE",
|
||||||
ExportName = "scePthreadRename",
|
ExportName = "scePthreadRename",
|
||||||
@@ -650,30 +571,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadOnce"/>. libKernel exports the
|
|
||||||
/// same routine under two NIDs, and shipped middleware links the plain name:
|
|
||||||
/// DOOM's libcohtml, PlayFab and party modules all import this one rather
|
|
||||||
/// than scePthreadOnce.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Z4QosVuAsA0",
|
|
||||||
ExportName = "pthread_once",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadRename"/>, following the same
|
|
||||||
/// two-NID pattern as <see cref="PthreadOncePOSIX"/>.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "9vyP6Z7bqzc",
|
|
||||||
ExportName = "pthread_rename_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx);
|
|
||||||
|
|
||||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
{
|
{
|
||||||
if (mutexAddress == 0)
|
if (mutexAddress == 0)
|
||||||
@@ -724,7 +621,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -756,63 +653,11 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
if (state.Type == MutexTypeRecursive)
|
|
||||||
{
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
var adaptiveResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult);
|
|
||||||
return adaptiveResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
|
||||||
return ownedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var canCooperativelyBlock = !tryOnly &&
|
var canCooperativelyBlock = !tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||||
PthreadMutexWaiter? waiter = null;
|
PthreadMutexWaiter? waiter = null;
|
||||||
var acquiredWhileQueueing = false;
|
lock (state)
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
@@ -823,30 +668,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gen5 runtime wrappers can layer an adaptive lock call over
|
|
||||||
// scePthreadMutexLock for one logical acquisition, followed by
|
|
||||||
// only one unlock. Keep the duplicate acquisition idempotent so
|
|
||||||
// the matching unlock fully releases the HLE mutex.
|
|
||||||
TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
{
|
||||||
if (tryOnly)
|
if (tryOnly)
|
||||||
{
|
{
|
||||||
@@ -855,7 +677,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
||||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
// over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here
|
||||||
// leaves that guest bookkeeping out of sync with the HLE owner and
|
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||||
// the compatibility recursion used by the original implementation;
|
// the compatibility recursion used by the original implementation;
|
||||||
@@ -874,17 +696,10 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pthread_mutex_trylock succeeds whenever the mutex is not currently
|
if (state.OwnerThreadId == 0 && state.Waiters.Count == 0)
|
||||||
// held; unlike the blocking lock it does not queue behind waiters
|
|
||||||
// (POSIX gives it no fairness obligation). Gating trylock on an empty
|
|
||||||
// wait queue is wrong and, worse, lets a single stale/undrainable
|
|
||||||
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
|
||||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
|
||||||
// blocked waiters are not starved by a barging locker.
|
|
||||||
if (state.OwnerThreadId == 0 &&
|
|
||||||
(tryOnly || state.Waiters.Count == 0) &&
|
|
||||||
state.TryAcquireOwner(currentThreadId))
|
|
||||||
{
|
{
|
||||||
|
state.OwnerThreadId = currentThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -896,14 +711,6 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
||||||
acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acquiredWhileQueueing)
|
|
||||||
{
|
|
||||||
waiter!.HostSignal?.Dispose();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCooperativelyBlock && waiter is not null &&
|
if (canCooperativelyBlock && waiter is not null &&
|
||||||
@@ -937,29 +744,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
string? nextWakeKey = null;
|
||||||
{
|
lock (state)
|
||||||
if (state.RecursionCount > 1)
|
|
||||||
{
|
|
||||||
state.DecrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.TryReleaseUncontended(currentThreadId))
|
|
||||||
{
|
|
||||||
if (state.QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
WakeFirstMutexWaiter(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PthreadMutexWaiter? nextWaiter = null;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
@@ -977,29 +763,16 @@ public static class KernelPthreadCompatExports
|
|||||||
if (state.RecursionCount == 0)
|
if (state.RecursionCount == 0)
|
||||||
{
|
{
|
||||||
state.OwnerThreadId = 0;
|
state.OwnerThreadId = 0;
|
||||||
|
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||||
// Hand the mutex directly to the head waiter instead of only
|
? state.Waiters.First.Value.WakeKey
|
||||||
// waking it and relying on it to re-acquire. A woken waiter that
|
: null;
|
||||||
// fails to self-grant (its wake races or is lost) would leave the
|
Monitor.PulseAll(state);
|
||||||
// mutex "free with a queued waiter"; the fast-acquire path refuses
|
|
||||||
// such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every
|
|
||||||
// later locker — including the game's main thread — then queues
|
|
||||||
// behind a head that never advances and the process wedges.
|
|
||||||
if (state.Waiters.First is { } headNode &&
|
|
||||||
TryGrantMutexWaiterLocked(state, headNode.Value))
|
|
||||||
{
|
|
||||||
nextWaiter = headNode.Value;
|
|
||||||
if (!nextWaiter.Cooperative)
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
if (nextWakeKey is not null)
|
||||||
{
|
{
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
@@ -1459,22 +1232,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
lock (mutexState.SyncRoot)
|
lock (mutexState)
|
||||||
{
|
{
|
||||||
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
|
||||||
{
|
|
||||||
// The guest holds the mutex through a path our host-side tracking
|
|
||||||
// never observed — most commonly libkernel's uncontended userspace
|
|
||||||
// fast-path, which locks the mutex word directly without an HLE
|
|
||||||
// call. Real pthread_cond_wait requires the caller to own the
|
|
||||||
// mutex and does not verify it for normal mutexes, so returning
|
|
||||||
// EPERM here is wrong: it spins the guest and, worse, leaves the
|
|
||||||
// mutex held (the unlock below is skipped), wedging every thread
|
|
||||||
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
|
||||||
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
|
||||||
_ = mutexState.TryAcquireOwner(currentThreadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||||
{
|
{
|
||||||
return mutexState.OwnerThreadId == currentThreadId
|
return mutexState.OwnerThreadId == currentThreadId
|
||||||
@@ -1626,32 +1385,6 @@ public static class KernelPthreadCompatExports
|
|||||||
bool cooperative,
|
bool cooperative,
|
||||||
string? wakeKey = null)
|
string? wakeKey = null)
|
||||||
{
|
{
|
||||||
// A guest thread can have at most one pending acquisition on a mutex —
|
|
||||||
// it is either running or blocked on exactly one wait. If a waiter for
|
|
||||||
// this thread is still queued when it comes back for a fresh
|
|
||||||
// acquisition, that entry is a stale leftover the thread abandoned
|
|
||||||
// (most often a cond_timedwait timeout whose re-acquire hand-off was
|
|
||||||
// lost). Stale entries clog the FIFO head with waiters no thread is
|
|
||||||
// blocked on, so the unlock hand-off wakes a dead wake-key and the
|
|
||||||
// mutex wedges permanently (observed deadlocking Hades: several
|
|
||||||
// re-acquire waiters from one thread piled ahead of a live locker).
|
|
||||||
// Prune any prior entry for this thread before enqueueing the new one.
|
|
||||||
if (threadId != 0)
|
|
||||||
{
|
|
||||||
for (var node = state.Waiters.First; node is not null;)
|
|
||||||
{
|
|
||||||
var next = node.Next;
|
|
||||||
if (node.Value.ThreadId == threadId)
|
|
||||||
{
|
|
||||||
state.Waiters.Remove(node);
|
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
node.Value.Node = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var waiter = new PthreadMutexWaiter
|
var waiter = new PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
ThreadId = threadId,
|
ThreadId = threadId,
|
||||||
@@ -1659,10 +1392,8 @@ public static class KernelPthreadCompatExports
|
|||||||
WakeKey = cooperative
|
WakeKey = cooperative
|
||||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
|
|
||||||
};
|
};
|
||||||
waiter.Node = state.Waiters.AddLast(waiter);
|
waiter.Node = state.Waiters.AddLast(waiter);
|
||||||
state.WaiterAddedLocked();
|
|
||||||
return waiter;
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1403,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var mutex = new PthreadMutexState();
|
var mutex = new PthreadMutexState();
|
||||||
PthreadMutexWaiter first;
|
PthreadMutexWaiter first;
|
||||||
PthreadMutexWaiter second;
|
PthreadMutexWaiter second;
|
||||||
lock (mutex.SyncRoot)
|
lock (mutex)
|
||||||
{
|
{
|
||||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||||
@@ -1716,72 +1447,26 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.TryAcquireOwner(waiter.ThreadId))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Waiters.Remove(waiter.Node);
|
state.Waiters.Remove(waiter.Node);
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
waiter.Node = null;
|
waiter.Node = null;
|
||||||
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
Volatile.Write(ref waiter.Granted, 1);
|
Volatile.Write(ref waiter.Granted, 1);
|
||||||
|
Monitor.PulseAll(state);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WakeFirstMutexWaiter(PthreadMutexState state)
|
|
||||||
{
|
|
||||||
PthreadMutexWaiter? nextWaiter;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
|
||||||
if (state.OwnerThreadId != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWaiter = state.Waiters.First?.Value;
|
|
||||||
if (nextWaiter is { Cooperative: false })
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
|
||||||
{
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
ManualResetEventSlim? hostSignal = null;
|
lock (state)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while (true)
|
while (!TryGrantMutexWaiterLocked(state, waiter))
|
||||||
{
|
{
|
||||||
lock (state.SyncRoot)
|
Monitor.Wait(state);
|
||||||
{
|
|
||||||
if (waiter.HostSignal is null)
|
|
||||||
{
|
|
||||||
waiter.Cooperative = false;
|
|
||||||
waiter.HostSignal = new ManualResetEventSlim(initialState: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal = waiter.HostSignal;
|
|
||||||
if (TryGrantMutexWaiterLocked(state, waiter))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
hostSignal?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGrantBlockedMutexLock(
|
private static bool TryGrantBlockedMutexLock(
|
||||||
@@ -1792,7 +1477,7 @@ public static class KernelPthreadCompatExports
|
|||||||
PthreadMutexWaiter waiter)
|
PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
var granted = false;
|
var granted = false;
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||||
}
|
}
|
||||||
@@ -1826,10 +1511,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
|
|
||||||
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
|
|
||||||
guestOwner == currentThreadId;
|
|
||||||
|
|
||||||
private static bool CompleteCondWaiterLocked(
|
private static bool CompleteCondWaiterLocked(
|
||||||
PthreadCondState state,
|
PthreadCondState state,
|
||||||
PthreadCondWaiter waiter,
|
PthreadCondWaiter waiter,
|
||||||
@@ -1845,7 +1526,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waiter.TimeoutTimer?.Dispose();
|
waiter.TimeoutTimer?.Dispose();
|
||||||
waiter.TimeoutTimer = null;
|
waiter.TimeoutTimer = null;
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||||
waiter.MutexState,
|
waiter.MutexState,
|
||||||
@@ -1893,7 +1574,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -860,18 +860,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
|
|
||||||
/// exports the same routine under two NIDs; middleware compiled against the
|
|
||||||
/// plain POSIX headers links this one rather than scePthreadAttrGetschedparam.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "qlk9pSLsUmM",
|
|
||||||
ExportName = "pthread_attr_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "FXPWHNk8Of0",
|
Nid = "FXPWHNk8Of0",
|
||||||
ExportName = "scePthreadAttrGetschedparam",
|
ExportName = "scePthreadAttrGetschedparam",
|
||||||
@@ -1145,90 +1133,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "SFxTMOfuCkE",
|
|
||||||
ExportName = "pthread_rwlock_tryrdlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "XhWHn6P5R7U",
|
|
||||||
ExportName = "pthread_rwlock_trywrlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
|
|
||||||
/// only if the lock is free right now, otherwise reports BUSY.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists
|
|
||||||
/// for the scheduler resume path and decrements WaitingWriters on success,
|
|
||||||
/// which is correct only for a thread that previously incremented it. A fresh
|
|
||||||
/// try never did, so reusing it would silently consume another thread's
|
|
||||||
/// waiter count and let a queued writer be skipped.
|
|
||||||
/// </remarks>
|
|
||||||
private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
|
||||||
{
|
|
||||||
if (rwlockAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
|
||||||
lock (rwlock.SyncRoot)
|
|
||||||
{
|
|
||||||
if (write)
|
|
||||||
{
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors the blocking path's re-entrant compat-writer grant so the
|
|
||||||
// two agree on what counts as already owning the lock.
|
|
||||||
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
rwlock.AddCompatWriter(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId != 0 ||
|
|
||||||
rwlock.ReaderTotalCount != 0 ||
|
|
||||||
rwlock.CompatWriterTotalCount != 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock");
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
rwlock.AddReader(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "+L98PIbGttk",
|
Nid = "+L98PIbGttk",
|
||||||
ExportName = "scePthreadRwlockUnlock",
|
ExportName = "scePthreadRwlockUnlock",
|
||||||
@@ -1915,94 +1819,4 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX-named aliases. libKernel exports each of these routines under two
|
|
||||||
// NIDs -- a scePthread* name and the plain POSIX name -- and middleware
|
|
||||||
// compiled against POSIX headers links the latter. Both take identical
|
|
||||||
// arguments and, per the convention already used by scePthreadOnce's alias,
|
|
||||||
// return the same OrbisGen2Result rather than translating to errno.
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "a2P9wYGeZvc",
|
|
||||||
ExportName = "pthread_setprio",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "FIs3-UQT9sg",
|
|
||||||
ExportName = "pthread_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "vQm4fDEsWi8",
|
|
||||||
ExportName = "pthread_attr_getstack",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Ucsu-OK+els",
|
|
||||||
ExportName = "pthread_attr_get_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JarMIy8kKEY",
|
|
||||||
ExportName = "pthread_attr_setschedpolicy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "E+tyo3lp5Lw",
|
|
||||||
ExportName = "pthread_attr_setdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "euKRgm0Vn2M",
|
|
||||||
ExportName = "pthread_attr_setschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7ZlAakEf0Qg",
|
|
||||||
ExportName = "pthread_attr_setinheritsched",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "0qOtCR-ZHck",
|
|
||||||
ExportName = "pthread_attr_getstacksize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "VUT1ZSrHT0I",
|
|
||||||
ExportName = "pthread_attr_getdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JKyG3SWyA10",
|
|
||||||
ExportName = "pthread_attr_setguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JNkVVsVDmOk",
|
|
||||||
ExportName = "pthread_attr_getguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2058,13 +2058,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "NhpspxdjEKU",
|
|
||||||
ExportName = "_nanosleep",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yS8U2TGCe1A",
|
Nid = "yS8U2TGCe1A",
|
||||||
ExportName = "nanosleep",
|
ExportName = "nanosleep",
|
||||||
|
|||||||
@@ -191,27 +191,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
WakePredicate,
|
WakePredicate,
|
||||||
deadline))
|
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)
|
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)}");
|
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);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "GEnUkDZoUwY",
|
|
||||||
ExportName = "scePthreadSemInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores
|
|
||||||
if (ctx[CpuRegister.Rsi] != 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PosixSemInit(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YCV5dGGBcCo",
|
Nid = "YCV5dGGBcCo",
|
||||||
ExportName = "sem_wait",
|
ExportName = "sem_wait",
|
||||||
@@ -483,13 +446,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelWaitSema(ctx);
|
return KernelWaitSema(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "C36iRE0F5sE",
|
|
||||||
ExportName = "scePthreadSemWait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WBWzsRifCEA",
|
Nid = "WBWzsRifCEA",
|
||||||
ExportName = "sem_trywait",
|
ExportName = "sem_trywait",
|
||||||
@@ -507,19 +463,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelPollSema(ctx, handle, 1);
|
return KernelPollSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "H2a+IN9TP0E",
|
|
||||||
ExportName = "scePthreadSemTrywait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemTryWait(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var result = PosixSemTryWait(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN)
|
|
||||||
: result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "w5IHyvahg-o",
|
Nid = "w5IHyvahg-o",
|
||||||
ExportName = "sem_timedwait",
|
ExportName = "sem_timedwait",
|
||||||
@@ -556,13 +499,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelSignalSema(ctx, handle, 1);
|
return KernelSignalSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "aishVAiFaYM",
|
|
||||||
ExportName = "scePthreadSemPost",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Bq+LRV-N6Hk",
|
Nid = "Bq+LRV-N6Hk",
|
||||||
ExportName = "sem_getvalue",
|
ExportName = "sem_getvalue",
|
||||||
@@ -613,13 +549,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Vwc+L05e6oE",
|
|
||||||
ExportName = "scePthreadSemDestroy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx);
|
|
||||||
|
|
||||||
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||||
{
|
{
|
||||||
handle = 0;
|
handle = 0;
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
|
||||||
|
|
||||||
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
|
|
||||||
// futex-style wait/wake: a thread parks on a guest address until another thread
|
|
||||||
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
|
|
||||||
// build their own spinlocks/queues on top of it and call the wait in a hot
|
|
||||||
// loop; left unimplemented, every wait returns immediately and the runtime
|
|
||||||
// busy-spins forever (millions of calls, no forward progress).
|
|
||||||
//
|
|
||||||
// This implements wait/wake over the existing cooperative-block scheduler,
|
|
||||||
// keyed on the address. The real primitive takes a compare value so the wait
|
|
||||||
// only sleeps while the address still holds the expected value; that exact
|
|
||||||
// value is not recovered here, so each wait is given a bounded deadline and
|
|
||||||
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
|
||||||
// self-heals when the deadline expires and the guest re-checks its own
|
|
||||||
// condition, which futex callers already tolerate. A matching wake releases
|
|
||||||
// waiters immediately through the same key.
|
|
||||||
public static class KernelSyncOnAddressCompatExports
|
|
||||||
{
|
|
||||||
// Safety-net poll interval. Real releases come from the wake side (generation
|
|
||||||
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
|
||||||
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
|
||||||
// large: a short interval turns every parked waiter into a hot re-poll that
|
|
||||||
// steals scheduler bandwidth from the threads that actually make progress
|
|
||||||
// (including the ones that would issue the wake), so it must be a rare last
|
|
||||||
// resort, not a spin substitute.
|
|
||||||
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
|
||||||
|
|
||||||
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
|
||||||
// which cannot use the guest-thread scheduler's block mechanism.
|
|
||||||
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
|
||||||
|
|
||||||
// Per-address wake generation. A wait captures the current generation and
|
|
||||||
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
|
||||||
// wake bumps it. This is what actually holds the thread blocked: a bare
|
|
||||||
// "always satisfied" predicate is treated as an immediate late-arrival by
|
|
||||||
// the dispatcher's race guard and never yields, leaving the guest to
|
|
||||||
// busy-spin. The generation also closes the register-vs-park race for free:
|
|
||||||
// a wake landing in that window bumps the generation, so the predicate is
|
|
||||||
// already satisfied and the guest correctly resumes at once.
|
|
||||||
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
|
||||||
|
|
||||||
private static long CurrentGeneration(ulong address) =>
|
|
||||||
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
|
||||||
|
|
||||||
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Hc4CaR6JBL0",
|
|
||||||
ExportName = "sceKernelSyncOnAddressWait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int SyncOnAddressWait(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var address = ctx[CpuRegister.Rdi];
|
|
||||||
if (address == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var observedGeneration = CurrentGeneration(address);
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
|
||||||
|
|
||||||
// Cooperative path: stay parked until a wake bumps this address's
|
|
||||||
// generation (or the deadline expires as a self-heal). The guest
|
|
||||||
// re-evaluates its own condition after resuming.
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelSyncOnAddressWait",
|
|
||||||
WakeKey(address),
|
|
||||||
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
||||||
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
|
||||||
deadline))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-cooperative caller (host main thread): bounded host wait so a
|
|
||||||
// missed wake self-heals instead of hanging.
|
|
||||||
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (CurrentGeneration(address) == observedGeneration)
|
|
||||||
{
|
|
||||||
Monitor.Wait(gate, WaitSelfHealTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "q2y-wDIVWZA",
|
|
||||||
ExportName = "sceKernelSyncOnAddressWake",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int SyncOnAddressWake(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var address = ctx[CpuRegister.Rdi];
|
|
||||||
if (address == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsi carries the number of waiters to release (1 = wake-one, a large
|
|
||||||
// value = wake-all); default to all if it looks unset.
|
|
||||||
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
|
||||||
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
|
||||||
|
|
||||||
// Bump the generation first so a wait that has registered but not yet
|
|
||||||
// parked sees the change and resumes instead of missing this wake.
|
|
||||||
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
|
||||||
|
|
||||||
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
|
||||||
|
|
||||||
if (_hostAddressGates.TryGetValue(address, out var gate))
|
|
||||||
{
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
Monitor.PulseAll(gate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
var value = (int)result;
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user