mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 14:39:42 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa2616d224 | |||
| 7b86a91dfa | |||
| 72645cb373 | |||
| 2ad9836d13 | |||
| 62e1775c5c | |||
| 6dacd59a08 | |||
| 9d88542efd | |||
| 373100a6b0 | |||
| f23161be9a | |||
| 081760be3f |
@@ -38,6 +38,7 @@ jobs:
|
|||||||
artifact-name: ${{ steps.vars.outputs.artifact-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 }}
|
||||||
steps:
|
steps:
|
||||||
- name: Compute workflow variables
|
- name: Compute workflow variables
|
||||||
@@ -53,6 +54,7 @@ jobs:
|
|||||||
|
|
||||||
{
|
{
|
||||||
echo "short-sha=${short_sha}"
|
echo "short-sha=${short_sha}"
|
||||||
|
echo "safe-ref=${safe_ref}"
|
||||||
echo "archive-name=${archive_name}"
|
echo "archive-name=${archive_name}"
|
||||||
echo "artifact-name=${artifact_name}"
|
echo "artifact-name=${artifact_name}"
|
||||||
echo "release-tag=${release_tag}"
|
echo "release-tag=${release_tag}"
|
||||||
@@ -100,6 +102,9 @@ jobs:
|
|||||||
- name: Build solution
|
- name: Build solution
|
||||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||||
|
|
||||||
|
- name: Validate synthetic shaders
|
||||||
|
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||||
|
|
||||||
- name: Publish win-x64 CLI
|
- name: Publish win-x64 CLI
|
||||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
@@ -121,6 +126,65 @@ jobs:
|
|||||||
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
|
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
|
||||||
if-no-files-found: error
|
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-${{ 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-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
needs:
|
needs:
|
||||||
@@ -158,3 +222,46 @@ jobs:
|
|||||||
--notes "${notes}" \
|
--notes "${notes}" \
|
||||||
--target "${GITHUB_SHA}"
|
--target "${GITHUB_SHA}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
release-posix:
|
||||||
|
name: Publish GitHub Release (${{ matrix.rid }})
|
||||||
|
needs:
|
||||||
|
- init
|
||||||
|
- build-posix
|
||||||
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
rid: [linux-x64, osx-x64]
|
||||||
|
steps:
|
||||||
|
- name: Download build artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||||
|
path: release
|
||||||
|
|
||||||
|
- name: Create or update release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ARCHIVE_NAME: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RELEASE_NAME: SharpEmu ${{ matrix.rid }} ${{ needs.init.outputs.short-sha }}
|
||||||
|
RELEASE_TAG: ${{ matrix.rid }}-${{ needs.init.outputs.safe-ref }}-${{ needs.init.outputs.short-sha }}
|
||||||
|
RID: ${{ matrix.rid }}
|
||||||
|
run: |
|
||||||
|
asset_path="release/${ARCHIVE_NAME}"
|
||||||
|
notes="Automated ${RID} build for commit ${GITHUB_SHA}."
|
||||||
|
|
||||||
|
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||||
|
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
|
||||||
|
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
||||||
|
else
|
||||||
|
gh release create "${RELEASE_TAG}" "${asset_path}" \
|
||||||
|
--title "${RELEASE_NAME}" \
|
||||||
|
--notes "${notes}" \
|
||||||
|
--target "${GITHUB_SHA}"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -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,7 +16,9 @@ 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>
|
||||||
@@ -29,6 +31,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<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 +75,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",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
@@ -21,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;
|
||||||
@@ -41,16 +49,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
];
|
];
|
||||||
private readonly IVirtualMemory _virtualMemory;
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
private readonly IModuleManager _moduleManager;
|
private readonly IModuleManager _moduleManager;
|
||||||
|
private readonly IHostPlatform? _hostPlatform;
|
||||||
private INativeCpuBackend? _nativeCpuBackend;
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
public CpuDispatcher(
|
public CpuDispatcher(
|
||||||
IVirtualMemory virtualMemory,
|
IVirtualMemory virtualMemory,
|
||||||
IModuleManager moduleManager,
|
IModuleManager moduleManager,
|
||||||
INativeCpuBackend? nativeCpuBackend = null)
|
INativeCpuBackend? nativeCpuBackend = null,
|
||||||
|
IHostPlatform? hostPlatform = null)
|
||||||
{
|
{
|
||||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
_nativeCpuBackend = nativeCpuBackend;
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
|
_hostPlatform = hostPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong? LastEntryPoint { get; private set; }
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
@@ -266,7 +277,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
entryFrameDiagnostic,
|
entryFrameDiagnostic,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
"CpuEngine: native-only");
|
"CpuEngine: native-only");
|
||||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
||||||
if (_nativeCpuBackend.TryExecute(
|
if (_nativeCpuBackend.TryExecute(
|
||||||
context,
|
context,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -134,8 +135,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num2 = 0;
|
int num2 = 0;
|
||||||
List<ulong> list = new List<ulong>(16);
|
List<ulong> list = new List<ulong>(16);
|
||||||
ulong num3 = scanStart;
|
ulong num3 = scanStart;
|
||||||
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
var hostMemory = ResolveDiagnosticsHostMemory();
|
||||||
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
HostRegionInfo lpBuffer;
|
||||||
|
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
||||||
{
|
{
|
||||||
ulong baseAddress = lpBuffer.BaseAddress;
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
@@ -145,7 +147,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
ulong value = Math.Max(num3, baseAddress);
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
ulong num5 = Math.Min(num4, scanEnd);
|
ulong num5 = Math.Min(num4, scanEnd);
|
||||||
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
||||||
{
|
{
|
||||||
ulong num6 = AlignUp(value, 8uL);
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
@@ -350,7 +352,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -359,7 +361,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -391,12 +393,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
||||||
if (executable)
|
if (executable)
|
||||||
{
|
{
|
||||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
@@ -415,6 +417,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return (value + num) & ~num;
|
return (value + num) & ~num;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnostics helpers are static (reachable from static handler paths), so
|
||||||
|
// they use the platform injected into the backend active on this thread and
|
||||||
|
// fall back to the process-wide singleton only when no run is bound.
|
||||||
|
private static IHostMemory ResolveDiagnosticsHostMemory()
|
||||||
|
{
|
||||||
|
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsReadableProtection(uint protect)
|
private static bool IsReadableProtection(uint protect)
|
||||||
{
|
{
|
||||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -20,14 +22,20 @@ 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 = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_rawExceptionHandlerStub == 0)
|
if (_rawExceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||||
}
|
}
|
||||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -37,22 +45,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_exceptionHandlerStub == 0)
|
if (_exceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||||
}
|
}
|
||||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||||
if (_unhandledFilterStub == 0)
|
if (_unhandledFilterStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
}
|
}
|
||||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -60,8 +68,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
||||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
@@ -100,8 +108,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
|
|
||||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||||
// the managed import gateway means the VEH->managed reentry happened from
|
// the managed import gateway means the VEH->managed reentry happened from
|
||||||
@@ -112,7 +120,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -127,10 +135,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
break;
|
break;
|
||||||
case 3221226505u:
|
case WindowsFaultCodes.FastFail:
|
||||||
{
|
{
|
||||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
@@ -140,21 +148,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rax = ReadCtxU64(contextRecord, 120);
|
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
||||||
ulong rbx = ReadCtxU64(contextRecord, 144);
|
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
||||||
ulong rcx = ReadCtxU64(contextRecord, 128);
|
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
||||||
ulong rdx = ReadCtxU64(contextRecord, 136);
|
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
||||||
ulong rsi = ReadCtxU64(contextRecord, 168);
|
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
||||||
ulong rdi = ReadCtxU64(contextRecord, 176);
|
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
||||||
ulong rbp = ReadCtxU64(contextRecord, 160);
|
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
||||||
ulong r8 = ReadCtxU64(contextRecord, 184);
|
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
||||||
ulong r9 = ReadCtxU64(contextRecord, 192);
|
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
||||||
ulong r10 = ReadCtxU64(contextRecord, 200);
|
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
||||||
ulong r11 = ReadCtxU64(contextRecord, 208);
|
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
||||||
ulong r12 = ReadCtxU64(contextRecord, 216);
|
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
||||||
ulong r13 = ReadCtxU64(contextRecord, 224);
|
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
||||||
ulong r14 = ReadCtxU64(contextRecord, 232);
|
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
||||||
ulong r15 = ReadCtxU64(contextRecord, 240);
|
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
@@ -185,7 +193,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong accessType = 0;
|
ulong accessType = 0;
|
||||||
ulong target = 0;
|
ulong target = 0;
|
||||||
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
||||||
{
|
{
|
||||||
accessType = *exceptionRecord->ExceptionInformation;
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
target = exceptionRecord->ExceptionInformation[1];
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
@@ -198,26 +206,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
};
|
};
|
||||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||||
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
if (_hostMemory.Query(target, out var mbi))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||||
for (int i = 0; i < 16; i++)
|
for (int i = 0; i < 16; i++)
|
||||||
{
|
{
|
||||||
ulong stackAddr = rsp + (ulong)(i * 8);
|
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||||
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
|
if (!TryReadHostQword(stackAddr, out ulong value))
|
||||||
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.");
|
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -230,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)
|
||||||
@@ -248,16 +256,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case 3221225477u:
|
case WindowsFaultCodes.AccessViolation:
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
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];
|
byte[] code = new byte[16];
|
||||||
Marshal.Copy((nint)rip, code, 0, code.Length);
|
if (TryReadHostBytes(rip, code))
|
||||||
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -273,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];
|
byte[] before = new byte[16];
|
||||||
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
|
if (rip > 16 && TryReadHostBytes(rip - 16, before))
|
||||||
|
{
|
||||||
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];
|
byte[] window = new byte[64];
|
||||||
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
|
if (rip > 32 && TryReadHostBytes(rip - 32, window))
|
||||||
|
{
|
||||||
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");
|
||||||
}
|
}
|
||||||
@@ -295,11 +300,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
DumpGuestReferenceDiagnostics();
|
DumpGuestReferenceDiagnostics();
|
||||||
DumpGuestPointerWindowDiagnostics();
|
DumpGuestPointerWindowDiagnostics();
|
||||||
break;
|
break;
|
||||||
case 2147483651u:
|
case WindowsFaultCodes.Breakpoint:
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
break;
|
break;
|
||||||
case 3221225501u:
|
case WindowsFaultCodes.IllegalInstruction:
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -332,8 +337,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
void* contextRecord = pointers->ContextRecord;
|
void* contextRecord = pointers->ContextRecord;
|
||||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
||||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
||||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -479,7 +484,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong address = scanBase;
|
ulong address = scanBase;
|
||||||
while (address < scanEnd)
|
while (address < scanEnd)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -491,9 +496,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == MEM_COMMIT &&
|
if (mbi.State == HostRegionState.Committed &&
|
||||||
IsReadableProtection(mbi.Protect) &&
|
IsReadableProtection(mbi.RawProtection) &&
|
||||||
IsExecutableProtection(mbi.Protect))
|
IsExecutableProtection(mbi.RawProtection))
|
||||||
{
|
{
|
||||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||||
}
|
}
|
||||||
@@ -798,13 +803,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(address, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||||
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -821,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}";
|
||||||
@@ -916,25 +976,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (!_hostMemory.Query(faultAddress, out var mbi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
||||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
||||||
{
|
{
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -943,10 +1003,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
ulong committedSize = 0;
|
ulong committedSize = 0;
|
||||||
|
|
||||||
if (mbi.State == 65536)
|
if (mbi.State == HostRegionState.Free)
|
||||||
{
|
{
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = windowBase;
|
committedBase = windowBase;
|
||||||
@@ -955,7 +1015,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = largeBase;
|
||||||
@@ -966,13 +1026,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -985,7 +1045,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -993,13 +1053,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State != 8192)
|
if (mbi.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = commitWindowBase;
|
committedBase = commitWindowBase;
|
||||||
@@ -1008,7 +1068,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = largeCommitBase;
|
||||||
@@ -1019,19 +1079,19 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
committedSize = 8192uL;
|
committedSize = 8192uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1044,7 +1104,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1085,31 +1145,33 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
||||||
|
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
||||||
|
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||||
{
|
{
|
||||||
if (!TryReserveRange(reserveAddress, reserveSize))
|
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return TryCommitRange(commitAddress, commitSize, protection);
|
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.Core.Cpu.Native.Windows;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -69,23 +71,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||||
ulong value = ReadCtxU64(contextRecord, 248);
|
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
|
||||||
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
||||||
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||||
WriteCtxU64(contextRecord, 120, 0uL);
|
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
|
||||||
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
||||||
{
|
{
|
||||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
|
||||||
WriteCtxU64(contextRecord, 248, returnRip);
|
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
|
||||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||||
if (LogThreadMode)
|
if (LogThreadMode)
|
||||||
{
|
{
|
||||||
@@ -533,8 +535,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -544,8 +545,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,8 +676,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -687,8 +686,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1725,9 +1723,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
var candidateBase = ImportStubRegionCanonicalBase -
|
var candidateBase = ImportStubRegionCanonicalBase -
|
||||||
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||||
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
|
||||||
memoryInfo.RegionSize == 0 ||
|
memoryInfo.RegionSize == 0 ||
|
||||||
memoryInfo.State != 4096)
|
memoryInfo.State != HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1968,24 +1966,37 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
|
||||||
Span<byte> destination = stackalloc byte[1];
|
// fallback must probe exactly up to the terminator), but the bytes land in a
|
||||||
|
// stack buffer instead of a List<byte> + ToArray per symbol resolution.
|
||||||
|
const int StackBufferLength = 512;
|
||||||
|
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
|
||||||
|
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||||
|
try
|
||||||
|
{
|
||||||
for (int i = 0; i < maxLength; i++)
|
for (int i = 0; i < maxLength; i++)
|
||||||
{
|
{
|
||||||
if (!TryReadByteCompat(address + (ulong)i, destination))
|
if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (destination[0] == 0)
|
if (buffer[i] == 0)
|
||||||
{
|
{
|
||||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
list.Add(destination[0]);
|
|
||||||
}
|
}
|
||||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rented is not null)
|
||||||
|
{
|
||||||
|
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
||||||
{
|
{
|
||||||
@@ -2066,7 +2077,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
|
if (Marshal.ReadByte(num2) != 232 || !_hostMemory.Protect((ulong)(void*)num, 5u, HostPageProtection.ReadWriteExecute, out flNewProtect))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2074,7 +2085,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Marshal.WriteByte(num2 + i, 144);
|
Marshal.WriteByte(num2 + i, 144);
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
|
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
|
||||||
_patchedEa020eLookupCall = true;
|
_patchedEa020eLookupCall = true;
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
||||||
}
|
}
|
||||||
@@ -2085,7 +2096,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (flNewProtect != 0)
|
if (flNewProtect != 0)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
|
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -35,20 +37,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint CreateThread(
|
|
||||||
nint lpThreadAttributes,
|
|
||||||
nuint dwStackSize,
|
|
||||||
nint lpStartAddress,
|
|
||||||
nint lpParameter,
|
|
||||||
uint dwCreationFlags,
|
|
||||||
out uint lpThreadId);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
|
||||||
|
|
||||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||||
// thread; falls back to the historical inline calli (guest frames above this
|
// thread; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// thread's managed frames) when workers are disabled or unavailable.
|
||||||
@@ -61,7 +49,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var worker = RentNativeGuestExecutor();
|
var worker = RentNativeGuestExecutor();
|
||||||
if (worker is null)
|
if (worker is null)
|
||||||
{
|
{
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return CallNativeEntry(entryStub);
|
return CallNativeEntry(entryStub);
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -185,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;
|
||||||
@@ -225,11 +225,16 @@ 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)
|
||||||
{
|
{
|
||||||
if (!EnsureKernel32Exports())
|
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -242,32 +247,27 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureKernel32Exports()
|
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
||||||
{
|
{
|
||||||
if (_exitThreadAddress != 0)
|
if (_exitThreadAddress != 0)
|
||||||
{
|
{
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||||
}
|
}
|
||||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
||||||
if (kernel32 == 0)
|
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
||||||
{
|
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
|
||||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
|
||||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Initialize()
|
private bool Initialize()
|
||||||
{
|
{
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
||||||
if (_controlBlock == null)
|
if (_controlBlock == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
||||||
if (_loopStub == null)
|
if (_loopStub == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -276,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;
|
||||||
@@ -349,17 +375,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
||||||
_threadHandle = CreateThread(
|
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
||||||
0,
|
|
||||||
WorkerStackReservation,
|
|
||||||
(nint)_loopStub,
|
(nint)_loopStub,
|
||||||
0,
|
0,
|
||||||
StackSizeParamIsAReservation,
|
WorkerStackReservation,
|
||||||
out _nativeThreadId);
|
out _nativeThreadId);
|
||||||
if (_threadHandle == 0)
|
if (_threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -397,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;
|
||||||
@@ -411,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)
|
||||||
{
|
{
|
||||||
@@ -465,7 +511,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||||
_prevState = _activeGuestThreadState;
|
_prevState = _activeGuestThreadState;
|
||||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
||||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||||
_entered = true;
|
_entered = true;
|
||||||
_activeExecutionBackend = backend;
|
_activeExecutionBackend = backend;
|
||||||
@@ -477,11 +523,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||||
if (_runState is { } state)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
||||||
}
|
}
|
||||||
if (_runAffinityMask != 0)
|
if (_runAffinityMask != 0)
|
||||||
{
|
{
|
||||||
@@ -511,7 +557,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
@@ -540,7 +586,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_workAvailable.Set();
|
SignalWorkAvailable();
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
@@ -548,8 +594,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var exited = _threadHandle == 0;
|
var exited = _threadHandle == 0;
|
||||||
if (_threadHandle != 0)
|
if (_threadHandle != 0)
|
||||||
{
|
{
|
||||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
||||||
CloseHandle(_threadHandle);
|
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
||||||
_threadHandle = 0;
|
_threadHandle = 0;
|
||||||
}
|
}
|
||||||
if (!exited)
|
if (!exited)
|
||||||
@@ -563,20 +609,30 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (_loopStub != null)
|
if (_loopStub != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_loopStub, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_loopStub);
|
||||||
_loopStub = null;
|
_loopStub = null;
|
||||||
}
|
}
|
||||||
if (_controlBlock != null)
|
if (_controlBlock != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_controlBlock, 0u, 32768u);
|
_backend._hostMemory.Free((ulong)_controlBlock);
|
||||||
_controlBlock = null;
|
_controlBlock = null;
|
||||||
}
|
}
|
||||||
if (_selfHandle.IsAllocated)
|
if (_selfHandle.IsAllocated)
|
||||||
{
|
{
|
||||||
_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);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
private readonly List<nint> _allocatedStubs = new();
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private byte* _pltMemory;
|
private byte* _pltMemory;
|
||||||
private int _pltOffset;
|
private int _pltOffset;
|
||||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
public StubManager()
|
public StubManager(IHostMemory? hostMemory = null)
|
||||||
{
|
{
|
||||||
_pltMemory = (byte*)VirtualAlloc(
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
null,
|
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
||||||
(nuint)PltMemorySize,
|
|
||||||
AllocationType.Reserve | AllocationType.Commit,
|
|
||||||
MemoryProtection.ExecuteReadWrite);
|
|
||||||
|
|
||||||
if (_pltMemory == null)
|
if (_pltMemory == null)
|
||||||
{
|
{
|
||||||
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_pltMemory != null)
|
if (_pltMemory != null)
|
||||||
{
|
{
|
||||||
VirtualFree(_pltMemory, 0, FreeType.Release);
|
_hostMemory.Free((ulong)_pltMemory);
|
||||||
_pltMemory = null;
|
_pltMemory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
_stubAddresses.Clear();
|
_stubAddresses.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum AllocationType : uint
|
|
||||||
{
|
|
||||||
Commit = 0x1000,
|
|
||||||
Reserve = 0x2000,
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum MemoryProtection : uint
|
|
||||||
{
|
|
||||||
ExecuteReadWrite = 0x40,
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum FreeType : uint
|
|
||||||
{
|
|
||||||
Release = 0x8000,
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void ImportHandler(CpuContext context);
|
public delegate void ImportHandler(CpuContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
|
||||||
|
/// handlers. The handlers read/write guest registers directly at these offsets
|
||||||
|
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
|
||||||
|
/// class for its mcontext layout.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Win64ContextOffsets
|
||||||
|
{
|
||||||
|
public const int Size = 0x4D0;
|
||||||
|
public const int Mxcsr = 52;
|
||||||
|
public const int Rax = 120;
|
||||||
|
public const int Rcx = 128;
|
||||||
|
public const int Rdx = 136;
|
||||||
|
public const int Rbx = 144;
|
||||||
|
public const int Rsp = 152;
|
||||||
|
public const int Rbp = 160;
|
||||||
|
public const int Rsi = 168;
|
||||||
|
public const int Rdi = 176;
|
||||||
|
public const int R8 = 184;
|
||||||
|
public const int R9 = 192;
|
||||||
|
public const int R10 = 200;
|
||||||
|
public const int R11 = 208;
|
||||||
|
public const int R12 = 216;
|
||||||
|
public const int R13 = 224;
|
||||||
|
public const int R14 = 232;
|
||||||
|
public const int R15 = 240;
|
||||||
|
public const int Rip = 248;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
|
||||||
|
/// fault handlers filter on. Values are the same numbers the handlers previously
|
||||||
|
/// compared as bare literals; only the spelling changed.
|
||||||
|
/// </summary>
|
||||||
|
internal static class WindowsFaultCodes
|
||||||
|
{
|
||||||
|
public const uint AccessViolation = 0xC0000005u; // 3221225477
|
||||||
|
public const uint Breakpoint = 0x80000003u; // 2147483651
|
||||||
|
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
|
||||||
|
public const uint FastFail = 0xC0000409u; // 3221226505
|
||||||
|
public const uint StackOverflow = 0xC00000FDu;
|
||||||
|
public const uint ClrManagedException = 0xE0434352u;
|
||||||
|
|
||||||
|
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
|
||||||
|
public const ulong AccessRead = 0;
|
||||||
|
public const ulong AccessWrite = 1;
|
||||||
|
public const ulong AccessExecute = 8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vectored-exception-handler installation and the handler pre-filter thunk.
|
||||||
|
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
|
||||||
|
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
|
||||||
|
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
|
||||||
|
/// sigaction/sigaltstack instead.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||||
|
{
|
||||||
|
private readonly IHostMemory _memory;
|
||||||
|
|
||||||
|
public WindowsFaultHandling(IHostMemory memory)
|
||||||
|
{
|
||||||
|
_memory = memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||||
|
{
|
||||||
|
const uint stubSize = 256u;
|
||||||
|
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||||
|
if (ptr == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte* code = (byte*)ptr;
|
||||||
|
int offset = 0;
|
||||||
|
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||||
|
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||||
|
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||||
|
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||||
|
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||||
|
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||||
|
// survived inside the emulator. Continue the handler search without touching
|
||||||
|
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||||
|
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||||
|
// returned CONTINUE_SEARCH for them.
|
||||||
|
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||||
|
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||||
|
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||||
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
|
{
|
||||||
|
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||||
|
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||||
|
EmitByte(code, ref offset, 0x74); // je pass
|
||||||
|
passJumpOffsets[i] = offset;
|
||||||
|
EmitByte(code, ref offset, 0x00);
|
||||||
|
}
|
||||||
|
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||||
|
int passOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||||
|
EmitByte(code, ref offset, 0xC3); // ret
|
||||||
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
|
{
|
||||||
|
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||||
|
}
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
||||||
|
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||||
|
EmitUInt32(code, ref offset, 8u);
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
|
||||||
|
int aboveStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
|
||||||
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||||
|
EmitUInt32(code, ref offset, 0x10u);
|
||||||
|
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
|
||||||
|
int belowStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = managedCallback;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xE9);
|
||||||
|
int hostRestoreJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
int guestStackOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xB9);
|
||||||
|
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = tlsGetValueAddress;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||||
|
int missingTlsJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
|
||||||
|
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
|
||||||
|
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||||
|
int missingHostStackJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
|
*(nint*)(code + offset) = managedCallback;
|
||||||
|
offset += sizeof(nint);
|
||||||
|
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||||
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
|
EmitByte(code, ref offset, 0xE9);
|
||||||
|
int guestRestoreJump = offset;
|
||||||
|
EmitUInt32(code, ref offset, 0u);
|
||||||
|
|
||||||
|
int passThroughOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
|
||||||
|
int restoreOffset = offset;
|
||||||
|
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
|
||||||
|
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
|
||||||
|
EmitByte(code, ref offset, 0xC3);
|
||||||
|
|
||||||
|
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
|
||||||
|
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
|
||||||
|
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
|
||||||
|
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
|
||||||
|
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||||
|
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||||
|
|
||||||
|
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||||
|
_ = _memory.Free((ulong)ptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
|
||||||
|
return (nint)ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FreeThunk(nint thunk)
|
||||||
|
{
|
||||||
|
_ = _memory.Free((ulong)thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint AddFirstChanceHandler(nint thunk)
|
||||||
|
{
|
||||||
|
return (nint)AddVectoredExceptionHandler(1u, thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveHandler(nint handle)
|
||||||
|
{
|
||||||
|
_ = RemoveVectoredExceptionHandler((void*)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUnhandledFilter(nint thunk)
|
||||||
|
{
|
||||||
|
_ = SetUnhandledExceptionFilter(thunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitByte(byte* code, ref int offset, byte value)
|
||||||
|
{
|
||||||
|
code[offset++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitUInt32(byte* code, ref int offset, uint value)
|
||||||
|
{
|
||||||
|
*(uint*)(code + offset) = value;
|
||||||
|
offset += sizeof(uint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint RemoveVectoredExceptionHandler(void* handle);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
|
|||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
|
||||||
{
|
{
|
||||||
private readonly ICpuMemory _inner;
|
private readonly ICpuMemory _inner;
|
||||||
|
|
||||||
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
address = 0;
|
address = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IGuestAddressSpace, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||||
|
|
||||||
@@ -28,41 +29,27 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
private const uint MEM_RESERVE = 0x2000;
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
private const uint MEM_RELEASE = 0x8000;
|
// value of the host platform in use, and these classification helpers only
|
||||||
|
// ever see values this class itself assigned (see IHostMemory.ProtectRaw).
|
||||||
private const uint PAGE_EXECUTE_READ = 0x20;
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
private const uint PAGE_EXECUTE = 0x10;
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
private const uint PAGE_NOACCESS = 0x01;
|
|
||||||
private const uint PAGE_READWRITE = 0x04;
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
private const uint PAGE_READONLY = 0x02;
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
|
private readonly IHostMemory _hostMemory;
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||||
|
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
{
|
||||||
|
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
}
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
private static extern void* GetCurrentProcess();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -74,17 +61,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualAddress = (ulong)result;
|
actualAddress = result;
|
||||||
if (actualAddress != desiredAddress)
|
if (actualAddress != desiredAddress)
|
||||||
{
|
{
|
||||||
VirtualFree(result, 0, MEM_RELEASE);
|
_hostMemory.Free(result);
|
||||||
actualAddress = 0;
|
actualAddress = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -119,33 +106,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
|
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var preferReserveOnly = !executable &&
|
var preferReserveOnly = !executable &&
|
||||||
alignedSize >= LargeDataReserveThreshold &&
|
alignedSize >= LargeDataReserveThreshold &&
|
||||||
alignedSize > FullCommitRegionLimit;
|
alignedSize > FullCommitRegionLimit;
|
||||||
|
|
||||||
void* result = null;
|
ulong result = 0;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
if (result == 0 && allowAlternative)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
reservedOnly = true;
|
reservedOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
if (!allowAlternative)
|
if (!allowAlternative)
|
||||||
{
|
{
|
||||||
@@ -153,32 +140,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
result = _hostMemory.Allocate(0, alignedSize, hostProtection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
if (!executable)
|
if (!executable)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
if (result == null && allowAlternative)
|
if (result == 0 && allowAlternative)
|
||||||
{
|
{
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
reservedOnly = true;
|
reservedOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualAddress = (ulong)result;
|
var actualAddress = result;
|
||||||
|
|
||||||
var lazyPrimeState = "n/a";
|
var lazyPrimeState = "n/a";
|
||||||
if (reservedOnly)
|
if (reservedOnly)
|
||||||
@@ -191,9 +178,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
var remaining = primeBytes - committedBytes;
|
var remaining = primeBytes - committedBytes;
|
||||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||||
var commitAddress = (void*)(actualAddress + committedBytes);
|
var commitAddress = actualAddress + committedBytes;
|
||||||
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
|
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||||
if (committed == null)
|
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -263,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)
|
||||||
@@ -297,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;
|
||||||
@@ -316,7 +389,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
GuestAllocationArenaSize,
|
GuestAllocationArenaSize,
|
||||||
executable: false,
|
executable: false,
|
||||||
allowAlternative: true);
|
allowAlternative: true);
|
||||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
_guestAllocationFreeRanges.Add(
|
||||||
|
GuestAllocationArenaStartOffset,
|
||||||
|
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -324,18 +399,128 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
ulong rangeOffset = 0;
|
||||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
ulong rangeSize = 0;
|
||||||
|
ulong alignedOffset = 0;
|
||||||
|
var found = false;
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
alignedOffset = AlignUp(range.Key, alignment);
|
||||||
|
if (alignedOffset >= range.Key &&
|
||||||
|
alignedOffset - range.Key <= range.Value &&
|
||||||
|
size <= range.Value - (alignedOffset - range.Key))
|
||||||
|
{
|
||||||
|
rangeOffset = range.Key;
|
||||||
|
rangeSize = range.Value;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Remove(rangeOffset);
|
||||||
|
if (alignedOffset > rangeOffset)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationEnd = alignedOffset + size;
|
||||||
|
var rangeEnd = rangeOffset + rangeSize;
|
||||||
|
if (allocationEnd < rangeEnd)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
|
||||||
|
}
|
||||||
|
|
||||||
address = _guestAllocationArenaBase + alignedOffset;
|
address = _guestAllocationArenaBase + alignedOffset;
|
||||||
_guestAllocationOffset = alignedOffset + size;
|
_guestAllocations.Add(address, (alignedOffset, size));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
lock (_guestAllocationGate)
|
||||||
|
{
|
||||||
|
if (!_guestAllocations.Remove(address, out var allocation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeOffset = allocation.Offset;
|
||||||
|
var freeSize = allocation.Size;
|
||||||
|
ulong? previousOffset = null;
|
||||||
|
ulong? nextOffset = null;
|
||||||
|
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
if (range.Key < freeOffset)
|
||||||
|
{
|
||||||
|
previousOffset = range.Key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOffset = range.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousOffset is { } previous &&
|
||||||
|
previous + _guestAllocationFreeRanges[previous] == freeOffset)
|
||||||
|
{
|
||||||
|
freeOffset = previous;
|
||||||
|
freeSize += _guestAllocationFreeRanges[previous];
|
||||||
|
_guestAllocationFreeRanges.Remove(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextOffset is { } next && freeOffset + freeSize == next)
|
||||||
|
{
|
||||||
|
freeSize += _guestAllocationFreeRanges[next];
|
||||||
|
_guestAllocationFreeRanges.Remove(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
|
||||||
|
// performed before this seam existed; the Windows backend maps each case back
|
||||||
|
// to the identical PAGE_* value.
|
||||||
|
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
|
||||||
|
{
|
||||||
|
var read = (protection & GuestPageProtection.Read) != 0;
|
||||||
|
var write = (protection & GuestPageProtection.Write) != 0;
|
||||||
|
var execute = (protection & GuestPageProtection.Execute) != 0;
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
return write
|
||||||
|
? HostPageProtection.ReadWriteExecute
|
||||||
|
: read
|
||||||
|
? HostPageProtection.ReadExecute
|
||||||
|
: HostPageProtection.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return write
|
||||||
|
? HostPageProtection.ReadWrite
|
||||||
|
: read
|
||||||
|
? HostPageProtection.ReadOnly
|
||||||
|
: HostPageProtection.NoAccess;
|
||||||
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
@@ -345,7 +530,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
_hostMemory.Free(region.VirtualAddress);
|
||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
@@ -360,7 +545,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
_guestAllocationOffset = 0;
|
_guestAllocationFreeRanges.Clear();
|
||||||
|
_guestAllocations.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,46 +605,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
|
var runStart = mapStart;
|
||||||
|
var runFlags = ProgramHeaderFlags.None;
|
||||||
|
var hasRun = false;
|
||||||
|
|
||||||
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||||
var mergedFlags = existingFlags | flags;
|
var mergedFlags = existingFlags | flags;
|
||||||
_pageProtections[pageAddress] = mergedFlags;
|
_pageProtections[pageAddress] = mergedFlags;
|
||||||
SetProtection(pageAddress, PageSize, mergedFlags);
|
|
||||||
|
if (!hasRun)
|
||||||
|
{
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
hasRun = true;
|
||||||
|
}
|
||||||
|
else if (mergedFlags != runFlags)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, pageAddress - runStart, runFlags);
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRun)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, mapEnd - runStart, runFlags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
uint protection;
|
HostPageProtection protection;
|
||||||
|
|
||||||
if (flags == ProgramHeaderFlags.None)
|
if (flags == ProgramHeaderFlags.None)
|
||||||
{
|
{
|
||||||
protection = PAGE_NOACCESS;
|
protection = HostPageProtection.NoAccess;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
else if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
protection = (flags & ProgramHeaderFlags.Write) != 0
|
protection = (flags & ProgramHeaderFlags.Write) != 0
|
||||||
? PAGE_EXECUTE_READWRITE
|
? HostPageProtection.ReadWriteExecute
|
||||||
: PAGE_EXECUTE_READ;
|
: HostPageProtection.ReadExecute;
|
||||||
}
|
}
|
||||||
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
||||||
{
|
{
|
||||||
protection = PAGE_READWRITE;
|
protection = HostPageProtection.ReadWrite;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
protection = PAGE_READONLY;
|
protection = HostPageProtection.ReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
|
if (!_hostMemory.Protect(address, size, protection, out _))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
_hostMemory.FlushInstructionCache(address, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +937,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
if (!_hostMemory.Protect((ulong)destPtr, (ulong)source.Length, HostPageProtection.ReadWriteExecute, out var oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -744,10 +951,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
|
||||||
if (IsExecutableProtection(oldProtect))
|
if (IsExecutableProtection(oldProtect))
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,9 +976,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, 1) is not null
|
var region = FindRegion(virtualAddress, 1);
|
||||||
? (void*)virtualAddress
|
if (region is null ||
|
||||||
: null;
|
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (void*)virtualAddress;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -973,12 +1185,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetCommitProtection(MemoryRegion region)
|
private static HostPageProtection GetCommitProtection(MemoryRegion region)
|
||||||
{
|
{
|
||||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
return region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
private bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
|
||||||
{
|
{
|
||||||
if (size == 0 || !region.IsReservedOnly)
|
if (size == 0 || !region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
@@ -992,7 +1204,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
while (pageAddress < endPage)
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (!_hostMemory.Query(pageAddress, out var info))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1006,19 +1218,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State != MEM_RESERVE)
|
if (info.State != HostRegionState.Reserved)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commitSize = rangeEnd - pageAddress;
|
var commitSize = rangeEnd - pageAddress;
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1039,11 +1251,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var temporaryProtection = region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var temporaryProtection = region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||||
|
|
||||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
if (!VirtualProtect((void*)pageAddress, (nuint)PageSize, temporaryProtection, out var oldProtection))
|
if (!_hostMemory.Protect(pageAddress, PageSize, temporaryProtection, out var oldProtection))
|
||||||
{
|
{
|
||||||
RestorePageProtections(touchedPages);
|
RestorePageProtections(touchedPages);
|
||||||
touchedPages.Clear();
|
touchedPages.Clear();
|
||||||
@@ -1056,11 +1268,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
private void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
|
||||||
{
|
{
|
||||||
foreach (var (pageAddress, protection) in touchedPages)
|
foreach (var (pageAddress, protection) in touchedPages)
|
||||||
{
|
{
|
||||||
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
|
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,16 +1329,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
public uint Protection { get; set; }
|
public uint Protection { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct MemoryBasicInformation64
|
|
||||||
{
|
|
||||||
public ulong BaseAddress;
|
|
||||||
public ulong AllocationBase;
|
|
||||||
public uint AllocationProtect;
|
|
||||||
public uint Alignment1;
|
|
||||||
public ulong RegionSize;
|
|
||||||
public uint State;
|
|
||||||
public uint Protect;
|
|
||||||
public uint Type;
|
|
||||||
public uint Alignment2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
foreach (var existing in _regions)
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
{
|
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
|
||||||
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_regions.Add(new MappedRegion(
|
_regions.Insert(insertionIndex, new MappedRegion(
|
||||||
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||||
endAddress,
|
endAddress,
|
||||||
backingMemory));
|
backingMemory));
|
||||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,40 +87,128 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
private bool TryValidateRange(
|
||||||
|
ulong virtualAddress,
|
||||||
|
int length,
|
||||||
|
ProgramHeaderFlags requiredProtection,
|
||||||
|
out int regionIndex)
|
||||||
{
|
{
|
||||||
foreach (var candidate in _regions)
|
regionIndex = FindContainingRegionIndex(virtualAddress);
|
||||||
|
if (regionIndex < 0)
|
||||||
{
|
{
|
||||||
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
|
||||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
region = candidate;
|
|
||||||
offset = candidateOffset;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
region = default;
|
|
||||||
offset = 0;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
var remaining = length;
|
||||||
|
var currentIndex = regionIndex;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (currentIndex >= _regions.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var region = _regions[currentIndex];
|
||||||
|
if (currentAddress < region.Region.VirtualAddress ||
|
||||||
|
currentAddress >= region.EndAddress ||
|
||||||
|
(region.Region.Protection & requiredProtection) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var available = region.EndAddress - currentAddress;
|
||||||
|
var chunkLength = (int)Math.Min((ulong)remaining, available);
|
||||||
|
remaining -= chunkLength;
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindContainingRegionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
|
if (insertionIndex < _regions.Count &&
|
||||||
|
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
|
||||||
|
{
|
||||||
|
return insertionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIndex = insertionIndex - 1;
|
||||||
|
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
|
||||||
|
? candidateIndex
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < destination.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < source.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindInsertionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var lower = 0;
|
||||||
|
var upper = _regions.Count;
|
||||||
|
while (lower < upper)
|
||||||
|
{
|
||||||
|
var middle = lower + ((upper - lower) / 2);
|
||||||
|
if (_regions[middle].Region.VirtualAddress < virtualAddress)
|
||||||
|
{
|
||||||
|
lower = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
upper = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
@@ -86,14 +87,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
moduleManager.Freeze();
|
moduleManager.Freeze();
|
||||||
|
|
||||||
var virtualMemory = new PhysicalVirtualMemory();
|
// Resolve the host platform once at the composition root; on unsupported
|
||||||
|
// OSes this throws PlatformNotSupportedException with a clear message
|
||||||
|
// instead of failing on the first native call.
|
||||||
|
var hostPlatform = HostPlatform.Current;
|
||||||
|
|
||||||
|
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
|
||||||
|
|
||||||
var fileSystem = new PhysicalFileSystem();
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
return new SharpEmuRuntime(
|
return new SharpEmuRuntime(
|
||||||
new SelfLoader(),
|
new SelfLoader(),
|
||||||
virtualMemory,
|
virtualMemory,
|
||||||
new CpuDispatcher(virtualMemory, moduleManager),
|
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
||||||
moduleManager,
|
moduleManager,
|
||||||
Aerolib.Instance,
|
Aerolib.Instance,
|
||||||
cpuExecutionOptions,
|
cpuExecutionOptions,
|
||||||
|
|||||||
@@ -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, )",
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||||
public bool DiscordRichPresence { get; set; } = true;
|
public bool DiscordRichPresence { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||||
|
public List<string> EnvironmentToggles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
"Library.Loading": "Loading library…",
|
"Library.Loading": "Loading library…",
|
||||||
|
|
||||||
"Options.General": "General",
|
"Options.General": "General",
|
||||||
|
"Options.Env.Tab": "Environment",
|
||||||
|
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
|
||||||
|
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||||
|
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||||
|
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||||
"Options.Section.Emulation": "EMULATION",
|
"Options.Section.Emulation": "EMULATION",
|
||||||
"Options.Section.Logging": "LOGGING",
|
"Options.Section.Logging": "LOGGING",
|
||||||
"Options.Section.Launcher": "LAUNCHER",
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|||||||
@@ -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!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Português (Portugal)",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opções",
|
||||||
|
"Page.GameCount.One": "1 jogo",
|
||||||
|
"Page.GameCount.Other": "{0} jogos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Pesquisar biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
|
"Library.Rescan": "⟳ Reanalisar",
|
||||||
|
"Library.OpenFile": "Abrir ficheiro…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Iniciar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||||
|
"Library.Context.CopyPath": "Copiar caminho",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||||
|
"Library.Context.Remove": "Remover da biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "A sua biblioteca está vazia",
|
||||||
|
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
|
||||||
|
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
|
||||||
|
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Adicionar pasta de jogos",
|
||||||
|
|
||||||
|
"Library.Loading": "A carregar biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "Geral",
|
||||||
|
"Options.Env.Tab": "Ambiente",
|
||||||
|
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||||
|
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
|
||||||
|
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
|
||||||
|
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
|
||||||
|
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||||
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
||||||
|
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
||||||
|
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
||||||
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
|
"Options.Section.Logging": "REGISTOS",
|
||||||
|
"Options.Section.Launcher": "LANÇADOR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor de CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolução estrita de dynlib",
|
||||||
|
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nível de registo",
|
||||||
|
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Rastreio",
|
||||||
|
"Options.LogLevel.Debug": "Depuração",
|
||||||
|
"Options.LogLevel.Info": "Informação",
|
||||||
|
"Options.LogLevel.Warning": "Aviso",
|
||||||
|
"Options.LogLevel.Error": "Erro",
|
||||||
|
"Options.LogLevel.Critical": "Crítico",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de rastreio de importações",
|
||||||
|
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Registar para ficheiro",
|
||||||
|
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
|
||||||
|
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Selecionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música do título",
|
||||||
|
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Presença no Discord",
|
||||||
|
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma do emulador",
|
||||||
|
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
|
||||||
|
|
||||||
|
"Common.On": "Ativado",
|
||||||
|
"Common.Off": "Desativado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLA",
|
||||||
|
"Console.SearchWatermark": "Pesquisar...",
|
||||||
|
"Console.AutoScroll": "Deslocamento automático",
|
||||||
|
"Console.Split": "Dividir",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpar",
|
||||||
|
"Console.WindowTitle": "Consola do SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||||
|
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
|
||||||
|
"Launch.Idle": "Inativo",
|
||||||
|
"Launch.Console": "≡ Consola",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Parar",
|
||||||
|
"Launch.Running": "Em execução — {0}",
|
||||||
|
"Launch.Stopping": "A parar…",
|
||||||
|
"Launch.Exited": "Terminou com o código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Ficheiro de registo: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos inválidos",
|
||||||
|
"Exit.EbootNotFound": "eboot não encontrado",
|
||||||
|
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||||
|
"Exit.EmulationError": "erro de emulação",
|
||||||
|
"Exit.Unknown": "desconhecido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: a localizar…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "A analisar biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
|
||||||
|
"Status.Running": "A executar {0}",
|
||||||
|
"Status.Stopping": "A parar…",
|
||||||
|
"Status.Idle": "Inativo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Caminho",
|
||||||
|
"Clipboard.TitleId": "ID do título",
|
||||||
|
|
||||||
|
"Discord.Playing": "A jogar {0}",
|
||||||
|
"Discord.Browsing": "A navegar na biblioteca",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Executáveis PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
|
||||||
|
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
|
||||||
|
"Dialog.LogFiles": "Ficheiros de Registo",
|
||||||
|
|
||||||
|
"Options.About" : "Sobre",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||||
|
"About.GithubButton": "Contribua no GitHub!",
|
||||||
|
"About.DiscordButton": "Junte-se ao nosso Discord!"
|
||||||
|
}
|
||||||
@@ -387,6 +387,88 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
|
||||||
|
<TextBlock x:Name="EnvDesc"
|
||||||
|
Text="Switches passed to the emulator as environment variables at launch."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvBthidDesc"
|
||||||
|
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever. Leave off normally. Some titles freeze when init fails."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLoopGuardDesc"
|
||||||
|
Text="Do not force quit titles that repeat the same call for too long. Try this when a game exits on its own while loading."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvVkValidationDesc"
|
||||||
|
Text="Enable Vulkan validation layers for GPU debugging. Slow. Requires the Vulkan SDK to be installed."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvDumpSpirvDesc"
|
||||||
|
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder. Use when reporting shader or rendering bugs."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLogDirectMemoryDesc"
|
||||||
|
Text="Log direct memory allocations and failures to the console. Use when a game aborts or exits during boot."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLogNpDesc"
|
||||||
|
Text="Log NP (PlayStation Network) library calls to the console."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -125,6 +126,18 @@ public partial class MainWindow : Window
|
|||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
};
|
};
|
||||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||||
|
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||||
|
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||||
|
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||||
|
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||||
|
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||||
|
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
@@ -139,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),
|
||||||
@@ -212,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,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);
|
||||||
}
|
}
|
||||||
@@ -244,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))
|
||||||
{
|
{
|
||||||
@@ -270,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();
|
||||||
}
|
}
|
||||||
@@ -403,6 +416,15 @@ public partial class MainWindow : Window
|
|||||||
LoadingStateText.Text = loc.Get("Library.Loading");
|
LoadingStateText.Text = loc.Get("Library.Loading");
|
||||||
|
|
||||||
GeneralTabItem.Header = loc.Get("Options.General");
|
GeneralTabItem.Header = loc.Get("Options.General");
|
||||||
|
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
||||||
|
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
||||||
|
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||||
|
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||||
|
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||||
|
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||||
|
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||||
|
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||||
|
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||||
@@ -584,9 +606,34 @@ public partial class MainWindow : Window
|
|||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
|
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||||
|
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||||
|
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||||
|
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||||
|
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||||
|
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||||
UpdateLogFilePathText();
|
UpdateLogFilePathText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment variables set on this process at the previous launch; children
|
||||||
|
// inherit the process environment, so stale names must be cleared explicitly.
|
||||||
|
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private void SetEnvironmentToggle(string name, bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(name))
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_settings.EnvironmentToggles.Remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
{
|
{
|
||||||
return LogLevelBox.SelectedIndex switch
|
return LogLevelBox.SelectedIndex switch
|
||||||
@@ -1402,6 +1449,24 @@ public partial class MainWindow : Window
|
|||||||
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
||||||
DimLineBrush);
|
DimLineBrush);
|
||||||
|
|
||||||
|
// Apply the enabled switches to this process; both emulator launch paths
|
||||||
|
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
|
||||||
|
// off since the previous launch.
|
||||||
|
foreach (var staleName in _appliedEnvironmentVariables)
|
||||||
|
{
|
||||||
|
if (!_settings.EnvironmentToggles.Contains(staleName))
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(staleName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_appliedEnvironmentVariables.Clear();
|
||||||
|
foreach (var name in _settings.EnvironmentToggles)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(name, "1");
|
||||||
|
_appliedEnvironmentVariables.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<Version>0.0.1</Version>
|
<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 +44,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>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -238,24 +239,64 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new byte[capacity];
|
const int StackBufferLength = 512;
|
||||||
for (var index = 0; index < bytes.Length; index++)
|
const int ReadChunkLength = 128;
|
||||||
|
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
|
||||||
|
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
var length = 0;
|
||||||
|
while (length < capacity)
|
||||||
|
{
|
||||||
|
// Bulk-read in bounded chunks rather than the full capacity: the string
|
||||||
|
// may end just before unmapped memory, and overreading past the
|
||||||
|
// terminator by more than a chunk could fault where the old
|
||||||
|
// byte-by-byte loop succeeded.
|
||||||
|
var chunk = Math.Min(ReadChunkLength, capacity - length);
|
||||||
|
var span = bytes.Slice(length, chunk);
|
||||||
|
if (Memory.TryRead(address + (ulong)length, span))
|
||||||
|
{
|
||||||
|
var terminator = span.IndexOf((byte)0);
|
||||||
|
if (terminator >= 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
length += chunk;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chunk touches an unreadable range; fall back to per-byte reads so a
|
||||||
|
// terminator sitting before the bad byte still yields the string.
|
||||||
|
for (var i = 0; i < chunk; i++)
|
||||||
|
{
|
||||||
|
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes[index] == 0)
|
if (bytes[length + i] == 0)
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes);
|
length += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes[..capacity]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rented is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(rented);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool PushUInt64(ulong value)
|
public bool PushUInt64(ulong value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum GuestPageProtection
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Read = 1,
|
||||||
|
Write = 2,
|
||||||
|
Execute = 4,
|
||||||
|
}
|
||||||
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
|
|||||||
ulong LastReturnRip,
|
ulong LastReturnRip,
|
||||||
string? BlockReason);
|
string? BlockReason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
|
||||||
|
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
|
||||||
|
/// returns true when the waiter has a final result and the thread should be re-readied;
|
||||||
|
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
|
||||||
|
/// its return value becomes the guest's RAX for the resumed call.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
int Resume();
|
||||||
|
|
||||||
|
bool TryWake();
|
||||||
|
}
|
||||||
|
|
||||||
public interface IGuestThreadScheduler
|
public interface IGuestThreadScheduler
|
||||||
{
|
{
|
||||||
bool SupportsGuestContextTransfer { get; }
|
bool SupportsGuestContextTransfer { get; }
|
||||||
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
|
|||||||
private static string? _pendingBlockWakeKey;
|
private static string? _pendingBlockWakeKey;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static Func<int>? _pendingBlockResumeHandler;
|
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static Func<bool>? _pendingBlockWakeHandler;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static long _pendingBlockDeadlineTimestamp;
|
private static long _pendingBlockDeadlineTimestamp;
|
||||||
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
|
|||||||
CpuContext? context,
|
CpuContext? context,
|
||||||
string reason,
|
string reason,
|
||||||
string? wakeKey = null,
|
string? wakeKey = null,
|
||||||
Func<int>? resumeHandler = null,
|
IGuestThreadBlockWaiter? waiter = null,
|
||||||
Func<bool>? wakeHandler = null,
|
|
||||||
long blockDeadlineTimestamp = 0)
|
long blockDeadlineTimestamp = 0)
|
||||||
{
|
{
|
||||||
if (!IsGuestThread)
|
if (!IsGuestThread)
|
||||||
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
|
|||||||
|
|
||||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||||
_pendingBlockResumeHandler = resumeHandler;
|
_pendingBlockWaiter = waiter;
|
||||||
_pendingBlockWakeHandler = wakeHandler;
|
|
||||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||||
{
|
{
|
||||||
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
|
|||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter)
|
||||||
out Func<bool>? wakeHandler)
|
|
||||||
{
|
{
|
||||||
return TryConsumeCurrentThreadBlock(
|
return TryConsumeCurrentThreadBlock(
|
||||||
out reason,
|
out reason,
|
||||||
out continuation,
|
out continuation,
|
||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out wakeKey,
|
out wakeKey,
|
||||||
out resumeHandler,
|
out waiter,
|
||||||
out wakeHandler,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter,
|
||||||
out Func<bool>? wakeHandler,
|
|
||||||
out long blockDeadlineTimestamp)
|
out long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
reason = _pendingBlockReason ?? string.Empty;
|
reason = _pendingBlockReason ?? string.Empty;
|
||||||
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
|
|||||||
continuation = default;
|
continuation = default;
|
||||||
hasContinuation = false;
|
hasContinuation = false;
|
||||||
wakeKey = string.Empty;
|
wakeKey = string.Empty;
|
||||||
resumeHandler = null;
|
waiter = null;
|
||||||
wakeHandler = null;
|
|
||||||
blockDeadlineTimestamp = 0;
|
blockDeadlineTimestamp = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
|
|||||||
continuation = _pendingBlockContinuation;
|
continuation = _pendingBlockContinuation;
|
||||||
hasContinuation = _pendingBlockContinuationValid;
|
hasContinuation = _pendingBlockContinuationValid;
|
||||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||||
resumeHandler = _pendingBlockResumeHandler;
|
waiter = _pendingBlockWaiter;
|
||||||
wakeHandler = _pendingBlockWakeHandler;
|
|
||||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General-purpose register snapshot of a suspended thread, produced by
|
||||||
|
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
|
||||||
|
/// after the guest ISA (x86-64), which every supported host executes natively.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostCapturedRegisters(
|
||||||
|
ulong Rip,
|
||||||
|
ulong Rsp,
|
||||||
|
ulong Rbp,
|
||||||
|
ulong Rax,
|
||||||
|
ulong Rbx,
|
||||||
|
ulong Rcx,
|
||||||
|
ulong Rdx);
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Platform-neutral page protection. Values intentionally enumerate the exact
|
||||||
|
/// combinations the emulator uses today so each maps 1:1 onto a single native
|
||||||
|
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
|
||||||
|
/// </summary>
|
||||||
|
public enum HostPageProtection
|
||||||
|
{
|
||||||
|
NoAccess,
|
||||||
|
ReadOnly,
|
||||||
|
ReadWrite,
|
||||||
|
Execute,
|
||||||
|
ReadExecute,
|
||||||
|
ReadWriteExecute,
|
||||||
|
ExecuteWriteCopy,
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
|
using SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-wide access point for the host platform backend. Static HLE export
|
||||||
|
/// classes (which cannot receive constructor injection) resolve host primitives
|
||||||
|
/// through <see cref="Current"/>; injectable components should instead accept an
|
||||||
|
/// <see cref="IHostPlatform"/> and merely default to this.
|
||||||
|
/// </summary>
|
||||||
|
public static class HostPlatform
|
||||||
|
{
|
||||||
|
private static readonly Lazy<IHostPlatform> Instance = new(Create);
|
||||||
|
|
||||||
|
public static IHostPlatform Current => Instance.Value;
|
||||||
|
|
||||||
|
private static IHostPlatform Create()
|
||||||
|
{
|
||||||
|
// The Windows backend executes guest x86-64 natively and emits x86-64
|
||||||
|
// stubs, so a native ARM64 process must be rejected here rather than
|
||||||
|
// crash undefined later (x64 processes under emulation report X64).
|
||||||
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return new WindowsHostPlatform();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) &&
|
||||||
|
RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return new PosixHostPlatform();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PlatformNotSupportedException(
|
||||||
|
"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,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
|
||||||
|
/// untranslated OS values so call sites migrated from direct VirtualQuery use
|
||||||
|
/// keep comparing (and logging) the exact native words they did before;
|
||||||
|
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostRegionInfo(
|
||||||
|
ulong BaseAddress,
|
||||||
|
ulong AllocationBase,
|
||||||
|
ulong RegionSize,
|
||||||
|
HostRegionState State,
|
||||||
|
uint RawState,
|
||||||
|
HostPageProtection Protection,
|
||||||
|
uint RawProtection,
|
||||||
|
uint RawAllocationProtection);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
public enum HostRegionState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Reserved,
|
||||||
|
Committed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host functions whose addresses the execution engine bakes into emitted
|
||||||
|
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
|
||||||
|
/// free-form name lookup: each platform's emitters need their own specific
|
||||||
|
/// functions, and this set is exactly what the current emitters consume.
|
||||||
|
/// </summary>
|
||||||
|
public enum HostRuntimeFunction
|
||||||
|
{
|
||||||
|
TlsGetValue,
|
||||||
|
QueryPerformanceCounter,
|
||||||
|
SwitchToThread,
|
||||||
|
Sleep,
|
||||||
|
WaitForSingleObject,
|
||||||
|
SetEvent,
|
||||||
|
ExitThread,
|
||||||
|
}
|
||||||
@@ -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,32 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installation mechanics for the process-wide fault interception the execution
|
||||||
|
/// engine relies on to catch guest faults. Deliberately thin: the managed
|
||||||
|
/// handlers keep receiving the platform's raw exception data, and the emitted
|
||||||
|
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
|
||||||
|
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostFaultHandling
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
|
||||||
|
/// exception codes that must never enter managed code and, when the fault
|
||||||
|
/// happened on a guest stack, switches to the host stack saved in
|
||||||
|
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
|
||||||
|
|
||||||
|
void FreeThunk(nint thunk);
|
||||||
|
|
||||||
|
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
|
||||||
|
nint AddFirstChanceHandler(nint thunk);
|
||||||
|
|
||||||
|
void RemoveHandler(nint handle);
|
||||||
|
|
||||||
|
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
|
||||||
|
void SetUnhandledFilter(nint thunk);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host page-allocation primitives used by the native execution engine.
|
||||||
|
/// Allocate/Reserve/Commit are deliberately separate members (rather than a
|
||||||
|
/// flags parameter) so every call site maps 1:1 onto the exact native call it
|
||||||
|
/// replaced, keeping the Windows behavior byte-for-byte identical.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostMemory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reserves and commits pages in one step. <paramref name="desiredAddress"/> of 0
|
||||||
|
/// lets the OS choose the address. Returns the base address, or 0 on failure.
|
||||||
|
/// The OS may satisfy the request at a different address than desired; callers
|
||||||
|
/// that require an exact placement must check the result themselves.
|
||||||
|
/// </summary>
|
||||||
|
ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Reserves address space without committing pages (lazy regions).</summary>
|
||||||
|
ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Commits pages inside a previously reserved range (fault-path lazy commit).</summary>
|
||||||
|
bool Commit(ulong address, ulong size, HostPageProtection protection);
|
||||||
|
|
||||||
|
/// <summary>Releases an entire allocation or reservation by its base address.</summary>
|
||||||
|
bool Free(ulong address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes protection on committed pages. <paramref name="rawOldProtection"/> is the
|
||||||
|
/// untranslated previous OS protection value (see <see cref="HostRegionInfo.RawProtection"/>).
|
||||||
|
/// </summary>
|
||||||
|
bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores a raw protection value previously returned by <see cref="Protect"/> or
|
||||||
|
/// <see cref="Query"/> on this same platform. Raw values are opaque to callers and
|
||||||
|
/// must never cross platforms; this exists so save/restore protection sequences
|
||||||
|
/// round-trip OS-specific modifier bits the neutral enum cannot represent.
|
||||||
|
/// </summary>
|
||||||
|
bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection);
|
||||||
|
|
||||||
|
bool Query(ulong address, out HostRegionInfo info);
|
||||||
|
|
||||||
|
void FlushInstructionCache(ulong address, ulong size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregates the host-OS primitives the native execution engine depends on.
|
||||||
|
/// Each supported platform provides one implementation; consumers reach the
|
||||||
|
/// process-wide instance through <see cref="HostPlatform.Current"/> or accept
|
||||||
|
/// one by injection.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostPlatform
|
||||||
|
{
|
||||||
|
IHostMemory Memory { get; }
|
||||||
|
|
||||||
|
IHostThreading Threading { get; }
|
||||||
|
|
||||||
|
IHostSymbolResolver Symbols { get; }
|
||||||
|
|
||||||
|
IHostAudioOutput Audio { get; }
|
||||||
|
|
||||||
|
IHostInput Input { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
public interface IHostSymbolResolver
|
||||||
|
{
|
||||||
|
/// <summary>Returns the native address of the function, or 0 if unavailable.</summary>
|
||||||
|
nint GetAddress(HostRuntimeFunction function);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raw host thread and native-TLS primitives for the execution engine. Guest
|
||||||
|
/// code must run on threads the CLR did not create (no managed frames below
|
||||||
|
/// guest frames), so thread creation takes a native entry point and is not
|
||||||
|
/// expressible with managed threads.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostThreading
|
||||||
|
{
|
||||||
|
/// <summary>Allocates a native TLS slot; returns <see cref="uint.MaxValue"/> on failure.</summary>
|
||||||
|
uint AllocateTlsSlot();
|
||||||
|
|
||||||
|
bool FreeTlsSlot(uint slot);
|
||||||
|
|
||||||
|
bool SetTlsValue(uint slot, nint value);
|
||||||
|
|
||||||
|
nint GetTlsValue(uint slot);
|
||||||
|
|
||||||
|
uint CurrentThreadId { get; }
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||||
|
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||||
|
/// Returns the thread handle, or 0 on failure.
|
||||||
|
/// </summary>
|
||||||
|
nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId);
|
||||||
|
|
||||||
|
/// <summary>Waits for the thread to exit; true when it did within the timeout.</summary>
|
||||||
|
bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds);
|
||||||
|
|
||||||
|
void CloseThreadHandle(nint threadHandle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suspends the thread, snapshots its general-purpose registers, and resumes it —
|
||||||
|
/// one indivisible operation (diagnostics only). The caller must not pass the
|
||||||
|
/// current thread. Returns false if the thread cannot be opened or suspended.
|
||||||
|
/// </summary>
|
||||||
|
bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// 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 implementation over VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery.
|
||||||
|
/// Sealed so the JIT can devirtualize interface calls on fault-handling hot paths.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed unsafe partial class WindowsHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
|
private const uint MEM_RESERVE = 0x2000;
|
||||||
|
private const uint MEM_RELEASE = 0x8000;
|
||||||
|
private const uint MEM_FREE = 0x10000;
|
||||||
|
|
||||||
|
private const uint PAGE_NOACCESS = 0x01;
|
||||||
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
|
private const uint PAGE_WRITECOPY = 0x08;
|
||||||
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_COMMIT | MEM_RESERVE, ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_RESERVE, ToNativeProtection(protection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
return VirtualAlloc((void*)address, (nuint)size, MEM_COMMIT, ToNativeProtection(protection)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Free(ulong address)
|
||||||
|
{
|
||||||
|
return VirtualFree((void*)address, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return VirtualProtect((void*)address, (nuint)size, ToNativeProtection(protection), out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
return VirtualProtect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
mbi.BaseAddress,
|
||||||
|
mbi.AllocationBase,
|
||||||
|
mbi.RegionSize,
|
||||||
|
ToRegionState(mbi.State),
|
||||||
|
mbi.State,
|
||||||
|
ToHostProtection(mbi.Protect),
|
||||||
|
mbi.Protect,
|
||||||
|
mbi.AllocationProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_WRITECOPY,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostRegionState ToRegionState(uint state) => state switch
|
||||||
|
{
|
||||||
|
MEM_COMMIT => HostRegionState.Committed,
|
||||||
|
MEM_RESERVE => HostRegionState.Reserved,
|
||||||
|
MEM_FREE => HostRegionState.Free,
|
||||||
|
_ => HostRegionState.Free,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostPageProtection ToHostProtection(uint rawProtection)
|
||||||
|
{
|
||||||
|
// Strip PAGE_GUARD/PAGE_NOCACHE/PAGE_WRITECOMBINE modifiers; callers needing
|
||||||
|
// them compare HostRegionInfo.RawProtection directly.
|
||||||
|
return (rawProtection & 0xFF) switch
|
||||||
|
{
|
||||||
|
PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||||
|
PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||||
|
PAGE_WRITECOPY => HostPageProtection.ReadWrite,
|
||||||
|
PAGE_EXECUTE => HostPageProtection.Execute,
|
||||||
|
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||||
|
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||||
|
PAGE_EXECUTE_WRITECOPY => HostPageProtection.ExecuteWriteCopy,
|
||||||
|
_ => HostPageProtection.NoAccess,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial void* GetCurrentProcess();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
|
private struct MemoryBasicInformation64
|
||||||
|
{
|
||||||
|
public ulong BaseAddress;
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public uint Alignment1;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
public uint Alignment2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
|
internal sealed class WindowsHostPlatform : IHostPlatform
|
||||||
|
{
|
||||||
|
public IHostMemory Memory { get; } = new WindowsHostMemory();
|
||||||
|
|
||||||
|
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||||
|
|
||||||
|
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
||||||
|
|
||||||
|
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
|
||||||
|
|
||||||
|
public IHostInput Input { get; } = new WindowsHostInput();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 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 WindowsHostSymbolResolver : IHostSymbolResolver
|
||||||
|
{
|
||||||
|
public nint GetAddress(HostRuntimeFunction function)
|
||||||
|
{
|
||||||
|
var kernel32 = GetModuleHandle("kernel32.dll");
|
||||||
|
if (kernel32 == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetProcAddress(kernel32, function switch
|
||||||
|
{
|
||||||
|
HostRuntimeFunction.TlsGetValue => "TlsGetValue",
|
||||||
|
HostRuntimeFunction.QueryPerformanceCounter => "QueryPerformanceCounter",
|
||||||
|
HostRuntimeFunction.SwitchToThread => "SwitchToThread",
|
||||||
|
HostRuntimeFunction.Sleep => "Sleep",
|
||||||
|
HostRuntimeFunction.WaitForSingleObject => "WaitForSingleObject",
|
||||||
|
HostRuntimeFunction.SetEvent => "SetEvent",
|
||||||
|
HostRuntimeFunction.ExitThread => "ExitThread",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utf16 marshalling pins the managed string and passes its address directly
|
||||||
|
// (no copy); Utf8 stack-allocates the transient buffer for these short
|
||||||
|
// ASCII export names. LibraryImport is exact-spelling, hence the W entry point.
|
||||||
|
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
private static partial nint GetModuleHandle(string lpModuleName);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf8)]
|
||||||
|
private static partial nint GetProcAddress(nint hModule, string procName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
// 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 unsafe partial class WindowsHostThreading : IHostThreading
|
||||||
|
{
|
||||||
|
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||||
|
private const uint ThreadGetContext = 0x0008u;
|
||||||
|
private const uint ThreadSuspendResume = 0x0002u;
|
||||||
|
|
||||||
|
// Win64 CONTEXT layout (CONTROL | INTEGER only — no XMM state is requested).
|
||||||
|
private const int Win64ContextSize = 0x4D0;
|
||||||
|
private const int Win64ContextFlagsOffset = 0x30;
|
||||||
|
private const uint ContextAmd64ControlInteger = 0x00100003u;
|
||||||
|
private const int CtxRax = 120;
|
||||||
|
private const int CtxRcx = 128;
|
||||||
|
private const int CtxRdx = 136;
|
||||||
|
private const int CtxRbx = 144;
|
||||||
|
private const int CtxRsp = 152;
|
||||||
|
private const int CtxRbp = 160;
|
||||||
|
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 bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||||
|
|
||||||
|
public bool SetTlsValue(uint slot, nint value) => TlsSetValue(slot, value);
|
||||||
|
|
||||||
|
public nint GetTlsValue(uint slot) => TlsGetValue(slot);
|
||||||
|
|
||||||
|
public uint CurrentThreadId => GetCurrentThreadId();
|
||||||
|
|
||||||
|
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
|
||||||
|
{
|
||||||
|
return SetThreadAffinityMask(GetCurrentThread(), affinityMask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
|
||||||
|
{
|
||||||
|
return CreateThread(0, stackReserveBytes, entry, parameter, StackSizeParamIsAReservation, out threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
return WaitForSingleObject(threadHandle, timeoutMilliseconds) == 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseThreadHandle(nint threadHandle)
|
||||||
|
{
|
||||||
|
_ = CloseHandle(threadHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
|
||||||
|
{
|
||||||
|
registers = default;
|
||||||
|
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume, false, threadId);
|
||||||
|
if (threadHandle == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* contextRecord = null;
|
||||||
|
var suspended = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (SuspendThread(threadHandle) == uint.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
suspended = true;
|
||||||
|
// CONTEXT requires 16-byte alignment (it embeds M128A fields);
|
||||||
|
// NativeMemory.AllocZeroed guarantees max_align_t, stackalloc only
|
||||||
|
// pointer-size — so this stays a native allocation.
|
||||||
|
contextRecord = NativeMemory.AllocZeroed((nuint)Win64ContextSize);
|
||||||
|
*(uint*)((byte*)contextRecord + Win64ContextFlagsOffset) = ContextAmd64ControlInteger;
|
||||||
|
if (!GetThreadContext(threadHandle, contextRecord))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
registers = new HostCapturedRegisters(
|
||||||
|
ReadU64(contextRecord, CtxRip),
|
||||||
|
ReadU64(contextRecord, CtxRsp),
|
||||||
|
ReadU64(contextRecord, CtxRbp),
|
||||||
|
ReadU64(contextRecord, CtxRax),
|
||||||
|
ReadU64(contextRecord, CtxRbx),
|
||||||
|
ReadU64(contextRecord, CtxRcx),
|
||||||
|
ReadU64(contextRecord, CtxRdx));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (contextRecord != null)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(contextRecord);
|
||||||
|
}
|
||||||
|
if (suspended)
|
||||||
|
{
|
||||||
|
_ = ResumeThread(threadHandle);
|
||||||
|
}
|
||||||
|
_ = CloseHandle(threadHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ReadU64(void* contextRecord, int offset)
|
||||||
|
{
|
||||||
|
return *(ulong*)((byte*)contextRecord + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint TlsAlloc();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool TlsFree(uint dwTlsIndex);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool TlsSetValue(uint dwTlsIndex, nint lpTlsValue);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nint TlsGetValue(uint dwTlsIndex);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial uint GetCurrentThreadId();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
private static partial nint GetCurrentThread();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nint CreateThread(
|
||||||
|
nint lpThreadAttributes,
|
||||||
|
nuint dwStackSize,
|
||||||
|
nint lpStartAddress,
|
||||||
|
nint lpParameter,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
out uint lpThreadId);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint SuspendThread(nint hThread);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static partial uint ResumeThread(nint hThread);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool GetThreadContext(nint hThread, void* lpContext);
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by memories that decorate another <see cref="ICpuMemory"/>
|
||||||
|
/// (e.g. access trackers) so capability lookups can unwrap to the real
|
||||||
|
/// implementation without reflection.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICpuMemoryWrapper
|
||||||
|
{
|
||||||
|
ICpuMemory Inner { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guest address-space manipulation beyond plain allocation: fixed-address
|
||||||
|
/// mapping and page-protection changes. Guest addresses are identity-mapped
|
||||||
|
/// onto host pages by the implementing memory, so HLE exports (mmap, mprotect)
|
||||||
|
/// reach these operations through <c>ctx.Memory</c> instead of calling host
|
||||||
|
/// APIs directly. Member signatures deliberately mirror the implementation in
|
||||||
|
/// SharpEmu.Core so existing call sites migrate call-for-call.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuestAddressSpace : IGuestMemoryAllocator
|
||||||
|
{
|
||||||
|
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||||
|
|
||||||
|
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
}
|
||||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
|||||||
public interface IGuestMemoryAllocator
|
public interface IGuestMemoryAllocator
|
||||||
{
|
{
|
||||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||||
|
|
||||||
|
bool TryFreeGuestMemory(ulong address);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -29,6 +30,7 @@ public static class AgcExports
|
|||||||
private const uint ItDispatchDirect = 0x15;
|
private const uint ItDispatchDirect = 0x15;
|
||||||
private const uint ItDispatchIndirect = 0x16;
|
private const uint ItDispatchIndirect = 0x16;
|
||||||
private const uint ItWaitRegMem = 0x3C;
|
private const uint ItWaitRegMem = 0x3C;
|
||||||
|
private const uint ItIndirectBuffer = 0x3F;
|
||||||
private const uint ItEventWrite = 0x46;
|
private const uint ItEventWrite = 0x46;
|
||||||
private const uint ItDmaData = 0x50;
|
private const uint ItDmaData = 0x50;
|
||||||
private const uint ItSetContextReg = 0x69;
|
private const uint ItSetContextReg = 0x69;
|
||||||
@@ -157,7 +159,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
|
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
|
||||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||||
@@ -1943,6 +1945,96 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guest draw-command builders: emit valid (skippable) packets and return a live
|
||||||
|
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8N2tmT3jmC8",
|
||||||
|
ExportName = "sceAgcDcbSetIndexCount",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbSetIndexCount(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, indexCount))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xSAR0LTcRKM",
|
||||||
|
ExportName = "sceAgcDcbJump",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbJump(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var target = ctx[CpuRegister.Rsi];
|
||||||
|
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bbFueFP+J4k",
|
||||||
|
ExportName = "sceAgcDcbSetPredication",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbSetPredication(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
|
var address = ctx[CpuRegister.Rsi];
|
||||||
|
if (dcb == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w6Dj1VJt5qY",
|
||||||
|
ExportName = "sceAgcSetPacketPredication",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int SetPacketPredication(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Global predication toggle on a packet; a no-op is safe for rendering.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "w2rJhmD+dsE",
|
Nid = "w2rJhmD+dsE",
|
||||||
ExportName = "sceAgcDriverAddEqEvent",
|
ExportName = "sceAgcDriverAddEqEvent",
|
||||||
@@ -3288,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}");
|
||||||
}
|
}
|
||||||
@@ -3324,17 +3417,20 @@ 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,
|
||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
translatedDraw.AttributeCount,
|
translatedDraw.AttributeCount,
|
||||||
|
translatedDraw.RenderTargets.Select(target =>
|
||||||
new VulkanGuestRenderTarget(
|
new VulkanGuestRenderTarget(
|
||||||
firstTarget.Address,
|
target.Address,
|
||||||
firstTarget.Width,
|
target.Width,
|
||||||
firstTarget.Height,
|
target.Height,
|
||||||
firstTarget.Format,
|
target.Format,
|
||||||
firstTarget.NumberType),
|
target.NumberType)).ToArray(),
|
||||||
translatedDraw.VertexSpirv,
|
translatedDraw.VertexSpirv,
|
||||||
translatedDraw.VertexCount,
|
translatedDraw.VertexCount,
|
||||||
translatedDraw.InstanceCount,
|
translatedDraw.InstanceCount,
|
||||||
@@ -3462,11 +3558,34 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var renderTargets = GetRenderTargets(state.CxRegisters)
|
var renderTargets = GetRenderTargets(state.CxRegisters)
|
||||||
.Where(target =>
|
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||||
target.Slot == 0 &&
|
.OrderBy(target => target.Slot)
|
||||||
HasPixelColorExport(pixelState, target.Slot))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||||
|
for (var index = 0; index < renderTargets.Length; index++)
|
||||||
|
{
|
||||||
|
var target = renderTargets[index];
|
||||||
|
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
|
||||||
|
target.Format,
|
||||||
|
target.NumberType,
|
||||||
|
out renderTargetFormats[index]))
|
||||||
|
{
|
||||||
|
error =
|
||||||
|
$"unsupported color target format={target.Format} number_type={target.NumberType}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixelOutputs = renderTargets
|
||||||
|
.Select((target, location) => new Gen5PixelOutputBinding(
|
||||||
|
target.Slot,
|
||||||
|
(uint)location,
|
||||||
|
renderTargetFormats[location].OutputKind))
|
||||||
|
.ToArray();
|
||||||
|
var outputLayout = string.Join(
|
||||||
|
';',
|
||||||
|
pixelOutputs.Select(output =>
|
||||||
|
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
|
||||||
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||||
@@ -3475,7 +3594,7 @@ public static class AgcExports
|
|||||||
exportStateFingerprint,
|
exportStateFingerprint,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
pixelStateFingerprint,
|
pixelStateFingerprint,
|
||||||
outputKind,
|
outputLayout,
|
||||||
attributeCount);
|
attributeCount);
|
||||||
var totalGlobalBuffers =
|
var totalGlobalBuffers =
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
@@ -3491,7 +3610,7 @@ public static class AgcExports
|
|||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
pixelEvaluation,
|
pixelEvaluation,
|
||||||
outputKind,
|
pixelOutputs,
|
||||||
out var pixelShader,
|
out var pixelShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: 0,
|
globalBufferBase: 0,
|
||||||
@@ -3579,7 +3698,7 @@ public static class AgcExports
|
|||||||
vertexInputs,
|
vertexInputs,
|
||||||
renderTargets,
|
renderTargets,
|
||||||
ApplyTransparentPremultipliedFillClear(
|
ApplyTransparentPremultipliedFillClear(
|
||||||
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
|
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||||
textures,
|
textures,
|
||||||
vertexInputs,
|
vertexInputs,
|
||||||
pixelEvaluation.InitialScalarRegisters));
|
pixelEvaluation.InitialScalarRegisters));
|
||||||
@@ -3595,7 +3714,8 @@ public static class AgcExports
|
|||||||
/// Chowdren resets its effect layers with an untextured transparent-black
|
/// Chowdren resets its effect layers with an untextured transparent-black
|
||||||
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
||||||
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
||||||
/// Treat precisely that draw shape as an overwrite.
|
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||||
|
/// attachment uses the same premultiplied blend pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||||
VulkanGuestRenderState renderState,
|
VulkanGuestRenderState renderState,
|
||||||
@@ -3607,13 +3727,7 @@ public static class AgcExports
|
|||||||
textures.Count != 0 ||
|
textures.Count != 0 ||
|
||||||
vertexInputs.Count != 0 ||
|
vertexInputs.Count != 0 ||
|
||||||
pixelUserData.Count < 4 ||
|
pixelUserData.Count < 4 ||
|
||||||
renderState.Blend is not
|
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
|
||||||
{
|
|
||||||
Enable: true,
|
|
||||||
ColorSrcFactor: 1,
|
|
||||||
ColorDstFactor: 5,
|
|
||||||
ColorFunc: 0,
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return renderState;
|
return renderState;
|
||||||
}
|
}
|
||||||
@@ -3628,10 +3742,21 @@ public static class AgcExports
|
|||||||
|
|
||||||
return renderState with
|
return renderState with
|
||||||
{
|
{
|
||||||
Blend = renderState.Blend with { Enable = false },
|
Blends = renderState.Blends
|
||||||
|
.Select(blend => blend with { Enable = false })
|
||||||
|
.ToArray(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
|
||||||
|
blend is
|
||||||
|
{
|
||||||
|
Enable: true,
|
||||||
|
ColorSrcFactor: 1,
|
||||||
|
ColorDstFactor: 5,
|
||||||
|
ColorFunc: 0,
|
||||||
|
};
|
||||||
|
|
||||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedDcbState state,
|
SubmittedDcbState state,
|
||||||
@@ -3654,19 +3779,15 @@ public static class AgcExports
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
|
|
||||||
numberType switch
|
|
||||||
{
|
|
||||||
4 => Gen5PixelOutputKind.Uint,
|
|
||||||
5 => Gen5PixelOutputKind.Sint,
|
|
||||||
_ => Gen5PixelOutputKind.Float,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||||
state.Program.Instructions.Any(instruction =>
|
GetPixelColorExportMask(state, target) != 0;
|
||||||
instruction.Control is Gen5ExportControl export &&
|
|
||||||
export.Target == target &&
|
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
|
||||||
export.EnableMask != 0);
|
state.Program.Instructions
|
||||||
|
.Select(instruction => instruction.Control)
|
||||||
|
.OfType<Gen5ExportControl>()
|
||||||
|
.Where(export => export.Target == target)
|
||||||
|
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
|
||||||
|
|
||||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||||
{
|
{
|
||||||
@@ -3824,11 +3945,25 @@ public static class AgcExports
|
|||||||
|
|
||||||
private static VulkanGuestRenderState CreateRenderState(
|
private static VulkanGuestRenderState CreateRenderState(
|
||||||
IReadOnlyDictionary<uint, uint> registers,
|
IReadOnlyDictionary<uint, uint> registers,
|
||||||
RenderTargetDescriptor target)
|
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||||
|
Gen5ShaderState pixelState)
|
||||||
{
|
{
|
||||||
|
if (targets.Count == 0)
|
||||||
|
{
|
||||||
|
return VulkanGuestRenderState.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = targets[0];
|
||||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||||
return new VulkanGuestRenderState(
|
return new VulkanGuestRenderState(
|
||||||
DecodeBlendState(registers, target.Slot),
|
targets.Select(target =>
|
||||||
|
{
|
||||||
|
var blend = DecodeBlendState(registers, target.Slot);
|
||||||
|
return blend with
|
||||||
|
{
|
||||||
|
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
|
||||||
|
};
|
||||||
|
}).ToArray(),
|
||||||
scissor,
|
scissor,
|
||||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||||
}
|
}
|
||||||
@@ -3853,7 +3988,7 @@ public static class AgcExports
|
|||||||
(control >> 24) & 0x1Fu,
|
(control >> 24) & 0x1Fu,
|
||||||
(control >> 21) & 0x7u,
|
(control >> 21) & 0x7u,
|
||||||
((control >> 29) & 1u) != 0,
|
((control >> 29) & 1u) != 0,
|
||||||
writeMask == 0 ? 0xFu : writeMask);
|
writeMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VulkanGuestRect? DecodeScissor(
|
private static VulkanGuestRect? DecodeScissor(
|
||||||
@@ -4250,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;
|
||||||
}
|
}
|
||||||
@@ -4267,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;
|
||||||
}
|
}
|
||||||
@@ -4302,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;
|
||||||
@@ -4328,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;
|
||||||
}
|
}
|
||||||
@@ -4355,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(
|
||||||
@@ -4375,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,
|
||||||
@@ -5191,7 +5492,7 @@ public static class AgcExports
|
|||||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||||
out var compiledPixel,
|
out var compiledPixel,
|
||||||
out var compileError))
|
out var compileError))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
|
|||||||
Sint,
|
Sint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal readonly record struct Gen5PixelOutputBinding(
|
||||||
|
uint GuestSlot,
|
||||||
|
uint HostLocation,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
internal enum Gen5SpirvStage
|
internal enum Gen5SpirvStage
|
||||||
{
|
{
|
||||||
Vertex,
|
Vertex,
|
||||||
|
|||||||
@@ -19,13 +19,59 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1) =>
|
||||||
|
TryCompilePixelShader(
|
||||||
|
state,
|
||||||
|
evaluation,
|
||||||
|
[new Gen5PixelOutputBinding(0, 0, outputKind)],
|
||||||
|
out shader,
|
||||||
|
out error,
|
||||||
|
globalBufferBase,
|
||||||
|
totalGlobalBufferCount,
|
||||||
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
|
|
||||||
|
public static bool TryCompilePixelShader(
|
||||||
|
Gen5ShaderState state,
|
||||||
|
Gen5ShaderEvaluation evaluation,
|
||||||
|
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||||
|
out Gen5SpirvShader shader,
|
||||||
|
out string error,
|
||||||
|
int globalBufferBase = 0,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
|
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
|
||||||
|
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel output guest slots and host locations must be unique";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputs
|
||||||
|
.OrderBy(output => output.HostLocation)
|
||||||
|
.Select((output, index) => output.HostLocation == (uint)index)
|
||||||
|
.All(isDense => isDense))
|
||||||
|
{
|
||||||
|
shader = default!;
|
||||||
|
error = "pixel output host locations must be dense in the 0..N-1 range";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Pixel,
|
Gen5SpirvStage.Pixel,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
outputKind,
|
outputs,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -50,7 +96,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Gen5SpirvStage.Vertex,
|
Gen5SpirvStage.Vertex,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -74,7 +120,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Gen5SpirvStage.Compute,
|
Gen5SpirvStage.Compute,
|
||||||
state,
|
state,
|
||||||
evaluation,
|
evaluation,
|
||||||
Gen5PixelOutputKind.Float,
|
[],
|
||||||
Math.Max(localSizeX, 1),
|
Math.Max(localSizeX, 1),
|
||||||
Math.Max(localSizeY, 1),
|
Math.Max(localSizeY, 1),
|
||||||
Math.Max(localSizeZ, 1),
|
Math.Max(localSizeZ, 1),
|
||||||
@@ -91,7 +137,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly Gen5SpirvStage _stage;
|
private readonly Gen5SpirvStage _stage;
|
||||||
private readonly Gen5ShaderState _state;
|
private readonly Gen5ShaderState _state;
|
||||||
private readonly Gen5ShaderEvaluation _evaluation;
|
private readonly Gen5ShaderEvaluation _evaluation;
|
||||||
private readonly Gen5PixelOutputKind _outputKind;
|
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
|
||||||
private readonly uint _localSizeX;
|
private readonly uint _localSizeX;
|
||||||
private readonly uint _localSizeY;
|
private readonly uint _localSizeY;
|
||||||
private readonly uint _localSizeZ;
|
private readonly uint _localSizeZ;
|
||||||
@@ -101,6 +147,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly int _scalarRegisterBufferIndex;
|
private readonly int _scalarRegisterBufferIndex;
|
||||||
private readonly List<uint> _interfaces = [];
|
private readonly List<uint> _interfaces = [];
|
||||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||||
|
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
|
||||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||||
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
||||||
private readonly List<SpirvImageResource> _imageResources = [];
|
private readonly List<SpirvImageResource> _imageResources = [];
|
||||||
@@ -133,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private uint _lds;
|
private uint _lds;
|
||||||
private uint _workgroupUintPointer;
|
private uint _workgroupUintPointer;
|
||||||
private uint _positionOutput;
|
private uint _positionOutput;
|
||||||
private uint _pixelOutput;
|
|
||||||
private uint _vertexIndexInput;
|
private uint _vertexIndexInput;
|
||||||
private uint _instanceIndexInput;
|
private uint _instanceIndexInput;
|
||||||
private uint _fragCoordInput;
|
private uint _fragCoordInput;
|
||||||
@@ -163,11 +209,16 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
uint Type,
|
uint Type,
|
||||||
uint ComponentCount);
|
uint ComponentCount);
|
||||||
|
|
||||||
|
private readonly record struct SpirvPixelOutput(
|
||||||
|
uint Variable,
|
||||||
|
uint Type,
|
||||||
|
Gen5PixelOutputKind Kind);
|
||||||
|
|
||||||
public CompilationContext(
|
public CompilationContext(
|
||||||
Gen5SpirvStage stage,
|
Gen5SpirvStage stage,
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
Gen5ShaderEvaluation evaluation,
|
Gen5ShaderEvaluation evaluation,
|
||||||
Gen5PixelOutputKind outputKind,
|
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
|
||||||
uint localSizeX,
|
uint localSizeX,
|
||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
@@ -179,7 +230,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_stage = stage;
|
_stage = stage;
|
||||||
_state = state;
|
_state = state;
|
||||||
_evaluation = evaluation;
|
_evaluation = evaluation;
|
||||||
_outputKind = outputKind;
|
_pixelOutputBindings = pixelOutputBindings;
|
||||||
_localSizeX = localSizeX;
|
_localSizeX = localSizeX;
|
||||||
_localSizeY = localSizeY;
|
_localSizeY = localSizeY;
|
||||||
_localSizeZ = localSizeZ;
|
_localSizeZ = localSizeZ;
|
||||||
@@ -700,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)SpirvBuiltIn.FragCoord);
|
(uint)SpirvBuiltIn.FragCoord);
|
||||||
_interfaces.Add(_fragCoordInput);
|
_interfaces.Add(_fragCoordInput);
|
||||||
|
|
||||||
var outputType = _outputKind switch
|
foreach (var binding in _pixelOutputBindings)
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
var outputType = GetPixelOutputType(binding.Kind);
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
|
||||||
_ => _vec4Type,
|
|
||||||
};
|
|
||||||
var outputPointer =
|
var outputPointer =
|
||||||
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
_module.TypePointer(SpirvStorageClass.Output, outputType);
|
||||||
_pixelOutput = _module.AddGlobalVariable(
|
var variable = _module.AddGlobalVariable(
|
||||||
outputPointer,
|
outputPointer,
|
||||||
SpirvStorageClass.Output);
|
SpirvStorageClass.Output);
|
||||||
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
|
_module.AddName(variable, $"mrt{binding.GuestSlot}");
|
||||||
_interfaces.Add(_pixelOutput);
|
_module.AddDecoration(
|
||||||
|
variable,
|
||||||
|
SpirvDecoration.Location,
|
||||||
|
binding.HostLocation);
|
||||||
|
_pixelOutputs.Add(
|
||||||
|
binding.GuestSlot,
|
||||||
|
new SpirvPixelOutput(variable, outputType, binding.Kind));
|
||||||
|
_interfaces.Add(variable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -831,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1);
|
1);
|
||||||
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
|
||||||
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
|
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
|
||||||
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
|
foreach (var output in _pixelOutputs.Values)
|
||||||
|
{
|
||||||
|
Store(output.Variable, _module.ConstantNull(output.Type));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2084,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
if (_stage == Gen5SpirvStage.Pixel)
|
if (_stage == Gen5SpirvStage.Pixel)
|
||||||
{
|
{
|
||||||
if (export.Target != 0)
|
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputType = GetPixelOutputType();
|
|
||||||
var values = new uint[4];
|
var values = new uint[4];
|
||||||
for (var component = 0; component < 4; component++)
|
for (var component = 0; component < 4; component++)
|
||||||
{
|
{
|
||||||
var enabled = (export.EnableMask & (1u << component)) != 0;
|
var enabled = (export.EnableMask & (1u << component)) != 0;
|
||||||
if (!enabled)
|
if (!enabled)
|
||||||
{
|
{
|
||||||
values[component] = _outputKind == Gen5PixelOutputKind.Sint
|
values[component] = _module.AddInstruction(
|
||||||
? Bitcast(_intType, UInt(0))
|
SpirvOp.CompositeExtract,
|
||||||
: UInt(0);
|
output.Kind switch
|
||||||
|
{
|
||||||
|
Gen5PixelOutputKind.Uint => _uintType,
|
||||||
|
Gen5PixelOutputKind.Sint => _intType,
|
||||||
|
_ => _floatType,
|
||||||
|
},
|
||||||
|
Load(output.Type, output.Variable),
|
||||||
|
(uint)component);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2107,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var value = LoadCompressedExportComponent(
|
var value = LoadCompressedExportComponent(
|
||||||
instruction,
|
instruction,
|
||||||
component);
|
component);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
Gen5PixelOutputKind.Uint => _module.AddInstruction(
|
||||||
SpirvOp.ConvertFToU,
|
SpirvOp.ConvertFToU,
|
||||||
@@ -2123,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var raw = LoadV(instruction.Sources[component].Value);
|
var raw = LoadV(instruction.Sources[component].Value);
|
||||||
values[component] = _outputKind switch
|
values[component] = output.Kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => raw,
|
Gen5PixelOutputKind.Uint => raw,
|
||||||
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
|
||||||
@@ -2133,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
var vector = _module.AddInstruction(
|
var vector = _module.AddInstruction(
|
||||||
SpirvOp.CompositeConstruct,
|
SpirvOp.CompositeConstruct,
|
||||||
outputType,
|
output.Type,
|
||||||
values);
|
values);
|
||||||
vector = _module.AddInstruction(
|
vector = _module.AddInstruction(
|
||||||
SpirvOp.Select,
|
SpirvOp.Select,
|
||||||
outputType,
|
output.Type,
|
||||||
Load(_boolType, _exec),
|
Load(_boolType, _exec),
|
||||||
vector,
|
vector,
|
||||||
Load(outputType, _pixelOutput));
|
Load(output.Type, output.Variable));
|
||||||
Store(_pixelOutput, vector);
|
Store(output.Variable, vector);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2209,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
(uint)(component & 1));
|
(uint)(component & 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint GetPixelOutputType() =>
|
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
|
||||||
_outputKind switch
|
kind switch
|
||||||
{
|
{
|
||||||
Gen5PixelOutputKind.Uint => _uvec4Type,
|
Gen5PixelOutputKind.Uint => _uvec4Type,
|
||||||
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
|
||||||
|
|||||||
@@ -348,6 +348,18 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Zi3dBUjgyXI",
|
||||||
|
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
|
||||||
|
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "C+IEj+BsAFM",
|
Nid = "C+IEj+BsAFM",
|
||||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||||
@@ -440,6 +452,40 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "o67gODLFpls",
|
||||||
|
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
|
var equeue = ctx[CpuRegister.Rsi];
|
||||||
|
var ident = ctx[CpuRegister.Rdx];
|
||||||
|
var completionToken = ctx[CpuRegister.Rcx];
|
||||||
|
var userData = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (commandBuffer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AppendKernelEventQueueRecord(
|
||||||
|
ctx,
|
||||||
|
commandBuffer,
|
||||||
|
equeue,
|
||||||
|
ident,
|
||||||
|
completionToken,
|
||||||
|
userData))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "sJXyWHjP-F8",
|
Nid = "sJXyWHjP-F8",
|
||||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -34,9 +34,43 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EventFlagWaiter
|
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
||||||
{
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required EventFlagState State { get; init; }
|
||||||
|
public required ulong Pattern { get; init; }
|
||||||
|
public required uint WaitMode { get; init; }
|
||||||
|
public required ulong ResultAddress { get; init; }
|
||||||
|
public bool Timed { get; init; }
|
||||||
|
|
||||||
|
// Timed-wait completion state; unused when Timed is false.
|
||||||
|
public ulong TimeoutAddress { get; init; }
|
||||||
|
public long DeadlineTimestamp { get; init; }
|
||||||
|
|
||||||
public OrbisGen2Result? Result { get; set; }
|
public OrbisGen2Result? Result { get; set; }
|
||||||
|
|
||||||
|
// Untimed waits stash the prepared result here at wake and return it at resume.
|
||||||
|
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
public int Resume() => Timed
|
||||||
|
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
||||||
|
: (int)_blockedResult;
|
||||||
|
|
||||||
|
public bool TryWake()
|
||||||
|
{
|
||||||
|
if (Timed)
|
||||||
|
{
|
||||||
|
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blockedResult = preparedResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||||
var timedWaiter = new EventFlagWaiter();
|
var timedWaiter = new EventFlagWaiter
|
||||||
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
State = state,
|
||||||
|
Pattern = pattern,
|
||||||
|
WaitMode = waitMode,
|
||||||
|
ResultAddress = resultAddress,
|
||||||
|
Timed = true,
|
||||||
|
TimeoutAddress = timeoutAddress,
|
||||||
|
DeadlineTimestamp = deadline,
|
||||||
|
};
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
resumeHandler: () => CompleteBlockedTimedWait(
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
timedWaiter,
|
timedWaiter,
|
||||||
pattern,
|
|
||||||
waitMode,
|
|
||||||
resultAddress,
|
|
||||||
timeoutAddress,
|
|
||||||
deadline),
|
|
||||||
wakeHandler: () => TryCompleteBlockedTimedWait(
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
timedWaiter,
|
|
||||||
pattern,
|
|
||||||
waitMode,
|
|
||||||
resultAddress),
|
|
||||||
blockDeadlineTimestamp: deadline))
|
blockDeadlineTimestamp: deadline))
|
||||||
{
|
{
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
() => (int)blockedWaitResult,
|
new EventFlagWaiter
|
||||||
() =>
|
|
||||||
{
|
{
|
||||||
if (!TryPrepareBlockedWait(
|
Ctx = ctx,
|
||||||
ctx,
|
State = state,
|
||||||
state,
|
Pattern = pattern,
|
||||||
pattern,
|
WaitMode = waitMode,
|
||||||
waitMode,
|
ResultAddress = resultAddress,
|
||||||
resultAddress,
|
|
||||||
out var preparedResult))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockedWaitResult = preparedResult;
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
|
|||||||
@@ -2,7 +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 System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = new();
|
private static readonly HashSet<ulong> _eventQueues = new();
|
||||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
|
||||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
|
|||||||
short Filter,
|
short Filter,
|
||||||
ulong UserData);
|
ulong UserData);
|
||||||
|
|
||||||
|
// Grow-only ring buffer standing in for LinkedList<KernelQueuedEvent>, which
|
||||||
|
// allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge
|
||||||
|
// per registered queue. Mutated only under _eventQueueGate.
|
||||||
|
private sealed class KernelEventDeque
|
||||||
|
{
|
||||||
|
private KernelQueuedEvent[] _items = new KernelQueuedEvent[4];
|
||||||
|
private int _head;
|
||||||
|
|
||||||
|
public int Count { get; private set; }
|
||||||
|
|
||||||
|
public KernelQueuedEvent this[int index]
|
||||||
|
{
|
||||||
|
get => _items[(_head + index) % _items.Length];
|
||||||
|
set => _items[(_head + index) % _items.Length] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddLast(in KernelQueuedEvent item)
|
||||||
|
{
|
||||||
|
if (Count == _items.Length)
|
||||||
|
{
|
||||||
|
var grown = new KernelQueuedEvent[_items.Length * 2];
|
||||||
|
for (var i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
grown[i] = this[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
_items = grown;
|
||||||
|
_head = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_items[(_head + Count) % _items.Length] = item;
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KernelQueuedEvent RemoveFirst()
|
||||||
|
{
|
||||||
|
var value = _items[_head];
|
||||||
|
_head = (_head + 1) % _items.Length;
|
||||||
|
Count--;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int FindIndex(ulong ident, short filter)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
var candidate = this[i];
|
||||||
|
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong Handle { get; init; }
|
||||||
|
public required ulong EventsAddress { get; init; }
|
||||||
|
public required int EventCapacity { get; init; }
|
||||||
|
public required ulong OutCountAddress { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
|
||||||
|
|
||||||
|
public bool TryWake() => HasPendingEvents(Handle);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
_pendingEvents[handle] = new KernelEventDeque();
|
||||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
_registeredEvents.Remove(handle);
|
_registeredEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wakeKeys.TryRemove(handle, out _);
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEqueue",
|
"sceKernelWaitEqueue",
|
||||||
GetEventQueueWakeKey(handle),
|
GetEventQueueWakeKey(handle),
|
||||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
new EqueueWaiter
|
||||||
() => HasPendingEvents(handle)))
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
Handle = handle,
|
||||||
|
EventsAddress = eventsAddress,
|
||||||
|
EventCapacity = eventCapacity,
|
||||||
|
OutCountAddress = outCountAddress,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", handle);
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var events))
|
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||||
{
|
{
|
||||||
events = new LinkedList<KernelQueuedEvent>();
|
events = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = events;
|
_pendingEvents[handle] = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 1UL;
|
var count = 1UL;
|
||||||
var pendingNode = FindPendingEvent(events, ident, filter);
|
var pendingIndex = events.FindIndex(ident, filter);
|
||||||
if (pendingNode is not null)
|
if (pendingIndex >= 0)
|
||||||
{
|
{
|
||||||
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||||
@@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports
|
|||||||
eventData,
|
eventData,
|
||||||
userData);
|
userData);
|
||||||
|
|
||||||
if (pendingNode is not null)
|
if (pendingIndex >= 0)
|
||||||
{
|
{
|
||||||
pendingNode.Value = triggeredEvent;
|
events[pendingIndex] = triggeredEvent;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void QueueOrUpdateEvent(
|
private static void QueueOrUpdateEvent(
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
KernelEventDeque queue,
|
||||||
KernelQueuedEvent queuedEvent)
|
KernelQueuedEvent queuedEvent)
|
||||||
{
|
{
|
||||||
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
|
||||||
if (pendingNode is null)
|
if (pendingIndex < 0)
|
||||||
{
|
{
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingNode.Value = queuedEvent with
|
queue[pendingIndex] = queuedEvent with
|
||||||
{
|
{
|
||||||
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||||
ulong ident,
|
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||||
short filter)
|
|
||||||
{
|
|
||||||
for (var node = queue.First; node is not null; node = node.Next)
|
|
||||||
{
|
|
||||||
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
|
||||||
{
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||||
$"sceKernelWaitEqueue:{handle:X16}";
|
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||||
|
|
||||||
private static void WakeEventQueue(ulong handle)
|
private static void WakeEventQueue(ulong handle)
|
||||||
{
|
{
|
||||||
@@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Engines wait on the vblank/flip equeue every frame, so the delivery buffer
|
||||||
|
// (usually a single event) comes from the pool instead of a per-call array.
|
||||||
KernelQueuedEvent[] events;
|
KernelQueuedEvent[] events;
|
||||||
|
int count;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||||
@@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = Math.Min(eventCapacity, queue.Count);
|
count = Math.Min(eventCapacity, queue.Count);
|
||||||
events = new KernelQueuedEvent[count];
|
events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
events[i] = queue.First!.Value;
|
events[i] = queue.RemoveFirst();
|
||||||
queue.RemoveFirst();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < events.Length; i++)
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||||
{
|
{
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<KernelQueuedEvent>.Shared.Return(events);
|
||||||
|
}
|
||||||
|
|
||||||
return events.Length;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||||
@@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports
|
|||||||
return ctx.Memory.TryWrite(address, eventBytes);
|
return ctx.Memory.TryWrite(address, eventBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly bool _logEqueue =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
if (!_logEqueue)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
@@ -17,6 +18,10 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int MaxGuestStringLength = 4096;
|
private const int MaxGuestStringLength = 4096;
|
||||||
private const int WideCharSize = sizeof(ushort);
|
private const int WideCharSize = sizeof(ushort);
|
||||||
private const int MemsetChunkSize = 16 * 1024;
|
private const int MemsetChunkSize = 16 * 1024;
|
||||||
|
private const int MemcpyChunkSize = 256 * 1024;
|
||||||
|
|
||||||
|
// Shared all-zero scratch for chunked zero-fill loops; never written to.
|
||||||
|
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||||
private const int TlsModuleBlockSize = 0x10000;
|
private const int TlsModuleBlockSize = 0x10000;
|
||||||
private const int O_WRONLY = 0x1;
|
private const int O_WRONLY = 0x1;
|
||||||
private const int O_RDWR = 0x2;
|
private const int O_RDWR = 0x2;
|
||||||
@@ -52,14 +57,13 @@ public static class KernelMemoryCompatExports
|
|||||||
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
||||||
private const int OrbisVirtualQueryInfoSize = 72;
|
private const int OrbisVirtualQueryInfoSize = 72;
|
||||||
private const int OrbisKernelMaximumNameLength = 32;
|
private const int OrbisKernelMaximumNameLength = 32;
|
||||||
private const uint MemCommit = 0x1000;
|
// Raw Windows PAGE_* values used only against HostRegionInfo.RawProtection,
|
||||||
private const uint MemReserve = 0x2000;
|
// which by contract carries the untranslated protection word of the host
|
||||||
private const uint MemRelease = 0x8000;
|
// platform in use (see IHostMemory).
|
||||||
private const uint HostPageNoAccess = 0x01;
|
private const uint HostPageNoAccess = 0x01;
|
||||||
private const uint HostPageReadOnly = 0x02;
|
private const uint HostPageReadOnly = 0x02;
|
||||||
private const uint HostPageReadWrite = 0x04;
|
private const uint HostPageReadWrite = 0x04;
|
||||||
private const uint HostPageWriteCopy = 0x08;
|
private const uint HostPageWriteCopy = 0x08;
|
||||||
private const uint HostPageExecute = 0x10;
|
|
||||||
private const uint HostPageExecuteRead = 0x20;
|
private const uint HostPageExecuteRead = 0x20;
|
||||||
private const uint HostPageExecuteReadWrite = 0x40;
|
private const uint HostPageExecuteReadWrite = 0x40;
|
||||||
private const uint HostPageExecuteWriteCopy = 0x80;
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
||||||
@@ -121,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;
|
||||||
@@ -136,31 +147,9 @@ public static class KernelMemoryCompatExports
|
|||||||
private static string? _cachedApp0Root;
|
private static string? _cachedApp0Root;
|
||||||
private static string? _cachedDownload0Root;
|
private static string? _cachedDownload0Root;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
// Property (not a cached field) so merely touching this type never resolves
|
||||||
private struct MemoryBasicInformation
|
// the platform backend; non-Windows hosts only throw if a call is reached.
|
||||||
{
|
private static IHostMemory HostMemory => HostPlatform.Current.Memory;
|
||||||
public nint BaseAddress;
|
|
||||||
public nint AllocationBase;
|
|
||||||
public uint AllocationProtect;
|
|
||||||
public nuint RegionSize;
|
|
||||||
public uint State;
|
|
||||||
public uint Protect;
|
|
||||||
public uint Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
|
||||||
|
|
||||||
private sealed class OpenDirectory
|
private sealed class OpenDirectory
|
||||||
{
|
{
|
||||||
@@ -259,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)
|
||||||
@@ -277,11 +266,10 @@ public static class KernelMemoryCompatExports
|
|||||||
DirectStart: 0);
|
DirectStart: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
|
||||||
for (ulong offset = 0; offset < mappedLength;)
|
for (ulong offset = 0; offset < mappedLength;)
|
||||||
{
|
{
|
||||||
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
|
||||||
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -444,13 +432,22 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = new byte[MemsetChunkSize];
|
// Rent may hand back a larger array than requested; only the first chunkLength
|
||||||
Array.Fill(chunk, value);
|
// bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
|
||||||
|
var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
|
||||||
|
var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||||
|
if (value != 0)
|
||||||
|
{
|
||||||
|
chunk.AsSpan(0, chunkLength).Fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var remaining = length;
|
var remaining = length;
|
||||||
var cursor = destination;
|
var cursor = destination;
|
||||||
while (remaining > 0)
|
while (remaining > 0)
|
||||||
{
|
{
|
||||||
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||||
{
|
{
|
||||||
if (length <= 0x40)
|
if (length <= 0x40)
|
||||||
@@ -472,6 +469,14 @@ public static class KernelMemoryCompatExports
|
|||||||
cursor += (ulong)take;
|
cursor += (ulong)take;
|
||||||
remaining -= (ulong)take;
|
remaining -= (ulong)take;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (value != 0)
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -1207,11 +1212,10 @@ public static class KernelMemoryCompatExports
|
|||||||
var rawCount = ctx[CpuRegister.Rdx];
|
var rawCount = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
// upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
|
||||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
// copy attempt from a guest-thread call context, which corrupted the CLR outright
|
||||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
// ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
// code") instead of throwing a normal exception. Reject anything above a sane bound.
|
||||||
// exception. Reject anything above a sane bound before allocating.
|
|
||||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||||
if (rawCount > maxSaneCount)
|
if (rawCount > maxSaneCount)
|
||||||
{
|
{
|
||||||
@@ -1220,15 +1224,52 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = (int)rawCount;
|
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
|
||||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
if (rawCount == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap iterations at the requested chunk size, not chunk.Length: Rent may hand
|
||||||
|
// back a larger array, and the copy granularity should not depend on pool
|
||||||
|
// bucketing internals.
|
||||||
|
var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize);
|
||||||
|
var chunk = ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// memmove aliases this export, so overlapping ranges must survive the chunked
|
||||||
|
// copy: when the destination starts inside the source range, copy high-to-low
|
||||||
|
// so no source byte is overwritten before it has been read.
|
||||||
|
var copyBackward = destination > source && destination - source < rawCount;
|
||||||
|
var remaining = rawCount;
|
||||||
|
ulong offset = copyBackward ? rawCount : 0;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||||
|
if (copyBackward)
|
||||||
|
{
|
||||||
|
offset -= (ulong)take;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = chunk.AsSpan(0, take);
|
||||||
|
if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span))
|
||||||
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
if (!copyBackward)
|
||||||
|
{
|
||||||
|
offset += (ulong)take;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= (ulong)take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1856,6 +1897,45 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fgIsQ10xYVA",
|
||||||
|
ExportName = "sceKernelChmod",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelChmod(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (pathAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
|
||||||
|
{
|
||||||
|
AddNegativeStatCacheForGuestPath(guestPath);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSIX permission bits have no host equivalent on Windows; accept the call
|
||||||
|
// so guests that chmod their freshly created files/directories can proceed.
|
||||||
|
LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "NNtFaKJbPt0",
|
Nid = "NNtFaKJbPt0",
|
||||||
ExportName = "_close",
|
ExportName = "_close",
|
||||||
@@ -3112,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)
|
||||||
@@ -3218,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)
|
||||||
{
|
{
|
||||||
@@ -3563,7 +3643,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -3597,7 +3677,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -5797,42 +5877,40 @@ public static class KernelMemoryCompatExports
|
|||||||
return alignedLength != 0;
|
return alignedLength != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection)
|
private static bool TryProtectHostRange(CpuContext ctx, ulong address, ulong length, int orbisProtection)
|
||||||
{
|
{
|
||||||
if (length == 0 || length > nuint.MaxValue)
|
if (length == 0 || length > nuint.MaxValue)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostProtection = ResolveHostProtection(orbisProtection);
|
if (!KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return addressSpace.TryProtect(address, length, ResolveGuestProtection(orbisProtection));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ResolveHostProtection(int orbisProtection)
|
private static GuestPageProtection ResolveGuestProtection(int orbisProtection)
|
||||||
{
|
{
|
||||||
var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0;
|
var protection = GuestPageProtection.None;
|
||||||
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0;
|
if ((orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0)
|
||||||
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
|
|
||||||
|
|
||||||
if (execute)
|
|
||||||
{
|
{
|
||||||
return write
|
protection |= GuestPageProtection.Read;
|
||||||
? HostPageExecuteReadWrite
|
|
||||||
: read
|
|
||||||
? HostPageExecuteRead
|
|
||||||
: HostPageExecute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return write
|
if ((orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0)
|
||||||
? HostPageReadWrite
|
{
|
||||||
: read
|
protection |= GuestPageProtection.Write;
|
||||||
? HostPageReadOnly
|
}
|
||||||
: HostPageNoAccess;
|
|
||||||
|
if ((orbisProtection & OrbisProtCpuExec) != 0)
|
||||||
|
{
|
||||||
|
protection |= GuestPageProtection.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return protection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
||||||
@@ -6120,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;
|
||||||
@@ -6218,7 +6345,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var effectiveAlignment = Math.Max(alignment, pageSize);
|
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||||
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||||
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||||
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
var baseAddress = unchecked((nint)HostMemory.Allocate(0, reservationSize, HostPageProtection.ReadWrite));
|
||||||
if (baseAddress == 0)
|
if (baseAddress == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -6226,9 +6353,9 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||||
var guardAddress = alignedAddress + (ulong)usableSize;
|
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||||
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
if (!HostMemory.Protect(guardAddress, pageSize, HostPageProtection.NoAccess, out _))
|
||||||
{
|
{
|
||||||
_ = VirtualFree(baseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)baseAddress));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6363,7 +6490,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
if (allocation.IsGuarded)
|
if (allocation.IsGuarded)
|
||||||
{
|
{
|
||||||
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
_ = HostMemory.Free(unchecked((ulong)allocation.BaseAddress));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6459,7 +6586,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6470,7 +6597,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.RawProtection, writeAccess))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6478,16 +6605,14 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info)
|
private static bool TryQueryHostPage(ulong address, out HostRegionInfo info)
|
||||||
{
|
{
|
||||||
info = default;
|
if (!HostMemory.Query(address, out info))
|
||||||
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
|
||||||
if (VirtualQuery((nint)address, out info, size) == 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.State == MemCommit;
|
return info.State == HostRegionState.Committed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
||||||
|
|||||||
@@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports
|
|||||||
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong MutexAddress { get; init; }
|
||||||
|
public required ulong ResolvedAddress { get; init; }
|
||||||
|
public required PthreadMutexState State { get; init; }
|
||||||
public int Reserved;
|
public int Reserved;
|
||||||
|
|
||||||
|
public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||||
|
|
||||||
|
public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required CpuContext Ctx { get; init; }
|
||||||
|
public required ulong CondAddress { get; init; }
|
||||||
|
public required ulong MutexAddress { get; init; }
|
||||||
|
public required PthreadCondState State { get; init; }
|
||||||
|
public required ulong ObservedEpoch { get; init; }
|
||||||
|
public required bool Timed { get; init; }
|
||||||
|
public required int ReleasedRecursion { get; init; }
|
||||||
|
public required bool PosixResult { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult);
|
||||||
|
|
||||||
|
public bool TryWake() => State.SignalEpoch != ObservedEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadCondState
|
private sealed class PthreadCondState
|
||||||
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, state))
|
if (!InitializeMutexObject(ctx, handle, state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -724,7 +751,6 @@ public static class KernelPthreadCompatExports
|
|||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
|
||||||
// Fibers retain the synchronous fallback to preserve switch state.
|
// Fibers retain the synchronous fallback to preserve switch state.
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||||
@@ -736,8 +762,14 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
new PthreadMutexWaiter
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
{
|
||||||
|
ThreadId = currentThreadId,
|
||||||
|
Ctx = ctx,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
ResolvedAddress = resolvedAddress,
|
||||||
|
State = state,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -895,6 +927,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexAttrStates.Remove(handle);
|
_mutexAttrStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +969,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = condAddress;
|
resolvedAddress = condAddress;
|
||||||
state = raced;
|
state = raced;
|
||||||
return true;
|
return true;
|
||||||
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
Span<byte> initialData = stackalloc byte[size];
|
Span<byte> initialData = stackalloc byte[size];
|
||||||
initialData.Clear();
|
initialData.Clear();
|
||||||
return ctx.Memory.TryWrite(address, initialData);
|
if (ctx.Memory.TryWrite(address, initialData))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
address = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
|
||||||
|
{
|
||||||
|
if (ctx.Memory is IGuestMemoryAllocator allocator)
|
||||||
|
{
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,8 +1417,17 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
new PthreadCondWaiter
|
||||||
() => state.SignalEpoch != observedEpoch,
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
CondAddress = condAddress,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
State = state,
|
||||||
|
ObservedEpoch = observedEpoch,
|
||||||
|
Timed = timed,
|
||||||
|
ReleasedRecursion = releasedRecursion,
|
||||||
|
PosixResult = posixResult,
|
||||||
|
},
|
||||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
{
|
{
|
||||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
@@ -1775,6 +1838,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, createdState))
|
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = mutexAddress;
|
resolvedAddress = mutexAddress;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mutexStates.TryGetValue(handle, out state))
|
if (_mutexStates.TryGetValue(handle, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = handle;
|
resolvedAddress = handle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
public required PthreadRwlockState Rwlock { get; init; }
|
||||||
|
public required ulong ThreadId { get; init; }
|
||||||
|
public required bool Write { get; init; }
|
||||||
|
|
||||||
|
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct TlsKeyState(ulong Destructor);
|
private readonly record struct TlsKeyState(ulong Destructor);
|
||||||
|
|
||||||
private readonly record struct PthreadAttrState(
|
private readonly record struct PthreadAttrState(
|
||||||
@@ -382,6 +393,47 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oIRFTjoILbg",
|
||||||
|
ExportName = "scePthreadSetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "P41kTWUS3EI",
|
||||||
|
ExportName = "scePthreadGetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetschedparam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int policy;
|
||||||
|
int priority;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
policy = state.Attributes.SchedPolicy;
|
||||||
|
priority = state.Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
||||||
|
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "nsYoNRywwNg",
|
Nid = "nsYoNRywwNg",
|
||||||
ExportName = "scePthreadAttrInit",
|
ExportName = "scePthreadAttrInit",
|
||||||
@@ -1318,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_wrlock",
|
"pthread_rwlock_wrlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
|
||||||
{
|
{
|
||||||
transferredToScheduler = true;
|
transferredToScheduler = true;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -1355,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_rdlock",
|
"pthread_rwlock_rdlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -48,9 +49,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
private const int MapFlagFixed = 0x10;
|
private const int MapFlagFixed = 0x10;
|
||||||
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
||||||
private const int AioInitParamSize = 0x3C;
|
private const int AioInitParamSize = 0x3C;
|
||||||
private const uint MemCommit = 0x1000;
|
|
||||||
private const uint MemReserve = 0x2000;
|
|
||||||
private const uint PageExecuteReadWrite = 0x40;
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
|
||||||
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
|
||||||
@@ -1893,7 +1891,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nint stubAddress = VirtualAlloc(nint.Zero, (nuint)16, MemCommit | MemReserve, PageExecuteReadWrite);
|
nint stubAddress = unchecked((nint)HostPlatform.Current.Memory.Allocate(0, 16, HostPageProtection.ReadWriteExecute));
|
||||||
if (stubAddress == 0)
|
if (stubAddress == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -1923,9 +1921,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
|
||||||
|
|
||||||
private static bool TryReserveVirtualRange(
|
private static bool TryReserveVirtualRange(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
|
||||||
|
public required string WakeKey { get; init; }
|
||||||
public required int InitialCount { get; init; }
|
public required int InitialCount { get; init; }
|
||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SemaphoreWaiter
|
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
||||||
{
|
{
|
||||||
|
public required KernelSemaphoreState Semaphore { get; init; }
|
||||||
public required int NeedCount { get; init; }
|
public required int NeedCount { get; init; }
|
||||||
public required int CancelEpochAtBlock { get; init; }
|
public required int CancelEpochAtBlock { get; init; }
|
||||||
public bool Timed { get; init; }
|
public bool Timed { get; init; }
|
||||||
|
|
||||||
|
// Timed-wait completion state; unused when Timed is false.
|
||||||
|
public CpuContext? Ctx { get; init; }
|
||||||
|
public ulong TimeoutAddress { get; init; }
|
||||||
|
public long DeadlineTimestamp { get; init; }
|
||||||
|
|
||||||
// Written and read only under the owning semaphore's Gate.
|
// Written and read only under the owning semaphore's Gate.
|
||||||
public int? Result { get; set; }
|
public int? Result { get; set; }
|
||||||
|
|
||||||
|
public int Resume() => Timed
|
||||||
|
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
||||||
|
: CompleteBlockedSemaWait(Semaphore, this);
|
||||||
|
|
||||||
|
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||||
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var state = new KernelSemaphoreState
|
var state = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
InitialCount = initialCount,
|
InitialCount = initialCount,
|
||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
|
|||||||
state.Deleted = true;
|
state.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (timeoutMicros == 0)
|
if (timeoutMicros == 0)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
pollTimedOut = true;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||||
var timedWaiter = new SemaphoreWaiter
|
var timedWaiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
Timed = true,
|
Timed = true,
|
||||||
|
Ctx = ctx,
|
||||||
|
TimeoutAddress = timeoutAddress,
|
||||||
|
DeadlineTimestamp = deadline,
|
||||||
};
|
};
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
timedWaiter,
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
|
||||||
blockDeadlineTimestamp: deadline))
|
blockDeadlineTimestamp: deadline))
|
||||||
{
|
{
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||||
// immediate-timeout poll the callers already tolerate.
|
// immediate-timeout poll the callers already tolerate.
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
pollTimedOut = true;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
var waiter = new SemaphoreWaiter
|
var waiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
};
|
};
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
waiter))
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
{
|
||||||
|
if (_traceSema)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,13 +277,19 @@ public static class KernelSemaphoreCompatExports
|
|||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
|
{
|
||||||
|
if (_traceSema)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||||
// waiter can proceed.
|
// waiter can proceed.
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +370,13 @@ public static class KernelSemaphoreCompatExports
|
|||||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||||
// and silently absorb the increment of a waiter that parks between this
|
// and silently absorb the increment of a waiter that parks between this
|
||||||
// gate release and the wake-all below.
|
// gate release and the wake-all below.
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Deleted = true;
|
semaphore.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Count -= waiter.NeedCount;
|
semaphore.Count -= waiter.NeedCount;
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,8 +497,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
if (_traceSema)
|
||||||
|
{
|
||||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = waiter.Result!.Value;
|
result = waiter.Result!.Value;
|
||||||
}
|
}
|
||||||
@@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call sites must check this before building the interpolated message; the trace
|
||||||
|
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||||
|
private static readonly bool _traceSema =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
{
|
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,12 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
internal static class KernelVirtualRangeAllocator
|
internal static class KernelVirtualRangeAllocator
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
|
||||||
|
|
||||||
public static bool TryReserve(
|
public static bool TryReserve(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
@@ -31,38 +28,24 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
if (!TryResolveAddressSpace(ctx.Memory, out var addressSpace))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
if (allowSearch &&
|
||||||
{
|
addressSpace.TryAllocateAtOrAbove(desiredAddress, length, executable, alignment, out var searchedAddress) &&
|
||||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
searchedAddress != 0)
|
||||||
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
{
|
||||||
mappedAddress = searchedAddress;
|
mappedAddress = searchedAddress;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.AllocateAt is null)
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {typeof(ulong).FullName} value=0");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
|
||||||
: new object[] { desiredAddress, length, executable };
|
|
||||||
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,89 +54,44 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
/// <summary>
|
||||||
|
/// Finds the <see cref="IGuestAddressSpace"/> behind <paramref name="rootMemory"/>,
|
||||||
|
/// unwrapping decorators (bounded, like the reflection walker this replaced).
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryResolveAddressSpace(ICpuMemory rootMemory, [NotNullWhen(true)] out IGuestAddressSpace? addressSpace)
|
||||||
{
|
{
|
||||||
target = rootMemory;
|
var target = rootMemory;
|
||||||
accessor = default;
|
|
||||||
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
for (var depth = 0; depth < 4; depth++)
|
||||||
{
|
{
|
||||||
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
if (target is IGuestAddressSpace resolved)
|
||||||
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
|
||||||
{
|
{
|
||||||
|
addressSpace = resolved;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.InnerProperty is null)
|
if (target is not ICpuMemoryWrapper wrapper)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerValue = accessor.InnerProperty.GetValue(target);
|
var inner = wrapper.Inner;
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, target))
|
if (inner is null || ReferenceEquals(inner, target))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
target = innerValue;
|
target = inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addressSpace = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Accessor DiscoverAccessor(Type type)
|
|
||||||
{
|
|
||||||
MethodInfo? allocateAt = null;
|
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
|
|
||||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct Accessor(
|
|
||||||
MethodInfo? AllocateAt,
|
|
||||||
MethodInfo? AllocateAtOrAbove,
|
|
||||||
bool AllocateAtHasAllowAlternativeArg,
|
|
||||||
PropertyInfo? InnerProperty);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
|
|||||||
|
|
||||||
public static class MouseExports
|
public static class MouseExports
|
||||||
{
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Qs0wWulgl7U",
|
||||||
|
ExportName = "sceMouseInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||||
// as an sceNgs2VoiceGetState alias.
|
// as an sceNgs2VoiceGetState alias.
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ public static class NetCtlExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1NE9OWdBIww",
|
||||||
|
ExportName = "sceNetCtlRegisterCallbackV6",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "obuxdTiwkF8",
|
Nid = "obuxdTiwkF8",
|
||||||
ExportName = "sceNetCtlGetInfo",
|
ExportName = "sceNetCtlGetInfo",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
|
||||||
|
// Common::initialize returns a negative SCE error, so no-op success is required to boot.
|
||||||
|
public static class NpCppWebApiExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "UYPxv8MIzGo",
|
||||||
|
ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpCppWebApi")]
|
||||||
|
public static int CppWebApiCommonInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// int Common::initialize(const InitParams&, LibContext&) — 0 on success.
|
||||||
|
TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
|
||||||
|
// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
|
||||||
|
public static class BluetoothHidExports
|
||||||
|
{
|
||||||
|
private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
|
||||||
|
|
||||||
|
private static readonly bool _reportUnavailable = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int Result(CpuContext ctx) =>
|
||||||
|
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tul3-GzejQc",
|
||||||
|
ExportName = "sceBluetoothHidInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4FUZ+c52d2k",
|
||||||
|
ExportName = "sceBluetoothHidRegisterDevice",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4Ypfo9RIwfM",
|
||||||
|
ExportName = "sceBluetoothHidRegisterCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceBluetoothHid")]
|
||||||
|
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +47,18 @@ public static class PadExports
|
|||||||
ExportName = "scePadOpen",
|
ExportName = "scePadOpen",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadOpen(CpuContext ctx)
|
public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WFIiSfXGUq8",
|
||||||
|
ExportName = "scePadOpenExt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||||
|
|
||||||
|
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||||
|
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||||
|
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
@@ -64,25 +74,37 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||||
|
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||||
{
|
{
|
||||||
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] 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.");
|
: "[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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6ncge5+l5Qs",
|
||||||
|
ExportName = "scePadClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return handle == PrimaryPadHandle
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "clVvL4ZDntw",
|
Nid = "clVvL4ZDntw",
|
||||||
ExportName = "scePadSetMotionSensorState",
|
ExportName = "scePadSetMotionSensorState",
|
||||||
@@ -131,6 +153,47 @@ public static class PadExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hGbf2QTBmqc",
|
||||||
|
ExportName = "scePadGetExtControllerInformation",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadGetExtControllerInformation(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (informationAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base ScePadControllerInformation + device-class/connection fields: report a connected
|
||||||
|
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
|
||||||
|
Span<byte> information = stackalloc byte[0x40];
|
||||||
|
information.Clear();
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||||
|
information[0x08] = 30;
|
||||||
|
information[0x09] = 30;
|
||||||
|
information[0x0A] = StandardPortType;
|
||||||
|
information[0x0B] = 1; // connected count
|
||||||
|
information[0x0C] = 1; // connected
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||||
|
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
|
||||||
|
information[0x1D] = 1; // connected (ext)
|
||||||
|
information[0x1E] = 0; // connectionType: local
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(informationAddress, information)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YndgXqQVV7c",
|
Nid = "YndgXqQVV7c",
|
||||||
ExportName = "scePadReadState",
|
ExportName = "scePadReadState",
|
||||||
@@ -216,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);
|
||||||
@@ -260,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,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(
|
||||||
@@ -400,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ReadKeyboardButtons()
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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" />
|
||||||
|
|||||||
@@ -185,6 +185,31 @@ public static class UserServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "woNpu+45RLk",
|
||||||
|
ExportName = "sceUserServiceGetAgeLevel",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAgeLevel(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var ageLevelAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId != 1000)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ageLevelAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report an adult account so titles skip parental-restriction paths.
|
||||||
|
return ctx.TryWriteInt32(ageLevelAddress, 21)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceUserService(string message)
|
private static void TraceUserService(string message)
|
||||||
{
|
{
|
||||||
if (_traceUserService)
|
if (_traceUserService)
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
// 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;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -67,7 +69,7 @@ public static class VideoOutExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HostTimerResolution.Request();
|
HostPlatform.Current.Threading.RequestTimerResolution();
|
||||||
|
|
||||||
_vblankPumpThread = new Thread(VblankPumpLoop)
|
_vblankPumpThread = new Thread(VblankPumpLoop)
|
||||||
{
|
{
|
||||||
@@ -180,6 +182,10 @@ public static class VideoOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only ever touched by the vblank pump thread; reused across edges so the 60 Hz
|
||||||
|
// pump does not allocate a fresh snapshot per edge.
|
||||||
|
private static readonly List<VideoOutPortState> _vblankPumpPorts = new();
|
||||||
|
|
||||||
private static void PumpVblanks()
|
private static void PumpVblanks()
|
||||||
{
|
{
|
||||||
lock (_vblankEdgeGate)
|
lock (_vblankEdgeGate)
|
||||||
@@ -188,7 +194,7 @@ public static class VideoOutExports
|
|||||||
Monitor.PulseAll(_vblankEdgeGate);
|
Monitor.PulseAll(_vblankEdgeGate);
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoOutPortState[] ports;
|
_vblankPumpPorts.Clear();
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (_ports.Count == 0)
|
if (_ports.Count == 0)
|
||||||
@@ -198,10 +204,16 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
foreach (var port in _ports.Values)
|
||||||
|
{
|
||||||
|
if (port.VblankEvents.Count != 0)
|
||||||
|
{
|
||||||
|
_vblankPumpPorts.Add(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var port in ports)
|
foreach (var port in _vblankPumpPorts)
|
||||||
{
|
{
|
||||||
SignalVblank(port);
|
SignalVblank(port);
|
||||||
}
|
}
|
||||||
@@ -221,6 +233,12 @@ public static class VideoOutExports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
// Call sites must check this before building the interpolated message; the trace
|
||||||
|
// strings would otherwise be allocated on the per-frame flip path even with tracing off.
|
||||||
|
private static readonly bool _logVideoOut = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private static readonly bool _dumpVideoOut = string.Equals(
|
private static readonly bool _dumpVideoOut = string.Equals(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||||
"1",
|
"1",
|
||||||
@@ -403,6 +421,34 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+I4K03i3EL0",
|
||||||
|
ExportName = "sceVideoOutInitializeOutputOptions",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutInitializeOutputOptions(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w0hLuNarQxY",
|
||||||
|
ExportName = "sceVideoOutConfigureOutput",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutConfigureOutput(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Accept the requested output configuration; the presenter always renders
|
||||||
|
// at the display buffer's native size.
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!TryGetPort(handle, out _))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "utPrVdxio-8",
|
Nid = "utPrVdxio-8",
|
||||||
ExportName = "sceVideoOutGetOutputStatus",
|
ExportName = "sceVideoOutGetOutputStatus",
|
||||||
@@ -558,7 +604,10 @@ public static class VideoOutExports
|
|||||||
// Some engines wait on this queue before issuing their first flip. Provide a first
|
// Some engines wait on this queue before issuing their first flip. Provide a first
|
||||||
// edge now; later calls to WaitVblank advance the same notification sequence.
|
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||||
SignalVblank(port);
|
SignalVblank(port);
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||||
|
}
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +644,10 @@ public static class VideoOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||||
|
}
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +708,10 @@ public static class VideoOutExports
|
|||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||||
|
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||||
|
}
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,33 +1104,53 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
private static void SignalVblank(VideoOutPortState port)
|
private static void SignalVblank(VideoOutPortState port)
|
||||||
{
|
{
|
||||||
List<FlipEventRegistration> vblankEvents;
|
// Snapshot the registrations into a pooled rental so the triggers can run outside
|
||||||
|
// _stateGate without copying the list into a fresh allocation on every edge.
|
||||||
|
// A per-port reusable buffer would race: the pump thread and a guest thread's
|
||||||
|
// first-edge signal (AddVblankEvent) can signal the same port concurrently.
|
||||||
|
FlipEventRegistration[]? vblankEvents = null;
|
||||||
|
int vblankEventCount;
|
||||||
ulong eventHint;
|
ulong eventHint;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
port.VblankCount++;
|
port.VblankCount++;
|
||||||
eventHint = SceVideoOutInternalEventVblank |
|
eventHint = SceVideoOutInternalEventVblank |
|
||||||
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||||
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
|
vblankEventCount = port.VblankEvents.Count;
|
||||||
|
if (vblankEventCount != 0)
|
||||||
|
{
|
||||||
|
vblankEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(vblankEventCount);
|
||||||
|
port.VblankEvents.CopyTo(vblankEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||||
|
|
||||||
foreach (var vblankEvent in vblankEvents)
|
if (vblankEvents is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < vblankEventCount; i++)
|
||||||
{
|
{
|
||||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
vblankEvent.Equeue,
|
vblankEvents[i].Equeue,
|
||||||
SceVideoOutInternalEventVblank,
|
SceVideoOutInternalEventVblank,
|
||||||
OrbisKernelEventFilterVideoOut,
|
OrbisKernelEventFilterVideoOut,
|
||||||
eventHint,
|
eventHint,
|
||||||
vblankEvent.UserData);
|
vblankEvents[i].UserData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<FlipEventRegistration>.Shared.Return(vblankEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
||||||
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
|
$"queues={vblankEventCount} hint=0x{eventHint:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,8 +1172,11 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidIndex;
|
return OrbisVideoOutErrorInvalidIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pooled snapshot for the same reason as SignalVblank: triggers run outside
|
||||||
|
// _stateGate, and SubmitFlip is per-frame so a fresh List copy is steady churn.
|
||||||
ulong eventHint;
|
ulong eventHint;
|
||||||
List<FlipEventRegistration> flipEvents;
|
FlipEventRegistration[]? flipEvents = null;
|
||||||
|
int flipEventCount;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||||
@@ -1110,7 +1188,12 @@ public static class VideoOutExports
|
|||||||
port.FlipCount++;
|
port.FlipCount++;
|
||||||
eventHint = SceVideoOutInternalEventFlip |
|
eventHint = SceVideoOutInternalEventFlip |
|
||||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
flipEventCount = port.FlipEvents.Count;
|
||||||
|
if (flipEventCount != 0)
|
||||||
|
{
|
||||||
|
flipEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(flipEventCount);
|
||||||
|
port.FlipEvents.CopyTo(flipEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var guestImageSubmitted = false;
|
var guestImageSubmitted = false;
|
||||||
@@ -1132,14 +1215,24 @@ public static class VideoOutExports
|
|||||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var flipEvent in flipEvents)
|
if (flipEvents is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < flipEventCount; i++)
|
||||||
{
|
{
|
||||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
flipEvent.Equeue,
|
flipEvents[i].Equeue,
|
||||||
SceVideoOutInternalEventFlip,
|
SceVideoOutInternalEventFlip,
|
||||||
OrbisKernelEventFilterVideoOut,
|
OrbisKernelEventFilterVideoOut,
|
||||||
eventHint,
|
eventHint,
|
||||||
flipEvent.UserData);
|
flipEvents[i].UserData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<FlipEventRegistration>.Shared.Return(flipEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||||
@@ -1148,10 +1241,13 @@ public static class VideoOutExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||||
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
||||||
$"flipQueues={flipEvents.Count}");
|
$"flipQueues={flipEventCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
|
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEventCount}");
|
||||||
|
}
|
||||||
ReportFrameRate(presented: false);
|
ReportFrameRate(presented: false);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1233,8 +1329,11 @@ public static class VideoOutExports
|
|||||||
slot.AddressRight = 0;
|
slot.AddressRight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut(
|
TraceVideoOut(
|
||||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||||
|
}
|
||||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||||
|
|
||||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||||
@@ -1400,7 +1499,10 @@ public static class VideoOutExports
|
|||||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,7 +1541,10 @@ public static class VideoOutExports
|
|||||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||||
|
if (_logVideoOut)
|
||||||
|
{
|
||||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,11 +1771,6 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
private static void TraceVideoOut(string message)
|
private static void TraceVideoOut(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user