mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eb021c7d1 | |||
| 4ac29f340a | |||
| fa2616d224 | |||
| 7b86a91dfa | |||
| 72645cb373 | |||
| 2ad9836d13 |
@@ -34,29 +34,31 @@ jobs:
|
|||||||
name: Init
|
name: Init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
archive-name: ${{ steps.vars.outputs.archive-name }}
|
|
||||||
artifact-name: ${{ steps.vars.outputs.artifact-name }}
|
|
||||||
release-name: ${{ steps.vars.outputs.release-name }}
|
release-name: ${{ steps.vars.outputs.release-name }}
|
||||||
release-tag: ${{ steps.vars.outputs.release-tag }}
|
release-tag: ${{ steps.vars.outputs.release-tag }}
|
||||||
|
safe-ref: ${{ steps.vars.outputs.safe-ref }}
|
||||||
short-sha: ${{ steps.vars.outputs.short-sha }}
|
short-sha: ${{ steps.vars.outputs.short-sha }}
|
||||||
|
version: ${{ steps.vars.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Compute workflow variables
|
- name: Compute workflow variables
|
||||||
id: vars
|
id: vars
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
short_sha="${GITHUB_SHA::7}"
|
short_sha="${GITHUB_SHA::7}"
|
||||||
safe_ref="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g')"
|
safe_ref="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g')"
|
||||||
archive_name="sharpemu-win64-${short_sha}.zip"
|
version="$(python3 -c 'import sys, xml.etree.ElementTree as ET; version = ET.parse("Directory.Build.props").getroot().findtext(".//SharpEmuVersion"); sys.exit("Directory.Build.props is missing SharpEmuVersion") if not version or not version.strip() else print(version.strip())')"
|
||||||
artifact_name="sharpemu-win64-${short_sha}"
|
release_tag="v${version}"
|
||||||
release_tag="win64-${safe_ref}-${short_sha}"
|
release_name="SharpEmu v${version}"
|
||||||
release_name="SharpEmu win64 ${short_sha}"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "short-sha=${short_sha}"
|
echo "short-sha=${short_sha}"
|
||||||
echo "archive-name=${archive_name}"
|
echo "safe-ref=${safe_ref}"
|
||||||
echo "artifact-name=${artifact_name}"
|
|
||||||
echo "release-tag=${release_tag}"
|
echo "release-tag=${release_tag}"
|
||||||
echo "release-name=${release_name}"
|
echo "release-name=${release_name}"
|
||||||
|
echo "version=${version}"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
reuse:
|
reuse:
|
||||||
@@ -110,7 +112,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|
||||||
$archivePath = Join-Path $env:RELEASE_DIR "${{ needs.init.outputs.archive-name }}"
|
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip"
|
||||||
|
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||||
if (Test-Path $archivePath) {
|
if (Test-Path $archivePath) {
|
||||||
Remove-Item $archivePath -Force
|
Remove-Item $archivePath -Force
|
||||||
}
|
}
|
||||||
@@ -120,8 +123,67 @@ jobs:
|
|||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.init.outputs.artifact-name }}
|
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
|
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-posix:
|
||||||
|
name: Build ${{ matrix.rid }}
|
||||||
|
needs:
|
||||||
|
- init
|
||||||
|
- reuse
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
rid: linux-x64
|
||||||
|
- os: macos-latest
|
||||||
|
rid: osx-x64
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
|
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup .NET SDK
|
||||||
|
uses: actions/setup-dotnet@v5
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.103
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: |
|
||||||
|
Directory.Packages.props
|
||||||
|
src/**/packages.lock.json
|
||||||
|
|
||||||
|
- name: Restore solution
|
||||||
|
run: dotnet restore SharpEmu.slnx --locked-mode
|
||||||
|
|
||||||
|
- name: Build solution
|
||||||
|
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||||
|
|
||||||
|
- name: Publish ${{ matrix.rid }} CLI
|
||||||
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR"
|
||||||
|
|
||||||
|
- name: Stage MoltenVK next to the build
|
||||||
|
if: matrix.rid == 'osx-x64'
|
||||||
|
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
# tar keeps the executable bit, which zip would drop.
|
||||||
|
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz" \
|
||||||
|
-C "$PUBLISH_DIR" .
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||||
|
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -129,34 +191,39 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- init
|
- init
|
||||||
- build
|
- build
|
||||||
|
- build-posix
|
||||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.init.outputs.artifact-name }}
|
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Create or update release
|
- name: Create or update release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
ARCHIVE_NAME: ${{ needs.init.outputs.archive-name }}
|
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
asset_path="release/${ARCHIVE_NAME}"
|
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
notes="Automated Windows build for commit ${GITHUB_SHA}."
|
if [ "${#assets[@]}" -eq 0 ]; then
|
||||||
|
echo "No release assets found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
||||||
|
|
||||||
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||||
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
|
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
||||||
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
||||||
else
|
else
|
||||||
gh release create "${RELEASE_TAG}" "${asset_path}" \
|
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||||
--title "${RELEASE_NAME}" \
|
--title "${RELEASE_NAME}" \
|
||||||
--notes "${notes}" \
|
--notes "${notes}" \
|
||||||
--target "${GITHUB_SHA}"
|
--target "${GITHUB_SHA}"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
|
<SharpEmuVersion>0.0.1</SharpEmuVersion>
|
||||||
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageVersion Include="Silk.NET.Input" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!WARNING]
|
> [!NOTE]
|
||||||
> Currently the primary development target is Windows.
|
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||||
|
> can run the macOS x64 build through Rosetta 2.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility.
|
> SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility.
|
||||||
@@ -59,14 +60,33 @@ Current capabilities include:
|
|||||||
|
|
||||||
Some games have reached like `sceVideoOut` and AGC stages.
|
Some games have reached like `sceVideoOut` and AGC stages.
|
||||||
|
|
||||||
Currently the project primarily targets Windows. Cross-platform support (Linux and macOS) is planned, but development is currently focused on Windows to simplify early-stage debugging and iteration.
|
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
|
||||||
|
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
|
||||||
|
so compatibility and performance vary by game, operating system, and GPU driver.
|
||||||
|
|
||||||
## Using
|
## Using
|
||||||
|
|
||||||
* Build or Publish project or download in release tab.
|
Download the release archive for your operating system, extract it, and launch
|
||||||
* Open Powershell.
|
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||||
* Run Emulator GUI.
|
|
||||||
* Or command: `.\SharpEmu "eboot.bin" 2>&1 | Tee-Object -FilePath "log.txt"`
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
|
||||||
|
Tee-Object -FilePath "SharpEmu.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux and macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ./SharpEmu
|
||||||
|
|
||||||
|
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
|
||||||
|
tee SharpEmu.log
|
||||||
|
```
|
||||||
|
|
||||||
|
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||||
|
release includes the MoltenVK Vulkan implementation.
|
||||||
|
|
||||||
## Games Tested
|
## Games Tested
|
||||||
|
|
||||||
@@ -94,7 +114,7 @@ Currently the project primarily targets Windows. Cross-platform support (Linux a
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
1. Install the **.NET SDK**.
|
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||||
2. Clone the repository: `git clone https://github.com/par274/sharpemu.git`
|
2. Clone the repository: `git clone https://github.com/par274/sharpemu.git`
|
||||||
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
||||||
4. Build the project: `dotnet build` or `dotnet publish`
|
4. Build the project: `dotnet build` or `dotnet publish`
|
||||||
|
|||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Downloads the official (universal x86_64+arm64) MoltenVK dylib and stages
|
||||||
|
# it next to a SharpEmu build as libvulkan.1.dylib. The macOS build runs as
|
||||||
|
# an x86-64 process under Rosetta 2, so Homebrew's arm64-only Vulkan
|
||||||
|
# libraries cannot be used; the presenter looks for this app-local copy.
|
||||||
|
#
|
||||||
|
# Usage: scripts/fetch-macos-moltenvk.sh [output-dir]
|
||||||
|
# (default output: artifacts/bin/Debug/net10.0/osx-x64)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MVK_VERSION="${MVK_VERSION:-v1.4.0}"
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT_DIR="${1:-$REPO_ROOT/artifacts/bin/Debug/net10.0/osx-x64}"
|
||||||
|
|
||||||
|
if [[ ! -d "$OUT_DIR" ]]; then
|
||||||
|
echo "output directory does not exist: $OUT_DIR (build first?)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
echo ">> Downloading MoltenVK $MVK_VERSION..."
|
||||||
|
curl -sL -o "$WORK_DIR/mvk.tar" \
|
||||||
|
"https://github.com/KhronosGroup/MoltenVK/releases/download/$MVK_VERSION/MoltenVK-macos.tar"
|
||||||
|
tar -xf "$WORK_DIR/mvk.tar" -C "$WORK_DIR" \
|
||||||
|
MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib
|
||||||
|
|
||||||
|
DYLIB="$WORK_DIR/MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib"
|
||||||
|
file "$DYLIB" | grep -q x86_64 || { echo "downloaded dylib lacks x86_64 slice" >&2; exit 3; }
|
||||||
|
|
||||||
|
cp "$DYLIB" "$OUT_DIR/libMoltenVK.dylib"
|
||||||
|
cp "$DYLIB" "$OUT_DIR/libvulkan.1.dylib"
|
||||||
|
echo ">> Staged libMoltenVK.dylib + libvulkan.1.dylib in $OUT_DIR"
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Smoke-tests the linux-x64 build inside an amd64 container. Useful from any
|
||||||
|
# host (including Apple Silicon, where Docker runs the amd64 image under
|
||||||
|
# emulation) to confirm the cross-platform layer keeps working on Linux.
|
||||||
|
#
|
||||||
|
# Usage: scripts/test-linux-docker.sh /path/to/eboot.bin
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GAME_PATH="${1:-}"
|
||||||
|
if [[ -z "$GAME_PATH" || ! -f "$GAME_PATH" ]]; then
|
||||||
|
echo "usage: $0 <path-to-eboot.bin>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
GAME_DIR="$(cd "$(dirname "$GAME_PATH")" && pwd)"
|
||||||
|
GAME_FILE="$(basename "$GAME_PATH")"
|
||||||
|
PUBLISH_DIR="$REPO_ROOT/artifacts/publish/SharpEmu.CLI/Debug/net10.0/linux-x64"
|
||||||
|
|
||||||
|
echo ">> Publishing linux-x64 self-contained build..."
|
||||||
|
dotnet publish "$REPO_ROOT/src/SharpEmu.CLI" \
|
||||||
|
-c Debug -r linux-x64 --self-contained -p:PublishSingleFile=false
|
||||||
|
|
||||||
|
echo ">> Running inside linux/amd64 container..."
|
||||||
|
docker run --rm --platform linux/amd64 \
|
||||||
|
-v "$PUBLISH_DIR":/app:ro \
|
||||||
|
-v "$GAME_DIR":/game:ro \
|
||||||
|
mcr.microsoft.com/dotnet/runtime-deps:10.0 \
|
||||||
|
/app/SharpEmu --log-level=info "/game/$GAME_FILE"
|
||||||
@@ -75,6 +75,121 @@ internal static partial class Program
|
|||||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CheckHostArchitecture())
|
||||||
|
{
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
PreloadMacVulkanLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLFW requires window creation and event processing on the
|
||||||
|
// process main thread: AppKit demands it on macOS, and X11 has a
|
||||||
|
// single event queue that must be serviced from the main thread
|
||||||
|
// (a window created and polled off it may never map, which showed
|
||||||
|
// as a running game with no visible window on Linux). Emulation
|
||||||
|
// moves to a worker thread and the main thread services the window
|
||||||
|
// work the video presenter posts. Windows keeps a per-thread event
|
||||||
|
// queue, so its window stays on the presenter's own thread.
|
||||||
|
var exitCode = 0;
|
||||||
|
HostMainThread.Enable();
|
||||||
|
var emulation = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
exitCode = RunEmulator(args, isMitigatedChild);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
HostMainThread.Shutdown();
|
||||||
|
}
|
||||||
|
}, 32 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
Name = "SharpEmu Emulation",
|
||||||
|
};
|
||||||
|
emulation.Start();
|
||||||
|
HostMainThread.Pump();
|
||||||
|
emulation.Join();
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunEmulator(args, isMitigatedChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The supported host execution model, checked before any emulation
|
||||||
|
/// starts: the CPU backend executes guest x86-64 code natively, so the
|
||||||
|
/// host process must be x86-64 — win-x64/linux-x64 on x64 hardware, or
|
||||||
|
/// osx-x64 under Rosetta 2 on Apple Silicon (Rosetta translates the
|
||||||
|
/// whole process, so it still reports as X64 here). An arm64 process
|
||||||
|
/// (e.g. the osx-arm64 build) can browse the GUI but cannot run games;
|
||||||
|
/// failing up front distinguishes that from MoltenVK, signal-handler,
|
||||||
|
/// or guest-memory startup problems.
|
||||||
|
/// </summary>
|
||||||
|
private static bool CheckHostArchitecture()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] Unsupported process architecture " +
|
||||||
|
$"{RuntimeInformation.ProcessArchitecture}: guest code executes " +
|
||||||
|
"natively, so SharpEmu must run as an x86-64 process.");
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][ERROR] On Apple Silicon, use the osx-x64 build under " +
|
||||||
|
"Rosetta 2 (install with: softwareupdate --install-rosetta).");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
||||||
|
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
||||||
|
/// x86-64 (Rosetta 2) process, so a universal libMoltenVK.dylib placed
|
||||||
|
/// next to the executable (named libvulkan.1.dylib) is preloaded here;
|
||||||
|
/// dyld then resolves GLFW's bare-name dlopen to the loaded image.
|
||||||
|
/// </summary>
|
||||||
|
private static void PreloadMacVulkanLoader()
|
||||||
|
{
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "libvulkan.1.dylib"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "libMoltenVK.dylib"),
|
||||||
|
Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".sharpemu", "x64lib", "libvulkan.1.dylib"),
|
||||||
|
};
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out _))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Vulkan loader preloaded: {candidate}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeLibrary.TryLoad("libvulkan.1.dylib", out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] No x86-64 Vulkan loader found; video output will be unavailable. " +
|
||||||
|
"Place a universal libMoltenVK.dylib (from the MoltenVK releases) next to SharpEmu " +
|
||||||
|
"as libvulkan.1.dylib.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||||
|
|||||||
@@ -16,19 +16,30 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<AssemblyName>SharpEmu</AssemblyName>
|
<AssemblyName>SharpEmu</AssemblyName>
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||||
|
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||||
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<Version>0.0.1</Version>
|
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Background GC's write-watch revisit calls FlushProcessWriteBuffers,
|
||||||
|
which on macOS uses thread_get_register_pointer_values; under Rosetta 2
|
||||||
|
that Mach call can stall indefinitely on threads executing translated
|
||||||
|
guest code, wedging the whole runtime (every allocating thread then
|
||||||
|
blocks behind the never-finishing GC). Non-concurrent GC never takes
|
||||||
|
that path. Windows and Linux keep concurrent GC. -->
|
||||||
|
<PropertyGroup Condition="$([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('osx'))">
|
||||||
|
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
@@ -63,7 +74,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<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'))" />
|
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>
|
||||||
|
|||||||
@@ -135,6 +135,23 @@
|
|||||||
"Ultz.Native.GLFW": "3.4.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": {
|
"Silk.NET.Maths": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.23.0",
|
"resolved": "2.23.0",
|
||||||
@@ -225,6 +242,7 @@
|
|||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Input": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
@@ -282,6 +300,16 @@
|
|||||||
"resolved": "1.21.0",
|
"resolved": "1.21.0",
|
||||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
"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": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
@@ -434,6 +462,59 @@
|
|||||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
"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": {
|
"net10.0/win-x64": {
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
|||||||
@@ -22,15 +22,22 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
ModuleInitializer,
|
ModuleInitializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
private const ulong StackBaseAddress = 0x7FFF_F000_0000UL;
|
// The top of the x86-64 user address space (0x7FFD..0x7FFF) is only
|
||||||
|
// freely mappable on Windows; on macOS/Linux it hosts the dyld shared
|
||||||
|
// cache / vdso and (under Rosetta 2) the translator runtime, so POSIX
|
||||||
|
// hosts use the equivalent layout one slot lower at 0x6FFx.
|
||||||
|
private static readonly ulong StackBaseAddress = OperatingSystem.IsWindows() ? 0x7FFF_F000_0000UL : 0x6FFF_F000_0000UL;
|
||||||
private const ulong StackSize = 0x0020_0000UL;
|
private const ulong StackSize = 0x0020_0000UL;
|
||||||
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
|
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||||
private const ulong TlsSize = 0x0001_0000UL;
|
private const ulong TlsSize = 0x0001_0000UL;
|
||||||
private const ulong TlsPrefixSize = 0x0000_1000UL;
|
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
||||||
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
// amd64 variant II); libc.prx alone reaches beyond -0x1700, so give the
|
||||||
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
|
// prefix a full 64KB on POSIX. Windows keeps its historical 4KB prefix.
|
||||||
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
|
private static readonly ulong TlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
||||||
private const ulong ReturnToHostStubBaseAddress = 0x7FFD_C000_0000UL;
|
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
||||||
|
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
||||||
|
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
||||||
|
private static readonly ulong ReturnToHostStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_C000_0000UL : 0x6FFD_C000_0000UL;
|
||||||
private const ulong BootstrapRegionSize = 0x0000_1000UL;
|
private const ulong BootstrapRegionSize = 0x0000_1000UL;
|
||||||
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
|
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
|
||||||
private const ulong BootstrapPayloadResultOffset = 0x28UL;
|
private const ulong BootstrapPayloadResultOffset = 0x28UL;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
SetupPosixExceptionHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
@@ -207,19 +213,16 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||||
for (int i = 0; i < 16; i++)
|
if (!TryReadHostQword(stackAddr, out ulong value))
|
||||||
{
|
{
|
||||||
ulong stackAddr = rsp + (ulong)(i * 8);
|
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||||
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
|
break;
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
|
||||||
}
|
}
|
||||||
}
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||||
catch
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -232,8 +235,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
|
if (!TryReadHostQword(frame, out ulong next) || !TryReadHostQword(frame + 8, out ulong ret))
|
||||||
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
string extra = TryFormatNearestRuntimeSymbol(ret, out string retSym) ? $" [{retSym}]" : string.Empty;
|
string extra = TryFormatNearestRuntimeSymbol(ret, out string retSym) ? $" [{retSym}]" : string.Empty;
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] frame#{i}: rbp=0x{frame:X16} ret=0x{ret:X16}{extra} next=0x{next:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] frame#{i}: rbp=0x{frame:X16} ret=0x{ret:X16}{extra} next=0x{next:X16}");
|
||||||
if (next <= frame)
|
if (next <= frame)
|
||||||
@@ -256,10 +262,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
||||||
try
|
byte[] code = new byte[16];
|
||||||
|
if (TryReadHostBytes(rip, code))
|
||||||
{
|
{
|
||||||
byte[] code = new byte[16];
|
|
||||||
Marshal.Copy((nint)rip, code, 0, code.Length);
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
||||||
if (code[0] == 100)
|
if (code[0] == 100)
|
||||||
{
|
{
|
||||||
@@ -275,20 +280,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
|
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
||||||
}
|
}
|
||||||
if (rip > 16)
|
byte[] before = new byte[16];
|
||||||
|
if (rip > 16 && TryReadHostBytes(rip - 16, before))
|
||||||
{
|
{
|
||||||
byte[] before = new byte[16];
|
|
||||||
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
||||||
}
|
}
|
||||||
if (rip > 32)
|
byte[] window = new byte[64];
|
||||||
|
if (rip > 32 && TryReadHostBytes(rip - 32, window))
|
||||||
{
|
{
|
||||||
byte[] window = new byte[64];
|
|
||||||
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||||
}
|
}
|
||||||
@@ -823,6 +826,61 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadHostQword(ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// A stray read inside the signal handler would raise a nested
|
||||||
|
// SIGSEGV and kill the process before diagnostics finish, so
|
||||||
|
// probe the region table instead of relying on try/catch.
|
||||||
|
return TryReadStackU64(address, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = (ulong)Marshal.ReadInt64((nint)address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||||
|
{
|
||||||
|
if (address < 65536)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// See TryReadHostQword: probe every touched page before reading.
|
||||||
|
ulong end = address + (ulong)buffer.Length;
|
||||||
|
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
||||||
|
{
|
||||||
|
if (!_hostMemory.Query(page, out var mbi) ||
|
||||||
|
mbi.State != HostRegionState.Committed ||
|
||||||
|
!IsReadableProtection(mbi.RawProtection))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string FormatPointerWithNearestSymbol(ulong value)
|
private string FormatPointerWithNearestSymbol(ulong value)
|
||||||
{
|
{
|
||||||
string text = $"0x{value:X16}";
|
string text = $"0x{value:X16}";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -172,8 +173,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private static nint _exitThreadAddress;
|
private static nint _exitThreadAddress;
|
||||||
|
|
||||||
private readonly DirectExecutionBackend _backend;
|
private readonly DirectExecutionBackend _backend;
|
||||||
private readonly AutoResetEvent _workAvailable = new(false);
|
// Windows uses AutoResetEvent (its SafeWaitHandle is a real kernel
|
||||||
private readonly AutoResetEvent _workCompleted = new(false);
|
// event the emitted loop can wait on); POSIX uses worker-event
|
||||||
|
// semaphores shared the same way via PosixHostStubs.
|
||||||
|
private readonly AutoResetEvent? _workAvailable;
|
||||||
|
private readonly AutoResetEvent? _workCompleted;
|
||||||
|
private nint _workSemaphore;
|
||||||
|
private nint _doneSemaphore;
|
||||||
|
|
||||||
|
// RunPrologue/RunEpilogue compile to the host ABI (SysV on POSIX); the
|
||||||
|
// emitted loop calls them with Win64 registers, so POSIX routes the
|
||||||
|
// calls through register-shuffling thunks (shared by all workers).
|
||||||
|
private static nint _posixPrologueThunk;
|
||||||
|
private static nint _posixEpilogueThunk;
|
||||||
|
private static readonly object PosixThunkGate = new();
|
||||||
private GCHandle _selfHandle;
|
private GCHandle _selfHandle;
|
||||||
private void* _controlBlock;
|
private void* _controlBlock;
|
||||||
private void* _loopStub;
|
private void* _loopStub;
|
||||||
@@ -212,6 +225,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private NativeGuestExecutor(DirectExecutionBackend backend)
|
private NativeGuestExecutor(DirectExecutionBackend backend)
|
||||||
{
|
{
|
||||||
_backend = backend;
|
_backend = backend;
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
_workAvailable = new AutoResetEvent(false);
|
||||||
|
_workCompleted = new AutoResetEvent(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||||
@@ -258,8 +276,34 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||||
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
nint workHandle;
|
||||||
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
nint doneHandle;
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
workHandle = _workAvailable!.SafeWaitHandle.DangerousGetHandle();
|
||||||
|
doneHandle = _workCompleted!.SafeWaitHandle.DangerousGetHandle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lock (PosixThunkGate)
|
||||||
|
{
|
||||||
|
if (_posixPrologueThunk == 0)
|
||||||
|
{
|
||||||
|
_posixPrologueThunk = PosixHostStubs.CreateWin64ToSysVThunk(prologuePtr);
|
||||||
|
_posixEpilogueThunk = PosixHostStubs.CreateWin64ToSysVThunk(epiloguePtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prologuePtr = _posixPrologueThunk;
|
||||||
|
epiloguePtr = _posixEpilogueThunk;
|
||||||
|
_workSemaphore = PosixHostStubs.CreateWorkerEvent();
|
||||||
|
_doneSemaphore = PosixHostStubs.CreateWorkerEvent();
|
||||||
|
if (_workSemaphore == 0 || _doneSemaphore == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
workHandle = _workSemaphore;
|
||||||
|
doneHandle = _doneSemaphore;
|
||||||
|
}
|
||||||
|
|
||||||
byte* code = (byte*)_loopStub;
|
byte* code = (byte*)_loopStub;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
@@ -377,8 +421,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_runYieldRequested = false;
|
_runYieldRequested = false;
|
||||||
_runYieldReason = null;
|
_runYieldReason = null;
|
||||||
_runForcedExit = false;
|
_runForcedExit = false;
|
||||||
_workAvailable.Set();
|
SignalWorkAvailable();
|
||||||
_workCompleted.WaitOne();
|
WaitWorkCompleted();
|
||||||
_runContext = null;
|
_runContext = null;
|
||||||
_runState = null;
|
_runState = null;
|
||||||
yieldRequested = _runYieldRequested;
|
yieldRequested = _runYieldRequested;
|
||||||
@@ -391,6 +435,28 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return _runNativeResult;
|
return _runNativeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SignalWorkAvailable()
|
||||||
|
{
|
||||||
|
if (_workAvailable is not null)
|
||||||
|
{
|
||||||
|
_workAvailable.Set();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = PosixHostStubs.SignalWorkerEvent(_workSemaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WaitWorkCompleted()
|
||||||
|
{
|
||||||
|
if (_workCompleted is not null)
|
||||||
|
{
|
||||||
|
_workCompleted.WaitOne();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = PosixHostStubs.WaitWorkerEvent(_doneSemaphore, -1);
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
[UnmanagedCallersOnly]
|
||||||
private static nint RunPrologue(nint executorHandle)
|
private static nint RunPrologue(nint executorHandle)
|
||||||
{
|
{
|
||||||
@@ -520,7 +586,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_workAvailable.Set();
|
SignalWorkAvailable();
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
@@ -555,8 +621,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
_selfHandle.Free();
|
_selfHandle.Free();
|
||||||
}
|
}
|
||||||
_workAvailable.Dispose();
|
_workAvailable?.Dispose();
|
||||||
_workCompleted.Dispose();
|
_workCompleted?.Dispose();
|
||||||
|
if (_workSemaphore != 0)
|
||||||
|
{
|
||||||
|
PosixHostStubs.DestroyWorkerEvent(_workSemaphore);
|
||||||
|
_workSemaphore = 0;
|
||||||
|
}
|
||||||
|
if (_doneSemaphore != 0)
|
||||||
|
{
|
||||||
|
PosixHostStubs.DestroyWorkerEvent(_doneSemaphore);
|
||||||
|
_doneSemaphore = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed unsafe partial class DirectExecutionBackend
|
||||||
|
{
|
||||||
|
// POSIX bridge for the Windows vectored-exception-handler logic. A
|
||||||
|
// sigaction(SIGSEGV/SIGBUS/SIGILL) handler rebuilds the EXCEPTION_POINTERS
|
||||||
|
// view the shared handlers expect (Win64 CONTEXT register offsets) from
|
||||||
|
// the signal's mcontext, runs the same recovery chain the VEH path uses
|
||||||
|
// (unresolved-import trap sentinels, demand-paging of lazily-committed
|
||||||
|
// guest pages, fault diagnostics), and writes register changes back into
|
||||||
|
// the mcontext so sigreturn resumes the repaired guest. Unrecovered
|
||||||
|
// faults are forwarded to the previously installed handler so the .NET
|
||||||
|
// runtime keeps turning its own faults into managed exceptions.
|
||||||
|
|
||||||
|
private const int PosixSigIll = 4;
|
||||||
|
private const int PosixSigSegv = 11;
|
||||||
|
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
||||||
|
|
||||||
|
// struct sigaction: the handler pointer leads on both platforms; Darwin
|
||||||
|
// packs { handler(8), mask(4), flags(4) }, Linux glibc/musl packs
|
||||||
|
// { handler(8), mask(128), flags(4), restorer(8) }.
|
||||||
|
private static readonly int PosixSigactionSize = OperatingSystem.IsMacOS() ? 16 : 152;
|
||||||
|
private static readonly int PosixSigactionFlagsOffset = OperatingSystem.IsMacOS() ? 12 : 136;
|
||||||
|
|
||||||
|
private static readonly int PosixSaSigInfo = OperatingSystem.IsMacOS() ? 0x0040 : 0x0004;
|
||||||
|
private static readonly int PosixSaNoDefer = OperatingSystem.IsMacOS() ? 0x0010 : 0x40000000;
|
||||||
|
|
||||||
|
// siginfo_t.si_addr: Darwin { signo, errno, code, pid, uid, status, addr },
|
||||||
|
// Linux { signo, errno, code, pad32, addr }.
|
||||||
|
private static readonly int PosixSigInfoAddressOffset = OperatingSystem.IsMacOS() ? 24 : 16;
|
||||||
|
|
||||||
|
// Darwin ucontext_t stores a pointer to __darwin_mcontext64 at +48; the
|
||||||
|
// general registers live in its __ss thread state after the 16-byte
|
||||||
|
// exception state. Linux glibc embeds mcontext_t inline at +40 with the
|
||||||
|
// registers in gregs[23]. Rosetta 2 delivers the regular x86-64 layout
|
||||||
|
// to translated processes.
|
||||||
|
private const int DarwinUcontextMcontextOffset = 48;
|
||||||
|
private const int DarwinMcontextErrOffset = 4;
|
||||||
|
private const int DarwinMcontextFaultAddressOffset = 8;
|
||||||
|
private const int LinuxUcontextGregsOffset = 40;
|
||||||
|
private const int LinuxGregsErrOffset = 19 * 8;
|
||||||
|
|
||||||
|
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||||
|
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||||
|
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||||
|
// against the x86-64 platform headers.
|
||||||
|
private static readonly int[] PosixRegisterOffsets = OperatingSystem.IsMacOS()
|
||||||
|
? new[] { 16, 32, 40, 24, 72, 64, 56, 48, 80, 88, 96, 104, 112, 120, 128, 136, 144 }
|
||||||
|
: new[] { 104, 112, 96, 88, 120, 80, 72, 64, 0, 8, 16, 24, 32, 40, 48, 56, 128 };
|
||||||
|
|
||||||
|
private static DirectExecutionBackend? _posixSignalBackend;
|
||||||
|
private static bool _posixSignalHandlersInstalled;
|
||||||
|
private static bool _posixRawRecoveryEnabled;
|
||||||
|
private static bool _posixSignalWarmup;
|
||||||
|
private static readonly nint[] _posixPreviousActions = new nint[32];
|
||||||
|
private static int _posixSignalTraceCount;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static int _posixSignalHandlerDepth;
|
||||||
|
|
||||||
|
private void SetupPosixExceptionHandler()
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][WARN] POSIX signal exception bridge disabled by SHARPEMU_DISABLE_POSIX_SIGNALS=1; guest faults will not be recovered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixSignalBackend = this;
|
||||||
|
if (_posixSignalHandlersInstalled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixRawRecoveryEnabled = !string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal);
|
||||||
|
if (!_posixRawRecoveryEnabled)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Raw sentinel recovery disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
|
||||||
|
}
|
||||||
|
|
||||||
|
WarmUpPosixSignalPath();
|
||||||
|
|
||||||
|
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigIll))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixSignalHandlersInstalled = true;
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the signal-recovery path once with fabricated inputs before the
|
||||||
|
/// handlers are installed. The first entry into the handler must not
|
||||||
|
/// require JIT compilation (a fault can interrupt arbitrary runtime
|
||||||
|
/// states), and under Rosetta 2 the signal trampoline cannot enter x86
|
||||||
|
/// code that has never been executed (and therefore never translated): a
|
||||||
|
/// cold handler is silently never invoked and the faulting instruction
|
||||||
|
/// retries forever.
|
||||||
|
/// </summary>
|
||||||
|
private void WarmUpPosixSignalPath()
|
||||||
|
{
|
||||||
|
byte* fakeUcontext = stackalloc byte[512];
|
||||||
|
new Span<byte>(fakeUcontext, 512).Clear();
|
||||||
|
byte* fakeMcontext = stackalloc byte[512];
|
||||||
|
new Span<byte>(fakeMcontext, 512).Clear();
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixSignalWarmup = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)fakeUcontext);
|
||||||
|
|
||||||
|
// Warm the branches the fabricated fault above skips without
|
||||||
|
// spamming diagnostics: the benign-exception path through
|
||||||
|
// VectoredHandler, the lazy-commit probe (fault address 0 bails
|
||||||
|
// out immediately), and the chain helper (signal 0 has no saved
|
||||||
|
// action and sigaction(0, ...) fails with EINVAL).
|
||||||
|
EXCEPTION_RECORD record = default;
|
||||||
|
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
||||||
|
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
||||||
|
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
||||||
|
EXCEPTION_POINTERS pointers;
|
||||||
|
pointers.ExceptionRecord = &record;
|
||||||
|
pointers.ContextRecord = contextRecord;
|
||||||
|
_ = VectoredHandler(&pointers);
|
||||||
|
|
||||||
|
record.ExceptionCode = 3221225477u;
|
||||||
|
record.NumberParameters = 2;
|
||||||
|
// 0x70000 is never guest-owned, so this walks the vmem region
|
||||||
|
// scan and the PRT range check, then bails out silently.
|
||||||
|
record.ExceptionInformation[1] = 0x70000;
|
||||||
|
_ = TryHandleLazyCommittedPage(&record, 0, 0);
|
||||||
|
ChainPreviousPosixAction(0, 0, 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_posixSignalWarmup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InstallPosixSignalHandler(int signal)
|
||||||
|
{
|
||||||
|
byte* action = stackalloc byte[PosixSigactionSize];
|
||||||
|
new Span<byte>(action, PosixSigactionSize).Clear();
|
||||||
|
*(nint*)action = (nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal;
|
||||||
|
// No SA_ONSTACK: the runtime's alternate stacks are far too small for
|
||||||
|
// the recovery/diagnostic path (JIT compilation of cold handler code
|
||||||
|
// can run inside the signal frame). Guest faults deliver onto the 2MB
|
||||||
|
// guest stack, host faults onto the regular thread stack — the same
|
||||||
|
// stacks Windows dispatches exceptions on.
|
||||||
|
*(int*)(action + PosixSigactionFlagsOffset) = PosixSaSigInfo | PosixSaNoDefer;
|
||||||
|
|
||||||
|
var previous = (byte*)NativeMemory.AllocZeroed((nuint)PosixSigactionSize);
|
||||||
|
if (sigaction(signal, action, previous) != 0)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(previous);
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] sigaction({signal}) failed: errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixPreviousActions[signal] = (nint)previous;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly]
|
||||||
|
private static void HandlePosixSignal(int signal, nint siginfo, nint ucontext)
|
||||||
|
{
|
||||||
|
if (_posixSignalHandlerDepth > 0)
|
||||||
|
{
|
||||||
|
// A fault inside our own fault handler (diagnostics touched an
|
||||||
|
// unmapped address): restore the default action and return so the
|
||||||
|
// re-executed instruction terminates the process.
|
||||||
|
RestoreDefaultPosixAction(signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_posixSignalHandlerDepth++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A managed exception must never unwind out of a signal frame.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_posixSignalHandlerDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChainPreviousPosixAction(signal, siginfo, ucontext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryHandlePosixFault(int signal, nint siginfo, nint ucontext)
|
||||||
|
{
|
||||||
|
byte* registers = GetPosixRegisterBase(ucontext);
|
||||||
|
if (registers == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
||||||
|
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
||||||
|
int[] offsets = PosixRegisterOffsets;
|
||||||
|
for (int i = 0; i < offsets.Length; i++)
|
||||||
|
{
|
||||||
|
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXCEPTION_RECORD record = default;
|
||||||
|
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
|
if (signal == PosixSigIll)
|
||||||
|
{
|
||||||
|
record.ExceptionCode = 3221225501u;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||||
|
record.ExceptionCode = 3221225477u;
|
||||||
|
record.NumberParameters = 2;
|
||||||
|
record.ExceptionInformation[0] = GetPosixAccessType(registers, faultAddress, ReadCtxU64(contextRecord, CTX_RIP));
|
||||||
|
record.ExceptionInformation[1] = faultAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXCEPTION_POINTERS pointers;
|
||||||
|
pointers.ExceptionRecord = &record;
|
||||||
|
pointers.ContextRecord = contextRecord;
|
||||||
|
|
||||||
|
int traceIndex = _posixSignalWarmup ? 0 : Interlocked.Increment(ref _posixSignalTraceCount);
|
||||||
|
bool traceSignal = traceIndex > 0 && (traceIndex <= 16 || traceIndex % 1024 == 0 ||
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_POSIX_SIGNALS"), "1", StringComparison.Ordinal));
|
||||||
|
if (traceSignal)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] posix-signal#{traceIndex}: sig={signal} rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16} " +
|
||||||
|
$"fault=0x{record.ExceptionInformation[1]:X16} access={record.ExceptionInformation[0]} rsp=0x{ReadCtxU64(contextRecord, CTX_RSP):X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sentinel recovery runs first: on Windows both vectored handlers see
|
||||||
|
// every fault anyway, and recovering here avoids dumping the full
|
||||||
|
// VectoredHandler diagnostics for each recoverable trap.
|
||||||
|
int disposition = 0;
|
||||||
|
if (_posixRawRecoveryEnabled)
|
||||||
|
{
|
||||||
|
disposition = TryRecoverUnresolvedSentinel(&pointers);
|
||||||
|
}
|
||||||
|
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
|
||||||
|
{
|
||||||
|
disposition = backend.VectoredHandler(&pointers);
|
||||||
|
}
|
||||||
|
if (traceSignal)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] posix-signal#{traceIndex}: recovered={disposition == -1} new_rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
if (disposition != -1 && !_posixSignalWarmup)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < offsets.Length; i++)
|
||||||
|
{
|
||||||
|
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte* GetPosixRegisterBase(nint ucontext)
|
||||||
|
{
|
||||||
|
if (ucontext == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return *(byte**)((byte*)ucontext + DarwinUcontextMcontextOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte*)ucontext + LinuxUcontextGregsOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
|
||||||
|
{
|
||||||
|
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
|
||||||
|
if (address == 0 && OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
address = *(ulong*)(registers + DarwinMcontextFaultAddressOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong GetPosixAccessType(byte* registers, ulong faultAddress, ulong rip)
|
||||||
|
{
|
||||||
|
// x86 page-fault error code: bit 1 = write access, bit 4 = instruction
|
||||||
|
// fetch. Fall back to comparing the fault address against RIP when
|
||||||
|
// the error code is not populated (e.g. under Rosetta 2 translation).
|
||||||
|
ulong error = OperatingSystem.IsMacOS()
|
||||||
|
? *(uint*)(registers + DarwinMcontextErrOffset)
|
||||||
|
: *(ulong*)(registers + LinuxGregsErrOffset);
|
||||||
|
if ((error & 0x10) != 0)
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if ((error & 0x2) != 0)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return faultAddress != 0 && faultAddress == rip ? 8u : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreDefaultPosixAction(int signal)
|
||||||
|
{
|
||||||
|
byte* action = stackalloc byte[PosixSigactionSize];
|
||||||
|
new Span<byte>(action, PosixSigactionSize).Clear();
|
||||||
|
_ = sigaction(signal, action, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ChainPreviousPosixAction(int signal, nint siginfo, nint ucontext)
|
||||||
|
{
|
||||||
|
byte* previous = (uint)signal < (uint)_posixPreviousActions.Length
|
||||||
|
? (byte*)_posixPreviousActions[signal]
|
||||||
|
: null;
|
||||||
|
nint handler = previous != null ? *(nint*)previous : 0;
|
||||||
|
if (handler == 0)
|
||||||
|
{
|
||||||
|
// SIG_DFL (or nothing saved): reinstate the default action and
|
||||||
|
// return, so re-executing the faulting instruction terminates the
|
||||||
|
// process with the original fault context intact.
|
||||||
|
RestoreDefaultPosixAction(signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (handler == 1)
|
||||||
|
{
|
||||||
|
// SIG_IGN
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = *(int*)(previous + PosixSigactionFlagsOffset);
|
||||||
|
if ((flags & PosixSaSigInfo) != 0)
|
||||||
|
{
|
||||||
|
((delegate* unmanaged<int, nint, nint, void>)handler)(signal, siginfo, ucontext);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
((delegate* unmanaged<int, void>)handler)(signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int sigaction(int signum, void* act, void* oldact);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ using SharpEmu.Core.Loader;
|
|||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -138,15 +139,25 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private const ulong GuestImageScanEnd = 36507222016uL;
|
private const ulong GuestImageScanEnd = 36507222016uL;
|
||||||
|
|
||||||
private const ulong GuestThreadStackBaseAddress = 0x7FFF_E000_0000UL;
|
// The 0x7FFx window is Windows-specific; dyld and Rosetta reserve that
|
||||||
|
// range on macOS, so POSIX guest threads use the lower 0x6FFx window.
|
||||||
|
// The POSIX stack base sits a further 1GB down: the import-stub region
|
||||||
|
// descends from 0x7000_0000_0000 on the same 16MB grid and reaches
|
||||||
|
// 0x6FFF_C000_0000 at its 64-module limit, which would otherwise consume
|
||||||
|
// the top stack slots (on Windows the two bands are ~15TB apart).
|
||||||
|
private static readonly ulong GuestThreadStackBaseAddress =
|
||||||
|
OperatingSystem.IsWindows() ? 0x7FFF_E000_0000UL : 0x6FFF_A000_0000UL;
|
||||||
|
|
||||||
private const ulong GuestThreadTlsBaseAddress = 0x7FFE_0000_0000UL;
|
private static readonly ulong GuestThreadTlsBaseAddress =
|
||||||
|
OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||||
|
|
||||||
private const ulong GuestThreadStackSize = 0x0020_0000UL;
|
private const ulong GuestThreadStackSize = 0x0020_0000UL;
|
||||||
|
|
||||||
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
|
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
|
||||||
|
|
||||||
private const ulong GuestThreadTlsPrefixSize = 0x0000_1000UL;
|
// Matches CpuDispatcher.TlsPrefixSize: static TLS blocks sit below the
|
||||||
|
// TCB, and libc.prx already reaches beyond -0x1700 on POSIX.
|
||||||
|
private static readonly ulong GuestThreadTlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
||||||
|
|
||||||
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
||||||
|
|
||||||
@@ -546,6 +557,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly object _guestThreadGate = new object();
|
private readonly object _guestThreadGate = new object();
|
||||||
|
|
||||||
|
// Diagnostic owner tracking for _guestThreadGate; written only while the
|
||||||
|
// gate is held, read lock-free by the stall watchdog's periodic snapshot.
|
||||||
|
private volatile string? _gateOwnerSite;
|
||||||
|
private int _gateOwnerManagedThreadId;
|
||||||
|
private long _gateAcquireTimestamp;
|
||||||
|
|
||||||
|
private GateHolder LockGate(string site)
|
||||||
|
{
|
||||||
|
Monitor.Enter(_guestThreadGate);
|
||||||
|
_gateOwnerSite = site;
|
||||||
|
Volatile.Write(ref _gateOwnerManagedThreadId, Environment.CurrentManagedThreadId);
|
||||||
|
Volatile.Write(ref _gateAcquireTimestamp, Stopwatch.GetTimestamp());
|
||||||
|
return new GateHolder(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct GateHolder : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DirectExecutionBackend _owner;
|
||||||
|
|
||||||
|
public GateHolder(DirectExecutionBackend owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_owner._gateOwnerSite = null;
|
||||||
|
Volatile.Write(ref _owner._gateOwnerManagedThreadId, 0);
|
||||||
|
Monitor.Exit(_owner._guestThreadGate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
||||||
|
|
||||||
// Once set, guest worker threads are unwound to the host at their next import
|
// Once set, guest worker threads are unwound to the host at their next import
|
||||||
@@ -610,8 +653,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
private static readonly RawExceptionHandlerDelegate RawVectoredHandlerDelegateInstance = RawVectoredHandlerManaged;
|
private static readonly RawExceptionHandlerDelegate RawVectoredHandlerDelegateInstance = RawVectoredHandlerManaged;
|
||||||
private static readonly RawExceptionHandlerDelegate RawUnhandledFilterDelegateInstance = RawUnhandledFilterManaged;
|
private static readonly RawExceptionHandlerDelegate RawUnhandledFilterDelegateInstance = RawUnhandledFilterManaged;
|
||||||
|
|
||||||
private static readonly nint ImportGatewayPtr =
|
private static readonly nint ImportGatewayPtr = ResolveImportGatewayPtr();
|
||||||
Marshal.GetFunctionPointerForDelegate(ImportGatewayDelegateInstance);
|
|
||||||
|
// Emitted import trampolines use the Win64 ABI. Managed callbacks use the
|
||||||
|
// host ABI, so POSIX needs a small Win64-to-SysV register-shuffling thunk.
|
||||||
|
private static nint ResolveImportGatewayPtr()
|
||||||
|
{
|
||||||
|
var managedPtr = Marshal.GetFunctionPointerForDelegate(ImportGatewayDelegateInstance);
|
||||||
|
return OperatingSystem.IsWindows()
|
||||||
|
? managedPtr
|
||||||
|
: PosixHostStubs.CreateWin64ToSysVThunk(managedPtr);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly nint RawVectoredHandlerPtrManaged =
|
private static readonly nint RawVectoredHandlerPtrManaged =
|
||||||
Marshal.GetFunctionPointerForDelegate(RawVectoredHandlerDelegateInstance);
|
Marshal.GetFunctionPointerForDelegate(RawVectoredHandlerDelegateInstance);
|
||||||
@@ -865,7 +917,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_hostThreading = _hostPlatform.Threading;
|
_hostThreading = _hostPlatform.Threading;
|
||||||
_hostSymbols = _hostPlatform.Symbols;
|
_hostSymbols = _hostPlatform.Symbols;
|
||||||
_hostMemory = _hostPlatform.Memory;
|
_hostMemory = _hostPlatform.Memory;
|
||||||
_faultHandling = faultHandling ?? new WindowsFaultHandling(_hostMemory);
|
_faultHandling = faultHandling ?? (OperatingSystem.IsWindows()
|
||||||
|
? new WindowsFaultHandling(_hostMemory)
|
||||||
|
: NullHostFaultHandling.Instance);
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
||||||
_guestTlsBaseTlsIndex = _hostThreading.AllocateTlsSlot();
|
_guestTlsBaseTlsIndex = _hostThreading.AllocateTlsSlot();
|
||||||
@@ -1976,26 +2030,54 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
throw new OutOfMemoryException("Failed to allocate TLS handler");
|
throw new OutOfMemoryException("Failed to allocate TLS handler");
|
||||||
}
|
}
|
||||||
|
// The handler runs in place of a patched guest `mov reg, fs:[0]`,
|
||||||
|
// which preserves every register and the flags. TlsGetValue (and the
|
||||||
|
// Win64 ABI in general) clobbers rcx/rdx/r8-r11 and the arithmetic
|
||||||
|
// flags, so save them all: guest code legitimately keeps live values
|
||||||
|
// and comparison results across TLS reads, and losing them corrupted
|
||||||
|
// deterministic computations (e.g. procedural texture generation).
|
||||||
byte* tlsHandlerAddress = (byte*)_tlsHandlerAddress;
|
byte* tlsHandlerAddress = (byte*)_tlsHandlerAddress;
|
||||||
int num = 0;
|
int num = 0;
|
||||||
tlsHandlerAddress[num++] = 72;
|
tlsHandlerAddress[num++] = 0x9C; // pushfq
|
||||||
tlsHandlerAddress[num++] = 131;
|
tlsHandlerAddress[num++] = 0x51; // push rcx
|
||||||
tlsHandlerAddress[num++] = 236;
|
tlsHandlerAddress[num++] = 0x52; // push rdx
|
||||||
tlsHandlerAddress[num++] = 40;
|
tlsHandlerAddress[num++] = 0x41; // push r8
|
||||||
tlsHandlerAddress[num++] = 185;
|
tlsHandlerAddress[num++] = 0x50;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // push r9
|
||||||
|
tlsHandlerAddress[num++] = 0x51;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // push r10
|
||||||
|
tlsHandlerAddress[num++] = 0x52;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // push r11
|
||||||
|
tlsHandlerAddress[num++] = 0x53;
|
||||||
|
tlsHandlerAddress[num++] = 0x48; // sub rsp, 0x20
|
||||||
|
tlsHandlerAddress[num++] = 0x83;
|
||||||
|
tlsHandlerAddress[num++] = 0xEC;
|
||||||
|
tlsHandlerAddress[num++] = 0x20;
|
||||||
|
tlsHandlerAddress[num++] = 0xB9; // mov ecx, index
|
||||||
*(uint*)(tlsHandlerAddress + num) = _guestTlsBaseTlsIndex;
|
*(uint*)(tlsHandlerAddress + num) = _guestTlsBaseTlsIndex;
|
||||||
num += 4;
|
num += 4;
|
||||||
tlsHandlerAddress[num++] = 72;
|
tlsHandlerAddress[num++] = 0x48; // mov rax, TlsGetValue
|
||||||
tlsHandlerAddress[num++] = 184;
|
tlsHandlerAddress[num++] = 0xB8;
|
||||||
*(long*)(tlsHandlerAddress + num) = _tlsGetValueAddress;
|
*(long*)(tlsHandlerAddress + num) = _tlsGetValueAddress;
|
||||||
num += 8;
|
num += 8;
|
||||||
tlsHandlerAddress[num++] = byte.MaxValue;
|
tlsHandlerAddress[num++] = 0xFF; // call rax
|
||||||
tlsHandlerAddress[num++] = 208;
|
tlsHandlerAddress[num++] = 0xD0;
|
||||||
tlsHandlerAddress[num++] = 72;
|
tlsHandlerAddress[num++] = 0x48; // add rsp, 0x20
|
||||||
tlsHandlerAddress[num++] = 131;
|
tlsHandlerAddress[num++] = 0x83;
|
||||||
tlsHandlerAddress[num++] = 196;
|
tlsHandlerAddress[num++] = 0xC4;
|
||||||
tlsHandlerAddress[num++] = 40;
|
tlsHandlerAddress[num++] = 0x20;
|
||||||
tlsHandlerAddress[num++] = 195;
|
tlsHandlerAddress[num++] = 0x41; // pop r11
|
||||||
|
tlsHandlerAddress[num++] = 0x5B;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // pop r10
|
||||||
|
tlsHandlerAddress[num++] = 0x5A;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // pop r9
|
||||||
|
tlsHandlerAddress[num++] = 0x59;
|
||||||
|
tlsHandlerAddress[num++] = 0x41; // pop r8
|
||||||
|
tlsHandlerAddress[num++] = 0x58;
|
||||||
|
tlsHandlerAddress[num++] = 0x5A; // pop rdx
|
||||||
|
tlsHandlerAddress[num++] = 0x59; // pop rcx
|
||||||
|
tlsHandlerAddress[num++] = 0x9D; // popfq
|
||||||
|
tlsHandlerAddress[num++] = 0xC3; // ret
|
||||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
if (!_hostMemory.Protect((ulong)(void*)_tlsHandlerAddress, TlsHandlerRegionSize, HostPageProtection.ReadExecute, out num2))
|
if (!_hostMemory.Protect((ulong)(void*)_tlsHandlerAddress, TlsHandlerRegionSize, HostPageProtection.ReadExecute, out num2))
|
||||||
@@ -2613,7 +2695,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
lock (_guestThreadGate)
|
using (LockGate("TryStartThread"))
|
||||||
{
|
{
|
||||||
_guestThreads[request.ThreadHandle] = thread;
|
_guestThreads[request.ThreadHandle] = thread;
|
||||||
_readyGuestThreads.Enqueue(thread);
|
_readyGuestThreads.Enqueue(thread);
|
||||||
@@ -2649,10 +2731,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Joins regularly park here for minutes (a game main thread joining a
|
||||||
|
// streamer); polling at a fixed 1ms burns half a host core for the
|
||||||
|
// whole wait, so back off toward a 10ms cadence once the join is
|
||||||
|
// clearly long-lived.
|
||||||
|
var joinPollMilliseconds = 1;
|
||||||
while (!ActiveForcedGuestExit)
|
while (!ActiveForcedGuestExit)
|
||||||
{
|
{
|
||||||
Thread? hostThread;
|
Thread? hostThread;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("TryJoinThread"))
|
||||||
{
|
{
|
||||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
|
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
|
||||||
{
|
{
|
||||||
@@ -2689,16 +2776,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
hostThread.Join(1);
|
hostThread.Join(joinPollMilliseconds);
|
||||||
}
|
}
|
||||||
catch (ThreadStateException)
|
catch (ThreadStateException)
|
||||||
{
|
{
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(joinPollMilliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(joinPollMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (joinPollMilliseconds < 10)
|
||||||
|
{
|
||||||
|
joinPollMilliseconds++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2728,7 +2820,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
{
|
{
|
||||||
GuestThreadState? thread = null;
|
GuestThreadState? thread = null;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("Pump.dequeue"))
|
||||||
{
|
{
|
||||||
while (_readyGuestThreads.Count > 0)
|
while (_readyGuestThreads.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -2759,7 +2851,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
Name = $"SharpEmu-{thread.Name}",
|
Name = $"SharpEmu-{thread.Name}",
|
||||||
Priority = MapGuestThreadPriority(thread.Priority),
|
Priority = MapGuestThreadPriority(thread.Priority),
|
||||||
};
|
};
|
||||||
lock (_guestThreadGate)
|
using (LockGate("Pump.bind_host"))
|
||||||
{
|
{
|
||||||
thread.HostThread = hostThread;
|
thread.HostThread = hostThread;
|
||||||
}
|
}
|
||||||
@@ -2780,7 +2872,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wakeCount = 0;
|
var wakeCount = 0;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("WakeBlockedThreads"))
|
||||||
{
|
{
|
||||||
foreach (var thread in _guestThreads.Values)
|
foreach (var thread in _guestThreads.Values)
|
||||||
{
|
{
|
||||||
@@ -2829,7 +2921,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads()
|
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads()
|
||||||
{
|
{
|
||||||
lock (_guestThreadGate)
|
using (LockGate("SnapshotThreads"))
|
||||||
{
|
{
|
||||||
var snapshots = new GuestThreadSnapshot[_guestThreads.Count];
|
var snapshots = new GuestThreadSnapshot[_guestThreads.Count];
|
||||||
var index = 0;
|
var index = 0;
|
||||||
@@ -2861,7 +2953,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_guestThreadGate)
|
using (LockGate("RegisterBlockedContinuation"))
|
||||||
{
|
{
|
||||||
if (!_guestThreads.TryGetValue(guestThreadHandle, out var thread))
|
if (!_guestThreads.TryGetValue(guestThreadHandle, out var thread))
|
||||||
{
|
{
|
||||||
@@ -2880,7 +2972,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
var now = Stopwatch.GetTimestamp();
|
var now = Stopwatch.GetTimestamp();
|
||||||
var wakeCount = 0;
|
var wakeCount = 0;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("WakeExpiredBlockedGuestThreads"))
|
||||||
{
|
{
|
||||||
foreach (var thread in _guestThreads.Values)
|
foreach (var thread in _guestThreads.Values)
|
||||||
{
|
{
|
||||||
@@ -2971,7 +3063,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private GuestThreadState[] SnapshotGuestThreads()
|
private GuestThreadState[] SnapshotGuestThreads()
|
||||||
{
|
{
|
||||||
lock (_guestThreadGate)
|
using (LockGate("SnapshotGuestThreads"))
|
||||||
{
|
{
|
||||||
return _guestThreads.Values.ToArray();
|
return _guestThreads.Values.ToArray();
|
||||||
}
|
}
|
||||||
@@ -3161,7 +3253,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
if (currentGuestThreadHandle != 0)
|
if (currentGuestThreadHandle != 0)
|
||||||
{
|
{
|
||||||
GuestContinuationRunner? runner;
|
GuestContinuationRunner? runner;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("TryCallGuestContinuation"))
|
||||||
{
|
{
|
||||||
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
|
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
|
||||||
{
|
{
|
||||||
@@ -3253,7 +3345,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
_guestTeardownRequested = true;
|
_guestTeardownRequested = true;
|
||||||
Thread[] hostThreads;
|
Thread[] hostThreads;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("RequestGuestThreadTeardown"))
|
||||||
{
|
{
|
||||||
_readyGuestThreads.Clear();
|
_readyGuestThreads.Clear();
|
||||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||||
@@ -3283,7 +3375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
private void ClearGuestThreads()
|
private void ClearGuestThreads()
|
||||||
{
|
{
|
||||||
GuestContinuationRunner[] runners;
|
GuestContinuationRunner[] runners;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("ClearGuestThreads"))
|
||||||
{
|
{
|
||||||
runners = _guestThreads.Values
|
runners = _guestThreads.Values
|
||||||
.Select(static thread => thread.ContinuationRunner)
|
.Select(static thread => thread.ContinuationRunner)
|
||||||
@@ -3572,7 +3664,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
GuestCpuContinuation continuation = default;
|
GuestCpuContinuation continuation = default;
|
||||||
IGuestThreadBlockWaiter? blockWaiter = null;
|
IGuestThreadBlockWaiter? blockWaiter = null;
|
||||||
var resumeContinuation = false;
|
var resumeContinuation = false;
|
||||||
lock (_guestThreadGate)
|
using (LockGate("RunGuestThread.block"))
|
||||||
{
|
{
|
||||||
if (thread.HasBlockedContinuation)
|
if (thread.HasBlockedContinuation)
|
||||||
{
|
{
|
||||||
@@ -3602,7 +3694,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
var exitReason = resumeContinuation
|
var exitReason = resumeContinuation
|
||||||
? ExecuteBlockedGuestThreadContinuation(thread.Context, continuation, thread.Name, out var blockReason)
|
? ExecuteBlockedGuestThreadContinuation(thread.Context, continuation, thread.Name, out var blockReason)
|
||||||
: ExecuteGuestThreadEntry(thread.Context, thread.EntryPoint, thread.Name, out blockReason);
|
: ExecuteGuestThreadEntry(thread.Context, thread.EntryPoint, thread.Name, out blockReason);
|
||||||
lock (_guestThreadGate)
|
using (LockGate("RunGuestThread.exit"))
|
||||||
{
|
{
|
||||||
switch (exitReason)
|
switch (exitReason)
|
||||||
{
|
{
|
||||||
@@ -4355,6 +4447,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return 20;
|
return 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SHARPEMU_PERIODIC_SNAPSHOT_SECONDS=N: dump the stall snapshot every N
|
||||||
|
// seconds regardless of progress, for diagnosing soft stalls where imports
|
||||||
|
// keep flowing but the game stops advancing.
|
||||||
|
private static int GetPeriodicSnapshotSeconds()
|
||||||
|
{
|
||||||
|
if (int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_PERIODIC_SNAPSHOT_SECONDS"), out var result))
|
||||||
|
{
|
||||||
|
return Math.Max(0, result);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void StartStallWatchdog()
|
private void StartStallWatchdog()
|
||||||
{
|
{
|
||||||
int stallWatchdogSeconds = GetStallWatchdogSeconds();
|
int stallWatchdogSeconds = GetStallWatchdogSeconds();
|
||||||
@@ -4384,6 +4488,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
dispatcherThread.Start();
|
dispatcherThread.Start();
|
||||||
|
|
||||||
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
||||||
|
var periodicSnapshotTicks = (long)((double)GetPeriodicSnapshotSeconds() * Stopwatch.Frequency);
|
||||||
|
var lastPeriodicSnapshot = Stopwatch.GetTimestamp();
|
||||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||||
{
|
{
|
||||||
while (!_stallWatchdogStop)
|
while (!_stallWatchdogStop)
|
||||||
@@ -4393,6 +4499,58 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (periodicSnapshotTicks > 0 &&
|
||||||
|
Stopwatch.GetTimestamp() - lastPeriodicSnapshot >= periodicSnapshotTicks)
|
||||||
|
{
|
||||||
|
lastPeriodicSnapshot = Stopwatch.GetTimestamp();
|
||||||
|
var gateOwnerSite = _gateOwnerSite;
|
||||||
|
var gateOwnerTid = Volatile.Read(ref _gateOwnerManagedThreadId);
|
||||||
|
var gateHeldMs = gateOwnerSite is null
|
||||||
|
? 0.0
|
||||||
|
: Stopwatch.GetElapsedTime(Volatile.Read(ref _gateAcquireTimestamp)).TotalMilliseconds;
|
||||||
|
var snapshotText = new System.Text.StringBuilder();
|
||||||
|
snapshotText.AppendLine(
|
||||||
|
$"[LOADER][DIAG] Periodic snapshot: gate_owner={gateOwnerSite ?? "none"} " +
|
||||||
|
$"gate_tid={gateOwnerTid} gate_held_ms={gateHeldMs:0}");
|
||||||
|
// Never touch the gate here: the periodic snapshot must keep
|
||||||
|
// reporting even (especially) when the gate is wedged.
|
||||||
|
// Dump guest threads without the lock; tolerate torn reads.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var thread in _guestThreads.Values)
|
||||||
|
{
|
||||||
|
snapshotText.AppendLine(
|
||||||
|
$"[LOADER][DIAG] gateless guest-thread: handle=0x{thread.ThreadHandle:X16} name='{thread.Name}' " +
|
||||||
|
$"state={thread.State} imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||||
|
$"nid={Volatile.Read(ref thread.LastImportNid) ?? "none"} ret=0x{Volatile.Read(ref thread.LastReturnRip):X16} " +
|
||||||
|
$"block={thread.BlockReason ?? "none"} wake={thread.BlockWakeKey ?? "none"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception snapshotError)
|
||||||
|
{
|
||||||
|
snapshotText.AppendLine($"[LOADER][DIAG] gateless snapshot failed: {snapshotError.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console can be wedged by whatever is being diagnosed, so
|
||||||
|
// write to a side file when one is configured and fall back
|
||||||
|
// to stderr otherwise.
|
||||||
|
var snapshotPath = Environment.GetEnvironmentVariable("SHARPEMU_PERIODIC_SNAPSHOT_FILE");
|
||||||
|
if (!string.IsNullOrWhiteSpace(snapshotPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.File.AppendAllText(snapshotPath, snapshotText.ToString());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.Write(snapshotText.ToString());
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
long num2 = Stopwatch.GetTimestamp() - Volatile.Read(ref _lastProgressTimestamp);
|
long num2 = Stopwatch.GetTimestamp() - Volatile.Read(ref _lastProgressTimestamp);
|
||||||
if (num2 < num)
|
if (num2 < num)
|
||||||
{
|
{
|
||||||
@@ -4441,7 +4599,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
private bool HasReadyGuestThread()
|
private bool HasReadyGuestThread()
|
||||||
{
|
{
|
||||||
WakeExpiredBlockedGuestThreads();
|
WakeExpiredBlockedGuestThreads();
|
||||||
lock (_guestThreadGate)
|
using (LockGate("HasReadyGuestThread"))
|
||||||
{
|
{
|
||||||
foreach (var thread in _guestThreads.Values)
|
foreach (var thread in _guestThreads.Values)
|
||||||
{
|
{
|
||||||
@@ -4630,6 +4788,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// Native guest workers park idle once every guest thread has unwound; stop
|
// Native guest workers park idle once every guest thread has unwound; stop
|
||||||
// them before any executable stub or TLS index they reference is freed.
|
// them before any executable stub or TLS index they reference is freed.
|
||||||
DisposeNativeGuestExecutors();
|
DisposeNativeGuestExecutors();
|
||||||
|
|
||||||
|
if (ReferenceEquals(_posixSignalBackend, this))
|
||||||
|
{
|
||||||
|
// The signal handlers stay installed (they chain to the previous
|
||||||
|
// action when no backend is active), but must stop dispatching
|
||||||
|
// into a disposed backend.
|
||||||
|
_posixSignalBackend = null;
|
||||||
|
}
|
||||||
ClearImportHandlerTrampolines();
|
ClearImportHandlerTrampolines();
|
||||||
_importEntries = Array.Empty<ImportStubEntry>();
|
_importEntries = Array.Empty<ImportStubEntry>();
|
||||||
_runtimeSymbolsByName.Clear();
|
_runtimeSymbolsByName.Clear();
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Placeholder for hosts whose fault bridge is installed directly by the
|
||||||
|
/// execution backend. POSIX uses its sigaction bridge and never calls these
|
||||||
|
/// Windows-shaped registration methods.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class NullHostFaultHandling : IHostFaultHandling
|
||||||
|
{
|
||||||
|
public static NullHostFaultHandling Instance { get; } = new();
|
||||||
|
|
||||||
|
private NullHostFaultHandling()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||||
|
{
|
||||||
|
_ = managedCallback;
|
||||||
|
_ = hostRspSwitchTlsSlot;
|
||||||
|
_ = tlsGetValueAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FreeThunk(nint thunk)
|
||||||
|
{
|
||||||
|
_ = thunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint AddFirstChanceHandler(nint thunk)
|
||||||
|
{
|
||||||
|
_ = thunk;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveHandler(nint handle)
|
||||||
|
{
|
||||||
|
_ = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUnhandledFilter(nint thunk)
|
||||||
|
{
|
||||||
|
_ = thunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ namespace SharpEmu.Core.Cpu.Native.Windows;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class Win64ContextOffsets
|
internal static class Win64ContextOffsets
|
||||||
{
|
{
|
||||||
|
public const int Size = 0x4D0;
|
||||||
public const int Mxcsr = 52;
|
public const int Mxcsr = 52;
|
||||||
public const int Rax = 120;
|
public const int Rax = 120;
|
||||||
public const int Rcx = 128;
|
public const int Rcx = 128;
|
||||||
|
|||||||
@@ -249,6 +249,71 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
|
// Under Rosetta 2 the kernel can ignore placement hints for whole
|
||||||
|
// windows, so page-stepped exact probes are pathological on macOS.
|
||||||
|
// Linux must keep using the exact-address search below: PS5 resource
|
||||||
|
// descriptors cannot represent ordinary 0x7F... host mappings. Linux
|
||||||
|
// HostMemory uses MAP_FIXED_NOREPLACE, making those low-address probes
|
||||||
|
// safe without clobbering existing host mappings.
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Prefer the requested low address. Besides matching the guest
|
||||||
|
// address model, this keeps the allocation representable by every
|
||||||
|
// PS5 GPU descriptor (the strictest ones carry 40 address bits).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exactAddress = AllocateAt(
|
||||||
|
cursor,
|
||||||
|
alignedSize,
|
||||||
|
executable,
|
||||||
|
allowAlternative: false);
|
||||||
|
if (exactAddress == cursor)
|
||||||
|
{
|
||||||
|
actualAddress = exactAddress;
|
||||||
|
UpdateAllocationSearchCursor(
|
||||||
|
desiredAddress,
|
||||||
|
effectiveAlignment,
|
||||||
|
executable,
|
||||||
|
exactAddress + alignedSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Over-allocate by the alignment so a kernel-chosen placement
|
||||||
|
// always contains an aligned start; the unused head/tail stays
|
||||||
|
// part of the tracked region and is simply never handed out.
|
||||||
|
var reserveSize = effectiveAlignment > PageSize
|
||||||
|
? alignedSize + effectiveAlignment
|
||||||
|
: alignedSize;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var posixAddress = AllocateAt(cursor, reserveSize, executable, allowAlternative: true);
|
||||||
|
if (posixAddress != 0)
|
||||||
|
{
|
||||||
|
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
||||||
|
const ulong gpuAddressLimit = 1UL << 40;
|
||||||
|
if (alignedBase < gpuAddressLimit &&
|
||||||
|
alignedSize <= gpuAddressLimit - alignedBase &&
|
||||||
|
alignedBase + alignedSize <= posixAddress + reserveSize)
|
||||||
|
{
|
||||||
|
actualAddress = alignedBase;
|
||||||
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseUntrackedAllocation(posixAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||||
{
|
{
|
||||||
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
|
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
|
||||||
@@ -283,6 +348,28 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReleaseUntrackedAllocation(ulong address)
|
||||||
|
{
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
if (_regions[i].VirtualAddress == address)
|
||||||
|
{
|
||||||
|
_regions.RemoveAt(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hostMemory.Free(address);
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
{
|
{
|
||||||
address = 0;
|
address = 0;
|
||||||
|
|||||||
@@ -36,6 +36,23 @@
|
|||||||
"Ultz.Native.GLFW": "3.4.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": {
|
"Silk.NET.Maths": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.23.0",
|
"resolved": "2.23.0",
|
||||||
@@ -74,6 +91,7 @@
|
|||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Input": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
@@ -83,6 +101,16 @@
|
|||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
"Library.Loading": "Carregando biblioteca…",
|
"Library.Loading": "Carregando biblioteca…",
|
||||||
|
|
||||||
"Options.General": "Opções Gerais",
|
"Options.General": "Opções Gerais",
|
||||||
|
"Options.Env.Tab": "Ambiente",
|
||||||
|
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||||
|
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
|
||||||
|
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||||
|
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||||
"Options.Section.Emulation": "EMULAÇÃO",
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "LOGS",
|
"Options.Section.Logging": "LOGS",
|
||||||
"Options.Section.Launcher": "INICIALIZADOR",
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Hungarian",
|
||||||
|
|
||||||
|
"Page.Library": "Könyvtár",
|
||||||
|
"Page.Options": "Beállítások",
|
||||||
|
"Page.GameCount.One": "1 játék",
|
||||||
|
"Page.GameCount.Other": "{0} játékok",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Keresés a könyvtárban",
|
||||||
|
"Library.AddFolder": "+ Mappa hozzáadása",
|
||||||
|
"Library.Rescan": "⟳ Újrakeresés",
|
||||||
|
"Library.OpenFile": "Fájl megnyitása…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Inditás",
|
||||||
|
"Library.Context.OpenFolder": "Játékmappa megnyitása",
|
||||||
|
"Library.Context.CopyPath": "Elérési út másolása",
|
||||||
|
"Library.Context.CopyTitleId": "Cím ID másolása",
|
||||||
|
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "A könyvtárad üres",
|
||||||
|
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
|
||||||
|
"Library.Empty.SearchTitle": "Nincs találat a elemre",
|
||||||
|
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
|
||||||
|
"Library.Empty.AddFolder": "+ Játékmappa hozzáadása",
|
||||||
|
|
||||||
|
"Library.Loading": "Könyvtár betöltése",
|
||||||
|
|
||||||
|
"Options.General": "Általános",
|
||||||
|
"Options.Env.Tab": "Környezet",
|
||||||
|
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
|
||||||
|
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
|
||||||
|
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
|
||||||
|
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
|
||||||
|
"Options.Section.Emulation": "EMULÁCIÓ",
|
||||||
|
"Options.Section.Logging": "LOGOLÁS",
|
||||||
|
"Options.Section.Launcher": "INDITÓ",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU motor",
|
||||||
|
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
|
||||||
|
"Options.CpuEngine.Native": "Natív",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Szigorú dynlib felbontás",
|
||||||
|
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Naplózási szint",
|
||||||
|
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Információ",
|
||||||
|
"Options.LogLevel.Warning": "Figyelmeztetés",
|
||||||
|
"Options.LogLevel.Error": "Hiba",
|
||||||
|
"Options.LogLevel.Critical": "Kritikus",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Import trace limit",
|
||||||
|
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Naplozás fájlba",
|
||||||
|
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Naplófájl elérési útja",
|
||||||
|
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
|
||||||
|
"Options.LogFilePath.Select": "Kiválasztás…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
|
||||||
|
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Címzene",
|
||||||
|
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord jelenlét",
|
||||||
|
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulátor nyelve",
|
||||||
|
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
|
||||||
|
|
||||||
|
"Common.On": "Be",
|
||||||
|
"Common.Off": "Ki",
|
||||||
|
|
||||||
|
"Console.Title": "KONZOL",
|
||||||
|
"Console.SearchWatermark": "Keresés...",
|
||||||
|
"Console.AutoScroll": "Automatikus görgetés",
|
||||||
|
"Console.Split": "Felosztás",
|
||||||
|
"Console.Copy": "Másolás",
|
||||||
|
"Console.Clear": "Törlés",
|
||||||
|
"Console.WindowTitle": "SharpEmu Konzol",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nincs játék kiválasztva",
|
||||||
|
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
|
||||||
|
"Launch.Idle": "Tétlen",
|
||||||
|
"Launch.Console": "≡ Konzol",
|
||||||
|
"Launch.Launch": "▶ Inditás",
|
||||||
|
"Launch.Stop": "■ Leállítás",
|
||||||
|
"Launch.Running": "Fut — {0}",
|
||||||
|
"Launch.Stopping": "Leállítás…",
|
||||||
|
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
|
||||||
|
"Launch.LogFile": "Naplófájl: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
|
||||||
|
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "nemérvényes argumentumok",
|
||||||
|
"Exit.EbootNotFound": "eboot nem található",
|
||||||
|
"Exit.RuntimeException": "runtime exception",
|
||||||
|
"Exit.EmulationError": "emulációs hiba",
|
||||||
|
"Exit.Unknown": "ismeretlen",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulátor: keresés…",
|
||||||
|
"Status.EmulatorPath": "Emulátor: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
|
||||||
|
"Status.ScanningLibrary": "Könyvtár beolvasása…",
|
||||||
|
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
|
||||||
|
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
|
||||||
|
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
|
||||||
|
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
|
||||||
|
"Status.Running": "Fut {0}",
|
||||||
|
"Status.Stopping": "Leállítás…",
|
||||||
|
"Status.Idle": "Nyugodt",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Út",
|
||||||
|
"Clipboard.TitleId": "Cím ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Játékban {0}",
|
||||||
|
"Discord.Browsing": "Böngéssz a könyvtárban",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
|
||||||
|
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
|
||||||
|
"Dialog.PsExecutables": "PS futtatható fájlok",
|
||||||
|
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
|
||||||
|
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
|
||||||
|
"Dialog.LogFiles": "Naplózási fájlok",
|
||||||
|
|
||||||
|
"Options.About" : "Erről",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
|
||||||
|
"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.GithubButton": "Járulj hozzá GitHubon!",
|
||||||
|
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ using Avalonia.Platform;
|
|||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using SharpEmu.Libs.Pad;
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Windows;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -62,7 +63,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
// Controller navigation state.
|
// Controller navigation state.
|
||||||
private readonly DispatcherTimer _gamepadTimer;
|
private readonly DispatcherTimer _gamepadTimer;
|
||||||
private uint _previousPadButtons;
|
private HostGamepadButtons _previousPadButtons;
|
||||||
private long _navLeftNextAt;
|
private long _navLeftNextAt;
|
||||||
private long _navRightNextAt;
|
private long _navRightNextAt;
|
||||||
private long _navUpNextAt;
|
private long _navUpNextAt;
|
||||||
@@ -151,8 +152,8 @@ public partial class MainWindow : Window
|
|||||||
Opened += async (_, _) => await OnOpenedAsync();
|
Opened += async (_, _) => await OnOpenedAsync();
|
||||||
Closing += (_, _) => OnWindowClosing();
|
Closing += (_, _) => OnWindowClosing();
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
WindowsDualSenseReader.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
WindowsXInputReader.EnsureStarted();
|
||||||
_gamepadTimer = new DispatcherTimer
|
_gamepadTimer = new DispatcherTimer
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromMilliseconds(50),
|
Interval = TimeSpan.FromMilliseconds(50),
|
||||||
@@ -224,9 +225,9 @@ public partial class MainWindow : Window
|
|||||||
private void PollGamepad()
|
private void PollGamepad()
|
||||||
{
|
{
|
||||||
// DualSense wins when both are connected; XInput covers Xbox pads.
|
// DualSense wins when both are connected; XInput covers Xbox pads.
|
||||||
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
|
if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
|
||||||
{
|
{
|
||||||
_previousPadButtons = 0;
|
_previousPadButtons = HostGamepadButtons.None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,12 +240,12 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((shoulderPressed & OrbisPadButton.L1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||||
{
|
{
|
||||||
SetActivePage(0);
|
SetActivePage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((shoulderPressed & OrbisPadButton.R1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.R1) != 0)
|
||||||
{
|
{
|
||||||
SetActivePage(1);
|
SetActivePage(1);
|
||||||
}
|
}
|
||||||
@@ -256,10 +257,10 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var now = Environment.TickCount64;
|
var now = Environment.TickCount64;
|
||||||
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
|
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
|
||||||
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
|
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
|
||||||
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
|
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
|
||||||
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
|
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
|
||||||
|
|
||||||
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
||||||
{
|
{
|
||||||
@@ -282,12 +283,12 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pressed = pad.Buttons & ~_previousPadButtons;
|
var pressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((pressed & 0x4000) != 0) // Cross
|
if ((pressed & HostGamepadButtons.Cross) != 0)
|
||||||
{
|
{
|
||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((pressed & 0x2000) != 0) // Circle
|
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||||
{
|
{
|
||||||
StopEmulator();
|
StopEmulator();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
the executable is started without arguments. -->
|
the executable is started without arguments. -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<Version>0.0.1</Version>
|
<!-- Required by the source-generated LibraryImport stubs in the linked
|
||||||
|
controller readers below. -->
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
||||||
@@ -41,14 +43,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
|
|
||||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||||
with the emulator's pad HLE. They are dependency-free, so they are
|
with the emulator's host input backend. They are dependency-free, so
|
||||||
compiled in directly rather than pulling a reference to all of
|
they are compiled in directly rather than pulling a reference to all
|
||||||
SharpEmu.Libs into the launcher. -->
|
of SharpEmu.HLE into the launcher. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
|
||||||
|
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
|
||||||
|
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum HostGamepadButtons : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Up = 1 << 0,
|
||||||
|
Down = 1 << 1,
|
||||||
|
Left = 1 << 2,
|
||||||
|
Right = 1 << 3,
|
||||||
|
Cross = 1 << 4,
|
||||||
|
Circle = 1 << 5,
|
||||||
|
Square = 1 << 6,
|
||||||
|
Triangle = 1 << 7,
|
||||||
|
L1 = 1 << 8,
|
||||||
|
R1 = 1 << 9,
|
||||||
|
L2 = 1 << 10,
|
||||||
|
R2 = 1 << 11,
|
||||||
|
L3 = 1 << 12,
|
||||||
|
R3 = 1 << 13,
|
||||||
|
Options = 1 << 14,
|
||||||
|
TouchPad = 1 << 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
|
||||||
|
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
|
||||||
|
/// snapshot buffers.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostGamepadState(
|
||||||
|
bool Connected,
|
||||||
|
HostGamepadButtons Buttons,
|
||||||
|
byte LeftX,
|
||||||
|
byte LeftY,
|
||||||
|
byte RightX,
|
||||||
|
byte RightY,
|
||||||
|
byte LeftTrigger,
|
||||||
|
byte RightTrigger);
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
using SharpEmu.HLE.Host.Windows;
|
using SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
namespace SharpEmu.HLE.Host;
|
namespace SharpEmu.HLE.Host;
|
||||||
@@ -28,7 +29,14 @@ public static class HostPlatform
|
|||||||
return new WindowsHostPlatform();
|
return new WindowsHostPlatform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) &&
|
||||||
|
RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return new PosixHostPlatform();
|
||||||
|
}
|
||||||
|
|
||||||
throw new PlatformNotSupportedException(
|
throw new PlatformNotSupportedException(
|
||||||
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
|
"SharpEmu native guest execution requires an x86-64 process on Windows, Linux, or macOS. " +
|
||||||
|
"On Apple Silicon, use the osx-x64 build under Rosetta 2.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host audio-output device access. The HLE audio exports convert guest submissions to
|
||||||
|
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
|
||||||
|
/// streams opened here; everything device-specific — queueing, backpressure, native
|
||||||
|
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostAudioOutput
|
||||||
|
{
|
||||||
|
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
|
||||||
|
string BackendName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
|
||||||
|
/// Throws when the host has no usable output device; callers degrade to a silent
|
||||||
|
/// port and pace the guest instead.
|
||||||
|
/// </summary>
|
||||||
|
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
|
||||||
|
/// the sample rate the stream was opened with.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostAudioStream : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Submits one buffer. May block briefly while the device drains its queue (this is
|
||||||
|
/// what paces the guest's audio loop); returns false when the stream cannot accept
|
||||||
|
/// audio, in which case the caller paces the guest itself.
|
||||||
|
/// </summary>
|
||||||
|
bool Submit(ReadOnlySpan<byte> stereoPcm16);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
|
||||||
|
/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
|
||||||
|
/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
|
||||||
|
/// keyboard lives in the HLE pad exports.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostInput
|
||||||
|
{
|
||||||
|
/// <summary>Starts the background device readers once; safe to call repeatedly.</summary>
|
||||||
|
void EnsureStarted();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills <paramref name="destination"/> with snapshots of currently connected
|
||||||
|
/// gamepads and returns how many were written (0 when none are connected).
|
||||||
|
/// </summary>
|
||||||
|
int GetGamepadStates(Span<HostGamepadState> destination);
|
||||||
|
|
||||||
|
/// <summary>Human-readable name of the first connected gamepad, or null.</summary>
|
||||||
|
string? DescribeConnectedGamepad();
|
||||||
|
|
||||||
|
/// <summary>Sets rumble on all connected gamepads; large = strong/left motor.</summary>
|
||||||
|
void SetRumble(byte largeMotor, byte smallMotor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Approximates per-trigger vibration on gamepads without independent trigger
|
||||||
|
/// actuators; null leaves that trigger's current value unchanged.
|
||||||
|
/// </summary>
|
||||||
|
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
|
||||||
|
|
||||||
|
void SetLightbar(byte red, byte green, byte blue);
|
||||||
|
|
||||||
|
void ResetLightbar();
|
||||||
|
|
||||||
|
/// <summary>True when a window of this process has keyboard focus.</summary>
|
||||||
|
bool IsHostWindowFocused();
|
||||||
|
|
||||||
|
/// <summary>Windows virtual-key code semantics; other backends translate.</summary>
|
||||||
|
bool IsKeyDown(int virtualKey);
|
||||||
|
}
|
||||||
@@ -16,4 +16,8 @@ public interface IHostPlatform
|
|||||||
IHostThreading Threading { get; }
|
IHostThreading Threading { get; }
|
||||||
|
|
||||||
IHostSymbolResolver Symbols { get; }
|
IHostSymbolResolver Symbols { get; }
|
||||||
|
|
||||||
|
IHostAudioOutput Audio { get; }
|
||||||
|
|
||||||
|
IHostInput Input { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ public interface IHostThreading
|
|||||||
|
|
||||||
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the OS for ~1 ms timed-wait granularity for the life of the process
|
||||||
|
/// (idempotent; best-effort). No-op on platforms whose default is already fine.
|
||||||
|
/// </summary>
|
||||||
|
void RequestTimerResolution();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||||
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ALSA-based playback for Linux. The PCM device is opened in blocking mode
|
||||||
|
/// with a device buffer sized to match the 32KB queue the other backends
|
||||||
|
/// keep, so snd_pcm_writei itself provides the backpressure pacing. The
|
||||||
|
/// "default" device routes through PulseAudio/PipeWire on desktops and to
|
||||||
|
/// the hardware on bare ALSA setups; SHARPEMU_ALSA_DEVICE overrides it.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe class PosixAlsaAudioStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
// 32KB of stereo PCM16 at 48kHz is ~170ms; keep the same device-side
|
||||||
|
// queue depth the WinMM/CoreAudio ports enforce in managed code.
|
||||||
|
private const uint DeviceLatencyMicroseconds = 170_000;
|
||||||
|
private const int StreamPlayback = 0;
|
||||||
|
private const int FormatS16LittleEndian = 2;
|
||||||
|
private const int AccessReadWriteInterleaved = 3;
|
||||||
|
private const int ErrorPipe = -32; // -EPIPE, underrun
|
||||||
|
private const int ErrorStreamPipe = -86; // -ESTRPIPE, suspended
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private nint _pcm;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public PosixAlsaAudioStream(uint sampleRate)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("ALSA audio is only available on Linux.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var device = Environment.GetEnvironmentVariable("SHARPEMU_ALSA_DEVICE");
|
||||||
|
if (string.IsNullOrWhiteSpace(device))
|
||||||
|
{
|
||||||
|
device = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = snd_pcm_open(out _pcm, device, StreamPlayback, 0);
|
||||||
|
if (status != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"snd_pcm_open(\"{device}\") failed: {DescribeError(status)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
status = snd_pcm_set_params(
|
||||||
|
_pcm,
|
||||||
|
FormatS16LittleEndian,
|
||||||
|
AccessReadWriteInterleaved,
|
||||||
|
2,
|
||||||
|
sampleRate,
|
||||||
|
1,
|
||||||
|
DeviceLatencyMicroseconds);
|
||||||
|
if (status != 0)
|
||||||
|
{
|
||||||
|
_ = snd_pcm_close(_pcm);
|
||||||
|
_pcm = 0;
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"snd_pcm_set_params({sampleRate} Hz) failed: {DescribeError(status)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WritePcm(stereoPcm16, (uint)(stereoPcm16.Length / 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_pcm != 0)
|
||||||
|
{
|
||||||
|
_ = snd_pcm_drop(_pcm);
|
||||||
|
_ = snd_pcm_close(_pcm);
|
||||||
|
_pcm = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool WritePcm(ReadOnlySpan<byte> pcm, uint frames)
|
||||||
|
{
|
||||||
|
var recovered = false;
|
||||||
|
fixed (byte* data = pcm)
|
||||||
|
{
|
||||||
|
var offset = 0L;
|
||||||
|
while (offset < frames)
|
||||||
|
{
|
||||||
|
var written = snd_pcm_writei(
|
||||||
|
_pcm,
|
||||||
|
data + (offset * 4),
|
||||||
|
(nuint)(frames - offset));
|
||||||
|
if (written >= 0)
|
||||||
|
{
|
||||||
|
offset += written;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One recovery attempt per submit covers underruns (-EPIPE)
|
||||||
|
// and suspend/resume (-ESTRPIPE); anything else, or a second
|
||||||
|
// failure, drops the buffer rather than stalling the guest.
|
||||||
|
if (recovered ||
|
||||||
|
(written != ErrorPipe && written != ErrorStreamPipe) ||
|
||||||
|
snd_pcm_recover(_pcm, (int)written, 1) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
recovered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DescribeError(long status)
|
||||||
|
{
|
||||||
|
var message = Marshal.PtrToStringUTF8(snd_strerror((int)status));
|
||||||
|
return $"{message ?? "unknown error"} ({status})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string Alsa = "libasound.so.2";
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern int snd_pcm_open(
|
||||||
|
out nint pcm,
|
||||||
|
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
|
||||||
|
int stream,
|
||||||
|
int mode);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern int snd_pcm_set_params(
|
||||||
|
nint pcm,
|
||||||
|
int format,
|
||||||
|
int access,
|
||||||
|
uint channels,
|
||||||
|
uint rate,
|
||||||
|
int softResample,
|
||||||
|
uint latencyUs);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern long snd_pcm_writei(nint pcm, byte* buffer, nuint frames);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern int snd_pcm_recover(nint pcm, int error, int silent);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern int snd_pcm_drop(nint pcm);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern int snd_pcm_close(nint pcm);
|
||||||
|
|
||||||
|
[DllImport(Alsa)]
|
||||||
|
private static extern nint snd_strerror(int error);
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AudioQueue-based playback for macOS. Buffers are enqueued as stereo PCM16
|
||||||
|
/// and returned by the queue's internal thread through the output callback;
|
||||||
|
/// Submit applies the same 32KB backpressure the WinMM backend uses so guest
|
||||||
|
/// pacing works identically.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe class PosixCoreAudioStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||||
|
private const uint FormatLinearPcm = 0x6C70636D; // 'lpcm'
|
||||||
|
private const uint FlagIsSignedInteger = 0x4;
|
||||||
|
private const uint FlagIsPacked = 0x8;
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly AutoResetEvent _completion = new(false);
|
||||||
|
private readonly Queue<nint> _freeBuffers = new();
|
||||||
|
private GCHandle _selfHandle;
|
||||||
|
private nint _queue;
|
||||||
|
private int _queuedPcmBytes;
|
||||||
|
private bool _started;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public PosixCoreAudioStream(uint sampleRate)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("CoreAudio is only available on macOS.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = new AudioStreamBasicDescription
|
||||||
|
{
|
||||||
|
SampleRate = sampleRate,
|
||||||
|
FormatId = FormatLinearPcm,
|
||||||
|
FormatFlags = FlagIsSignedInteger | FlagIsPacked,
|
||||||
|
BytesPerPacket = 4,
|
||||||
|
FramesPerPacket = 1,
|
||||||
|
BytesPerFrame = 4,
|
||||||
|
ChannelsPerFrame = 2,
|
||||||
|
BitsPerChannel = 16,
|
||||||
|
};
|
||||||
|
|
||||||
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
|
var status = AudioQueueNewOutput(
|
||||||
|
&format,
|
||||||
|
&OutputCallback,
|
||||||
|
GCHandle.ToIntPtr(_selfHandle),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
out _queue);
|
||||||
|
if (status != 0)
|
||||||
|
{
|
||||||
|
_selfHandle.Free();
|
||||||
|
throw new InvalidOperationException($"AudioQueueNewOutput failed with OSStatus {status}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _queue == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputLength = stereoPcm16.Length;
|
||||||
|
while (_queuedPcmBytes != 0 &&
|
||||||
|
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
||||||
|
{
|
||||||
|
Monitor.Exit(_gate);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Dispose can free the event while this thread waits
|
||||||
|
// outside the gate; treat that like a timed-out wait.
|
||||||
|
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Enter(_gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryTakeBuffer(outputLength, out var buffer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var audioData = ((AudioQueueBuffer*)buffer)->AudioData;
|
||||||
|
stereoPcm16.CopyTo(new Span<byte>(audioData, outputLength));
|
||||||
|
|
||||||
|
((AudioQueueBuffer*)buffer)->AudioDataByteSize = (uint)outputLength;
|
||||||
|
if (AudioQueueEnqueueBuffer(_queue, buffer, 0, 0) != 0)
|
||||||
|
{
|
||||||
|
_freeBuffers.Enqueue(buffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedPcmBytes += outputLength;
|
||||||
|
if (!_started)
|
||||||
|
{
|
||||||
|
if (AudioQueueStart(_queue, 0) != 0)
|
||||||
|
{
|
||||||
|
// A queue that never starts never drains, so later
|
||||||
|
// submits would block on backpressure until their
|
||||||
|
// timeout. Tear the queue down and fail fast instead.
|
||||||
|
_ = AudioQueueDispose(_queue, true);
|
||||||
|
_queue = 0;
|
||||||
|
_queuedPcmBytes = 0;
|
||||||
|
_freeBuffers.Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_queue != 0)
|
||||||
|
{
|
||||||
|
// Synchronous dispose stops the queue, frees its buffers, and
|
||||||
|
// guarantees no further callbacks reference this instance.
|
||||||
|
_ = AudioQueueDispose(_queue, true);
|
||||||
|
_queue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_freeBuffers.Clear();
|
||||||
|
// Wake any submitter waiting on backpressure before the event
|
||||||
|
// goes away; a late waiter observes ObjectDisposedException and
|
||||||
|
// bails out in Submit.
|
||||||
|
_completion.Set();
|
||||||
|
_completion.Dispose();
|
||||||
|
if (_selfHandle.IsAllocated)
|
||||||
|
{
|
||||||
|
_selfHandle.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryTakeBuffer(int length, out nint buffer)
|
||||||
|
{
|
||||||
|
while (_freeBuffers.TryDequeue(out buffer))
|
||||||
|
{
|
||||||
|
if (((AudioQueueBuffer*)buffer)->AudioDataBytesCapacity >= (uint)length)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = AudioQueueFreeBuffer(_queue, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AudioQueueAllocateBuffer(_queue, (uint)length, out buffer) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly]
|
||||||
|
private static void OutputCallback(nint userData, nint queue, nint buffer)
|
||||||
|
{
|
||||||
|
if (GCHandle.FromIntPtr(userData).Target is not PosixCoreAudioStream port)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (port._gate)
|
||||||
|
{
|
||||||
|
if (port._disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
port._queuedPcmBytes -= checked((int)((AudioQueueBuffer*)buffer)->AudioDataByteSize);
|
||||||
|
port._freeBuffers.Enqueue(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
port._completion.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct AudioStreamBasicDescription
|
||||||
|
{
|
||||||
|
public double SampleRate;
|
||||||
|
public uint FormatId;
|
||||||
|
public uint FormatFlags;
|
||||||
|
public uint BytesPerPacket;
|
||||||
|
public uint FramesPerPacket;
|
||||||
|
public uint BytesPerFrame;
|
||||||
|
public uint ChannelsPerFrame;
|
||||||
|
public uint BitsPerChannel;
|
||||||
|
public uint Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct AudioQueueBuffer
|
||||||
|
{
|
||||||
|
public uint AudioDataBytesCapacity;
|
||||||
|
public void* AudioData;
|
||||||
|
public uint AudioDataByteSize;
|
||||||
|
public nint UserData;
|
||||||
|
public uint PacketDescriptionCapacity;
|
||||||
|
public nint PacketDescriptions;
|
||||||
|
public uint PacketDescriptionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string AudioToolbox =
|
||||||
|
"/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox";
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueNewOutput(
|
||||||
|
AudioStreamBasicDescription* format,
|
||||||
|
delegate* unmanaged<nint, nint, nint, void> callback,
|
||||||
|
nint userData,
|
||||||
|
nint callbackRunLoop,
|
||||||
|
nint runLoopMode,
|
||||||
|
uint flags,
|
||||||
|
out nint queue);
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueAllocateBuffer(nint queue, uint bufferByteSize, out nint buffer);
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueFreeBuffer(nint queue, nint buffer);
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueEnqueueBuffer(nint queue, nint buffer, uint packetDescriptionCount, nint packetDescriptions);
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueStart(nint queue, nint startTime);
|
||||||
|
|
||||||
|
[DllImport(AudioToolbox)]
|
||||||
|
private static extern int AudioQueueDispose(nint queue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX audio output: CoreAudio (AudioQueue) on macOS, ALSA on Linux. Both
|
||||||
|
/// streams accept the seam's interleaved stereo PCM16 and pace the guest via
|
||||||
|
/// device-queue backpressure.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PosixHostAudio : IHostAudioOutput
|
||||||
|
{
|
||||||
|
public string BackendName => OperatingSystem.IsMacOS() ? "coreaudio" : "alsa";
|
||||||
|
|
||||||
|
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? new PosixCoreAudioStream(sampleRate)
|
||||||
|
: new PosixAlsaAudioStream(sampleRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bridges a window-provided input source into the host input seam. POSIX
|
||||||
|
/// hosts have no user32/XInput/raw-HID readers; keyboard and gamepad state
|
||||||
|
/// come from the presenter's GLFW window instead, which registers itself via
|
||||||
|
/// <see cref="SetSource"/> once the window exists. Until then (and with no
|
||||||
|
/// window at all, e.g. headless runs) every query reports neutral input.
|
||||||
|
/// Rumble and lightbar are unsupported by the GLFW input layer and no-op.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPosixWindowInputSource
|
||||||
|
{
|
||||||
|
/// <summary>True while the window's keyboard is delivering events.</summary>
|
||||||
|
bool HasKeyboardFocus { get; }
|
||||||
|
|
||||||
|
/// <summary>Windows virtual-key semantics; the source translates.</summary>
|
||||||
|
bool IsKeyDown(int virtualKey);
|
||||||
|
|
||||||
|
/// <summary>Same contract as <see cref="IHostInput.GetGamepadStates"/>.</summary>
|
||||||
|
int GetGamepadStates(Span<HostGamepadState> destination);
|
||||||
|
|
||||||
|
string? DescribeConnectedGamepad();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public so the presenter's window layer (SharpEmu.Libs) can register its
|
||||||
|
// input source; the platform still constructs the singleton itself.
|
||||||
|
public sealed class PosixHostInput : IHostInput
|
||||||
|
{
|
||||||
|
private static volatile IPosixWindowInputSource? _source;
|
||||||
|
|
||||||
|
/// <summary>Called by the presenter's window layer when input is ready.</summary>
|
||||||
|
public static void SetSource(IPosixWindowInputSource source)
|
||||||
|
{
|
||||||
|
_source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureStarted()
|
||||||
|
{
|
||||||
|
// Device readers are event-driven off the window thread; nothing to start.
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||||
|
{
|
||||||
|
return _source?.GetGamepadStates(destination) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? DescribeConnectedGamepad() => _source?.DescribeConnectedGamepad();
|
||||||
|
|
||||||
|
public void SetRumble(byte largeMotor, byte smallMotor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLightbar(byte red, byte green, byte blue)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetLightbar()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsHostWindowFocused()
|
||||||
|
{
|
||||||
|
// GLFW only delivers key events to the focused window, so a
|
||||||
|
// delivering keyboard implies focus.
|
||||||
|
return _source?.HasKeyboardFocus ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKeyDown(int virtualKey)
|
||||||
|
{
|
||||||
|
return _source?.IsKeyDown(virtualKey) ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX virtual memory backend implemented over mmap/mprotect/munmap with a
|
||||||
|
/// shadow region table that answers VirtualQuery-style questions and tracks
|
||||||
|
/// page protections.
|
||||||
|
/// POSIX anonymous mappings are demand-paged by the kernel, so Win32
|
||||||
|
/// "reserve-only" regions are mapped as committed memory directly and
|
||||||
|
/// commit requests become protection changes.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
|
private const uint MEM_RESERVE = 0x2000;
|
||||||
|
private const uint MEM_RELEASE = 0x8000;
|
||||||
|
private const uint MEM_FREE_STATE = 0x10000;
|
||||||
|
private const uint MEM_PRIVATE = 0x20000;
|
||||||
|
|
||||||
|
private const uint PAGE_NOACCESS = 0x01;
|
||||||
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
|
||||||
|
private const ulong PageSize = 0x1000;
|
||||||
|
|
||||||
|
private struct BasicInfo
|
||||||
|
{
|
||||||
|
public ulong BaseAddress;
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)Posix.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
MEM_COMMIT | MEM_RESERVE,
|
||||||
|
ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)Posix.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
MEM_RESERVE,
|
||||||
|
ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return Posix.Alloc(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
MEM_COMMIT,
|
||||||
|
ToNativeProtection(protection)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Free(ulong address)
|
||||||
|
{
|
||||||
|
return Posix.Free((void*)address, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Protect(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
HostPageProtection protection,
|
||||||
|
out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return Posix.Protect(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
ToNativeProtection(protection),
|
||||||
|
out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProtectRaw(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
uint rawProtection,
|
||||||
|
out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return Posix.Protect(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
rawProtection,
|
||||||
|
out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
if (Posix.Query((void*)address, out var nativeInfo) == 0)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
nativeInfo.BaseAddress,
|
||||||
|
nativeInfo.AllocationBase,
|
||||||
|
nativeInfo.RegionSize,
|
||||||
|
nativeInfo.State switch
|
||||||
|
{
|
||||||
|
MEM_COMMIT => HostRegionState.Committed,
|
||||||
|
MEM_RESERVE => HostRegionState.Reserved,
|
||||||
|
_ => HostRegionState.Free,
|
||||||
|
},
|
||||||
|
nativeInfo.State,
|
||||||
|
ToHostProtection(nativeInfo.Protect),
|
||||||
|
nativeInfo.Protect,
|
||||||
|
nativeInfo.AllocationProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
_ = address;
|
||||||
|
_ = size;
|
||||||
|
// The supported POSIX process is x86-64 (including Rosetta 2), whose
|
||||||
|
// instruction cache is coherent. A future arm64 backend must call the
|
||||||
|
// platform instruction-cache invalidation API here.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
|
||||||
|
{
|
||||||
|
HostPageProtection.NoAccess => PAGE_NOACCESS,
|
||||||
|
HostPageProtection.ReadOnly => PAGE_READONLY,
|
||||||
|
HostPageProtection.ReadWrite => PAGE_READWRITE,
|
||||||
|
HostPageProtection.Execute => PAGE_EXECUTE,
|
||||||
|
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
|
||||||
|
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
|
||||||
|
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_READWRITE,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostPageProtection ToHostProtection(uint protection) => protection switch
|
||||||
|
{
|
||||||
|
PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||||
|
PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||||
|
PAGE_EXECUTE => HostPageProtection.Execute,
|
||||||
|
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||||
|
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||||
|
_ => HostPageProtection.NoAccess,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static class Posix
|
||||||
|
{
|
||||||
|
private const int PROT_NONE = 0x0;
|
||||||
|
private const int PROT_READ = 0x1;
|
||||||
|
private const int PROT_WRITE = 0x2;
|
||||||
|
private const int PROT_EXEC = 0x4;
|
||||||
|
|
||||||
|
private const int MAP_PRIVATE = 0x02;
|
||||||
|
private const int MAP_FIXED = 0x10;
|
||||||
|
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||||
|
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||||
|
|
||||||
|
// Linux-only: fail instead of clobbering an existing mapping.
|
||||||
|
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||||
|
|
||||||
|
private static readonly nint MAP_FAILED = -1;
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly SortedList<ulong, Region> Regions = new();
|
||||||
|
|
||||||
|
private sealed class Region
|
||||||
|
{
|
||||||
|
public ulong Base;
|
||||||
|
public ulong Size;
|
||||||
|
public uint DefaultProtect;
|
||||||
|
public Dictionary<ulong, uint>? PageProtects;
|
||||||
|
|
||||||
|
public ulong End => Base + Size;
|
||||||
|
|
||||||
|
public uint ProtectAt(ulong pageAddress)
|
||||||
|
{
|
||||||
|
if (PageProtects is not null && PageProtects.TryGetValue(pageAddress, out var overriden))
|
||||||
|
{
|
||||||
|
return overriden;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultProtect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedSize = AlignUp((ulong)size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (allocationType == MEM_COMMIT && address != null &&
|
||||||
|
TryFindRegionLocked((ulong)address, out var existing))
|
||||||
|
{
|
||||||
|
// Note: MEM_RESERVE requests that overlap an existing
|
||||||
|
// region must fail like Win32 does; only a pure commit
|
||||||
|
// may target pages inside a tracked mapping.
|
||||||
|
// Commit inside an existing mapping: the pages are already
|
||||||
|
// backed (demand paged), so only apply the protection.
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = AlignUp((ulong)address + alignedSize, PageSize);
|
||||||
|
if (end <= start || end > existing.End)
|
||||||
|
{
|
||||||
|
// Win32 fails a commit that runs past its reservation
|
||||||
|
// instead of committing a prefix; committing partially
|
||||||
|
// here would let callers believe the whole range is
|
||||||
|
// usable.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(protect)) != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(existing, start, end - start, protect);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((allocationType & MEM_RESERVE) == 0)
|
||||||
|
{
|
||||||
|
// MEM_COMMIT alone outside any known region is invalid here.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var posixProtect = ToPosixProtect(protect);
|
||||||
|
var flags = MAP_PRIVATE | MAP_ANON;
|
||||||
|
if ((allocationType & MEM_COMMIT) == 0)
|
||||||
|
{
|
||||||
|
// Reserve-only: keep the requested protection so the region
|
||||||
|
// is usable without a separate commit step, but tell the
|
||||||
|
// kernel not to account swap for it where supported.
|
||||||
|
flags |= MAP_NORESERVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
nint result;
|
||||||
|
if (address != null)
|
||||||
|
{
|
||||||
|
// Win32 maps at exactly the requested address or fails
|
||||||
|
// without touching existing mappings. Fail up front on
|
||||||
|
// any overlap we track, then place the mapping: Linux
|
||||||
|
// gets MAP_FIXED_NOREPLACE (fails cleanly on host
|
||||||
|
// mappings too). Darwin lacks NOREPLACE and plain
|
||||||
|
// MAP_FIXED would silently clobber untracked host
|
||||||
|
// memory (dyld, the runtime's JIT heap, Rosetta), so
|
||||||
|
// pass the address as a hint instead -- the kernel
|
||||||
|
// honors it when the range is free and relocates the
|
||||||
|
// mapping otherwise, which we treat as failure.
|
||||||
|
if (OverlapsTrackedRegionLocked((ulong)address, alignedSize))
|
||||||
|
{
|
||||||
|
Trace($"exact overlap: addr=0x{(ulong)address:X16} size=0x{alignedSize:X}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
|
||||||
|
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
|
||||||
|
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||||
|
{
|
||||||
|
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
if (result != MAP_FAILED)
|
||||||
|
{
|
||||||
|
munmap(result, (nuint)alignedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = mmap(0, (nuint)alignedSize, posixProtect, flags, -1, 0);
|
||||||
|
if (result == MAP_FAILED)
|
||||||
|
{
|
||||||
|
Trace($"mmap failed: size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions[(ulong)result] = new Region
|
||||||
|
{
|
||||||
|
Base = (ulong)result,
|
||||||
|
Size = alignedSize,
|
||||||
|
DefaultProtect = protect
|
||||||
|
};
|
||||||
|
|
||||||
|
return (void*)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Free(void* address, nuint size, uint freeType)
|
||||||
|
{
|
||||||
|
_ = size;
|
||||||
|
_ = freeType;
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!Regions.TryGetValue((ulong)address, out var region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions.Remove((ulong)address);
|
||||||
|
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||||
|
{
|
||||||
|
oldProtect = PAGE_NOACCESS;
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = AlignUp((ulong)address + size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!TryFindRegionLocked(start, out var region) || end > region.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldProtect = region.ProtectAt(start);
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(newProtect)) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(region, start, end - start, newProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nuint Query(void* address, out BasicInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (TryFindRegionLocked(pageAddress, out var region))
|
||||||
|
{
|
||||||
|
// Win32 VirtualQuery reports a run of pages sharing the
|
||||||
|
// same protection, so stop the run where it changes.
|
||||||
|
var protect = region.ProtectAt(pageAddress);
|
||||||
|
var runEnd = pageAddress + PageSize;
|
||||||
|
while (runEnd < region.End && region.ProtectAt(runEnd) == protect)
|
||||||
|
{
|
||||||
|
runEnd += PageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = region.Base;
|
||||||
|
info.AllocationProtect = region.DefaultProtect;
|
||||||
|
info.RegionSize = runEnd - pageAddress;
|
||||||
|
info.State = MEM_COMMIT;
|
||||||
|
info.Protect = protect;
|
||||||
|
info.Type = MEM_PRIVATE;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untracked host memory (runtime heaps, stacks, libraries) is
|
||||||
|
// reported as a free block reaching to the next tracked region
|
||||||
|
// so scanning callers keep advancing.
|
||||||
|
var nextBase = ulong.MaxValue;
|
||||||
|
foreach (var regionBase in Regions.Keys)
|
||||||
|
{
|
||||||
|
if (regionBase > pageAddress)
|
||||||
|
{
|
||||||
|
nextBase = regionBase;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = 0;
|
||||||
|
info.AllocationProtect = PAGE_NOACCESS;
|
||||||
|
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
|
||||||
|
info.State = MEM_FREE_STATE;
|
||||||
|
info.Protect = PAGE_NOACCESS;
|
||||||
|
info.Type = 0;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OverlapsTrackedRegionLocked(ulong start, ulong size)
|
||||||
|
{
|
||||||
|
var end = start + size;
|
||||||
|
foreach (var region in Regions.Values)
|
||||||
|
{
|
||||||
|
if (region.Base < end && start < region.End)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||||
|
{
|
||||||
|
region = null!;
|
||||||
|
var keys = Regions.Keys;
|
||||||
|
var low = 0;
|
||||||
|
var high = keys.Count - 1;
|
||||||
|
Region? candidate = null;
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
var middle = low + ((high - low) >> 1);
|
||||||
|
var entry = Regions.Values[middle];
|
||||||
|
if (entry.Base <= address)
|
||||||
|
{
|
||||||
|
candidate = entry;
|
||||||
|
low = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate is null || address >= candidate.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||||
|
{
|
||||||
|
if (start == region.Base && size >= region.Size)
|
||||||
|
{
|
||||||
|
region.DefaultProtect = protect;
|
||||||
|
region.PageProtects = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||||
|
var end = start + size;
|
||||||
|
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||||
|
{
|
||||||
|
if (protect == region.DefaultProtect)
|
||||||
|
{
|
||||||
|
region.PageProtects.Remove(pageAddress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
region.PageProtects[pageAddress] = protect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToPosixProtect(uint win32Protect)
|
||||||
|
{
|
||||||
|
return win32Protect switch
|
||||||
|
{
|
||||||
|
PAGE_NOACCESS => PROT_NONE,
|
||||||
|
PAGE_READONLY => PROT_READ,
|
||||||
|
PAGE_READWRITE => PROT_READ | PROT_WRITE,
|
||||||
|
PAGE_EXECUTE => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READ => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READWRITE => PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||||
|
_ => PROT_READ | PROT_WRITE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[HOSTMEM] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment) => checked((value + alignment - 1) & ~(alignment - 1));
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int munmap(nint addr, nuint length);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
internal sealed class PosixHostPlatform : IHostPlatform
|
||||||
|
{
|
||||||
|
public IHostMemory Memory { get; } = new PosixHostMemory();
|
||||||
|
|
||||||
|
public IHostThreading Threading { get; } = new PosixHostThreading();
|
||||||
|
|
||||||
|
public IHostSymbolResolver Symbols { get; } = new PosixHostSymbolResolver();
|
||||||
|
|
||||||
|
public IHostAudioOutput Audio { get; } = new PosixHostAudio();
|
||||||
|
|
||||||
|
public IHostInput Input { get; } = new PosixHostInput();
|
||||||
|
}
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX replacements for the kernel32 helpers the native backend embeds in
|
||||||
|
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
|
||||||
|
/// convention the emitted call sites were written for (first argument in
|
||||||
|
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
|
||||||
|
/// emission code stays identical across platforms.
|
||||||
|
/// </summary>
|
||||||
|
internal static unsafe class PosixHostStubs
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static bool _initialized;
|
||||||
|
private static nint _tlsGetValueStub;
|
||||||
|
private static nint _queryPerformanceCounterStub;
|
||||||
|
private static nint _switchToThreadStub;
|
||||||
|
private static nint _sleepStub;
|
||||||
|
private static nint _waitForSingleObjectStub;
|
||||||
|
private static nint _setEventStub;
|
||||||
|
private static nint _exitThreadStub;
|
||||||
|
|
||||||
|
public static nint TlsGetValueStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _tlsGetValueStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint QueryPerformanceCounterStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SwitchToThreadStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _switchToThreadStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SleepStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _sleepStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Win64-convention replacements for the kernel32 event/thread helpers the
|
||||||
|
/// native guest worker loop embeds. The "handle" they take is a worker
|
||||||
|
/// event created by <see cref="CreateWorkerEvent"/>: a dispatch semaphore
|
||||||
|
/// on macOS, an unnamed POSIX semaphore on Linux. The wait stub always
|
||||||
|
/// waits forever (the worker loop passes INFINITE) and retries EINTR.
|
||||||
|
/// </summary>
|
||||||
|
public static nint WaitForSingleObjectStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _waitForSingleObjectStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SetEventStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _setEventStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint ExitThreadStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _exitThreadStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a binary-semaphore worker event signalable/waitable both from
|
||||||
|
/// managed code and from emitted native code (via the stub addresses
|
||||||
|
/// above). Returns 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
public static nint CreateWorkerEvent()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_create(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var semaphore = Marshal.AllocHGlobal(64);
|
||||||
|
if (sem_init(semaphore, 0, 0) != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(semaphore);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return semaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SignalWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
_ = dispatch_semaphore_signal(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sem_post(handle) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Waits for a worker event; a negative timeout waits forever.</summary>
|
||||||
|
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
|
||||||
|
return dispatch_semaphore_wait(handle, deadline) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
while (sem_wait(handle) != 0)
|
||||||
|
{
|
||||||
|
// EINTR: retry.
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
||||||
|
while (sem_trywait(handle) != 0)
|
||||||
|
{
|
||||||
|
if (Environment.TickCount64 >= deadlineTicks)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DestroyWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
dispatch_release(handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = sem_destroy(handle);
|
||||||
|
Marshal.FreeHGlobal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts a raw pthread at a native entry point (pthread entries take their
|
||||||
|
/// argument in RDI; the worker loop stub ignores it). Returns an opaque
|
||||||
|
/// handle for <see cref="WaitForWorkerThreadExit"/>/<see cref="CloseWorkerThreadHandle"/>,
|
||||||
|
/// or 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
public static nint CreateWorkerThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
|
||||||
|
{
|
||||||
|
threadId = 0;
|
||||||
|
byte* attr = stackalloc byte[512];
|
||||||
|
if (pthread_attr_init(attr) != 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (stackReserveBytes != 0)
|
||||||
|
{
|
||||||
|
_ = pthread_attr_setstacksize(attr, nuint.Max(stackReserveBytes, 512 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
nint thread;
|
||||||
|
if (pthread_create(&thread, attr, entry, parameter) != 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
ulong numericId;
|
||||||
|
if (pthread_threadid_np(thread, &numericId) == 0)
|
||||||
|
{
|
||||||
|
threadId = unchecked((uint)numericId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
threadId = unchecked((uint)thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
var holder = (nint*)Marshal.AllocHGlobal(sizeof(nint) * 2);
|
||||||
|
holder[0] = thread;
|
||||||
|
holder[1] = 0; // joined flag
|
||||||
|
return (nint)holder;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ = pthread_attr_destroy(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for a worker thread to exit. Liveness is probed with
|
||||||
|
/// pthread_kill(thread, 0) (ESRCH once the thread has terminated) because
|
||||||
|
/// neither platform offers a portable timed join; the exited thread is then
|
||||||
|
/// joined so its resources are reclaimed.
|
||||||
|
/// </summary>
|
||||||
|
public static bool WaitForWorkerThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
var holder = (nint*)threadHandle;
|
||||||
|
if (holder == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder[1] != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var thread = holder[0];
|
||||||
|
var deadline = Environment.TickCount64 + timeoutMilliseconds;
|
||||||
|
while (pthread_kill(thread, 0) == 0)
|
||||||
|
{
|
||||||
|
if (Environment.TickCount64 >= deadline)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = pthread_join(thread, null);
|
||||||
|
holder[1] = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CloseWorkerThreadHandle(nint threadHandle)
|
||||||
|
{
|
||||||
|
var holder = (nint*)threadHandle;
|
||||||
|
if (holder == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder[1] == 0)
|
||||||
|
{
|
||||||
|
// Never observed exiting: detach so the thread does not leak a
|
||||||
|
// zombie join target when it eventually terminates.
|
||||||
|
_ = pthread_detach(holder[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Marshal.FreeHGlobal(threadHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
|
||||||
|
public static uint TlsAlloc()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
nuint key;
|
||||||
|
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint key32;
|
||||||
|
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsFree(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_key_delete_mac((nuint)key) == 0
|
||||||
|
: pthread_key_delete_linux(key) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsSetValue(uint key, nint value)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_setspecific_mac((nuint)key, value) == 0
|
||||||
|
: pthread_setspecific_linux(key, value) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint TlsGetValue(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_getspecific_mac((nuint)key)
|
||||||
|
: pthread_getspecific_linux(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
|
||||||
|
public static uint GetCurrentThreadId()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
ulong tid;
|
||||||
|
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unchecked((uint)gettid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
|
||||||
|
/// thunk that accepts up to four integer arguments in the Win64 ABI the
|
||||||
|
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
|
||||||
|
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
|
||||||
|
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
|
||||||
|
/// the stack 16-byte aligned for the call, and forwards the rax result.
|
||||||
|
/// </summary>
|
||||||
|
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
|
||||||
|
{
|
||||||
|
var memory = HostPlatform.Current.Memory;
|
||||||
|
var page = (byte*)memory.Allocate(
|
||||||
|
0,
|
||||||
|
4096,
|
||||||
|
HostPageProtection.ReadWriteExecute);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
|
||||||
|
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
|
||||||
|
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.FlushInstructionCache((ulong)page, (ulong)offset);
|
||||||
|
return (nint)page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildStubs();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildStubs()
|
||||||
|
{
|
||||||
|
var memory = HostPlatform.Current.Memory;
|
||||||
|
var page = (byte*)memory.Allocate(
|
||||||
|
0,
|
||||||
|
4096,
|
||||||
|
HostPageProtection.ReadWriteExecute);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
|
||||||
|
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
|
||||||
|
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
|
||||||
|
_sleepStub = EmitSleep(page, ref offset);
|
||||||
|
_waitForSingleObjectStub = EmitWaitForSingleObject(page, ref offset);
|
||||||
|
_setEventStub = EmitSetEvent(page, ref offset);
|
||||||
|
_exitThreadStub = EmitExitThread(page, ref offset);
|
||||||
|
|
||||||
|
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.FlushInstructionCache((ulong)page, (ulong)offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitTlsGetValue(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// On macOS x86-64 pthread keys index the gs-based thread specific
|
||||||
|
// data array directly, so TlsGetValue(index in ecx) collapses to a
|
||||||
|
// single load that clobbers nothing but RAX.
|
||||||
|
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||||
|
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: call pthread_getspecific, preserving the registers that are
|
||||||
|
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
||||||
|
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
|
||||||
|
// consumers only need a monotonically increasing counter, which rdtsc
|
||||||
|
// provides without leaving Win64-safe registers.
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
|
||||||
|
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
|
||||||
|
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSwitchToThread(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var schedYield = ResolveLibcExport("sched_yield");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSleep(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||||
|
var usleep = ResolveLibcExport("usleep");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||||
|
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||||
|
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||||
|
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||||
|
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitWaitForSingleObject(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// DWORD WaitForSingleObject(worker event in rcx, timeout in edx): the
|
||||||
|
// worker loop only ever waits forever, so the timeout is ignored.
|
||||||
|
// macOS waits on a dispatch semaphore (needs DISPATCH_TIME_FOREVER in
|
||||||
|
// rsi), Linux on a sem_t; both retry until the wait succeeds (EINTR).
|
||||||
|
var wait = ResolveLibcExport(
|
||||||
|
OperatingSystem.IsMacOS() ? "dispatch_semaphore_wait" : "sem_wait");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x53); // push rbx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xCB); // mov rbx, rcx
|
||||||
|
var retry = offset;
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xDF); // mov rdi, rbx
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
Emit(page, ref offset, 0x48, 0xC7, 0xC6, 0xFF, 0xFF, 0xFF, 0xFF); // mov rsi, DISPATCH_TIME_FOREVER
|
||||||
|
}
|
||||||
|
EmitMovRaxImm64(page, ref offset, wait); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
|
||||||
|
Emit(page, ref offset, 0x75, unchecked((byte)(retry - (offset + 2)))); // jnz retry
|
||||||
|
Emit(page, ref offset, 0x31, 0xC0); // xor eax, eax (WAIT_OBJECT_0)
|
||||||
|
Emit(page, ref offset, 0x5B); // pop rbx
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSetEvent(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// BOOL SetEvent(worker event in rcx) -> dispatch_semaphore_signal /
|
||||||
|
// sem_post.
|
||||||
|
var signal = ResolveLibcExport(
|
||||||
|
OperatingSystem.IsMacOS() ? "dispatch_semaphore_signal" : "sem_post");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||||
|
EmitMovRaxImm64(page, ref offset, signal); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitExitThread(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// void ExitThread(code in ecx) -> pthread_exit(NULL); never returns,
|
||||||
|
// so no registers need preserving. pthread_exit runs the thread's TSD
|
||||||
|
// destructors, which detaches the CLR if the thread lazily attached.
|
||||||
|
var pthreadExit = ResolveLibcExport("pthread_exit");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x31, 0xFF); // xor edi, edi
|
||||||
|
EmitMovRaxImm64(page, ref offset, pthreadExit); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0xCC); // int3 (never returns)
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint ResolveLibcExport(string name)
|
||||||
|
{
|
||||||
|
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
|
||||||
|
return NativeLibrary.GetExport(libc, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Emit(byte* page, ref int offset, params byte[] bytes)
|
||||||
|
{
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
page[offset++] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
|
||||||
|
{
|
||||||
|
Emit(page, ref offset, 0x48, 0xB8);
|
||||||
|
*(long*)(page + offset) = value;
|
||||||
|
offset += sizeof(long);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_linux(uint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_mac(nuint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_linux(uint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int gettid();
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_attr_init(byte* attr);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_attr_destroy(byte* attr);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_attr_setstacksize(byte* attr, nuint stackSize);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_create(nint* thread, byte* attr, nint startRoutine, nint arg);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_join(nint thread, nint* returnValue);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_detach(nint thread);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_kill(nint thread, int signal);
|
||||||
|
|
||||||
|
// macOS: dispatch semaphores back the worker events (unnamed sem_init is
|
||||||
|
// unsupported on Darwin). libSystem reexports libdispatch, so "libc"
|
||||||
|
// resolves these like the pthread imports above.
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_create(long value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_signal(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern void dispatch_release(nint handle);
|
||||||
|
|
||||||
|
// Linux: unnamed POSIX semaphores.
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_init(nint semaphore, int shared, uint value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_post(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_wait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_trywait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_destroy(nint semaphore);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
internal sealed class PosixHostSymbolResolver : IHostSymbolResolver
|
||||||
|
{
|
||||||
|
public nint GetAddress(HostRuntimeFunction function) => function switch
|
||||||
|
{
|
||||||
|
HostRuntimeFunction.TlsGetValue => PosixHostStubs.TlsGetValueStubAddress,
|
||||||
|
HostRuntimeFunction.QueryPerformanceCounter => PosixHostStubs.QueryPerformanceCounterStubAddress,
|
||||||
|
HostRuntimeFunction.SwitchToThread => PosixHostStubs.SwitchToThreadStubAddress,
|
||||||
|
HostRuntimeFunction.Sleep => PosixHostStubs.SleepStubAddress,
|
||||||
|
HostRuntimeFunction.WaitForSingleObject => PosixHostStubs.WaitForSingleObjectStubAddress,
|
||||||
|
HostRuntimeFunction.SetEvent => PosixHostStubs.SetEventStubAddress,
|
||||||
|
HostRuntimeFunction.ExitThread => PosixHostStubs.ExitThreadStubAddress,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
internal sealed class PosixHostThreading : IHostThreading
|
||||||
|
{
|
||||||
|
public uint AllocateTlsSlot() => PosixHostStubs.TlsAlloc();
|
||||||
|
|
||||||
|
public bool FreeTlsSlot(uint slot) => PosixHostStubs.TlsFree(slot);
|
||||||
|
|
||||||
|
public bool SetTlsValue(uint slot, nint value) => PosixHostStubs.TlsSetValue(slot, value);
|
||||||
|
|
||||||
|
public nint GetTlsValue(uint slot) => PosixHostStubs.TlsGetValue(slot);
|
||||||
|
|
||||||
|
public uint CurrentThreadId => PosixHostStubs.GetCurrentThreadId();
|
||||||
|
|
||||||
|
public void RequestTimerResolution()
|
||||||
|
{
|
||||||
|
// POSIX sleep primitives are already high-resolution; there is no
|
||||||
|
// timeBeginPeriod equivalent to request.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread affinity is advisory on POSIX hosts (macOS offers no
|
||||||
|
// pthread-level affinity API); callers treat false as "not applied".
|
||||||
|
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
|
||||||
|
{
|
||||||
|
_ = affinityMask;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateNativeThread(
|
||||||
|
nint entry,
|
||||||
|
nint parameter,
|
||||||
|
nuint stackReserveBytes,
|
||||||
|
out uint threadId)
|
||||||
|
{
|
||||||
|
return PosixHostStubs.CreateWorkerThread(entry, parameter, stackReserveBytes, out threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
return PosixHostStubs.WaitForWorkerThreadExit(threadHandle, timeoutMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseThreadHandle(nint threadHandle)
|
||||||
|
{
|
||||||
|
PosixHostStubs.CloseWorkerThreadHandle(threadHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
|
||||||
|
{
|
||||||
|
_ = threadId;
|
||||||
|
registers = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-49
@@ -3,21 +3,21 @@
|
|||||||
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a DualSense controller over raw HID on a background thread.
|
/// Reads a DualSense controller over raw HID on a background thread.
|
||||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
/// activated by requesting feature report 0x05), with hot-plug retry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class DualSenseReader
|
internal static class WindowsDualSenseReader
|
||||||
{
|
{
|
||||||
private const ushort SonyVendorId = 0x054C;
|
private const ushort SonyVendorId = 0x054C;
|
||||||
private const ushort DualSenseProductId = 0x0CE6;
|
private const ushort DualSenseProductId = 0x0CE6;
|
||||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static PadState _state;
|
private static HostGamepadState _state;
|
||||||
private static bool _started;
|
private static bool _started;
|
||||||
|
|
||||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
// Output (rumble/lightbar) state, all guarded by Gate.
|
||||||
@@ -37,6 +37,8 @@ internal static class DualSenseReader
|
|||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
internal static void EnsureStarted()
|
||||||
{
|
{
|
||||||
|
// The GUI source-links this reader and calls it directly, without the
|
||||||
|
// host-platform resolution that otherwise guarantees Windows.
|
||||||
if (!OperatingSystem.IsWindows())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -59,7 +61,7 @@ internal static class DualSenseReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool TryGetState(out PadState state)
|
internal static bool TryGetState(out HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -69,7 +71,7 @@ internal static class DualSenseReader
|
|||||||
return state.Connected;
|
return state.Connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
private static void SetState(in HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -148,11 +150,11 @@ internal static class DualSenseReader
|
|||||||
{
|
{
|
||||||
if (_outputStream is null)
|
if (_outputStream is null)
|
||||||
{
|
{
|
||||||
var handle = HidNative.CreateFile(
|
var handle = WindowsHidNative.CreateFile(
|
||||||
_devicePath,
|
_devicePath,
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (handle.IsInvalid)
|
if (handle.IsInvalid)
|
||||||
{
|
{
|
||||||
handle.Dispose();
|
handle.Dispose();
|
||||||
@@ -262,7 +264,7 @@ internal static class DualSenseReader
|
|||||||
// to the full 0x31 input report. Harmless over USB.
|
// to the full 0x31 input report. Harmless over USB.
|
||||||
var feature = new byte[41];
|
var feature = new byte[41];
|
||||||
feature[0] = 0x05;
|
feature[0] = 0x05;
|
||||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
_ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||||
|
|
||||||
if (!announcedConnect)
|
if (!announcedConnect)
|
||||||
{
|
{
|
||||||
@@ -320,18 +322,18 @@ internal static class DualSenseReader
|
|||||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
||||||
{
|
{
|
||||||
devicePath = null;
|
devicePath = null;
|
||||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
foreach (var path in WindowsHidNative.EnumerateHidDevicePaths())
|
||||||
{
|
{
|
||||||
// Open without access rights just to query VID/PID.
|
// Open without access rights just to query VID/PID.
|
||||||
using var probe = HidNative.CreateFile(
|
using var probe = WindowsHidNative.CreateFile(
|
||||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (probe.IsInvalid)
|
if (probe.IsInvalid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
|
||||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||||
attributes.VendorId != SonyVendorId ||
|
attributes.VendorId != SonyVendorId ||
|
||||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
||||||
{
|
{
|
||||||
@@ -339,19 +341,19 @@ internal static class DualSenseReader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read+write so feature reports work; fall back to read-only.
|
// Read+write so feature reports work; fall back to read-only.
|
||||||
var handle = HidNative.CreateFile(
|
var handle = WindowsHidNative.CreateFile(
|
||||||
path,
|
path,
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (handle.IsInvalid)
|
if (handle.IsInvalid)
|
||||||
{
|
{
|
||||||
handle.Dispose();
|
handle.Dispose();
|
||||||
handle = HidNative.CreateFile(
|
handle = WindowsHidNative.CreateFile(
|
||||||
path,
|
path,
|
||||||
HidNative.GenericRead,
|
WindowsHidNative.GenericRead,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handle.IsInvalid)
|
if (!handle.IsInvalid)
|
||||||
@@ -366,7 +368,7 @@ internal static class DualSenseReader
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
|
private static bool TryParseReport(ReadOnlySpan<byte> report, out HostGamepadState state)
|
||||||
{
|
{
|
||||||
// USB: report id 0x01, payload starts at [1].
|
// USB: report id 0x01, payload starts at [1].
|
||||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
||||||
@@ -395,43 +397,43 @@ internal static class DualSenseReader
|
|||||||
var buttons1 = report[offset + 8];
|
var buttons1 = report[offset + 8];
|
||||||
var buttons2 = report[offset + 9];
|
var buttons2 = report[offset + 9];
|
||||||
|
|
||||||
uint buttons = 0;
|
var buttons = HostGamepadButtons.None;
|
||||||
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
|
||||||
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
|
||||||
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
|
||||||
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||||
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
|
||||||
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
|
||||||
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
|
||||||
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
|
||||||
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
|
||||||
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
|
||||||
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
|
||||||
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||||
|
|
||||||
state = new PadState(
|
state = new HostGamepadState(
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Buttons: buttons,
|
Buttons: buttons,
|
||||||
LeftX: leftX,
|
LeftX: leftX,
|
||||||
LeftY: leftY,
|
LeftY: leftY,
|
||||||
RightX: rightX,
|
RightX: rightX,
|
||||||
RightY: rightY,
|
RightY: rightY,
|
||||||
L2: l2,
|
LeftTrigger: l2,
|
||||||
R2: r2);
|
RightTrigger: r2);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint HatToButtons(int hat) => hat switch
|
private static HostGamepadButtons HatToButtons(int hat) => hat switch
|
||||||
{
|
{
|
||||||
0 => OrbisPadButton.Up,
|
0 => HostGamepadButtons.Up,
|
||||||
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
|
||||||
2 => OrbisPadButton.Right,
|
2 => HostGamepadButtons.Right,
|
||||||
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
|
||||||
4 => OrbisPadButton.Down,
|
4 => HostGamepadButtons.Down,
|
||||||
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
|
||||||
6 => OrbisPadButton.Left,
|
6 => HostGamepadButtons.Left,
|
||||||
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+23
-18
@@ -4,13 +4,13 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
||||||
/// directly, without any external input library.
|
/// directly, without any external input library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static partial class HidNative
|
internal static partial class WindowsHidNative
|
||||||
{
|
{
|
||||||
internal const int DigcfPresent = 0x02;
|
internal const int DigcfPresent = 0x02;
|
||||||
internal const int DigcfDeviceInterface = 0x10;
|
internal const int DigcfDeviceInterface = 0x10;
|
||||||
@@ -38,28 +38,32 @@ internal static partial class HidNative
|
|||||||
public ushort VersionNumber;
|
public ushort VersionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
|
internal static partial void HidD_GetHidGuid(out Guid hidGuid);
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
|
||||||
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
[LibraryImport("setupapi.dll")]
|
||||||
internal static extern bool SetupDiEnumDeviceInterfaces(
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiEnumDeviceInterfaces(
|
||||||
nint deviceInfoSet,
|
nint deviceInfoSet,
|
||||||
nint deviceInfoData,
|
nint deviceInfoData,
|
||||||
ref Guid interfaceClassGuid,
|
ref Guid interfaceClassGuid,
|
||||||
int memberIndex,
|
int memberIndex,
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData);
|
ref SpDeviceInterfaceData deviceInterfaceData);
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
|
||||||
internal static extern bool SetupDiGetDeviceInterfaceDetail(
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiGetDeviceInterfaceDetail(
|
||||||
nint deviceInfoSet,
|
nint deviceInfoSet,
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData,
|
ref SpDeviceInterfaceData deviceInterfaceData,
|
||||||
nint deviceInterfaceDetailData,
|
nint deviceInterfaceDetailData,
|
||||||
@@ -67,11 +71,12 @@ internal static partial class HidNative
|
|||||||
out int requiredSize,
|
out int requiredSize,
|
||||||
nint deviceInfoData);
|
nint deviceInfoData);
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
[LibraryImport("setupapi.dll")]
|
||||||
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
[LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||||
internal static extern SafeFileHandle CreateFile(
|
internal static partial SafeFileHandle CreateFile(
|
||||||
string fileName,
|
string fileName,
|
||||||
uint desiredAccess,
|
uint desiredAccess,
|
||||||
uint shareMode,
|
uint shareMode,
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows input backend: DualSense over raw HID plus XInput controllers for gamepads,
|
||||||
|
/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
|
||||||
|
/// only exists on the DualSense.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed partial class WindowsHostInput : IHostInput
|
||||||
|
{
|
||||||
|
public void EnsureStarted()
|
||||||
|
{
|
||||||
|
WindowsDualSenseReader.EnsureStarted();
|
||||||
|
WindowsXInputReader.EnsureStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
|
||||||
|
{
|
||||||
|
destination[count++] = dualSense;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
|
||||||
|
{
|
||||||
|
destination[count++] = xinput;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? DescribeConnectedGamepad()
|
||||||
|
{
|
||||||
|
if (WindowsDualSenseReader.TryGetState(out _))
|
||||||
|
{
|
||||||
|
return "DualSense";
|
||||||
|
}
|
||||||
|
|
||||||
|
return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRumble(byte largeMotor, byte smallMotor)
|
||||||
|
{
|
||||||
|
WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
|
||||||
|
WindowsXInputReader.SetRumble(largeMotor, smallMotor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
|
||||||
|
WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
|
||||||
|
|
||||||
|
public void SetLightbar(byte red, byte green, byte blue) =>
|
||||||
|
WindowsDualSenseReader.SetLightbar(red, green, blue);
|
||||||
|
|
||||||
|
public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
|
||||||
|
|
||||||
|
public bool IsHostWindowFocused()
|
||||||
|
{
|
||||||
|
var foregroundWindow = GetForegroundWindow();
|
||||||
|
if (foregroundWindow == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
||||||
|
return processId == (uint)Environment.ProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKeyDown(int virtualKey) =>
|
||||||
|
(GetAsyncKeyState(virtualKey) & 0x8000) != 0;
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
private static partial short GetAsyncKeyState(int vKey);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
private static partial nint GetForegroundWindow();
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||||
|
}
|
||||||
@@ -10,4 +10,8 @@ internal sealed class WindowsHostPlatform : IHostPlatform
|
|||||||
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||||
|
|
||||||
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
||||||
|
|
||||||
|
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
|
||||||
|
|
||||||
|
public IHostInput Input { get; } = new WindowsHostInput();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,31 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
|||||||
private const int CtxRbp = 160;
|
private const int CtxRbp = 160;
|
||||||
private const int CtxRip = 248;
|
private const int CtxRip = 248;
|
||||||
|
|
||||||
|
private static int _timerResolutionRequested;
|
||||||
|
|
||||||
|
public void RequestTimerResolution()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TimeBeginPeriod(1) != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Host timer resolution request rejected; " +
|
||||||
|
"timed waits keep the default ~15.6 ms granularity.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DllNotFoundException exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public uint AllocateTlsSlot() => TlsAlloc();
|
public uint AllocateTlsSlot() => TlsAlloc();
|
||||||
|
|
||||||
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||||
@@ -162,4 +187,7 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
|||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static partial bool CloseHandle(nint hObject);
|
private static partial bool CloseHandle(nint hObject);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
|
||||||
|
private static partial uint TimeBeginPeriod(uint uPeriod);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
internal sealed partial class WindowsWaveOutAudio : IHostAudioOutput
|
||||||
|
{
|
||||||
|
public string BackendName => "winmm";
|
||||||
|
|
||||||
|
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
|
||||||
|
|
||||||
|
private sealed partial class WaveOutStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
private const uint WaveMapper = uint.MaxValue;
|
||||||
|
private const uint CallbackEvent = 0x0005_0000;
|
||||||
|
private const ushort WaveFormatPcm = 1;
|
||||||
|
private const uint WaveHeaderDone = 0x0000_0001;
|
||||||
|
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly AutoResetEvent _completion = new(false);
|
||||||
|
private readonly Queue<NativeBuffer> _buffers = new();
|
||||||
|
private IntPtr _device;
|
||||||
|
private int _queuedPcmBytes;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public WaveOutStream(uint sampleRate)
|
||||||
|
{
|
||||||
|
var format = new WaveFormat
|
||||||
|
{
|
||||||
|
FormatTag = WaveFormatPcm,
|
||||||
|
Channels = 2,
|
||||||
|
SamplesPerSecond = sampleRate,
|
||||||
|
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||||
|
BlockAlign = 4,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
ExtraSize = 0,
|
||||||
|
};
|
||||||
|
var result = WaveOutOpen(
|
||||||
|
out _device,
|
||||||
|
WaveMapper,
|
||||||
|
ref format,
|
||||||
|
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||||
|
IntPtr.Zero,
|
||||||
|
CallbackEvent);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
while (_queuedPcmBytes != 0 &&
|
||||||
|
_queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
|
||||||
|
{
|
||||||
|
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QueueBuffer(stereoPcm16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_device != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
WaveOutReset(_device);
|
||||||
|
while (_buffers.TryDequeue(out var buffer))
|
||||||
|
{
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
WaveOutClose(_device);
|
||||||
|
_device = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
_completion.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||||
|
var headerAddress = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = new WaveHeader
|
||||||
|
{
|
||||||
|
Data = dataAddress,
|
||||||
|
BufferLength = checked((uint)data.Length),
|
||||||
|
};
|
||||||
|
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||||
|
Marshal.StructureToPtr(header, headerAddress, false);
|
||||||
|
|
||||||
|
var result = WaveOutPrepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WaveOutWrite(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||||
|
_queuedPcmBytes += data.Length;
|
||||||
|
dataAddress = IntPtr.Zero;
|
||||||
|
headerAddress = IntPtr.Zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (headerAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(headerAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dataAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReapCompletedBuffers()
|
||||||
|
{
|
||||||
|
while (_buffers.TryPeek(out var buffer))
|
||||||
|
{
|
||||||
|
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||||
|
if ((header.Flags & WaveHeaderDone) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Dequeue();
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseBuffer(NativeBuffer buffer)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
buffer.Header,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
_queuedPcmBytes -= buffer.Length;
|
||||||
|
Marshal.FreeHGlobal(buffer.Header);
|
||||||
|
Marshal.FreeHGlobal(buffer.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||||
|
private struct WaveFormat
|
||||||
|
{
|
||||||
|
public ushort FormatTag;
|
||||||
|
public ushort Channels;
|
||||||
|
public uint SamplesPerSecond;
|
||||||
|
public uint AverageBytesPerSecond;
|
||||||
|
public ushort BlockAlign;
|
||||||
|
public ushort BitsPerSample;
|
||||||
|
public ushort ExtraSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WaveHeader
|
||||||
|
{
|
||||||
|
public IntPtr Data;
|
||||||
|
public uint BufferLength;
|
||||||
|
public uint BytesRecorded;
|
||||||
|
public nuint User;
|
||||||
|
public uint Flags;
|
||||||
|
public uint Loops;
|
||||||
|
public IntPtr Next;
|
||||||
|
public nuint Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||||
|
private static partial uint WaveOutOpen(
|
||||||
|
out IntPtr device,
|
||||||
|
uint deviceId,
|
||||||
|
ref WaveFormat format,
|
||||||
|
IntPtr callback,
|
||||||
|
IntPtr instance,
|
||||||
|
uint flags);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||||
|
private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||||
|
private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||||
|
private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||||
|
private static partial uint WaveOutReset(IntPtr device);
|
||||||
|
|
||||||
|
[LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||||
|
private static partial uint WaveOutClose(IntPtr device);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
-34
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
||||||
/// the Windows XInput API on a background thread, translated to the same
|
/// the Windows XInput API on a background thread, translated to
|
||||||
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
|
/// <see cref="HostGamepadState"/> conventions. Supports rumble and hot-plug
|
||||||
/// and hot-plug retry; the first connected slot (of four) is used.
|
/// retry; the first connected slot (of four) is used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class XInputReader
|
internal static partial class WindowsXInputReader
|
||||||
{
|
{
|
||||||
private const uint ErrorSuccess = 0;
|
private const uint ErrorSuccess = 0;
|
||||||
private const int SlotCount = 4;
|
private const int SlotCount = 4;
|
||||||
@@ -34,7 +34,7 @@ internal static class XInputReader
|
|||||||
private const ushort XinputY = 0x8000;
|
private const ushort XinputY = 0x8000;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static PadState _state;
|
private static HostGamepadState _state;
|
||||||
private static bool _started;
|
private static bool _started;
|
||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
private static byte _motorLeft;
|
private static byte _motorLeft;
|
||||||
@@ -45,6 +45,8 @@ internal static class XInputReader
|
|||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
internal static void EnsureStarted()
|
||||||
{
|
{
|
||||||
|
// The GUI source-links this reader and calls it directly, without the
|
||||||
|
// host-platform resolution that otherwise guarantees Windows.
|
||||||
if (!OperatingSystem.IsWindows())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -67,7 +69,7 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool TryGetState(out PadState state)
|
internal static bool TryGetState(out HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -77,7 +79,7 @@ internal static class XInputReader
|
|||||||
return state.Connected;
|
return state.Connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
private static void SetState(in HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -204,40 +206,40 @@ internal static class XInputReader
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PadState Translate(in XInputGamepad pad)
|
private static HostGamepadState Translate(in XInputGamepad pad)
|
||||||
{
|
{
|
||||||
uint buttons = 0;
|
var buttons = HostGamepadButtons.None;
|
||||||
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
|
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
|
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
|
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
|
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
|
||||||
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
|
buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
|
||||||
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
|
buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||||
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
|
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
|
||||||
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
|
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
|
||||||
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
|
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
|
||||||
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
|
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
|
||||||
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
|
buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
|
||||||
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
|
buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
|
||||||
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
|
buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
|
||||||
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
|
buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||||
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
|
buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
|
||||||
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
|
buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
|
||||||
|
|
||||||
return new PadState(
|
return new HostGamepadState(
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Buttons: buttons,
|
Buttons: buttons,
|
||||||
LeftX: AxisToByte(pad.ThumbLX),
|
LeftX: AxisToByte(pad.ThumbLX),
|
||||||
LeftY: AxisToByteInverted(pad.ThumbLY),
|
LeftY: AxisToByteInverted(pad.ThumbLY),
|
||||||
RightX: AxisToByte(pad.ThumbRX),
|
RightX: AxisToByte(pad.ThumbRX),
|
||||||
RightY: AxisToByteInverted(pad.ThumbRY),
|
RightY: AxisToByteInverted(pad.ThumbRY),
|
||||||
L2: pad.LeftTrigger,
|
LeftTrigger: pad.LeftTrigger,
|
||||||
R2: pad.RightTrigger);
|
RightTrigger: pad.RightTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
||||||
|
|
||||||
// XInput Y grows upward, ORBIS pads report Y growing downward.
|
// XInput Y grows upward, host pad conventions report Y growing downward.
|
||||||
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
@@ -267,9 +269,9 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// xinput1_4.dll ships with Windows 8 and later.
|
// xinput1_4.dll ships with Windows 8 and later.
|
||||||
[DllImport("xinput1_4.dll")]
|
[LibraryImport("xinput1_4.dll")]
|
||||||
private static extern uint XInputGetState(uint userIndex, out XInputState state);
|
private static partial uint XInputGetState(uint userIndex, out XInputState state);
|
||||||
|
|
||||||
[DllImport("xinput1_4.dll")]
|
[LibraryImport("xinput1_4.dll")]
|
||||||
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs work on the real process main thread. GLFW windowing must live on
|
||||||
|
/// that thread on macOS (AppKit) and Linux (X11's single event queue), so the
|
||||||
|
/// CLI moves emulation onto a worker thread, parks the main thread in
|
||||||
|
/// <see cref="Pump"/>, and the video presenter posts its window loop here. On
|
||||||
|
/// Windows <see cref="IsAvailable"/> stays false and the window keeps its own
|
||||||
|
/// thread.
|
||||||
|
/// </summary>
|
||||||
|
public static class HostMainThread
|
||||||
|
{
|
||||||
|
private static readonly BlockingCollection<Action> _work = new();
|
||||||
|
private static Action? _shutdownRequestHandler;
|
||||||
|
|
||||||
|
public static bool IsAvailable { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a callback invoked by <see cref="Shutdown"/> so a
|
||||||
|
/// long-running posted work item (the presenter's window loop) can be
|
||||||
|
/// asked to return to the pump.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetShutdownRequestHandler(Action handler) =>
|
||||||
|
_shutdownRequestHandler = handler;
|
||||||
|
|
||||||
|
/// <summary>Marks the pump as present. Call before guest code can run.</summary>
|
||||||
|
public static void Enable() => IsAvailable = true;
|
||||||
|
|
||||||
|
public static void Post(Action work)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_work.Add(work);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// Shutdown already requested; the process is exiting.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Services posted work on the calling (main) thread until
|
||||||
|
/// <see cref="Shutdown"/> is called and the queue drains.
|
||||||
|
/// </summary>
|
||||||
|
public static void Pump()
|
||||||
|
{
|
||||||
|
foreach (var work in _work.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
work();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] Main-thread work failed: {exception}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Shutdown()
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_shutdownRequestHandler?.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Main-thread shutdown handler failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_work.CompleteAdding();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -3379,6 +3380,7 @@ public static class AgcExports
|
|||||||
$"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " +
|
$"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " +
|
||||||
$"fmt={target.Format} tile={target.TileMode} " +
|
$"fmt={target.Format} tile={target.TileMode} " +
|
||||||
$"size={target.Width}x{target.Height} vertices={vertexCount} " +
|
$"size={target.Width}x{target.Height} vertices={vertexCount} " +
|
||||||
|
$"prim=0x{primitiveType:X} indexed={indexed} " +
|
||||||
$"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " +
|
$"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " +
|
||||||
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}");
|
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}");
|
||||||
}
|
}
|
||||||
@@ -3415,6 +3417,8 @@ public static class AgcExports
|
|||||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||||
var vertexBuffers =
|
var vertexBuffers =
|
||||||
CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
|
CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||||
|
TraceRectListVertices(translatedDraw, vertexBuffers);
|
||||||
|
TraceGrassDrawVertices(translatedDraw, textures, vertexBuffers);
|
||||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||||
translatedDraw.PixelSpirv,
|
translatedDraw.PixelSpirv,
|
||||||
textures,
|
textures,
|
||||||
@@ -4381,6 +4385,7 @@ public static class AgcExports
|
|||||||
descriptor.Width > 8192 ||
|
descriptor.Width > 8192 ||
|
||||||
descriptor.Height > 8192)
|
descriptor.Height > 8192)
|
||||||
{
|
{
|
||||||
|
TraceTextureFallback(descriptor, "invalid-descriptor");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4398,6 +4403,9 @@ public static class AgcExports
|
|||||||
sourceByteCount > MaxPresentedTextureBytes ||
|
sourceByteCount > MaxPresentedTextureBytes ||
|
||||||
sourceByteCount > int.MaxValue)
|
sourceByteCount > int.MaxValue)
|
||||||
{
|
{
|
||||||
|
TraceTextureFallback(
|
||||||
|
descriptor,
|
||||||
|
$"invalid-byte-count:{sourceByteCount}");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4433,7 +4441,10 @@ public static class AgcExports
|
|||||||
if (descriptor.Address != 0)
|
if (descriptor.Address != 0)
|
||||||
{
|
{
|
||||||
var storageSource = new byte[(int)sourceByteCount];
|
var storageSource = new byte[(int)sourceByteCount];
|
||||||
if (ctx.Memory.TryRead(descriptor.Address, storageSource) &&
|
if ((ctx.Memory.TryRead(descriptor.Address, storageSource) ||
|
||||||
|
KernelMemoryCompatExports.TryReadTrackedLibcHeapGpuAlias(
|
||||||
|
descriptor.Address,
|
||||||
|
storageSource)) &&
|
||||||
storageSource.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
storageSource.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
||||||
{
|
{
|
||||||
initialPixels = storageSource;
|
initialPixels = storageSource;
|
||||||
@@ -4459,8 +4470,14 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var source = new byte[(int)sourceByteCount];
|
var source = new byte[(int)sourceByteCount];
|
||||||
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
if (!ctx.Memory.TryRead(descriptor.Address, source) &&
|
||||||
|
!KernelMemoryCompatExports.TryReadTrackedLibcHeapGpuAlias(
|
||||||
|
descriptor.Address,
|
||||||
|
source))
|
||||||
{
|
{
|
||||||
|
TraceTextureFallback(
|
||||||
|
descriptor,
|
||||||
|
$"guest-read-failed:{sourceByteCount}");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4486,6 +4503,7 @@ public static class AgcExports
|
|||||||
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
||||||
$"dst=0x{descriptor.DstSelect:X3} " +
|
$"dst=0x{descriptor.DstSelect:X3} " +
|
||||||
$"bytes={source.Length} nonzero64={nonZero}");
|
$"bytes={source.Length} nonzero64={nonZero}");
|
||||||
|
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
|
||||||
|
|
||||||
var rgba = source;
|
var rgba = source;
|
||||||
texture = new VulkanGuestDrawTexture(
|
texture = new VulkanGuestDrawTexture(
|
||||||
@@ -4506,6 +4524,158 @@ public static class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int _textureFallbackTraceCount;
|
||||||
|
|
||||||
|
private static void TraceTextureFallback(
|
||||||
|
TextureDescriptor descriptor,
|
||||||
|
string reason)
|
||||||
|
{
|
||||||
|
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||||
|
if ((!string.Equals(mode, "1", StringComparison.Ordinal) &&
|
||||||
|
!string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase)) ||
|
||||||
|
Interlocked.Increment(ref _textureFallbackTraceCount) > 64)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] agc.texture_fallback reason={reason} " +
|
||||||
|
$"addr=0x{descriptor.Address:X16} type={descriptor.Type} " +
|
||||||
|
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
||||||
|
$"fmt={descriptor.Format} num={descriptor.NumberType} " +
|
||||||
|
$"tile={descriptor.TileMode} mip={descriptor.MipLevels} " +
|
||||||
|
$"dst=0x{descriptor.DstSelect:X3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static int _grassTraceCount;
|
||||||
|
|
||||||
|
private static void TraceGrassDrawVertices(
|
||||||
|
TranslatedGuestDraw draw,
|
||||||
|
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
|
||||||
|
{
|
||||||
|
if (_grassTraceCount >= 6 ||
|
||||||
|
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
|
||||||
|
vertexBuffers.Count == 0 ||
|
||||||
|
Interlocked.Increment(ref _grassTraceCount) > 6)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = new System.Text.StringBuilder();
|
||||||
|
text.Append($"agc.grassdraw prim=0x{draw.PrimitiveType:X} verts={draw.VertexCount} ");
|
||||||
|
text.Append($"indexed={draw.IndexBuffer is not null} buffers={vertexBuffers.Count}");
|
||||||
|
foreach (var buffer in vertexBuffers)
|
||||||
|
{
|
||||||
|
text.Append(
|
||||||
|
$"\n loc={buffer.Location} fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount} " +
|
||||||
|
$"stride={buffer.Stride} offset={buffer.OffsetBytes} bytes={buffer.Data.Length}");
|
||||||
|
var stride = Math.Max(buffer.Stride, 4u);
|
||||||
|
var maxVerts = Math.Min(6, (int)((buffer.Data.Length - buffer.OffsetBytes) / stride));
|
||||||
|
for (var vertex = 0; vertex < maxVerts; vertex++)
|
||||||
|
{
|
||||||
|
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
||||||
|
var components = Math.Min(4, (int)((buffer.Data.Length - baseOffset) / 4));
|
||||||
|
text.Append($"\n v{vertex}:");
|
||||||
|
for (var c = 0; c < components; c++)
|
||||||
|
{
|
||||||
|
text.Append($" {BitConverter.ToSingle(buffer.Data, baseOffset + c * 4):0.#####}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgcShader(text.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int _rectListTraceCount;
|
||||||
|
|
||||||
|
private static void TraceRectListVertices(
|
||||||
|
TranslatedGuestDraw draw,
|
||||||
|
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
|
||||||
|
{
|
||||||
|
if (draw.PrimitiveType != 0x11 ||
|
||||||
|
draw.IndexBuffer is not null ||
|
||||||
|
vertexBuffers.Count == 0 ||
|
||||||
|
_rectListTraceCount >= 8 ||
|
||||||
|
Interlocked.Increment(ref _rectListTraceCount) > 8)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = vertexBuffers[0];
|
||||||
|
var stride = Math.Max(buffer.Stride, 4u);
|
||||||
|
var text = new System.Text.StringBuilder();
|
||||||
|
for (var vertex = 0; vertex < 3; vertex++)
|
||||||
|
{
|
||||||
|
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
||||||
|
if (baseOffset + 16 > buffer.Data.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = BitConverter.ToSingle(buffer.Data, baseOffset);
|
||||||
|
var y = BitConverter.ToSingle(buffer.Data, baseOffset + 4);
|
||||||
|
var z = BitConverter.ToSingle(buffer.Data, baseOffset + 8);
|
||||||
|
var w = BitConverter.ToSingle(buffer.Data, baseOffset + 12);
|
||||||
|
text.Append($" v{vertex}=({x:0.###},{y:0.###},{z:0.###},{w:0.###})");
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgcShader(
|
||||||
|
$"agc.rectlist verts={draw.VertexCount} stride={buffer.Stride} " +
|
||||||
|
$"fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount}{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int _textureDumpCount;
|
||||||
|
private static readonly ConcurrentDictionary<string, int> _textureDumpKeys = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes raw sampled-texture bytes (as read from guest memory) when
|
||||||
|
/// SHARPEMU_TEXTURE_DUMP_DIR is set, so upload-time content can be
|
||||||
|
/// inspected offline. File name records size and effective pitch.
|
||||||
|
/// </summary>
|
||||||
|
private static void DumpTextureSourceIfRequested(
|
||||||
|
in TextureDescriptor descriptor,
|
||||||
|
uint sourcePitch,
|
||||||
|
byte[] source)
|
||||||
|
{
|
||||||
|
var directory = Environment.GetEnvironmentVariable("SHARPEMU_TEXTURE_DUMP_DIR");
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = $"0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}";
|
||||||
|
var occurrence = _textureDumpKeys.AddOrUpdate(key, 1, static (_, count) => count + 1);
|
||||||
|
// First uses plus periodic later snapshots (the game reuses the same
|
||||||
|
// allocation for successive full-screen images).
|
||||||
|
if ((occurrence > 3 && occurrence % 500 >= 3) ||
|
||||||
|
Interlocked.Increment(ref _textureDumpCount) > 200)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _textureDumpCount;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
var path = Path.Combine(
|
||||||
|
directory,
|
||||||
|
$"{index:D3}-0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}" +
|
||||||
|
$"-p{sourcePitch}-f{descriptor.Format}-t{descriptor.TileMode}.bin");
|
||||||
|
File.WriteAllBytes(path, source);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// A bad SHARPEMU_TEXTURE_DUMP_DIR (permissions, invalid path)
|
||||||
|
// must not take the emulator down; the dump is a debug aid.
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Texture dump failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||||
bool isStorage,
|
bool isStorage,
|
||||||
uint format,
|
uint format,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -27,7 +28,7 @@ public static class AudioOutExports
|
|||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat,
|
bool isFloat,
|
||||||
WinMmAudioPort? backend)
|
IHostAudioStream? backend)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
Type = type;
|
Type = type;
|
||||||
@@ -48,7 +49,7 @@ public static class AudioOutExports
|
|||||||
public int Channels { get; }
|
public int Channels { get; }
|
||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
public WinMmAudioPort? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
public int BufferByteLength =>
|
public int BufferByteLength =>
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
@@ -103,12 +104,13 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
WinMmAudioPort? backend = null;
|
IHostAudioStream? backend = null;
|
||||||
string backendName;
|
string backendName;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
backend = new WinMmAudioPort(frequency);
|
var audio = HostPlatform.Current.Audio;
|
||||||
backendName = "winmm";
|
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||||
|
backendName = audio.BackendName;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -180,15 +182,31 @@ 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 (port.Backend is null ||
|
if (port.Backend is null)
|
||||||
!port.Backend.Submit(
|
|
||||||
source,
|
|
||||||
port.BufferLength,
|
|
||||||
port.Channels,
|
|
||||||
port.BytesPerSample,
|
|
||||||
port.IsFloat))
|
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||||
|
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AudioPcmConversion.ConvertToStereoPcm16(
|
||||||
|
source,
|
||||||
|
output.AsSpan(0, outputLength),
|
||||||
|
checked((int)port.BufferLength),
|
||||||
|
port.Channels,
|
||||||
|
port.BytesPerSample,
|
||||||
|
port.IsFloat);
|
||||||
|
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||||
|
{
|
||||||
|
port.PaceSilence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
|
||||||
|
/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
|
||||||
|
/// device specifics live behind IHostAudioStream.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AudioPcmConversion
|
||||||
|
{
|
||||||
|
/// <summary>Bytes per output frame: two 16-bit channels.</summary>
|
||||||
|
public const int OutputFrameSize = 4;
|
||||||
|
|
||||||
|
public static void ConvertToStereoPcm16(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
Span<byte> destination,
|
||||||
|
int frames,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||||
|
for (var frame = 0; frame < frames; frame++)
|
||||||
|
{
|
||||||
|
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||||
|
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||||
|
var right = channels == 1
|
||||||
|
? left
|
||||||
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short ReadSample(
|
||||||
|
ReadOnlySpan<byte> frame,
|
||||||
|
int channel,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||||
|
if (!isFloat)
|
||||||
|
{
|
||||||
|
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||||
|
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||||
|
return checked((short)MathF.Round(value * short.MaxValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
|
||||||
|
|
||||||
internal sealed class WinMmAudioPort : IDisposable
|
|
||||||
{
|
|
||||||
private const uint WaveMapper = uint.MaxValue;
|
|
||||||
private const uint CallbackEvent = 0x0005_0000;
|
|
||||||
private const ushort WaveFormatPcm = 1;
|
|
||||||
private const uint WaveHeaderDone = 0x0000_0001;
|
|
||||||
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
|
||||||
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private readonly AutoResetEvent _completion = new(false);
|
|
||||||
private readonly Queue<NativeBuffer> _buffers = new();
|
|
||||||
private IntPtr _device;
|
|
||||||
private int _queuedPcmBytes;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public WinMmAudioPort(uint sampleRate)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var format = new WaveFormat
|
|
||||||
{
|
|
||||||
FormatTag = WaveFormatPcm,
|
|
||||||
Channels = 2,
|
|
||||||
SamplesPerSecond = sampleRate,
|
|
||||||
AverageBytesPerSecond = checked(sampleRate * 4),
|
|
||||||
BlockAlign = 4,
|
|
||||||
BitsPerSample = 16,
|
|
||||||
ExtraSize = 0,
|
|
||||||
};
|
|
||||||
var result = WaveOutOpen(
|
|
||||||
out _device,
|
|
||||||
WaveMapper,
|
|
||||||
ref format,
|
|
||||||
_completion.SafeWaitHandle.DangerousGetHandle(),
|
|
||||||
IntPtr.Zero,
|
|
||||||
CallbackEvent);
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Submit(
|
|
||||||
ReadOnlySpan<byte> source,
|
|
||||||
uint frames,
|
|
||||||
int channels,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputLength = checked((int)frames * 4);
|
|
||||||
ReapCompletedBuffers();
|
|
||||||
while (_queuedPcmBytes != 0 &&
|
|
||||||
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
|
||||||
{
|
|
||||||
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReapCompletedBuffers();
|
|
||||||
}
|
|
||||||
|
|
||||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ConvertToStereoPcm16(
|
|
||||||
source,
|
|
||||||
output.AsSpan(0, outputLength),
|
|
||||||
checked((int)frames),
|
|
||||||
channels,
|
|
||||||
bytesPerSample,
|
|
||||||
isFloat);
|
|
||||||
return QueueBuffer(output.AsSpan(0, outputLength));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
if (_device != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
WaveOutReset(_device);
|
|
||||||
while (_buffers.TryDequeue(out var buffer))
|
|
||||||
{
|
|
||||||
ReleaseBuffer(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
WaveOutClose(_device);
|
|
||||||
_device = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
_completion.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
|
||||||
var headerAddress = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
var header = new WaveHeader
|
|
||||||
{
|
|
||||||
Data = dataAddress,
|
|
||||||
BufferLength = checked((uint)data.Length),
|
|
||||||
};
|
|
||||||
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
|
||||||
Marshal.StructureToPtr(header, headerAddress, false);
|
|
||||||
|
|
||||||
var result = WaveOutPrepareHeader(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = WaveOutWrite(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
WaveOutUnprepareHeader(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
|
||||||
_queuedPcmBytes += data.Length;
|
|
||||||
dataAddress = IntPtr.Zero;
|
|
||||||
headerAddress = IntPtr.Zero;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (headerAddress != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(headerAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataAddress != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(dataAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReapCompletedBuffers()
|
|
||||||
{
|
|
||||||
while (_buffers.TryPeek(out var buffer))
|
|
||||||
{
|
|
||||||
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
|
||||||
if ((header.Flags & WaveHeaderDone) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buffers.Dequeue();
|
|
||||||
ReleaseBuffer(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReleaseBuffer(NativeBuffer buffer)
|
|
||||||
{
|
|
||||||
WaveOutUnprepareHeader(
|
|
||||||
_device,
|
|
||||||
buffer.Header,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
_queuedPcmBytes -= buffer.Length;
|
|
||||||
Marshal.FreeHGlobal(buffer.Header);
|
|
||||||
Marshal.FreeHGlobal(buffer.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConvertToStereoPcm16(
|
|
||||||
ReadOnlySpan<byte> source,
|
|
||||||
Span<byte> destination,
|
|
||||||
int frames,
|
|
||||||
int channels,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
|
||||||
for (var frame = 0; frame < frames; frame++)
|
|
||||||
{
|
|
||||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
|
||||||
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
|
||||||
var right = channels == 1
|
|
||||||
? left
|
|
||||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static short ReadSample(
|
|
||||||
ReadOnlySpan<byte> frame,
|
|
||||||
int channel,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
|
||||||
if (!isFloat)
|
|
||||||
{
|
|
||||||
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
|
||||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
|
||||||
return checked((short)MathF.Round(value * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
|
||||||
private struct WaveFormat
|
|
||||||
{
|
|
||||||
public ushort FormatTag;
|
|
||||||
public ushort Channels;
|
|
||||||
public uint SamplesPerSecond;
|
|
||||||
public uint AverageBytesPerSecond;
|
|
||||||
public ushort BlockAlign;
|
|
||||||
public ushort BitsPerSample;
|
|
||||||
public ushort ExtraSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct WaveHeader
|
|
||||||
{
|
|
||||||
public IntPtr Data;
|
|
||||||
public uint BufferLength;
|
|
||||||
public uint BytesRecorded;
|
|
||||||
public nuint User;
|
|
||||||
public uint Flags;
|
|
||||||
public uint Loops;
|
|
||||||
public IntPtr Next;
|
|
||||||
public nuint Reserved;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
|
||||||
private static extern uint WaveOutOpen(
|
|
||||||
out IntPtr device,
|
|
||||||
uint deviceId,
|
|
||||||
ref WaveFormat format,
|
|
||||||
IntPtr callback,
|
|
||||||
IntPtr instance,
|
|
||||||
uint flags);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
|
||||||
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
|
||||||
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
|
||||||
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
|
||||||
private static extern uint WaveOutReset(IntPtr device);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
|
||||||
private static extern uint WaveOutClose(IntPtr device);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs;
|
|
||||||
|
|
||||||
public static class HostTimerResolution
|
|
||||||
{
|
|
||||||
private const uint TargetPeriodMilliseconds = 1;
|
|
||||||
|
|
||||||
private static int _requested;
|
|
||||||
|
|
||||||
public static void Request()
|
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _requested, 1) != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] Host timer resolution request rejected; " +
|
|
||||||
"timed waits keep the default ~15.6 ms granularity.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (DllNotFoundException exception)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
|
|
||||||
private static extern uint TimeBeginPeriod(uint uPeriod);
|
|
||||||
}
|
|
||||||
@@ -125,6 +125,13 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
private static ulong _nextPhysicalAddress;
|
private static ulong _nextPhysicalAddress;
|
||||||
private static ulong _nextVirtualAddress;
|
private static ulong _nextVirtualAddress;
|
||||||
|
// First guest virtual address handed out for direct/flexible mappings
|
||||||
|
// when the game does not request one. 4GB is free on Windows, but on
|
||||||
|
// POSIX hosts it belongs to the host image / runtime (the Mach-O image
|
||||||
|
// base is 0x100000000 on macOS), so search from a guest-owned window
|
||||||
|
// well clear of host mappings instead.
|
||||||
|
private static readonly ulong DefaultMapSearchBase =
|
||||||
|
OperatingSystem.IsWindows() ? 0x1_0000_0000UL : 0x20_0000_0000UL;
|
||||||
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
||||||
private static ulong _allocatedFlexibleBytes;
|
private static ulong _allocatedFlexibleBytes;
|
||||||
private static ulong _threadAtexitCountCallback;
|
private static ulong _threadAtexitCountCallback;
|
||||||
@@ -241,7 +248,7 @@ public static class KernelMemoryCompatExports
|
|||||||
lock (_memoryGate)
|
lock (_memoryGate)
|
||||||
{
|
{
|
||||||
var desiredAddress = AlignUp(
|
var desiredAddress = AlignUp(
|
||||||
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress,
|
||||||
effectiveAlignment);
|
effectiveAlignment);
|
||||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
||||||
address == 0)
|
address == 0)
|
||||||
@@ -3185,7 +3192,7 @@ public static class KernelMemoryCompatExports
|
|||||||
? requestedAddress
|
? requestedAddress
|
||||||
: directMemoryStart != 0
|
: directMemoryStart != 0
|
||||||
? AlignUp(directMemoryStart, effectiveAlignment)
|
? AlignUp(directMemoryStart, effectiveAlignment)
|
||||||
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, effectiveAlignment);
|
: AlignUp(_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress, effectiveAlignment);
|
||||||
|
|
||||||
var reserved = false;
|
var reserved = false;
|
||||||
if (fixedMapping && requestedAddress != 0)
|
if (fixedMapping && requestedAddress != 0)
|
||||||
@@ -3291,7 +3298,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var fixedMapping = (flags & 0x10UL) != 0;
|
var fixedMapping = (flags & 0x10UL) != 0;
|
||||||
var desiredAddress = requestedAddress != 0
|
var desiredAddress = requestedAddress != 0
|
||||||
? requestedAddress
|
? requestedAddress
|
||||||
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, 0x1000UL);
|
: AlignUp(_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress, 0x1000UL);
|
||||||
|
|
||||||
if (fixedMapping && requestedAddress != 0)
|
if (fixedMapping && requestedAddress != 0)
|
||||||
{
|
{
|
||||||
@@ -6191,6 +6198,55 @@ public static class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool TryReadTrackedLibcHeapGpuAlias(
|
||||||
|
ulong packedAddress,
|
||||||
|
Span<byte> destination)
|
||||||
|
{
|
||||||
|
if (destination.IsEmpty)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gen5 texture descriptors retain 46 bits of the byte address. Host
|
||||||
|
// libc allocations can live at 0x7F... on Linux, so recover the full
|
||||||
|
// tracked allocation address when the descriptor contains its packed
|
||||||
|
// low-bit alias.
|
||||||
|
const ulong textureAddressMask = (1UL << 46) - 1;
|
||||||
|
var length = (ulong)destination.Length;
|
||||||
|
ulong resolvedAddress = 0;
|
||||||
|
lock (_libcAllocGate)
|
||||||
|
{
|
||||||
|
foreach (var (allocationAddress, allocation) in _libcAllocations)
|
||||||
|
{
|
||||||
|
var packedBase = allocationAddress & textureAddressMask;
|
||||||
|
if (packedAddress < packedBase)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = packedAddress - packedBase;
|
||||||
|
var allocationSize = (ulong)allocation.Size;
|
||||||
|
if (offset > allocationSize || length > allocationSize - offset)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = allocationAddress + offset;
|
||||||
|
if (resolvedAddress != 0 && resolvedAddress != candidate)
|
||||||
|
{
|
||||||
|
// Do not guess if two live host allocations collide after
|
||||||
|
// descriptor address packing.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedAddress = candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedAddress != 0 &&
|
||||||
|
TryReadHostMemory(resolvedAddress, destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
|
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
|
||||||
{
|
{
|
||||||
address = 0;
|
address = 0;
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw");
|
// Expected when a fixed-address request cannot be satisfied on
|
||||||
|
// this host; the caller falls back or reports the failure.
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] {traceName}: no host mapping at 0x{desiredAddress:X16} len=0x{length:X}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
|
using Silk.NET.Input;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keyboard and gamepad state sampled from the presenter's window, feeding
|
||||||
|
/// the POSIX host input seam (macOS/Linux have no user32/XInput/raw-HID
|
||||||
|
/// readers). The presenter attaches the window's input context once the
|
||||||
|
/// window exists; input events arrive on the window thread and pad reads
|
||||||
|
/// happen on guest threads, so all state is guarded.
|
||||||
|
/// </summary>
|
||||||
|
public static class HostWindowInput
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly HashSet<Key> Pressed = new();
|
||||||
|
private static volatile bool _connected;
|
||||||
|
|
||||||
|
// Latest window-gamepad snapshot in the host seam's conventions.
|
||||||
|
private static bool _gamepadConnected;
|
||||||
|
private static string? _gamepadName;
|
||||||
|
private static HostGamepadButtons _gamepadButtons;
|
||||||
|
private static byte _gamepadLeftX = 128;
|
||||||
|
private static byte _gamepadLeftY = 128;
|
||||||
|
private static byte _gamepadRightX = 128;
|
||||||
|
private static byte _gamepadRightY = 128;
|
||||||
|
private static byte _gamepadL2;
|
||||||
|
private static byte _gamepadR2;
|
||||||
|
|
||||||
|
/// <summary>True once a window keyboard is delivering events.</summary>
|
||||||
|
public static bool IsConnected => _connected;
|
||||||
|
|
||||||
|
public static void Attach(IInputContext input)
|
||||||
|
{
|
||||||
|
foreach (var keyboard in input.Keyboards)
|
||||||
|
{
|
||||||
|
keyboard.KeyDown += (_, key, _) =>
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
Pressed.Add(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
keyboard.KeyUp += (_, key, _) =>
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
Pressed.Remove(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Keyboards.Count > 0)
|
||||||
|
{
|
||||||
|
_connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var gamepad in input.Gamepads)
|
||||||
|
{
|
||||||
|
AttachGamepad(gamepad);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.ConnectionChanged += (device, connected) =>
|
||||||
|
{
|
||||||
|
if (device is not IGamepad gamepad)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
AttachGamepad(gamepad);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_gamepadConnected = false;
|
||||||
|
_gamepadName = null;
|
||||||
|
_gamepadButtons = HostGamepadButtons.None;
|
||||||
|
_gamepadLeftX = 128;
|
||||||
|
_gamepadLeftY = 128;
|
||||||
|
_gamepadRightX = 128;
|
||||||
|
_gamepadRightY = 128;
|
||||||
|
_gamepadL2 = 0;
|
||||||
|
_gamepadR2 = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PosixHostInput.SetSource(new WindowInputSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsKeyDown(Key key)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return Pressed.Contains(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WindowInputSource : IPosixWindowInputSource
|
||||||
|
{
|
||||||
|
public bool HasKeyboardFocus => _connected;
|
||||||
|
|
||||||
|
public bool IsKeyDown(int virtualKey)
|
||||||
|
{
|
||||||
|
return TryMapVirtualKey(virtualKey, out var key) && HostWindowInput.IsKeyDown(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!_gamepadConnected || destination.Length == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[0] = new HostGamepadState(
|
||||||
|
Connected: true,
|
||||||
|
Buttons: _gamepadButtons,
|
||||||
|
LeftX: _gamepadLeftX,
|
||||||
|
LeftY: _gamepadLeftY,
|
||||||
|
RightX: _gamepadRightX,
|
||||||
|
RightY: _gamepadRightY,
|
||||||
|
LeftTrigger: _gamepadL2,
|
||||||
|
RightTrigger: _gamepadR2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? DescribeConnectedGamepad()
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return _gamepadConnected ? _gamepadName ?? "GLFW gamepad" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryMapVirtualKey(int vk, out Key key)
|
||||||
|
{
|
||||||
|
key = vk switch
|
||||||
|
{
|
||||||
|
0x08 => Key.Backspace,
|
||||||
|
0x09 => Key.Tab,
|
||||||
|
0x0D => Key.Enter,
|
||||||
|
0x1B => Key.Escape,
|
||||||
|
0x25 => Key.Left,
|
||||||
|
0x26 => Key.Up,
|
||||||
|
0x27 => Key.Right,
|
||||||
|
0x28 => Key.Down,
|
||||||
|
>= 0x41 and <= 0x5A => Key.A + (vk - 0x41),
|
||||||
|
_ => Key.Unknown,
|
||||||
|
};
|
||||||
|
return key != Key.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AttachGamepad(IGamepad gamepad)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_gamepadConnected = true;
|
||||||
|
_gamepadName = gamepad.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
gamepad.ButtonDown += (_, button) =>
|
||||||
|
{
|
||||||
|
var bit = MapButton(button.Name);
|
||||||
|
if (bit == HostGamepadButtons.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_gamepadButtons |= bit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gamepad.ButtonUp += (_, button) =>
|
||||||
|
{
|
||||||
|
var bit = MapButton(button.Name);
|
||||||
|
if (bit == HostGamepadButtons.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_gamepadButtons &= ~bit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gamepad.ThumbstickMoved += (_, thumbstick) =>
|
||||||
|
{
|
||||||
|
// Silk's GLFW backend reports sticks -1..1 with +Y pointing down,
|
||||||
|
// matching the seam's 0..255 down-growing convention after biasing.
|
||||||
|
var x = ToStickByte(thumbstick.X);
|
||||||
|
var y = ToStickByte(thumbstick.Y);
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (thumbstick.Index == 0)
|
||||||
|
{
|
||||||
|
_gamepadLeftX = x;
|
||||||
|
_gamepadLeftY = y;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gamepadRightX = x;
|
||||||
|
_gamepadRightY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gamepad.TriggerMoved += (_, trigger) =>
|
||||||
|
{
|
||||||
|
// GLFW gamepad triggers rest at -1 and saturate at +1.
|
||||||
|
var value = (byte)Math.Clamp((int)((trigger.Position + 1.0f) * 0.5f * 255.0f), 0, 255);
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (trigger.Index == 0)
|
||||||
|
{
|
||||||
|
_gamepadL2 = value;
|
||||||
|
if (value > 64)
|
||||||
|
{
|
||||||
|
_gamepadButtons |= HostGamepadButtons.L2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gamepadButtons &= ~HostGamepadButtons.L2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gamepadR2 = value;
|
||||||
|
if (value > 64)
|
||||||
|
{
|
||||||
|
_gamepadButtons |= HostGamepadButtons.R2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gamepadButtons &= ~HostGamepadButtons.R2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte ToStickByte(float value)
|
||||||
|
{
|
||||||
|
return (byte)Math.Clamp((int)(128.0f + value * 127.0f), 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
||||||
|
{
|
||||||
|
// GLFW reports the Xbox layout: A=Cross, B=Circle, X=Square, Y=Triangle.
|
||||||
|
ButtonName.A => HostGamepadButtons.Cross,
|
||||||
|
ButtonName.B => HostGamepadButtons.Circle,
|
||||||
|
ButtonName.X => HostGamepadButtons.Square,
|
||||||
|
ButtonName.Y => HostGamepadButtons.Triangle,
|
||||||
|
ButtonName.LeftBumper => HostGamepadButtons.L1,
|
||||||
|
ButtonName.RightBumper => HostGamepadButtons.R1,
|
||||||
|
ButtonName.Back => HostGamepadButtons.TouchPad,
|
||||||
|
ButtonName.Start => HostGamepadButtons.Options,
|
||||||
|
ButtonName.LeftStick => HostGamepadButtons.L3,
|
||||||
|
ButtonName.RightStick => HostGamepadButtons.R3,
|
||||||
|
ButtonName.DPadUp => HostGamepadButtons.Up,
|
||||||
|
ButtonName.DPadRight => HostGamepadButtons.Right,
|
||||||
|
ButtonName.DPadDown => HostGamepadButtons.Down,
|
||||||
|
ButtonName.DPadLeft => HostGamepadButtons.Left,
|
||||||
|
_ => HostGamepadButtons.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
@@ -38,8 +38,7 @@ public static class PadExports
|
|||||||
public static int PadInit(CpuContext ctx)
|
public static int PadInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
DualSenseReader.EnsureStarted();
|
HostPlatform.Current.Input.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +80,13 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
var input = HostPlatform.Current.Input;
|
||||||
XInputReader.EnsureStarted();
|
input.EnsureStarted();
|
||||||
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
? $"[LOADER][INFO] Controls: {gamepadName} connected (keyboard fallback also active)."
|
||||||
: XInputReader.TryGetState(out _)
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
@@ -282,7 +279,7 @@ public static class PadExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var triggerMask = parameter[0];
|
var triggerMask = parameter[0];
|
||||||
XInputReader.SetTriggerRumble(
|
HostPlatform.Current.Input.SetTriggerRumble(
|
||||||
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||||
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
@@ -326,8 +323,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
|
||||||
XInputReader.SetRumble(parameter[0], parameter[1]);
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +353,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
|
HostPlatform.Current.Input.SetLightbar(color[0], color[1], color[2]);
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +370,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.ResetLightbar();
|
HostPlatform.Current.Input.ResetLightbar();
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,37 +416,35 @@ public static class PadExports
|
|||||||
return _cachedInputState;
|
return _cachedInputState;
|
||||||
}
|
}
|
||||||
|
|
||||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
var input = HostPlatform.Current.Input;
|
||||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
var acceptsKeyboardInput = input.IsHostWindowFocused();
|
||||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
|
||||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
|
||||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
|
||||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
|
||||||
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
|
||||||
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
var l2 = acceptsKeyboardInput && input.IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||||
|
var r2 = acceptsKeyboardInput && input.IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||||
|
|
||||||
if (DualSenseReader.TryGetState(out var pad))
|
Span<HostGamepadState> gamepads = stackalloc HostGamepadState[2];
|
||||||
|
var gamepadCount = input.GetGamepadStates(gamepads);
|
||||||
|
for (var index = 0; index < gamepadCount; index++)
|
||||||
{
|
{
|
||||||
buttons |= pad.Buttons;
|
var pad = gamepads[index];
|
||||||
|
buttons |= ToOrbisButtons(pad.Buttons);
|
||||||
// The controller stick wins whenever it is deflected past a
|
// The controller stick wins whenever it is deflected past a
|
||||||
// small deadzone; otherwise any keyboard value stays.
|
// small deadzone; otherwise any keyboard value stays.
|
||||||
leftX = MergeAxis(pad.LeftX, leftX);
|
leftX = MergeAxis(pad.LeftX, leftX);
|
||||||
leftY = MergeAxis(pad.LeftY, leftY);
|
leftY = MergeAxis(pad.LeftY, leftY);
|
||||||
rightX = MergeAxis(pad.RightX, rightX);
|
rightX = MergeAxis(pad.RightX, rightX);
|
||||||
rightY = MergeAxis(pad.RightY, rightY);
|
rightY = MergeAxis(pad.RightY, rightY);
|
||||||
l2 = Math.Max(l2, pad.L2);
|
l2 = Math.Max(l2, pad.LeftTrigger);
|
||||||
r2 = Math.Max(r2, pad.R2);
|
r2 = Math.Max(r2, pad.RightTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (XInputReader.TryGetState(out var xpad))
|
if (IsAutoCrossActive())
|
||||||
{
|
{
|
||||||
buttons |= xpad.Buttons;
|
buttons |= 0x4000;
|
||||||
leftX = MergeAxis(xpad.LeftX, leftX);
|
|
||||||
leftY = MergeAxis(xpad.LeftY, leftY);
|
|
||||||
rightX = MergeAxis(xpad.RightX, rightX);
|
|
||||||
rightY = MergeAxis(xpad.RightY, rightY);
|
|
||||||
l2 = Math.Max(l2, xpad.L2);
|
|
||||||
r2 = Math.Max(r2, xpad.R2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedInputState = new PadState(
|
_cachedInputState = new PadState(
|
||||||
@@ -466,50 +460,94 @@ public static class PadExports
|
|||||||
return _cachedInputState;
|
return _cachedInputState;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
private static readonly long PadStartTimestamp = Stopwatch.GetTimestamp();
|
||||||
private static extern short GetAsyncKeyState(int vKey);
|
private static readonly double[] AutoCrossTimes = ParseAutoCrossTimes();
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
private static double[] ParseAutoCrossTimes()
|
||||||
private static extern nint GetForegroundWindow();
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
|
||||||
|
|
||||||
private static bool IsKeyDown(int vk) =>
|
|
||||||
(GetAsyncKeyState(vk) & 0x8000) != 0;
|
|
||||||
|
|
||||||
private static bool IsEmulatorWindowFocused()
|
|
||||||
{
|
{
|
||||||
var foregroundWindow = GetForegroundWindow();
|
// SHARPEMU_AUTO_CROSS="40,52,64": presses Cross for 0.4s at each
|
||||||
if (foregroundWindow == 0)
|
// second offset from process start. Debug aid for unattended runs.
|
||||||
|
var raw = Environment.GetEnvironmentVariable("SHARPEMU_AUTO_CROSS");
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new List<double>();
|
||||||
|
foreach (var token in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (double.TryParse(token, System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||||
|
{
|
||||||
|
values.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAutoCrossActive()
|
||||||
|
{
|
||||||
|
var times = AutoCrossTimes;
|
||||||
|
if (times.Length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
var elapsed = (Stopwatch.GetTimestamp() - PadStartTimestamp) / (double)Stopwatch.Frequency;
|
||||||
return processId == (uint)Environment.ProcessId;
|
foreach (var time in times)
|
||||||
|
{
|
||||||
|
if (elapsed >= time && elapsed < time + 0.4)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ReadKeyboardButtons()
|
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
|
||||||
|
private static uint ToOrbisButtons(HostGamepadButtons buttons)
|
||||||
|
{
|
||||||
|
uint result = 0;
|
||||||
|
if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
|
||||||
|
if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
|
||||||
|
if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
|
||||||
|
if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
|
||||||
|
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
|
||||||
|
if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
|
||||||
|
if ((buttons & HostGamepadButtons.Square) != 0) result |= OrbisPadButton.Square;
|
||||||
|
if ((buttons & HostGamepadButtons.Triangle) != 0) result |= OrbisPadButton.Triangle;
|
||||||
|
if ((buttons & HostGamepadButtons.L1) != 0) result |= OrbisPadButton.L1;
|
||||||
|
if ((buttons & HostGamepadButtons.R1) != 0) result |= OrbisPadButton.R1;
|
||||||
|
if ((buttons & HostGamepadButtons.L2) != 0) result |= OrbisPadButton.L2;
|
||||||
|
if ((buttons & HostGamepadButtons.R2) != 0) result |= OrbisPadButton.R2;
|
||||||
|
if ((buttons & HostGamepadButtons.L3) != 0) result |= OrbisPadButton.L3;
|
||||||
|
if ((buttons & HostGamepadButtons.R3) != 0) result |= OrbisPadButton.R3;
|
||||||
|
if ((buttons & HostGamepadButtons.Options) != 0) result |= OrbisPadButton.Options;
|
||||||
|
if ((buttons & HostGamepadButtons.TouchPad) != 0) result |= OrbisPadButton.TouchPad;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ReadKeyboardButtons(IHostInput input)
|
||||||
{
|
{
|
||||||
uint buttons = 0;
|
uint buttons = 0;
|
||||||
// D-pad
|
// D-pad
|
||||||
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
|
if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
|
||||||
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
|
if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
|
||||||
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
|
if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
|
||||||
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
|
if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
|
||||||
// Face buttons
|
// Face buttons
|
||||||
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
|
if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
|
||||||
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
|
if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
|
||||||
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
|
if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
|
||||||
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
|
if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
|
||||||
// Shoulder buttons
|
// Shoulder buttons
|
||||||
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
|
if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
|
||||||
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
|
if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
|
||||||
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
|
if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
|
||||||
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
|
if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
|
||||||
// Options (Start)
|
// Options (Start)
|
||||||
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
|
if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Silk.NET.Input" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Audio;
|
using SharpEmu.Libs.Audio;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
@@ -68,7 +69,7 @@ public static class VideoOutExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HostTimerResolution.Request();
|
HostPlatform.Current.Threading.RequestTimerResolution();
|
||||||
|
|
||||||
_vblankPumpThread = new Thread(VblankPumpLoop)
|
_vblankPumpThread = new Thread(VblankPumpLoop)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
using Silk.NET.Core;
|
using Silk.NET.Core;
|
||||||
using Silk.NET.Core.Native;
|
using Silk.NET.Core.Native;
|
||||||
using Silk.NET.Maths;
|
using Silk.NET.Maths;
|
||||||
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Agc;
|
using SharpEmu.Libs.Agc;
|
||||||
|
using Silk.NET.Input;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using Silk.NET.Vulkan.Extensions.KHR;
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
using Silk.NET.Vulkan.Extensions.EXT;
|
using Silk.NET.Vulkan.Extensions.EXT;
|
||||||
@@ -164,7 +166,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private const uint DefaultWindowWidth = 1280;
|
private const uint DefaultWindowWidth = 1280;
|
||||||
private const uint DefaultWindowHeight = 720;
|
private const uint DefaultWindowHeight = 720;
|
||||||
private const int MaxPendingGuestWork = 16;
|
private const int MaxPendingGuestWork = 16;
|
||||||
private const int MaxGuestWorkPerRender = 16;
|
// A single guest frame commonly contains 30-50 translated draws. Limiting
|
||||||
|
// this to 16 split one frame across several 60 Hz window callbacks and
|
||||||
|
// unnecessarily throttled the producer behind the bounded work queue.
|
||||||
|
private const int MaxGuestWorkPerRender = 128;
|
||||||
private const uint GuestPrimitiveRectList = 0x11;
|
private const uint GuestPrimitiveRectList = 0x11;
|
||||||
private const uint GuestFormatR32Uint = 0x10004;
|
private const uint GuestFormatR32Uint = 0x10004;
|
||||||
private const uint GuestFormatR32Sint = 0x20004;
|
private const uint GuestFormatR32Sint = 0x20004;
|
||||||
@@ -189,8 +194,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static uint _windowWidth;
|
private static uint _windowWidth;
|
||||||
private static uint _windowHeight;
|
private static uint _windowHeight;
|
||||||
private static bool _closed;
|
private static bool _closed;
|
||||||
|
private static bool _presenterCloseRequested;
|
||||||
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
||||||
private const uint NvidiaVendorId = 0x10DE;
|
private const uint NvidiaVendorId = 0x10DE;
|
||||||
|
private const string PortabilityEnumerationExtensionName = "VK_KHR_portability_enumeration";
|
||||||
|
private const string PortabilitySubsetExtensionName = "VK_KHR_portability_subset";
|
||||||
private static bool _splashHidden;
|
private static bool _splashHidden;
|
||||||
private static long _enqueuedGuestWorkSequence;
|
private static long _enqueuedGuestWorkSequence;
|
||||||
private static long _completedGuestWorkSequence;
|
private static long _completedGuestWorkSequence;
|
||||||
@@ -202,6 +210,29 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceGuestImageSubmissionsForDiagnostics()
|
||||||
|
{
|
||||||
|
return string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldSamplePresentedGuestImageForDiagnostics(long frame)
|
||||||
|
{
|
||||||
|
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||||
|
if (string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// A 4K Vulkan readback is deliberately synchronous and can take
|
||||||
|
// several seconds on Linux. The lightweight "present" mode only
|
||||||
|
// needs one proof that the final image is non-black.
|
||||||
|
return frame == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(mode, "1", StringComparison.Ordinal) &&
|
||||||
|
(frame is 1 or 30 or 120 || frame % 600 == 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static void EnsureStarted(uint width, uint height)
|
public static void EnsureStarted(uint width, uint height)
|
||||||
{
|
{
|
||||||
if (width == 0 || height == 0)
|
if (width == 0 || height == 0)
|
||||||
@@ -259,12 +290,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
_thread = new Thread(Run)
|
StartPresenterLocked();
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Vulkan VideoOut",
|
|
||||||
};
|
|
||||||
_thread.Start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +326,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTraceGuestImageSubmissionsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
|
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
|
||||||
}
|
}
|
||||||
@@ -330,12 +356,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_windowWidth = width;
|
_windowWidth = width;
|
||||||
_windowHeight = height;
|
_windowHeight = height;
|
||||||
_thread = new Thread(Run)
|
StartPresenterLocked();
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Vulkan VideoOut",
|
|
||||||
};
|
|
||||||
_thread.Start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +367,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTraceGuestImageSubmissionsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
|
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
|
||||||
}
|
}
|
||||||
@@ -380,12 +401,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_windowWidth = width;
|
_windowWidth = width;
|
||||||
_windowHeight = height;
|
_windowHeight = height;
|
||||||
_thread = new Thread(Run)
|
StartPresenterLocked();
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Vulkan VideoOut",
|
|
||||||
};
|
|
||||||
_thread.Start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +425,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTraceGuestImageSubmissionsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
|
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
|
||||||
@@ -451,12 +467,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_windowWidth = width;
|
_windowWidth = width;
|
||||||
_windowHeight = height;
|
_windowHeight = height;
|
||||||
_thread = new Thread(Run)
|
StartPresenterLocked();
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Vulkan VideoOut",
|
|
||||||
};
|
|
||||||
_thread.Start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +533,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTraceGuestImageSubmissionsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
||||||
@@ -671,7 +682,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
// VideoOut registration does not imply a rendered Vulkan image.
|
// VideoOut registration does not imply a rendered Vulkan image.
|
||||||
var known = _gpuGuestImages.ContainsKey(address);
|
var known = _gpuGuestImages.ContainsKey(address);
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
if (ShouldTraceGuestImageSubmissionsForDiagnostics())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
|
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
|
||||||
@@ -708,7 +719,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
sequence,
|
sequence,
|
||||||
GuestDrawKind.None,
|
GuestDrawKind.None,
|
||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: 0,
|
// A flip targets the image produced by all work already queued
|
||||||
|
// for this frame. Do not expose it until those draws finish.
|
||||||
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false,
|
IsSplash: false,
|
||||||
GuestImageAddress: address);
|
GuestImageAddress: address);
|
||||||
System.Threading.Monitor.PulseAll(_gate);
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
@@ -964,6 +977,200 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return pixels;
|
return pixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void StartPresenterLocked()
|
||||||
|
{
|
||||||
|
if (HostMainThread.IsAvailable)
|
||||||
|
{
|
||||||
|
// GLFW windowing must run on the process main thread (AppKit on
|
||||||
|
// macOS, X11's single event queue on Linux), so hand the whole
|
||||||
|
// window loop to the main-thread pump the CLI parked for us.
|
||||||
|
// _thread only marks the presenter as running; Run() clears it on
|
||||||
|
// exit either way.
|
||||||
|
_thread = Thread.CurrentThread;
|
||||||
|
HostMainThread.SetShutdownRequestHandler(RequestClose);
|
||||||
|
HostMainThread.Post(Run);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu Vulkan VideoOut",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks a running presenter to close its window; used at emulator
|
||||||
|
/// shutdown so a main-thread-hosted window loop returns to the pump.
|
||||||
|
/// </summary>
|
||||||
|
public static void RequestClose()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _presenterCloseRequested, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GLFW resolves Vulkan with dlopen("libvulkan.1.dylib"), which cannot
|
||||||
|
/// find the app-local MoltenVK on macOS (Homebrew's Vulkan libraries are
|
||||||
|
/// arm64-only and this is an x86-64 process). GLFW 3.4 accepts the
|
||||||
|
/// loader entry point directly instead, so hand it MoltenVK's
|
||||||
|
/// vkGetInstanceProcAddr before any window exists.
|
||||||
|
/// </summary>
|
||||||
|
private static unsafe void InitializeMacVulkanLoader()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nint vulkan = 0;
|
||||||
|
foreach (var candidate in new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "libvulkan.1.dylib"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "libMoltenVK.dylib"),
|
||||||
|
"libvulkan.1.dylib",
|
||||||
|
"libMoltenVK.dylib",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (System.Runtime.InteropServices.NativeLibrary.TryLoad(candidate, out vulkan))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vulkan == 0 ||
|
||||||
|
!System.Runtime.InteropServices.NativeLibrary.TryGetExport(
|
||||||
|
vulkan, "vkGetInstanceProcAddr", out var procAddr))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] No Vulkan loader for GLFW; place a universal libMoltenVK.dylib " +
|
||||||
|
"next to SharpEmu as libvulkan.1.dylib.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var glfw = System.Runtime.InteropServices.NativeLibrary.Load(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "libglfw.3.dylib"));
|
||||||
|
var initVulkanLoader = (delegate* unmanaged<nint, void>)
|
||||||
|
System.Runtime.InteropServices.NativeLibrary.GetExport(glfw, "glfwInitVulkanLoader");
|
||||||
|
initVulkanLoader(procAddr);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] GLFW Vulkan loader wired to MoltenVK.");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] GLFW Vulkan loader setup failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLFW platform enum (GLFW 3.4): glfwInitHint(GLFW_PLATFORM, ...) selects a
|
||||||
|
// backend, glfwGetPlatform() reports the one in use.
|
||||||
|
private const int GlfwPlatformHint = 0x00050003;
|
||||||
|
private const int GlfwPlatformWin32 = 0x00060001;
|
||||||
|
private const int GlfwPlatformCocoa = 0x00060002;
|
||||||
|
private const int GlfwPlatformWayland = 0x00060003;
|
||||||
|
private const int GlfwPlatformX11 = 0x00060004;
|
||||||
|
private const int GlfwPlatformNull = 0x00060005;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GLFW's native Wayland backend does not reliably map the Vulkan window
|
||||||
|
/// with some drivers (notably NVIDIA): the surface presents frames but the
|
||||||
|
/// window never becomes visible, so the game runs with no picture while
|
||||||
|
/// audio works. XWayland is dependable, so on a Wayland session that also
|
||||||
|
/// exposes an X server (DISPLAY set) we force GLFW's X11 backend through
|
||||||
|
/// its GLFW_PLATFORM init hint before GLFW initializes — the supported way
|
||||||
|
/// to pick a backend, applied by calling into the same libglfw GLFW loads.
|
||||||
|
/// Opt back into native Wayland with SHARPEMU_ENABLE_WAYLAND=1.
|
||||||
|
/// </summary>
|
||||||
|
private static unsafe void PreferX11OnLinuxWayland()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsLinux() ||
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_ENABLE_WAYLAND"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only steer on a Wayland session (WAYLAND_DISPLAY set). Forcing X11
|
||||||
|
// needs an X server to fall back to (XWayland, DISPLAY set); without
|
||||||
|
// one, forcing it would make glfwInit fail outright, so leave GLFW
|
||||||
|
// alone and say why the window may not appear.
|
||||||
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY")))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Wayland session without an X server (DISPLAY unset); " +
|
||||||
|
"cannot steer GLFW to XWayland. If the window does not appear, install " +
|
||||||
|
"XWayland, or run natively with SHARPEMU_ENABLE_WAYLAND=1.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryLoadGlfw(out var glfw))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var initHint = (delegate* unmanaged<int, int, void>)
|
||||||
|
System.Runtime.InteropServices.NativeLibrary.GetExport(glfw, "glfwInitHint");
|
||||||
|
initHint(GlfwPlatformHint, GlfwPlatformX11);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Wayland session detected; requested GLFW X11/XWayland " +
|
||||||
|
"backend (set SHARPEMU_ENABLE_WAYLAND=1 to force native Wayland).");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Could not set GLFW X11 platform hint: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Logs the backend GLFW actually selected, so a "no window"
|
||||||
|
/// report shows Wayland vs X11 at a glance.</summary>
|
||||||
|
private static unsafe void LogGlfwPlatformInUse()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows() || !TryLoadGlfw(out var glfw))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getPlatform = (delegate* unmanaged<int>)
|
||||||
|
System.Runtime.InteropServices.NativeLibrary.GetExport(glfw, "glfwGetPlatform");
|
||||||
|
var platform = getPlatform();
|
||||||
|
var label = platform switch
|
||||||
|
{
|
||||||
|
GlfwPlatformWin32 => "Win32",
|
||||||
|
GlfwPlatformCocoa => "Cocoa",
|
||||||
|
GlfwPlatformWayland => "Wayland",
|
||||||
|
GlfwPlatformX11 => "X11",
|
||||||
|
GlfwPlatformNull => "Null",
|
||||||
|
_ => $"0x{platform:X}",
|
||||||
|
};
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] GLFW windowing platform in use: {label}");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Could not query GLFW platform: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoadGlfw(out nint handle)
|
||||||
|
{
|
||||||
|
var name = OperatingSystem.IsMacOS() ? "libglfw.3.dylib" : "libglfw.so.3";
|
||||||
|
return System.Runtime.InteropServices.NativeLibrary.TryLoad(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, name), out handle) ||
|
||||||
|
System.Runtime.InteropServices.NativeLibrary.TryLoad(name, out handle);
|
||||||
|
}
|
||||||
|
|
||||||
private static void Run()
|
private static void Run()
|
||||||
{
|
{
|
||||||
uint width;
|
uint width;
|
||||||
@@ -974,6 +1181,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
|
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InitializeMacVulkanLoader();
|
||||||
|
PreferX11OnLinuxWayland();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var presenter = new Presenter(width, height);
|
using var presenter = new Presenter(width, height);
|
||||||
@@ -1133,6 +1343,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private bool _swapchainRecreateDeferred;
|
private bool _swapchainRecreateDeferred;
|
||||||
private bool _tracedPresentedSwapchain;
|
private bool _tracedPresentedSwapchain;
|
||||||
private bool _swapchainReadbackPending;
|
private bool _swapchainReadbackPending;
|
||||||
|
private static int _guestImageDumpSequence;
|
||||||
|
private readonly System.Collections.Concurrent.ConcurrentQueue<GuestImageResource> _pendingAliasImageDumps = new();
|
||||||
private bool _deviceLost;
|
private bool _deviceLost;
|
||||||
private bool _deviceLostLogged;
|
private bool _deviceLostLogged;
|
||||||
private int _directPresentationCount;
|
private int _directPresentationCount;
|
||||||
@@ -1330,6 +1542,20 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_window.SetWindowIcon(ref icon);
|
_window.SetWindowIcon(ref icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogGlfwPlatformInUse();
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Pad.HostWindowInput.Attach(_window.CreateInput());
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Window keyboard input attached for pad emulation.");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Window keyboard input unavailable: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WaitForRenderDocAttachIfRequested();
|
WaitForRenderDocAttachIfRequested();
|
||||||
_vk = Vk.GetApi();
|
_vk = Vk.GetApi();
|
||||||
CreateInstance();
|
CreateInstance();
|
||||||
@@ -1542,8 +1768,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
var extensions = _window.VkSurface!.GetRequiredExtensions(out var extensionCount);
|
var extensions = _window.VkSurface!.GetRequiredExtensions(out var extensionCount);
|
||||||
byte* debugUtilsExtension = null;
|
byte* debugUtilsExtension = null;
|
||||||
|
byte* portabilityExtension = null;
|
||||||
|
var instanceCreateFlags = InstanceCreateFlags.None;
|
||||||
var enabledExtensionCount = (int)extensionCount;
|
var enabledExtensionCount = (int)extensionCount;
|
||||||
var enabledExtensions = stackalloc byte*[(int)extensionCount + 1];
|
var enabledExtensions = stackalloc byte*[(int)extensionCount + 2];
|
||||||
for (var index = 0; index < (int)extensionCount; index++)
|
for (var index = 0; index < (int)extensionCount; index++)
|
||||||
{
|
{
|
||||||
enabledExtensions[index] = extensions[index];
|
enabledExtensions[index] = extensions[index];
|
||||||
@@ -1555,6 +1783,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsInstanceExtensionAvailable(PortabilityEnumerationExtensionName))
|
||||||
|
{
|
||||||
|
// MoltenVK is a portability (non-conformant) implementation;
|
||||||
|
// without this flag + extension the loader hides it.
|
||||||
|
portabilityExtension = (byte*)SilkMarshal.StringToPtr(PortabilityEnumerationExtensionName);
|
||||||
|
enabledExtensions[enabledExtensionCount++] = portabilityExtension;
|
||||||
|
instanceCreateFlags |= InstanceCreateFlags.EnumeratePortabilityBitKhr;
|
||||||
|
}
|
||||||
|
|
||||||
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
||||||
{
|
{
|
||||||
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
||||||
@@ -1573,6 +1810,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var createInfo = new InstanceCreateInfo
|
var createInfo = new InstanceCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.InstanceCreateInfo,
|
SType = StructureType.InstanceCreateInfo,
|
||||||
|
Flags = instanceCreateFlags,
|
||||||
PApplicationInfo = &applicationInfo,
|
PApplicationInfo = &applicationInfo,
|
||||||
EnabledExtensionCount = (uint)enabledExtensionCount,
|
EnabledExtensionCount = (uint)enabledExtensionCount,
|
||||||
PpEnabledExtensionNames = enabledExtensions,
|
PpEnabledExtensionNames = enabledExtensions,
|
||||||
@@ -1601,6 +1839,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
SilkMarshal.Free((nint)debugUtilsExtension);
|
SilkMarshal.Free((nint)debugUtilsExtension);
|
||||||
}
|
}
|
||||||
|
if (portabilityExtension is not null)
|
||||||
|
{
|
||||||
|
SilkMarshal.Free((nint)portabilityExtension);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -1613,6 +1855,40 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsDeviceExtensionAvailable(string extensionName)
|
||||||
|
{
|
||||||
|
uint extensionCount = 0;
|
||||||
|
if (_vk.EnumerateDeviceExtensionProperties(_physicalDevice, (byte*)null, &extensionCount, null) != Result.Success ||
|
||||||
|
extensionCount == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = new ExtensionProperties[extensionCount];
|
||||||
|
fixed (ExtensionProperties* propertyPointer = properties)
|
||||||
|
{
|
||||||
|
if (_vk.EnumerateDeviceExtensionProperties(
|
||||||
|
_physicalDevice,
|
||||||
|
(byte*)null,
|
||||||
|
&extensionCount,
|
||||||
|
propertyPointer) != Result.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = Encoding.UTF8.GetBytes(extensionName);
|
||||||
|
for (var index = 0; index < extensionCount; index++)
|
||||||
|
{
|
||||||
|
if (Utf8NullTerminatedEquals(propertyPointer[index].ExtensionName, expected))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsInstanceLayerAvailable(string layerName)
|
private bool IsInstanceLayerAvailable(string layerName)
|
||||||
{
|
{
|
||||||
uint layerCount = 0;
|
uint layerCount = 0;
|
||||||
@@ -1748,8 +2024,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||||
_maxColorAttachments = selected.Limits.MaxColorAttachments;
|
_maxColorAttachments = selected.Limits.MaxColorAttachments;
|
||||||
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
||||||
|
var apiMajor = (selected.ApiVersion >> 22) & 0x7F;
|
||||||
|
var apiMinor = (selected.ApiVersion >> 12) & 0x3FF;
|
||||||
|
var apiPatch = selected.ApiVersion & 0xFFF;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
|
$"[LOADER][INFO] Vulkan device: {selectedName} " +
|
||||||
|
$"(type={selected.DeviceType}, api={apiMajor}.{apiMinor}.{apiPatch})");
|
||||||
VideoOutExports.SetSelectedGpuName(selectedName);
|
VideoOutExports.SetSelectedGpuName(selectedName);
|
||||||
_window.Title = VideoOutExports.GetWindowTitle();
|
_window.Title = VideoOutExports.GetWindowTitle();
|
||||||
}
|
}
|
||||||
@@ -1859,6 +2139,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
||||||
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
||||||
|
var supportsRobustBufferAccess2 = robustness2Features.RobustBufferAccess2;
|
||||||
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
||||||
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
||||||
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
||||||
@@ -1879,9 +2160,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
||||||
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
|
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
|
||||||
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
|
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
|
||||||
|
var portabilitySubsetExtension = (byte*)SilkMarshal.StringToPtr(PortabilitySubsetExtensionName);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var extensions = stackalloc byte*[3];
|
var extensions = stackalloc byte*[4];
|
||||||
var extensionCount = 0u;
|
var extensionCount = 0u;
|
||||||
extensions[extensionCount++] = swapchainExtension;
|
extensions[extensionCount++] = swapchainExtension;
|
||||||
if (supportsMaintenance8)
|
if (supportsMaintenance8)
|
||||||
@@ -1894,10 +2176,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
extensions[extensionCount++] = robustness2Extension;
|
extensions[extensionCount++] = robustness2Extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsDeviceExtensionAvailable(PortabilitySubsetExtensionName))
|
||||||
|
{
|
||||||
|
// The spec requires enabling this when the (MoltenVK)
|
||||||
|
// device advertises it.
|
||||||
|
extensions[extensionCount++] = portabilitySubsetExtension;
|
||||||
|
}
|
||||||
|
|
||||||
maintenance8Features.Maintenance8 = supportsMaintenance8;
|
maintenance8Features.Maintenance8 = supportsMaintenance8;
|
||||||
maintenance8Features.PNext = null;
|
maintenance8Features.PNext = null;
|
||||||
robustness2Features.RobustBufferAccess2 =
|
robustness2Features.RobustBufferAccess2 =
|
||||||
supportsRobustImageAccess2 && supportedFeatures.RobustBufferAccess;
|
supportsRobustBufferAccess2 && supportedFeatures.RobustBufferAccess;
|
||||||
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
|
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
|
||||||
robustness2Features.NullDescriptor = supportsNullDescriptor;
|
robustness2Features.NullDescriptor = supportsNullDescriptor;
|
||||||
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
|
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
|
||||||
@@ -1926,6 +2215,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
SilkMarshal.Free((nint)swapchainExtension);
|
SilkMarshal.Free((nint)swapchainExtension);
|
||||||
SilkMarshal.Free((nint)maintenance8Extension);
|
SilkMarshal.Free((nint)maintenance8Extension);
|
||||||
SilkMarshal.Free((nint)robustness2Extension);
|
SilkMarshal.Free((nint)robustness2Extension);
|
||||||
|
SilkMarshal.Free((nint)portabilitySubsetExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||||
@@ -3371,6 +3661,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"tile={texture.TileMode} format={vkFormat}");
|
$"tile={texture.TileMode} format={vkFormat}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES"),
|
||||||
|
"alias",
|
||||||
|
StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
_tracedGuestImageContents.Add(guestImage.Address))
|
||||||
|
{
|
||||||
|
// Deferred: reading back here would clobber the command
|
||||||
|
// buffer mid-recording; drained after the next present.
|
||||||
|
_pendingAliasImageDumps.Enqueue(guestImage);
|
||||||
|
}
|
||||||
|
|
||||||
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
|
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
|
||||||
{
|
{
|
||||||
return refresh;
|
return refresh;
|
||||||
@@ -4464,6 +4765,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
checked((uint)(bottom - top)));
|
checked((uint)(bottom - top)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly float ViewportDebugEpsilon = float.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_VIEWPORT_EPSILON"),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var viewportEpsilon)
|
||||||
|
? viewportEpsilon
|
||||||
|
: 0f;
|
||||||
|
|
||||||
private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
|
private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
|
||||||
{
|
{
|
||||||
if (viewport is not { } rect)
|
if (viewport is not { } rect)
|
||||||
@@ -4471,21 +4780,26 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
|
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxX = (float)extent.Width;
|
// Do NOT trim the rectangle to the render target: Vulkan allows
|
||||||
var maxY = (float)extent.Height;
|
// viewports that extend beyond the framebuffer (rendering is
|
||||||
var left = Math.Clamp(rect.X, 0f, maxX);
|
// confined by the scissor), and trimming changes the guest's
|
||||||
var right = Math.Clamp(rect.X + rect.Width, left, maxX);
|
// scale and offset. That skews texel addressing on 1:1 draws -
|
||||||
var yOrigin = Math.Clamp(rect.Y, 0f, maxY);
|
// source rows get skipped or duplicated - which shredded the
|
||||||
var yEnd = Math.Clamp(rect.Y + rect.Height, 0f, maxY);
|
// game's pre-composed tile surfaces. Only guard what the spec
|
||||||
|
// requires: a positive width and hardware viewport bounds.
|
||||||
|
const float bound = 32767f;
|
||||||
|
var x = Math.Clamp(rect.X, -bound, bound);
|
||||||
|
var y = Math.Clamp(rect.Y, -bound, bound);
|
||||||
|
var width = Math.Clamp(rect.Width, 1e-3f, bound);
|
||||||
|
var height = Math.Clamp(rect.Height, -bound, bound);
|
||||||
|
if (height == 0f)
|
||||||
|
{
|
||||||
|
height = extent.Height;
|
||||||
|
}
|
||||||
|
|
||||||
var minDepth = Math.Clamp(rect.MinDepth, 0f, 1f);
|
var minDepth = Math.Clamp(rect.MinDepth, 0f, 1f);
|
||||||
var maxDepth = Math.Clamp(rect.MaxDepth, minDepth, 1f);
|
var maxDepth = Math.Clamp(rect.MaxDepth, minDepth, 1f);
|
||||||
return new Viewport(
|
return new Viewport(x, y, width, height, minDepth, maxDepth);
|
||||||
left,
|
|
||||||
yOrigin,
|
|
||||||
right - left,
|
|
||||||
yEnd - yOrigin,
|
|
||||||
minDepth,
|
|
||||||
maxDepth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] CreateFallbackTexturePixels(uint format, uint width, uint height, ulong expectedSize)
|
private static byte[] CreateFallbackTexturePixels(uint format, uint width, uint height, ulong expectedSize)
|
||||||
@@ -5075,10 +5389,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var target in targets)
|
foreach (var target in targets)
|
||||||
{
|
{
|
||||||
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
var traceSmallWrites =
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WRITES") == "small" &&
|
||||||
|
target.Width <= 512 && target.Height <= 256;
|
||||||
|
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address) || traceSmallWrites)
|
||||||
{
|
{
|
||||||
var writeCount = _tracedGuestWriteCounts.TryGetValue(
|
var writeCount = _tracedGuestWriteCounts.TryGetValue(
|
||||||
target.Address,
|
target.Address,
|
||||||
@@ -5086,7 +5402,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
? previousCount + 1
|
? previousCount + 1
|
||||||
: 1;
|
: 1;
|
||||||
_tracedGuestWriteCounts[target.Address] = writeCount;
|
_tracedGuestWriteCounts[target.Address] = writeCount;
|
||||||
if (writeCount <= 3)
|
if (writeCount <= (traceSmallWrites ? 48 : 3))
|
||||||
{
|
{
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
Check(
|
Check(
|
||||||
@@ -5599,7 +5915,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void UpdatePerformanceHud()
|
private void UpdatePerformanceHud()
|
||||||
{
|
{
|
||||||
if (!_performanceHudEnabled || !OperatingSystem.IsWindows())
|
if (!_performanceHudEnabled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5621,28 +5937,34 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var hottestThreadId = 0;
|
var hottestThreadId = 0;
|
||||||
var hottestThreadCpuSeconds = 0.0;
|
var hottestThreadCpuSeconds = 0.0;
|
||||||
|
|
||||||
foreach (ProcessThread thread in process.Threads)
|
// Per-thread CPU times and thread names come from Windows-only
|
||||||
|
// APIs; on POSIX the HUD reports process totals with an "idle"
|
||||||
|
// hottest-thread slot.
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
using (thread)
|
foreach (ProcessThread thread in process.Threads)
|
||||||
{
|
{
|
||||||
try
|
using (thread)
|
||||||
{
|
{
|
||||||
var threadId = thread.Id;
|
try
|
||||||
var cpu = thread.TotalProcessorTime;
|
|
||||||
currentThreadIds.Add(threadId);
|
|
||||||
currentThreadCpu[threadId] = cpu;
|
|
||||||
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
|
|
||||||
{
|
{
|
||||||
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
|
var threadId = thread.Id;
|
||||||
if (deltaSeconds > hottestThreadCpuSeconds)
|
var cpu = thread.TotalProcessorTime;
|
||||||
|
currentThreadIds.Add(threadId);
|
||||||
|
currentThreadCpu[threadId] = cpu;
|
||||||
|
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
|
||||||
{
|
{
|
||||||
hottestThreadCpuSeconds = deltaSeconds;
|
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
|
||||||
hottestThreadId = threadId;
|
if (deltaSeconds > hottestThreadCpuSeconds)
|
||||||
|
{
|
||||||
|
hottestThreadCpuSeconds = deltaSeconds;
|
||||||
|
hottestThreadId = threadId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (InvalidOperationException)
|
||||||
catch (InvalidOperationException)
|
{
|
||||||
{
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5812,6 +6134,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void Render(double _)
|
private void Render(double _)
|
||||||
{
|
{
|
||||||
|
if (Volatile.Read(ref _presenterCloseRequested))
|
||||||
|
{
|
||||||
|
_window.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_vulkanReady)
|
if (!_vulkanReady)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -5884,6 +6212,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
TranslatedDrawResources? translatedResources = null;
|
TranslatedDrawResources? translatedResources = null;
|
||||||
GuestImageResource? presentedGuestImage = null;
|
GuestImageResource? presentedGuestImage = null;
|
||||||
|
var tracePresentedGuestImage = false;
|
||||||
if (presentation.GuestImageAddress != 0 &&
|
if (presentation.GuestImageAddress != 0 &&
|
||||||
(!_guestImages.TryGetValue(
|
(!_guestImages.TryGetValue(
|
||||||
presentation.GuestImageAddress,
|
presentation.GuestImageAddress,
|
||||||
@@ -5895,13 +6224,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (presentedGuestImage is not null)
|
if (presentedGuestImage is not null)
|
||||||
{
|
{
|
||||||
_directPresentationCount++;
|
_directPresentationCount++;
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
if (ShouldSamplePresentedGuestImageForDiagnostics(
|
||||||
_directPresentationCount is 1 or 30 or 120)
|
_directPresentationCount))
|
||||||
{
|
{
|
||||||
|
tracePresentedGuestImage = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " +
|
$"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " +
|
||||||
$"addr=0x{presentedGuestImage.Address:X16}");
|
$"addr=0x{presentedGuestImage.Address:X16}");
|
||||||
TraceGuestImageContents(presentedGuestImage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6089,6 +6418,19 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CompletePendingPresentation(wait: true);
|
CompletePendingPresentation(wait: true);
|
||||||
TraceSwapchainReadback();
|
TraceSwapchainReadback();
|
||||||
}
|
}
|
||||||
|
// Report the actual presented pixels before starting the larger
|
||||||
|
// source-image readback. If a guest draw wedges the GPU, the
|
||||||
|
// source probe can block in vkQueueWaitIdle; doing it first used
|
||||||
|
// to hide whether the swapchain itself was black and made the
|
||||||
|
// diagnostic run stop immediately after vk.present_sample.
|
||||||
|
if (tracePresentedGuestImage && presentedGuestImage is not null)
|
||||||
|
{
|
||||||
|
TraceGuestImageContents(presentedGuestImage);
|
||||||
|
}
|
||||||
|
while (_pendingAliasImageDumps.TryDequeue(out var aliasImage))
|
||||||
|
{
|
||||||
|
TraceGuestImageContents(aliasImage);
|
||||||
|
}
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
|
|
||||||
_imageInitialized[imageIndex] = true;
|
_imageInitialized[imageIndex] = true;
|
||||||
@@ -6132,7 +6474,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var bytesPerPixel = GetReadbackBytesPerPixel(image.Format);
|
var bytesPerPixel = GetReadbackBytesPerPixel(image.Format);
|
||||||
if (bytesPerPixel == 0)
|
if (bytesPerPixel == 0)
|
||||||
{
|
{
|
||||||
TraceVulkanShader(
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][TRACE] " +
|
||||||
$"vk.guest_image addr=0x{image.Address:X16} " +
|
$"vk.guest_image addr=0x{image.Address:X16} " +
|
||||||
$"format={image.Format} readback=unsupported");
|
$"format={image.Format} readback=unsupported");
|
||||||
return;
|
return;
|
||||||
@@ -6267,7 +6610,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
(int)bytesPerPixel);
|
(int)bytesPerPixel);
|
||||||
var center = Convert.ToHexString(
|
var center = Convert.ToHexString(
|
||||||
bytes.Slice(centerOffset, (int)bytesPerPixel));
|
bytes.Slice(centerOffset, (int)bytesPerPixel));
|
||||||
TraceVulkanShader(
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][TRACE] " +
|
||||||
$"vk.guest_image addr=0x{image.Address:X16} " +
|
$"vk.guest_image addr=0x{image.Address:X16} " +
|
||||||
$"size={image.Width}x{image.Height} format={image.Format} " +
|
$"size={image.Width}x{image.Height} format={image.Format} " +
|
||||||
$"nonzero_bytes={nonzeroBytes}/{byteCount} " +
|
$"nonzero_bytes={nonzeroBytes}/{byteCount} " +
|
||||||
@@ -6299,9 +6643,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
|
var sequence = Interlocked.Increment(ref _guestImageDumpSequence);
|
||||||
var path = Path.Combine(
|
var path = Path.Combine(
|
||||||
directory,
|
directory,
|
||||||
$"0x{image.Address:X16}-{image.Width}x{image.Height}-{image.Format}.rgba");
|
$"{sequence:D4}-0x{image.Address:X16}-{image.Width}x{image.Height}-{image.Format}.rgba");
|
||||||
File.WriteAllBytes(path, bytes.ToArray());
|
File.WriteAllBytes(path, bytes.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6387,8 +6732,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasPriorContents = texture.GuestImage is { } guestImage &&
|
// InitialUploadPending means this upload still has to perform
|
||||||
(guestImage.Initialized || guestImage.InitialUploadPending);
|
// the image's first layout transition. Treating it as prior
|
||||||
|
// contents records ShaderReadOnlyOptimal as oldLayout even
|
||||||
|
// though a freshly created image is still Undefined. Linux
|
||||||
|
// validation reports VUID-vkCmdDraw-None-09600 and NVIDIA
|
||||||
|
// samples the uninitialized (black) image in that case.
|
||||||
|
var hasPriorContents =
|
||||||
|
texture.GuestImage is { Initialized: true };
|
||||||
var toTransfer = new ImageMemoryBarrier
|
var toTransfer = new ImageMemoryBarrier
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
@@ -6754,6 +7105,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var drawViewport = ClampViewport(resources.Viewport, extent);
|
var drawViewport = ClampViewport(resources.Viewport, extent);
|
||||||
|
if (ViewportDebugEpsilon != 0f)
|
||||||
|
{
|
||||||
|
drawViewport.X += ViewportDebugEpsilon;
|
||||||
|
drawViewport.Y += ViewportDebugEpsilon;
|
||||||
|
}
|
||||||
_vk.CmdSetViewport(_commandBuffer, 0, 1, &drawViewport);
|
_vk.CmdSetViewport(_commandBuffer, 0, 1, &drawViewport);
|
||||||
if (resources.VertexBuffers.Length != 0)
|
if (resources.VertexBuffers.Length != 0)
|
||||||
{
|
{
|
||||||
@@ -6989,7 +7345,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var sourceToTransfer = new ImageMemoryBarrier
|
var sourceToTransfer = new ImageMemoryBarrier
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
SrcAccessMask = AccessFlags.ShaderReadBit,
|
// An offscreen target is last written as a color attachment,
|
||||||
|
// then put in ShaderReadOnlyOptimal for later sampling. A
|
||||||
|
// layout-only handoff to ShaderRead does not make that write
|
||||||
|
// visible to this transfer when no shader sample occurs in
|
||||||
|
// between. NVIDIA's Linux driver exposed the resulting stale
|
||||||
|
// (usually black) image while Windows drivers happened to
|
||||||
|
// tolerate it. Include all preceding writes before blitting
|
||||||
|
// the image into the swapchain.
|
||||||
|
SrcAccessMask = AccessFlags.MemoryWriteBit | AccessFlags.ShaderReadBit,
|
||||||
DstAccessMask = AccessFlags.TransferReadBit,
|
DstAccessMask = AccessFlags.TransferReadBit,
|
||||||
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
|
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||||
@@ -7060,6 +7424,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
1),
|
1),
|
||||||
DstOffsets = destinationOffsets,
|
DstOffsets = destinationOffsets,
|
||||||
};
|
};
|
||||||
|
// Nearest keeps integer upscales pixel-crisp, but any fractional
|
||||||
|
// scale (e.g. a 3840x2160 guest frame into a 2560x1440 swapchain)
|
||||||
|
// must blend neighbours or it silently drops every Nth source
|
||||||
|
// row/column, which shreds 1-2px features in the guest frame.
|
||||||
|
var isIntegerUpscale =
|
||||||
|
source.Width != 0 && source.Height != 0 &&
|
||||||
|
_extent.Width >= source.Width && _extent.Height >= source.Height &&
|
||||||
|
_extent.Width % source.Width == 0 && _extent.Height % source.Height == 0;
|
||||||
_vk.CmdBlitImage(
|
_vk.CmdBlitImage(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
source.Image,
|
source.Image,
|
||||||
@@ -7068,7 +7440,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageLayout.TransferDstOptimal,
|
ImageLayout.TransferDstOptimal,
|
||||||
1,
|
1,
|
||||||
®ion,
|
®ion,
|
||||||
Filter.Nearest);
|
isIntegerUpscale ? Filter.Nearest : Filter.Linear);
|
||||||
|
|
||||||
if (traceDestination)
|
if (traceDestination)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net10.0": {
|
"net10.0": {
|
||||||
|
"Silk.NET.Input": {
|
||||||
|
"type": "Direct",
|
||||||
|
"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": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
@@ -69,6 +79,23 @@
|
|||||||
"Ultz.Native.GLFW": "3.4.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": {
|
"Silk.NET.Maths": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.23.0",
|
"resolved": "2.23.0",
|
||||||
|
|||||||
@@ -81,6 +81,23 @@
|
|||||||
"Ultz.Native.GLFW": "3.4.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": {
|
"Silk.NET.Maths": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.23.0",
|
"resolved": "2.23.0",
|
||||||
@@ -168,6 +185,7 @@
|
|||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Input": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
@@ -183,6 +201,16 @@
|
|||||||
"resolved": "1.21.0",
|
"resolved": "1.21.0",
|
||||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
"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": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
Reference in New Issue
Block a user