mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd65609076 | |||
| 459ae7e3f7 | |||
| 8f405caebe | |||
| dabf723b3e | |||
| 2129a12684 | |||
| 2db1fae282 | |||
| 33f96252da | |||
| f7981a7ed7 | |||
| e10efa3ae1 | |||
| 16a2131b67 | |||
| 9883a9445d | |||
| f2d9051358 | |||
| 53d414e096 | |||
| 52d2874fa8 | |||
| c5a82c1065 | |||
| a8be1daca4 | |||
| 26dbad8ac8 | |||
| 2196a1f786 | |||
| 8db2c0622d | |||
| 53da00fa89 | |||
| b4b95014f1 | |||
| 7b91c964fc | |||
| 9bacb883f1 | |||
| 864cbb0fa0 | |||
| ad5a7d3799 | |||
| f69fdd4027 | |||
| 9eef39549a | |||
| 90651f26a9 | |||
| b5930465f2 | |||
| 83303e953e | |||
| 92497689ab | |||
| de7973af55 | |||
| 3da7ae7d70 | |||
| 1be009ce40 | |||
| ad92ab30fd | |||
| 5f97d2edc4 | |||
| 5c8ace82c0 | |||
| 341c2a0cb6 | |||
| 320dbcacba | |||
| 30fdd8d6ed | |||
| c69ac6ddab | |||
| fa2616d224 |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -0,0 +1,120 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Posts (and keeps updated) a single PR comment linking the per-platform build
|
||||
# artifacts once "Build and Release" finishes.
|
||||
#
|
||||
# This is a workflow_run workflow on purpose: PRs from forks run "Build and
|
||||
# Release" with a read-only GITHUB_TOKEN and cannot comment. workflow_run runs
|
||||
# afterwards in the base-repo context with a read-write token and does not check
|
||||
# out untrusted fork code, so it can comment safely. Because of that, GitHub only
|
||||
# triggers it from the copy on the default branch — it does nothing until merged
|
||||
# to main.
|
||||
name: PR Build Links
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Post artifact links
|
||||
runs-on: ubuntu-latest
|
||||
# Only for successful PR builds — artifacts exist only when the build passed.
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Post or update the artifact-links comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = context.payload.workflow_run;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Resolve the PR. Same-repo PRs are in workflow_run.pull_requests, but
|
||||
// fork PRs leave it empty AND their head commit is not on a base-repo
|
||||
// branch, so listPullRequestsAssociatedWithCommit misses them too. Match
|
||||
// the open PR by its head "owner:branch" instead, which works for forks.
|
||||
let prNumber;
|
||||
if (run.pull_requests && run.pull_requests.length > 0) {
|
||||
prNumber = run.pull_requests[0].number;
|
||||
} else {
|
||||
let candidates = [];
|
||||
const headOwner = run.head_repository && run.head_repository.owner
|
||||
? run.head_repository.owner.login
|
||||
: null;
|
||||
if (headOwner && run.head_branch) {
|
||||
const byHead = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open',
|
||||
head: `${headOwner}:${run.head_branch}`, per_page: 10,
|
||||
});
|
||||
candidates = byHead.data;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
const byCommit = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner, repo, commit_sha: run.head_sha,
|
||||
});
|
||||
candidates = byCommit.data.filter(pr => pr.state === 'open');
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
core.info('No open PR for this build; nothing to comment.');
|
||||
return;
|
||||
}
|
||||
prNumber = candidates[0].number;
|
||||
}
|
||||
|
||||
// Collect the per-platform artifacts the build produced.
|
||||
const artifacts = await github.paginate(
|
||||
github.rest.actions.listWorkflowRunArtifacts,
|
||||
{ owner, repo, run_id: run.id, per_page: 100 },
|
||||
);
|
||||
const platforms = [
|
||||
{ key: 'win-x64', label: 'Windows (win-x64)' },
|
||||
{ key: 'linux-x64', label: 'Linux (linux-x64)' },
|
||||
{ key: 'osx-x64', label: 'macOS (osx-x64)' },
|
||||
];
|
||||
const rows = [];
|
||||
for (const platform of platforms) {
|
||||
const artifact = artifacts.find(a => a.name.includes(platform.key));
|
||||
if (!artifact) {
|
||||
continue;
|
||||
}
|
||||
const url = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`;
|
||||
rows.push(`| ${platform.label} | [\`${artifact.name}\`](${url}) |`);
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
core.info('No platform artifacts on this run; nothing to comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = '<!-- pr-build-links -->';
|
||||
const body = [
|
||||
marker,
|
||||
`### 📦 Build artifacts — \`${run.head_sha.substring(0, 7)}\``,
|
||||
'',
|
||||
'| Platform | Download |',
|
||||
'| --- | --- |',
|
||||
...rows,
|
||||
'',
|
||||
`From [build run #${run.run_number}](${run.html_url}). ` +
|
||||
'Downloads require a GitHub login and expire after 90 days.',
|
||||
].join('\n');
|
||||
|
||||
// Upsert one comment so repeated builds refresh it in place.
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: prNumber, per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||
}
|
||||
+113
-20
@@ -34,29 +34,31 @@ jobs:
|
||||
name: Init
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
archive-name: ${{ steps.vars.outputs.archive-name }}
|
||||
artifact-name: ${{ steps.vars.outputs.artifact-name }}
|
||||
release-name: ${{ steps.vars.outputs.release-name }}
|
||||
release-tag: ${{ steps.vars.outputs.release-tag }}
|
||||
safe-ref: ${{ steps.vars.outputs.safe-ref }}
|
||||
short-sha: ${{ steps.vars.outputs.short-sha }}
|
||||
version: ${{ steps.vars.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Compute workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
short_sha="${GITHUB_SHA::7}"
|
||||
safe_ref="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g')"
|
||||
archive_name="sharpemu-win64-${short_sha}.zip"
|
||||
artifact_name="sharpemu-win64-${short_sha}"
|
||||
release_tag="win64-${safe_ref}-${short_sha}"
|
||||
release_name="SharpEmu win64 ${short_sha}"
|
||||
version="$(python3 -c 'import sys, xml.etree.ElementTree as ET; version = ET.parse("Directory.Build.props").getroot().findtext(".//SharpEmuVersion"); sys.exit("Directory.Build.props is missing SharpEmuVersion") if not version or not version.strip() else print(version.strip())')"
|
||||
release_tag="v${version}"
|
||||
release_name="SharpEmu v${version}"
|
||||
|
||||
{
|
||||
echo "short-sha=${short_sha}"
|
||||
echo "archive-name=${archive_name}"
|
||||
echo "artifact-name=${artifact_name}"
|
||||
echo "safe-ref=${safe_ref}"
|
||||
echo "release-tag=${release_tag}"
|
||||
echo "release-name=${release_name}"
|
||||
echo "version=${version}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
reuse:
|
||||
@@ -92,14 +94,20 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
Directory.Packages.props
|
||||
src/**/packages.lock.json
|
||||
Directory.Build.props
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore SharpEmu.slnx --locked-mode
|
||||
run: dotnet restore SharpEmu.slnx
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||
|
||||
# Runs every test project in the solution; a test failure fails the build, as
|
||||
# does an aerolib.bin generation failure in the build step above (the MSBuild
|
||||
# task logs an error and returns false).
|
||||
- name: Run tests
|
||||
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
|
||||
|
||||
- name: Validate synthetic shaders
|
||||
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||
|
||||
@@ -110,7 +118,8 @@ jobs:
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||
|
||||
$archivePath = Join-Path $env:RELEASE_DIR "${{ needs.init.outputs.archive-name }}"
|
||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||
if (Test-Path $archivePath) {
|
||||
Remove-Item $archivePath -Force
|
||||
}
|
||||
@@ -120,8 +129,72 @@ jobs:
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ needs.init.outputs.artifact-name }}
|
||||
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
|
||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||
if-no-files-found: error
|
||||
|
||||
build-posix:
|
||||
name: Build ${{ matrix.rid }}
|
||||
needs:
|
||||
- init
|
||||
- reuse
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rid: linux-x64
|
||||
- os: macos-latest
|
||||
rid: osx-x64
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.103
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
Directory.Packages.props
|
||||
Directory.Build.props
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore SharpEmu.slnx
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||
|
||||
# Also runs on Linux/macOS: the aerolib.bin MSBuild task does platform-sensitive
|
||||
# path handling, so a cross-platform generation regression surfaces here.
|
||||
- name: Run tests
|
||||
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
|
||||
|
||||
- name: Publish ${{ matrix.rid }} CLI
|
||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR"
|
||||
|
||||
- name: Stage MoltenVK next to the build
|
||||
if: matrix.rid == 'osx-x64'
|
||||
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
# tar keeps the executable bit, which zip would drop.
|
||||
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||
-C "$PUBLISH_DIR" .
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -129,35 +202,55 @@ jobs:
|
||||
needs:
|
||||
- init
|
||||
- build
|
||||
- build-posix
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ needs.init.outputs.artifact-name }}
|
||||
path: release
|
||||
|
||||
- name: Create or update release
|
||||
shell: bash
|
||||
env:
|
||||
ARCHIVE_NAME: ${{ needs.init.outputs.archive-name }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
asset_path="release/${ARCHIVE_NAME}"
|
||||
notes="Automated Windows build for commit ${GITHUB_SHA}."
|
||||
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||
if [ "${#assets[@]}" -eq 0 ]; then
|
||||
echo "No release assets found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
notes="Automated SharpEmu v${VERSION} 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 upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
||||
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
||||
else
|
||||
gh release create "${RELEASE_TAG}" "${asset_path}" \
|
||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes "${notes}" \
|
||||
--target "${GITHUB_SHA}"
|
||||
fi
|
||||
|
||||
keep=()
|
||||
for asset_path in "${assets[@]}"; do
|
||||
keep+=("$(basename "${asset_path}")")
|
||||
done
|
||||
mapfile -t release_assets < <(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)
|
||||
for asset in "${release_assets[@]}"; do
|
||||
case "${asset}" in
|
||||
sharpemu-${VERSION}-*.zip|sharpemu-${VERSION}-*.tar.gz)
|
||||
if ! printf '%s\n' "${keep[@]}" | grep -Fxq "${asset}"; then
|
||||
gh release delete-asset "${RELEASE_TAG}" "${asset}" --yes
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -9,7 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<SharpEmuVersion>0.0.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
<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.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
@@ -22,4 +26,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -25,8 +25,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
---
|
||||
|
||||
> [!WARNING]
|
||||
> Currently the primary development target is Windows.
|
||||
> [!NOTE]
|
||||
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||
> can run the macOS x64 build through Rosetta 2.
|
||||
|
||||
> [!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.
|
||||
@@ -40,6 +41,16 @@ This project is developed purely for research and educational purposes. There ar
|
||||
SharpEmu focuses exclusively on the PlayStation 5.
|
||||
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
|
||||
|
||||
## Games Tested
|
||||
|
||||
| Demons Souls Remake | Dreaming Sarah |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Void Terrarium | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
## Status
|
||||
|
||||
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
|
||||
@@ -59,33 +70,33 @@ Current capabilities include:
|
||||
|
||||
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
|
||||
|
||||
* Build or Publish project or download in release tab.
|
||||
* Open Powershell.
|
||||
* Run Emulator GUI.
|
||||
* Or command: `.\SharpEmu "eboot.bin" 2>&1 | Tee-Object -FilePath "log.txt"`
|
||||
Download the release archive for your operating system, extract it, and launch
|
||||
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||
|
||||
## Games Tested
|
||||
Windows PowerShell:
|
||||
|
||||
* **Demon's Souls Remake**
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/par274/sharpemu/issues/2)
|
||||
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
|
||||

|
||||
```powershell
|
||||
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
|
||||
Tee-Object -FilePath "SharpEmu.log"
|
||||
```
|
||||
|
||||
* **Poppy Playtime Chapter 1**
|
||||
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/par274/sharpemu/issues/3)
|
||||
Linux and macOS:
|
||||
|
||||
* **SILENT HILL: The Short Message**
|
||||
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/par274/sharpemu/issues/4)
|
||||
```bash
|
||||
chmod +x ./SharpEmu
|
||||
|
||||
* **Dreaming Sarah**
|
||||
* [Dreaming Sarah [PPSA02929]](https://github.com/par274/sharpemu/issues/9)
|
||||
* Real texture rendering for this game;
|
||||

|
||||
./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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project does **not** support or condone piracy.
|
||||
@@ -94,8 +105,8 @@ Currently the project primarily targets Windows. Cross-platform support (Linux a
|
||||
|
||||
## Build
|
||||
|
||||
1. Install the **.NET SDK**.
|
||||
2. Clone the repository: `git clone https://github.com/par274/sharpemu.git`
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
|
||||
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
||||
4. Build the project: `dotnet build` or `dotnet publish`
|
||||
5. Build artifacts will be located in the `artifacts` directory.
|
||||
@@ -121,7 +132,7 @@ Provided valuable references for filesystem handling and low-level C# implementa
|
||||
|
||||
# License
|
||||
|
||||
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
|
||||
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
|
||||
@@ -11,8 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
||||
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Bink 2 bridge
|
||||
|
||||
Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
|
||||
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||
decoder cannot observe or replace those frames.
|
||||
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||
|
||||
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||
not-found so games that mark cinematics as optional progress to their next
|
||||
state instead of waiting on an empty Bink GPU texture.
|
||||
|
||||
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||
only; it does not decode the movie or alter its game logic. Set
|
||||
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||
|
||||
## Supplying the adapter
|
||||
|
||||
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||
The adapter deliberately contains only a three-function C ABI so the managed
|
||||
emulator never depends on RAD's private binary ABI.
|
||||
|
||||
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||
executable, or point to it explicitly:
|
||||
|
||||
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||
./SharpEmu /path/to/eboot.bin
|
||||
|
||||
The expected exports are sharpemu_bink2_open_utf8,
|
||||
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||
frame. The managed side validates dimensions and retains ownership of the
|
||||
destination buffer.
|
||||
|
||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||
and retains the existing guest rendering path.
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*
|
||||
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "bink.h"
|
||||
|
||||
typedef struct sharpemu_bink2_info {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t frames_per_second_numerator;
|
||||
uint32_t frames_per_second_denominator;
|
||||
} sharpemu_bink2_info;
|
||||
|
||||
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||
HBINK bink;
|
||||
if (!path || !movie || !info) return 0;
|
||||
|
||||
bink = BinkOpen(path, 0);
|
||||
if (!bink) return 0;
|
||||
|
||||
*movie = bink;
|
||||
info->width = bink->Width;
|
||||
info->height = bink->Height;
|
||||
info->frames_per_second_numerator = bink->FrameRate;
|
||||
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||
uint32_t stride, uint32_t destination_bytes) {
|
||||
uint64_t needed;
|
||||
if (!movie || !destination || stride < movie->Width * 4) return 0;
|
||||
needed = (uint64_t)stride * movie->Height;
|
||||
if (needed > destination_bytes) return 0;
|
||||
|
||||
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||
if (BinkWait(movie)) return 0;
|
||||
BinkDoFrame(movie);
|
||||
BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
|
||||
BinkNextFrame(movie);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void sharpemu_bink2_close(HBINK movie) {
|
||||
if (movie) BinkClose(movie);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
|
||||
|
||||
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
|
||||
Known historic mislabels may be allowlisted with a one-line reason.
|
||||
|
||||
Run from the repository root:
|
||||
python scripts/check_sysabi_aerolib.py
|
||||
python scripts/check_sysabi_aerolib.py --strict
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
|
||||
SRC_ROOT = Path("src")
|
||||
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
|
||||
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
|
||||
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
|
||||
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
|
||||
|
||||
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
|
||||
ALLOWLISTED_NIDS: dict[str, str] = {
|
||||
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
|
||||
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
|
||||
}
|
||||
|
||||
|
||||
def name2nid(name: str) -> str:
|
||||
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
|
||||
id_val = struct.unpack("<Q", symbol[:8])[0]
|
||||
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
|
||||
return nid.decode("utf-8")
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
cwd = Path.cwd()
|
||||
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
|
||||
return cwd
|
||||
script_root = Path(__file__).resolve().parent.parent
|
||||
if (script_root / SRC_ROOT).is_dir():
|
||||
return script_root
|
||||
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
|
||||
|
||||
|
||||
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
|
||||
data = aerolib_path.read_bytes()
|
||||
if len(data) < 4:
|
||||
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
|
||||
|
||||
count = struct.unpack_from("<I", data, 0)[0]
|
||||
offset = 4
|
||||
nids: set[str] = set()
|
||||
for _ in range(count):
|
||||
if offset >= len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
|
||||
nid_len = data[offset]
|
||||
offset += 1
|
||||
nid = data[offset : offset + nid_len].decode("utf-8")
|
||||
offset += nid_len
|
||||
if offset + 2 > len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
|
||||
name_len = struct.unpack_from("<H", data, offset)[0]
|
||||
offset += 2 + name_len
|
||||
nids.add(nid)
|
||||
return nids
|
||||
|
||||
|
||||
def iter_sysabi_exports(cs_path: Path, text: str):
|
||||
for match in SYSABI_EXPORT_RE.finditer(text):
|
||||
block = match.group(1)
|
||||
nid_match = NID_RE.search(block)
|
||||
export_match = EXPORT_NAME_RE.search(block)
|
||||
if nid_match is None or export_match is None:
|
||||
continue
|
||||
|
||||
nid = nid_match.group(1)
|
||||
export_name = export_match.group(1)
|
||||
nid_attr = f'Nid = "{nid}"'
|
||||
abs_pos = text.find(nid_attr, match.start(), match.end())
|
||||
if abs_pos < 0:
|
||||
abs_pos = match.start()
|
||||
line = text.count("\n", 0, abs_pos) + 1
|
||||
yield cs_path, line, nid, export_name
|
||||
|
||||
|
||||
def scan(src_root: Path, catalog_nids: set[str]):
|
||||
checked = 0
|
||||
mismatches = []
|
||||
skipped_no_catalog = 0
|
||||
allowlisted = 0
|
||||
|
||||
for cs_path in sorted(src_root.rglob("*.cs")):
|
||||
text = cs_path.read_text(encoding="utf-8")
|
||||
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
|
||||
checked += 1
|
||||
computed = name2nid(export_name)
|
||||
if computed == nid:
|
||||
continue
|
||||
|
||||
if nid not in catalog_nids:
|
||||
skipped_no_catalog += 1
|
||||
continue
|
||||
|
||||
if nid in ALLOWLISTED_NIDS:
|
||||
allowlisted += 1
|
||||
continue
|
||||
|
||||
mismatches.append((path, line, nid, export_name, computed))
|
||||
|
||||
return checked, mismatches, skipped_no_catalog, allowlisted
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Print only the summary line.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
aerolib_path = repo_root / AEROLIB_BIN
|
||||
if not aerolib_path.is_file():
|
||||
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
|
||||
|
||||
catalog_nids = load_aerolib_nids(aerolib_path)
|
||||
checked, mismatches, skipped_no_catalog, allowlisted = scan(
|
||||
repo_root / SRC_ROOT, catalog_nids
|
||||
)
|
||||
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
|
||||
|
||||
if not args.quiet:
|
||||
for path, line, nid, export_name, computed in mismatches:
|
||||
rel = path.relative_to(repo_root).as_posix()
|
||||
print(
|
||||
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
|
||||
f"computed={computed}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"checked={checked} ok={ok} fail={len(mismatches)} "
|
||||
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
|
||||
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
|
||||
)
|
||||
|
||||
if args.strict and mismatches:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
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"
|
||||
@@ -1,54 +0,0 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
|
||||
NAMES = 'scripts/ps5_names.txt'
|
||||
OUTPUT = 'src/SharpEmu.HLE/Aerolib/aerolib.bin'
|
||||
|
||||
def name2nid(name):
|
||||
symbol = hashlib.sha1(name.encode() + uhx('518D64A635DED8C1E6B039B1C3E55230')).digest()
|
||||
id_val = struct.unpack('<Q', symbol[:8])[0]
|
||||
nid = base64enc(uhx('%016x' % id_val), b'+-').rstrip(b'=')
|
||||
return nid.decode('utf-8')
|
||||
|
||||
def generate():
|
||||
names_path = Path(NAMES)
|
||||
output_path = Path(OUTPUT)
|
||||
|
||||
entries = []
|
||||
with open(names_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
name = line.strip()
|
||||
if name:
|
||||
nid = name2nid(name)
|
||||
entries.append((nid, name))
|
||||
|
||||
print(f"Found {len(entries)} entries")
|
||||
|
||||
data = bytearray()
|
||||
data.extend(struct.pack('<I', len(entries)))
|
||||
|
||||
for nid, name in entries:
|
||||
nid_bytes = nid.encode('utf-8')
|
||||
name_bytes = name.encode('utf-8')
|
||||
data.append(len(nid_bytes))
|
||||
data.extend(nid_bytes)
|
||||
data.extend(struct.pack('<H', len(name_bytes)))
|
||||
data.extend(name_bytes)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||
print(f"Total entries: {len(entries)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate()
|
||||
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"
|
||||
@@ -45,6 +45,11 @@ internal static partial class Program
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
// Avoid blocking full collections while guest and render threads are
|
||||
// running, and establish the GC mode before the runtime reserves the
|
||||
// fixed guest address-space window.
|
||||
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
|
||||
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
@@ -58,6 +63,11 @@ internal static partial class Program
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
if (Updater.TryApply(args, out var updateExitCode))
|
||||
{
|
||||
return updateExitCode;
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
@@ -75,6 +85,142 @@ internal static partial class Program
|
||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||
}
|
||||
|
||||
if (!CheckHostArchitecture())
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
ConfigureMoltenVkDefaults();
|
||||
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>
|
||||
/// Applies MoltenVK performance defaults before the Vulkan loader is
|
||||
/// loaded. Existing user-provided values always take precedence.
|
||||
/// </summary>
|
||||
private static void ConfigureMoltenVkDefaults()
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = MacSetEnv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0);
|
||||
_ = MacSetEnv("MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION", "1", 0);
|
||||
_ = MacSetEnv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0);
|
||||
_ = MacSetEnv("MVK_CONFIG_RESUME_LOST_DEVICE", "1", 0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Failed to set MoltenVK defaults: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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");
|
||||
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
@@ -1204,4 +1350,7 @@ internal static partial class Program
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
|
||||
[DllImport("libSystem", EntryPoint = "setenv")]
|
||||
private static extern int MacSetEnv(string name, string value, int overwrite);
|
||||
}
|
||||
|
||||
@@ -16,19 +16,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<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>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>0.0.1</Version>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<!-- Server GC reserves one large heap range per logical processor. On
|
||||
high-core-count hosts those ranges can overlap fixed PS5 image bases
|
||||
before the guest address space is established. Workstation GC avoids
|
||||
that collision and keeps the emulator's required mappings available. -->
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</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'">
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<DebugType>none</DebugType>
|
||||
@@ -63,7 +78,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_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 Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
|
||||
@@ -1,491 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.3, )",
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Iced": "[1.21.0, )",
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"SharpEmu.Libs": "[1.0.0, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,33 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
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 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 TlsPrefixSize = 0x0000_1000UL;
|
||||
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
||||
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
|
||||
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
|
||||
private const ulong ReturnToHostStubBaseAddress = 0x7FFD_C000_0000UL;
|
||||
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
||||
// amd64 variant II). Keep every host in sync with GuestTlsTemplate's
|
||||
// startup reservation; PS5 modules routinely reach beyond one host page.
|
||||
private const ulong TlsPrefixSize = GuestTlsTemplate.StartupStaticTlsReservation;
|
||||
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 ReturnToHostStubStride = 0x0100_0000UL;
|
||||
private const ulong BootstrapPayloadResultOffset = 0x28UL;
|
||||
@@ -42,19 +45,16 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
];
|
||||
private readonly IVirtualMemory _virtualMemory;
|
||||
private readonly IModuleManager _moduleManager;
|
||||
private readonly IHostPlatform? _hostPlatform;
|
||||
private INativeCpuBackend? _nativeCpuBackend;
|
||||
|
||||
public CpuDispatcher(
|
||||
IVirtualMemory virtualMemory,
|
||||
IModuleManager moduleManager,
|
||||
INativeCpuBackend? nativeCpuBackend = null,
|
||||
IHostPlatform? hostPlatform = null)
|
||||
INativeCpuBackend? nativeCpuBackend = null)
|
||||
{
|
||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||
_nativeCpuBackend = nativeCpuBackend;
|
||||
_hostPlatform = hostPlatform;
|
||||
}
|
||||
|
||||
public ulong? LastEntryPoint { get; private set; }
|
||||
@@ -87,8 +87,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -96,7 +96,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -109,8 +110,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -125,7 +126,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -139,7 +141,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -270,7 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
entryFrameDiagnostic,
|
||||
Environment.NewLine,
|
||||
"CpuEngine: native-only");
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||
if (_nativeCpuBackend.TryExecute(
|
||||
context,
|
||||
entryPoint,
|
||||
@@ -311,7 +313,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
@@ -370,11 +372,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
||||
{
|
||||
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
|
||||
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
||||
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
||||
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
|
||||
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
||||
if (!context.TryWriteUInt64(tlsBase - 0xF0, 0) ||
|
||||
!context.TryWriteUInt64(tlsBase + 0x00, tlsBase) ||
|
||||
!context.TryWriteUInt64(tlsBase + 0x10, tlsBase) ||
|
||||
!context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBA00UL) ||
|
||||
!context.TryWriteUInt64(tlsBase + 0x60, tlsBase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seed the static TLS block below the thread pointer with the main
|
||||
// module's initialized thread-locals (variant II layout).
|
||||
SharpEmu.HLE.GuestTlsTemplate.SeedThreadBlock(context, tlsBase);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
||||
@@ -400,35 +410,54 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
ulong programExitHandlerAddress)
|
||||
{
|
||||
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
||||
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
|
||||
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
|
||||
? stackalloc byte[encodedNameLength + 1]
|
||||
: new byte[encodedNameLength + 1];
|
||||
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
|
||||
var arguments = new List<string>(3) { imageName };
|
||||
var configuredArguments = Environment.GetEnvironmentVariable("SHARPEMU_GUEST_ARGS");
|
||||
if (!string.IsNullOrWhiteSpace(configuredArguments))
|
||||
{
|
||||
return false;
|
||||
// The PS5 entry-parameter ABI exposes three inline argv pointers.
|
||||
// Two compatibility arguments are therefore safe without changing
|
||||
// the fixed 0x20-byte structure expected by existing titles.
|
||||
var compatibilityArguments = configuredArguments.Split(
|
||||
(char[]?)null,
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
arguments.AddRange(compatibilityArguments.Take(2));
|
||||
}
|
||||
|
||||
argv0Buffer[encodedNameLength] = 0;
|
||||
var cursor = context[CpuRegister.Rsp];
|
||||
|
||||
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
|
||||
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
|
||||
var argumentAddresses = new ulong[arguments.Count];
|
||||
for (var index = arguments.Count - 1; index >= 0; index--)
|
||||
{
|
||||
return false;
|
||||
var encoded = Encoding.UTF8.GetBytes(arguments[index] + '\0');
|
||||
cursor = AlignDown(cursor - (ulong)encoded.Length, 16);
|
||||
if (!context.Memory.TryWrite(cursor, encoded))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
argumentAddresses[index] = cursor;
|
||||
}
|
||||
|
||||
const ulong entryParamsSize = 0x20;
|
||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||
var entryParamsAddress = AlignDown(cursor - entryParamsSize, 16);
|
||||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, (uint)arguments.Count) ||
|
||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argumentAddresses[0]) ||
|
||||
!context.TryWriteUInt64(
|
||||
entryParamsAddress + 0x10,
|
||||
argumentAddresses.Length > 1 ? argumentAddresses[1] : 0) ||
|
||||
!context.TryWriteUInt64(
|
||||
entryParamsAddress + 0x18,
|
||||
argumentAddresses.Length > 2 ? argumentAddresses[2] : 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arguments.Count > 1)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[DISPATCHER] Guest arguments: {string.Join(' ', arguments.Skip(1))}");
|
||||
}
|
||||
|
||||
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
||||
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
||||
{
|
||||
@@ -461,6 +490,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return value & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return context.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static string BuildEntryFrameDiagnostic(
|
||||
ulong entryPoint,
|
||||
CpuContext context,
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
|
||||
/// bit-manipulation instructions.
|
||||
///
|
||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
|
||||
/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
|
||||
/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
|
||||
/// register-only arithmetic so the exception handler can finish the instruction in software and
|
||||
/// resume, instead of aborting the title.
|
||||
///
|
||||
/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
|
||||
/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
|
||||
/// backend adapter. Each method returns the result already masked to the operand width and, where
|
||||
/// the instruction is defined to touch flags, updates <paramref name="eflags"/> in place. Flags
|
||||
/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
|
||||
/// deterministic across hosts.
|
||||
/// </summary>
|
||||
public static class BmiInstructionEmulator
|
||||
{
|
||||
private const uint FlagCarry = 1u << 0;
|
||||
private const uint FlagZero = 1u << 6;
|
||||
private const uint FlagSign = 1u << 7;
|
||||
private const uint FlagOverflow = 1u << 11;
|
||||
|
||||
private static ulong WidthMask(GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
|
||||
|
||||
private static int WidthBits(GprOperandSize size) => (int)size;
|
||||
|
||||
private static bool SignSet(ulong value, GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
|
||||
|
||||
private static uint WithFlag(uint eflags, uint flag, bool set) =>
|
||||
set ? eflags | flag : eflags & ~flag;
|
||||
|
||||
// Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
|
||||
// the result, and the caller supplies CF because each instruction defines it differently.
|
||||
private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
|
||||
{
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return eflags;
|
||||
}
|
||||
|
||||
/// <summary>ANDN: <c>dest = (~src1) & src2</c>. CF and OF are cleared.</summary>
|
||||
public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var result = (~src1 & src2) & WidthMask(size);
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSI: isolate the lowest set bit, <c>dest = (-src) & src</c>. CF = (src != 0).</summary>
|
||||
public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((0UL - s) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSMSK: mask up to and including the lowest set bit, <c>dest = (src - 1) ^ src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) ^ s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSR: reset the lowest set bit, <c>dest = (src - 1) & src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BEXTR: extract <c>len</c> bits of <paramref name="src"/> starting at bit <c>start</c>, where
|
||||
/// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
|
||||
/// </summary>
|
||||
public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var start = (int)(control & 0xFF);
|
||||
var length = (int)((control >> 8) & 0xFF);
|
||||
|
||||
ulong result;
|
||||
if (start >= bits)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shifted = (src & WidthMask(size)) >> start;
|
||||
if (length == 0)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else if (length >= bits)
|
||||
{
|
||||
result = shifted;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = shifted & ((1UL << length) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
result &= WidthMask(size);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagCarry, false);
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BZHI: zero the bits of <paramref name="src"/> from bit position <c>index[7:0]</c> upward.
|
||||
/// CF is set when the requested position is at or beyond the operand width.
|
||||
/// </summary>
|
||||
public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var n = (int)(index & 0xFF);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (n >= bits)
|
||||
{
|
||||
result = s;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = s & ((1UL << n) - 1);
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.TrailingZeroCount(s)
|
||||
: BitOperations.TrailingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.LeadingZeroCount(s)
|
||||
: BitOperations.LeadingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>RORX: rotate <paramref name="src"/> right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Rorx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var rotate = count & (bits - 1);
|
||||
if (rotate == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
return ((s >> rotate) | (s << (bits - rotate))) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SARX: arithmetic shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Sarx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
if (size == GprOperandSize.Bits64)
|
||||
{
|
||||
return (ulong)((long)src >> shift);
|
||||
}
|
||||
|
||||
return (ulong)(uint)((int)(uint)src >> shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHLX: logical shift left by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shlx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
return (src << shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHRX: logical shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shrx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var shift = count & (bits - 1);
|
||||
return s >> shift;
|
||||
}
|
||||
|
||||
/// <summary>PDEP: deposit contiguous low bits of <paramref name="src"/> into the positions selected by <paramref name="mask"/>. No flags.</summary>
|
||||
public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
var position = 1UL << i;
|
||||
if ((selector & position) != 0)
|
||||
{
|
||||
if (((src >> bit) & 1UL) != 0)
|
||||
{
|
||||
result |= position;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
|
||||
/// <summary>PEXT: gather the bits of <paramref name="src"/> selected by <paramref name="mask"/> into contiguous low bits. No flags.</summary>
|
||||
public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
if ((selector & (1UL << i)) != 0)
|
||||
{
|
||||
if (((src >> i) & 1UL) != 0)
|
||||
{
|
||||
result |= 1UL << bit;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
|
||||
/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
|
||||
/// </summary>
|
||||
public enum GprOperandSize
|
||||
{
|
||||
Bits32 = 32,
|
||||
Bits64 = 64,
|
||||
}
|
||||
@@ -7,8 +7,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
@@ -17,6 +17,55 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||
|
||||
private static readonly bool _perfHleHistogram =
|
||||
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE"), "1", System.StringComparison.Ordinal);
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, long> _perfHleCounts = new();
|
||||
private static long _perfHleTotal;
|
||||
private static long _perfHleDispatchTicks;
|
||||
|
||||
private static void RecordPerfHleDispatchTime(long ticks)
|
||||
{
|
||||
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
|
||||
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
|
||||
if (calls > 0 && calls % 500000 == 0)
|
||||
{
|
||||
var avgUs = (double)total / System.Diagnostics.Stopwatch.Frequency * 1_000_000.0 / calls;
|
||||
System.Console.Error.WriteLine($"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us total_managed_s={(double)total / System.Diagnostics.Stopwatch.Frequency:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly bool _perfHleNoDict =
|
||||
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE_NODICT"), "1", System.StringComparison.Ordinal);
|
||||
|
||||
private static void RecordPerfHleCall(string name)
|
||||
{
|
||||
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
|
||||
if (!_perfHleNoDict)
|
||||
{
|
||||
_perfHleCounts.AddOrUpdate(name, 1, static (_, v) => v + 1);
|
||||
}
|
||||
|
||||
if (total % 500000 == 0 && !_perfHleNoDict)
|
||||
{
|
||||
// Snapshot via foreach (a safe moving enumerator) before sorting.
|
||||
// LINQ over a ConcurrentDictionary uses ICollection.CopyTo, which
|
||||
// throws ArgumentException if another thread adds a key between the
|
||||
// Count read and the copy — that exception was being swallowed into
|
||||
// a CPU_TRAP return and crashing the guest.
|
||||
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, long>>(_perfHleCounts.Count + 16);
|
||||
foreach (var kvp in _perfHleCounts)
|
||||
{
|
||||
snapshot.Add(kvp);
|
||||
}
|
||||
|
||||
var top = snapshot
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.Take(20)
|
||||
.Select(kvp => $"{kvp.Key}={kvp.Value}");
|
||||
System.Console.Error.WriteLine($"[PERF][HLE] total={total} top: {string.Join(", ", top)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordRecentImportTrace(
|
||||
long dispatchIndex,
|
||||
string nid,
|
||||
@@ -25,101 +74,40 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong arg1,
|
||||
ulong arg2)
|
||||
{
|
||||
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||
var trace = _recentImportTrace;
|
||||
trace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||
dispatchIndex,
|
||||
nid,
|
||||
returnRip,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2);
|
||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
||||
arg2,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
Environment.CurrentManagedThreadId);
|
||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % trace.Length;
|
||||
if (_recentImportTraceCount < trace.Length)
|
||||
{
|
||||
_recentImportTraceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordDeferredBootstrapTrace(
|
||||
long dispatchIndex,
|
||||
ulong op,
|
||||
ulong symbolPointer,
|
||||
ulong outputPointer,
|
||||
ulong returnRip)
|
||||
{
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
||||
dispatchIndex,
|
||||
op,
|
||||
symbolPointer,
|
||||
outputPointer,
|
||||
returnRip);
|
||||
_deferredBootstrapTraceWriteIndex =
|
||||
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
||||
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
||||
{
|
||||
_deferredBootstrapTraceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainDeferredBootstrapTraces()
|
||||
{
|
||||
if (!_logBootstrap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeferredBootstrapTraceEntry[] pending;
|
||||
lock (_deferredBootstrapTraceGate)
|
||||
{
|
||||
if (_deferredBootstrapTraceCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
||||
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
||||
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
||||
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
||||
{
|
||||
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
||||
}
|
||||
|
||||
_deferredBootstrapTraceCount = 0;
|
||||
}
|
||||
|
||||
foreach (var entry in pending)
|
||||
{
|
||||
var symbolText = "<unreadable>";
|
||||
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
||||
{
|
||||
symbolText = sym;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
||||
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
||||
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpRecentImportTrace()
|
||||
{
|
||||
if (_recentImportTraceCount == 0)
|
||||
var trace = _recentImportTrace;
|
||||
if (trace is null || _recentImportTraceCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||
Log.Info($" Recent import calls for managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ({_recentImportTraceCount}):");
|
||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + trace.Length) % trace.Length;
|
||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||
{
|
||||
int num2 = (num + i) % _recentImportTrace.Length;
|
||||
var entry = _recentImportTrace[num2];
|
||||
int num2 = (num + i) % trace.Length;
|
||||
var entry = trace[num2];
|
||||
if (!string.IsNullOrEmpty(entry.Nid))
|
||||
{
|
||||
Log.Info(
|
||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
$" #{entry.DispatchIndex} managed={entry.ManagedThreadId} guest=0x{entry.GuestThreadHandle:X16} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||
}
|
||||
}
|
||||
@@ -135,9 +123,8 @@ public sealed partial class DirectExecutionBackend
|
||||
int num2 = 0;
|
||||
List<ulong> list = new List<ulong>(16);
|
||||
ulong num3 = scanStart;
|
||||
var hostMemory = ResolveDiagnosticsHostMemory();
|
||||
HostRegionInfo lpBuffer;
|
||||
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
||||
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
||||
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||
{
|
||||
ulong baseAddress = lpBuffer.BaseAddress;
|
||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||
@@ -147,7 +134,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
ulong value = Math.Max(num3, baseAddress);
|
||||
ulong num5 = Math.Min(num4, scanEnd);
|
||||
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
||||
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
||||
{
|
||||
ulong num6 = AlignUp(value, 8uL);
|
||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||
@@ -186,6 +173,50 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int preludeSize = 192;
|
||||
Span<byte> prelude = stackalloc byte[preludeSize];
|
||||
if (returnRip >= preludeSize && cpuContext.Memory.TryRead(returnRip - preludeSize, prelude))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} pre-return bytes @0x{returnRip - preludeSize:X16}: " +
|
||||
BitConverter.ToString(prelude.ToArray()).Replace("-", " "));
|
||||
|
||||
List<DecodedInst>? bestCallChain = null;
|
||||
var preludeAddress = returnRip - preludeSize;
|
||||
for (var startOffset = 0; startOffset < preludeSize; startOffset++)
|
||||
{
|
||||
var cursor = preludeAddress + (ulong)startOffset;
|
||||
var candidate = new List<DecodedInst>();
|
||||
while (cursor < returnRip && candidate.Count < 96 &&
|
||||
IcedDecoder.TryReadGuestBytes(cpuContext.Memory, cursor, 15, out var instructionBytes) &&
|
||||
IcedDecoder.TryDecode(cursor, instructionBytes, out var instruction) &&
|
||||
instruction.Length > 0 &&
|
||||
cursor + (ulong)instruction.Length <= returnRip)
|
||||
{
|
||||
candidate.Add(instruction);
|
||||
cursor += (ulong)instruction.Length;
|
||||
}
|
||||
|
||||
if (cursor == returnRip &&
|
||||
candidate.Count > 0 &&
|
||||
string.Equals(candidate[^1].Mnemonic, "Call", StringComparison.OrdinalIgnoreCase) &&
|
||||
(bestCallChain is null || candidate.Count > bestCallChain.Count))
|
||||
{
|
||||
bestCallChain = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCallChain is not null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} pre-return disassembly:");
|
||||
foreach (var instruction in bestCallChain.TakeLast(32))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] 0x{instruction.Rip:X16}: {instruction.Text} " +
|
||||
$"bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Span<byte> destination = stackalloc byte[128];
|
||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||
{
|
||||
@@ -237,15 +268,14 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
var importEntries = _importEntries;
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
{
|
||||
if (importEntries[importIndex].Address != target)
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
@@ -272,14 +302,14 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
{
|
||||
if (importEntries[importIndex].Address != slotTarget)
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
@@ -303,6 +333,28 @@ public sealed partial class DirectExecutionBackend
|
||||
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
||||
}
|
||||
|
||||
private static ulong ParseOptionalHexAddress(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var text = value.Trim();
|
||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
text = text[2..];
|
||||
}
|
||||
|
||||
return ulong.TryParse(
|
||||
text,
|
||||
System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var address)
|
||||
? address
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static bool IsPlausibleReturnAddress(ulong address)
|
||||
{
|
||||
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
||||
@@ -352,7 +404,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -361,7 +413,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
||||
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -393,12 +445,12 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||
if (executable)
|
||||
{
|
||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||
@@ -417,14 +469,6 @@ public sealed partial class DirectExecutionBackend
|
||||
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)
|
||||
{
|
||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||
|
||||
@@ -9,9 +9,7 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.Core.Cpu.Native.Windows;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -19,17 +17,25 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||
private static int _lazyCommitTraceCount;
|
||||
private static int _guestAllocatorHoleRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||
|
||||
private unsafe void SetupExceptionHandler()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
SetupPosixExceptionHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||
if (_rawExceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||
}
|
||||
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||
}
|
||||
else
|
||||
@@ -39,22 +45,22 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
_handlerDelegate = VectoredHandler;
|
||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||
if (_exceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||
}
|
||||
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||
|
||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||
if (_unhandledFilterStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||
}
|
||||
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||
}
|
||||
|
||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||
@@ -62,8 +68,8 @@ public sealed partial class DirectExecutionBackend
|
||||
try
|
||||
{
|
||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||
@@ -102,19 +108,28 @@ public sealed partial class DirectExecutionBackend
|
||||
return 0;
|
||||
}
|
||||
|
||||
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
||||
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
return -1;
|
||||
}
|
||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == 3221225477u &&
|
||||
TryRecoverGuestAllocatorHole(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -129,10 +144,10 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
switch (exceptionCode)
|
||||
{
|
||||
case WindowsFaultCodes.AccessViolation:
|
||||
case 3221225477u:
|
||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||
break;
|
||||
case WindowsFaultCodes.FastFail:
|
||||
case 3221226505u:
|
||||
{
|
||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||
@@ -142,27 +157,56 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
||||
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
||||
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
||||
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
||||
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
||||
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
||||
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
||||
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
||||
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
||||
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
||||
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
||||
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
||||
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
||||
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
||||
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
||||
ulong rax = ReadCtxU64(contextRecord, 120);
|
||||
ulong rbx = ReadCtxU64(contextRecord, 144);
|
||||
ulong rcx = ReadCtxU64(contextRecord, 128);
|
||||
ulong rdx = ReadCtxU64(contextRecord, 136);
|
||||
ulong rsi = ReadCtxU64(contextRecord, 168);
|
||||
ulong rdi = ReadCtxU64(contextRecord, 176);
|
||||
ulong rbp = ReadCtxU64(contextRecord, 160);
|
||||
ulong r8 = ReadCtxU64(contextRecord, 184);
|
||||
ulong r9 = ReadCtxU64(contextRecord, 192);
|
||||
ulong r10 = ReadCtxU64(contextRecord, 200);
|
||||
ulong r11 = ReadCtxU64(contextRecord, 208);
|
||||
ulong r12 = ReadCtxU64(contextRecord, 216);
|
||||
ulong r13 = ReadCtxU64(contextRecord, 224);
|
||||
ulong r14 = ReadCtxU64(contextRecord, 232);
|
||||
ulong r15 = ReadCtxU64(contextRecord, 240);
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Host thread: managed={Environment.CurrentManagedThreadId} " +
|
||||
$"name='{Thread.CurrentThread.Name ?? "<unnamed>"}'");
|
||||
if (_activeGuestThreadState is { } activeGuestThread)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Guest thread: handle=0x{activeGuestThread.ThreadHandle:X16} " +
|
||||
$"name='{activeGuestThread.Name}' state={activeGuestThread.State} " +
|
||||
$"last_import={activeGuestThread.LastImportNid ?? "<none>"} " +
|
||||
$"last_ret=0x{activeGuestThread.LastReturnRip:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Last import registers: " +
|
||||
$"rax=0x{Volatile.Read(ref activeGuestThread.LastImportRax):X16} " +
|
||||
$"result_valid={Volatile.Read(ref activeGuestThread.LastImportResultValid) != 0} " +
|
||||
$"rdi=0x{activeGuestThread.LastImportRdi:X16} " +
|
||||
$"rsi=0x{activeGuestThread.LastImportRsi:X16} " +
|
||||
$"rdx=0x{activeGuestThread.LastImportRdx:X16} " +
|
||||
$"rcx=0x{activeGuestThread.LastImportRcx:X16} " +
|
||||
$"r8=0x{activeGuestThread.LastImportR8:X16} " +
|
||||
$"r9=0x{activeGuestThread.LastImportR9:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Last import stack args: " +
|
||||
$"0=0x{activeGuestThread.LastImportStack0:X16} " +
|
||||
$"1=0x{activeGuestThread.LastImportStack1:X16} " +
|
||||
$"2=0x{activeGuestThread.LastImportStack2:X16} " +
|
||||
$"3=0x{activeGuestThread.LastImportStack3:X16} " +
|
||||
$"4=0x{activeGuestThread.LastImportStack4:X16} " +
|
||||
$"5=0x{activeGuestThread.LastImportStack5:X16}");
|
||||
}
|
||||
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
||||
@@ -187,7 +231,7 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
ulong accessType = 0;
|
||||
ulong target = 0;
|
||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
||||
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
||||
{
|
||||
accessType = *exceptionRecord->ExceptionInformation;
|
||||
target = exceptionRecord->ExceptionInformation[1];
|
||||
@@ -200,27 +244,57 @@ public sealed partial class DirectExecutionBackend
|
||||
};
|
||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||
if (_hostMemory.Query(target, out var mbi))
|
||||
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||
{
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||
for (int i = 0; i < 16; i++)
|
||||
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||
if (!TryReadHostQword(stackAddr, out ulong value))
|
||||
{
|
||||
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||
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}");
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_FAULT_STACK_WINDOW"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Full fault stack window (RSP-0x300..RSP+0x100):");
|
||||
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
|
||||
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
|
||||
{
|
||||
if (!TryReadHostQword(stackAddr, out var value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = unchecked((long)(stackAddr - rsp));
|
||||
var relativeText = relative >= 0
|
||||
? $"+0x{relative:X}"
|
||||
: $"-0x{-relative:X}";
|
||||
var symbolText = TryFormatNearestRuntimeSymbol(value, out var stackSymbol)
|
||||
? $" [{stackSymbol}]"
|
||||
: string.Empty;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] [rsp{relativeText}] " +
|
||||
$"@0x{stackAddr:X16} = 0x{value:X16}{symbolText}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||
}
|
||||
|
||||
DumpPointerWindow("fault-register-rbx", rbx, 0x60);
|
||||
DumpPointerWindow("fault-register-rsi", rsi, 0x60);
|
||||
DumpPointerWindow("fault-register-rdi", rdi, 0x60);
|
||||
DumpPointerWindow("fault-register-r13", r13, 0x60);
|
||||
DumpPointerWindow("fault-register-r14", r14, 0x60);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -228,12 +302,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong frame = rbp;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
||||
if (frame < 0x10000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
|
||||
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
|
||||
if (!TryReadHostQword(frame, out ulong next) || !TryReadHostQword(frame + 8, out ulong ret))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
|
||||
break;
|
||||
}
|
||||
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}");
|
||||
if (next <= frame)
|
||||
@@ -250,16 +327,15 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
switch (exceptionCode)
|
||||
{
|
||||
case WindowsFaultCodes.AccessViolation:
|
||||
case 3221225477u:
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||
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 accessed unmapped memory");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
||||
try
|
||||
byte[] code = new byte[16];
|
||||
if (TryReadHostBytes(rip, code))
|
||||
{
|
||||
byte[] code = new byte[16];
|
||||
Marshal.Copy((nint)rip, code, 0, code.Length);
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
||||
if (code[0] == 100)
|
||||
{
|
||||
@@ -275,34 +351,67 @@ public sealed partial class DirectExecutionBackend
|
||||
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})");
|
||||
}
|
||||
if (rip > 16)
|
||||
byte[] before = new byte[16];
|
||||
if (rip > 16 && TryReadHostBytes(rip - 16, before))
|
||||
{
|
||||
byte[] before = new byte[16];
|
||||
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
||||
}
|
||||
if (rip > 32)
|
||||
byte[] window = new byte[64];
|
||||
if (rip > 32 && TryReadHostBytes(rip - 32, window))
|
||||
{
|
||||
byte[] window = new byte[64];
|
||||
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||
}
|
||||
for (var stackIndex = 0; stackIndex < 16; stackIndex++)
|
||||
{
|
||||
byte[] stackSlot = new byte[8];
|
||||
if (!TryReadHostBytes(rsp + (ulong)(stackIndex * 8), stackSlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var candidate = BitConverter.ToUInt64(stackSlot);
|
||||
if (candidate < _entryPoint || candidate >= _entryPoint + 0x10000000 || candidate < 24)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
byte[] callSiteWindow = new byte[48];
|
||||
if (TryReadHostBytes(candidate - 24, callSiteWindow))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
|
||||
BitConverter.ToString(callSiteWindow).Replace("-", " "));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||
}
|
||||
DumpRecentImportTrace();
|
||||
DumpGuestDisasmDiagnostics(rip, rbp);
|
||||
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||
DumpGuestRegisterWindowDiagnostics(
|
||||
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp,
|
||||
r8, r9, r10, r11, r12, r13, r14, r15);
|
||||
DumpGuestReferenceDiagnostics();
|
||||
DumpGuestPointerWindowDiagnostics();
|
||||
break;
|
||||
case WindowsFaultCodes.Breakpoint:
|
||||
case 2147483651u:
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||
break;
|
||||
case WindowsFaultCodes.IllegalInstruction:
|
||||
case 1073741845u:
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Abort (SIGABRT)");
|
||||
DumpRecentImportTrace();
|
||||
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||
break;
|
||||
case 3221225501u:
|
||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||
byte[] illegalCode = new byte[16];
|
||||
if (TryReadHostBytes(rip, illegalCode))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(illegalCode).Replace("-", " "));
|
||||
}
|
||||
DumpRecentImportTrace();
|
||||
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -316,6 +425,108 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverAuxiliaryThreadExecuteFault(
|
||||
EXCEPTION_RECORD* exceptionRecord,
|
||||
void* contextRecord,
|
||||
ulong rip)
|
||||
{
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||
rip >= 0x0000000800000000UL ||
|
||||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostExit = ActiveEntryReturnSentinelRip;
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
hostExit = unchecked((ulong)_guestReturnStub);
|
||||
}
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Could not recover auxiliary TBB execute fault: target=0x{rip:X16} " +
|
||||
$"active_exit=0x{ActiveEntryReturnSentinelRip:X16} guest_return_stub=0x{unchecked((ulong)_guestReturnStub):X16}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, hostExit);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] opcode = new byte[2];
|
||||
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = Interlocked.Increment(ref _ignoredGuestInt41Count);
|
||||
WriteCtxU64(contextRecord, 248, rip + 2);
|
||||
if (count <= 16 || count % 65536 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe static bool TryRecoverGuestAllocatorHole(
|
||||
EXCEPTION_RECORD* exceptionRecord,
|
||||
void* contextRecord,
|
||||
ulong rip)
|
||||
{
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY"),
|
||||
"1",
|
||||
StringComparison.Ordinal) ||
|
||||
exceptionRecord->NumberParameters < 2 ||
|
||||
exceptionRecord->ExceptionInformation[0] != 0 ||
|
||||
exceptionRecord->ExceptionInformation[1] != 8 ||
|
||||
ReadCtxU64(contextRecord, CTX_RDI) != 0 ||
|
||||
rip < 0x10000)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Demon's Souls occasionally leaves an empty payload in a locked pool
|
||||
// tree node. The allocator dereferences payload+8 before reaching its
|
||||
// existing empty-pool fallback. Match the instruction stream instead of
|
||||
// a title-specific absolute address, then resume at that fallback so the
|
||||
// lock is released and the allocator can try its next backing pool.
|
||||
const ulong allocatorHoleSignature = 0x634CFF568D08778BUL;
|
||||
if (*(ulong*)rip != allocatorHoleSignature || *((byte*)rip + 8) != 0xF2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const ulong emptyPoolFallbackDelta = 0x8E;
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + emptyPoolFallbackDelta);
|
||||
var recovery = Interlocked.Increment(ref _guestAllocatorHoleRecoveries);
|
||||
if (recovery <= 16 || (recovery & (recovery - 1)) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Guest allocator empty-node adapter recovery #{recovery}: " +
|
||||
$"rip=0x{rip:X16} -> 0x{rip + emptyPoolFallbackDelta:X16}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsBenignHostDebugException(uint exceptionCode)
|
||||
{
|
||||
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
||||
@@ -334,8 +545,8 @@ public sealed partial class DirectExecutionBackend
|
||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||
void* contextRecord = pointers->ContextRecord;
|
||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||
Console.Error.WriteLine(
|
||||
@@ -395,7 +606,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp)
|
||||
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp, ulong rsp)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
@@ -407,12 +618,20 @@ public sealed partial class DirectExecutionBackend
|
||||
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
||||
}
|
||||
|
||||
// Optimized guest code frequently omits frame pointers. The return
|
||||
// address at RSP is then more useful than an RBP walk and identifies the
|
||||
// exact call site that supplied the faulting arguments.
|
||||
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
|
||||
{
|
||||
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ulong frame = rbp;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
||||
if (frame < 0x10000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -481,7 +700,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong address = scanBase;
|
||||
while (address < scanEnd)
|
||||
{
|
||||
if (!_hostMemory.Query(address, out var mbi))
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -493,9 +712,9 @@ public sealed partial class DirectExecutionBackend
|
||||
break;
|
||||
}
|
||||
|
||||
if (mbi.State == HostRegionState.Committed &&
|
||||
IsReadableProtection(mbi.RawProtection) &&
|
||||
IsExecutableProtection(mbi.RawProtection))
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
IsReadableProtection(mbi.Protect) &&
|
||||
IsExecutableProtection(mbi.Protect))
|
||||
{
|
||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||
}
|
||||
@@ -556,6 +775,55 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpGuestRegisterWindowDiagnostics(
|
||||
ulong rax,
|
||||
ulong rbx,
|
||||
ulong rcx,
|
||||
ulong rdx,
|
||||
ulong rsi,
|
||||
ulong rdi,
|
||||
ulong rbp,
|
||||
ulong rsp,
|
||||
ulong r8,
|
||||
ulong r9,
|
||||
ulong r10,
|
||||
ulong r11,
|
||||
ulong r12,
|
||||
ulong r13,
|
||||
ulong r14,
|
||||
ulong r15)
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_REGISTER_WINDOWS"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A register can be the only surviving reference to the object or
|
||||
// argument array that caused a native guest fault. Capture a compact
|
||||
// window while the process is alive so the post-mortem log can
|
||||
// distinguish an absent object from a partially initialized one.
|
||||
var registers = new (string Name, ulong Value)[]
|
||||
{
|
||||
("rax", rax), ("rbx", rbx), ("rcx", rcx), ("rdx", rdx),
|
||||
("rsi", rsi), ("rdi", rdi), ("rbp", rbp), ("rsp", rsp),
|
||||
("r8", r8), ("r9", r9), ("r10", r10), ("r11", r11),
|
||||
("r12", r12), ("r13", r13), ("r14", r14), ("r15", r15),
|
||||
};
|
||||
var seen = new HashSet<ulong>();
|
||||
foreach (var (name, value) in registers)
|
||||
{
|
||||
if (value < 0x10000 || !seen.Add(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DumpPointerWindow($"register-{name}", value, 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanExecutableRegionForTargetReferences(
|
||||
ulong regionBase,
|
||||
ulong regionEnd,
|
||||
@@ -800,13 +1068,13 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_hostMemory.Query(address, out var mbi))
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
||||
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -823,6 +1091,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 static 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 (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
mbi.State != MEM_COMMIT ||
|
||||
!IsReadableProtection(mbi.Protect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatPointerWithNearestSymbol(ulong value)
|
||||
{
|
||||
string text = $"0x{value:X16}";
|
||||
@@ -918,25 +1241,25 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!_hostMemory.Query(faultAddress, out var mbi))
|
||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||
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.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection: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.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||
}
|
||||
|
||||
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
||||
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||
{
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -945,10 +1268,10 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong committedBase = 0;
|
||||
ulong committedSize = 0;
|
||||
|
||||
if (mbi.State == HostRegionState.Free)
|
||||
if (mbi.State == 65536)
|
||||
{
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = windowBase;
|
||||
@@ -957,7 +1280,7 @@ public sealed partial class DirectExecutionBackend
|
||||
else
|
||||
{
|
||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeBase;
|
||||
@@ -968,13 +1291,13 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = region64kBase;
|
||||
committedSize = 65536uL;
|
||||
}
|
||||
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
@@ -987,7 +1310,7 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
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}");
|
||||
@@ -995,13 +1318,13 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mbi.State != HostRegionState.Reserved)
|
||||
if (mbi.State != 8192)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = commitWindowBase;
|
||||
@@ -1010,7 +1333,7 @@ public sealed partial class DirectExecutionBackend
|
||||
else
|
||||
{
|
||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeCommitBase;
|
||||
@@ -1021,19 +1344,19 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = region64kBase;
|
||||
committedSize = 65536uL;
|
||||
}
|
||||
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
||||
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
committedSize = 8192uL;
|
||||
}
|
||||
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
||||
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = pageBase;
|
||||
@@ -1046,7 +1369,7 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
@@ -1087,33 +1410,31 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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)
|
||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
||||
}
|
||||
|
||||
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
||||
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
||||
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
||||
}
|
||||
|
||||
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||
{
|
||||
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
||||
if (!TryReserveRange(reserveAddress, reserveSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
||||
return TryCommitRange(commitAddress, commitSize, protection);
|
||||
}
|
||||
|
||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
|
||||
//
|
||||
// Guest code runs natively, so the guest and host share the same virtual address space and the
|
||||
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
|
||||
// one of these extensions it raises #UD instead of executing the opcode; without this the title
|
||||
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
|
||||
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
|
||||
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
|
||||
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
|
||||
// does not fully model.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
|
||||
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
|
||||
private const int CTX_EFLAGS = 68;
|
||||
|
||||
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
|
||||
private const uint StatusIllegalInstruction = 0xC000001Du;
|
||||
|
||||
private const int MaxInstructionBytes = 15;
|
||||
|
||||
// Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
|
||||
private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
|
||||
|
||||
private static int _bmiSoftwareFallbackAnnounced;
|
||||
private static long _bmiInstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Op0Kind != OpKind.Register ||
|
||||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, result);
|
||||
if (flagsChanged)
|
||||
{
|
||||
WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _bmiInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryEvaluate(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
GprOperandSize size,
|
||||
out ulong result,
|
||||
out bool flagsChanged,
|
||||
out uint eflags)
|
||||
{
|
||||
result = 0;
|
||||
flagsChanged = false;
|
||||
eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
|
||||
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Andn:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Blsi:
|
||||
case Mnemonic.Blsmsk:
|
||||
case Mnemonic.Blsr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
|
||||
Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
|
||||
_ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
|
||||
};
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bextr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bzhi:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Tzcnt:
|
||||
case Mnemonic.Lzcnt:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Tzcnt
|
||||
? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
|
||||
: BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Rorx:
|
||||
if (instruction.Op2Kind != OpKind.Immediate8 ||
|
||||
!TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
|
||||
return true;
|
||||
|
||||
case Mnemonic.Sarx:
|
||||
case Mnemonic.Shlx:
|
||||
case Mnemonic.Shrx:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
|
||||
Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
|
||||
_ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
|
||||
};
|
||||
return true;
|
||||
|
||||
case Mnemonic.Pdep:
|
||||
case Mnemonic.Pext:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Pdep
|
||||
? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
|
||||
: BmiInstructionEmulator.Pext(packSrc, packMask, size);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
|
||||
{
|
||||
// Try the full instruction window first, then shrink so a fault near the end of a mapped
|
||||
// page (where fewer than 15 bytes are readable) still decodes.
|
||||
foreach (var attempt in DecodeWindowSizes)
|
||||
{
|
||||
var buffer = new byte[attempt];
|
||||
if (!TryReadHostBytes(rip, buffer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
|
||||
decoder.IP = rip;
|
||||
decoder.Decode(out instruction);
|
||||
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
instruction = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe bool TryReadOperand(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
int operandIndex,
|
||||
GprOperandSize size,
|
||||
out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
switch (instruction.GetOpKind(operandIndex))
|
||||
{
|
||||
case OpKind.Register:
|
||||
if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var raw = ReadCtxU64(contextRecord, offset);
|
||||
value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
|
||||
return true;
|
||||
|
||||
case OpKind.Memory:
|
||||
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
|
||||
var buffer = new byte[byteCount];
|
||||
if (!TryReadHostBytes(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = byteCount == 8
|
||||
? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
|
||||
// FS/GS-relative operands need the guest segment base, which is not modelled here.
|
||||
if (instruction.SegmentPrefix != Register.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.IsIPRelativeMemoryOperand)
|
||||
{
|
||||
address = instruction.IPRelativeMemoryAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
var effective = instruction.MemoryDisplacement64;
|
||||
if (instruction.MemoryBase != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, baseOffset);
|
||||
}
|
||||
|
||||
if (instruction.MemoryIndex != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
|
||||
}
|
||||
|
||||
address = effective;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
|
||||
private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
|
||||
default:
|
||||
if (TryGetGpr64Offset(register, out offset))
|
||||
{
|
||||
size = GprOperandSize.Bits64;
|
||||
return true;
|
||||
}
|
||||
|
||||
size = GprOperandSize.Bits32;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetGpr64Offset(Register register, out int offset)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.RAX: offset = CTX_RAX; return true;
|
||||
case Register.RCX: offset = CTX_RCX; return true;
|
||||
case Register.RDX: offset = CTX_RDX; return true;
|
||||
case Register.RBX: offset = CTX_RBX; return true;
|
||||
case Register.RSP: offset = CTX_RSP; return true;
|
||||
case Register.RBP: offset = CTX_RBP; return true;
|
||||
case Register.RSI: offset = CTX_RSI; return true;
|
||||
case Register.RDI: offset = CTX_RDI; return true;
|
||||
case Register.R8: offset = CTX_R8; return true;
|
||||
case Register.R9: offset = CTX_R9; return true;
|
||||
case Register.R10: offset = CTX_R10; return true;
|
||||
case Register.R11: offset = CTX_R11; return true;
|
||||
case Register.R12: offset = CTX_R12; return true;
|
||||
case Register.R13: offset = CTX_R13; return true;
|
||||
case Register.R14: offset = CTX_R14; return true;
|
||||
case Register.R15: offset = CTX_R15; return true;
|
||||
default: offset = 0; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -36,6 +35,20 @@ public sealed partial class DirectExecutionBackend
|
||||
private bool _nativeWorkersDisposed;
|
||||
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
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
@@ -48,7 +61,7 @@ public sealed partial class DirectExecutionBackend
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
try
|
||||
@@ -79,7 +92,10 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
if (NativeGuestWorkersDisabled)
|
||||
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||
// kernel32!CreateThread. POSIX hosts use the established inline entry
|
||||
// path until the worker loop has a pthread/eventfd implementation.
|
||||
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -172,8 +188,20 @@ public sealed partial class DirectExecutionBackend
|
||||
private static nint _exitThreadAddress;
|
||||
|
||||
private readonly DirectExecutionBackend _backend;
|
||||
private readonly AutoResetEvent _workAvailable = new(false);
|
||||
private readonly AutoResetEvent _workCompleted = new(false);
|
||||
// Windows uses AutoResetEvent (its SafeWaitHandle is a real kernel
|
||||
// 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 void* _controlBlock;
|
||||
private void* _loopStub;
|
||||
@@ -212,11 +240,16 @@ public sealed partial class DirectExecutionBackend
|
||||
private NativeGuestExecutor(DirectExecutionBackend backend)
|
||||
{
|
||||
_backend = backend;
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_workAvailable = new AutoResetEvent(false);
|
||||
_workCompleted = new AutoResetEvent(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||
{
|
||||
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
||||
if (!EnsureKernel32Exports())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -229,27 +262,32 @@ public sealed partial class DirectExecutionBackend
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
||||
private static bool EnsureKernel32Exports()
|
||||
{
|
||||
if (_exitThreadAddress != 0)
|
||||
{
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||
}
|
||||
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
||||
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
||||
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
if (kernel32 == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||
}
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||
if (_controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||
if (_loopStub == null)
|
||||
{
|
||||
return false;
|
||||
@@ -258,8 +296,34 @@ public sealed partial class DirectExecutionBackend
|
||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
||||
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
||||
nint workHandle;
|
||||
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;
|
||||
int offset = 0;
|
||||
@@ -331,15 +395,17 @@ public sealed partial class DirectExecutionBackend
|
||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||
|
||||
uint oldProtect = 0;
|
||||
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
||||
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
||||
(nint)_loopStub,
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
(nint)_loopStub,
|
||||
0,
|
||||
StackSizeParamIsAReservation,
|
||||
out _nativeThreadId);
|
||||
if (_threadHandle == 0)
|
||||
{
|
||||
@@ -377,8 +443,8 @@ public sealed partial class DirectExecutionBackend
|
||||
_runYieldRequested = false;
|
||||
_runYieldReason = null;
|
||||
_runForcedExit = false;
|
||||
_workAvailable.Set();
|
||||
_workCompleted.WaitOne();
|
||||
SignalWorkAvailable();
|
||||
WaitWorkCompleted();
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
@@ -391,6 +457,28 @@ public sealed partial class DirectExecutionBackend
|
||||
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]
|
||||
private static nint RunPrologue(nint executorHandle)
|
||||
{
|
||||
@@ -445,7 +533,7 @@ public sealed partial class DirectExecutionBackend
|
||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||
_prevState = _activeGuestThreadState;
|
||||
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||
_entered = true;
|
||||
_activeExecutionBackend = backend;
|
||||
@@ -457,11 +545,11 @@ public sealed partial class DirectExecutionBackend
|
||||
_activeGuestThreadYieldReason = null;
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
}
|
||||
if (_runAffinityMask != 0)
|
||||
{
|
||||
@@ -491,7 +579,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
@@ -520,7 +608,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
try
|
||||
{
|
||||
_workAvailable.Set();
|
||||
SignalWorkAvailable();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -528,8 +616,8 @@ public sealed partial class DirectExecutionBackend
|
||||
var exited = _threadHandle == 0;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
||||
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
}
|
||||
if (!exited)
|
||||
@@ -543,20 +631,30 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
if (_loopStub != null)
|
||||
{
|
||||
_backend._hostMemory.Free((ulong)_loopStub);
|
||||
VirtualFree(_loopStub, 0u, 32768u);
|
||||
_loopStub = null;
|
||||
}
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
_backend._hostMemory.Free((ulong)_controlBlock);
|
||||
VirtualFree(_controlBlock, 0u, 32768u);
|
||||
_controlBlock = null;
|
||||
}
|
||||
if (_selfHandle.IsAllocated)
|
||||
{
|
||||
_selfHandle.Free();
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
_workCompleted.Dispose();
|
||||
_workAvailable?.Dispose();
|
||||
_workCompleted?.Dispose();
|
||||
if (_workSemaphore != 0)
|
||||
{
|
||||
PosixHostStubs.DestroyWorkerEvent(_workSemaphore);
|
||||
_workSemaphore = 0;
|
||||
}
|
||||
if (_doneSemaphore != 0)
|
||||
{
|
||||
PosixHostStubs.DestroyWorkerEvent(_doneSemaphore);
|
||||
_doneSemaphore = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
// 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.HLE;
|
||||
|
||||
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 PosixSigTrap = 5;
|
||||
private const int PosixSigAbort = 6;
|
||||
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;
|
||||
private static long _perfSignalCount;
|
||||
private static readonly bool _perfSignalCounter =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PERF_MEM"), "1", StringComparison.Ordinal);
|
||||
|
||||
[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();
|
||||
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
|
||||
|
||||
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||
!InstallPosixSignalHandler(PosixSigIll) ||
|
||||
!InstallPosixSignalHandler(PosixSigTrap) ||
|
||||
!InstallPosixSignalHandler(PosixSigAbort))
|
||||
{
|
||||
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[Win64ContextSize];
|
||||
new Span<byte>(contextRecord, Win64ContextSize).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++;
|
||||
if (_perfSignalCounter)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _perfSignalCount);
|
||||
if (n % 100000 == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[PERF][MEM] posix_faults={n}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
// Guest-image write tracking runs first: it only needs the fault
|
||||
// address (safe for host and guest threads alike) and must resume
|
||||
// the faulting write immediately after restoring write access.
|
||||
if (signal != PosixSigIll &&
|
||||
siginfo != 0 &&
|
||||
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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[Win64ContextSize];
|
||||
new Span<byte>(contextRecord, Win64ContextSize).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 if (signal == PosixSigTrap)
|
||||
{
|
||||
record.ExceptionCode = 2147483651u;
|
||||
}
|
||||
else if (signal == PosixSigAbort)
|
||||
{
|
||||
record.ExceptionCode = 1073741845u;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
/// <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;
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
||||
while (sem_trywait(handle) != 0)
|
||||
{
|
||||
if (Environment.TickCount64 >= deadlineTicks)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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>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 page = (byte*)HostMemory.Alloc(
|
||||
null,
|
||||
4096,
|
||||
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||
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 (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
||||
}
|
||||
|
||||
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
||||
return (nint)page;
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildStubs();
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildStubs()
|
||||
{
|
||||
var page = (byte*)HostMemory.Alloc(
|
||||
null,
|
||||
4096,
|
||||
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||
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);
|
||||
|
||||
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
||||
}
|
||||
|
||||
HostMemory.FlushInstructionCache(page, (nuint)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 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 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);
|
||||
|
||||
[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);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -12,15 +11,17 @@ public sealed unsafe class StubManager : IDisposable
|
||||
private readonly List<nint> _allocatedStubs = new();
|
||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||
private readonly IHostMemory _hostMemory;
|
||||
private byte* _pltMemory;
|
||||
private int _pltOffset;
|
||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||
|
||||
public StubManager(IHostMemory? hostMemory = null)
|
||||
public StubManager()
|
||||
{
|
||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
||||
_pltMemory = (byte*)VirtualAlloc(
|
||||
null,
|
||||
(nuint)PltMemorySize,
|
||||
AllocationType.Reserve | AllocationType.Commit,
|
||||
MemoryProtection.ExecuteReadWrite);
|
||||
|
||||
if (_pltMemory == null)
|
||||
{
|
||||
@@ -184,7 +185,7 @@ public sealed unsafe class StubManager : IDisposable
|
||||
{
|
||||
if (_pltMemory != null)
|
||||
{
|
||||
_hostMemory.Free((ulong)_pltMemory);
|
||||
VirtualFree(_pltMemory, 0, FreeType.Release);
|
||||
_pltMemory = null;
|
||||
}
|
||||
|
||||
@@ -193,5 +194,29 @@ public sealed unsafe class StubManager : IDisposable
|
||||
_stubAddresses.Clear();
|
||||
}
|
||||
|
||||
private static void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect) =>
|
||||
HostMemory.Alloc(lpAddress, dwSize, (uint)flAllocationType, (uint)flProtect);
|
||||
|
||||
private static bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType) =>
|
||||
HostMemory.Free(lpAddress, dwSize, (uint)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);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||
/// </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;
|
||||
|
||||
@@ -48,6 +48,8 @@ public enum ProgramHeaderType : uint
|
||||
|
||||
Tls = 7,
|
||||
|
||||
GnuEhFrame = 0x6474E550,
|
||||
|
||||
SceRela = 0x60000000,
|
||||
|
||||
SceProcParam = 0x61000001,
|
||||
|
||||
@@ -24,7 +24,10 @@ public sealed class SelfImage
|
||||
ulong procParamAddress = 0,
|
||||
string? title = null,
|
||||
string? titleId = null,
|
||||
string? version = null)
|
||||
string? version = null,
|
||||
uint tlsModuleId = 0,
|
||||
ulong tlsMemorySize = 0,
|
||||
ulong tlsStaticOffset = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||
@@ -44,6 +47,9 @@ public sealed class SelfImage
|
||||
Title = title;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
TlsModuleId = tlsModuleId;
|
||||
TlsMemorySize = tlsMemorySize;
|
||||
TlsStaticOffset = tlsStaticOffset;
|
||||
}
|
||||
|
||||
public bool IsSelf { get; }
|
||||
@@ -75,4 +81,11 @@ public sealed class SelfImage
|
||||
public string? TitleId { get; }
|
||||
|
||||
public string? Version { get; }
|
||||
|
||||
public uint TlsModuleId { get; }
|
||||
|
||||
public ulong TlsMemorySize { get; }
|
||||
|
||||
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
|
||||
public ulong TlsStaticOffset { get; }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -10,16 +11,12 @@ using SharpEmu.Core;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||
private const uint ElfMagic = 0x7F454C46;
|
||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||
@@ -58,11 +55,22 @@ public sealed class SelfLoader : ISelfLoader
|
||||
private const long DtSceSymTab = 0x61000039;
|
||||
private const long DtSceSymTabSize = 0x6100003F;
|
||||
|
||||
private const uint RelocationTypeNone = 0;
|
||||
private const uint RelocationTypeAbsolute64 = 1;
|
||||
private const uint RelocationTypePc32 = 2;
|
||||
private const uint RelocationTypePlt32 = 4;
|
||||
private const uint RelocationTypeGlobalData = 6;
|
||||
private const uint RelocationTypeJumpSlot = 7;
|
||||
private const uint RelocationTypeRelative = 8;
|
||||
private const uint RelocationTypeTlsModuleId = 16;
|
||||
private const uint RelocationTypeUnsigned32 = 10;
|
||||
private const uint RelocationTypeSigned32 = 11;
|
||||
private const uint RelocationTypeTlsModuleId = 16; // R_X86_64_DTPMOD64
|
||||
private const uint RelocationTypeTlsDtpOff64 = 17; // R_X86_64_DTPOFF64
|
||||
private const uint RelocationTypeTlsTpOff64 = 18; // R_X86_64_TPOFF64
|
||||
private const uint RelocationTypePc64 = 24;
|
||||
private const uint RelocationTypeSize32 = 32;
|
||||
private const uint RelocationTypeSize64 = 33;
|
||||
private const uint RelocationTypeRelative64 = 38;
|
||||
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
||||
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
||||
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
|
||||
@@ -88,6 +96,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
|
||||
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
|
||||
|
||||
static SelfLoader()
|
||||
{
|
||||
RunRelocationSelfChecks();
|
||||
}
|
||||
|
||||
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
|
||||
{
|
||||
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
|
||||
@@ -156,16 +169,22 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
virtualMemory.Clear();
|
||||
_nextTlsModuleId = 1;
|
||||
GuestTlsTemplate.Reset();
|
||||
}
|
||||
|
||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
||||
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||
|
||||
var loadContext = ParseLayout(imageData);
|
||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||
ValidateElfHeader(elfHeader);
|
||||
|
||||
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
|
||||
var hasTlsSegment = TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var processTlsHeader, out _) &&
|
||||
processTlsHeader.MemorySize != 0;
|
||||
var tlsModuleId = hasTlsSegment
|
||||
? (_nextTlsModuleId == 0 ? 1u : _nextTlsModuleId)
|
||||
: 0u;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} " +
|
||||
$"assigned={tlsModuleId} has_pt_tls={hasTlsSegment}");
|
||||
|
||||
var totalImageSize = CalculateTotalImageSize(programHeaders);
|
||||
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
|
||||
@@ -198,6 +217,13 @@ public sealed class SelfLoader : ISelfLoader
|
||||
}
|
||||
|
||||
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
|
||||
// Register every module before relocations so DTPMOD/DTPOFF/TPOFF use
|
||||
// the module's real PT_TLS identity and Variant II static offset.
|
||||
var tlsInfo = RegisterModuleTlsTemplate(
|
||||
programHeaders,
|
||||
virtualMemory,
|
||||
imageBase,
|
||||
tlsModuleId);
|
||||
var importStubs = ResolveAndPatchImportStubs(
|
||||
imageData,
|
||||
loadContext,
|
||||
@@ -255,11 +281,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
|
||||
}
|
||||
|
||||
if (_nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
||||
if (tlsModuleId != 0 && _nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
||||
{
|
||||
_nextTlsModuleId++;
|
||||
}
|
||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
|
||||
return new SelfImage(
|
||||
loadContext.IsSelf,
|
||||
@@ -276,7 +302,10 @@ public sealed class SelfLoader : ISelfLoader
|
||||
procParamAddress,
|
||||
applicationInfo.Title,
|
||||
applicationInfo.TitleId,
|
||||
applicationInfo.Version);
|
||||
applicationInfo.Version,
|
||||
tlsModuleId,
|
||||
tlsInfo.MemorySize,
|
||||
tlsInfo.StaticOffset);
|
||||
}
|
||||
|
||||
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
||||
@@ -325,8 +354,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||
}
|
||||
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
||||
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||
{
|
||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||
@@ -348,10 +376,19 @@ public sealed class SelfLoader : ISelfLoader
|
||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
||||
}
|
||||
|
||||
if (magic != ElfMagic)
|
||||
// Not a recognized (fake-signed) SELF. Only a bare, decrypted ELF is
|
||||
// acceptable here; anything else — most commonly a still-encrypted
|
||||
// retail eboot — must be reported clearly rather than failing later
|
||||
// with an opaque "not a valid ELF header" message.
|
||||
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
|
||||
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (leadingWord != ElfMagicBigEndian)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported executable signature 0x{magic:X8}");
|
||||
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
|
||||
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
|
||||
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
|
||||
$"fake-signed (fSELF) image.");
|
||||
}
|
||||
|
||||
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
||||
@@ -414,15 +451,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var virtualAddress = header.VirtualAddress + imageBase;
|
||||
|
||||
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
if (header.Alignment > 1)
|
||||
{
|
||||
var vaddrMod = virtualAddress % header.Alignment;
|
||||
var offsetMod = header.Offset % header.Alignment;
|
||||
if (vaddrMod != offsetMod)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Segment {index} ELF alignment mismatch! " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||
}
|
||||
@@ -465,6 +502,43 @@ public sealed class SelfLoader : ISelfLoader
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ModuleTlsInfo RegisterModuleTlsTemplate(
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
IVirtualMemory virtualMemory,
|
||||
ulong imageBase,
|
||||
uint tlsModuleId)
|
||||
{
|
||||
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var tlsHeader, out _) ||
|
||||
tlsHeader.MemorySize == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// tdata (initialized) bytes come from the mapped segment; tbss is the
|
||||
// implicitly-zero remainder up to MemorySize.
|
||||
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
|
||||
var initImage = fileSize > 0 ? new byte[fileSize] : [];
|
||||
if (fileSize > 0 &&
|
||||
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
|
||||
initImage = [];
|
||||
}
|
||||
|
||||
var staticOffset = GuestTlsTemplate.RegisterModule(
|
||||
tlsModuleId,
|
||||
initImage,
|
||||
tlsHeader.MemorySize,
|
||||
tlsHeader.Alignment,
|
||||
tlsHeader.VirtualAddress);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Module {tlsModuleId} TLS template: memsz=0x{tlsHeader.MemorySize:X} " +
|
||||
$"filesz=0x{tlsHeader.FileSize:X} align=0x{tlsHeader.Alignment:X} " +
|
||||
$"static_offset=0x{staticOffset:X} total_static=0x{GuestTlsTemplate.StaticTlsSize:X}");
|
||||
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
||||
ReadOnlySpan<byte> imageData,
|
||||
LoadContext loadContext,
|
||||
@@ -544,12 +618,17 @@ public sealed class SelfLoader : ISelfLoader
|
||||
uint maxSymbolIndex = 0;
|
||||
foreach (var relocation in relocations)
|
||||
{
|
||||
if (relocation.Type == RelocationTypeNone)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsSupportedRelocationType(relocation.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relocation.Type is RelocationTypeRelative or RelocationTypeTlsModuleId)
|
||||
if (relocation.Type is RelocationTypeNone or RelocationTypeRelative or RelocationTypeRelative64 or RelocationTypeTlsModuleId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -633,13 +712,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
importedRelocations = BuildImportedRelocations(descriptors);
|
||||
|
||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids);
|
||||
var stubImportNids = orderedImportNids
|
||||
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||
.ToArray();
|
||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||
|
||||
int printCount = Math.Min(10, orderedImportNids.Count);
|
||||
int printCount = Math.Min(10, stubImportNids.Length);
|
||||
for (int i = 0; i < printCount; i++)
|
||||
{
|
||||
var nid = orderedImportNids[i];
|
||||
var nid = stubImportNids[i];
|
||||
var addr = stubsByAddress.First(x => x.Value == nid).Key;
|
||||
}
|
||||
|
||||
@@ -663,45 +745,65 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
ulong targetValue;
|
||||
ulong symbolValue;
|
||||
if (descriptor.ImportNid is null)
|
||||
{
|
||||
targetValue = AddSigned(descriptor.SymbolValue, descriptor.Addend);
|
||||
symbolValue = descriptor.SymbolValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
||||
if (addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
||||
{
|
||||
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
||||
symbolValue = stubAddress;
|
||||
}
|
||||
|
||||
targetValue = AddSigned(stubAddress, descriptor.Addend);
|
||||
}
|
||||
|
||||
if (targetValue < 0x1000)
|
||||
{
|
||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||
else if (descriptor.IsWeak)
|
||||
{
|
||||
Log.Debug(
|
||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
// ELF unresolved weak definitions use S=0. They must not
|
||||
// receive a trap import stub, which would turn a permitted
|
||||
// null test into a call to an unresolved-import handler.
|
||||
symbolValue = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryWriteUInt64(virtualMemory, descriptor.TargetAddress, targetValue))
|
||||
var targetValue = ComputeRelocationValue(descriptor, symbolValue);
|
||||
|
||||
if (targetValue < 0x1000 && descriptor.ValueKind is
|
||||
RelocationValueKind.TlsOffset or
|
||||
RelocationValueKind.PcRelative or
|
||||
RelocationValueKind.SymbolSize)
|
||||
{
|
||||
throw new InvalidDataException($"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}.");
|
||||
// A TLS offset (TPOFF64/DTPOFF64) is a signed displacement, not a
|
||||
// mapped address, so a small or negative value here is expected.
|
||||
}
|
||||
else if (targetValue < 0x1000 && !descriptor.IsWeak)
|
||||
{
|
||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryWriteRelocationValue(virtualMemory, descriptor, targetValue, out var writeError))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}: {writeError}");
|
||||
}
|
||||
|
||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||
{
|
||||
Log.Debug(
|
||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,29 +896,36 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Log.Debug(
|
||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
}
|
||||
|
||||
if (!IsSupportedRelocationType(relocation.Type))
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
// Surface unsupported relocation types loudly instead of
|
||||
// skipping silently: TLS-relative (17/18), COPY (5), and
|
||||
// IRELATIVE/ifunc (37) leave their targets unrelocated, which
|
||||
// manifests later as reads of zero or calls into address 0.
|
||||
ReportUnsupportedRelocation(relocation.Type, relocation.Offset, imageBase);
|
||||
if (relocation.Type is 5 or 37)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
throw new NotSupportedException(
|
||||
$"Relocation type {relocation.Type} requires deferred runtime-linker processing.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress))
|
||||
var relocationWriteSize = GetRelocationWriteSize(relocation.Type);
|
||||
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, relocationWriteSize, out var targetAddress))
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relocation.Type == RelocationTypeRelative)
|
||||
if (relocation.Type is RelocationTypeRelative or RelocationTypeRelative64)
|
||||
{
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
targetAddress,
|
||||
@@ -830,7 +939,12 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (relocation.Type == RelocationTypeTlsModuleId)
|
||||
{
|
||||
var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId;
|
||||
if (tlsModuleId == 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"R_X86_64_DTPMOD64 at 0x{targetAddress:X16} references a module without PT_TLS.");
|
||||
}
|
||||
var dtpmodValue = tlsModuleId;
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
targetAddress,
|
||||
0,
|
||||
@@ -851,50 +965,100 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend;
|
||||
var symbolBind = GetSymbolBind(symbol.Info);
|
||||
if (symbolBind == SymbolBindLocal)
|
||||
if (relocation.Type is RelocationTypeTlsDtpOff64 or RelocationTypeTlsTpOff64)
|
||||
{
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
// Variant II static TLS: the module block sits at
|
||||
// [tp - blockSize, tp). DTPOFF64 is the module-relative offset
|
||||
// (st_value + addend); TPOFF64 is that offset expressed relative
|
||||
// to the thread pointer, i.e. minus the aligned block size.
|
||||
var tlsSymbolOffset = AddSigned(symbol.Value, relocation.Addend);
|
||||
if (!GuestTlsTemplate.TryGetStaticOffset(tlsModuleId, out var moduleStaticOffset))
|
||||
{
|
||||
Log.Warning(
|
||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Missing PT_TLS registration for module {tlsModuleId}; " +
|
||||
$"cannot apply relocation {relocation.Type} at 0x{targetAddress:X16}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var tlsValue = relocation.Type == RelocationTypeTlsTpOff64
|
||||
? unchecked(tlsSymbolOffset - moduleStaticOffset)
|
||||
: tlsSymbolOffset;
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
targetAddress,
|
||||
addend,
|
||||
0,
|
||||
null,
|
||||
symbolAddress,
|
||||
RelocationValueKind.Pointer,
|
||||
tlsValue,
|
||||
RelocationValueKind.TlsOffset,
|
||||
IsDataImport: false));
|
||||
continue;
|
||||
}
|
||||
|
||||
var symbolBind = GetSymbolBind(symbol.Info);
|
||||
if (symbolIndex == 0)
|
||||
{
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
symbol,
|
||||
symbolAddress: 0,
|
||||
importNid: null,
|
||||
isWeak: false));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbolBind == SymbolBindLocal)
|
||||
{
|
||||
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||
? 0
|
||||
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
symbol,
|
||||
symbolAddress,
|
||||
importNid: null,
|
||||
isWeak: false));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Value != 0)
|
||||
{
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||
? 0
|
||||
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
addend,
|
||||
null,
|
||||
symbol,
|
||||
symbolAddress,
|
||||
RelocationValueKind.Pointer,
|
||||
IsDataImport: false));
|
||||
importNid: null,
|
||||
isWeak: false));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -902,16 +1066,26 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
|
||||
{
|
||||
if (symbolBind == SymbolBindWeak)
|
||||
{
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
symbol,
|
||||
symbolAddress: 0,
|
||||
importNid: null,
|
||||
isWeak: true));
|
||||
}
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -919,6 +1093,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var nid = ExtractNid(symbolName);
|
||||
if (string.IsNullOrWhiteSpace(nid))
|
||||
{
|
||||
if (symbolBind == SymbolBindWeak)
|
||||
{
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
symbol,
|
||||
symbolAddress: 0,
|
||||
importNid: null,
|
||||
isWeak: true));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -927,16 +1111,74 @@ public sealed class SelfLoader : ISelfLoader
|
||||
orderedImportNids.Add(nid);
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||
relocation,
|
||||
targetAddress,
|
||||
addend,
|
||||
nid,
|
||||
0,
|
||||
RelocationValueKind.Pointer,
|
||||
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject));
|
||||
symbol,
|
||||
symbolAddress: 0,
|
||||
importNid: nid,
|
||||
isWeak: symbolBind == SymbolBindWeak));
|
||||
}
|
||||
}
|
||||
|
||||
private static RelocationDescriptor CreateSymbolRelocationDescriptor(
|
||||
ElfRelocation relocation,
|
||||
ulong targetAddress,
|
||||
ElfSymbol symbol,
|
||||
ulong symbolAddress,
|
||||
string? importNid,
|
||||
bool isWeak)
|
||||
{
|
||||
var valueKind = relocation.Type switch
|
||||
{
|
||||
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypePc64 => RelocationValueKind.PcRelative,
|
||||
RelocationTypeSize32 or RelocationTypeSize64 => RelocationValueKind.SymbolSize,
|
||||
_ => RelocationValueKind.Pointer,
|
||||
};
|
||||
var writeKind = relocation.Type switch
|
||||
{
|
||||
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypeSigned32 => RelocationWriteKind.Int32,
|
||||
RelocationTypeUnsigned32 or RelocationTypeSize32 => RelocationWriteKind.UInt32,
|
||||
_ => RelocationWriteKind.UInt64,
|
||||
};
|
||||
var symbolValue = valueKind == RelocationValueKind.SymbolSize
|
||||
? symbol.Size
|
||||
: symbolAddress;
|
||||
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot
|
||||
? 0
|
||||
: relocation.Addend;
|
||||
return new RelocationDescriptor(
|
||||
targetAddress,
|
||||
addend,
|
||||
importNid,
|
||||
symbolValue,
|
||||
valueKind,
|
||||
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject,
|
||||
writeKind,
|
||||
isWeak);
|
||||
}
|
||||
|
||||
private static bool ShouldCreateImportStub(
|
||||
string nid,
|
||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||
IModuleManager? moduleManager)
|
||||
{
|
||||
for (var i = 0; i < descriptors.Count; i++)
|
||||
{
|
||||
var descriptor = descriptors[i];
|
||||
if (!string.Equals(descriptor.ImportNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void RegisterRuntimeSymbolsAndHooks(
|
||||
ReadOnlySpan<byte> imageData,
|
||||
LoadContext loadContext,
|
||||
@@ -959,8 +1201,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,8 +1276,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1418,8 +1660,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||
Log.Debug(
|
||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
if (mapSize > int.MaxValue)
|
||||
{
|
||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||
@@ -1691,11 +1933,51 @@ public sealed class SelfLoader : ISelfLoader
|
||||
private static bool IsSupportedRelocationType(uint relocationType)
|
||||
{
|
||||
return relocationType is
|
||||
RelocationTypeNone or
|
||||
RelocationTypeAbsolute64 or
|
||||
RelocationTypePc32 or
|
||||
RelocationTypePlt32 or
|
||||
RelocationTypeGlobalData or
|
||||
RelocationTypeJumpSlot or
|
||||
RelocationTypeRelative or
|
||||
RelocationTypeTlsModuleId;
|
||||
RelocationTypeUnsigned32 or
|
||||
RelocationTypeSigned32 or
|
||||
RelocationTypeTlsModuleId or
|
||||
RelocationTypeTlsDtpOff64 or
|
||||
RelocationTypeTlsTpOff64 or
|
||||
RelocationTypePc64 or
|
||||
RelocationTypeSize32 or
|
||||
RelocationTypeSize64 or
|
||||
RelocationTypeRelative64;
|
||||
}
|
||||
|
||||
private static readonly HashSet<uint> _reportedUnsupportedRelocationTypes = new();
|
||||
|
||||
private static void ReportUnsupportedRelocation(uint relocationType, ulong offset, ulong imageBase)
|
||||
{
|
||||
// Report each distinct unsupported type once to keep the log useful.
|
||||
lock (_reportedUnsupportedRelocationTypes)
|
||||
{
|
||||
if (!_reportedUnsupportedRelocationTypes.Add(relocationType))
|
||||
{
|
||||
if (IsFocusRelocationOffset(offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocationType}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var name = relocationType switch
|
||||
{
|
||||
5 => "R_X86_64_COPY",
|
||||
37 => "R_X86_64_IRELATIVE (ifunc)",
|
||||
_ => "unknown",
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Unsupported relocation type {relocationType} ({name}) rejected " +
|
||||
$"(first at off=0x{offset:X16}); COPY requires dependency symbol storage and IRELATIVE requires resolver execution.");
|
||||
}
|
||||
|
||||
private static ulong DetermineRequestedImageBase(
|
||||
@@ -1851,16 +2133,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||
|
||||
var guestAddr = location + imageBase;
|
||||
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||
{
|
||||
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (virtualMemory.TryRead(location, tableBytes))
|
||||
{
|
||||
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1868,11 +2150,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
var slice = elfData.Slice((int)location, (int)size);
|
||||
tableBytes = slice.ToArray();
|
||||
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
tableBytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
@@ -1929,17 +2211,17 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||
{
|
||||
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
|
||||
if (address == 0)
|
||||
{
|
||||
Log.Debug("-> return 0 (null)");
|
||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||
{
|
||||
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -1948,18 +2230,18 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var rebased = address + imageBase;
|
||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||
{
|
||||
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
||||
return resolvedRebased;
|
||||
}
|
||||
}
|
||||
|
||||
if (address < 0x10000)
|
||||
{
|
||||
Log.Debug($"-> reject small 0x{address:X}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||
return address;
|
||||
}
|
||||
|
||||
@@ -2049,6 +2331,106 @@ public sealed class SelfLoader : ISelfLoader
|
||||
return virtualMemory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static ulong ComputeRelocationValue(RelocationDescriptor descriptor, ulong resolvedSymbolValue)
|
||||
{
|
||||
var baseValue = descriptor.ValueKind == RelocationValueKind.SymbolSize
|
||||
? descriptor.SymbolValue
|
||||
: resolvedSymbolValue;
|
||||
var value = AddSigned(baseValue, descriptor.Addend);
|
||||
return descriptor.ValueKind == RelocationValueKind.PcRelative
|
||||
? unchecked(value - descriptor.TargetAddress)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static bool TryWriteRelocationValue(
|
||||
IVirtualMemory virtualMemory,
|
||||
RelocationDescriptor descriptor,
|
||||
ulong value,
|
||||
out string? error)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
var length = sizeof(ulong);
|
||||
switch (descriptor.WriteKind)
|
||||
{
|
||||
case RelocationWriteKind.UInt64:
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
break;
|
||||
|
||||
case RelocationWriteKind.UInt32:
|
||||
if (value > uint.MaxValue)
|
||||
{
|
||||
error = $"value 0x{value:X16} overflows an unsigned 32-bit relocation";
|
||||
return false;
|
||||
}
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)value);
|
||||
length = sizeof(uint);
|
||||
break;
|
||||
|
||||
case RelocationWriteKind.Int32:
|
||||
var signedValue = unchecked((long)value);
|
||||
if (signedValue is < int.MinValue or > int.MaxValue)
|
||||
{
|
||||
error = $"value {signedValue} overflows a signed 32-bit relocation";
|
||||
return false;
|
||||
}
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer, (int)signedValue);
|
||||
length = sizeof(int);
|
||||
break;
|
||||
|
||||
default:
|
||||
error = $"unknown relocation write kind {descriptor.WriteKind}";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = virtualMemory.TryWrite(descriptor.TargetAddress, buffer[..length])
|
||||
? null
|
||||
: "target memory is not writable";
|
||||
return error is null;
|
||||
}
|
||||
|
||||
private static int GetRelocationWriteSize(uint relocationType)
|
||||
{
|
||||
return relocationType is
|
||||
RelocationTypePc32 or
|
||||
RelocationTypePlt32 or
|
||||
RelocationTypeUnsigned32 or
|
||||
RelocationTypeSigned32 or
|
||||
RelocationTypeSize32
|
||||
? sizeof(uint)
|
||||
: sizeof(ulong);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void RunRelocationSelfChecks()
|
||||
{
|
||||
var pc32 = new RelocationDescriptor(
|
||||
TargetAddress: 0x1000,
|
||||
Addend: -4,
|
||||
ImportNid: null,
|
||||
SymbolValue: 0x1800,
|
||||
RelocationValueKind.PcRelative,
|
||||
IsDataImport: false,
|
||||
RelocationWriteKind.Int32);
|
||||
Debug.Assert(
|
||||
unchecked((long)ComputeRelocationValue(pc32, pc32.SymbolValue)) == 0x7FC,
|
||||
"R_X86_64_PC32 did not apply S + A - P.");
|
||||
|
||||
var weak = new RelocationDescriptor(
|
||||
TargetAddress: 0x2000,
|
||||
Addend: 7,
|
||||
ImportNid: "weak",
|
||||
SymbolValue: 0,
|
||||
RelocationValueKind.Pointer,
|
||||
IsDataImport: false,
|
||||
IsWeak: true);
|
||||
Debug.Assert(
|
||||
ComputeRelocationValue(weak, resolvedSymbolValue: 0) == 7,
|
||||
"An unresolved weak relocation did not use S=0.");
|
||||
Debug.Assert(
|
||||
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||
"An unresolved weak symbol incorrectly received a trap import stub.");
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
{
|
||||
var mask = alignment - 1;
|
||||
@@ -2257,6 +2639,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
|
||||
}
|
||||
|
||||
// The CPU backend executes guest instructions natively, so a non
|
||||
// x86-64 image can only fail deep in execution with an opaque SIGILL.
|
||||
// Catch it here with an actionable message. (EM_X86_64 == 62.)
|
||||
const ushort ElfMachineX86_64 = 62;
|
||||
if (header.Machine != ElfMachineX86_64)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported ELF machine type 0x{header.Machine:X4}; SharpEmu only runs x86-64 (EM_X86_64) images.");
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
|
||||
@@ -2337,10 +2729,25 @@ public sealed class SelfLoader : ISelfLoader
|
||||
public uint Type => (uint)(Info & uint.MaxValue);
|
||||
}
|
||||
|
||||
private readonly record struct ModuleTlsInfo(ulong MemorySize, ulong StaticOffset);
|
||||
|
||||
private enum RelocationValueKind : byte
|
||||
{
|
||||
Pointer = 0,
|
||||
TlsModuleId = 1
|
||||
TlsModuleId = 1,
|
||||
// A pre-computed TLS offset written verbatim (TPOFF64/DTPOFF64). Unlike
|
||||
// Pointer it is a signed displacement, not a mapped address, so it is
|
||||
// patched as-is without the low-address validity warning.
|
||||
TlsOffset = 2,
|
||||
PcRelative = 3,
|
||||
SymbolSize = 4,
|
||||
}
|
||||
|
||||
private enum RelocationWriteKind : byte
|
||||
{
|
||||
UInt64 = 0,
|
||||
UInt32 = 1,
|
||||
Int32 = 2,
|
||||
}
|
||||
|
||||
private readonly record struct RelocationDescriptor(
|
||||
@@ -2349,7 +2756,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
string? ImportNid,
|
||||
ulong SymbolValue,
|
||||
RelocationValueKind ValueKind,
|
||||
bool IsDataImport);
|
||||
bool IsDataImport,
|
||||
RelocationWriteKind WriteKind = RelocationWriteKind.UInt64,
|
||||
bool IsWeak = false);
|
||||
|
||||
private enum SelfSegmentResolveStatus
|
||||
{
|
||||
@@ -2389,14 +2798,10 @@ public sealed class SelfLoader : ISelfLoader
|
||||
public ulong FileSize => _fileSize;
|
||||
|
||||
public bool HasKnownLayout =>
|
||||
((_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D) ||
|
||||
(_ident0 == 0x54 &&
|
||||
_ident1 == 0x14 &&
|
||||
_ident2 == 0xF5 &&
|
||||
_ident3 == 0xEE)) &&
|
||||
_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D &&
|
||||
_ident4 == 0x00 &&
|
||||
_ident5 == 0x01 &&
|
||||
_ident6 == 0x01 &&
|
||||
|
||||
@@ -48,7 +48,107 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||
{
|
||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
||||
_hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance;
|
||||
}
|
||||
|
||||
private sealed class CrossPlatformHostMemory : IHostMemory
|
||||
{
|
||||
public static readonly CrossPlatformHostMemory Instance = new();
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
unchecked((ulong)HostMemory.Alloc(
|
||||
(void*)desiredAddress,
|
||||
(nuint)size,
|
||||
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT,
|
||||
ToRawProtection(protection)));
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
unchecked((ulong)HostMemory.Alloc(
|
||||
(void*)desiredAddress,
|
||||
(nuint)size,
|
||||
HostMemory.MEM_RESERVE,
|
||||
ToRawProtection(protection)));
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) =>
|
||||
HostMemory.Alloc(
|
||||
(void*)address,
|
||||
(nuint)size,
|
||||
HostMemory.MEM_COMMIT,
|
||||
ToRawProtection(protection)) != null;
|
||||
|
||||
public bool Free(ulong address) =>
|
||||
HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE);
|
||||
|
||||
public bool Protect(
|
||||
ulong address,
|
||||
ulong size,
|
||||
HostPageProtection protection,
|
||||
out uint rawOldProtection) =>
|
||||
HostMemory.Protect(
|
||||
(void*)address,
|
||||
(nuint)size,
|
||||
ToRawProtection(protection),
|
||||
out rawOldProtection);
|
||||
|
||||
public bool ProtectRaw(
|
||||
ulong address,
|
||||
ulong size,
|
||||
uint rawProtection,
|
||||
out uint rawOldProtection) =>
|
||||
HostMemory.Protect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
if (HostMemory.Query((void*)address, out var raw) == 0)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var state = raw.State switch
|
||||
{
|
||||
HostMemory.MEM_FREE_STATE => HostRegionState.Free,
|
||||
HostMemory.MEM_RESERVE => HostRegionState.Reserved,
|
||||
_ => HostRegionState.Committed,
|
||||
};
|
||||
|
||||
info = new HostRegionInfo(
|
||||
raw.BaseAddress,
|
||||
raw.AllocationBase,
|
||||
raw.RegionSize,
|
||||
state,
|
||||
raw.State,
|
||||
FromRawProtection(raw.Protect),
|
||||
raw.Protect,
|
||||
raw.AllocationProtect);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size) =>
|
||||
HostMemory.FlushInstructionCache((void*)address, (nuint)size);
|
||||
|
||||
private static uint ToRawProtection(HostPageProtection protection) => protection switch
|
||||
{
|
||||
HostPageProtection.NoAccess => HostMemory.PAGE_NOACCESS,
|
||||
HostPageProtection.ReadOnly => HostMemory.PAGE_READONLY,
|
||||
HostPageProtection.ReadWrite => HostMemory.PAGE_READWRITE,
|
||||
HostPageProtection.Execute => HostMemory.PAGE_EXECUTE,
|
||||
HostPageProtection.ReadExecute => HostMemory.PAGE_EXECUTE_READ,
|
||||
HostPageProtection.ReadWriteExecute => HostMemory.PAGE_EXECUTE_READWRITE,
|
||||
HostPageProtection.ExecuteWriteCopy => 0x80,
|
||||
_ => HostMemory.PAGE_NOACCESS,
|
||||
};
|
||||
|
||||
private static HostPageProtection FromRawProtection(uint protection) => protection switch
|
||||
{
|
||||
HostMemory.PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||
HostMemory.PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||
HostMemory.PAGE_EXECUTE => HostPageProtection.Execute,
|
||||
HostMemory.PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||
HostMemory.PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||
0x80 => HostPageProtection.ExecuteWriteCopy,
|
||||
_ => HostPageProtection.NoAccess,
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
@@ -249,6 +349,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
// macOS needs alignment over-allocation; Linux uses exact-address search.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
var reserveSize = effectiveAlignment > PageSize
|
||||
? alignedSize + effectiveAlignment
|
||||
: alignedSize;
|
||||
try
|
||||
{
|
||||
var posixAddress = AllocateAt(cursor, reserveSize, executable, allowAlternative: true);
|
||||
if (posixAddress != 0)
|
||||
{
|
||||
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
||||
if (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++)
|
||||
{
|
||||
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
|
||||
@@ -262,19 +391,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||
if (actualAddress == cursor)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
actualAddress = 0;
|
||||
}
|
||||
catch
|
||||
if (TryAllocateAtExact(cursor, alignedSize, executable, out actualAddress))
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
||||
@@ -283,6 +403,28 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
{
|
||||
address = 0;
|
||||
|
||||
@@ -7,25 +7,22 @@ using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using SharpEmu.Libs.SystemService;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image, int Handle, bool StartAtBoot);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -84,22 +81,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
};
|
||||
var moduleManager = new ModuleManager();
|
||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
|
||||
// registration source; content tests in SharpEmu.Libs.Tests pin its invariants.
|
||||
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
||||
moduleManager.Freeze();
|
||||
|
||||
// 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 virtualMemory = new PhysicalVirtualMemory();
|
||||
|
||||
var fileSystem = new PhysicalFileSystem();
|
||||
|
||||
return new SharpEmuRuntime(
|
||||
new SelfLoader(),
|
||||
virtualMemory,
|
||||
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
||||
new CpuDispatcher(virtualMemory, moduleManager),
|
||||
moduleManager,
|
||||
Aerolib.Instance,
|
||||
cpuExecutionOptions,
|
||||
@@ -137,20 +131,21 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
LastBasicBlockTrace = null;
|
||||
LastMilestoneLog = null;
|
||||
FiberExports.ResetRuntimeState();
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
|
||||
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -173,7 +168,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -181,8 +176,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -192,8 +187,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -364,66 +359,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||
{
|
||||
var executableName = Path.GetFileName(ebootPath);
|
||||
if (string.IsNullOrWhiteSpace(executableName))
|
||||
{
|
||||
executableName = "eboot.bin";
|
||||
}
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("App bundle info:");
|
||||
builder.AppendLine($"- Display name: {displayName}");
|
||||
builder.AppendLine($"- Version: {version}");
|
||||
builder.AppendLine($"- Title ID: {titleId}");
|
||||
builder.AppendLine($"- Content ID: {contentId}");
|
||||
builder.AppendLine($"- Executable: {executableName}");
|
||||
builder.Append("- Platform: PlayStation 5");
|
||||
Log.Info(builder.ToString());
|
||||
}
|
||||
|
||||
private static string? ResolveContentId(string? bundleRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bundleRoot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||
Path.Combine(bundleRoot, "param.json"),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||
{
|
||||
return contentId.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||
{
|
||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||
@@ -438,6 +373,20 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dump tools commonly place decrypted executables in an app0/decrypted
|
||||
// sidecar while leaving Unity data in the parent app0 directory. Keep
|
||||
// loading code/modules beside the decrypted eboot, but mount /app0 at
|
||||
// the content-bearing parent so boot.config, metadata and assets resolve.
|
||||
if (string.Equals(Path.GetFileName(app0Root), "decrypted", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parentRoot = Path.GetDirectoryName(app0Root);
|
||||
if (!string.IsNullOrWhiteSpace(parentRoot) &&
|
||||
Directory.Exists(Path.Combine(parentRoot, "Media")))
|
||||
{
|
||||
app0Root = parentRoot;
|
||||
}
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
||||
return new App0BindingScope(app0VariableName);
|
||||
}
|
||||
@@ -483,6 +432,56 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryGetEhFrameInfo(
|
||||
SelfImage image,
|
||||
ulong imageSize,
|
||||
out ulong ehFrameHeaderAddress,
|
||||
out ulong ehFrameAddress,
|
||||
out ulong ehFrameSize)
|
||||
{
|
||||
ehFrameHeaderAddress = 0;
|
||||
ehFrameAddress = 0;
|
||||
ehFrameSize = 0;
|
||||
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
|
||||
? image.EntryPoint - image.ElfHeader.EntryPoint
|
||||
: 0UL;
|
||||
for (var i = 0; i < image.ProgramHeaders.Count; i++)
|
||||
{
|
||||
var header = image.ProgramHeaders[i];
|
||||
if (header.HeaderType != ProgramHeaderType.GnuEhFrame || header.MemorySize < 8)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var headerAddress = imageBase + header.VirtualAddress;
|
||||
Span<byte> ehHeader = stackalloc byte[8];
|
||||
if (!_virtualMemory.TryRead(headerAddress, ehHeader) ||
|
||||
ehHeader[0] != 1 ||
|
||||
ehHeader[1] != 0x1B)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relativeOffset = BinaryPrimitives.ReadInt32LittleEndian(ehHeader[4..]);
|
||||
ehFrameHeaderAddress = headerAddress;
|
||||
ehFrameAddress = unchecked((ulong)((long)headerAddress + 4 + relativeOffset));
|
||||
if (ehFrameAddress < 0x10000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relativeEhFrameAddress = ehFrameAddress - imageBase;
|
||||
ehFrameSize = header.VirtualAddress > relativeEhFrameAddress
|
||||
? header.VirtualAddress - relativeEhFrameAddress
|
||||
: imageSize > header.VirtualAddress
|
||||
? imageSize - header.VirtualAddress
|
||||
: 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private OrbisGen2Result? RunPreloadedModuleInitializers(
|
||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||
Generation generation,
|
||||
@@ -492,20 +491,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
for (var i = 0; i < loadedModuleImages.Count; i++)
|
||||
{
|
||||
var loadedModule = loadedModuleImages[i];
|
||||
if (!loadedModule.StartAtBoot)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
|
||||
if (initEntryPoint < 0x10000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!KernelModuleRegistry.TryBeginModuleStart(loadedModule.Handle, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moduleName = Path.GetFileName(loadedModule.Path);
|
||||
if (string.IsNullOrWhiteSpace(moduleName))
|
||||
{
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -514,10 +523,13 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
activeRuntimeSymbols,
|
||||
moduleName,
|
||||
_cpuExecutionOptions);
|
||||
KernelModuleRegistry.CompleteModuleStart(
|
||||
loadedModule.Handle,
|
||||
result == OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -538,8 +550,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -578,8 +590,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -611,12 +623,17 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
var moduleDirectories = new[]
|
||||
{
|
||||
Path.Combine(ebootDirectory, "sce_module"),
|
||||
Path.Combine(ebootDirectory, "sce_modules"),
|
||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
|
||||
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
|
||||
// them up front so the HLE loader can return a real module handle and dlsym
|
||||
// can resolve their exports, but defer DT_INIT until the guest requests them.
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
|
||||
}
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(Directory.Exists)
|
||||
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.Where(entry => Directory.Exists(entry.Path))
|
||||
.ToArray();
|
||||
|
||||
if (moduleDirectories.Length == 0)
|
||||
@@ -626,28 +643,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(path =>
|
||||
.EnumerateFiles(directory.Path)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => (Path: path, directory.StartAtBoot)))
|
||||
.Where(entry =>
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(entry.Path);
|
||||
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.ToArray();
|
||||
|
||||
var modulePaths = allModulePaths
|
||||
.Where(ShouldPreloadModule)
|
||||
.Where(entry => ShouldPreloadModule(entry.Path))
|
||||
.ToArray();
|
||||
var skippedModules = allModulePaths
|
||||
.Where(path => !ShouldPreloadModule(path))
|
||||
.Select(Path.GetFileName)
|
||||
.Where(entry => !ShouldPreloadModule(entry.Path))
|
||||
.Select(entry => Path.GetFileName(entry.Path))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.ToArray();
|
||||
if (skippedModules.Length > 0)
|
||||
{
|
||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
}
|
||||
|
||||
if (modulePaths.Length == 0)
|
||||
@@ -655,14 +674,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories.Select(entry => entry.Path))}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
var mergedSymbolCount = 0;
|
||||
foreach (var modulePath in modulePaths)
|
||||
foreach (var moduleEntry in modulePaths)
|
||||
{
|
||||
var modulePath = moduleEntry.Path;
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(modulePath);
|
||||
@@ -687,25 +707,52 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
||||
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
||||
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
InstallNativePluginCompatibilityHooks(importStubs, moduleImage, modulePath);
|
||||
var moduleHandle = RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
||||
var moduleName = Path.GetFileName(modulePath);
|
||||
var startAtBoot = moduleEntry.StartAtBoot ||
|
||||
string.Equals(moduleName, "libfmod.prx", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(moduleName, "libfmodstudio.prx", StringComparison.OrdinalIgnoreCase);
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage, moduleHandle, startAtBoot));
|
||||
loadedModules++;
|
||||
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedModules++;
|
||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
private static void InstallNativePluginCompatibilityHooks(
|
||||
IDictionary<ulong, string> importStubs,
|
||||
SelfImage moduleImage,
|
||||
string modulePath)
|
||||
{
|
||||
if (!string.Equals(Path.GetFileName(modulePath), "libfmod.prx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOnlySpan<string> compatibilityNids = ["uPLTdl3psGk"];
|
||||
foreach (var nid in compatibilityNids)
|
||||
{
|
||||
if (moduleImage.RuntimeSymbols.TryGetValue(nid, out var address) && address >= 0x10000)
|
||||
{
|
||||
importStubs[address] = nid;
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Installed FMOD compatibility hook: {nid} -> 0x{address:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebindImportedDataSymbols(
|
||||
SelfImage mainImage,
|
||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||
@@ -722,8 +769,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,8 +802,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -768,8 +815,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -778,8 +825,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -815,8 +862,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -910,7 +957,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return !PreloadSkipModules.Contains(fileName);
|
||||
}
|
||||
|
||||
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
||||
private int RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
||||
{
|
||||
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
||||
{
|
||||
@@ -918,15 +965,27 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
size = 0;
|
||||
}
|
||||
|
||||
_ = TryGetEhFrameInfo(
|
||||
image,
|
||||
size,
|
||||
out var ehFrameHeaderAddress,
|
||||
out var ehFrameAddress,
|
||||
out var ehFrameSize);
|
||||
var handle = KernelModuleRegistry.RegisterModule(
|
||||
modulePath,
|
||||
baseAddress,
|
||||
size,
|
||||
image.EntryPoint,
|
||||
image.InitFunctionEntryPoint,
|
||||
ehFrameHeaderAddress,
|
||||
ehFrameAddress,
|
||||
ehFrameSize,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Log.Info(
|
||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
KernelModuleRegistry.RegisterModuleSymbols(handle, image.RuntimeSymbols);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||
|
||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Iced": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ public sealed class GuiSettings
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
public bool CheckForUpdatesOnStartup { 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();
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"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.Desc": "Variáveis de ambiente passadas ao emulador durante a inicialização.",
|
||||
"Options.Env.Bthid.Desc": "Informa o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica verificando indefinidamente.\nNormalmente, deixe desativado. Alguns títulos travam quando a inicialização falha.",
|
||||
"Options.Env.LoopGuard.Desc": "Não força o encerramento de títulos que repetem a mesma chamada por tempo excessivo.\nExperimente esta opção se um jogo fechar 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).",
|
||||
@@ -83,7 +83,7 @@
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Pesquisar...",
|
||||
"Console.AutoScroll": "Rolagem automática",
|
||||
"Console.Split": "Recortar",
|
||||
"Console.Split": "Dividir",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpar",
|
||||
"Console.WindowTitle": "Console do SharpEmu",
|
||||
@@ -134,5 +134,13 @@
|
||||
"Dialog.PsExecutables": "Executáveis de PS",
|
||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||
"Dialog.LogFiles": "Arquivos de log"
|
||||
"Dialog.LogFiles": "Arquivos de log",
|
||||
|
||||
"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": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||
"About.GithubButton": "Contribua no GitHub!",
|
||||
"About.DiscordButton": "Entre no nosso Discord!"
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
"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.WritableApp0.Desc": "Allow titles to create and write files inside their install folder.\nNeeded by unpackaged dumps that write their save or config data under /app0.",
|
||||
"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.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
@@ -139,8 +141,25 @@
|
||||
"Options.About" : "About",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Source code, issues and project development.",
|
||||
"About.Github.LatestCommitLabel": "Latest Commit",
|
||||
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||
"About.GithubButton": "Contribute in GitHub!",
|
||||
"About.DiscordButton": "Join our Discord!"
|
||||
"About.DiscordButton": "Join our Discord!",
|
||||
|
||||
"Updater.Auto.Label": "Check for updates on startup",
|
||||
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",
|
||||
"Updater.Label": "Updates",
|
||||
"Updater.Check": "Check for updates",
|
||||
"Updater.DownloadRestart": "Download and restart",
|
||||
"Updater.Status.Ready": "Current build: {0}",
|
||||
"Updater.Status.Checking": "Checking for updates…",
|
||||
"Updater.Status.Current": "You are up to date ({0}).",
|
||||
"Updater.Status.Available": "A new build is available: {0}",
|
||||
"Updater.Status.Downloading": "Downloading update… {0}%",
|
||||
"Updater.Status.Installing": "Installing update…",
|
||||
"Updater.Status.Timeout": "Update check timed out after 10 seconds.",
|
||||
"Updater.Status.Failed": "Could not check for updates.",
|
||||
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Github.LatestCommitLabel": "Último Commit",
|
||||
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||
"About.GithubButton": "Contribuye en GitHub!",
|
||||
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
"Library.Loading": "Chargement de la bibliothèque…",
|
||||
|
||||
"Options.General": "Général",
|
||||
"Options.Env.Tab": "Environnement",
|
||||
"Options.Section.Environment": "VARIABLES D’ENVIRONNEMENT",
|
||||
"Options.Env.Desc": "Paramètres passés à l’émulateur comme variables d’environnement au lancement.",
|
||||
"Options.Env.Bthid.Desc": "Signaler le HID Bluetooth comme indisponible pour les titres dont le middleware volant/FFB interroge indéfiniment.\nLaissez désactivé normalement. Certains titres se bloquent si l’initialisation échoue.",
|
||||
"Options.Env.LoopGuard.Desc": "Ne pas forcer la fermeture des titres qui répètent le même appel trop longtemps.\nEssayez ceci quand un jeu se ferme tout seul pendant le chargement.",
|
||||
"Options.Env.VkValidation.Desc": "Activer les couches de validation Vulkan pour le débogage GPU.\nRalentit. Nécessite l’installation du Vulkan SDK.",
|
||||
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
|
||||
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
"Options.Section.Launcher": "LANCEUR",
|
||||
@@ -125,5 +134,13 @@
|
||||
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||
"Dialog.LogFiles": "Fichiers journaux"
|
||||
"Dialog.LogFiles": "Fichiers journaux",
|
||||
|
||||
"Options.About": "À propos",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Code source, issues et développement du projet.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
||||
"About.GithubButton": "Contribuer sur GitHub !",
|
||||
"About.DiscordButton": "Rejoindre notre Discord !"
|
||||
}
|
||||
|
||||
@@ -125,5 +125,20 @@
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
"Dialog.LogFiles": "Günlük Dosyaları",
|
||||
|
||||
"Updater.Auto.Label": "Açılışta güncellemeleri denetle",
|
||||
"Updater.Auto.Desc": "Açılışı geciktirmeden GitHub'ı denetler.",
|
||||
"Updater.Label": "Güncellemeler",
|
||||
"Updater.Check": "Güncellemeleri denetle",
|
||||
"Updater.DownloadRestart": "İndir ve yeniden başlat",
|
||||
"Updater.Status.Ready": "Mevcut build: {0}",
|
||||
"Updater.Status.Checking": "Güncellemeler denetleniyor…",
|
||||
"Updater.Status.Current": "Güncelsiniz ({0}).",
|
||||
"Updater.Status.Available": "Yeni build mevcut: {0}",
|
||||
"Updater.Status.Downloading": "Güncelleme indiriliyor… %{0}",
|
||||
"Updater.Status.Installing": "Güncelleme kuruluyor…",
|
||||
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
||||
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
||||
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
|
||||
}
|
||||
|
||||
@@ -315,6 +315,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
|
||||
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="card">
|
||||
@@ -322,6 +332,40 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TextBlock x:Name="AboutSectionTitle"
|
||||
Classes="sectionTitle"
|
||||
Text="ABOUT" />
|
||||
|
||||
<!--Latest commit info-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||
Width="24" Height="24" VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
|
||||
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
|
||||
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
|
||||
VerticalAlignment="Center" IsEnabled="False" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<!--Update-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||
Content="Check for updates" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--Github-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -420,6 +464,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvWritableApp0Desc"
|
||||
Text="Allow titles to create and write files inside their install folder. Needed by unpackaged dumps that write their save or config data under /app0."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" 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" />
|
||||
@@ -453,6 +508,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogIoDesc"
|
||||
Text="Log file open, read, and path-resolve activity to the console. Use when a game cannot find its data files during boot."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" 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" />
|
||||
|
||||
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
@@ -34,6 +35,12 @@ public partial class MainWindow : Window
|
||||
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
|
||||
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
|
||||
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
|
||||
private static readonly StringComparer FilePathComparer = OperatingSystem.IsWindows()
|
||||
? StringComparer.OrdinalIgnoreCase
|
||||
: StringComparer.Ordinal;
|
||||
private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
private readonly List<GameEntry> _allGames = new();
|
||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||
@@ -51,6 +58,9 @@ public partial class MainWindow : Window
|
||||
private bool _isRunning;
|
||||
private int _autoScrollTicks;
|
||||
private int _activePageIndex;
|
||||
private Updater.UpdateInfo? _availableUpdate;
|
||||
private string _updateStatusKey = "Updater.Status.Ready";
|
||||
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
|
||||
|
||||
// Discord Rich Presence state.
|
||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
@@ -69,6 +79,10 @@ public partial class MainWindow : Window
|
||||
private long _navUpNextAt;
|
||||
private long _navDownNextAt;
|
||||
|
||||
//Github http client for latest commit
|
||||
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
|
||||
private string? _latestCommitSha;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -125,17 +139,24 @@ public partial class MainWindow : Window
|
||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||
UpdateDiscordPresence();
|
||||
};
|
||||
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
|
||||
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
|
||||
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
|
||||
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);
|
||||
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.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);
|
||||
EnvLogIoToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
|
||||
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||
@@ -166,7 +187,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "https://github.com/par274/sharpemu",
|
||||
FileName = "https://github.com/sharpemu/sharpemu",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
@@ -179,6 +200,21 @@ public partial class MainWindow : Window
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
|
||||
LatestCommitHashText.Click += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_latestCommitSha))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName =
|
||||
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -220,6 +256,91 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Github http client config ----
|
||||
// This is for getting lash commit id
|
||||
private static HttpClient CreateGithubHttpClient()
|
||||
{
|
||||
var client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("SharpEmu/1.0");
|
||||
client.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/vnd.github.sha"));
|
||||
|
||||
client.DefaultRequestHeaders.Add(
|
||||
"X-GitHub-Api-Version",
|
||||
"2026-03-10");
|
||||
|
||||
return client;
|
||||
}
|
||||
private async Task LoadLatestCommitAsync()
|
||||
{
|
||||
const string apiUrl =
|
||||
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
|
||||
|
||||
_latestCommitSha = null;
|
||||
LatestCommitHashText.Content = "Loading…";
|
||||
LatestCommitHashText.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await GithubHttpClient.GetAsync(apiUrl);
|
||||
var responseBody =
|
||||
(await response.Content.ReadAsStringAsync()).Trim();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LatestCommitHashText.Content =
|
||||
$"HTTP {(int)response.StatusCode}";
|
||||
|
||||
ToolTip.SetTip(
|
||||
LatestCommitHashText,
|
||||
string.IsNullOrWhiteSpace(responseBody)
|
||||
? response.ReasonPhrase
|
||||
: responseBody);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseBody.Length < 7)
|
||||
{
|
||||
LatestCommitHashText.Content = "Invalid response";
|
||||
ToolTip.SetTip(LatestCommitHashText, responseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the complete SHA for the URL.
|
||||
_latestCommitSha = responseBody;
|
||||
|
||||
// Display only the short SHA.
|
||||
LatestCommitHashText.Content =
|
||||
responseBody[..Math.Min(7, responseBody.Length)];
|
||||
|
||||
LatestCommitHashText.IsEnabled = true;
|
||||
|
||||
ToolTip.SetTip(
|
||||
LatestCommitHashText,
|
||||
$"Open commit {_latestCommitSha}");
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Timeout";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Connection error";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Error";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Controller navigation ----
|
||||
|
||||
private void PollGamepad()
|
||||
@@ -365,6 +486,12 @@ public partial class MainWindow : Window
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
UpdateDiscordPresence();
|
||||
_ = LoadLatestCommitAsync();
|
||||
|
||||
if (_settings.CheckForUpdatesOnStartup)
|
||||
{
|
||||
_ = CheckForUpdatesAsync();
|
||||
}
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
@@ -421,9 +548,11 @@ public partial class MainWindow : Window
|
||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.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");
|
||||
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
|
||||
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||
@@ -466,8 +595,10 @@ public partial class MainWindow : Window
|
||||
|
||||
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
|
||||
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
|
||||
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
|
||||
{
|
||||
toggle.OnContent = loc.Get("Common.On");
|
||||
toggle.OffContent = loc.Get("Common.Off");
|
||||
@@ -491,6 +622,10 @@ public partial class MainWindow : Window
|
||||
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||
GithubButton.Content = loc.Get("About.GithubButton");
|
||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
||||
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
||||
RefreshUpdateText();
|
||||
|
||||
UpdateEmptyStateTexts();
|
||||
UpdateSelectedGameTexts();
|
||||
@@ -606,15 +741,87 @@ public partial class MainWindow : Window
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
|
||||
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
|
||||
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");
|
||||
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
|
||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||
UpdateLogFilePathText();
|
||||
}
|
||||
|
||||
private async Task OnUpdateButtonAsync()
|
||||
{
|
||||
if (_availableUpdate is null)
|
||||
{
|
||||
await CheckForUpdatesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
var progress = new Progress<int>(value =>
|
||||
SetUpdateStatus("Updater.Status.Downloading", value));
|
||||
await Updater.DownloadAndRestartAsync(_availableUpdate, progress);
|
||||
SetUpdateStatus("Updater.Status.Installing");
|
||||
Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
UpdateButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
_availableUpdate = null;
|
||||
UpdateButton.IsEnabled = false;
|
||||
SetUpdateStatus("Updater.Status.Checking");
|
||||
try
|
||||
{
|
||||
_availableUpdate = await Updater.CheckAsync(BuildInfo.CommitSha);
|
||||
SetUpdateStatus(
|
||||
_availableUpdate is null ? "Updater.Status.Current" : "Updater.Status.Available",
|
||||
_availableUpdate?.Sha ?? BuildInfo.CommitSha ?? "dev");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Timeout");
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Unsupported");
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateButton.IsEnabled = true;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUpdateStatus(string key, params object?[] args)
|
||||
{
|
||||
_updateStatusKey = key;
|
||||
_updateStatusArgs = args;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
|
||||
private void RefreshUpdateText()
|
||||
{
|
||||
UpdateStatusText.Text = Localization.Instance.Format(_updateStatusKey, _updateStatusArgs);
|
||||
UpdateButton.Content = Localization.Instance.Get(
|
||||
_availableUpdate is null ? "Updater.Check" : "Updater.DownloadRestart");
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -727,7 +934,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_settings.GameFolders.Contains(path, FilePathComparer))
|
||||
{
|
||||
_settings.GameFolders.Add(path);
|
||||
changed = true;
|
||||
@@ -737,7 +944,7 @@ public partial class MainWindow : Window
|
||||
// games beneath it that were removed from the library earlier.
|
||||
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
||||
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
||||
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
|
||||
excluded.StartsWith(prefix, FilePathComparison)) > 0;
|
||||
|
||||
if (changed)
|
||||
{
|
||||
@@ -750,7 +957,7 @@ public partial class MainWindow : Window
|
||||
private async Task RescanLibraryAsync()
|
||||
{
|
||||
var folders = _settings.GameFolders.ToArray();
|
||||
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
||||
var excluded = new HashSet<string>(_settings.ExcludedGames, FilePathComparer);
|
||||
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
|
||||
EmptyState.IsVisible = false;
|
||||
LoadingState.IsVisible = true;
|
||||
@@ -868,7 +1075,7 @@ public partial class MainWindow : Window
|
||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
||||
{
|
||||
var games = new List<GameEntry>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var seen = new HashSet<string>(FilePathComparer);
|
||||
var enumeration = new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = true,
|
||||
@@ -1132,13 +1339,13 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_settings.ExcludedGames.Contains(game.Path, FilePathComparer))
|
||||
{
|
||||
_settings.ExcludedGames.Add(game.Path);
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, FilePathComparison));
|
||||
GameList.SelectedItem = null;
|
||||
RefreshVisibleGames();
|
||||
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
|
||||
@@ -1162,7 +1369,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
if (selectedPath is not null &&
|
||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, FilePathComparison))
|
||||
is { } reselected)
|
||||
{
|
||||
GameList.SelectedItem = reselected;
|
||||
@@ -1487,7 +1694,7 @@ public partial class MainWindow : Window
|
||||
_isRunning = true;
|
||||
_runningGameName = displayName;
|
||||
_runningGameTitleId = _allGames
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?
|
||||
.TitleId;
|
||||
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
StatusDot.Fill = SuccessLineBrush;
|
||||
|
||||
@@ -9,7 +9,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
the executable is started without arguments. -->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<Version>0.0.1</Version>
|
||||
<!-- Required by the source-generated LibraryImport stubs in the linked
|
||||
controller readers below. -->
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
@@ -33,6 +32,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>Self-contained Windows updater; the emulator layers do not depend on it.</summary>
|
||||
public static class Updater
|
||||
{
|
||||
private const string ApplyArgument = "--sharpemu-apply-update";
|
||||
private const string LatestReleaseUrl = "https://api.github.com/repos/sharpemu/sharpemu/releases/latest";
|
||||
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
|
||||
private static readonly HttpClient Http = CreateHttpClient();
|
||||
|
||||
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
|
||||
|
||||
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var platform = CurrentPlatform();
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(CheckTimeout);
|
||||
|
||||
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return ParseRelease(
|
||||
await response.Content.ReadAsStringAsync(timeout.Token),
|
||||
currentSha,
|
||||
platform.Rid,
|
||||
platform.Extension);
|
||||
}
|
||||
|
||||
public static async Task DownloadAndRestartAsync(
|
||||
UpdateInfo update,
|
||||
IProgress<int>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "SharpEmu.Update");
|
||||
var payload = Path.Combine(root, "payload");
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(root);
|
||||
var archive = Path.Combine(root, update.Name);
|
||||
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await using var output = File.Create(archive);
|
||||
var buffer = new byte[81920];
|
||||
long written = 0;
|
||||
int read;
|
||||
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||
written += read;
|
||||
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
|
||||
}
|
||||
|
||||
if (written != update.Size)
|
||||
{
|
||||
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
|
||||
}
|
||||
}
|
||||
|
||||
var platform = CurrentPlatform();
|
||||
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
|
||||
|
||||
var start = new ProcessStartInfo(stagedExe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = payload,
|
||||
};
|
||||
start.ArgumentList.Add(ApplyArgument);
|
||||
start.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||
start.ArgumentList.Add(AppContext.BaseDirectory);
|
||||
using var helper = Process.Start(start)
|
||||
?? throw new InvalidOperationException("The update installer could not be started.");
|
||||
}
|
||||
|
||||
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
|
||||
public static bool TryApply(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
if (args.Length != 3 || args[0] != ApplyArgument)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (int.TryParse(args[1], out var oldPid))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Process.GetProcessById(oldPid).WaitForExit(30_000))
|
||||
{
|
||||
throw new TimeoutException("SharpEmu did not close within 30 seconds.");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// The old process has already exited.
|
||||
}
|
||||
}
|
||||
|
||||
var source = AppContext.BaseDirectory;
|
||||
var target = Path.GetFullPath(args[2]);
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.GetRelativePath(source, file);
|
||||
if (relative.Equals("gui-settings.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("user" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("logs" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("Languages" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destination = Path.Combine(target, relative);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.Copy(file, destination, overwrite: true);
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(destination, File.GetUnixFileMode(file));
|
||||
}
|
||||
}
|
||||
|
||||
using var restarted = Process.Start(new ProcessStartInfo(
|
||||
Path.Combine(target, CurrentPlatform().ExecutableName))
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = target,
|
||||
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exitCode = 1;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort diagnostics only.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static UpdateInfo? ParseRelease(
|
||||
string json,
|
||||
string? currentSha,
|
||||
string rid,
|
||||
string extension)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
|
||||
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
|
||||
{
|
||||
var name = asset.GetProperty("name").GetString() ?? "";
|
||||
var marker = $"-{rid}-";
|
||||
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
|
||||
markerIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sha = name[(markerIndex + marker.Length)..^extension.Length];
|
||||
if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add((
|
||||
asset.GetProperty("created_at").GetDateTimeOffset(),
|
||||
new UpdateInfo(
|
||||
sha,
|
||||
name,
|
||||
asset.GetProperty("browser_download_url").GetString()!,
|
||||
asset.GetProperty("size").GetInt64())));
|
||||
}
|
||||
|
||||
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
|
||||
return latest is null || string.Equals(latest.Sha, currentSha, StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: latest;
|
||||
}
|
||||
|
||||
private static string ExtractArchive(
|
||||
string archive,
|
||||
string payload,
|
||||
string extension,
|
||||
string executableName)
|
||||
{
|
||||
if (extension == ".zip")
|
||||
{
|
||||
ZipFile.ExtractToDirectory(archive, payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(payload);
|
||||
using var compressed = File.OpenRead(archive);
|
||||
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||
TarFile.ExtractToDirectory(gzip, payload, overwriteFiles: false);
|
||||
}
|
||||
|
||||
var executable = Path.Combine(payload, executableName);
|
||||
if (!File.Exists(executable))
|
||||
{
|
||||
throw new InvalidDataException($"The update archive does not contain {executableName}.");
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(executable, File.GetUnixFileMode(executable) | UnixFileMode.UserExecute);
|
||||
}
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient()
|
||||
{
|
||||
var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpEmu", "0.0.1"));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
return client;
|
||||
}
|
||||
|
||||
private static PlatformInfo CurrentPlatform()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
throw new PlatformNotSupportedException("SharpEmu releases require an x64 process.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows()) return new("win-x64", ".zip", "SharpEmu.exe");
|
||||
if (OperatingSystem.IsLinux()) return new("linux-x64", ".tar.gz", "SharpEmu");
|
||||
if (OperatingSystem.IsMacOS()) return new("osx-x64", ".tar.gz", "SharpEmu");
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Avalonia": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -26,6 +26,12 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
|
||||
public ulong GsBase { get; set; }
|
||||
|
||||
/// <summary>x87 control word observed at the current guest boundary.</summary>
|
||||
public ushort FpuControlWord { get; set; } = 0x037F;
|
||||
|
||||
/// <summary>MXCSR observed at the current guest boundary.</summary>
|
||||
public uint Mxcsr { get; set; } = 0x1F80;
|
||||
|
||||
public ulong this[CpuRegister register]
|
||||
{
|
||||
get => _registers[(int)register];
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a string parameter of a [SysAbiExport] handler as a guest null-terminated
|
||||
/// UTF-8 pointer. The generated thunk reads the string from the argument register's
|
||||
/// address (bounded by <see cref="MaxLength"/>) before invoking the handler, and
|
||||
/// returns ORBIS_GEN2_ERROR_MEMORY_FAULT to the guest when the read fails — including
|
||||
/// a null pointer, matching TryReadNullTerminatedUtf8's contract.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GuestCStringAttribute : Attribute
|
||||
{
|
||||
public GuestCStringAttribute(int maxLength) => MaxLength = maxLength;
|
||||
|
||||
/// <summary>Upper bound in bytes for the guest read, terminator included.</summary>
|
||||
public int MaxLength { get; }
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Detects guest CPU writes into memory that backs a host GPU image. On PS5
|
||||
/// render targets alias unified memory, so games freely mix CPU writes and GPU
|
||||
/// draws on the same surface (Chowdren titles memset their fog layers every
|
||||
/// frame). Host GPU images are separate storage, so the video backend needs to
|
||||
/// know when the guest CPU rewrote a surface to re-upload it. Ranges are
|
||||
/// write-protected; the first write faults, the fault handler restores write
|
||||
/// access and marks the range dirty, and the video backend consumes the dirty
|
||||
/// flag once per flip and re-arms protection after re-uploading.
|
||||
/// </summary>
|
||||
public static unsafe class GuestImageWriteTracker
|
||||
{
|
||||
private const int ProtRead = 0x1;
|
||||
private const int ProtWrite = 0x2;
|
||||
private const int ClockMonotonicRaw = 4;
|
||||
|
||||
private sealed class TrackedRange
|
||||
{
|
||||
public ulong Address;
|
||||
public ulong ByteCount;
|
||||
public ulong Start;
|
||||
public ulong End;
|
||||
public int Dirty;
|
||||
public int Armed;
|
||||
public int FirstCpuWriteSeen;
|
||||
public int PendingFirstCpuWrite;
|
||||
public bool TraceLifetime;
|
||||
public long SourceSequence;
|
||||
public long FirstCpuWriteTraceSequence;
|
||||
public long FirstCpuWriteTimestampNanoseconds;
|
||||
public ulong FirstCpuWriteAddress;
|
||||
public ulong FirstCpuWritePage;
|
||||
public string Source = "unspecified";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct Timespec
|
||||
{
|
||||
public long Seconds;
|
||||
public long Nanoseconds;
|
||||
}
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
||||
|
||||
// Snapshot array read lock-free from the signal handler; rebuilt on every
|
||||
// mutation under the gate. Signal handlers must not take managed locks.
|
||||
private static TrackedRange[] _rangeSnapshot = [];
|
||||
|
||||
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
||||
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||
private static readonly (bool Wildcard, string[] Sources) _lifetimeSourceTraceFilter =
|
||||
ParseSourceList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME"));
|
||||
private static readonly bool _lifetimeTraceEnabled =
|
||||
_lifetimeTraceFilter.Wildcard ||
|
||||
_lifetimeTraceFilter.Addresses.Length != 0 ||
|
||||
_lifetimeSourceTraceFilter.Wildcard ||
|
||||
_lifetimeSourceTraceFilter.Sources.Length != 0;
|
||||
private static readonly long _lifetimeTraceEpochNanoseconds =
|
||||
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
||||
private static long _lifetimeTraceSequence;
|
||||
|
||||
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
||||
private static extern int Mprotect(nint address, nuint length, int protection);
|
||||
|
||||
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
||||
private static extern int ClockGetTime(int clockId, Timespec* time);
|
||||
|
||||
public static bool Enabled => _enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Exercises the fault-handling path once outside signal context so every
|
||||
/// branch is JIT-compiled (and, under Rosetta 2, translated) before a real
|
||||
/// fault arrives — a cold signal path is silently never entered there.
|
||||
/// </summary>
|
||||
public static void WarmUp()
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scratch = NativeMemory.AllocZeroed(4096);
|
||||
try
|
||||
{
|
||||
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
||||
// capture path before a real protected-page write reaches it.
|
||||
_ = GetMonotonicNanoseconds();
|
||||
var address = (ulong)scratch;
|
||||
Track(address, 4096);
|
||||
_ = TryHandleWriteFault(address);
|
||||
_ = ConsumeDirty(address);
|
||||
Untrack(address);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMemory.Free(scratch);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Registers a range and arms write protection on it.</summary>
|
||||
public static void Track(
|
||||
ulong address,
|
||||
ulong byteCount,
|
||||
long sourceSequence = 0,
|
||||
string source = "unspecified")
|
||||
{
|
||||
if (!_enabled || address == 0 || byteCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (start, length) = PageAlign(address, byteCount);
|
||||
lock (_gate)
|
||||
{
|
||||
_rangesByAddress.TryGetValue(address, out var range);
|
||||
if (range is not null &&
|
||||
(range.Start != start ||
|
||||
range.End != start + length ||
|
||||
range.ByteCount != byteCount))
|
||||
{
|
||||
// Never resize an object that is still reachable from the
|
||||
// signal handler's lock-free snapshot. Retire it and publish
|
||||
// a fresh immutable range.
|
||||
DisarmLocked(range, "replace-range");
|
||||
_rangesByAddress.Remove(address);
|
||||
range = null;
|
||||
}
|
||||
|
||||
if (range is null)
|
||||
{
|
||||
range = new TrackedRange
|
||||
{
|
||||
Address = address,
|
||||
ByteCount = byteCount,
|
||||
Start = start,
|
||||
End = start + length,
|
||||
TraceLifetime =
|
||||
ShouldTraceRange(start, start + length) || ShouldTraceSource(source),
|
||||
SourceSequence = sourceSequence,
|
||||
Source = source,
|
||||
};
|
||||
_rangesByAddress[address] = range;
|
||||
RebuildSnapshotLocked();
|
||||
}
|
||||
else
|
||||
{
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
}
|
||||
|
||||
range.SourceSequence = sourceSequence;
|
||||
range.Source = source;
|
||||
range.TraceLifetime =
|
||||
ShouldTraceRange(range.Start, range.End) || ShouldTraceSource(source);
|
||||
ArmLocked(range, "arm");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Untrack(ulong address)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||
{
|
||||
DisarmLocked(range, "untrack");
|
||||
_rangesByAddress.Remove(address);
|
||||
RebuildSnapshotLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the guest CPU wrote the range since the last call,
|
||||
/// clearing the flag. The caller re-arms via <see cref="Rearm"/> after it
|
||||
/// finished reading the guest bytes.
|
||||
/// </summary>
|
||||
public static bool ConsumeDirty(ulong address)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
return Interlocked.Exchange(ref range.Dirty, 0) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-consuming variant of <see cref="ConsumeDirty"/>: reports whether
|
||||
/// the range has been written since it was last re-armed, leaving the
|
||||
/// flag for the owner that evicts and re-uploads.
|
||||
/// </summary>
|
||||
public static bool PeekDirty(ulong address)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
return Volatile.Read(ref range.Dirty) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Rearm(ulong address)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||
{
|
||||
ArmLocked(range, "rearm");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||
/// POSIX signal bridge, but a managed Buffer.MemoryCopy into a protected
|
||||
/// page is surfaced by the runtime as a fatal AccessViolation instead of
|
||||
/// a resumable guest fault. Visit every page in the write span up front so
|
||||
/// all overlapping texture owners are dirtied and made writable.
|
||||
/// </summary>
|
||||
public static void NotifyManagedWrite(ulong address, ulong byteCount)
|
||||
{
|
||||
if (!_enabled || address == 0 || byteCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var end = address > ulong.MaxValue - byteCount
|
||||
? ulong.MaxValue
|
||||
: address + byteCount;
|
||||
var candidate = address;
|
||||
while (candidate < end)
|
||||
{
|
||||
_ = TryHandleWriteFault(candidate);
|
||||
var nextPage = (candidate & ~0xFFFUL) + 0x1000UL;
|
||||
if (nextPage <= candidate)
|
||||
{
|
||||
break;
|
||||
}
|
||||
candidate = nextPage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes scalar first-write records captured by the POSIX signal handler.
|
||||
/// Call only from ordinary managed execution, never from signal context.
|
||||
/// </summary>
|
||||
public static void FlushPendingDiagnostics()
|
||||
{
|
||||
if (!_enabled || !_lifetimeTraceEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var range in _rangesByAddress.Values)
|
||||
{
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal-handler entry: if the fault address lies in a tracked, armed
|
||||
/// range, restore write access, mark the range dirty, and return true so
|
||||
/// the faulting write can be retried. Must not allocate or lock.
|
||||
/// </summary>
|
||||
public static bool TryHandleWriteFault(ulong faultAddress)
|
||||
{
|
||||
if (!_enabled || faultAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ranges = Volatile.Read(ref _rangeSnapshot);
|
||||
var writableStart = ulong.MaxValue;
|
||||
var writableEnd = 0UL;
|
||||
for (var index = 0; index < ranges.Length; index++)
|
||||
{
|
||||
var range = ranges[index];
|
||||
if (faultAddress < range.Start || faultAddress >= range.End)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
writableStart = Math.Min(writableStart, range.Start);
|
||||
writableEnd = Math.Max(writableEnd, range.End);
|
||||
}
|
||||
|
||||
if (writableStart == ulong.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ranges are page-aligned and may overlap (font atlases and other
|
||||
// suballocations commonly share pages). Unprotecting one range also
|
||||
// makes every overlapping tracked page writable. Expand to the full
|
||||
// transitive overlap and dirty/disarm every owner, otherwise only the
|
||||
// first dictionary entry observes the write and the others retain a
|
||||
// stale cached texture indefinitely.
|
||||
var expanded = true;
|
||||
while (expanded)
|
||||
{
|
||||
expanded = false;
|
||||
for (var index = 0; index < ranges.Length; index++)
|
||||
{
|
||||
var range = ranges[index];
|
||||
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = Math.Min(writableStart, range.Start);
|
||||
var end = Math.Max(writableEnd, range.End);
|
||||
if (start != writableStart || end != writableEnd)
|
||||
{
|
||||
writableStart = start;
|
||||
writableEnd = end;
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var needsUnprotect = false;
|
||||
for (var index = 0; index < ranges.Length; index++)
|
||||
{
|
||||
var range = ranges[index];
|
||||
if (range.Start < writableEnd && range.End > writableStart &&
|
||||
Volatile.Read(ref range.Armed) != 0)
|
||||
{
|
||||
needsUnprotect = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUnprotect &&
|
||||
Mprotect(
|
||||
(nint)writableStart,
|
||||
(nuint)(writableEnd - writableStart),
|
||||
ProtRead | ProtWrite) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < ranges.Length; index++)
|
||||
{
|
||||
var range = ranges[index];
|
||||
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||
if (wasArmed &&
|
||||
range.TraceLifetime &&
|
||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||
{
|
||||
// Signal context: capture preallocated scalar fields only.
|
||||
// Formatting and I/O are deferred to a locked safe path.
|
||||
range.FirstCpuWriteTraceSequence =
|
||||
Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||
range.FirstCpuWriteTimestampNanoseconds = GetMonotonicNanoseconds();
|
||||
range.FirstCpuWriteAddress = faultAddress;
|
||||
range.FirstCpuWritePage = faultAddress & ~0xFFFUL;
|
||||
Volatile.Write(ref range.PendingFirstCpuWrite, 1);
|
||||
Volatile.Write(ref range.FirstCpuWriteSeen, 2);
|
||||
}
|
||||
|
||||
Volatile.Write(ref range.Dirty, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ArmLocked(TrackedRange range, string operation)
|
||||
{
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
if (Interlocked.Exchange(ref range.Armed, 1) == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A new publication/rearm starts a new first-write lifetime.
|
||||
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
||||
var failed = Mprotect(
|
||||
(nint)range.Start,
|
||||
(nuint)(range.End - range.Start),
|
||||
ProtRead) != 0;
|
||||
if (failed)
|
||||
{
|
||||
Volatile.Write(ref range.Armed, 0);
|
||||
}
|
||||
|
||||
if (range.TraceLifetime)
|
||||
{
|
||||
TraceLifetime(
|
||||
range,
|
||||
failed ? $"{operation}-failed-errno-{Marshal.GetLastPInvokeError()}" : operation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisarmLocked(TrackedRange range, string operation)
|
||||
{
|
||||
FlushPendingFirstCpuWrite(range);
|
||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
|
||||
if (wasArmed)
|
||||
{
|
||||
_ = Mprotect(
|
||||
(nint)range.Start,
|
||||
(nuint)(range.End - range.Start),
|
||||
ProtRead | ProtWrite);
|
||||
}
|
||||
|
||||
if (range.TraceLifetime)
|
||||
{
|
||||
TraceLifetime(range, wasArmed ? operation : $"{operation}-already-disarmed");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebuildSnapshotLocked()
|
||||
{
|
||||
_rangeSnapshot = _rangesByAddress.Values.ToArray();
|
||||
}
|
||||
|
||||
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
||||
{
|
||||
const ulong pageMask = 0xFFFUL;
|
||||
var start = address & ~pageMask;
|
||||
var end = (address + byteCount + pageMask) & ~pageMask;
|
||||
return (start, end - start);
|
||||
}
|
||||
|
||||
private static bool ShouldTraceRange(ulong start, ulong end)
|
||||
{
|
||||
if (_lifetimeTraceFilter.Wildcard)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var addresses = _lifetimeTraceFilter.Addresses;
|
||||
for (var index = 0; index < addresses.Length; index++)
|
||||
{
|
||||
if (addresses[index] >= start && addresses[index] < end)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static (bool Wildcard, ulong[] Addresses) ParseAddressList(string? addresses)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(addresses))
|
||||
{
|
||||
return (false, []);
|
||||
}
|
||||
|
||||
var parsedAddresses = new List<ulong>();
|
||||
foreach (var token in addresses.Split(
|
||||
[',', ';', ' ', '\t'],
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (token == "*")
|
||||
{
|
||||
return (true, []);
|
||||
}
|
||||
|
||||
var span = token.AsSpan();
|
||||
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
span = span[2..];
|
||||
}
|
||||
|
||||
if (ulong.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
parsedAddresses.Add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, parsedAddresses.ToArray());
|
||||
}
|
||||
|
||||
private static bool ShouldTraceSource(string source)
|
||||
{
|
||||
if (_lifetimeSourceTraceFilter.Wildcard)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Array.IndexOf(_lifetimeSourceTraceFilter.Sources, source) >= 0;
|
||||
}
|
||||
|
||||
private static (bool Wildcard, string[] Sources) ParseSourceList(string? sources)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sources))
|
||||
{
|
||||
return (false, []);
|
||||
}
|
||||
|
||||
var parsedSources = new List<string>();
|
||||
foreach (var token in sources.Split(
|
||||
[',', ';'],
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (token == "*")
|
||||
{
|
||||
return (true, []);
|
||||
}
|
||||
|
||||
parsedSources.Add(token);
|
||||
}
|
||||
|
||||
return (false, parsedSources.ToArray());
|
||||
}
|
||||
|
||||
private static void FlushPendingFirstCpuWrite(TrackedRange range)
|
||||
{
|
||||
var spin = new SpinWait();
|
||||
while (Volatile.Read(ref range.FirstCpuWriteSeen) == 1)
|
||||
{
|
||||
spin.SpinOnce();
|
||||
}
|
||||
|
||||
if (!range.TraceLifetime || Interlocked.Exchange(ref range.PendingFirstCpuWrite, 0) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TraceLifetime(
|
||||
range,
|
||||
"first-cpu-write-disarm",
|
||||
range.FirstCpuWriteAddress,
|
||||
range.FirstCpuWritePage,
|
||||
range.FirstCpuWriteTraceSequence,
|
||||
range.FirstCpuWriteTimestampNanoseconds);
|
||||
}
|
||||
|
||||
private static void TraceLifetime(
|
||||
TrackedRange range,
|
||||
string operation,
|
||||
ulong faultAddress = 0,
|
||||
ulong faultPage = 0,
|
||||
long traceSequence = 0,
|
||||
long timestampNanoseconds = 0)
|
||||
{
|
||||
if (traceSequence == 0)
|
||||
{
|
||||
traceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||
}
|
||||
|
||||
if (timestampNanoseconds == 0)
|
||||
{
|
||||
timestampNanoseconds = GetMonotonicNanoseconds();
|
||||
}
|
||||
|
||||
var elapsedMilliseconds =
|
||||
(timestampNanoseconds - _lifetimeTraceEpochNanoseconds) / 1_000_000.0;
|
||||
Console.Error.WriteLine(
|
||||
$"[WT][LIFETIME] seq={traceSequence} t_ms={elapsedMilliseconds:F3} " +
|
||||
$"event={operation} source_seq={range.SourceSequence} source='{range.Source}' " +
|
||||
$"requested=0x{range.Address:X16}+0x{range.ByteCount:X} " +
|
||||
$"range=0x{range.Start:X16}..0x{range.End:X16} " +
|
||||
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
|
||||
}
|
||||
|
||||
private static long GetMonotonicNanoseconds()
|
||||
{
|
||||
Timespec time;
|
||||
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
||||
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@ public interface IGuestThreadScheduler
|
||||
{
|
||||
bool SupportsGuestContextTransfer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Associates a pthread identity created on the primary guest executor
|
||||
/// with its live CPU context. Primary execution does not pass through
|
||||
/// TryStartThread, but kernel exception delivery must still be able to
|
||||
/// target it.
|
||||
/// </summary>
|
||||
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
@@ -53,6 +61,19 @@ public interface IGuestThreadScheduler
|
||||
|
||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new guest scheduling priority to a live thread, mapping it
|
||||
/// onto the host thread if one is running. Returns false when the thread
|
||||
/// handle is unknown.
|
||||
/// </summary>
|
||||
bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority);
|
||||
|
||||
/// <summary>
|
||||
/// Records a new affinity mask for a guest thread and re-applies it to
|
||||
/// the host thread where the platform supports it.
|
||||
/// </summary>
|
||||
bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask);
|
||||
|
||||
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
||||
|
||||
bool TryCallGuestFunction(
|
||||
@@ -65,11 +86,36 @@ public interface IGuestThreadScheduler
|
||||
string reason,
|
||||
out string? error);
|
||||
|
||||
bool TryCallGuestFunction(
|
||||
CpuContext callerContext,
|
||||
ulong entryPoint,
|
||||
ulong arg0,
|
||||
ulong arg1,
|
||||
ulong arg2,
|
||||
ulong stackAddress,
|
||||
ulong stackSize,
|
||||
string reason,
|
||||
out ulong returnValue,
|
||||
out string? error);
|
||||
|
||||
bool TryCallGuestContinuation(
|
||||
CpuContext callerContext,
|
||||
GuestCpuContinuation continuation,
|
||||
string reason,
|
||||
out string? error);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously invokes an installed kernel exception handler as the
|
||||
/// target guest thread. This is used by IL2CPP's stop-the-world collector:
|
||||
/// the handler acknowledges suspension and may remain blocked until the
|
||||
/// collecting thread resumes it.
|
||||
/// </summary>
|
||||
bool TryRaiseGuestException(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
ulong handler,
|
||||
int exceptionType,
|
||||
out string? error);
|
||||
}
|
||||
|
||||
public readonly record struct GuestImportCallFrame(
|
||||
@@ -94,13 +140,34 @@ public readonly record struct GuestCpuContinuation(
|
||||
ulong Rdi,
|
||||
ulong R8,
|
||||
ulong R9,
|
||||
ulong R10,
|
||||
ulong R11,
|
||||
ulong R12,
|
||||
ulong R13,
|
||||
ulong R14,
|
||||
ulong R15);
|
||||
ulong R15,
|
||||
ushort FpuControlWord,
|
||||
uint Mxcsr,
|
||||
bool RestoreFullFpuState);
|
||||
|
||||
public static class GuestThreadExecution
|
||||
{
|
||||
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
private readonly Func<int> _resume;
|
||||
private readonly Func<bool> _tryWake;
|
||||
|
||||
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
|
||||
{
|
||||
_resume = resume;
|
||||
_tryWake = tryWake;
|
||||
}
|
||||
|
||||
public int Resume() => _resume();
|
||||
|
||||
public bool TryWake() => _tryWake();
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentGuestThreadHandle;
|
||||
|
||||
@@ -246,6 +313,23 @@ public static class GuestThreadExecution
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compatibility bridge for exports that still describe blocked work as a
|
||||
// resume/wake delegate pair. New hot paths should provide an
|
||||
// IGuestThreadBlockWaiter directly to avoid allocating closures.
|
||||
public static bool RequestCurrentThreadBlock(
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey,
|
||||
Func<int> resumeHandler,
|
||||
Func<bool> wakeHandler,
|
||||
long blockDeadlineTimestamp = 0) =>
|
||||
RequestCurrentThreadBlock(
|
||||
context,
|
||||
reason,
|
||||
wakeKey,
|
||||
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
|
||||
blockDeadlineTimestamp);
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||
@@ -314,6 +398,27 @@ public static class GuestThreadExecution
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler,
|
||||
out long blockDeadlineTimestamp)
|
||||
{
|
||||
var consumed = TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out wakeKey,
|
||||
out var waiter,
|
||||
out blockDeadlineTimestamp);
|
||||
resumeHandler = waiter is null ? null : waiter.Resume;
|
||||
wakeHandler = waiter is null ? null : waiter.TryWake;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||
{
|
||||
if (timeout <= TimeSpan.Zero)
|
||||
@@ -360,10 +465,15 @@ public static class GuestThreadExecution
|
||||
context[CpuRegister.Rdi],
|
||||
context[CpuRegister.R8],
|
||||
context[CpuRegister.R9],
|
||||
context[CpuRegister.R10],
|
||||
context[CpuRegister.R11],
|
||||
context[CpuRegister.R12],
|
||||
context[CpuRegister.R13],
|
||||
context[CpuRegister.R14],
|
||||
context[CpuRegister.R15]);
|
||||
context[CpuRegister.R15],
|
||||
context.FpuControlWord,
|
||||
context.Mxcsr,
|
||||
RestoreFullFpuState: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide registry of ELF PT_TLS templates and per-thread dynamic thread
|
||||
/// vectors. AMD64 uses TLS Variant II: startup/static module blocks precede the
|
||||
/// thread pointer, while modules introduced after a thread was initialized are
|
||||
/// represented by separately allocated DTV entries.
|
||||
/// </summary>
|
||||
public static class GuestTlsTemplate
|
||||
{
|
||||
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||
// modules can require more than one host page of Variant II static TLS;
|
||||
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||
private static readonly object _gate = new();
|
||||
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||
private static ulong _staticTlsSize;
|
||||
private static ulong _maximumAlignment = 1;
|
||||
private static ulong _generation;
|
||||
|
||||
static GuestTlsTemplate()
|
||||
{
|
||||
RunTlsLayoutSelfChecks();
|
||||
}
|
||||
|
||||
private sealed class ModuleTemplate
|
||||
{
|
||||
public required ulong ModuleId { get; init; }
|
||||
public required byte[] InitImage { get; init; }
|
||||
public required ulong MemorySize { get; init; }
|
||||
public required ulong Alignment { get; init; }
|
||||
public required ulong AlignmentBias { get; init; }
|
||||
public required ulong StaticOffset { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ThreadDtv
|
||||
{
|
||||
public ulong Generation { get; set; }
|
||||
public Dictionary<ulong, DtvEntry> Entries { get; } = new();
|
||||
public nint DtvAllocationBase { get; set; }
|
||||
public ulong DtvAddress { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DtvEntry
|
||||
{
|
||||
public required ulong Address { get; init; }
|
||||
public nint AllocationBase { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Main executable's initialized PT_TLS bytes.</summary>
|
||||
public static byte[] InitImage
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _modules.TryGetValue(1, out var module)
|
||||
? (byte[])module.InitImage.Clone()
|
||||
: [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Aligned size of the main executable's static TLS block.</summary>
|
||||
public static ulong BlockSize
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _modules.TryGetValue(1, out var module)
|
||||
? module.StaticOffset
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Main executable's PT_TLS alignment.</summary>
|
||||
public static ulong Alignment
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _modules.TryGetValue(1, out var module)
|
||||
? module.Alignment
|
||||
: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Total Variant II static TLS span required below the TCB.</summary>
|
||||
public static ulong StaticTlsSize
|
||||
{
|
||||
get { lock (_gate) { return _staticTlsSize; } }
|
||||
}
|
||||
|
||||
/// <summary>Largest PT_TLS alignment required by the registered modules.</summary>
|
||||
public static ulong MaximumAlignment
|
||||
{
|
||||
get { lock (_gate) { return _maximumAlignment; } }
|
||||
}
|
||||
|
||||
/// <summary>Current DTV generation, incremented for every registered module.</summary>
|
||||
public static ulong Generation
|
||||
{
|
||||
get { lock (_gate) { return _generation; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backwards-compatible main-module registration entry point.
|
||||
/// </summary>
|
||||
public static void Set(ReadOnlySpan<byte> initImage, ulong memorySize, ulong alignment)
|
||||
{
|
||||
Reset();
|
||||
RegisterModule(1, initImage, memorySize, alignment, alignmentBias: 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one module's PT_TLS template and allocates its Variant II
|
||||
/// static offset. Modules must be registered in loader module-ID order.
|
||||
/// </summary>
|
||||
public static ulong RegisterModule(
|
||||
ulong moduleId,
|
||||
ReadOnlySpan<byte> initImage,
|
||||
ulong memorySize,
|
||||
ulong alignment,
|
||||
ulong alignmentBias = 0)
|
||||
{
|
||||
if (moduleId == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(moduleId));
|
||||
}
|
||||
|
||||
var normalizedAlignment = alignment == 0 ? 1 : alignment;
|
||||
if ((normalizedAlignment & (normalizedAlignment - 1)) != 0)
|
||||
{
|
||||
throw new InvalidDataException($"PT_TLS alignment 0x{alignment:X} is not a power of two.");
|
||||
}
|
||||
if (memorySize > int.MaxValue || (ulong)initImage.Length > memorySize)
|
||||
{
|
||||
throw new InvalidDataException("PT_TLS template size is invalid or exceeds the supported process limit.");
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_modules.TryGetValue(moduleId, out var existing))
|
||||
{
|
||||
if (existing.MemorySize != memorySize ||
|
||||
existing.Alignment != normalizedAlignment ||
|
||||
existing.AlignmentBias != alignmentBias ||
|
||||
!existing.InitImage.AsSpan().SequenceEqual(initImage))
|
||||
{
|
||||
throw new InvalidOperationException($"TLS module {moduleId} was registered with a different template.");
|
||||
}
|
||||
|
||||
return existing.StaticOffset;
|
||||
}
|
||||
|
||||
// FreeBSD/AMD64 Variant II: choose the smallest offset satisfying
|
||||
// offset - previous >= size and (-offset) % align == p_vaddr % align.
|
||||
var staticOffset = CalculateStaticOffset(
|
||||
_staticTlsSize,
|
||||
memorySize,
|
||||
normalizedAlignment,
|
||||
alignmentBias);
|
||||
if (staticOffset > StartupStaticTlsReservation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Static TLS requires 0x{staticOffset:X} bytes, but startup maps only " +
|
||||
$"0x{StartupStaticTlsReservation:X} bytes below the thread pointer.");
|
||||
}
|
||||
_modules.Add(moduleId, new ModuleTemplate
|
||||
{
|
||||
ModuleId = moduleId,
|
||||
InitImage = initImage.ToArray(),
|
||||
MemorySize = memorySize,
|
||||
Alignment = normalizedAlignment,
|
||||
AlignmentBias = alignmentBias,
|
||||
StaticOffset = staticOffset,
|
||||
});
|
||||
_staticTlsSize = staticOffset;
|
||||
_maximumAlignment = Math.Max(_maximumAlignment, normalizedAlignment);
|
||||
_generation++;
|
||||
return staticOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the static TP-relative block offset for a module.</summary>
|
||||
public static bool TryGetStaticOffset(ulong moduleId, out ulong staticOffset)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_modules.TryGetValue(moduleId, out var module))
|
||||
{
|
||||
staticOffset = module.StaticOffset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
staticOffset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Clears all module templates and frees dynamic DTV blocks.</summary>
|
||||
public static void Reset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var dtv in _threadDtvs.Values)
|
||||
{
|
||||
FreeDynamicEntries(dtv);
|
||||
}
|
||||
|
||||
_threadDtvs.Clear();
|
||||
_modules.Clear();
|
||||
_staticTlsSize = 0;
|
||||
_maximumAlignment = 1;
|
||||
_generation++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes every currently registered startup TLS block for a thread
|
||||
/// and records their addresses in that thread's DTV.
|
||||
/// </summary>
|
||||
public static void SeedThreadBlock(CpuContext context, ulong threadPointer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
if (threadPointer == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_threadDtvs.Remove(threadPointer, out var oldDtv))
|
||||
{
|
||||
FreeDynamicEntries(oldDtv);
|
||||
}
|
||||
|
||||
var dtv = new ThreadDtv();
|
||||
_threadDtvs.Add(threadPointer, dtv);
|
||||
foreach (var module in _modules.Values)
|
||||
{
|
||||
dtv.Entries[module.ModuleId] = CreateStaticOrDynamicEntry(context, threadPointer, module);
|
||||
}
|
||||
RebuildGuestDtv(context, threadPointer, dtv);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the DTV lookup used by <c>__tls_get_addr</c>. Unknown module
|
||||
/// IDs and offsets outside the module's TLS image are rejected with zero.
|
||||
/// </summary>
|
||||
public static ulong ResolveAddress(CpuContext context, ulong moduleId, ulong offset)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
if (context.FsBase == 0 || moduleId == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_modules.TryGetValue(moduleId, out var module) || offset >= module.MemorySize)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!_threadDtvs.TryGetValue(context.FsBase, out var dtv))
|
||||
{
|
||||
dtv = new ThreadDtv();
|
||||
_threadDtvs.Add(context.FsBase, dtv);
|
||||
foreach (var startupModule in _modules.Values)
|
||||
{
|
||||
dtv.Entries[startupModule.ModuleId] = CreateStaticOrDynamicEntry(context, context.FsBase, startupModule);
|
||||
}
|
||||
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||
}
|
||||
|
||||
var entryAdded = false;
|
||||
if (!dtv.Entries.TryGetValue(moduleId, out var entry))
|
||||
{
|
||||
// This module appeared after the thread's startup TLS layout was
|
||||
// seeded. Model rtld's lazy DTV allocation instead of assuming
|
||||
// unused space exists below the already-live thread pointer.
|
||||
entry = CreateDynamicEntry(module);
|
||||
dtv.Entries.Add(moduleId, entry);
|
||||
entryAdded = true;
|
||||
}
|
||||
|
||||
if (entryAdded || dtv.Generation != _generation)
|
||||
{
|
||||
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||
}
|
||||
|
||||
return checked(entry.Address + offset);
|
||||
}
|
||||
}
|
||||
|
||||
private static DtvEntry CreateStaticOrDynamicEntry(
|
||||
CpuContext context,
|
||||
ulong threadPointer,
|
||||
ModuleTemplate module)
|
||||
{
|
||||
if (threadPointer >= module.StaticOffset)
|
||||
{
|
||||
var address = threadPointer - module.StaticOffset;
|
||||
var zeroImage = module.MemorySize == 0 ? [] : new byte[(int)module.MemorySize];
|
||||
if ((zeroImage.Length == 0 || context.Memory.TryWrite(address, zeroImage)) &&
|
||||
(module.InitImage.Length == 0 || context.Memory.TryWrite(address, module.InitImage)))
|
||||
{
|
||||
return new DtvEntry { Address = address };
|
||||
}
|
||||
}
|
||||
|
||||
return CreateDynamicEntry(module);
|
||||
}
|
||||
|
||||
private static DtvEntry CreateDynamicEntry(ModuleTemplate module)
|
||||
{
|
||||
var size = Math.Max(1UL, module.MemorySize);
|
||||
var allocationSize = checked(size + module.Alignment - 1);
|
||||
if (allocationSize > int.MaxValue)
|
||||
{
|
||||
throw new OutOfMemoryException("TLS module allocation exceeds the supported host allocation size.");
|
||||
}
|
||||
|
||||
var allocationBase = Marshal.AllocHGlobal((int)allocationSize);
|
||||
var alignedAddress = AlignUp(unchecked((ulong)allocationBase), module.Alignment);
|
||||
Marshal.Copy(new byte[(int)size], 0, unchecked((nint)alignedAddress), (int)size);
|
||||
if (module.InitImage.Length != 0)
|
||||
{
|
||||
Marshal.Copy(module.InitImage, 0, unchecked((nint)alignedAddress), module.InitImage.Length);
|
||||
}
|
||||
|
||||
return new DtvEntry
|
||||
{
|
||||
Address = alignedAddress,
|
||||
AllocationBase = allocationBase,
|
||||
};
|
||||
}
|
||||
|
||||
private static void FreeDynamicEntries(ThreadDtv dtv)
|
||||
{
|
||||
foreach (var entry in dtv.Entries.Values)
|
||||
{
|
||||
if (entry.AllocationBase != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(entry.AllocationBase);
|
||||
}
|
||||
}
|
||||
if (dtv.DtvAllocationBase != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||
dtv.DtvAllocationBase = 0;
|
||||
dtv.DtvAddress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebuildGuestDtv(CpuContext context, ulong threadPointer, ThreadDtv dtv)
|
||||
{
|
||||
var maximumModuleId = _modules.Count == 0 ? 0UL : _modules.Keys.Max();
|
||||
var byteSize = checked(2UL * sizeof(ulong) + maximumModuleId * sizeof(ulong));
|
||||
if (byteSize > int.MaxValue)
|
||||
{
|
||||
throw new OutOfMemoryException("Guest DTV exceeds the supported host allocation size.");
|
||||
}
|
||||
|
||||
var newAllocation = Marshal.AllocHGlobal((int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||
var newAddress = unchecked((ulong)newAllocation);
|
||||
Marshal.Copy(new byte[(int)Math.Max((ulong)sizeof(ulong) * 2, byteSize)], 0, newAllocation, (int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||
Marshal.WriteInt64(newAllocation, 0, unchecked((long)_generation));
|
||||
Marshal.WriteInt64(newAllocation, sizeof(ulong), unchecked((long)maximumModuleId));
|
||||
foreach (var (moduleId, entry) in dtv.Entries)
|
||||
{
|
||||
var slotOffset = checked((int)(2UL * sizeof(ulong) + (moduleId - 1) * sizeof(ulong)));
|
||||
Marshal.WriteInt64(newAllocation, slotOffset, unchecked((long)entry.Address));
|
||||
}
|
||||
|
||||
if (!context.TryWriteUInt64(threadPointer + sizeof(ulong), newAddress))
|
||||
{
|
||||
Marshal.FreeHGlobal(newAllocation);
|
||||
throw new InvalidOperationException("Failed to install the guest DTV pointer in the thread control block.");
|
||||
}
|
||||
|
||||
if (dtv.DtvAllocationBase != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||
}
|
||||
dtv.DtvAllocationBase = newAllocation;
|
||||
dtv.DtvAddress = newAddress;
|
||||
dtv.Generation = _generation;
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
{
|
||||
var mask = alignment - 1;
|
||||
return checked((value + mask) & ~mask);
|
||||
}
|
||||
|
||||
private static ulong CalculateStaticOffset(
|
||||
ulong previousOffset,
|
||||
ulong size,
|
||||
ulong alignment,
|
||||
ulong alignmentBias)
|
||||
{
|
||||
var result = checked(previousOffset + size + alignment - 1);
|
||||
return result - ((result + alignmentBias) & (alignment - 1));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void RunTlsLayoutSelfChecks()
|
||||
{
|
||||
var firstOffset = RegisterModule(1, [0x11, 0x22], 0x20, 0x10, 0);
|
||||
var secondOffset = RegisterModule(2, [0x7A], 0x18, 0x20, 8);
|
||||
Debug.Assert(firstOffset == 0x20, "First Variant II TLS offset is incorrect.");
|
||||
Debug.Assert(secondOffset >= firstOffset + 0x18, "TLS modules overlap in the static layout.");
|
||||
Debug.Assert((unchecked(0UL - secondOffset) & 0x1F) == 8, "PT_TLS virtual-address congruence was not preserved.");
|
||||
|
||||
var lateEntry = CreateDynamicEntry(_modules[2]);
|
||||
try
|
||||
{
|
||||
Debug.Assert(lateEntry.AllocationBase != 0, "A late TLS module did not receive a dynamic DTV block.");
|
||||
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address)) == 0x7A, "Late-module tdata was not initialized.");
|
||||
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address + 1)) == 0, "Late-module tbss was not zero initialized.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(lateEntry.AllocationBase);
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ public static class HleDataSymbols
|
||||
private const string LibcNeedFlagNid = "P330P3dFF68";
|
||||
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
|
||||
private const int ProgNameMaxBytes = 511;
|
||||
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBABEUL;
|
||||
// Terminator canaries reserve the low byte as NUL. Keep the process data
|
||||
// symbol and every per-thread TLS copy byte-for-byte identical.
|
||||
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBA00UL;
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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;
|
||||
@@ -28,7 +29,14 @@ public static class HostPlatform
|
||||
return new WindowsHostPlatform();
|
||||
}
|
||||
|
||||
if ((OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) &&
|
||||
RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
return new PosixHostPlatform();
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException(
|
||||
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
|
||||
"SharpEmu native guest execution requires an x86-64 process on Windows, Linux, or macOS. " +
|
||||
"On Apple Silicon, use the osx-x64 build under Rosetta 2.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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,588 @@
|
||||
// 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 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 const int KERN_SUCCESS = 0;
|
||||
|
||||
// On Darwin fixed placement is represented by the absence of
|
||||
// VM_FLAGS_ANYWHERE. Do not add VM_FLAGS_OVERWRITE: overlap must fail.
|
||||
private const int VM_FLAGS_FIXED = 0;
|
||||
|
||||
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 bool UsesMachAllocation;
|
||||
|
||||
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;
|
||||
var usesMachAllocation = false;
|
||||
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)
|
||||
{
|
||||
munmap(result, (nuint)alignedSize);
|
||||
result = MAP_FAILED;
|
||||
}
|
||||
|
||||
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
|
||||
{
|
||||
// The hint-only attempt above didn't land at the requested
|
||||
// address. mach_vm_allocate with fixed placement and no
|
||||
// overwrite flag atomically maps the requested range or
|
||||
// fails if any host mapping already owns it. Unlike
|
||||
// MAP_FIXED, it cannot clobber CLR, dyld, JIT, or Rosetta
|
||||
// memory that is absent from our shadow table.
|
||||
Trace($"exact mmap hint failed, retrying with fixed Mach allocation: addr=0x{(ulong)address:X16}");
|
||||
result = AllocateDarwinFixed(
|
||||
(nint)address,
|
||||
alignedSize,
|
||||
posixProtect);
|
||||
usesMachAllocation = result != MAP_FAILED;
|
||||
}
|
||||
|
||||
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,
|
||||
UsesMachAllocation = usesMachAllocation,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var released = region.UsesMachAllocation
|
||||
? mach_vm_deallocate(mach_task_self(), (ulong)address, region.Size) == KERN_SUCCESS
|
||||
: munmap((nint)address, (nuint)region.Size) == 0;
|
||||
if (released)
|
||||
{
|
||||
Regions.Remove((ulong)address);
|
||||
}
|
||||
|
||||
return released;
|
||||
}
|
||||
}
|
||||
|
||||
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 nint AllocateDarwinFixed(nint requestedAddress, ulong size, int posixProtect)
|
||||
{
|
||||
var address = (ulong)requestedAddress;
|
||||
var result = mach_vm_allocate(
|
||||
mach_task_self(),
|
||||
ref address,
|
||||
size,
|
||||
VM_FLAGS_FIXED);
|
||||
if (result != KERN_SUCCESS || address != (ulong)requestedAddress)
|
||||
{
|
||||
if (result == KERN_SUCCESS)
|
||||
{
|
||||
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"fixed Mach allocation failed: addr=0x{(ulong)requestedAddress:X16} " +
|
||||
$"size=0x{size:X} kern_return={result}");
|
||||
return MAP_FAILED;
|
||||
}
|
||||
|
||||
if (mprotect((nint)address, (nuint)size, posixProtect) == 0)
|
||||
{
|
||||
return (nint)address;
|
||||
}
|
||||
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||
Trace(
|
||||
$"fixed Mach allocation protection failed: addr=0x{address:X16} " +
|
||||
$"size=0x{size:X} errno={error}");
|
||||
return MAP_FAILED;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern uint mach_task_self();
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern int mach_vm_allocate(uint target, ref ulong address, ulong size, int flags);
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern int mach_vm_deallocate(uint target, ulong address, ulong size);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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. macOS only allows AppKit (and
|
||||
/// therefore GLFW windowing) on that thread, 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 other platforms
|
||||
/// <see cref="IsAvailable"/> stays false and nothing changes.
|
||||
/// </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,498 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Cross-platform host virtual memory API with Win32 semantics.
|
||||
/// On Windows this forwards directly to kernel32. On POSIX systems it is
|
||||
/// 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>
|
||||
public static unsafe class HostMemory
|
||||
{
|
||||
public const uint MEM_COMMIT = 0x1000;
|
||||
public const uint MEM_RESERVE = 0x2000;
|
||||
public const uint MEM_RELEASE = 0x8000;
|
||||
public const uint MEM_FREE_STATE = 0x10000;
|
||||
public const uint MEM_PRIVATE = 0x20000;
|
||||
|
||||
public const uint PAGE_NOACCESS = 0x01;
|
||||
public const uint PAGE_READONLY = 0x02;
|
||||
public const uint PAGE_READWRITE = 0x04;
|
||||
public const uint PAGE_EXECUTE = 0x10;
|
||||
public const uint PAGE_EXECUTE_READ = 0x20;
|
||||
public const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
private const ulong PageSize = 0x1000;
|
||||
|
||||
/// <summary>Win32 MEMORY_BASIC_INFORMATION (64-bit) layout.</summary>
|
||||
public struct BasicInfo
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Win32VirtualAlloc(address, size, allocationType, protect);
|
||||
}
|
||||
|
||||
return Posix.Alloc(address, size, allocationType, protect);
|
||||
}
|
||||
|
||||
public static bool Free(void* address, nuint size, uint freeType)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Win32VirtualFree(address, size, freeType);
|
||||
}
|
||||
|
||||
return Posix.Free(address, size, freeType);
|
||||
}
|
||||
|
||||
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Win32VirtualProtect(address, size, newProtect, out oldProtect);
|
||||
}
|
||||
|
||||
return Posix.Protect(address, size, newProtect, out oldProtect);
|
||||
}
|
||||
|
||||
public static nuint Query(void* address, out BasicInfo info)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Win32VirtualQuery(address, out info, (nuint)sizeof(BasicInfo));
|
||||
}
|
||||
|
||||
return Posix.Query(address, out info);
|
||||
}
|
||||
|
||||
public static void FlushInstructionCache(void* address, nuint size)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Win32FlushInstructionCache(Win32GetCurrentProcess(), address, size);
|
||||
return;
|
||||
}
|
||||
|
||||
// The emulator only executes x86-64 guest code, so a non-Windows host
|
||||
// is either x86-64 (including Rosetta 2 translation) with a coherent
|
||||
// instruction cache, or would need sys_icache_invalidate for a future
|
||||
// arm64 recompiler. Nothing to do today.
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "VirtualAlloc", SetLastError = true)]
|
||||
private static extern void* Win32VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "VirtualFree", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool Win32VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "VirtualProtect", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool Win32VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "VirtualQuery")]
|
||||
private static extern nuint Win32VirtualQuery(void* lpAddress, out BasicInfo lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess")]
|
||||
private static extern void* Win32GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "FlushInstructionCache")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool Win32FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
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 = Math.Min(existing.End, AlignUp((ulong)address + alignedSize, PageSize));
|
||||
if (end <= start)
|
||||
{
|
||||
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. Do
|
||||
// not walk the whole mapping page-by-page here: PS5 GPU
|
||||
// apertures can span hundreds of GiB, while protection
|
||||
// overrides are sparse. A tiny read inside such a mapping
|
||||
// otherwise performs tens of millions of dictionary
|
||||
// lookups before it can return.
|
||||
var pageProtects = region.PageProtects;
|
||||
uint protect;
|
||||
ulong runEnd;
|
||||
if (pageProtects is null || pageProtects.Count == 0)
|
||||
{
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
}
|
||||
else if (pageProtects.TryGetValue(pageAddress, out protect))
|
||||
{
|
||||
runEnd = pageAddress + PageSize;
|
||||
while (runEnd < region.End &&
|
||||
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||
nextProtect == protect)
|
||||
{
|
||||
runEnd += PageSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
foreach (var overrideAddress in pageProtects.Keys)
|
||||
{
|
||||
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||
{
|
||||
runEnd = overrideAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public interface IModuleManager
|
||||
{
|
||||
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
|
||||
/// <summary>Registers pre-built exports (the compile-time generated registry).</summary>
|
||||
int RegisterExports(IReadOnlyList<ExportedFunction> exports);
|
||||
|
||||
void Freeze();
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ public sealed class ModuleManager : IModuleManager
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private readonly HashSet<Assembly> _warmupAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||
public int RegisterExports(IReadOnlyList<ExportedFunction> exports)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
ArgumentNullException.ThrowIfNull(exports);
|
||||
|
||||
lock (_registrationGate)
|
||||
{
|
||||
@@ -27,48 +27,21 @@ public sealed class ModuleManager : IModuleManager
|
||||
throw new InvalidOperationException("Module registration is frozen.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
foreach (var export in exports)
|
||||
{
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
|
||||
if (!_dispatchTable.TryAdd(export.Nid, export.Function))
|
||||
{
|
||||
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
|
||||
if (exportAttribute is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
|
||||
if (exportInfo is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
|
||||
exportInfo.Value.LibraryName,
|
||||
exportInfo.Value.Nid,
|
||||
exportInfo.Value.ExportName,
|
||||
exportInfo.Value.Target,
|
||||
(SysAbiFunction)handler);
|
||||
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
|
||||
|
||||
registeredCount++;
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{export.Nid}' ({export.Name}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_exportTable[export.Nid] = export;
|
||||
_exportNameTable.TryAdd(export.Name, export);
|
||||
// The warm sweep in Freeze() covers every assembly that contributed a
|
||||
// handler (generated thunks resolve to their home assembly too).
|
||||
_warmupAssemblies.Add(export.Function.Method.Module.Assembly);
|
||||
registeredCount++;
|
||||
}
|
||||
|
||||
return registeredCount;
|
||||
@@ -92,7 +65,8 @@ public sealed class ModuleManager : IModuleManager
|
||||
Assembly[] assemblies;
|
||||
lock (_registrationGate)
|
||||
{
|
||||
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||
assemblies = new Assembly[_warmupAssemblies.Count];
|
||||
_warmupAssemblies.CopyTo(assemblies);
|
||||
}
|
||||
|
||||
assemblies = WithGuestReachableDependencies(assemblies);
|
||||
@@ -328,107 +302,4 @@ public sealed class ModuleManager : IModuleManager
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
|
||||
{
|
||||
ValidateSignature(method);
|
||||
|
||||
object? target = null;
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
if (!instances.TryGetValue(ownerType, out target))
|
||||
{
|
||||
target = Activator.CreateInstance(ownerType)
|
||||
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
|
||||
instances.Add(ownerType, target);
|
||||
}
|
||||
}
|
||||
|
||||
var parameterCount = method.GetParameters().Length;
|
||||
if (parameterCount == 0)
|
||||
{
|
||||
var noArg = method.IsStatic
|
||||
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
|
||||
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
|
||||
|
||||
SysAbiFunction adapter = _ => noArg();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
return method.IsStatic
|
||||
? method.CreateDelegate(typeof(SysAbiFunction))
|
||||
: method.CreateDelegate(typeof(SysAbiFunction), target!);
|
||||
}
|
||||
|
||||
private static void ValidateSignature(MethodInfo method)
|
||||
{
|
||||
if (method.ReturnType != typeof(int))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
|
||||
}
|
||||
|
||||
private static ExportInfo? ResolveExportInfo(
|
||||
SysAbiExportAttribute exportAttribute,
|
||||
MethodInfo method,
|
||||
Generation generation,
|
||||
ISymbolCatalog? symbolCatalog)
|
||||
{
|
||||
var target = exportAttribute.Target == Generation.None
|
||||
? generation
|
||||
: exportAttribute.Target;
|
||||
if ((target & generation) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nid = exportAttribute.Nid;
|
||||
var exportName = exportAttribute.ExportName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
|
||||
{
|
||||
nid = byName.Nid;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
|
||||
{
|
||||
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
|
||||
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nid))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
exportName = method.Name;
|
||||
}
|
||||
|
||||
if ((target & generation) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
|
||||
return new ExportInfo(nid, exportName, libraryName, target);
|
||||
}
|
||||
|
||||
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
|
||||
}
|
||||
|
||||
@@ -12,11 +12,61 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
<!-- Forces build ordering for the aerolib task below; loaded as a build component,
|
||||
never a runtime dependency. -->
|
||||
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- aerolib.bin (the runtime NID -> name catalog) is derived data: generated here
|
||||
from scripts/ps5_names.txt via the same Ps5Nid implementation the analyzers use,
|
||||
embedded from the intermediate directory, and never committed. Replaces
|
||||
scripts/generate_aerolib_binary.py. -->
|
||||
<PropertyGroup>
|
||||
<SharpEmuAerolibNamesFile>$(MSBuildThisFileDirectory)..\..\scripts\ps5_names.txt</SharpEmuAerolibNamesFile>
|
||||
<SharpEmuAerolibTaskAssembly>$(MSBuildThisFileDirectory)..\..\artifacts\bin\$(Configuration)\netstandard2.0\SharpEmu.SourceGenerators.dll</SharpEmuAerolibTaskAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<UsingTask TaskName="SharpEmu.SourceGenerators.GenerateAerolibBinaryTask"
|
||||
TaskFactory="TaskHostFactory"
|
||||
AssemblyFile="$(SharpEmuAerolibTaskAssembly)" />
|
||||
|
||||
<!-- Runs after ResolveProjectReferences (which builds the task assembly) and before
|
||||
resource processing embeds the output. Skipped for design-time builds: the IDE
|
||||
resolves project references without compiling them, so on a fresh clone the task
|
||||
assembly does not exist yet and loading it would break the design-time build. -->
|
||||
<Target Name="GenerateAerolibBinary"
|
||||
DependsOnTargets="ResolveProjectReferences"
|
||||
Inputs="$(SharpEmuAerolibNamesFile);$(SharpEmuAerolibTaskAssembly)"
|
||||
Outputs="$(IntermediateOutputPath)aerolib.bin">
|
||||
<GenerateAerolibBinaryTask NamesFile="$(SharpEmuAerolibNamesFile)"
|
||||
OutputFile="$(IntermediateOutputPath)aerolib.bin" />
|
||||
</Target>
|
||||
|
||||
<!-- The resource item is created inside a target (not statically) so design-time
|
||||
builds never reference a file that was not generated, and in a separate target
|
||||
from the generation so an up-to-date skip of GenerateAerolibBinary cannot drop
|
||||
the item (an up-to-date target skips its whole body, ItemGroups included).
|
||||
Hooked before AssignTargetPaths: dynamic EmbeddedResource items added later
|
||||
than that miss the resource pipeline entirely. -->
|
||||
<Target Name="EmbedAerolibBinary"
|
||||
BeforeTargets="AssignTargetPaths"
|
||||
DependsOnTargets="GenerateAerolibBinary"
|
||||
Condition="'$(DesignTimeBuild)' != 'true'">
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(IntermediateOutputPath)aerolib.bin"
|
||||
LogicalName="SharpEmu.HLE.Aerolib.aerolib.bin"
|
||||
Visible="false" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6301
-1493
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static partial class AgcExports
|
||||
{
|
||||
// Exact Gen5 primary register defaults originally reverse-engineered by Kyty (MIT).
|
||||
// Hashes, offsets, and values match the tables consumed by the PS5 AGC SDK.
|
||||
private static RegisterDefaultGroup[] CreatePrimaryRegisterDefaults() =>
|
||||
[
|
||||
// context register groups
|
||||
new(0u, 0u, 0xE24F806Du, [new(0x202u, 0x00CC0010u)]), // CB_COLOR_CONTROL
|
||||
new(0u, 1u, 0xF6C28182u, [new(0x109u, 0x00000000u)]), // CB_DCC_CONTROL
|
||||
new(0u, 2u, 0x6F6E55A5u, [new(0x104u, 0x00000000u)]), // CB_RMI_GL2_CACHE_CONTROL
|
||||
new(0u, 3u, 0x0BC65DA4u, [new(0x08Fu, 0x00000000u)]), // CB_SHADER_MASK
|
||||
new(0u, 4u, 0x9E5AD592u, [new(0x08Eu, 0x0000000Fu)]), // CB_TARGET_MASK
|
||||
new(0u, 5u, 0xBB513B98u, [new(0x2DCu, 0x0000AA00u)]), // DB_ALPHA_TO_MASK
|
||||
new(0u, 6u, 0xAB64B23Bu, [new(0x001u, 0x00000000u)]), // DB_COUNT_CONTROL
|
||||
new(0u, 7u, 0x53C39964u, [new(0x200u, 0x00000000u)]), // DB_DEPTH_CONTROL
|
||||
new(0u, 8u, 0x01396B11u, [new(0x201u, 0x00000000u)]), // DB_EQAA
|
||||
new(0u, 9u, 0x7D42019Au, [new(0x000u, 0x00000000u)]), // DB_RENDER_CONTROL
|
||||
new(0u, 10u, 0x3548F523u, [new(0x006u, 0x00000000u)]), // PS_SHADER_SAMPLE_EXCLUSION_MASK
|
||||
new(0u, 11u, 0xF43AD28Au, [new(0x01Fu, 0x00000000u)]), // DB_RMI_L2_CACHE_CONTROL
|
||||
new(0u, 12u, 0x6DE4C312u, [new(0x203u, 0x00000000u)]), // DB_SHADER_CONTROL
|
||||
new(0u, 13u, 0x00A77AE0u, [new(0x2B0u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE0
|
||||
new(0u, 14u, 0x00A779B7u, [new(0x2B1u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE1
|
||||
new(0u, 15u, 0x5100100Cu, [new(0x10Cu, 0x00000000u)]), // DB_STENCILREFMASK
|
||||
new(0u, 16u, 0x59958BBAu, [new(0x10Du, 0x00000000u)]), // DB_STENCILREFMASK_BF
|
||||
new(0u, 17u, 0x0C06F17Cu, [new(0x10Bu, 0x00000000u)]), // DB_STENCIL_CONTROL
|
||||
new(0u, 18u, 0x6F104B72u, [new(0x1FFu, 0x00000000u)]), // GE_MAX_OUTPUT_PER_SUBGROUP
|
||||
new(0u, 19u, 0x25C70D9Cu, [new(0x204u, 0x00000000u)]), // PA_CL_CLIP_CNTL
|
||||
new(0u, 20u, 0x3881201Eu, [new(0x20Du, 0x00000000u)]), // PA_CL_OBJPRIM_ID_CNTL
|
||||
new(0u, 21u, 0x09AFDDAFu, [new(0x206u, 0x0000043Fu)]), // PA_CL_VTE_CNTL
|
||||
new(0u, 22u, 0x367D63CFu, [new(0x2F8u, 0x00000000u)]), // PA_SC_AA_CONFIG
|
||||
new(0u, 23u, 0x43707DB8u, [new(0x083u, 0x0000FFFFu)]), // PA_SC_CLIPRECT_RULE
|
||||
new(0u, 24u, 0xF6AE26BAu, [new(0x313u, 0x00000000u)]), // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL
|
||||
new(0u, 25u, 0x1B917652u, [new(0x800003FEu, 0x00000000u)]), // PA_SC_FSR_ENABLE
|
||||
new(0u, 26u, 0x94B1E4F7u, [new(0x0EAu, 0x00000000u)]), // PA_SC_HORIZ_GRID
|
||||
new(0u, 27u, 0xE3661B6Cu, [new(0x0E9u, 0x00000000u)]), // PA_SC_LEFT_VERT_GRID
|
||||
new(0u, 28u, 0x1EB8D73Au, [new(0x292u, 0x00000002u)]), // PA_SC_MODE_CNTL_0
|
||||
new(0u, 29u, 0x15051FA3u, [new(0x293u, 0x00000000u)]), // PA_SC_MODE_CNTL_1
|
||||
new(0u, 30u, 0x9C51A7F1u, [new(0x0E8u, 0x00000000u)]), // PA_SC_RIGHT_VERT_GRID
|
||||
new(0u, 31u, 0xA20EFC70u, [new(0x080u, 0x00000000u)]), // PA_SC_WINDOW_OFFSET
|
||||
new(0u, 32u, 0x0EC09F6Eu, [new(0x211u, 0x00000000u)]), // PA_STATE_STEREO_X
|
||||
new(0u, 33u, 0x34A7D6D3u, [new(0x210u, 0x00000000u)]), // PA_STEREO_CNTL
|
||||
new(0u, 34u, 0xCE831B94u, [new(0x08Du, 0x00000000u)]), // PA_SU_HARDWARE_SCREEN_OFFSET
|
||||
new(0u, 35u, 0x5CC72A74u, [new(0x282u, 0x00000008u)]), // PA_SU_LINE_CNTL
|
||||
new(0u, 36u, 0x3B77713Cu, [new(0x281u, 0xFFFF0000u)]), // PA_SU_POINT_MINMAX
|
||||
new(0u, 37u, 0x40F64410u, [new(0x280u, 0x00080008u)]), // PA_SU_POINT_SIZE
|
||||
new(0u, 38u, 0x69441268u, [new(0x2DFu, 0x00000000u)]), // PA_SU_POLY_OFFSET_CLAMP
|
||||
new(0u, 39u, 0x2E418B83u, [new(0x2DEu, 0x000001E9u)]), // PA_SU_POLY_OFFSET_DB_FMT_CNTL
|
||||
new(0u, 40u, 0xA00D0C8Du, [new(0x205u, 0x00000240u)]), // PA_SU_SC_MODE_CNTL
|
||||
new(0u, 41u, 0xB1289FB3u, [new(0x20Cu, 0x00000001u)]), // PA_SU_SMALL_PRIM_FILTER_CNTL
|
||||
new(0u, 42u, 0x144832FBu, [new(0x2F9u, 0x0000002Du)]), // PA_SU_VTX_CNTL
|
||||
new(0u, 43u, 0x9890D9FAu, [new(0x1BAu, 0x00000000u)]), // SPI_TMPRING_SIZE
|
||||
new(0u, 44u, 0x9016FAF1u, [new(0x2A6u, 0x00000000u)]), // VGT_DRAW_PAYLOAD_CNTL
|
||||
new(0u, 45u, 0x4B73CE27u, [new(0x2CEu, 0x00000400u)]), // VGT_GS_MAX_VERT_OUT
|
||||
new(0u, 46u, 0x5F5A3E7Bu, [new(0x29Bu, 0x00000002u)]), // VGT_GS_OUT_PRIM_TYPE
|
||||
new(0u, 47u, 0xD4AF3A51u, [new(0x2D6u, 0x00000000u)]), // VGT_LS_HS_CONFIG
|
||||
new(0u, 48u, 0x6CF4F543u, [new(0x2A3u, 0xFFFFFFFFu)]), // VGT_PRIMITIVEID_RESET
|
||||
new(0u, 49u, 0x5FB86CCBu, [new(0x2A1u, 0x00000000u)]), // VGT_PRIMITIVEID_EN
|
||||
new(0u, 50u, 0xEDEFA188u, [new(0x2ADu, 0x00000000u)]), // VGT_REUSE_OFF
|
||||
new(0u, 51u, 0xD0DE9EE6u, [new(0x2D5u, 0x00000000u)]), // VGT_SHADER_STAGES_EN
|
||||
new(0u, 52u, 0xC5831803u, [new(0x2D4u, 0x88101000u)]), // VGT_TESS_DISTRIBUTION
|
||||
new(0u, 53u, 0x8E6DE84Bu, [new(0x2DBu, 0x00000000u)]), // VGT_TF_PARAM
|
||||
new(0u, 54u, 0xD0771662u, [new(0x2F5u, 0x00000000u), new(0x2F6u, 0x00000000u)]), // PA_SC_CENTROID_PRIORITY_0, PA_SC_CENTROID_PRIORITY_1
|
||||
new(0u, 55u, 0x569F7444u, [new(0x2FEu, 0x00000000u)]), // PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_0
|
||||
new(0u, 56u, 0x5C6637CDu, [new(0x30Eu, 0xFFFFFFFFu), new(0x30Fu, 0xFFFFFFFFu)]), // PA_SC_AA_MASK_X0Y0_X1Y0, PA_SC_AA_MASK_X0Y1_X1Y1
|
||||
new(0u, 57u, 0xCAE3E690u, [new(0x311u, 0x00000002u), new(0x312u, 0x03FF0080u)]), // PA_SC_BINNER_CNTL_0, PA_SC_BINNER_CNTL_1
|
||||
new(0u, 58u, 0x43FBD769u, [new(0x105u, 0x00000000u), new(0x107u, 0x00000000u), new(0x106u, 0x00000000u), new(0x108u, 0x00000000u)]), // CB_BLEND_RED, CB_BLEND_BLUE, CB_BLEND_GREEN, CB_BLEND_ALPHA
|
||||
new(0u, 59u, 0xEF550356u, [new(0x1E0u, 0x20010001u)]), // CB_BLEND0_CONTROL
|
||||
new(0u, 60u, 0x8F52E279u, [new(0x020u, 0x00000000u), new(0x021u, 0x00000000u)]), // TA_BC_BASE_ADDR, TA_BC_BASE_ADDR_HI
|
||||
new(0u, 61u, 0x1F2D8149u, [new(0x084u, 0x00000000u), new(0x085u, 0x20002000u)]), // PA_SC_CLIPRECT_0_TL, PA_SC_CLIPRECT_0_BR
|
||||
new(0u, 62u, 0x853D0614u, [new(0x800003FFu, 0x00000000u)]), // CX_NOP
|
||||
new(0u, 63u, 0x4413C6F9u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // DB_DEPTH_BOUNDS_MIN, DB_DEPTH_BOUNDS_MAX
|
||||
new(0u, 64u, 0x67096014u, [new(0x010u, 0x80000000u), new(0x011u, 0x20000000u), new(0x012u, 0x00000000u), new(0x013u, 0x00000000u), new(0x014u, 0x00000000u), new(0x015u, 0x00000000u), new(0x01Au, 0x00000000u), new(0x01Bu, 0x00000000u), new(0x01Cu, 0x00000000u), new(0x01Du, 0x00000000u), new(0x01Eu, 0x00000000u), new(0x002u, 0x00000000u), new(0x005u, 0x00000000u), new(0x007u, 0x00000000u), new(0x00Bu, 0x00000000u), new(0x00Au, 0x00000000u)]), // DB_Z_INFO, DB_STENCIL_INFO, DB_Z_READ_BASE, DB_STENCIL_READ_BASE, DB_Z_WRITE_BASE, DB_STENCIL_WRITE_BASE, DB_Z_READ_BASE_HI, DB_STENCIL_READ_BASE_HI, DB_Z_WRITE_BASE_HI, DB_STENCIL_WRITE_BASE_HI, DB_HTILE_DATA_BASE_HI, DB_DEPTH_VIEW, DB_HTILE_DATA_BASE, DB_DEPTH_SIZE_XY, DB_DEPTH_CLEAR, DB_STENCIL_CLEAR
|
||||
new(0u, 65u, 0x88F5E915u, [new(0x0EBu, 0xFF00FF00u), new(0x0ECu, 0x00000000u)]), // PA_SC_FOV_WINDOW_LR, PA_SC_FOV_WINDOW_TB
|
||||
new(0u, 66u, 0x033F1EFFu, [new(0x800003FCu, 0x00000000u), new(0x800003FDu, 0x00000000u)]), // FSR_RECURSIONS0, FSR_RECURSIONS1
|
||||
new(0u, 67u, 0x918106BBu, [new(0x090u, 0x80000000u), new(0x091u, 0x40004000u)]), // PA_SC_GENERIC_SCISSOR_TL, PA_SC_GENERIC_SCISSOR_BR
|
||||
new(0u, 68u, 0x95F0E7ACu, [new(0x2FAu, 0x4E7E0000u), new(0x2FBu, 0x4E7E0000u), new(0x2FCu, 0x4E7E0000u), new(0x2FDu, 0x4E7E0000u)]), // PA_CL_GB_VERT_CLIP_ADJ, PA_CL_GB_VERT_DISC_ADJ, PA_CL_GB_HORZ_CLIP_ADJ, PA_CL_GB_HORZ_DISC_ADJ
|
||||
new(0u, 69u, 0xB48CBAB2u, [new(0x2E2u, 0x00000000u), new(0x2E3u, 0x00000000u)]), // PA_SU_POLY_OFFSET_BACK_SCALE, PA_SU_POLY_OFFSET_BACK_OFFSET
|
||||
new(0u, 70u, 0x05BB3BC6u, [new(0x2E0u, 0x00000000u), new(0x2E1u, 0x00000000u)]), // PA_SU_POLY_OFFSET_FRONT_SCALE, PA_SU_POLY_OFFSET_FRONT_OFFSET
|
||||
new(0u, 71u, 0x94FABA07u, [new(0x003u, 0x00000000u), new(0x004u, 0x00000000u)]), // DB_RENDER_OVERRIDE, DB_RENDER_OVERRIDE2
|
||||
new(0u, 72u, 0x38E92C91u, [new(0x318u, 0x00000000u), new(0x31Bu, 0x00000000u), new(0x31Cu, 0x00000000u), new(0x31Du, 0x00000000u), new(0x31Eu, 0x00000048u), new(0x31Fu, 0x00000000u), new(0x321u, 0x00000000u), new(0x323u, 0x00000000u), new(0x324u, 0x00000000u), new(0x325u, 0x00000000u), new(0x390u, 0x00000000u), new(0x398u, 0x00000000u), new(0x3A0u, 0x00000000u), new(0x3A8u, 0x00000000u), new(0x3B0u, 0x00000000u), new(0x3B8u, 0x0006C000u)]), // CB_COLOR0_BASE, CB_COLOR0_VIEW, CB_COLOR0_INFO, CB_COLOR0_ATTRIB, CB_COLOR0_DCC_CONTROL, CB_COLOR0_CMASK, CB_COLOR0_FMASK, CB_COLOR0_CLEAR_WORD0, CB_COLOR0_CLEAR_WORD1, CB_COLOR0_DCC_BASE, CB_COLOR0_BASE_EXT, CB_COLOR0_CMASK_BASE_EXT, CB_COLOR0_FMASK_BASE_EXT, CB_COLOR0_DCC_BASE_EXT, CB_COLOR0_ATTRIB2, CB_COLOR0_ATTRIB3
|
||||
new(0u, 73u, 0x0B177B43u, [new(0x00Cu, 0x00000000u), new(0x00Du, 0x40004000u)]), // PA_SC_SCREEN_SCISSOR_TL, PA_SC_SCREEN_SCISSOR_BR
|
||||
new(0u, 74u, 0x48531062u, [new(0x191u, 0x00000000u)]), // SPI_PS_INPUT_CNTL_0
|
||||
new(0u, 75u, 0xAAA964B9u, [new(0x16Fu, 0x00000000u), new(0x170u, 0x00000000u), new(0x171u, 0x00000000u), new(0x172u, 0x00000000u)]), // PA_CL_UCP_0_X, PA_CL_UCP_0_Y, PA_CL_UCP_0_Z, PA_CL_UCP_0_W
|
||||
new(0u, 76u, 0x7690AF6Fu, [new(0x10Fu, 0x4E7E0000u), new(0x111u, 0x4E7E0000u), new(0x113u, 0x4E7E0000u), new(0x110u, 0x00000000u), new(0x112u, 0x00000000u), new(0x114u, 0x00000000u), new(0x094u, 0x80000000u), new(0x095u, 0x40004000u), new(0x0B4u, 0x00000000u), new(0x0B5u, 0x00000000u)]), // PA_CL_VPORT_XSCALE, PA_CL_VPORT_YSCALE, PA_CL_VPORT_ZSCALE, PA_CL_VPORT_XOFFSET, PA_CL_VPORT_YOFFSET, PA_CL_VPORT_ZOFFSET, PA_SC_VPORT_SCISSOR_0_TL, PA_SC_VPORT_SCISSOR_0_BR, PA_SC_VPORT_ZMIN_0, PA_SC_VPORT_ZMAX_0
|
||||
new(0u, 77u, 0x078D7060u, [new(0x081u, 0x80000000u), new(0x082u, 0x40004000u)]), // PA_SC_WINDOW_SCISSOR_TL, PA_SC_WINDOW_SCISSOR_BR
|
||||
// shader register groups
|
||||
new(1u, 0u, 0x5D6E3EC7u, [new(0x212u, 0x00000000u)]), // COMPUTE_PGM_RSRC1
|
||||
new(1u, 1u, 0x57E7079Au, [new(0x213u, 0x00000000u)]), // COMPUTE_PGM_RSRC2
|
||||
new(1u, 2u, 0x7467FAFDu, [new(0x228u, 0x00000000u)]), // COMPUTE_PGM_RSRC3
|
||||
new(1u, 3u, 0x9E826B50u, [new(0x215u, 0x00000000u)]), // COMPUTE_RESOURCE_LIMITS
|
||||
new(1u, 4u, 0xDC484F18u, [new(0x218u, 0x00000000u)]), // COMPUTE_TMPRING_SIZE
|
||||
new(1u, 5u, 0x5DA8BCA3u, [new(0x08Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_GS
|
||||
new(1u, 6u, 0x5CA726D8u, [new(0x10Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_HS
|
||||
new(1u, 7u, 0x5DD28360u, [new(0x00Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_PS
|
||||
new(1u, 8u, 0x57EFA0BEu, [new(0x08Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_GS
|
||||
new(1u, 9u, 0x502363D5u, [new(0x10Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_HS
|
||||
new(1u, 10u, 0x506D14BDu, [new(0x00Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_PS
|
||||
new(1u, 11u, 0xB2609506u, [new(0x224u, 0x00000000u)]), // COMPUTE_USER_ACCUM_0
|
||||
new(1u, 12u, 0x9E5CFB8Au, [new(0x107u, 0x00000000u), new(0x087u, 0x00000000u), new(0x007u, 0x00000000u)]), // SPI_SHADER_PGM_RSRC3_HS, SPI_SHADER_PGM_RSRC3_GS, SPI_SHADER_PGM_RSRC3_PS
|
||||
new(1u, 13u, 0xC918DF3Eu, [new(0x20Cu, 0x00000000u), new(0x20Du, 0x00000000u)]), // COMPUTE_PGM_LO, COMPUTE_PGM_HI
|
||||
new(1u, 14u, 0xC9751C9Cu, [new(0x0C8u, 0x00000000u), new(0x0C9u, 0x00000000u)]), // SPI_SHADER_PGM_LO_ES, SPI_SHADER_PGM_HI_ES
|
||||
new(1u, 15u, 0xC97EF77Au, [new(0x088u, 0x00000000u), new(0x089u, 0x00000000u)]), // SPI_SHADER_PGM_LO_GS, SPI_SHADER_PGM_HI_GS
|
||||
new(1u, 16u, 0xC927C6B9u, [new(0x108u, 0x00000000u), new(0x109u, 0x00000000u)]), // SPI_SHADER_PGM_LO_HS, SPI_SHADER_PGM_HI_HS
|
||||
new(1u, 17u, 0xC92A1EC5u, [new(0x148u, 0x00000000u), new(0x149u, 0x00000000u)]), // SPI_SHADER_PGM_LO_LS, SPI_SHADER_PGM_HI_LS
|
||||
new(1u, 18u, 0xC9E01B31u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // SPI_SHADER_PGM_LO_PS, SPI_SHADER_PGM_HI_PS
|
||||
new(1u, 19u, 0x50685F29u, [new(0x800002FFu, 0x00000000u)]), // SH_NOP
|
||||
new(1u, 20u, 0xB26219CAu, [new(0x0B2u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_ESGS_0
|
||||
new(1u, 21u, 0xB25B6CF9u, [new(0x132u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_LSHS_0
|
||||
new(1u, 22u, 0xB2F86101u, [new(0x032u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_PS_0
|
||||
new(1u, 23u, 0x07E3B155u, [new(0x082u, 0x00000000u), new(0x083u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_GS, SPI_SHADER_USER_DATA_ADDR_HI_GS
|
||||
new(1u, 24u, 0x07E383C6u, [new(0x102u, 0x00000000u), new(0x103u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_HS, SPI_SHADER_USER_DATA_ADDR_HI_HS
|
||||
new(1u, 25u, 0xBDA98653u, [new(0x240u, 0x00000000u)]), // COMPUTE_USER_DATA_0
|
||||
new(1u, 26u, 0xBDBD1D0Fu, [new(0x08Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_GS_0
|
||||
new(1u, 27u, 0xBD946FD4u, [new(0x10Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_HS_0
|
||||
new(1u, 28u, 0xBDF02A4Cu, [new(0x00Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_PS_0
|
||||
// uconfig register groups
|
||||
new(2u, 0u, 0x19E93E85u, [new(0x41Fu, 0x00000000u)]), // GDS_OA_ADDRESS
|
||||
new(2u, 1u, 0x3B5C2AF3u, [new(0x41Du, 0x00000000u)]), // GDS_OA_CNTL
|
||||
new(2u, 2u, 0x47974A35u, [new(0x41Eu, 0x00000000u)]), // GDS_OA_COUNTER
|
||||
new(2u, 3u, 0x105971C2u, [new(0x25Bu, 0x00000000u)]), // GE_CNTL
|
||||
new(2u, 4u, 0x7D137765u, [new(0x24Au, 0x00000000u)]), // GE_INDX_OFFSET
|
||||
new(2u, 5u, 0xD187FEBCu, [new(0x24Bu, 0x00000000u)]), // GE_MULTI_PRIM_IB_RESET_EN
|
||||
new(2u, 6u, 0x12F854ACu, [new(0x25Fu, 0x00000000u)]), // GE_STEREO_CNTL
|
||||
new(2u, 7u, 0x40D49AD1u, [new(0x262u, 0x00000000u)]), // GE_USER_VGPR_EN
|
||||
new(2u, 8u, 0x8C0923DAu, [new(0x80003FF4u, 0x00000000u)]), // FSR_EXTEND_SUBPIXEL_ROUNDING
|
||||
new(2u, 9u, 0xBB8DF494u, [new(0x80003FFDu, 0x00000000u)]), // TEXTURE_GRADIENT_CONTROL
|
||||
new(2u, 10u, 0xF6D8A76Eu, [new(0x382u, 0x40000040u)]), // TEXTURE_GRADIENT_FACTORS
|
||||
new(2u, 11u, 0x7620F1E9u, [new(0x248u, 0x00000000u)]), // VGT_OBJECT_ID
|
||||
new(2u, 12u, 0x9EBFAB10u, [new(0x242u, 0x00000000u)]), // VGT_PRIMITIVE_TYPE
|
||||
new(2u, 13u, 0x98A09D0Eu, [new(0x380u, 0x00000000u), new(0x381u, 0x00000000u)]), // TA_CS_BC_BASE_ADDR, TA_CS_BC_BASE_ADDR_HI
|
||||
new(2u, 14u, 0x195D37D2u, [new(0x80003FF5u, 0x00000000u), new(0x80003FF6u, 0x00000000u)]), // FSR_ALPHA_VALUE0, FSR_ALPHA_VALUE1
|
||||
new(2u, 15u, 0xF9EC4F85u, [new(0x80003FF7u, 0x00000000u), new(0x80003FF8u, 0x00000000u), new(0x80003FF9u, 0x00000000u), new(0x80003FFAu, 0x00000000u)]), // FSR_CONTROL_POINT0, FSR_CONTROL_POINT1, FSR_CONTROL_POINT2, FSR_CONTROL_POINT3
|
||||
new(2u, 16u, 0x4626B750u, [new(0x80003FFBu, 0x00000000u), new(0x80003FFCu, 0x00000000u)]), // FSR_WINDOW0, FSR_WINDOW1
|
||||
new(2u, 17u, 0x4CC673A0u, [new(0x80003FFEu, 0x00000000u)]), // MEMORY_MAPPING_MASK
|
||||
new(2u, 18u, 0xDE5B3431u, [new(0x80003FFFu, 0x00000000u)]), // UC_NOP
|
||||
new(2u, 19u, 0x036AC8A6u, [new(0x25Cu, 0x00000000u)]), // GE_USER_VGPR1
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Wires the backend-neutral shader compiler to this assembly's HLE services. The
|
||||
/// module initializer runs before any Libs code can invoke the evaluator, so the hook
|
||||
/// is always installed first.
|
||||
/// </summary>
|
||||
internal static class AgcShaderCompilerHooks
|
||||
{
|
||||
[ModuleInitializer]
|
||||
[SuppressMessage(
|
||||
"Usage",
|
||||
"CA2255:The 'ModuleInitializer' attribute should not be used in libraries",
|
||||
Justification = "This is the rule's intended advanced scenario: the hook must be " +
|
||||
"installed before any code path can reach the evaluator, and every such path " +
|
||||
"enters through this assembly.")]
|
||||
internal static void Install()
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||
VulkanVideoPresenter.GuestDataPool;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,486 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
|
||||
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
|
||||
/// selected by the 5-bit SWIZZLE_MODE in the image descriptor; uploading those
|
||||
/// bytes verbatim samples as garbage.
|
||||
///
|
||||
/// The GFX10 addressing uses power-of-two swizzle blocks (256 B / 4 KiB /
|
||||
/// 64 KiB) whose internal element order follows the standard (S), display (D),
|
||||
/// render (R), or z-order/depth (Z) equations. This implements the exact base
|
||||
/// S and Z 2D single-sample modes plus the PS5's RB+ 64 KiB Z_X/R_X equations;
|
||||
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||
/// equations are being ported.
|
||||
/// </summary>
|
||||
internal static class GnmTiling
|
||||
{
|
||||
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
|
||||
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
|
||||
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||
private readonly record struct AddressBit(uint XMask, uint YMask);
|
||||
|
||||
private static readonly AddressBit[][] RbPlus64KRenderX =
|
||||
[
|
||||
// 1 byte/element: nibble01=0, nibble2=307, nibble3=379.
|
||||
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||
// 2 bytes/element: nibble01=1, nibble2=307, nibble3=389.
|
||||
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||
// 4 bytes/element: nibble01=39, nibble2=307, nibble3=381.
|
||||
[Zero, Zero, X(0), X(1), Y(0), Y(1), X(2), Y(2),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||
// 8 bytes/element: nibble01=6, nibble2=307, nibble3=382.
|
||||
[Zero, Zero, Zero, X(0), Y(0), X(1), X(2), Y(1),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||
// 16 bytes/element: nibble01=7, nibble2=307, nibble3=390.
|
||||
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||
];
|
||||
|
||||
private static readonly AddressBit[][] RbPlus64KDepthX =
|
||||
[
|
||||
// 1 byte/element: nibble01=8, nibble2=306, nibble3=379.
|
||||
[X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3), Y(3),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||
// 2 bytes/element: nibble01=9, nibble2=306, nibble3=389.
|
||||
[Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||
// 4 bytes/element: nibble01=10, nibble2=306, nibble3=381.
|
||||
[Zero, Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||
// 8 bytes/element: nibble01=11, nibble2=307, nibble3=382.
|
||||
[Zero, Zero, Zero, X(0), Y(0), X(1), Y(1), X(2),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||
// 16 bytes/element is identical to R_X for a 2D single-sample image.
|
||||
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||
];
|
||||
|
||||
private static readonly AddressBit[][] RbPlus64KStandard =
|
||||
[
|
||||
// GFX10_SW_64K_S_RBPLUS_PATINFO, 1 byte/element.
|
||||
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||
Y(4), X(4), Y(5), X(5), Y(6), X(6), Y(7), X(7)],
|
||||
// 2 bytes/element.
|
||||
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||
Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7)],
|
||||
// 4 bytes/element.
|
||||
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||
Y(3), X(3), Y(4), X(4), Y(5), X(5), Y(6), X(6)],
|
||||
// 8 bytes/element (also BC1/BC4 compressed blocks).
|
||||
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||
Y(2), X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6)],
|
||||
// 16 bytes/element (also 16-byte BC compressed blocks).
|
||||
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||
Y(2), X(2), Y(3), X(3), Y(4), X(4), Y(5), X(5)],
|
||||
];
|
||||
|
||||
// GFX10 4K_S has a separate 12-bit micro-tile equation. It is not the
|
||||
// generic x/y interleave used by the 64K standard block; using that larger
|
||||
// equation leaves a regular grid in linearized atlases.
|
||||
private static readonly AddressBit[][] Standard4K =
|
||||
[
|
||||
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||
Y(4), X(4), Y(5), X(5)],
|
||||
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||
Y(3), X(4), Y(4), X(5)],
|
||||
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||
Y(3), X(3), Y(4), X(4)],
|
||||
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||
Y(2), X(3), Y(3), X(4)],
|
||||
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||
Y(2), X(2), Y(3), X(3)],
|
||||
];
|
||||
|
||||
private static readonly bool _enabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly bool _disabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||
"0",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly HashSet<uint> _reportedModes = new();
|
||||
|
||||
public static bool Enabled => _enabled || !_disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Base S/Z modes and the Oberon RB+ 64 KiB Z_X/R_X modes for which this
|
||||
/// implementation carries the exact AddrLib address equations. Other
|
||||
/// bank/pipe-XOR variants remain opt-in.
|
||||
/// </summary>
|
||||
private static bool IsTrustedByDefault(uint swizzleMode) =>
|
||||
// Exact base S/Z modes. D/R use different GFX10 swizzle equations and
|
||||
// the T/X modes additionally apply pipe/bank XOR between blocks.
|
||||
swizzleMode is 1 or 4 or 5 or 8 or 9 or 24 or 27;
|
||||
|
||||
// Detile a surface when it is verified-correct by default (trusted base mode),
|
||||
// or when the user opts the approximate modes in with SHARPEMU_DETILE=1.
|
||||
// SHARPEMU_DETILE=0 forces the old raw-upload behavior for everything.
|
||||
private static bool ShouldDetile(uint swizzleMode) =>
|
||||
swizzleMode != 0 && !_disabled && (_enabled || IsTrustedByDefault(swizzleMode));
|
||||
|
||||
/// <summary>
|
||||
/// True when a surface with the given swizzle mode needs deswizzling.
|
||||
/// Mode 0 is linear and never needs it.
|
||||
/// </summary>
|
||||
public static bool NeedsDetile(uint swizzleMode) => ShouldDetile(swizzleMode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the physical byte span occupied by a tiled mip. GNM allocates whole
|
||||
/// swizzle blocks even when the logical image is much smaller than a block;
|
||||
/// reading only the logical texel count truncates most source offsets during
|
||||
/// detiling (a 64x64 BC1 mode-9 image is 2 KiB logically but occupies one
|
||||
/// 64 KiB swizzle block).
|
||||
/// </summary>
|
||||
public static bool TryGetTiledByteCount(
|
||||
uint swizzleMode,
|
||||
int elementsWide,
|
||||
int elementsHigh,
|
||||
int bytesPerElement,
|
||||
out ulong byteCount)
|
||||
{
|
||||
byteCount = 0;
|
||||
if (!ShouldDetile(swizzleMode) ||
|
||||
elementsWide <= 0 ||
|
||||
elementsHigh <= 0 ||
|
||||
bytesPerElement <= 0 ||
|
||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||
if (bppLog2 < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var blockElements = blockBytes >> bppLog2;
|
||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||
if (blockWidth == 0 || blockHeight == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var blocksWide = ((ulong)elementsWide + (ulong)blockWidth - 1) / (ulong)blockWidth;
|
||||
var blocksHigh = ((ulong)elementsHigh + (ulong)blockHeight - 1) / (ulong)blockHeight;
|
||||
try
|
||||
{
|
||||
byteCount = checked(blocksWide * blocksHigh * (ulong)blockBytes);
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
byteCount = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||
/// block-compressed formats, so callers pass the element grid dimensions
|
||||
/// and the bytes per element. Returns false (leaving output untouched) for
|
||||
/// unsupported swizzle modes so the caller can fall back to the raw bytes.
|
||||
/// </summary>
|
||||
public static bool TryDetile(
|
||||
ReadOnlySpan<byte> tiled,
|
||||
Span<byte> linear,
|
||||
uint swizzleMode,
|
||||
int elementsWide,
|
||||
int elementsHigh,
|
||||
int bytesPerElement)
|
||||
{
|
||||
if (!ShouldDetile(swizzleMode) || elementsWide <= 0 || elementsHigh <= 0 || bytesPerElement <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
|
||||
{
|
||||
ReportUnsupported(swizzleMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||
if (bppLog2 < 0)
|
||||
{
|
||||
// Non-power-of-two element size (e.g. 24-bit) is not swizzled in a
|
||||
// way this equation models.
|
||||
ReportUnsupported(swizzleMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block dimensions in elements: a swizzle block holds blockBytes bytes,
|
||||
// laid out as a square-ish power-of-two element grid scaled by bpp.
|
||||
var blockElements = blockBytes >> bppLog2;
|
||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||
if (blockWidth == 0 || blockHeight == 0)
|
||||
{
|
||||
ReportUnsupported(swizzleMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||
var requiredLinear = (long)elementsWide * elementsHigh * bytesPerElement;
|
||||
if (linear.Length < requiredLinear)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Precompute the within-block element offset for each (x, y) inside a
|
||||
// single block. The swizzle equation only depends on the in-block
|
||||
// coordinates, so this table is reused for every block — turning the
|
||||
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
|
||||
// Detiling a 2048x2048 texture is millions of elements; without this the
|
||||
// per-pixel math makes DETILE unusably slow during asset streaming.
|
||||
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||
{
|
||||
for (var bx = 0; bx < blockWidth; bx++)
|
||||
{
|
||||
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||
}
|
||||
}
|
||||
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
{
|
||||
var blockY = y / blockHeight;
|
||||
var inBlockY = y % blockHeight;
|
||||
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||
var tableRowBase = inBlockY * blockWidth;
|
||||
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||
for (var x = 0; x < elementsWide; x++)
|
||||
{
|
||||
var blockX = x / blockWidth;
|
||||
var inBlockX = x % blockWidth;
|
||||
|
||||
var blockIndex = rowBlockBase + blockX;
|
||||
var sourceByte = hasExactXorPattern
|
||||
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
|
||||
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||
(long)bytesPerElement;
|
||||
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||
if (sourceByte + bytesPerElement > tiled.Length ||
|
||||
destByte + bytesPerElement > linear.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tiled.Slice((int)sourceByte, bytesPerElement)
|
||||
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private enum SwizzleKind
|
||||
{
|
||||
Standard,
|
||||
ZOrder,
|
||||
}
|
||||
|
||||
private static readonly AddressBit Zero = new(0, 0);
|
||||
|
||||
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||
|
||||
private static AddressBit Y(int bit) => new(0, 1u << bit);
|
||||
|
||||
private static AddressBit XY(int xBit, int yBit) => new(1u << xBit, 1u << yBit);
|
||||
|
||||
private static AddressBit XY(int xBit, int yBit1, int yBit2) =>
|
||||
new(1u << xBit, (1u << yBit1) | (1u << yBit2));
|
||||
|
||||
private static bool TryGetExactXorPattern(
|
||||
uint swizzleMode,
|
||||
int bytesPerElementLog2,
|
||||
out AddressBit[] pattern)
|
||||
{
|
||||
pattern = [];
|
||||
if ((uint)bytesPerElementLog2 >= RbPlus64KRenderX.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pattern = swizzleMode switch
|
||||
{
|
||||
5 => Standard4K[bytesPerElementLog2],
|
||||
9 => RbPlus64KStandard[bytesPerElementLog2],
|
||||
24 => RbPlus64KDepthX[bytesPerElementLog2],
|
||||
27 => RbPlus64KRenderX[bytesPerElementLog2],
|
||||
_ => [],
|
||||
};
|
||||
return pattern.Length != 0;
|
||||
}
|
||||
|
||||
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
|
||||
{
|
||||
uint offset = 0;
|
||||
for (var bit = 0; bit < pattern.Length; bit++)
|
||||
{
|
||||
var equation = pattern[bit];
|
||||
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
|
||||
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
|
||||
offset |= (uint)parity << bit;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static bool TryGetSwizzleKind(uint swizzleMode, out SwizzleKind kind, out int blockBytes)
|
||||
{
|
||||
// GFX10 AddrLib SWIZZLE_MODE enumeration:
|
||||
// 1-3 = 256 B S/D/R
|
||||
// 4-7 = 4 KiB Z/S/D/R
|
||||
// 8-11 = 64 KiB Z/S/D/R
|
||||
// 16-19 = 64 KiB Z/S/D/R _T
|
||||
// 20-23 = 4 KiB Z/S/D/R _X
|
||||
// 24-27 = 64 KiB Z/S/D/R _X
|
||||
// The pipe/bank XOR (_T/_X) affects which block a given tile lands in,
|
||||
// but the *within-block* element order matches the base S/Z equation,
|
||||
// which is what dominates visible correctness. We model the block
|
||||
// interior and treat blocks as linear-ordered.
|
||||
kind = SwizzleKind.Standard;
|
||||
blockBytes = 0;
|
||||
switch (swizzleMode)
|
||||
{
|
||||
case 4: case 8: case 16: case 20: case 24:
|
||||
kind = SwizzleKind.ZOrder;
|
||||
break;
|
||||
case 1: case 2: case 3:
|
||||
case 5: case 6: case 7:
|
||||
case 9: case 10: case 11:
|
||||
case 17: case 18: case 19:
|
||||
case 21: case 22: case 23:
|
||||
case 25: case 26: case 27:
|
||||
kind = SwizzleKind.Standard;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
blockBytes = swizzleMode switch
|
||||
{
|
||||
>= 1 and <= 3 => 256,
|
||||
>= 4 and <= 7 => 4096,
|
||||
>= 20 and <= 23 => 4096,
|
||||
_ => 65536,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard (S) 2D swizzle: within a block, the element order interleaves x
|
||||
// and y bits with x taking the low bit — the AMD "standard" microtile.
|
||||
private static long StandardSwizzleOffset(uint x, uint y, int blockWidth, int blockHeight)
|
||||
{
|
||||
var widthBits = BitLog2((uint)blockWidth);
|
||||
var heightBits = BitLog2((uint)blockHeight);
|
||||
long offset = 0;
|
||||
var outBit = 0;
|
||||
var xi = 0;
|
||||
var yi = 0;
|
||||
while (xi < widthBits || yi < heightBits)
|
||||
{
|
||||
if (xi < widthBits)
|
||||
{
|
||||
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||
xi++;
|
||||
}
|
||||
|
||||
if (yi < heightBits)
|
||||
{
|
||||
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||
yi++;
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
// Z-order (Morton) swizzle: pure bit interleave, y taking the low bit.
|
||||
private static long MortonInterleave(uint x, uint y, int blockWidth, int blockHeight)
|
||||
{
|
||||
var widthBits = BitLog2((uint)blockWidth);
|
||||
var heightBits = BitLog2((uint)blockHeight);
|
||||
long offset = 0;
|
||||
var outBit = 0;
|
||||
var xi = 0;
|
||||
var yi = 0;
|
||||
while (xi < widthBits || yi < heightBits)
|
||||
{
|
||||
if (yi < heightBits)
|
||||
{
|
||||
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||
yi++;
|
||||
}
|
||||
|
||||
if (xi < widthBits)
|
||||
{
|
||||
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||
xi++;
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static (int Width, int Height) SquareBlockDimensions(int blockElements)
|
||||
{
|
||||
if (blockElements <= 0 || (blockElements & (blockElements - 1)) != 0)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
var totalBits = BitLog2((uint)blockElements);
|
||||
// Split as evenly as possible with width >= height (x gets the extra bit).
|
||||
var widthBits = (totalBits + 1) / 2;
|
||||
var heightBits = totalBits - widthBits;
|
||||
return (1 << widthBits, 1 << heightBits);
|
||||
}
|
||||
|
||||
private static int BitLog2(uint value)
|
||||
{
|
||||
if (value == 0 || (value & (value - 1)) != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return System.Numerics.BitOperations.TrailingZeroCount(value);
|
||||
}
|
||||
|
||||
private static void ReportUnsupported(uint swizzleMode)
|
||||
{
|
||||
lock (_reportedModes)
|
||||
{
|
||||
if (!_reportedModes.Add(swizzleMode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] GNM detile: unsupported swizzle mode {swizzleMode}; texture uploaded linear.");
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,17 @@
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
||||
// DmaData packets or by direct CPU writes).
|
||||
/// <summary>
|
||||
/// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM
|
||||
/// condition. AgcExports re-checks every waiter against guest memory on each
|
||||
/// submit and resumes the ones whose condition became true (labels are advanced
|
||||
/// by ReleaseMem / WriteData / DmaData packets, or by direct CPU writes).
|
||||
///
|
||||
/// This preserves cross-submit ordering: the work that follows a wait inside a
|
||||
/// DCB is only queued once the awaited completion label is genuinely written,
|
||||
/// instead of being force-satisfied at parse time and running ahead of the
|
||||
/// compute/graphics work it depends on (which produced a black composite).
|
||||
/// </summary>
|
||||
internal static class GpuWaitRegistry
|
||||
{
|
||||
public struct WaitingDcb
|
||||
@@ -19,13 +26,56 @@ internal static class GpuWaitRegistry
|
||||
public ulong ReferenceValue;
|
||||
public ulong Mask;
|
||||
public uint CompareFunction;
|
||||
public uint ControlValue;
|
||||
public bool Is64Bit;
|
||||
public bool IsStandard;
|
||||
public object? Memory;
|
||||
public string? QueueName;
|
||||
public ulong SubmissionId;
|
||||
// Stopwatch timestamp captured at registration. Stale waiters remain
|
||||
// registered; this only controls one-shot diagnostics.
|
||||
public long RegisteredTicks;
|
||||
public bool StaleReported;
|
||||
public object? State;
|
||||
}
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||
|
||||
public static int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var (_, list) in _waiters)
|
||||
{
|
||||
total += list.Count;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int CountForMemory(object memory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var (_, list) in _waiters)
|
||||
{
|
||||
foreach (var waiter in list)
|
||||
{
|
||||
total += ReferenceEquals(waiter.Memory, memory) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Register(ulong address, WaitingDcb waiter)
|
||||
{
|
||||
waiter.WaitAddress = address;
|
||||
@@ -41,9 +91,15 @@ internal static class GpuWaitRegistry
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
||||
/// <summary>
|
||||
/// Re-evaluates every registered waiter. <paramref name="readValue"/>
|
||||
/// receives (address, is64Bit) and returns null when the memory is
|
||||
/// unreadable; such waiters are kept registered. Returns the waiters whose
|
||||
/// condition is now satisfied (removed from the registry), or null.
|
||||
/// </summary>
|
||||
public static List<WaitingDcb>? CollectSatisfied(
|
||||
object memory,
|
||||
Func<ulong, bool, ulong?> readValue)
|
||||
{
|
||||
List<WaitingDcb>? woken = null;
|
||||
lock (_gate)
|
||||
@@ -53,6 +109,11 @@ internal static class GpuWaitRegistry
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!ReferenceEquals(list[i].Memory, memory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = readValue(address, list[i].Is64Bit);
|
||||
if (value is null || !Compare(list[i], value.Value))
|
||||
{
|
||||
@@ -71,7 +132,7 @@ internal static class GpuWaitRegistry
|
||||
}
|
||||
}
|
||||
|
||||
if (emptied != null)
|
||||
if (emptied is not null)
|
||||
{
|
||||
foreach (var address in emptied)
|
||||
{
|
||||
@@ -83,20 +144,122 @@ internal static class GpuWaitRegistry
|
||||
return woken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns waiters that have remained unsatisfied longer than
|
||||
/// <paramref name="maxAgeTicks"/> exactly once, without removing them or
|
||||
/// changing their labels. Missing GPU work must fail closed: advancing a
|
||||
/// command buffer without its real producer corrupts cross-queue ordering.
|
||||
/// </summary>
|
||||
public static List<WaitingDcb>? CollectUnreportedStale(
|
||||
object memory,
|
||||
long nowTicks,
|
||||
long maxAgeTicks)
|
||||
{
|
||||
List<WaitingDcb>? stale = null;
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var (_, list) in _waiters)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var waiter = list[i];
|
||||
if (!ReferenceEquals(waiter.Memory, memory) ||
|
||||
waiter.StaleReported ||
|
||||
nowTicks - waiter.RegisteredTicks < maxAgeTicks)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stale ??= new List<WaitingDcb>();
|
||||
waiter.StaleReported = true;
|
||||
list[i] = waiter;
|
||||
stale.Add(waiter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns watched labels overlapped by a newly discovered producer. Used
|
||||
/// only for diagnostics; producer completion still wakes through the
|
||||
/// normal CollectSatisfied path after the ordered memory write executes.
|
||||
/// </summary>
|
||||
public static List<(ulong Address, int Count)> SnapshotInRange(
|
||||
object memory,
|
||||
ulong start,
|
||||
ulong length)
|
||||
{
|
||||
var matches = new List<(ulong Address, int Count)>();
|
||||
if (length == 0)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
var end = start > ulong.MaxValue - length ? ulong.MaxValue : start + length;
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
var matchingCount = 0;
|
||||
var any64Bit = false;
|
||||
foreach (var waiter in list)
|
||||
{
|
||||
if (!ReferenceEquals(waiter.Memory, memory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matchingCount++;
|
||||
any64Bit |= waiter.Is64Bit;
|
||||
}
|
||||
|
||||
if (matchingCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var width = any64Bit
|
||||
? sizeof(ulong)
|
||||
: sizeof(uint);
|
||||
var waitEnd = address > ulong.MaxValue - (ulong)width
|
||||
? ulong.MaxValue
|
||||
: address + (ulong)width;
|
||||
if (start < waitEnd && address < end)
|
||||
{
|
||||
matches.Add((address, matchingCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||
{
|
||||
var masked = value & waiter.Mask;
|
||||
var reference = waiter.ReferenceValue & waiter.Mask;
|
||||
return waiter.CompareFunction switch
|
||||
{
|
||||
1 => masked < waiter.ReferenceValue,
|
||||
2 => masked <= waiter.ReferenceValue,
|
||||
3 => masked == waiter.ReferenceValue,
|
||||
4 => masked != waiter.ReferenceValue,
|
||||
5 => masked >= waiter.ReferenceValue,
|
||||
6 => masked > waiter.ReferenceValue,
|
||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
||||
0 => true,
|
||||
1 => masked < reference,
|
||||
2 => masked <= reference,
|
||||
3 => masked == reference,
|
||||
4 => masked != reference,
|
||||
5 => masked >= reference,
|
||||
6 => masked > reference,
|
||||
// 7 is reserved; treating it as satisfied keeps a malformed packet
|
||||
// from suspending forever.
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_waiters.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,31 +7,11 @@ namespace SharpEmu.Libs.Ajm;
|
||||
|
||||
public static class AjmExports
|
||||
{
|
||||
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
|
||||
private static int _nextContextId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dl+4eHSzUu4",
|
||||
ExportName = "sceAjmInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int Initialize(CpuContext ctx)
|
||||
{
|
||||
var reserved = ctx[CpuRegister.Rdi];
|
||||
var outContextAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
// AJM requires a zero reserved argument.
|
||||
if (reserved != 0 || outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
public static int Initialize(CpuContext ctx) =>
|
||||
SharpEmu.Libs.Audio.AjmExports.AjmInitialize(ctx);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public static class AmprExports
|
||||
public ulong Buffer;
|
||||
public ulong Size;
|
||||
public ulong WriteOffset;
|
||||
public ulong CommandCount;
|
||||
}
|
||||
|
||||
private sealed class CachedHostFile
|
||||
@@ -267,53 +268,19 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong bytesRead = 0;
|
||||
|
||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
||||
// speculative reads and only consume the bytes on success paths.
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||
{
|
||||
if (destination != 0 && size > 0)
|
||||
{
|
||||
int chunkSize = (int)Math.Min(size, 4096);
|
||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||
zeros.Clear();
|
||||
while (bytesRead < size)
|
||||
{
|
||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += (ulong)currentChunk;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Offset -1 means "continue after the previous read of this file id".
|
||||
if (fileOffset == unchecked((ulong)(long)-1))
|
||||
{
|
||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||
}
|
||||
else if (fileOffset > long.MaxValue)
|
||||
{
|
||||
fileOffset = 0;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||
|
||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -418,6 +385,35 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gzndltBEzWc",
|
||||
ExportName = "sceAmprCommandBufferGetNumCommands",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferGetNumCommands(CpuContext ctx)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
if (commandBuffer == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong commandCount;
|
||||
lock (state)
|
||||
{
|
||||
commandCount = state.CommandCount;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "get_num_commands", commandBuffer, commandCount, 0);
|
||||
ctx[CpuRegister.Rax] = commandCount;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "H896Pt-yB4I",
|
||||
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
|
||||
@@ -533,7 +529,7 @@ public static class AmprExports
|
||||
var offset = 0UL;
|
||||
while (offset < writeOffset)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
||||
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -649,6 +645,7 @@ public static class AmprExports
|
||||
state.Buffer = buffer;
|
||||
state.Size = size;
|
||||
state.WriteOffset = writeOffset;
|
||||
state.CommandCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,6 +678,7 @@ public static class AmprExports
|
||||
state.Buffer = buffer;
|
||||
state.Size = size;
|
||||
state.WriteOffset = 0;
|
||||
state.CommandCount = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -911,6 +909,7 @@ public static class AmprExports
|
||||
}
|
||||
|
||||
state.WriteOffset += recordSize;
|
||||
state.CommandCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -963,6 +962,19 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AjmExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
|
||||
private static int _nextContextId;
|
||||
public static int AjmInitialize(CpuContext ctx)
|
||||
{
|
||||
var reserved = ctx[CpuRegister.Rdi];
|
||||
var outputAddress = ctx[CpuRegister.Rsi];
|
||||
if (reserved != 0 || outputAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
|
||||
if (!ctx.Memory.TryWrite(outputAddress, value))
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
Contexts[contextId] = 0;
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MHur6qCsUus",
|
||||
ExportName = "sceAjmFinalize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmFinalize(CpuContext ctx)
|
||||
{
|
||||
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Q3dyFuwGn64",
|
||||
ExportName = "sceAjmModuleRegister",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmModuleRegister(CpuContext ctx)
|
||||
{
|
||||
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var reserved = ctx[CpuRegister.Rdx];
|
||||
if (reserved != 0 || !Contexts.ContainsKey(contextId))
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wi7DtlLV+KI",
|
||||
ExportName = "sceAjmModuleUnregister",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmModuleUnregister(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MmpF1XsQiHw",
|
||||
ExportName = "sceAjmBatchInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchInitialize(CpuContext ctx)
|
||||
{
|
||||
// The caller owns and initializes the batch storage. This API resets
|
||||
// its submission cursor on hardware; FMOD does not consume a return
|
||||
// value or an additional output object here.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,70 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOut2Exports
|
||||
{
|
||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||
// populate.
|
||||
private const int AudioOut2ContextParamSize = 0x30;
|
||||
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
|
||||
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately
|
||||
// following the 0x40-byte parameter block.
|
||||
private const int AudioOut2ContextParamSize = 0x40;
|
||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||
private static long _nextContextHandle = 1;
|
||||
private static long _nextUserHandle = 1;
|
||||
private static int _nextPortId;
|
||||
private static long _pushTraceCount;
|
||||
|
||||
// Per-context audio parameters captured at ContextCreate so ContextAdvance
|
||||
// can pace to the real playback cadence (grain samples at the sample rate).
|
||||
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
|
||||
|
||||
private sealed class ContextState
|
||||
{
|
||||
private readonly object _paceGate = new();
|
||||
private long _nextAdvanceTimestamp;
|
||||
|
||||
public ContextState(uint frequency, uint channels, uint grainSamples)
|
||||
{
|
||||
Frequency = frequency == 0 ? 48000 : frequency;
|
||||
Channels = channels == 0 ? 2 : channels;
|
||||
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
|
||||
}
|
||||
|
||||
public uint Frequency { get; }
|
||||
public uint Channels { get; }
|
||||
public uint GrainSamples { get; }
|
||||
|
||||
// Blocks the advancing thread until one grain worth of wall-clock time
|
||||
// has elapsed since the previous advance, matching hardware timing so
|
||||
// audio-gated titles neither spin nor drift ahead.
|
||||
public void PaceAdvance()
|
||||
{
|
||||
long delay;
|
||||
lock (_paceGate)
|
||||
{
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (_nextAdvanceTimestamp < now)
|
||||
{
|
||||
_nextAdvanceTimestamp = now;
|
||||
}
|
||||
|
||||
delay = _nextAdvanceTimestamp - now;
|
||||
_nextAdvanceTimestamp += checked(
|
||||
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
|
||||
}
|
||||
|
||||
if (delay > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "g2tViFIohHE",
|
||||
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
@@ -52,8 +100,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
memoryInfo.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -88,13 +143,34 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Read channels/frequency/grain from the reset-param blob so the
|
||||
// context can pace advances to the real audio cadence.
|
||||
uint channels = 2;
|
||||
uint frequency = 48000;
|
||||
uint grain = 256;
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
if (ctx.Memory.TryRead(paramAddress, param))
|
||||
{
|
||||
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
|
||||
if (pc is > 0 and <= 8) channels = pc;
|
||||
if (pf is >= 8000 and <= 192000) frequency = pf;
|
||||
// Values below one cache line are flags/counts in observed PS5
|
||||
// callers, not audio grains. Keep the hardware-sized default.
|
||||
if (pg is >= 64 and <= 0x4000) grain = pg;
|
||||
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
Contexts[handle] = new ContextState(frequency, channels, grain);
|
||||
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
|
||||
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||
{
|
||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DxGyV8dtOR8",
|
||||
ExportName = "sceAudioOut2ContextBedWrite",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aII9h5nli9U",
|
||||
ExportName = "sceAudioOut2ContextPush",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextPush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var traceCount = Interlocked.Increment(ref _pushTraceCount);
|
||||
if (traceCount <= 16)
|
||||
{
|
||||
TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
|
||||
}
|
||||
|
||||
if (Contexts.TryGetValue(handle, out var context))
|
||||
{
|
||||
// FMOD's PS5 output path uses ContextPush as the submission clock
|
||||
// and does not call ContextAdvance. Pace pushes to one hardware
|
||||
// grain so the feeder cannot outrun playback and starve the game.
|
||||
context.PaceAdvance();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PE2zHMqLSHs",
|
||||
ExportName = "sceAudioOut2ContextAdvance",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextAdvance(CpuContext ctx)
|
||||
{
|
||||
// Advancing renders one grain of audio on hardware; pace it to the same
|
||||
// wall-clock cadence so the guest audio thread runs at the right speed.
|
||||
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
|
||||
{
|
||||
context.PaceAdvance();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "R7d0F1g2qsU",
|
||||
ExportName = "sceAudioOut2ContextGetQueueLevel",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
|
||||
{
|
||||
// The advance path paces synchronously, so the queue is always drained.
|
||||
var levelAddress = ctx[CpuRegister.Rsi];
|
||||
if (levelAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64(ctx, levelAddress, 0);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JK2wamZPzwM",
|
||||
@@ -117,16 +263,23 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8XTArSPyWHk",
|
||||
ExportName = "sceAudioOut2PortSetAttributes",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gatEUKG+Ea4",
|
||||
ExportName = "sceAudioOut2PortGetState",
|
||||
@@ -138,7 +291,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle == 0 || stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var type = (int)((handle >> 16) & 0xFF);
|
||||
@@ -151,8 +304,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
@@ -175,8 +328,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -184,14 +337,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IaZXJ9M79uo",
|
||||
ExportName = "sceAudioOut2UserDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
|
||||
outUserAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceAudioOut2(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public static class AudioOutExports
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public IHostAudioStream? Backend { get; }
|
||||
public volatile float Volume = 1.0f;
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
|
||||
@@ -198,7 +199,8 @@ public static class AudioOutExports
|
||||
checked((int)port.BufferLength),
|
||||
port.Channels,
|
||||
port.BytesPerSample,
|
||||
port.IsFloat);
|
||||
port.IsFloat,
|
||||
port.Volume);
|
||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||
{
|
||||
port.PaceSilence();
|
||||
@@ -225,10 +227,43 @@ public static class AudioOutExports
|
||||
public static int AudioOutSetVolume(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return ctx.SetReturn(
|
||||
Ports.ContainsKey(handle)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var volumeArrayAddress = ctx[CpuRegister.Rdx];
|
||||
if (!Ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
const int unityVolume = 32768;
|
||||
var maxVolume = 0;
|
||||
var found = false;
|
||||
if (volumeArrayAddress != 0)
|
||||
{
|
||||
Span<byte> raw = stackalloc byte[sizeof(int)];
|
||||
for (var channel = 0; channel < 8; channel++)
|
||||
{
|
||||
if ((channelFlags & (1u << channel)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
|
||||
maxVolume = Math.Max(maxVolume, value);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
public static void ShutdownAllPorts()
|
||||
|
||||
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
bool isFloat,
|
||||
float volume)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
left = ApplyVolume(left, volume);
|
||||
right = ApplyVolume(right, volume);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||
}
|
||||
@@ -52,4 +55,10 @@ internal static class AudioPcmConversion
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
|
||||
private static short ApplyVolume(short sample, float volume)
|
||||
{
|
||||
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
|
||||
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class FmodCompatExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uPLTdl3psGk",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libfmod")]
|
||||
public static int FmodSystemSetOutput(CpuContext ctx)
|
||||
{
|
||||
var system = ctx[CpuRegister.Rdi];
|
||||
var output = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
|
||||
var resetPrematureInit = callCount <= 2;
|
||||
if (resetPrematureInit && system != 0)
|
||||
{
|
||||
Span<byte> zeroByte = stackalloc byte[1];
|
||||
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
|
||||
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
|
||||
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
|
||||
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
|
||||
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
|
||||
}
|
||||
|
||||
// The PS5 FMOD object arrives with its initialized byte set before Unity has
|
||||
// applied the startup output configuration. Preserve those settings while
|
||||
// clearing the premature marker so Studio can execute the real core init.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,403 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
/// <summary>
|
||||
/// Optional host-side Bink 2 bridge for games that ship a static Bink player.
|
||||
///
|
||||
/// The game in that case never imports libSceVideodec, so an HLE video-decoder
|
||||
/// export cannot see its movie frames. Kernel file opens identify the active
|
||||
/// .bk2 file and the presenter requests BGRA frames from a tiny native adapter.
|
||||
/// The adapter is deliberately a separate, user-supplied library: Bink 2 is a
|
||||
/// proprietary SDK and SharpEmu must neither bundle it nor depend on its ABI.
|
||||
/// </summary>
|
||||
internal static class Bink2MovieBridge
|
||||
{
|
||||
private const uint MaxDimension = 16384;
|
||||
private static readonly object Gate = new();
|
||||
private static NativeAdapter? _adapter;
|
||||
private static string? _activePath;
|
||||
private static IntPtr _activeMovie;
|
||||
private static Bink2MovieInfo _activeInfo;
|
||||
private static byte[]? _frameBuffer;
|
||||
private static bool _usingDummyMovie;
|
||||
private static bool _loadAttempted;
|
||||
private static bool _availabilityReported;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the guest should receive a normal "file not found"
|
||||
/// result for a Bink movie. This is the safe default without a decoder:
|
||||
/// games that treat movies as optional fall through to their next state
|
||||
/// rather than submitting an empty Bink GPU texture forever.
|
||||
/// </summary>
|
||||
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
||||
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||
ResolveMode() == MovieMode.Skip;
|
||||
|
||||
internal static void ObserveGuestMovie(string hostPath)
|
||||
{
|
||||
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||
!File.Exists(hostPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ResolveMode() == MovieMode.Dummy)
|
||||
{
|
||||
AttachDummyMovieLocked(hostPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var adapter = GetAdapterLocked();
|
||||
if (adapter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseActiveLocked();
|
||||
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValid(info))
|
||||
{
|
||||
adapter.Close(movie);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activePath = hostPath;
|
||||
_activeMovie = movie;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryDecodeNextFrame(
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
height = 0;
|
||||
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||
{
|
||||
if (_usingDummyMovie && _frameBuffer is not null)
|
||||
{
|
||||
pixels = _frameBuffer;
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* destination = _frameBuffer)
|
||||
{
|
||||
if (!_adapter.DecodeNextBgra(
|
||||
_activeMovie,
|
||||
(IntPtr)destination,
|
||||
_activeInfo.Width * 4,
|
||||
(uint)_frameBuffer.Length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels = _frameBuffer;
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValid(Bink2MovieInfo info) =>
|
||||
info.Width > 0 && info.Height > 0 &&
|
||||
info.Width <= MaxDimension && info.Height <= MaxDimension &&
|
||||
(ulong)info.Width * info.Height * 4 <= int.MaxValue;
|
||||
|
||||
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||
checked((int)((ulong)info.Width * info.Height * 4));
|
||||
|
||||
private static MovieMode ResolveMode()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||
if (string.Equals(configured, "dummy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Dummy;
|
||||
}
|
||||
|
||||
if (string.Equals(configured, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Native;
|
||||
}
|
||||
|
||||
if (string.Equals(configured, "skip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Skip;
|
||||
}
|
||||
|
||||
// With no SDK adapter present, returning "not found" makes optional
|
||||
// cinematics advance. Supplying either an explicit path or the normal
|
||||
// side-by-side adapter enables native playback automatically.
|
||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||
EnumerateAdapterCandidates().Any(File.Exists))
|
||||
{
|
||||
return MovieMode.Native;
|
||||
}
|
||||
|
||||
return MovieMode.Skip;
|
||||
}
|
||||
|
||||
private static void AttachDummyMovieLocked(string hostPath)
|
||||
{
|
||||
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink dummy could not read movie header '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
CloseActiveLocked();
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||
_usingDummyMovie = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + ".");
|
||||
}
|
||||
|
||||
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
{
|
||||
info = default;
|
||||
Span<byte> header = stackalloc byte[32];
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
if (stream.Read(header) != header.Length ||
|
||||
!header[..4].SequenceEqual("KB2j"u8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
info = new Bink2MovieInfo(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||
1);
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillDummyFrame(byte[] pixels, uint width, uint height)
|
||||
{
|
||||
for (var y = 0u; y < height; y++)
|
||||
{
|
||||
for (var x = 0u; x < width; x++)
|
||||
{
|
||||
var offset = checked((int)(((ulong)y * width + x) * 4));
|
||||
var band = ((x / 96) + (y / 96)) & 1;
|
||||
pixels[offset] = band == 0 ? (byte)0x28 : (byte)0x18;
|
||||
pixels[offset + 1] = band == 0 ? (byte)0x18 : (byte)0x28;
|
||||
pixels[offset + 2] = 0x10;
|
||||
pixels[offset + 3] = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeAdapter? GetAdapterLocked()
|
||||
{
|
||||
if (_loadAttempted)
|
||||
{
|
||||
return _adapter;
|
||||
}
|
||||
|
||||
_loadAttempted = true;
|
||||
foreach (var candidate in EnumerateAdapterCandidates())
|
||||
{
|
||||
if (!NativeLibrary.TryLoad(candidate, out var library))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (NativeAdapter.TryCreate(library, out var adapter))
|
||||
{
|
||||
_adapter = adapter;
|
||||
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(library);
|
||||
}
|
||||
|
||||
if (!_availabilityReported)
|
||||
{
|
||||
_availabilityReported = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateAdapterCandidates()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
yield return configured;
|
||||
}
|
||||
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseActiveLocked()
|
||||
{
|
||||
if (_activeMovie != IntPtr.Zero)
|
||||
{
|
||||
_adapter?.Close(_activeMovie);
|
||||
}
|
||||
|
||||
_activePath = null;
|
||||
_activeMovie = IntPtr.Zero;
|
||||
_activeInfo = default;
|
||||
_frameBuffer = null;
|
||||
_usingDummyMovie = false;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private readonly struct Bink2MovieInfo
|
||||
{
|
||||
public readonly uint Width;
|
||||
public readonly uint Height;
|
||||
public readonly uint FramesPerSecondNumerator;
|
||||
public readonly uint FramesPerSecondDenominator;
|
||||
|
||||
internal Bink2MovieInfo(
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
}
|
||||
}
|
||||
|
||||
private enum MovieMode
|
||||
{
|
||||
Skip,
|
||||
Dummy,
|
||||
Native,
|
||||
}
|
||||
|
||||
private sealed class NativeAdapter
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void CloseDelegate(IntPtr movie);
|
||||
|
||||
private readonly OpenUtf8Delegate _openUtf8;
|
||||
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
||||
private readonly CloseDelegate _close;
|
||||
|
||||
private NativeAdapter(
|
||||
OpenUtf8Delegate openUtf8,
|
||||
DecodeNextBgraDelegate decodeNextBgra,
|
||||
CloseDelegate close)
|
||||
{
|
||||
_openUtf8 = openUtf8;
|
||||
_decodeNextBgra = decodeNextBgra;
|
||||
_close = close;
|
||||
}
|
||||
|
||||
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||
{
|
||||
adapter = null;
|
||||
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
adapter = new NativeAdapter(
|
||||
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||
{
|
||||
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||
try
|
||||
{
|
||||
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(utf8);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
||||
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
||||
|
||||
internal void Close(IntPtr movie) => _close(movie);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Codec;
|
||||
|
||||
/// <summary>
|
||||
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
|
||||
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
|
||||
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
|
||||
/// and report "no output produced" so guests advance instead of failing on
|
||||
/// unresolved imports.
|
||||
/// </summary>
|
||||
public static class CodecExports
|
||||
{
|
||||
private const int Ok = 0;
|
||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
|
||||
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
|
||||
private static long _nextHandle = 1;
|
||||
|
||||
// ---- Video decoder ----
|
||||
|
||||
[SysAbiExport(Nid = "qkgRiwHyheU", ExportName = "sceVideodecCreateDecoder",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||
public static int VideodecCreateDecoder(CpuContext ctx)
|
||||
{
|
||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||
VideoDecoders[handle] = 1;
|
||||
return TryWriteHandle(ctx, outHandleAddress, handle) ? SetReturn(ctx, Ok) : SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "q0W5GJMovMs", ExportName = "sceVideodecDecode",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||
public static int VideodecDecode(CpuContext ctx)
|
||||
{
|
||||
// No decoder is present: report success with no picture produced.
|
||||
return SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "jeigLlKdp5I", ExportName = "sceVideodecFlush",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||
public static int VideodecFlush(CpuContext ctx) =>
|
||||
SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||
|
||||
[SysAbiExport(Nid = "U0kpGF1cl90", ExportName = "sceVideodecDeleteDecoder",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||
public static int VideodecDeleteDecoder(CpuContext ctx)
|
||||
{
|
||||
VideoDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// ---- Audio decoder ----
|
||||
|
||||
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||
public static int AudiodecCreateDecoder(CpuContext ctx)
|
||||
{
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||
AudioDecoders[handle] = 1;
|
||||
// sceAudiodec returns the handle directly (>= 0) or a negative error.
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return unchecked((int)handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||
public static int AudiodecDecode(CpuContext ctx)
|
||||
{
|
||||
// No decoder present: report success with zero output samples so the
|
||||
// caller treats the frame as silent rather than erroring.
|
||||
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||
public static int AudiodecDeleteDecoder(CpuContext ctx)
|
||||
{
|
||||
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
|
||||
ctx.TryWriteUInt64(address, handle);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
/// <summary>
|
||||
/// libSceErrorDialog. There is no host error dialog, so an opened dialog
|
||||
/// immediately reports finished — this keeps guests that block on the dialog
|
||||
/// status (a common fatal-error path) from spinning forever.
|
||||
/// </summary>
|
||||
public static class ErrorDialogExports
|
||||
{
|
||||
private const int AlreadyInitialized = unchecked((int)0x80ED0001);
|
||||
private const int NotInitialized = unchecked((int)0x80ED0002);
|
||||
private const int ArgNull = unchecked((int)0x80ED0005);
|
||||
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusFinished = 3;
|
||||
|
||||
private static int _initialized;
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "I88KChlynSs",
|
||||
ExportName = "sceErrorDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0 ? 0 : AlreadyInitialized;
|
||||
if (result == 0)
|
||||
{
|
||||
Volatile.Write(ref _status, StatusInitialized);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, result);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "M2ZF-ClLhgY",
|
||||
ExportName = "sceErrorDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogOpen(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return SetReturn(ctx, ArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _initialized) == 0)
|
||||
{
|
||||
return SetReturn(ctx, NotInitialized);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "t2FvHRXzgqk",
|
||||
ExportName = "sceErrorDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WWiGuh9XfgQ",
|
||||
ExportName = "sceErrorDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ekXHb1kDBl0",
|
||||
ExportName = "sceErrorDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogClose(CpuContext ctx)
|
||||
{
|
||||
Volatile.Write(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9XAxK2PMwk8",
|
||||
ExportName = "sceErrorDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceErrorDialog")]
|
||||
public static int ErrorDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
Volatile.Write(ref _status, StatusNone);
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -48,19 +48,25 @@ public static class DiscMapExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "fJgP+wqifno",
|
||||
ExportName = "sceDiscMapUnknownFJgP",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "ioKMruft1ek",
|
||||
ExportName = "sceDiscMapUnknownIoKM",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
private static int WriteMappingTriple(CpuContext ctx, string exportName)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
@@ -22,6 +23,10 @@ public static class FiberExports
|
||||
private const uint FiberStateIdle = 2;
|
||||
private const uint FiberStateTerminated = 3;
|
||||
private const uint FiberFlagContextSizeCheck = 0x10;
|
||||
private const uint FiberFlagSetFpuRegs = 0x100;
|
||||
private const uint Firmware350BuildVersion = 0x03500000;
|
||||
private const ushort InitialFpuControlWord = 0x037F;
|
||||
private const uint InitialMxcsr = 0x9FC0;
|
||||
|
||||
private const int FiberErrorNull = unchecked((int)0x80590001);
|
||||
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
||||
@@ -45,13 +50,26 @@ public static class FiberExports
|
||||
|
||||
private static int _contextSizeCheck;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentFiberAddress;
|
||||
|
||||
private static readonly object _fiberGate = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberThreadState> _threadStates = new();
|
||||
|
||||
static FiberExports()
|
||||
{
|
||||
RunFiberSelfChecks();
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
lock (_fiberGate)
|
||||
{
|
||||
_continuations.Clear();
|
||||
_stackRanges.Clear();
|
||||
_threadStates.Clear();
|
||||
Volatile.Write(ref _contextSizeCheck, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hVYD7Ou2pCQ",
|
||||
@@ -61,6 +79,7 @@ public static class FiberExports
|
||||
public static int FiberInitialize(CpuContext ctx)
|
||||
{
|
||||
var optParam = ReadStackArg64(ctx, 0);
|
||||
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||
return FiberInitializeCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
@@ -70,7 +89,8 @@ public static class FiberExports
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
optParam,
|
||||
flags: 0);
|
||||
flags: 0,
|
||||
buildVersion);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -82,6 +102,7 @@ public static class FiberExports
|
||||
{
|
||||
var optParam = ReadStackArg64(ctx, 0);
|
||||
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 2));
|
||||
return FiberInitializeCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
@@ -91,7 +112,8 @@ public static class FiberExports
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
optParam,
|
||||
flags);
|
||||
flags,
|
||||
buildVersion);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -104,17 +126,17 @@ public static class FiberExports
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -127,24 +149,23 @@ public static class FiberExports
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (state != FiberStateIdle)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return ctx.SetReturn(0);
|
||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -229,74 +250,47 @@ public static class FiberExports
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
ulong previousFiber;
|
||||
lock (_fiberGate)
|
||||
{
|
||||
_continuations[fiberAddress] = new FiberContinuation(
|
||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||
argOnRunAddress);
|
||||
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
var threadKey = GetThreadKey(ctx);
|
||||
if (!_threadStates.TryGetValue(threadKey, out var threadState) ||
|
||||
!TryWriteResumeArgument(ctx, threadState.RootContinuation, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
if (previousFiber != 0)
|
||||
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!returnTarget.ThreadContinuation.HasValue ||
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
transferTarget = threadState.RootContinuation.Context with { Rax = 0 };
|
||||
_threadStates.TryRemove(threadKey, out _);
|
||||
}
|
||||
|
||||
_currentFiberAddress = previousFiber;
|
||||
_ = GuestThreadExecution.EnterFiber(previousFiber);
|
||||
_ = GuestThreadExecution.EnterFiber(0);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
TraceFiber(
|
||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||
$"return-to-thread fiber=0x{fiberAddress:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -309,18 +303,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -334,35 +328,35 @@ public static class FiberExports
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
||||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -376,22 +370,22 @@ public static class FiberExports
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -403,12 +397,12 @@ public static class FiberExports
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -419,8 +413,8 @@ public static class FiberExports
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -433,17 +427,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0 ||
|
||||
!_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt64(ctx, outAddress, threadState.RootContinuation.Context.Rbp)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
@@ -455,69 +450,70 @@ public static class FiberExports
|
||||
ulong contextAddress,
|
||||
ulong contextSize,
|
||||
ulong optParam,
|
||||
uint flags)
|
||||
uint flags,
|
||||
uint buildVersion)
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
return SetReturn(ctx, FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||
{
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
flags = ApplyInitializationFlags(
|
||||
flags,
|
||||
buildVersion,
|
||||
Volatile.Read(ref _contextSizeCheck) != 0);
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||
@@ -531,8 +527,10 @@ public static class FiberExports
|
||||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||
}
|
||||
|
||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||
return ctx.SetReturn(0);
|
||||
TraceFiber(
|
||||
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
|
||||
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
@@ -547,12 +545,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
@@ -560,7 +558,7 @@ public static class FiberExports
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return ctx.SetReturn(attachResult);
|
||||
return SetReturn(ctx, attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,78 +566,96 @@ public static class FiberExports
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
var resumed = false;
|
||||
lock (_fiberGate)
|
||||
{
|
||||
var threadKey = GetThreadKey(ctx);
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
FiberContinuation targetContinuation;
|
||||
FiberContinuation targetContinuation = default;
|
||||
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
||||
{
|
||||
targetContinuation = savedContinuation;
|
||||
resumed = true;
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||
outArgumentAddress);
|
||||
|
||||
if (!resumed)
|
||||
{
|
||||
var rootStackTop = _threadStates.TryGetValue(threadKey, out var existingThreadState)
|
||||
? existingThreadState.RootContinuation.Context.Rsp
|
||||
: callerContinuation.Context.Rsp;
|
||||
if (!TryCreateInitialContinuation(
|
||||
ctx,
|
||||
fields,
|
||||
argOnRun,
|
||||
rootStackTop,
|
||||
out targetContinuation))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
else if (!TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||
previousState != FiberStateRun ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||
if (_threadStates.ContainsKey(threadKey))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
_threadStates[threadKey] = new FiberThreadState(callerContinuation, fiber, previousFiber);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
_continuations.TryRemove(previousFiber, out _);
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
else
|
||||
{
|
||||
_threadStates.TryRemove(threadKey, out _);
|
||||
}
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
@@ -648,35 +664,66 @@ public static class FiberExports
|
||||
}
|
||||
|
||||
transferTarget = targetContinuation.Context with { Rax = 0 };
|
||||
if (_threadStates.TryGetValue(threadKey, out var activeState))
|
||||
{
|
||||
_threadStates[threadKey] = activeState with
|
||||
{
|
||||
CurrentFiber = fiber,
|
||||
PreviousFiber = previousFiber,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_currentFiberAddress = fiber;
|
||||
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
CpuContext ctx,
|
||||
FiberFields fields,
|
||||
ulong argOnRun,
|
||||
ulong rootStackTop,
|
||||
out FiberContinuation continuation)
|
||||
{
|
||||
continuation = default;
|
||||
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
|
||||
ulong stackEnd;
|
||||
if (fields.ContextAddress == 0)
|
||||
{
|
||||
if (rootStackTop < 32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Contextless fibers are specified to execute on the root
|
||||
// sceFiberRun stack. Keep the suspended import return record
|
||||
// above the entry stack and use a synthetic transfer slot below it.
|
||||
stackEnd = rootStackTop & ~15UL;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fields.ContextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
}
|
||||
|
||||
if (!TryCalculateInitialStackLayout(stackEnd, out var entryRsp, out var transferSlot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!TryWriteUInt64(ctx, transferSlot, fields.Entry) ||
|
||||
!TryWriteUInt64(ctx, entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var setFpuRegisters = (fields.Flags & FiberFlagSetFpuRegs) != 0;
|
||||
|
||||
continuation = new FiberContinuation(
|
||||
new GuestCpuContinuation(
|
||||
@@ -698,7 +745,12 @@ public static class FiberExports
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
setFpuRegisters ? InitialFpuControlWord : ctx.FpuControlWord,
|
||||
setFpuRegisters ? InitialMxcsr : ctx.Mxcsr,
|
||||
RestoreFullFpuState: setFpuRegisters),
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
@@ -708,7 +760,7 @@ public static class FiberExports
|
||||
FiberContinuation continuation,
|
||||
ulong argument) =>
|
||||
continuation.ArgOnRunAddress == 0 ||
|
||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||
|
||||
private static GuestCpuContinuation CaptureContinuation(
|
||||
CpuContext ctx,
|
||||
@@ -731,26 +783,99 @@ public static class FiberExports
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
ctx[CpuRegister.R10],
|
||||
ctx[CpuRegister.R11],
|
||||
ctx[CpuRegister.R12],
|
||||
ctx[CpuRegister.R13],
|
||||
ctx[CpuRegister.R14],
|
||||
ctx[CpuRegister.R15]);
|
||||
ctx[CpuRegister.R15],
|
||||
ctx.FpuControlWord,
|
||||
ctx.Mxcsr,
|
||||
RestoreFullFpuState: false);
|
||||
|
||||
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
||||
{
|
||||
if (_currentFiberAddress != 0)
|
||||
{
|
||||
return _currentFiberAddress;
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
||||
{
|
||||
return GuestThreadExecution.CurrentFiberAddress;
|
||||
}
|
||||
|
||||
if (_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState) &&
|
||||
threadState.CurrentFiber != 0)
|
||||
{
|
||||
return threadState.CurrentFiber;
|
||||
}
|
||||
|
||||
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
||||
}
|
||||
|
||||
private static ulong GetThreadKey(CpuContext ctx)
|
||||
{
|
||||
var handle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (handle != 0)
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
|
||||
// The main guest thread does not always have a scheduler handle. Its
|
||||
// ABI thread pointer is stable across native/managed transitions.
|
||||
return ctx.FsBase != 0 ? ctx.FsBase : 1;
|
||||
}
|
||||
|
||||
private static uint ApplyInitializationFlags(
|
||||
uint flags,
|
||||
uint buildVersion,
|
||||
bool contextSizeCheck)
|
||||
{
|
||||
if (buildVersion >= Firmware350BuildVersion)
|
||||
{
|
||||
flags |= FiberFlagSetFpuRegs;
|
||||
}
|
||||
if (contextSizeCheck)
|
||||
{
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static bool TryCalculateInitialStackLayout(
|
||||
ulong stackEnd,
|
||||
out ulong entryRsp,
|
||||
out ulong transferSlot)
|
||||
{
|
||||
var alignedEnd = stackEnd & ~15UL;
|
||||
if (alignedEnd < 2 * sizeof(ulong))
|
||||
{
|
||||
entryRsp = 0;
|
||||
transferSlot = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
entryRsp = alignedEnd - sizeof(ulong);
|
||||
transferSlot = entryRsp - sizeof(ulong);
|
||||
return true;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void RunFiberSelfChecks()
|
||||
{
|
||||
Debug.Assert(
|
||||
ApplyInitializationFlags(0, Firmware350BuildVersion - 1, false) == 0,
|
||||
"Pre-3.50 fibers unexpectedly enable SetFpuRegs.");
|
||||
Debug.Assert(
|
||||
ApplyInitializationFlags(0, Firmware350BuildVersion, false) == FiberFlagSetFpuRegs,
|
||||
"3.50+ fibers must initialize x87/MXCSR control state.");
|
||||
Debug.Assert(
|
||||
ApplyInitializationFlags(1, Firmware350BuildVersion, true) ==
|
||||
(1u | FiberFlagSetFpuRegs | FiberFlagContextSizeCheck),
|
||||
"Fiber initialization discarded caller/internal flags.");
|
||||
Debug.Assert(
|
||||
TryCalculateInitialStackLayout(0x1017, out var rsp, out var slot) &&
|
||||
rsp == 0x1008 && slot == 0x1000 && (rsp & 15) == 8,
|
||||
"Initial fiber transfer slot does not produce SysV entry alignment.");
|
||||
}
|
||||
|
||||
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
||||
ResolveCurrentFiberAddress(ctx);
|
||||
|
||||
@@ -779,11 +904,11 @@ public static class FiberExports
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
{
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
@@ -839,8 +964,8 @@ public static class FiberExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
magicStart != FiberSignature0 ||
|
||||
magicEnd != FiberSignature1)
|
||||
{
|
||||
@@ -855,12 +980,12 @@ public static class FiberExports
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||
{
|
||||
return false;
|
||||
@@ -898,6 +1023,46 @@ public static class FiberExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
@@ -925,6 +1090,37 @@ public static class FiberExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
@@ -946,9 +1142,10 @@ public static class FiberExports
|
||||
GuestCpuContinuation Context,
|
||||
ulong ArgOnRunAddress);
|
||||
|
||||
private readonly record struct FiberReturnTarget(
|
||||
ulong PreviousFiber,
|
||||
FiberContinuation? ThreadContinuation);
|
||||
private sealed record FiberThreadState(
|
||||
FiberContinuation RootContinuation,
|
||||
ulong CurrentFiber,
|
||||
ulong PreviousFiber);
|
||||
|
||||
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Font;
|
||||
|
||||
public static class FontExports
|
||||
{
|
||||
private static readonly object AllocationGate = new();
|
||||
private static ulong _librarySelectionAddress;
|
||||
private static ulong _rendererSelectionAddress;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "whrS4oksXc4",
|
||||
ExportName = "sceFontMemoryInit",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int MemoryInit(CpuContext ctx)
|
||||
{
|
||||
var descriptorAddress = ctx[CpuRegister.Rdi];
|
||||
var regionAddress = ctx[CpuRegister.Rsi];
|
||||
var regionSize = (uint)ctx[CpuRegister.Rdx];
|
||||
var interfaceAddress = ctx[CpuRegister.Rcx];
|
||||
var mspaceAddress = ctx[CpuRegister.R8];
|
||||
var destroyCallback = ctx[CpuRegister.R9];
|
||||
if (descriptorAddress == 0 ||
|
||||
!TryWriteUInt32(ctx, descriptorAddress, 0x00000F00) ||
|
||||
!TryWriteUInt32(ctx, descriptorAddress + 0x04, regionSize) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x08, regionAddress) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x10, mspaceAddress) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x18, interfaceAddress) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x20, destroyCallback) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x28, 0) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt64(descriptorAddress + 0x38, mspaceAddress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "oM+XCzVG3oM",
|
||||
ExportName = "sceFontSelectLibraryFt",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFontFt")]
|
||||
public static int SelectLibraryFt(CpuContext ctx) =>
|
||||
ReturnSelection(ctx, ref _librarySelectionAddress, 0x38);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Xx974EW-QFY",
|
||||
ExportName = "sceFontSelectRendererFt",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFontFt")]
|
||||
public static int SelectRendererFt(CpuContext ctx) =>
|
||||
ReturnSelection(ctx, ref _rendererSelectionAddress, 0x100);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "n590hj5Oe-k",
|
||||
ExportName = "sceFontCreateLibraryWithEdition",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int CreateLibraryWithEdition(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F01);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WaSFJoRWXaI",
|
||||
ExportName = "sceFontCreateRendererWithEdition",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int CreateRendererWithEdition(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cKYtVmeSTcw",
|
||||
ExportName = "sceFontOpenFontSet",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int OpenFontSet(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KXUpebrFk1U",
|
||||
ExportName = "sceFontOpenFontMemory",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int OpenFontMemory(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JzCH3SCFnAU",
|
||||
ExportName = "sceFontOpenFontInstance",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int OpenFontInstance(CpuContext ctx)
|
||||
{
|
||||
var sourceHandle = ctx[CpuRegister.Rdi];
|
||||
var setupHandle = ctx[CpuRegister.Rsi];
|
||||
var outputAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (setupHandle != 0)
|
||||
{
|
||||
return ctx.TryWriteUInt64(outputAddress, setupHandle)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (!TryAllocateOpaque(ctx, 0x100, out var handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (sourceHandle != 0)
|
||||
{
|
||||
Span<byte> source = stackalloc byte[0x100];
|
||||
if (ctx.Memory.TryRead(sourceHandle, source))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(handle, source);
|
||||
}
|
||||
}
|
||||
|
||||
_ = TryWriteUInt16(ctx, handle, 0x0F02);
|
||||
return ctx.TryWriteUInt64(outputAddress, handle)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "SsRbbCiWoGw",
|
||||
ExportName = "sceFontSupportSystemFonts",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SupportSystemFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mz2iTY0MK4A",
|
||||
ExportName = "sceFontSupportExternalFonts",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SupportExternalFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CUKn5pX-NVY",
|
||||
ExportName = "sceFontAttachDeviceCacheBuffer",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int AttachDeviceCacheBuffer(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IQtleGLL5pQ",
|
||||
ExportName = "sceFontGetRenderCharGlyphMetrics",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int GetRenderCharGlyphMetrics(CpuContext ctx)
|
||||
{
|
||||
var metricsAddress = ctx[CpuRegister.Rdx];
|
||||
if (metricsAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
ctx,
|
||||
metricsAddress + (ulong)(index * sizeof(float)),
|
||||
BitConverter.SingleToUInt32Bits(values[index])))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gdUCnU0gHdI",
|
||||
ExportName = "sceFontRenderSurfaceInit",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int RenderSurfaceInit(CpuContext ctx)
|
||||
{
|
||||
var surfaceAddress = ctx[CpuRegister.Rdi];
|
||||
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||
var widthBytes = (uint)ctx[CpuRegister.Rdx];
|
||||
var pixelBytes = (uint)ctx[CpuRegister.Rcx] & 0xFF;
|
||||
var width = (uint)ctx[CpuRegister.R8];
|
||||
var height = (uint)ctx[CpuRegister.R9];
|
||||
if (surfaceAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(surfaceAddress, bufferAddress) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x08, widthBytes) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x0C, pixelBytes) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x10, width) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x14, height) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x18, 0) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x1C, 0) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x20, width) ||
|
||||
!TryWriteUInt32(ctx, surfaceAddress + 0x24, height))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
private static int ReturnSelection(CpuContext ctx, ref ulong selectionAddress, uint objectSize)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (AllocationGate)
|
||||
{
|
||||
if (selectionAddress == 0)
|
||||
{
|
||||
if (!TryAllocateOpaque(ctx, 0x20, out selectionAddress) ||
|
||||
!TryWriteUInt32(ctx, selectionAddress, 0) ||
|
||||
!TryWriteUInt32(ctx, selectionAddress + 4, objectSize))
|
||||
{
|
||||
selectionAddress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = selectionAddress;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int CreateOpaqueHandle(CpuContext ctx, ulong outputAddress, int size, ushort magic)
|
||||
{
|
||||
if (outputAddress == 0 || !TryAllocateOpaque(ctx, size, out var handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt16(ctx, handle, magic) || !ctx.TryWriteUInt64(outputAddress, handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
private static bool TryAllocateOpaque(CpuContext ctx, int size, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
|
||||
!allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[size];
|
||||
bytes.Clear();
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetSuccess(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
||||
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
||||
/// Vulkan is the only backend today; Metal/DX12 slot in here.
|
||||
/// </summary>
|
||||
internal static class GuestGpu
|
||||
{
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||
|
||||
public static IGuestGpuBackend Current => Instance.Value;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
// The types that cross the guest-GPU backend seam. Every field is either a neutral
|
||||
// primitive (dimensions, counts, host pixel bytes) or a raw guest/AGC value (guest
|
||||
// addresses, guest format and number-type codes, guest register bitfields). Host
|
||||
// graphics-API values must never appear here: each backend owns the guest -> native
|
||||
// translation for its API.
|
||||
|
||||
/// <summary>A guest texture referenced by a draw or dispatch. Format/NumberType/
|
||||
/// TileMode/DstSelect are raw guest descriptor codes.</summary>
|
||||
internal sealed record GuestDrawTexture(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
byte[] RgbaPixels,
|
||||
bool IsFallback,
|
||||
bool IsStorage,
|
||||
uint MipLevels = 1,
|
||||
uint MipLevel = 0,
|
||||
uint BaseMipLevel = 0,
|
||||
uint ResourceMipLevels = 1,
|
||||
uint Pitch = 0,
|
||||
uint TileMode = 0,
|
||||
uint DstSelect = 0xFAC,
|
||||
GuestSampler Sampler = default);
|
||||
|
||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||
internal readonly record struct GuestSampler(
|
||||
uint Word0,
|
||||
uint Word1,
|
||||
uint Word2,
|
||||
uint Word3);
|
||||
|
||||
internal sealed record GuestMemoryBuffer(
|
||||
ulong BaseAddress,
|
||||
byte[] Data,
|
||||
int Length,
|
||||
bool Pooled,
|
||||
bool Writable = false,
|
||||
bool WriteBackToGuest = true);
|
||||
|
||||
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
||||
internal sealed record GuestVertexBuffer(
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
uint DataFormat,
|
||||
uint NumberFormat,
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint OffsetBytes,
|
||||
byte[] Data,
|
||||
int Length,
|
||||
bool Pooled);
|
||||
|
||||
internal sealed record GuestIndexBuffer(
|
||||
byte[] Data,
|
||||
int Length,
|
||||
bool Is32Bit,
|
||||
bool Pooled);
|
||||
|
||||
internal readonly record struct GuestRect(
|
||||
int X,
|
||||
int Y,
|
||||
uint Width,
|
||||
uint Height);
|
||||
|
||||
internal readonly record struct GuestViewport(
|
||||
float X,
|
||||
float Y,
|
||||
float Width,
|
||||
float Height,
|
||||
float MinDepth,
|
||||
float MaxDepth);
|
||||
|
||||
internal readonly record struct GuestRasterState(
|
||||
bool CullFront,
|
||||
bool CullBack,
|
||||
bool FrontFaceClockwise,
|
||||
bool Wireframe)
|
||||
{
|
||||
public static GuestRasterState Default { get; } = new(false, false, false, false);
|
||||
}
|
||||
|
||||
// CompareOp uses the GCN DB_DEPTH_CONTROL ZFUNC encoding, which matches the
|
||||
// Vulkan CompareOp ordering (0=Never through 7=Always).
|
||||
internal readonly record struct GuestDepthState(
|
||||
bool TestEnable,
|
||||
bool WriteEnable,
|
||||
uint CompareOp,
|
||||
bool ClearEnable = false)
|
||||
{
|
||||
public static GuestDepthState Default { get; } = new(false, false, 7, false);
|
||||
}
|
||||
|
||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
||||
internal readonly record struct GuestBlendState(
|
||||
bool Enable,
|
||||
uint ColorSrcFactor,
|
||||
uint ColorDstFactor,
|
||||
uint ColorFunc,
|
||||
uint AlphaSrcFactor,
|
||||
uint AlphaDstFactor,
|
||||
uint AlphaFunc,
|
||||
bool SeparateAlphaBlend,
|
||||
uint WriteMask)
|
||||
{
|
||||
public static GuestBlendState Default { get; } = new(
|
||||
Enable: false,
|
||||
ColorSrcFactor: 1,
|
||||
ColorDstFactor: 0,
|
||||
ColorFunc: 0,
|
||||
AlphaSrcFactor: 1,
|
||||
AlphaDstFactor: 0,
|
||||
AlphaFunc: 0,
|
||||
SeparateAlphaBlend: false,
|
||||
WriteMask: 0xFu);
|
||||
}
|
||||
|
||||
internal sealed record GuestRenderState(
|
||||
IReadOnlyList<GuestBlendState> Blends,
|
||||
GuestRect? Scissor,
|
||||
GuestViewport? Viewport,
|
||||
GuestRasterState Raster,
|
||||
GuestDepthState Depth)
|
||||
{
|
||||
public static GuestRenderState Default { get; } = new(
|
||||
[GuestBlendState.Default],
|
||||
Scissor: null,
|
||||
Viewport: null,
|
||||
GuestRasterState.Default,
|
||||
GuestDepthState.Default);
|
||||
|
||||
public GuestBlendState Blend =>
|
||||
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
||||
}
|
||||
|
||||
/// <summary>Format/NumberType are raw guest render-target register codes.</summary>
|
||||
internal sealed record GuestRenderTarget(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
uint MipLevels = 1);
|
||||
|
||||
/// <summary>Guest DB surface bound alongside a color render target.</summary>
|
||||
internal sealed record GuestDepthTarget(
|
||||
ulong ReadAddress,
|
||||
ulong WriteAddress,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint GuestFormat,
|
||||
uint SwizzleMode,
|
||||
float ClearDepth,
|
||||
bool ReadOnly)
|
||||
{
|
||||
public ulong Address => WriteAddress != 0 ? WriteAddress : ReadAddress;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// A guest shader compiled by a backend, opaque to the export layers: only the backend
|
||||
/// that produced it can submit it. <see cref="Payload"/> is the backend-defined compiled
|
||||
/// bytes (SPIR-V words for Vulkan; MSL/DXIL for future backends), exposed solely for
|
||||
/// diagnostics dumps and size traces — callers must never interpret it.
|
||||
/// </summary>
|
||||
internal interface IGuestCompiledShader
|
||||
{
|
||||
byte[] Payload { get; }
|
||||
|
||||
/// <summary>File extension for diagnostics dumps of <see cref="Payload"/> ("spv",
|
||||
/// "msl", ...), so dumps stay honestly labeled whatever the backend.</summary>
|
||||
string PayloadFileExtension { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user