mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 940557ed4a | |||
| f69fdd4027 | |||
| 9eef39549a | |||
| 90651f26a9 | |||
| b5930465f2 | |||
| 83303e953e | |||
| 92497689ab | |||
| de7973af55 | |||
| 3da7ae7d70 | |||
| 1be009ce40 | |||
| ad92ab30fd | |||
| 5f97d2edc4 | |||
| 5c8ace82c0 | |||
| 341c2a0cb6 | |||
| 320dbcacba | |||
| 30fdd8d6ed | |||
| c69ac6ddab | |||
| fa2616d224 | |||
| 7b86a91dfa | |||
| 72645cb373 | |||
| 2ad9836d13 | |||
| 62e1775c5c | |||
| 6dacd59a08 | |||
| 9d88542efd |
@@ -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.1</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>
|
||||
|
||||
@@ -23,10 +23,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<strong>Join our Discord for development updates, compatibility discussions, support, and community chat.</strong>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [!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.
|
||||
@@ -59,30 +60,49 @@ 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.
|
||||
|
||||
## 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"`
|
||||
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
|
||||
|
||||
Download the release archive for your operating system, extract it, and launch
|
||||
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
|
||||
Tee-Object -FilePath "SharpEmu.log"
|
||||
```
|
||||
|
||||
Linux and macOS:
|
||||
|
||||
```bash
|
||||
chmod +x ./SharpEmu
|
||||
|
||||
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
|
||||
tee SharpEmu.log
|
||||
```
|
||||
|
||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||
release includes the MoltenVK Vulkan implementation.
|
||||
|
||||
## Games Tested
|
||||
|
||||
* **Demon's Souls Remake**
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/par274/sharpemu/issues/2)
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/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.
|
||||

|
||||
|
||||
* **Poppy Playtime Chapter 1**
|
||||
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/par274/sharpemu/issues/3)
|
||||
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3)
|
||||
|
||||
* **SILENT HILL: The Short Message**
|
||||
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/par274/sharpemu/issues/4)
|
||||
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/sharpemu/sharpemu/issues/4)
|
||||
|
||||
* **Dreaming Sarah**
|
||||
* [Dreaming Sarah [PPSA02929]](https://github.com/par274/sharpemu/issues/9)
|
||||
* [Dreaming Sarah [PPSA02929]](https://github.com/sharpemu/sharpemu/issues/9)
|
||||
* Real texture rendering for this game;
|
||||

|
||||
|
||||
@@ -94,8 +114,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 +141,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>
|
||||
|
||||
@@ -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"
|
||||
@@ -75,6 +75,121 @@ internal static partial class Program
|
||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||
}
|
||||
|
||||
if (!CheckHostArchitecture())
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
PreloadMacVulkanLoader();
|
||||
}
|
||||
|
||||
// GLFW requires window creation and event processing on the
|
||||
// process main thread: AppKit demands it on macOS, and X11 has a
|
||||
// single event queue that must be serviced from the main thread
|
||||
// (a window created and polled off it may never map, which showed
|
||||
// as a running game with no visible window on Linux). Emulation
|
||||
// moves to a worker thread and the main thread services the window
|
||||
// work the video presenter posts. Windows keeps a per-thread event
|
||||
// queue, so its window stays on the presenter's own thread.
|
||||
var exitCode = 0;
|
||||
HostMainThread.Enable();
|
||||
var emulation = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
exitCode = RunEmulator(args, isMitigatedChild);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HostMainThread.Shutdown();
|
||||
}
|
||||
}, 32 * 1024 * 1024)
|
||||
{
|
||||
Name = "SharpEmu Emulation",
|
||||
};
|
||||
emulation.Start();
|
||||
HostMainThread.Pump();
|
||||
emulation.Join();
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
return RunEmulator(args, isMitigatedChild);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The supported host execution model, checked before any emulation
|
||||
/// starts: the CPU backend executes guest x86-64 code natively, so the
|
||||
/// host process must be x86-64 — win-x64/linux-x64 on x64 hardware, or
|
||||
/// osx-x64 under Rosetta 2 on Apple Silicon (Rosetta translates the
|
||||
/// whole process, so it still reports as X64 here). An arm64 process
|
||||
/// (e.g. the osx-arm64 build) can browse the GUI but cannot run games;
|
||||
/// failing up front distinguishes that from MoltenVK, signal-handler,
|
||||
/// or guest-memory startup problems.
|
||||
/// </summary>
|
||||
private static bool CheckHostArchitecture()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Unsupported process architecture " +
|
||||
$"{RuntimeInformation.ProcessArchitecture}: guest code executes " +
|
||||
"natively, so SharpEmu must run as an x86-64 process.");
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][ERROR] On Apple Silicon, use the osx-x64 build under " +
|
||||
"Rosetta 2 (install with: softwareupdate --install-rosetta).");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
||||
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
||||
/// x86-64 (Rosetta 2) process, so a universal libMoltenVK.dylib placed
|
||||
/// next to the executable (named libvulkan.1.dylib) is preloaded here;
|
||||
/// dyld then resolves GLFW's bare-name dlopen to the loaded image.
|
||||
/// </summary>
|
||||
private static void PreloadMacVulkanLoader()
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "libvulkan.1.dylib"),
|
||||
Path.Combine(AppContext.BaseDirectory, "libMoltenVK.dylib"),
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".sharpemu", "x64lib", "libvulkan.1.dylib"),
|
||||
};
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out _))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Vulkan loader preloaded: {candidate}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (NativeLibrary.TryLoad("libvulkan.1.dylib", out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] No x86-64 Vulkan loader found; video output will be unavailable. " +
|
||||
"Place a universal libMoltenVK.dylib (from the MoltenVK releases) next to SharpEmu " +
|
||||
"as libvulkan.1.dylib.");
|
||||
}
|
||||
|
||||
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
|
||||
@@ -16,19 +16,30 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||
<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>
|
||||
<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 +74,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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,22 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
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); libc.prx alone reaches beyond -0x1700, so give the
|
||||
// prefix a full 64KB on POSIX. Windows keeps its historical 4KB prefix.
|
||||
private static readonly ulong TlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
||||
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
||||
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
||||
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
||||
private static readonly ulong ReturnToHostStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_C000_0000UL : 0x6FFD_C000_0000UL;
|
||||
private const ulong BootstrapRegionSize = 0x0000_1000UL;
|
||||
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
|
||||
private const ulong BootstrapPayloadResultOffset = 0x28UL;
|
||||
|
||||
@@ -22,6 +22,12 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
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);
|
||||
@@ -207,19 +213,16 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||
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;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -232,8 +235,11 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
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)
|
||||
@@ -256,10 +262,9 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code 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,20 +280,18 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] 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("-", " "));
|
||||
}
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||
}
|
||||
@@ -823,6 +826,61 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadHostQword(ulong address, out ulong value)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// A stray read inside the signal handler would raise a nested
|
||||
// SIGSEGV and kill the process before diagnostics finish, so
|
||||
// probe the region table instead of relying on try/catch.
|
||||
return TryReadStackU64(address, out value);
|
||||
}
|
||||
|
||||
value = 0;
|
||||
try
|
||||
{
|
||||
value = (ulong)Marshal.ReadInt64((nint)address);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (address < 65536)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// See TryReadHostQword: probe every touched page before reading.
|
||||
ulong end = address + (ulong)buffer.Length;
|
||||
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
||||
{
|
||||
if (!_hostMemory.Query(page, out var mbi) ||
|
||||
mbi.State != HostRegionState.Committed ||
|
||||
!IsReadableProtection(mbi.RawProtection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatPointerWithNearestSymbol(ulong value)
|
||||
{
|
||||
string text = $"0x{value:X16}";
|
||||
|
||||
@@ -535,8 +535,7 @@ public sealed partial class DirectExecutionBackend
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockResumeHandler,
|
||||
out var blockWakeHandler,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp) &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
@@ -546,8 +545,7 @@ public sealed partial class DirectExecutionBackend
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockResumeHandler,
|
||||
blockWakeHandler,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
@@ -678,8 +676,7 @@ public sealed partial class DirectExecutionBackend
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockResumeHandler,
|
||||
out var blockWakeHandler,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp) &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
@@ -689,8 +686,7 @@ public sealed partial class DirectExecutionBackend
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockResumeHandler,
|
||||
blockWakeHandler,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
@@ -1970,23 +1966,36 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||
Span<byte> destination = stackalloc byte[1];
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
|
||||
// fallback must probe exactly up to the terminator), but the bytes land in a
|
||||
// stack buffer instead of a List<byte> + ToArray per symbol resolution.
|
||||
const int StackBufferLength = 512;
|
||||
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
|
||||
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||
try
|
||||
{
|
||||
if (!TryReadByteCompat(address + (ulong)i, destination))
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
{
|
||||
return false;
|
||||
if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (buffer[i] == 0)
|
||||
{
|
||||
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented is not null)
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
if (destination[0] == 0)
|
||||
{
|
||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||
return true;
|
||||
}
|
||||
list.Add(destination[0]);
|
||||
}
|
||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -172,8 +173,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,6 +225,11 @@ 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)
|
||||
@@ -258,8 +276,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;
|
||||
@@ -377,8 +421,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 +435,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)
|
||||
{
|
||||
@@ -520,7 +586,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
try
|
||||
{
|
||||
_workAvailable.Set();
|
||||
SignalWorkAvailable();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -555,8 +621,18 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
_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,374 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Native.Windows;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
// POSIX bridge for the Windows vectored-exception-handler logic. A
|
||||
// sigaction(SIGSEGV/SIGBUS/SIGILL) handler rebuilds the EXCEPTION_POINTERS
|
||||
// view the shared handlers expect (Win64 CONTEXT register offsets) from
|
||||
// the signal's mcontext, runs the same recovery chain the VEH path uses
|
||||
// (unresolved-import trap sentinels, demand-paging of lazily-committed
|
||||
// guest pages, fault diagnostics), and writes register changes back into
|
||||
// the mcontext so sigreturn resumes the repaired guest. Unrecovered
|
||||
// faults are forwarded to the previously installed handler so the .NET
|
||||
// runtime keeps turning its own faults into managed exceptions.
|
||||
|
||||
private const int PosixSigIll = 4;
|
||||
private const int PosixSigSegv = 11;
|
||||
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
||||
|
||||
// struct sigaction: the handler pointer leads on both platforms; Darwin
|
||||
// packs { handler(8), mask(4), flags(4) }, Linux glibc/musl packs
|
||||
// { handler(8), mask(128), flags(4), restorer(8) }.
|
||||
private static readonly int PosixSigactionSize = OperatingSystem.IsMacOS() ? 16 : 152;
|
||||
private static readonly int PosixSigactionFlagsOffset = OperatingSystem.IsMacOS() ? 12 : 136;
|
||||
|
||||
private static readonly int PosixSaSigInfo = OperatingSystem.IsMacOS() ? 0x0040 : 0x0004;
|
||||
private static readonly int PosixSaNoDefer = OperatingSystem.IsMacOS() ? 0x0010 : 0x40000000;
|
||||
|
||||
// siginfo_t.si_addr: Darwin { signo, errno, code, pid, uid, status, addr },
|
||||
// Linux { signo, errno, code, pad32, addr }.
|
||||
private static readonly int PosixSigInfoAddressOffset = OperatingSystem.IsMacOS() ? 24 : 16;
|
||||
|
||||
// Darwin ucontext_t stores a pointer to __darwin_mcontext64 at +48; the
|
||||
// general registers live in its __ss thread state after the 16-byte
|
||||
// exception state. Linux glibc embeds mcontext_t inline at +40 with the
|
||||
// registers in gregs[23]. Rosetta 2 delivers the regular x86-64 layout
|
||||
// to translated processes.
|
||||
private const int DarwinUcontextMcontextOffset = 48;
|
||||
private const int DarwinMcontextErrOffset = 4;
|
||||
private const int DarwinMcontextFaultAddressOffset = 8;
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsErrOffset = 19 * 8;
|
||||
|
||||
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||
// against the x86-64 platform headers.
|
||||
private static readonly int[] PosixRegisterOffsets = OperatingSystem.IsMacOS()
|
||||
? new[] { 16, 32, 40, 24, 72, 64, 56, 48, 80, 88, 96, 104, 112, 120, 128, 136, 144 }
|
||||
: new[] { 104, 112, 96, 88, 120, 80, 72, 64, 0, 8, 16, 24, 32, 40, 48, 56, 128 };
|
||||
|
||||
private static DirectExecutionBackend? _posixSignalBackend;
|
||||
private static bool _posixSignalHandlersInstalled;
|
||||
private static bool _posixRawRecoveryEnabled;
|
||||
private static bool _posixSignalWarmup;
|
||||
private static readonly nint[] _posixPreviousActions = new nint[32];
|
||||
private static int _posixSignalTraceCount;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _posixSignalHandlerDepth;
|
||||
|
||||
private void SetupPosixExceptionHandler()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARN] POSIX signal exception bridge disabled by SHARPEMU_DISABLE_POSIX_SIGNALS=1; guest faults will not be recovered.");
|
||||
return;
|
||||
}
|
||||
|
||||
_posixSignalBackend = this;
|
||||
if (_posixSignalHandlersInstalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_posixRawRecoveryEnabled = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal);
|
||||
if (!_posixRawRecoveryEnabled)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Raw sentinel recovery disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
|
||||
}
|
||||
|
||||
WarmUpPosixSignalPath();
|
||||
|
||||
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||
!InstallPosixSignalHandler(PosixSigIll))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
||||
}
|
||||
|
||||
_posixSignalHandlersInstalled = true;
|
||||
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the signal-recovery path once with fabricated inputs before the
|
||||
/// handlers are installed. The first entry into the handler must not
|
||||
/// require JIT compilation (a fault can interrupt arbitrary runtime
|
||||
/// states), and under Rosetta 2 the signal trampoline cannot enter x86
|
||||
/// code that has never been executed (and therefore never translated): a
|
||||
/// cold handler is silently never invoked and the faulting instruction
|
||||
/// retries forever.
|
||||
/// </summary>
|
||||
private void WarmUpPosixSignalPath()
|
||||
{
|
||||
byte* fakeUcontext = stackalloc byte[512];
|
||||
new Span<byte>(fakeUcontext, 512).Clear();
|
||||
byte* fakeMcontext = stackalloc byte[512];
|
||||
new Span<byte>(fakeMcontext, 512).Clear();
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
|
||||
}
|
||||
|
||||
_posixSignalWarmup = true;
|
||||
try
|
||||
{
|
||||
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)fakeUcontext);
|
||||
|
||||
// Warm the branches the fabricated fault above skips without
|
||||
// spamming diagnostics: the benign-exception path through
|
||||
// VectoredHandler, the lazy-commit probe (fault address 0 bails
|
||||
// out immediately), and the chain helper (signal 0 has no saved
|
||||
// action and sigaction(0, ...) fails with EINVAL).
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
||||
EXCEPTION_POINTERS pointers;
|
||||
pointers.ExceptionRecord = &record;
|
||||
pointers.ContextRecord = contextRecord;
|
||||
_ = VectoredHandler(&pointers);
|
||||
|
||||
record.ExceptionCode = 3221225477u;
|
||||
record.NumberParameters = 2;
|
||||
// 0x70000 is never guest-owned, so this walks the vmem region
|
||||
// scan and the PRT range check, then bails out silently.
|
||||
record.ExceptionInformation[1] = 0x70000;
|
||||
_ = TryHandleLazyCommittedPage(&record, 0, 0);
|
||||
ChainPreviousPosixAction(0, 0, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_posixSignalWarmup = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool InstallPosixSignalHandler(int signal)
|
||||
{
|
||||
byte* action = stackalloc byte[PosixSigactionSize];
|
||||
new Span<byte>(action, PosixSigactionSize).Clear();
|
||||
*(nint*)action = (nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal;
|
||||
// No SA_ONSTACK: the runtime's alternate stacks are far too small for
|
||||
// the recovery/diagnostic path (JIT compilation of cold handler code
|
||||
// can run inside the signal frame). Guest faults deliver onto the 2MB
|
||||
// guest stack, host faults onto the regular thread stack — the same
|
||||
// stacks Windows dispatches exceptions on.
|
||||
*(int*)(action + PosixSigactionFlagsOffset) = PosixSaSigInfo | PosixSaNoDefer;
|
||||
|
||||
var previous = (byte*)NativeMemory.AllocZeroed((nuint)PosixSigactionSize);
|
||||
if (sigaction(signal, action, previous) != 0)
|
||||
{
|
||||
NativeMemory.Free(previous);
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] sigaction({signal}) failed: errno={Marshal.GetLastPInvokeError()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_posixPreviousActions[signal] = (nint)previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static void HandlePosixSignal(int signal, nint siginfo, nint ucontext)
|
||||
{
|
||||
if (_posixSignalHandlerDepth > 0)
|
||||
{
|
||||
// A fault inside our own fault handler (diagnostics touched an
|
||||
// unmapped address): restore the default action and return so the
|
||||
// re-executed instruction terminates the process.
|
||||
RestoreDefaultPosixAction(signal);
|
||||
return;
|
||||
}
|
||||
|
||||
_posixSignalHandlerDepth++;
|
||||
try
|
||||
{
|
||||
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A managed exception must never unwind out of a signal frame.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_posixSignalHandlerDepth--;
|
||||
}
|
||||
|
||||
ChainPreviousPosixAction(signal, siginfo, ucontext);
|
||||
}
|
||||
|
||||
private static bool TryHandlePosixFault(int signal, nint siginfo, nint ucontext)
|
||||
{
|
||||
byte* registers = GetPosixRegisterBase(ucontext);
|
||||
if (registers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
||||
int[] offsets = PosixRegisterOffsets;
|
||||
for (int i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||
}
|
||||
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||
if (signal == PosixSigIll)
|
||||
{
|
||||
record.ExceptionCode = 3221225501u;
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||
record.ExceptionCode = 3221225477u;
|
||||
record.NumberParameters = 2;
|
||||
record.ExceptionInformation[0] = GetPosixAccessType(registers, faultAddress, ReadCtxU64(contextRecord, CTX_RIP));
|
||||
record.ExceptionInformation[1] = faultAddress;
|
||||
}
|
||||
|
||||
EXCEPTION_POINTERS pointers;
|
||||
pointers.ExceptionRecord = &record;
|
||||
pointers.ContextRecord = contextRecord;
|
||||
|
||||
int traceIndex = _posixSignalWarmup ? 0 : Interlocked.Increment(ref _posixSignalTraceCount);
|
||||
bool traceSignal = traceIndex > 0 && (traceIndex <= 16 || traceIndex % 1024 == 0 ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_POSIX_SIGNALS"), "1", StringComparison.Ordinal));
|
||||
if (traceSignal)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] posix-signal#{traceIndex}: sig={signal} rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16} " +
|
||||
$"fault=0x{record.ExceptionInformation[1]:X16} access={record.ExceptionInformation[0]} rsp=0x{ReadCtxU64(contextRecord, CTX_RSP):X16}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
// Sentinel recovery runs first: on Windows both vectored handlers see
|
||||
// every fault anyway, and recovering here avoids dumping the full
|
||||
// VectoredHandler diagnostics for each recoverable trap.
|
||||
int disposition = 0;
|
||||
if (_posixRawRecoveryEnabled)
|
||||
{
|
||||
disposition = TryRecoverUnresolvedSentinel(&pointers);
|
||||
}
|
||||
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
|
||||
{
|
||||
disposition = backend.VectoredHandler(&pointers);
|
||||
}
|
||||
if (traceSignal)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] posix-signal#{traceIndex}: recovered={disposition == -1} new_rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
if (disposition != -1 && !_posixSignalWarmup)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte* GetPosixRegisterBase(nint ucontext)
|
||||
{
|
||||
if (ucontext == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return *(byte**)((byte*)ucontext + DarwinUcontextMcontextOffset);
|
||||
}
|
||||
|
||||
return (byte*)ucontext + LinuxUcontextGregsOffset;
|
||||
}
|
||||
|
||||
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
|
||||
{
|
||||
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
|
||||
if (address == 0 && OperatingSystem.IsMacOS())
|
||||
{
|
||||
address = *(ulong*)(registers + DarwinMcontextFaultAddressOffset);
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
private static ulong GetPosixAccessType(byte* registers, ulong faultAddress, ulong rip)
|
||||
{
|
||||
// x86 page-fault error code: bit 1 = write access, bit 4 = instruction
|
||||
// fetch. Fall back to comparing the fault address against RIP when
|
||||
// the error code is not populated (e.g. under Rosetta 2 translation).
|
||||
ulong error = OperatingSystem.IsMacOS()
|
||||
? *(uint*)(registers + DarwinMcontextErrOffset)
|
||||
: *(ulong*)(registers + LinuxGregsErrOffset);
|
||||
if ((error & 0x10) != 0)
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
if ((error & 0x2) != 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return faultAddress != 0 && faultAddress == rip ? 8u : 0u;
|
||||
}
|
||||
|
||||
private static void RestoreDefaultPosixAction(int signal)
|
||||
{
|
||||
byte* action = stackalloc byte[PosixSigactionSize];
|
||||
new Span<byte>(action, PosixSigactionSize).Clear();
|
||||
_ = sigaction(signal, action, null);
|
||||
}
|
||||
|
||||
private static void ChainPreviousPosixAction(int signal, nint siginfo, nint ucontext)
|
||||
{
|
||||
byte* previous = (uint)signal < (uint)_posixPreviousActions.Length
|
||||
? (byte*)_posixPreviousActions[signal]
|
||||
: null;
|
||||
nint handler = previous != null ? *(nint*)previous : 0;
|
||||
if (handler == 0)
|
||||
{
|
||||
// SIG_DFL (or nothing saved): reinstate the default action and
|
||||
// return, so re-executing the faulting instruction terminates the
|
||||
// process with the original fault context intact.
|
||||
RestoreDefaultPosixAction(signal);
|
||||
return;
|
||||
}
|
||||
if (handler == 1)
|
||||
{
|
||||
// SIG_IGN
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = *(int*)(previous + PosixSigactionFlagsOffset);
|
||||
if ((flags & PosixSaSigInfo) != 0)
|
||||
{
|
||||
((delegate* unmanaged<int, nint, nint, void>)handler)(signal, siginfo, ucontext);
|
||||
}
|
||||
else
|
||||
{
|
||||
((delegate* unmanaged<int, void>)handler)(signal);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int sigaction(int signum, void* act, void* oldact);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
@@ -138,15 +139,25 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private const ulong GuestImageScanEnd = 36507222016uL;
|
||||
|
||||
private const ulong GuestThreadStackBaseAddress = 0x7FFF_E000_0000UL;
|
||||
// The 0x7FFx window is Windows-specific; dyld and Rosetta reserve that
|
||||
// range on macOS, so POSIX guest threads use the lower 0x6FFx window.
|
||||
// The POSIX stack base sits a further 1GB down: the import-stub region
|
||||
// descends from 0x7000_0000_0000 on the same 16MB grid and reaches
|
||||
// 0x6FFF_C000_0000 at its 64-module limit, which would otherwise consume
|
||||
// the top stack slots (on Windows the two bands are ~15TB apart).
|
||||
private static readonly ulong GuestThreadStackBaseAddress =
|
||||
OperatingSystem.IsWindows() ? 0x7FFF_E000_0000UL : 0x6FFF_A000_0000UL;
|
||||
|
||||
private const ulong GuestThreadTlsBaseAddress = 0x7FFE_0000_0000UL;
|
||||
private static readonly ulong GuestThreadTlsBaseAddress =
|
||||
OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||
|
||||
private const ulong GuestThreadStackSize = 0x0020_0000UL;
|
||||
|
||||
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
|
||||
|
||||
private const ulong GuestThreadTlsPrefixSize = 0x0000_1000UL;
|
||||
// Matches CpuDispatcher.TlsPrefixSize: static TLS blocks sit below the
|
||||
// TCB, and libc.prx already reaches beyond -0x1700 on POSIX.
|
||||
private static readonly ulong GuestThreadTlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
||||
|
||||
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
||||
|
||||
@@ -434,9 +445,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public string? BlockWakeKey { get; set; }
|
||||
|
||||
public Func<int>? BlockResumeHandler { get; set; }
|
||||
|
||||
public Func<bool>? BlockWakeHandler { get; set; }
|
||||
// Stays set through the wake transition; Resume() consumes it when the thread pumps.
|
||||
public IGuestThreadBlockWaiter? BlockWaiter { get; set; }
|
||||
|
||||
public long BlockDeadlineTimestamp { get; set; }
|
||||
|
||||
@@ -547,6 +557,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private readonly object _guestThreadGate = new object();
|
||||
|
||||
// Diagnostic owner tracking for _guestThreadGate; written only while the
|
||||
// gate is held, read lock-free by the stall watchdog's periodic snapshot.
|
||||
private volatile string? _gateOwnerSite;
|
||||
private int _gateOwnerManagedThreadId;
|
||||
private long _gateAcquireTimestamp;
|
||||
|
||||
private GateHolder LockGate(string site)
|
||||
{
|
||||
Monitor.Enter(_guestThreadGate);
|
||||
_gateOwnerSite = site;
|
||||
Volatile.Write(ref _gateOwnerManagedThreadId, Environment.CurrentManagedThreadId);
|
||||
Volatile.Write(ref _gateAcquireTimestamp, Stopwatch.GetTimestamp());
|
||||
return new GateHolder(this);
|
||||
}
|
||||
|
||||
private readonly struct GateHolder : IDisposable
|
||||
{
|
||||
private readonly DirectExecutionBackend _owner;
|
||||
|
||||
public GateHolder(DirectExecutionBackend owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_owner._gateOwnerSite = null;
|
||||
Volatile.Write(ref _owner._gateOwnerManagedThreadId, 0);
|
||||
Monitor.Exit(_owner._guestThreadGate);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
||||
|
||||
// Once set, guest worker threads are unwound to the host at their next import
|
||||
@@ -611,8 +653,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private static readonly RawExceptionHandlerDelegate RawVectoredHandlerDelegateInstance = RawVectoredHandlerManaged;
|
||||
private static readonly RawExceptionHandlerDelegate RawUnhandledFilterDelegateInstance = RawUnhandledFilterManaged;
|
||||
|
||||
private static readonly nint ImportGatewayPtr =
|
||||
Marshal.GetFunctionPointerForDelegate(ImportGatewayDelegateInstance);
|
||||
private static readonly nint ImportGatewayPtr = ResolveImportGatewayPtr();
|
||||
|
||||
// Emitted import trampolines use the Win64 ABI. Managed callbacks use the
|
||||
// host ABI, so POSIX needs a small Win64-to-SysV register-shuffling thunk.
|
||||
private static nint ResolveImportGatewayPtr()
|
||||
{
|
||||
var managedPtr = Marshal.GetFunctionPointerForDelegate(ImportGatewayDelegateInstance);
|
||||
return OperatingSystem.IsWindows()
|
||||
? managedPtr
|
||||
: PosixHostStubs.CreateWin64ToSysVThunk(managedPtr);
|
||||
}
|
||||
|
||||
private static readonly nint RawVectoredHandlerPtrManaged =
|
||||
Marshal.GetFunctionPointerForDelegate(RawVectoredHandlerDelegateInstance);
|
||||
@@ -866,7 +917,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_hostThreading = _hostPlatform.Threading;
|
||||
_hostSymbols = _hostPlatform.Symbols;
|
||||
_hostMemory = _hostPlatform.Memory;
|
||||
_faultHandling = faultHandling ?? new WindowsFaultHandling(_hostMemory);
|
||||
_faultHandling = faultHandling ?? (OperatingSystem.IsWindows()
|
||||
? new WindowsFaultHandling(_hostMemory)
|
||||
: NullHostFaultHandling.Instance);
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
||||
_guestTlsBaseTlsIndex = _hostThreading.AllocateTlsSlot();
|
||||
@@ -1977,26 +2030,54 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate TLS handler");
|
||||
}
|
||||
// The handler runs in place of a patched guest `mov reg, fs:[0]`,
|
||||
// which preserves every register and the flags. TlsGetValue (and the
|
||||
// Win64 ABI in general) clobbers rcx/rdx/r8-r11 and the arithmetic
|
||||
// flags, so save them all: guest code legitimately keeps live values
|
||||
// and comparison results across TLS reads, and losing them corrupted
|
||||
// deterministic computations (e.g. procedural texture generation).
|
||||
byte* tlsHandlerAddress = (byte*)_tlsHandlerAddress;
|
||||
int num = 0;
|
||||
tlsHandlerAddress[num++] = 72;
|
||||
tlsHandlerAddress[num++] = 131;
|
||||
tlsHandlerAddress[num++] = 236;
|
||||
tlsHandlerAddress[num++] = 40;
|
||||
tlsHandlerAddress[num++] = 185;
|
||||
tlsHandlerAddress[num++] = 0x9C; // pushfq
|
||||
tlsHandlerAddress[num++] = 0x51; // push rcx
|
||||
tlsHandlerAddress[num++] = 0x52; // push rdx
|
||||
tlsHandlerAddress[num++] = 0x41; // push r8
|
||||
tlsHandlerAddress[num++] = 0x50;
|
||||
tlsHandlerAddress[num++] = 0x41; // push r9
|
||||
tlsHandlerAddress[num++] = 0x51;
|
||||
tlsHandlerAddress[num++] = 0x41; // push r10
|
||||
tlsHandlerAddress[num++] = 0x52;
|
||||
tlsHandlerAddress[num++] = 0x41; // push r11
|
||||
tlsHandlerAddress[num++] = 0x53;
|
||||
tlsHandlerAddress[num++] = 0x48; // sub rsp, 0x20
|
||||
tlsHandlerAddress[num++] = 0x83;
|
||||
tlsHandlerAddress[num++] = 0xEC;
|
||||
tlsHandlerAddress[num++] = 0x20;
|
||||
tlsHandlerAddress[num++] = 0xB9; // mov ecx, index
|
||||
*(uint*)(tlsHandlerAddress + num) = _guestTlsBaseTlsIndex;
|
||||
num += 4;
|
||||
tlsHandlerAddress[num++] = 72;
|
||||
tlsHandlerAddress[num++] = 184;
|
||||
tlsHandlerAddress[num++] = 0x48; // mov rax, TlsGetValue
|
||||
tlsHandlerAddress[num++] = 0xB8;
|
||||
*(long*)(tlsHandlerAddress + num) = _tlsGetValueAddress;
|
||||
num += 8;
|
||||
tlsHandlerAddress[num++] = byte.MaxValue;
|
||||
tlsHandlerAddress[num++] = 208;
|
||||
tlsHandlerAddress[num++] = 72;
|
||||
tlsHandlerAddress[num++] = 131;
|
||||
tlsHandlerAddress[num++] = 196;
|
||||
tlsHandlerAddress[num++] = 40;
|
||||
tlsHandlerAddress[num++] = 195;
|
||||
tlsHandlerAddress[num++] = 0xFF; // call rax
|
||||
tlsHandlerAddress[num++] = 0xD0;
|
||||
tlsHandlerAddress[num++] = 0x48; // add rsp, 0x20
|
||||
tlsHandlerAddress[num++] = 0x83;
|
||||
tlsHandlerAddress[num++] = 0xC4;
|
||||
tlsHandlerAddress[num++] = 0x20;
|
||||
tlsHandlerAddress[num++] = 0x41; // pop r11
|
||||
tlsHandlerAddress[num++] = 0x5B;
|
||||
tlsHandlerAddress[num++] = 0x41; // pop r10
|
||||
tlsHandlerAddress[num++] = 0x5A;
|
||||
tlsHandlerAddress[num++] = 0x41; // pop r9
|
||||
tlsHandlerAddress[num++] = 0x59;
|
||||
tlsHandlerAddress[num++] = 0x41; // pop r8
|
||||
tlsHandlerAddress[num++] = 0x58;
|
||||
tlsHandlerAddress[num++] = 0x5A; // pop rdx
|
||||
tlsHandlerAddress[num++] = 0x59; // pop rcx
|
||||
tlsHandlerAddress[num++] = 0x9D; // popfq
|
||||
tlsHandlerAddress[num++] = 0xC3; // ret
|
||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||
uint num2 = default(uint);
|
||||
if (!_hostMemory.Protect((ulong)(void*)_tlsHandlerAddress, TlsHandlerRegionSize, HostPageProtection.ReadExecute, out num2))
|
||||
@@ -2614,7 +2695,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("TryStartThread"))
|
||||
{
|
||||
_guestThreads[request.ThreadHandle] = thread;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
@@ -2650,10 +2731,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return false;
|
||||
}
|
||||
|
||||
// Joins regularly park here for minutes (a game main thread joining a
|
||||
// streamer); polling at a fixed 1ms burns half a host core for the
|
||||
// whole wait, so back off toward a 10ms cadence once the join is
|
||||
// clearly long-lived.
|
||||
var joinPollMilliseconds = 1;
|
||||
while (!ActiveForcedGuestExit)
|
||||
{
|
||||
Thread? hostThread;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("TryJoinThread"))
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
|
||||
{
|
||||
@@ -2690,16 +2776,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
try
|
||||
{
|
||||
hostThread.Join(1);
|
||||
hostThread.Join(joinPollMilliseconds);
|
||||
}
|
||||
catch (ThreadStateException)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
Thread.Sleep(joinPollMilliseconds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
Thread.Sleep(joinPollMilliseconds);
|
||||
}
|
||||
|
||||
if (joinPollMilliseconds < 10)
|
||||
{
|
||||
joinPollMilliseconds++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2729,7 +2820,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
GuestThreadState? thread = null;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("Pump.dequeue"))
|
||||
{
|
||||
while (_readyGuestThreads.Count > 0)
|
||||
{
|
||||
@@ -2760,7 +2851,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
Name = $"SharpEmu-{thread.Name}",
|
||||
Priority = MapGuestThreadPriority(thread.Priority),
|
||||
};
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("Pump.bind_host"))
|
||||
{
|
||||
thread.HostThread = hostThread;
|
||||
}
|
||||
@@ -2781,7 +2872,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
|
||||
var wakeCount = 0;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("WakeBlockedThreads"))
|
||||
{
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
@@ -2797,14 +2888,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
continue;
|
||||
}
|
||||
|
||||
if (thread.BlockWakeHandler is not null && !thread.BlockWakeHandler())
|
||||
if (thread.BlockWaiter is not null && !thread.BlockWaiter.TryWake())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
@@ -2831,7 +2921,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads()
|
||||
{
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("SnapshotThreads"))
|
||||
{
|
||||
var snapshots = new GuestThreadSnapshot[_guestThreads.Count];
|
||||
var index = 0;
|
||||
@@ -2855,8 +2945,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ulong guestThreadHandle,
|
||||
GuestCpuContinuation continuation,
|
||||
string wakeKey,
|
||||
Func<int>? resumeHandler,
|
||||
Func<bool>? wakeHandler,
|
||||
IGuestThreadBlockWaiter? waiter,
|
||||
long blockDeadlineTimestamp)
|
||||
{
|
||||
if (guestThreadHandle == 0 || continuation.Rip < 65536 || continuation.Rsp == 0)
|
||||
@@ -2864,7 +2953,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("RegisterBlockedContinuation"))
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(guestThreadHandle, out var thread))
|
||||
{
|
||||
@@ -2874,8 +2963,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.BlockedContinuation = continuation;
|
||||
thread.HasBlockedContinuation = true;
|
||||
thread.BlockWakeKey = wakeKey;
|
||||
thread.BlockResumeHandler = resumeHandler;
|
||||
thread.BlockWakeHandler = wakeHandler;
|
||||
thread.BlockWaiter = waiter;
|
||||
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
}
|
||||
}
|
||||
@@ -2884,7 +2972,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var wakeCount = 0;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("WakeExpiredBlockedGuestThreads"))
|
||||
{
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
@@ -2898,7 +2986,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
@@ -2976,7 +3063,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private GuestThreadState[] SnapshotGuestThreads()
|
||||
{
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("SnapshotGuestThreads"))
|
||||
{
|
||||
return _guestThreads.Values.ToArray();
|
||||
}
|
||||
@@ -3166,7 +3253,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
if (currentGuestThreadHandle != 0)
|
||||
{
|
||||
GuestContinuationRunner? runner;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("TryCallGuestContinuation"))
|
||||
{
|
||||
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
|
||||
{
|
||||
@@ -3258,7 +3345,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
_guestTeardownRequested = true;
|
||||
Thread[] hostThreads;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("RequestGuestThreadTeardown"))
|
||||
{
|
||||
_readyGuestThreads.Clear();
|
||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||
@@ -3288,7 +3375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private void ClearGuestThreads()
|
||||
{
|
||||
GuestContinuationRunner[] runners;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("ClearGuestThreads"))
|
||||
{
|
||||
runners = _guestThreads.Values
|
||||
.Select(static thread => thread.ContinuationRunner)
|
||||
@@ -3575,9 +3662,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
LastError = null;
|
||||
GuestCpuContinuation continuation = default;
|
||||
Func<int>? resumeHandler = null;
|
||||
IGuestThreadBlockWaiter? blockWaiter = null;
|
||||
var resumeContinuation = false;
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("RunGuestThread.block"))
|
||||
{
|
||||
if (thread.HasBlockedContinuation)
|
||||
{
|
||||
@@ -3585,17 +3672,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.BlockedContinuation = default;
|
||||
thread.HasBlockedContinuation = false;
|
||||
thread.BlockWakeKey = null;
|
||||
resumeHandler = thread.BlockResumeHandler;
|
||||
thread.BlockResumeHandler = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
blockWaiter = thread.BlockWaiter;
|
||||
thread.BlockWaiter = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
resumeContinuation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeHandler is not null)
|
||||
if (blockWaiter is not null)
|
||||
{
|
||||
continuation = continuation with { Rax = unchecked((ulong)(long)resumeHandler()) };
|
||||
continuation = continuation with { Rax = unchecked((ulong)(long)blockWaiter.Resume()) };
|
||||
}
|
||||
|
||||
if (_logGuestThreads)
|
||||
@@ -3608,7 +3694,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var exitReason = resumeContinuation
|
||||
? ExecuteBlockedGuestThreadContinuation(thread.Context, continuation, thread.Name, out var blockReason)
|
||||
: ExecuteGuestThreadEntry(thread.Context, thread.EntryPoint, thread.Name, out blockReason);
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("RunGuestThread.exit"))
|
||||
{
|
||||
switch (exitReason)
|
||||
{
|
||||
@@ -3620,12 +3706,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.State = GuestThreadRunState.Blocked;
|
||||
thread.BlockReason = blockReason;
|
||||
if (thread.HasBlockedContinuation &&
|
||||
thread.BlockWakeHandler is not null &&
|
||||
thread.BlockWakeHandler())
|
||||
thread.BlockWaiter is not null &&
|
||||
thread.BlockWaiter.TryWake())
|
||||
{
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
@@ -4362,6 +4447,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return 20;
|
||||
}
|
||||
|
||||
// SHARPEMU_PERIODIC_SNAPSHOT_SECONDS=N: dump the stall snapshot every N
|
||||
// seconds regardless of progress, for diagnosing soft stalls where imports
|
||||
// keep flowing but the game stops advancing.
|
||||
private static int GetPeriodicSnapshotSeconds()
|
||||
{
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_PERIODIC_SNAPSHOT_SECONDS"), out var result))
|
||||
{
|
||||
return Math.Max(0, result);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void StartStallWatchdog()
|
||||
{
|
||||
int stallWatchdogSeconds = GetStallWatchdogSeconds();
|
||||
@@ -4391,6 +4488,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
dispatcherThread.Start();
|
||||
|
||||
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
||||
var periodicSnapshotTicks = (long)((double)GetPeriodicSnapshotSeconds() * Stopwatch.Frequency);
|
||||
var lastPeriodicSnapshot = Stopwatch.GetTimestamp();
|
||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||
{
|
||||
while (!_stallWatchdogStop)
|
||||
@@ -4400,6 +4499,58 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (periodicSnapshotTicks > 0 &&
|
||||
Stopwatch.GetTimestamp() - lastPeriodicSnapshot >= periodicSnapshotTicks)
|
||||
{
|
||||
lastPeriodicSnapshot = Stopwatch.GetTimestamp();
|
||||
var gateOwnerSite = _gateOwnerSite;
|
||||
var gateOwnerTid = Volatile.Read(ref _gateOwnerManagedThreadId);
|
||||
var gateHeldMs = gateOwnerSite is null
|
||||
? 0.0
|
||||
: Stopwatch.GetElapsedTime(Volatile.Read(ref _gateAcquireTimestamp)).TotalMilliseconds;
|
||||
var snapshotText = new System.Text.StringBuilder();
|
||||
snapshotText.AppendLine(
|
||||
$"[LOADER][DIAG] Periodic snapshot: gate_owner={gateOwnerSite ?? "none"} " +
|
||||
$"gate_tid={gateOwnerTid} gate_held_ms={gateHeldMs:0}");
|
||||
// Never touch the gate here: the periodic snapshot must keep
|
||||
// reporting even (especially) when the gate is wedged.
|
||||
// Dump guest threads without the lock; tolerate torn reads.
|
||||
try
|
||||
{
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
snapshotText.AppendLine(
|
||||
$"[LOADER][DIAG] gateless guest-thread: handle=0x{thread.ThreadHandle:X16} name='{thread.Name}' " +
|
||||
$"state={thread.State} imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||
$"nid={Volatile.Read(ref thread.LastImportNid) ?? "none"} ret=0x{Volatile.Read(ref thread.LastReturnRip):X16} " +
|
||||
$"block={thread.BlockReason ?? "none"} wake={thread.BlockWakeKey ?? "none"}");
|
||||
}
|
||||
}
|
||||
catch (Exception snapshotError)
|
||||
{
|
||||
snapshotText.AppendLine($"[LOADER][DIAG] gateless snapshot failed: {snapshotError.Message}");
|
||||
}
|
||||
|
||||
// Console can be wedged by whatever is being diagnosed, so
|
||||
// write to a side file when one is configured and fall back
|
||||
// to stderr otherwise.
|
||||
var snapshotPath = Environment.GetEnvironmentVariable("SHARPEMU_PERIODIC_SNAPSHOT_FILE");
|
||||
if (!string.IsNullOrWhiteSpace(snapshotPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.AppendAllText(snapshotPath, snapshotText.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.Write(snapshotText.ToString());
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
long num2 = Stopwatch.GetTimestamp() - Volatile.Read(ref _lastProgressTimestamp);
|
||||
if (num2 < num)
|
||||
{
|
||||
@@ -4448,7 +4599,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private bool HasReadyGuestThread()
|
||||
{
|
||||
WakeExpiredBlockedGuestThreads();
|
||||
lock (_guestThreadGate)
|
||||
using (LockGate("HasReadyGuestThread"))
|
||||
{
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
@@ -4637,6 +4788,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
// Native guest workers park idle once every guest thread has unwound; stop
|
||||
// them before any executable stub or TLS index they reference is freed.
|
||||
DisposeNativeGuestExecutors();
|
||||
|
||||
if (ReferenceEquals(_posixSignalBackend, this))
|
||||
{
|
||||
// The signal handlers stay installed (they chain to the previous
|
||||
// action when no backend is active), but must stop dispatching
|
||||
// into a disposed backend.
|
||||
_posixSignalBackend = null;
|
||||
}
|
||||
ClearImportHandlerTrampolines();
|
||||
_importEntries = Array.Empty<ImportStubEntry>();
|
||||
_runtimeSymbolsByName.Clear();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for hosts whose fault bridge is installed directly by the
|
||||
/// execution backend. POSIX uses its sigaction bridge and never calls these
|
||||
/// Windows-shaped registration methods.
|
||||
/// </summary>
|
||||
internal sealed class NullHostFaultHandling : IHostFaultHandling
|
||||
{
|
||||
public static NullHostFaultHandling Instance { get; } = new();
|
||||
|
||||
private NullHostFaultHandling()
|
||||
{
|
||||
}
|
||||
|
||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||
{
|
||||
_ = managedCallback;
|
||||
_ = hostRspSwitchTlsSlot;
|
||||
_ = tlsGetValueAddress;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void FreeThunk(nint thunk)
|
||||
{
|
||||
_ = thunk;
|
||||
}
|
||||
|
||||
public nint AddFirstChanceHandler(nint thunk)
|
||||
{
|
||||
_ = thunk;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void RemoveHandler(nint handle)
|
||||
{
|
||||
_ = handle;
|
||||
}
|
||||
|
||||
public void SetUnhandledFilter(nint thunk)
|
||||
{
|
||||
_ = thunk;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace SharpEmu.Core.Cpu.Native.Windows;
|
||||
/// </summary>
|
||||
internal static class Win64ContextOffsets
|
||||
{
|
||||
public const int Size = 0x4D0;
|
||||
public const int Mxcsr = 52;
|
||||
public const int Rax = 120;
|
||||
public const int Rcx = 128;
|
||||
|
||||
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
||||
address = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryFreeGuestMemory(ulong address)
|
||||
{
|
||||
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private readonly IHostMemory _hostMemory;
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||
@@ -248,6 +249,71 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
// Under Rosetta 2 the kernel can ignore placement hints for whole
|
||||
// windows, so page-stepped exact probes are pathological on macOS.
|
||||
// Linux must keep using the exact-address search below: PS5 resource
|
||||
// descriptors cannot represent ordinary 0x7F... host mappings. Linux
|
||||
// HostMemory uses MAP_FIXED_NOREPLACE, making those low-address probes
|
||||
// safe without clobbering existing host mappings.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Prefer the requested low address. Besides matching the guest
|
||||
// address model, this keeps the allocation representable by every
|
||||
// PS5 GPU descriptor (the strictest ones carry 40 address bits).
|
||||
try
|
||||
{
|
||||
var exactAddress = AllocateAt(
|
||||
cursor,
|
||||
alignedSize,
|
||||
executable,
|
||||
allowAlternative: false);
|
||||
if (exactAddress == cursor)
|
||||
{
|
||||
actualAddress = exactAddress;
|
||||
UpdateAllocationSearchCursor(
|
||||
desiredAddress,
|
||||
effectiveAlignment,
|
||||
executable,
|
||||
exactAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Over-allocate by the alignment so a kernel-chosen placement
|
||||
// always contains an aligned start; the unused head/tail stays
|
||||
// part of the tracked region and is simply never handed out.
|
||||
var reserveSize = effectiveAlignment > PageSize
|
||||
? alignedSize + effectiveAlignment
|
||||
: alignedSize;
|
||||
try
|
||||
{
|
||||
var posixAddress = AllocateAt(cursor, reserveSize, executable, allowAlternative: true);
|
||||
if (posixAddress != 0)
|
||||
{
|
||||
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
||||
const ulong gpuAddressLimit = 1UL << 40;
|
||||
if (alignedBase < gpuAddressLimit &&
|
||||
alignedSize <= gpuAddressLimit - alignedBase &&
|
||||
alignedBase + alignedSize <= posixAddress + reserveSize)
|
||||
{
|
||||
actualAddress = alignedBase;
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
ReleaseUntrackedAllocation(posixAddress);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||
{
|
||||
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
|
||||
@@ -282,6 +348,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;
|
||||
@@ -301,7 +389,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
GuestAllocationArenaSize,
|
||||
executable: false,
|
||||
allowAlternative: true);
|
||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
||||
_guestAllocationFreeRanges.Add(
|
||||
GuestAllocationArenaStartOffset,
|
||||
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -309,14 +399,89 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
||||
ulong rangeOffset = 0;
|
||||
ulong rangeSize = 0;
|
||||
ulong alignedOffset = 0;
|
||||
var found = false;
|
||||
foreach (var range in _guestAllocationFreeRanges)
|
||||
{
|
||||
alignedOffset = AlignUp(range.Key, alignment);
|
||||
if (alignedOffset >= range.Key &&
|
||||
alignedOffset - range.Key <= range.Value &&
|
||||
size <= range.Value - (alignedOffset - range.Key))
|
||||
{
|
||||
rangeOffset = range.Key;
|
||||
rangeSize = range.Value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_guestAllocationFreeRanges.Remove(rangeOffset);
|
||||
if (alignedOffset > rangeOffset)
|
||||
{
|
||||
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
|
||||
}
|
||||
|
||||
var allocationEnd = alignedOffset + size;
|
||||
var rangeEnd = rangeOffset + rangeSize;
|
||||
if (allocationEnd < rangeEnd)
|
||||
{
|
||||
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
|
||||
}
|
||||
|
||||
address = _guestAllocationArenaBase + alignedOffset;
|
||||
_guestAllocationOffset = alignedOffset + size;
|
||||
_guestAllocations.Add(address, (alignedOffset, size));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryFreeGuestMemory(ulong address)
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
{
|
||||
if (!_guestAllocations.Remove(address, out var allocation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var freeOffset = allocation.Offset;
|
||||
var freeSize = allocation.Size;
|
||||
ulong? previousOffset = null;
|
||||
ulong? nextOffset = null;
|
||||
|
||||
foreach (var range in _guestAllocationFreeRanges)
|
||||
{
|
||||
if (range.Key < freeOffset)
|
||||
{
|
||||
previousOffset = range.Key;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextOffset = range.Key;
|
||||
break;
|
||||
}
|
||||
|
||||
if (previousOffset is { } previous &&
|
||||
previous + _guestAllocationFreeRanges[previous] == freeOffset)
|
||||
{
|
||||
freeOffset = previous;
|
||||
freeSize += _guestAllocationFreeRanges[previous];
|
||||
_guestAllocationFreeRanges.Remove(previous);
|
||||
}
|
||||
|
||||
if (nextOffset is { } next && freeOffset + freeSize == next)
|
||||
{
|
||||
freeSize += _guestAllocationFreeRanges[next];
|
||||
_guestAllocationFreeRanges.Remove(next);
|
||||
}
|
||||
|
||||
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -380,7 +545,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
_guestAllocationOffset = 0;
|
||||
_guestAllocationFreeRanges.Clear();
|
||||
_guestAllocations.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,12 +605,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||
{
|
||||
var runStart = mapStart;
|
||||
var runFlags = ProgramHeaderFlags.None;
|
||||
var hasRun = false;
|
||||
|
||||
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||
{
|
||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||
var mergedFlags = existingFlags | flags;
|
||||
_pageProtections[pageAddress] = mergedFlags;
|
||||
SetProtection(pageAddress, PageSize, mergedFlags);
|
||||
|
||||
if (!hasRun)
|
||||
{
|
||||
runStart = pageAddress;
|
||||
runFlags = mergedFlags;
|
||||
hasRun = true;
|
||||
}
|
||||
else if (mergedFlags != runFlags)
|
||||
{
|
||||
SetProtection(runStart, pageAddress - runStart, runFlags);
|
||||
runStart = pageAddress;
|
||||
runFlags = mergedFlags;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRun)
|
||||
{
|
||||
SetProtection(runStart, mapEnd - runStart, runFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,9 +976,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
var region = FindRegion(virtualAddress, 1);
|
||||
if (region is null ||
|
||||
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var existing in _regions)
|
||||
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
|
||||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
|
||||
{
|
||||
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
||||
{
|
||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||
}
|
||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||
}
|
||||
|
||||
_regions.Add(new MappedRegion(
|
||||
_regions.Insert(insertionIndex, new MappedRegion(
|
||||
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||
endAddress,
|
||||
backingMemory));
|
||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
||||
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
||||
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
||||
CopyToRegions(virtualAddress, source, regionIndex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
||||
private bool TryValidateRange(
|
||||
ulong virtualAddress,
|
||||
int length,
|
||||
ProgramHeaderFlags requiredProtection,
|
||||
out int regionIndex)
|
||||
{
|
||||
foreach (var candidate in _regions)
|
||||
regionIndex = FindContainingRegionIndex(virtualAddress);
|
||||
if (regionIndex < 0)
|
||||
{
|
||||
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
region = candidate;
|
||||
offset = candidateOffset;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
region = default;
|
||||
offset = 0;
|
||||
return false;
|
||||
var currentAddress = virtualAddress;
|
||||
var remaining = length;
|
||||
var currentIndex = regionIndex;
|
||||
while (true)
|
||||
{
|
||||
if (currentIndex >= _regions.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var region = _regions[currentIndex];
|
||||
if (currentAddress < region.Region.VirtualAddress ||
|
||||
currentAddress >= region.EndAddress ||
|
||||
(region.Region.Protection & requiredProtection) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remaining == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var available = region.EndAddress - currentAddress;
|
||||
var chunkLength = (int)Math.Min((ulong)remaining, available);
|
||||
remaining -= chunkLength;
|
||||
if (remaining == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
currentAddress += (ulong)chunkLength;
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private int FindContainingRegionIndex(ulong virtualAddress)
|
||||
{
|
||||
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||
if (insertionIndex < _regions.Count &&
|
||||
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
|
||||
{
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
var candidateIndex = insertionIndex - 1;
|
||||
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
|
||||
? candidateIndex
|
||||
: -1;
|
||||
}
|
||||
|
||||
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
|
||||
{
|
||||
var copied = 0;
|
||||
var currentAddress = virtualAddress;
|
||||
while (copied < destination.Length)
|
||||
{
|
||||
var region = _regions[regionIndex++];
|
||||
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
|
||||
copied += chunkLength;
|
||||
currentAddress += (ulong)chunkLength;
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
|
||||
{
|
||||
var copied = 0;
|
||||
var currentAddress = virtualAddress;
|
||||
while (copied < source.Length)
|
||||
{
|
||||
var region = _regions[regionIndex++];
|
||||
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
|
||||
copied += chunkLength;
|
||||
currentAddress += (ulong)chunkLength;
|
||||
}
|
||||
}
|
||||
|
||||
private int FindInsertionIndex(ulong virtualAddress)
|
||||
{
|
||||
var lower = 0;
|
||||
var upper = _regions.Count;
|
||||
while (lower < upper)
|
||||
{
|
||||
var middle = lower + ((upper - lower) / 2);
|
||||
if (_regions[middle].Region.VirtualAddress < virtualAddress)
|
||||
{
|
||||
lower = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
|
||||
return lower;
|
||||
}
|
||||
|
||||
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||
|
||||
@@ -84,7 +84,9 @@ 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,15 @@
|
||||
"Library.Loading": "Carregando biblioteca…",
|
||||
|
||||
"Options.General": "Opções Gerais",
|
||||
"Options.Env.Tab": "Ambiente",
|
||||
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||
"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).",
|
||||
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "INICIALIZADOR",
|
||||
@@ -74,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",
|
||||
@@ -125,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!"
|
||||
}
|
||||
|
||||
@@ -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 !"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"_languageName": "Hungarian",
|
||||
|
||||
"Page.Library": "Könyvtár",
|
||||
"Page.Options": "Beállítások",
|
||||
"Page.GameCount.One": "1 játék",
|
||||
"Page.GameCount.Other": "{0} játékok",
|
||||
|
||||
"Library.SearchWatermark": "Keresés a könyvtárban",
|
||||
"Library.AddFolder": "+ Mappa hozzáadása",
|
||||
"Library.Rescan": "⟳ Újrakeresés",
|
||||
"Library.OpenFile": "Fájl megnyitása…",
|
||||
|
||||
"Library.Context.Launch": "Inditás",
|
||||
"Library.Context.OpenFolder": "Játékmappa megnyitása",
|
||||
"Library.Context.CopyPath": "Elérési út másolása",
|
||||
"Library.Context.CopyTitleId": "Cím ID másolása",
|
||||
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
|
||||
|
||||
"Library.Empty.Title": "A könyvtárad üres",
|
||||
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
|
||||
"Library.Empty.SearchTitle": "Nincs találat a elemre",
|
||||
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
|
||||
"Library.Empty.AddFolder": "+ Játékmappa hozzáadása",
|
||||
|
||||
"Library.Loading": "Könyvtár betöltése",
|
||||
|
||||
"Options.General": "Általános",
|
||||
"Options.Env.Tab": "Környezet",
|
||||
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
|
||||
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
|
||||
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
|
||||
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
|
||||
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
|
||||
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
|
||||
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
|
||||
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
|
||||
"Options.Section.Emulation": "EMULÁCIÓ",
|
||||
"Options.Section.Logging": "LOGOLÁS",
|
||||
"Options.Section.Launcher": "INDITÓ",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU motor",
|
||||
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
|
||||
"Options.CpuEngine.Native": "Natív",
|
||||
|
||||
"Options.Strict.Label": "Szigorú dynlib felbontás",
|
||||
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
|
||||
|
||||
"Options.LogLevel.Label": "Naplózási szint",
|
||||
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Információ",
|
||||
"Options.LogLevel.Warning": "Figyelmeztetés",
|
||||
"Options.LogLevel.Error": "Hiba",
|
||||
"Options.LogLevel.Critical": "Kritikus",
|
||||
|
||||
"Options.TraceImports.Label": "Import trace limit",
|
||||
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
|
||||
|
||||
"Options.LogToFile.Label": "Naplozás fájlba",
|
||||
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
|
||||
|
||||
"Options.LogFilePath.Label": "Naplófájl elérési útja",
|
||||
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
|
||||
"Options.LogFilePath.Select": "Kiválasztás…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
|
||||
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
|
||||
|
||||
"Options.TitleMusic.Label": "Címzene",
|
||||
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
|
||||
|
||||
"Options.Discord.Label": "Discord jelenlét",
|
||||
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
|
||||
|
||||
"Options.Language.Label": "Emulátor nyelve",
|
||||
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
|
||||
|
||||
"Common.On": "Be",
|
||||
"Common.Off": "Ki",
|
||||
|
||||
"Console.Title": "KONZOL",
|
||||
"Console.SearchWatermark": "Keresés...",
|
||||
"Console.AutoScroll": "Automatikus görgetés",
|
||||
"Console.Split": "Felosztás",
|
||||
"Console.Copy": "Másolás",
|
||||
"Console.Clear": "Törlés",
|
||||
"Console.WindowTitle": "SharpEmu Konzol",
|
||||
|
||||
"Launch.NoGameSelected": "Nincs játék kiválasztva",
|
||||
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
|
||||
"Launch.Idle": "Tétlen",
|
||||
"Launch.Console": "≡ Konzol",
|
||||
"Launch.Launch": "▶ Inditás",
|
||||
"Launch.Stop": "■ Leállítás",
|
||||
"Launch.Running": "Fut — {0}",
|
||||
"Launch.Stopping": "Leállítás…",
|
||||
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
|
||||
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
|
||||
"Launch.LogFile": "Naplófájl: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
|
||||
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "nemérvényes argumentumok",
|
||||
"Exit.EbootNotFound": "eboot nem található",
|
||||
"Exit.RuntimeException": "runtime exception",
|
||||
"Exit.EmulationError": "emulációs hiba",
|
||||
"Exit.Unknown": "ismeretlen",
|
||||
|
||||
"Status.EmulatorLocating": "Emulátor: keresés…",
|
||||
"Status.EmulatorPath": "Emulátor: {0}",
|
||||
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
|
||||
"Status.ScanningLibrary": "Könyvtár beolvasása…",
|
||||
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
|
||||
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
|
||||
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
|
||||
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
|
||||
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
|
||||
"Status.Running": "Fut {0}",
|
||||
"Status.Stopping": "Leállítás…",
|
||||
"Status.Idle": "Nyugodt",
|
||||
|
||||
"Clipboard.Path": "Út",
|
||||
"Clipboard.TitleId": "Cím ID",
|
||||
|
||||
"Discord.Playing": "Játékban {0}",
|
||||
"Discord.Browsing": "Böngéssz a könyvtárban",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
|
||||
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
|
||||
"Dialog.PsExecutables": "PS futtatható fájlok",
|
||||
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
|
||||
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
|
||||
"Dialog.LogFiles": "Naplózási fájlok",
|
||||
|
||||
"Options.About" : "Erről",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
|
||||
"About.GithubButton": "Járulj hozzá GitHubon!",
|
||||
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"_languageName": "Português (Portugal)",
|
||||
|
||||
"Page.Library": "Biblioteca",
|
||||
"Page.Options": "Opções",
|
||||
"Page.GameCount.One": "1 jogo",
|
||||
"Page.GameCount.Other": "{0} jogos",
|
||||
|
||||
"Library.SearchWatermark": "Pesquisar biblioteca…",
|
||||
"Library.AddFolder": "+ Adicionar pasta",
|
||||
"Library.Rescan": "⟳ Reanalisar",
|
||||
"Library.OpenFile": "Abrir ficheiro…",
|
||||
|
||||
"Library.Context.Launch": "Iniciar",
|
||||
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||
"Library.Context.CopyPath": "Copiar caminho",
|
||||
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||
"Library.Context.Remove": "Remover da biblioteca",
|
||||
|
||||
"Library.Empty.Title": "A sua biblioteca está vazia",
|
||||
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
|
||||
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
|
||||
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Adicionar pasta de jogos",
|
||||
|
||||
"Library.Loading": "A carregar biblioteca…",
|
||||
|
||||
"Options.General": "Geral",
|
||||
"Options.Env.Tab": "Ambiente",
|
||||
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
|
||||
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
|
||||
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
|
||||
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
||||
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "REGISTOS",
|
||||
"Options.Section.Launcher": "LANÇADOR",
|
||||
|
||||
"Options.CpuEngine.Label": "Motor de CPU",
|
||||
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Resolução estrita de dynlib",
|
||||
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
|
||||
|
||||
"Options.LogLevel.Label": "Nível de registo",
|
||||
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
|
||||
"Options.LogLevel.Trace": "Rastreio",
|
||||
"Options.LogLevel.Debug": "Depuração",
|
||||
"Options.LogLevel.Info": "Informação",
|
||||
"Options.LogLevel.Warning": "Aviso",
|
||||
"Options.LogLevel.Error": "Erro",
|
||||
"Options.LogLevel.Critical": "Crítico",
|
||||
|
||||
"Options.TraceImports.Label": "Limite de rastreio de importações",
|
||||
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
|
||||
|
||||
"Options.LogToFile.Label": "Registar para ficheiro",
|
||||
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
|
||||
|
||||
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
|
||||
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
|
||||
"Options.LogFilePath.Select": "Selecionar…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
|
||||
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
|
||||
|
||||
"Options.TitleMusic.Label": "Música do título",
|
||||
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
|
||||
|
||||
"Options.Discord.Label": "Presença no Discord",
|
||||
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
|
||||
|
||||
"Options.Language.Label": "Idioma do emulador",
|
||||
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
|
||||
|
||||
"Common.On": "Ativado",
|
||||
"Common.Off": "Desativado",
|
||||
|
||||
"Console.Title": "CONSOLA",
|
||||
"Console.SearchWatermark": "Pesquisar...",
|
||||
"Console.AutoScroll": "Deslocamento automático",
|
||||
"Console.Split": "Dividir",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpar",
|
||||
"Console.WindowTitle": "Consola do SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
|
||||
"Launch.Idle": "Inativo",
|
||||
"Launch.Console": "≡ Consola",
|
||||
"Launch.Launch": "▶ Iniciar",
|
||||
"Launch.Stop": "■ Parar",
|
||||
"Launch.Running": "Em execução — {0}",
|
||||
"Launch.Stopping": "A parar…",
|
||||
"Launch.Exited": "Terminou com o código {0} ({1})",
|
||||
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Ficheiro de registo: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argumentos inválidos",
|
||||
"Exit.EbootNotFound": "eboot não encontrado",
|
||||
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||
"Exit.EmulationError": "erro de emulação",
|
||||
"Exit.Unknown": "desconhecido",
|
||||
|
||||
"Status.EmulatorLocating": "Emulador: a localizar…",
|
||||
"Status.EmulatorPath": "Emulador: {0}",
|
||||
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "A analisar biblioteca…",
|
||||
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
|
||||
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
|
||||
"Status.Running": "A executar {0}",
|
||||
"Status.Stopping": "A parar…",
|
||||
"Status.Idle": "Inativo",
|
||||
|
||||
"Clipboard.Path": "Caminho",
|
||||
"Clipboard.TitleId": "ID do título",
|
||||
|
||||
"Discord.Playing": "A jogar {0}",
|
||||
"Discord.Browsing": "A navegar na biblioteca",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
|
||||
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||
"Dialog.PsExecutables": "Executáveis PS",
|
||||
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
|
||||
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
|
||||
"Dialog.LogFiles": "Ficheiros de Registo",
|
||||
|
||||
"Options.About" : "Sobre",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||
"About.GithubButton": "Contribua no GitHub!",
|
||||
"About.DiscordButton": "Junte-se ao nosso Discord!"
|
||||
}
|
||||
@@ -12,7 +12,8 @@ using Avalonia.Platform;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using SharpEmu.Libs.Pad;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Windows;
|
||||
using SharpEmu.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -33,6 +34,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();
|
||||
@@ -62,7 +69,7 @@ public partial class MainWindow : Window
|
||||
|
||||
// Controller navigation state.
|
||||
private readonly DispatcherTimer _gamepadTimer;
|
||||
private uint _previousPadButtons;
|
||||
private HostGamepadButtons _previousPadButtons;
|
||||
private long _navLeftNextAt;
|
||||
private long _navRightNextAt;
|
||||
private long _navUpNextAt;
|
||||
@@ -151,8 +158,8 @@ public partial class MainWindow : Window
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
Closing += (_, _) => OnWindowClosing();
|
||||
|
||||
DualSenseReader.EnsureStarted();
|
||||
XInputReader.EnsureStarted();
|
||||
WindowsDualSenseReader.EnsureStarted();
|
||||
WindowsXInputReader.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
@@ -165,7 +172,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "https://github.com/par274/sharpemu",
|
||||
FileName = "https://github.com/sharpemu/sharpemu",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
@@ -224,9 +231,9 @@ public partial class MainWindow : Window
|
||||
private void PollGamepad()
|
||||
{
|
||||
// DualSense wins when both are connected; XInput covers Xbox pads.
|
||||
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
|
||||
if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
|
||||
{
|
||||
_previousPadButtons = 0;
|
||||
_previousPadButtons = HostGamepadButtons.None;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,12 +246,12 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||
if ((shoulderPressed & OrbisPadButton.L1) != 0)
|
||||
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||
{
|
||||
SetActivePage(0);
|
||||
}
|
||||
|
||||
if ((shoulderPressed & OrbisPadButton.R1) != 0)
|
||||
if ((shoulderPressed & HostGamepadButtons.R1) != 0)
|
||||
{
|
||||
SetActivePage(1);
|
||||
}
|
||||
@@ -256,10 +263,10 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var now = Environment.TickCount64;
|
||||
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
|
||||
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
|
||||
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
|
||||
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
|
||||
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
|
||||
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
|
||||
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
|
||||
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
|
||||
|
||||
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
||||
{
|
||||
@@ -282,12 +289,12 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var pressed = pad.Buttons & ~_previousPadButtons;
|
||||
if ((pressed & 0x4000) != 0) // Cross
|
||||
if ((pressed & HostGamepadButtons.Cross) != 0)
|
||||
{
|
||||
LaunchSelected();
|
||||
}
|
||||
|
||||
if ((pressed & 0x2000) != 0) // Circle
|
||||
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||
{
|
||||
StopEmulator();
|
||||
}
|
||||
@@ -726,7 +733,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;
|
||||
@@ -736,7 +743,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)
|
||||
{
|
||||
@@ -749,7 +756,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;
|
||||
@@ -867,7 +874,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,
|
||||
@@ -1131,13 +1138,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);
|
||||
@@ -1161,7 +1168,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;
|
||||
@@ -1486,7 +1493,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,9 @@ 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>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
||||
@@ -41,14 +43,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
SharpEmu.Libs into the launcher. -->
|
||||
with the emulator's host input backend. They are dependency-free, so
|
||||
they are compiled in directly rather than pulling a reference to all
|
||||
of SharpEmu.HLE into the launcher. -->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
||||
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
|
||||
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
|
||||
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
|
||||
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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.
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
const int StackBufferLength = 512;
|
||||
const int ReadChunkLength = 128;
|
||||
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
|
||||
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||
try
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
var length = 0;
|
||||
while (length < capacity)
|
||||
{
|
||||
return false;
|
||||
// Bulk-read in bounded chunks rather than the full capacity: the string
|
||||
// may end just before unmapped memory, and overreading past the
|
||||
// terminator by more than a chunk could fault where the old
|
||||
// byte-by-byte loop succeeded.
|
||||
var chunk = Math.Min(ReadChunkLength, capacity - length);
|
||||
var span = bytes.Slice(length, chunk);
|
||||
if (Memory.TryRead(address + (ulong)length, span))
|
||||
{
|
||||
var terminator = span.IndexOf((byte)0);
|
||||
if (terminator >= 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
length += chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The chunk touches an unreadable range; fall back to per-byte reads so a
|
||||
// terminator sitting before the bad byte still yields the string.
|
||||
for (var i = 0; i < chunk; i++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[length + i] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
length += chunk;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
value = Encoding.UTF8.GetString(bytes[..capacity]);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented is not null)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
ArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
|
||||
ulong LastReturnRip,
|
||||
string? BlockReason);
|
||||
|
||||
/// <summary>
|
||||
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
|
||||
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
|
||||
/// returns true when the waiter has a final result and the thread should be re-readied;
|
||||
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
|
||||
/// its return value becomes the guest's RAX for the resumed call.
|
||||
/// </summary>
|
||||
public interface IGuestThreadBlockWaiter
|
||||
{
|
||||
int Resume();
|
||||
|
||||
bool TryWake();
|
||||
}
|
||||
|
||||
public interface IGuestThreadScheduler
|
||||
{
|
||||
bool SupportsGuestContextTransfer { get; }
|
||||
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
|
||||
private static string? _pendingBlockWakeKey;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<int>? _pendingBlockResumeHandler;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<bool>? _pendingBlockWakeHandler;
|
||||
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _pendingBlockDeadlineTimestamp;
|
||||
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey = null,
|
||||
Func<int>? resumeHandler = null,
|
||||
Func<bool>? wakeHandler = null,
|
||||
IGuestThreadBlockWaiter? waiter = null,
|
||||
long blockDeadlineTimestamp = 0)
|
||||
{
|
||||
if (!IsGuestThread)
|
||||
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
|
||||
|
||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||
_pendingBlockResumeHandler = resumeHandler;
|
||||
_pendingBlockWakeHandler = wakeHandler;
|
||||
_pendingBlockWaiter = waiter;
|
||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||
{
|
||||
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
|
||||
out hasContinuation,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
|
||||
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler)
|
||||
out IGuestThreadBlockWaiter? waiter)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out wakeKey,
|
||||
out resumeHandler,
|
||||
out wakeHandler,
|
||||
out waiter,
|
||||
out _);
|
||||
}
|
||||
|
||||
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler,
|
||||
out IGuestThreadBlockWaiter? waiter,
|
||||
out long blockDeadlineTimestamp)
|
||||
{
|
||||
reason = _pendingBlockReason ?? string.Empty;
|
||||
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
|
||||
continuation = default;
|
||||
hasContinuation = false;
|
||||
wakeKey = string.Empty;
|
||||
resumeHandler = null;
|
||||
wakeHandler = null;
|
||||
waiter = null;
|
||||
blockDeadlineTimestamp = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
|
||||
continuation = _pendingBlockContinuation;
|
||||
hasContinuation = _pendingBlockContinuationValid;
|
||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||
resumeHandler = _pendingBlockResumeHandler;
|
||||
wakeHandler = _pendingBlockWakeHandler;
|
||||
waiter = _pendingBlockWaiter;
|
||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
|
||||
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
|
||||
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum HostGamepadButtons : uint
|
||||
{
|
||||
None = 0,
|
||||
Up = 1 << 0,
|
||||
Down = 1 << 1,
|
||||
Left = 1 << 2,
|
||||
Right = 1 << 3,
|
||||
Cross = 1 << 4,
|
||||
Circle = 1 << 5,
|
||||
Square = 1 << 6,
|
||||
Triangle = 1 << 7,
|
||||
L1 = 1 << 8,
|
||||
R1 = 1 << 9,
|
||||
L2 = 1 << 10,
|
||||
R2 = 1 << 11,
|
||||
L3 = 1 << 12,
|
||||
R3 = 1 << 13,
|
||||
Options = 1 << 14,
|
||||
TouchPad = 1 << 15,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
|
||||
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
|
||||
/// snapshot buffers.
|
||||
/// </summary>
|
||||
public readonly record struct HostGamepadState(
|
||||
bool Connected,
|
||||
HostGamepadButtons Buttons,
|
||||
byte LeftX,
|
||||
byte LeftY,
|
||||
byte RightX,
|
||||
byte RightY,
|
||||
byte LeftTrigger,
|
||||
byte RightTrigger);
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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,23 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Host audio-output device access. The HLE audio exports convert guest submissions to
|
||||
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
|
||||
/// streams opened here; everything device-specific — queueing, backpressure, native
|
||||
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
|
||||
/// </summary>
|
||||
public interface IHostAudioOutput
|
||||
{
|
||||
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
|
||||
string BackendName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
|
||||
/// Throws when the host has no usable output device; callers degrade to a silent
|
||||
/// port and pace the guest instead.
|
||||
/// </summary>
|
||||
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
|
||||
/// the sample rate the stream was opened with.
|
||||
/// </summary>
|
||||
public interface IHostAudioStream : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Submits one buffer. May block briefly while the device drains its queue (this is
|
||||
/// what paces the guest's audio loop); returns false when the stream cannot accept
|
||||
/// audio, in which case the caller paces the guest itself.
|
||||
/// </summary>
|
||||
bool Submit(ReadOnlySpan<byte> stereoPcm16);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
|
||||
/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
|
||||
/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
|
||||
/// keyboard lives in the HLE pad exports.
|
||||
/// </summary>
|
||||
public interface IHostInput
|
||||
{
|
||||
/// <summary>Starts the background device readers once; safe to call repeatedly.</summary>
|
||||
void EnsureStarted();
|
||||
|
||||
/// <summary>
|
||||
/// Fills <paramref name="destination"/> with snapshots of currently connected
|
||||
/// gamepads and returns how many were written (0 when none are connected).
|
||||
/// </summary>
|
||||
int GetGamepadStates(Span<HostGamepadState> destination);
|
||||
|
||||
/// <summary>Human-readable name of the first connected gamepad, or null.</summary>
|
||||
string? DescribeConnectedGamepad();
|
||||
|
||||
/// <summary>Sets rumble on all connected gamepads; large = strong/left motor.</summary>
|
||||
void SetRumble(byte largeMotor, byte smallMotor);
|
||||
|
||||
/// <summary>
|
||||
/// Approximates per-trigger vibration on gamepads without independent trigger
|
||||
/// actuators; null leaves that trigger's current value unchanged.
|
||||
/// </summary>
|
||||
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
|
||||
|
||||
void SetLightbar(byte red, byte green, byte blue);
|
||||
|
||||
void ResetLightbar();
|
||||
|
||||
/// <summary>True when a window of this process has keyboard focus.</summary>
|
||||
bool IsHostWindowFocused();
|
||||
|
||||
/// <summary>Windows virtual-key code semantics; other backends translate.</summary>
|
||||
bool IsKeyDown(int virtualKey);
|
||||
}
|
||||
@@ -16,4 +16,8 @@ public interface IHostPlatform
|
||||
IHostThreading Threading { get; }
|
||||
|
||||
IHostSymbolResolver Symbols { get; }
|
||||
|
||||
IHostAudioOutput Audio { get; }
|
||||
|
||||
IHostInput Input { get; }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ public interface IHostThreading
|
||||
|
||||
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
||||
|
||||
/// <summary>
|
||||
/// Asks the OS for ~1 ms timed-wait granularity for the life of the process
|
||||
/// (idempotent; best-effort). No-op on platforms whose default is already fine.
|
||||
/// </summary>
|
||||
void RequestTimerResolution();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||
|
||||
@@ -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,528 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
/// <summary>
|
||||
/// POSIX virtual memory backend implemented over mmap/mprotect/munmap with a
|
||||
/// shadow region table that answers VirtualQuery-style questions and tracks
|
||||
/// page protections.
|
||||
/// POSIX anonymous mappings are demand-paged by the kernel, so Win32
|
||||
/// "reserve-only" regions are mapped as committed memory directly and
|
||||
/// commit requests become protection changes.
|
||||
/// </summary>
|
||||
internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
{
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
private const uint MEM_RESERVE = 0x2000;
|
||||
private const uint MEM_RELEASE = 0x8000;
|
||||
private const uint MEM_FREE_STATE = 0x10000;
|
||||
private const uint MEM_PRIVATE = 0x20000;
|
||||
|
||||
private const uint PAGE_NOACCESS = 0x01;
|
||||
private const uint PAGE_READONLY = 0x02;
|
||||
private const uint PAGE_READWRITE = 0x04;
|
||||
private const uint PAGE_EXECUTE = 0x10;
|
||||
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
private const ulong PageSize = 0x1000;
|
||||
|
||||
private struct BasicInfo
|
||||
{
|
||||
public ulong BaseAddress;
|
||||
public ulong AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public ulong RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return (ulong)Posix.Alloc(
|
||||
(void*)desiredAddress,
|
||||
(nuint)size,
|
||||
MEM_COMMIT | MEM_RESERVE,
|
||||
ToNativeProtection(protection));
|
||||
}
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return (ulong)Posix.Alloc(
|
||||
(void*)desiredAddress,
|
||||
(nuint)size,
|
||||
MEM_RESERVE,
|
||||
ToNativeProtection(protection));
|
||||
}
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||
{
|
||||
return Posix.Alloc(
|
||||
(void*)address,
|
||||
(nuint)size,
|
||||
MEM_COMMIT,
|
||||
ToNativeProtection(protection)) != null;
|
||||
}
|
||||
|
||||
public bool Free(ulong address)
|
||||
{
|
||||
return Posix.Free((void*)address, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
public bool Protect(
|
||||
ulong address,
|
||||
ulong size,
|
||||
HostPageProtection protection,
|
||||
out uint rawOldProtection)
|
||||
{
|
||||
return Posix.Protect(
|
||||
(void*)address,
|
||||
(nuint)size,
|
||||
ToNativeProtection(protection),
|
||||
out rawOldProtection);
|
||||
}
|
||||
|
||||
public bool ProtectRaw(
|
||||
ulong address,
|
||||
ulong size,
|
||||
uint rawProtection,
|
||||
out uint rawOldProtection)
|
||||
{
|
||||
return Posix.Protect(
|
||||
(void*)address,
|
||||
(nuint)size,
|
||||
rawProtection,
|
||||
out rawOldProtection);
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
if (Posix.Query((void*)address, out var nativeInfo) == 0)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
info = new HostRegionInfo(
|
||||
nativeInfo.BaseAddress,
|
||||
nativeInfo.AllocationBase,
|
||||
nativeInfo.RegionSize,
|
||||
nativeInfo.State switch
|
||||
{
|
||||
MEM_COMMIT => HostRegionState.Committed,
|
||||
MEM_RESERVE => HostRegionState.Reserved,
|
||||
_ => HostRegionState.Free,
|
||||
},
|
||||
nativeInfo.State,
|
||||
ToHostProtection(nativeInfo.Protect),
|
||||
nativeInfo.Protect,
|
||||
nativeInfo.AllocationProtect);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
_ = address;
|
||||
_ = size;
|
||||
// The supported POSIX process is x86-64 (including Rosetta 2), whose
|
||||
// instruction cache is coherent. A future arm64 backend must call the
|
||||
// platform instruction-cache invalidation API here.
|
||||
}
|
||||
|
||||
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
|
||||
{
|
||||
HostPageProtection.NoAccess => PAGE_NOACCESS,
|
||||
HostPageProtection.ReadOnly => PAGE_READONLY,
|
||||
HostPageProtection.ReadWrite => PAGE_READWRITE,
|
||||
HostPageProtection.Execute => PAGE_EXECUTE,
|
||||
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
|
||||
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
|
||||
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_READWRITE,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
|
||||
};
|
||||
|
||||
private static HostPageProtection ToHostProtection(uint protection) => protection switch
|
||||
{
|
||||
PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||
PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||
PAGE_EXECUTE => HostPageProtection.Execute,
|
||||
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||
_ => HostPageProtection.NoAccess,
|
||||
};
|
||||
|
||||
private static class Posix
|
||||
{
|
||||
private const int PROT_NONE = 0x0;
|
||||
private const int PROT_READ = 0x1;
|
||||
private const int PROT_WRITE = 0x2;
|
||||
private const int PROT_EXEC = 0x4;
|
||||
|
||||
private const int MAP_PRIVATE = 0x02;
|
||||
private const int MAP_FIXED = 0x10;
|
||||
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||
|
||||
// Linux-only: fail instead of clobbering an existing mapping.
|
||||
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||
|
||||
private static readonly nint MAP_FAILED = -1;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly SortedList<ulong, Region> Regions = new();
|
||||
|
||||
private sealed class Region
|
||||
{
|
||||
public ulong Base;
|
||||
public ulong Size;
|
||||
public uint DefaultProtect;
|
||||
public Dictionary<ulong, uint>? PageProtects;
|
||||
|
||||
public ulong End => Base + Size;
|
||||
|
||||
public uint ProtectAt(ulong pageAddress)
|
||||
{
|
||||
if (PageProtects is not null && PageProtects.TryGetValue(pageAddress, out var overriden))
|
||||
{
|
||||
return overriden;
|
||||
}
|
||||
|
||||
return DefaultProtect;
|
||||
}
|
||||
}
|
||||
|
||||
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var alignedSize = AlignUp((ulong)size, PageSize);
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (allocationType == MEM_COMMIT && address != null &&
|
||||
TryFindRegionLocked((ulong)address, out var existing))
|
||||
{
|
||||
// Note: MEM_RESERVE requests that overlap an existing
|
||||
// region must fail like Win32 does; only a pure commit
|
||||
// may target pages inside a tracked mapping.
|
||||
// Commit inside an existing mapping: the pages are already
|
||||
// backed (demand paged), so only apply the protection.
|
||||
var start = AlignDown((ulong)address, PageSize);
|
||||
var end = AlignUp((ulong)address + alignedSize, PageSize);
|
||||
if (end <= start || end > existing.End)
|
||||
{
|
||||
// Win32 fails a commit that runs past its reservation
|
||||
// instead of committing a prefix; committing partially
|
||||
// here would let callers believe the whole range is
|
||||
// usable.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(protect)) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
SetProtectRangeLocked(existing, start, end - start, protect);
|
||||
return address;
|
||||
}
|
||||
|
||||
if ((allocationType & MEM_RESERVE) == 0)
|
||||
{
|
||||
// MEM_COMMIT alone outside any known region is invalid here.
|
||||
return null;
|
||||
}
|
||||
|
||||
var posixProtect = ToPosixProtect(protect);
|
||||
var flags = MAP_PRIVATE | MAP_ANON;
|
||||
if ((allocationType & MEM_COMMIT) == 0)
|
||||
{
|
||||
// Reserve-only: keep the requested protection so the region
|
||||
// is usable without a separate commit step, but tell the
|
||||
// kernel not to account swap for it where supported.
|
||||
flags |= MAP_NORESERVE;
|
||||
}
|
||||
|
||||
nint result;
|
||||
if (address != null)
|
||||
{
|
||||
// Win32 maps at exactly the requested address or fails
|
||||
// without touching existing mappings. Fail up front on
|
||||
// any overlap we track, then place the mapping: Linux
|
||||
// gets MAP_FIXED_NOREPLACE (fails cleanly on host
|
||||
// mappings too). Darwin lacks NOREPLACE and plain
|
||||
// MAP_FIXED would silently clobber untracked host
|
||||
// memory (dyld, the runtime's JIT heap, Rosetta), so
|
||||
// pass the address as a hint instead -- the kernel
|
||||
// honors it when the range is free and relocates the
|
||||
// mapping otherwise, which we treat as failure.
|
||||
if (OverlapsTrackedRegionLocked((ulong)address, alignedSize))
|
||||
{
|
||||
Trace($"exact overlap: addr=0x{(ulong)address:X16} size=0x{alignedSize:X}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
|
||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
|
||||
if (result != MAP_FAILED && (ulong)result != (ulong)address)
|
||||
{
|
||||
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. This is routinely the case for the PS5's fixed
|
||||
// 32 GiB image base under Rosetta 2 on Apple Silicon, where
|
||||
// the kernel never honors that hint. Retry with true
|
||||
// MAP_FIXED: this can clobber an untracked host mapping
|
||||
// (dyld, the runtime's JIT heap, Rosetta) if one already
|
||||
// sits exactly there, but without it guest images that
|
||||
// require this base never load at all on macOS.
|
||||
Trace($"exact mmap hint failed, retrying with MAP_FIXED: addr=0x{(ulong)address:X16}");
|
||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, flags | MAP_FIXED, -1, 0);
|
||||
}
|
||||
|
||||
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||
{
|
||||
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||
if (result != MAP_FAILED)
|
||||
{
|
||||
munmap(result, (nuint)alignedSize);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = mmap(0, (nuint)alignedSize, posixProtect, flags, -1, 0);
|
||||
if (result == MAP_FAILED)
|
||||
{
|
||||
Trace($"mmap failed: size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Regions[(ulong)result] = new Region
|
||||
{
|
||||
Base = (ulong)result,
|
||||
Size = alignedSize,
|
||||
DefaultProtect = protect
|
||||
};
|
||||
|
||||
return (void*)result;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Free(void* address, nuint size, uint freeType)
|
||||
{
|
||||
_ = size;
|
||||
_ = freeType;
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Regions.TryGetValue((ulong)address, out var region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Regions.Remove((ulong)address);
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||
{
|
||||
oldProtect = PAGE_NOACCESS;
|
||||
if (size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var start = AlignDown((ulong)address, PageSize);
|
||||
var end = AlignUp((ulong)address + size, PageSize);
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (!TryFindRegionLocked(start, out var region) || end > region.End)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
oldProtect = region.ProtectAt(start);
|
||||
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(newProtect)) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetProtectRangeLocked(region, start, end - start, newProtect);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static nuint Query(void* address, out BasicInfo info)
|
||||
{
|
||||
info = default;
|
||||
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (TryFindRegionLocked(pageAddress, out var region))
|
||||
{
|
||||
// Win32 VirtualQuery reports a run of pages sharing the
|
||||
// same protection, so stop the run where it changes.
|
||||
var protect = region.ProtectAt(pageAddress);
|
||||
var runEnd = pageAddress + PageSize;
|
||||
while (runEnd < region.End && region.ProtectAt(runEnd) == protect)
|
||||
{
|
||||
runEnd += PageSize;
|
||||
}
|
||||
|
||||
info.BaseAddress = pageAddress;
|
||||
info.AllocationBase = region.Base;
|
||||
info.AllocationProtect = region.DefaultProtect;
|
||||
info.RegionSize = runEnd - pageAddress;
|
||||
info.State = MEM_COMMIT;
|
||||
info.Protect = protect;
|
||||
info.Type = MEM_PRIVATE;
|
||||
return (nuint)sizeof(BasicInfo);
|
||||
}
|
||||
|
||||
// Untracked host memory (runtime heaps, stacks, libraries) is
|
||||
// reported as a free block reaching to the next tracked region
|
||||
// so scanning callers keep advancing.
|
||||
var nextBase = ulong.MaxValue;
|
||||
foreach (var regionBase in Regions.Keys)
|
||||
{
|
||||
if (regionBase > pageAddress)
|
||||
{
|
||||
nextBase = regionBase;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info.BaseAddress = pageAddress;
|
||||
info.AllocationBase = 0;
|
||||
info.AllocationProtect = PAGE_NOACCESS;
|
||||
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
|
||||
info.State = MEM_FREE_STATE;
|
||||
info.Protect = PAGE_NOACCESS;
|
||||
info.Type = 0;
|
||||
return (nuint)sizeof(BasicInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool OverlapsTrackedRegionLocked(ulong start, ulong size)
|
||||
{
|
||||
var end = start + size;
|
||||
foreach (var region in Regions.Values)
|
||||
{
|
||||
if (region.Base < end && start < region.End)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||
{
|
||||
region = null!;
|
||||
var keys = Regions.Keys;
|
||||
var low = 0;
|
||||
var high = keys.Count - 1;
|
||||
Region? candidate = null;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
var entry = Regions.Values[middle];
|
||||
if (entry.Base <= address)
|
||||
{
|
||||
candidate = entry;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate is null || address >= candidate.End)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
region = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||
{
|
||||
if (start == region.Base && size >= region.Size)
|
||||
{
|
||||
region.DefaultProtect = protect;
|
||||
region.PageProtects = null;
|
||||
return;
|
||||
}
|
||||
|
||||
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||
var end = start + size;
|
||||
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||
{
|
||||
if (protect == region.DefaultProtect)
|
||||
{
|
||||
region.PageProtects.Remove(pageAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
region.PageProtects[pageAddress] = protect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ToPosixProtect(uint win32Protect)
|
||||
{
|
||||
return win32Protect switch
|
||||
{
|
||||
PAGE_NOACCESS => PROT_NONE,
|
||||
PAGE_READONLY => PROT_READ,
|
||||
PAGE_READWRITE => PROT_READ | PROT_WRITE,
|
||||
PAGE_EXECUTE => PROT_READ | PROT_EXEC,
|
||||
PAGE_EXECUTE_READ => PROT_READ | PROT_EXEC,
|
||||
PAGE_EXECUTE_READWRITE => PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
_ => PROT_READ | PROT_WRITE
|
||||
};
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[HOSTMEM] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment) => checked((value + alignment - 1) & ~(alignment - 1));
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int munmap(nint addr, nuint length);
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
internal sealed class PosixHostPlatform : IHostPlatform
|
||||
{
|
||||
public IHostMemory Memory { get; } = new PosixHostMemory();
|
||||
|
||||
public IHostThreading Threading { get; } = new PosixHostThreading();
|
||||
|
||||
public IHostSymbolResolver Symbols { get; } = new PosixHostSymbolResolver();
|
||||
|
||||
public IHostAudioOutput Audio { get; } = new PosixHostAudio();
|
||||
|
||||
public IHostInput Input { get; } = new PosixHostInput();
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
/// <summary>
|
||||
/// POSIX replacements for the kernel32 helpers the native backend embeds in
|
||||
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
|
||||
/// convention the emitted call sites were written for (first argument in
|
||||
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
|
||||
/// emission code stays identical across platforms.
|
||||
/// </summary>
|
||||
internal static unsafe class PosixHostStubs
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static bool _initialized;
|
||||
private static nint _tlsGetValueStub;
|
||||
private static nint _queryPerformanceCounterStub;
|
||||
private static nint _switchToThreadStub;
|
||||
private static nint _sleepStub;
|
||||
private static nint _waitForSingleObjectStub;
|
||||
private static nint _setEventStub;
|
||||
private static nint _exitThreadStub;
|
||||
|
||||
public static nint TlsGetValueStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _tlsGetValueStub; }
|
||||
}
|
||||
|
||||
public static nint QueryPerformanceCounterStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
|
||||
}
|
||||
|
||||
public static nint SwitchToThreadStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _switchToThreadStub; }
|
||||
}
|
||||
|
||||
public static nint SleepStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _sleepStub; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Win64-convention replacements for the kernel32 event/thread helpers the
|
||||
/// native guest worker loop embeds. The "handle" they take is a worker
|
||||
/// event created by <see cref="CreateWorkerEvent"/>: a dispatch semaphore
|
||||
/// on macOS, an unnamed POSIX semaphore on Linux. The wait stub always
|
||||
/// waits forever (the worker loop passes INFINITE) and retries EINTR.
|
||||
/// </summary>
|
||||
public static nint WaitForSingleObjectStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _waitForSingleObjectStub; }
|
||||
}
|
||||
|
||||
public static nint SetEventStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _setEventStub; }
|
||||
}
|
||||
|
||||
public static nint ExitThreadStubAddress
|
||||
{
|
||||
get { EnsureInitialized(); return _exitThreadStub; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a binary-semaphore worker event signalable/waitable both from
|
||||
/// managed code and from emitted native code (via the stub addresses
|
||||
/// above). Returns 0 on failure.
|
||||
/// </summary>
|
||||
public static nint CreateWorkerEvent()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return dispatch_semaphore_create(0);
|
||||
}
|
||||
|
||||
var semaphore = Marshal.AllocHGlobal(64);
|
||||
if (sem_init(semaphore, 0, 0) != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(semaphore);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return semaphore;
|
||||
}
|
||||
|
||||
public static bool SignalWorkerEvent(nint handle)
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_ = dispatch_semaphore_signal(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return sem_post(handle) == 0;
|
||||
}
|
||||
|
||||
/// <summary>Waits for a worker event; a negative timeout waits forever.</summary>
|
||||
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (timeoutMilliseconds < 0)
|
||||
{
|
||||
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
|
||||
}
|
||||
|
||||
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
|
||||
return dispatch_semaphore_wait(handle, deadline) == 0;
|
||||
}
|
||||
|
||||
if (timeoutMilliseconds < 0)
|
||||
{
|
||||
while (sem_wait(handle) != 0)
|
||||
{
|
||||
// EINTR: retry.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
||||
while (sem_trywait(handle) != 0)
|
||||
{
|
||||
if (Environment.TickCount64 >= deadlineTicks)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void DestroyWorkerEvent(nint handle)
|
||||
{
|
||||
if (handle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
dispatch_release(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = sem_destroy(handle);
|
||||
Marshal.FreeHGlobal(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a raw pthread at a native entry point (pthread entries take their
|
||||
/// argument in RDI; the worker loop stub ignores it). Returns an opaque
|
||||
/// handle for <see cref="WaitForWorkerThreadExit"/>/<see cref="CloseWorkerThreadHandle"/>,
|
||||
/// or 0 on failure.
|
||||
/// </summary>
|
||||
public static nint CreateWorkerThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
|
||||
{
|
||||
threadId = 0;
|
||||
byte* attr = stackalloc byte[512];
|
||||
if (pthread_attr_init(attr) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (stackReserveBytes != 0)
|
||||
{
|
||||
_ = pthread_attr_setstacksize(attr, nuint.Max(stackReserveBytes, 512 * 1024));
|
||||
}
|
||||
|
||||
nint thread;
|
||||
if (pthread_create(&thread, attr, entry, parameter) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
ulong numericId;
|
||||
if (pthread_threadid_np(thread, &numericId) == 0)
|
||||
{
|
||||
threadId = unchecked((uint)numericId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
threadId = unchecked((uint)thread);
|
||||
}
|
||||
|
||||
var holder = (nint*)Marshal.AllocHGlobal(sizeof(nint) * 2);
|
||||
holder[0] = thread;
|
||||
holder[1] = 0; // joined flag
|
||||
return (nint)holder;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = pthread_attr_destroy(attr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a worker thread to exit. Liveness is probed with
|
||||
/// pthread_kill(thread, 0) (ESRCH once the thread has terminated) because
|
||||
/// neither platform offers a portable timed join; the exited thread is then
|
||||
/// joined so its resources are reclaimed.
|
||||
/// </summary>
|
||||
public static bool WaitForWorkerThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||
{
|
||||
var holder = (nint*)threadHandle;
|
||||
if (holder == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (holder[1] != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var thread = holder[0];
|
||||
var deadline = Environment.TickCount64 + timeoutMilliseconds;
|
||||
while (pthread_kill(thread, 0) == 0)
|
||||
{
|
||||
if (Environment.TickCount64 >= deadline)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(1);
|
||||
}
|
||||
|
||||
_ = pthread_join(thread, null);
|
||||
holder[1] = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void CloseWorkerThreadHandle(nint threadHandle)
|
||||
{
|
||||
var holder = (nint*)threadHandle;
|
||||
if (holder == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (holder[1] == 0)
|
||||
{
|
||||
// Never observed exiting: detach so the thread does not leak a
|
||||
// zombie join target when it eventually terminates.
|
||||
_ = pthread_detach(holder[0]);
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(threadHandle);
|
||||
}
|
||||
|
||||
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
|
||||
public static uint TlsAlloc()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
nuint key;
|
||||
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
|
||||
}
|
||||
|
||||
uint key32;
|
||||
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
|
||||
}
|
||||
|
||||
public static bool TlsFree(uint key)
|
||||
{
|
||||
return OperatingSystem.IsMacOS()
|
||||
? pthread_key_delete_mac((nuint)key) == 0
|
||||
: pthread_key_delete_linux(key) == 0;
|
||||
}
|
||||
|
||||
public static bool TlsSetValue(uint key, nint value)
|
||||
{
|
||||
return OperatingSystem.IsMacOS()
|
||||
? pthread_setspecific_mac((nuint)key, value) == 0
|
||||
: pthread_setspecific_linux(key, value) == 0;
|
||||
}
|
||||
|
||||
public static nint TlsGetValue(uint key)
|
||||
{
|
||||
return OperatingSystem.IsMacOS()
|
||||
? pthread_getspecific_mac((nuint)key)
|
||||
: pthread_getspecific_linux(key);
|
||||
}
|
||||
|
||||
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
|
||||
public static uint GetCurrentThreadId()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
ulong tid;
|
||||
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
|
||||
}
|
||||
|
||||
return unchecked((uint)gettid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
|
||||
/// thunk that accepts up to four integer arguments in the Win64 ABI the
|
||||
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
|
||||
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
|
||||
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
|
||||
/// the stack 16-byte aligned for the call, and forwards the rax result.
|
||||
/// </summary>
|
||||
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
|
||||
{
|
||||
var memory = HostPlatform.Current.Memory;
|
||||
var page = (byte*)memory.Allocate(
|
||||
0,
|
||||
4096,
|
||||
HostPageProtection.ReadWriteExecute);
|
||||
if (page == null)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
|
||||
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
|
||||
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
|
||||
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
|
||||
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
||||
}
|
||||
|
||||
memory.FlushInstructionCache((ulong)page, (ulong)offset);
|
||||
return (nint)page;
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildStubs();
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildStubs()
|
||||
{
|
||||
var memory = HostPlatform.Current.Memory;
|
||||
var page = (byte*)memory.Allocate(
|
||||
0,
|
||||
4096,
|
||||
HostPageProtection.ReadWriteExecute);
|
||||
if (page == null)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
|
||||
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
|
||||
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
|
||||
_sleepStub = EmitSleep(page, ref offset);
|
||||
_waitForSingleObjectStub = EmitWaitForSingleObject(page, ref offset);
|
||||
_setEventStub = EmitSetEvent(page, ref offset);
|
||||
_exitThreadStub = EmitExitThread(page, ref offset);
|
||||
|
||||
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
||||
}
|
||||
|
||||
memory.FlushInstructionCache((ulong)page, (ulong)offset);
|
||||
}
|
||||
|
||||
private static nint EmitTlsGetValue(byte* page, ref int offset)
|
||||
{
|
||||
var start = (nint)(page + offset);
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// On macOS x86-64 pthread keys index the gs-based thread specific
|
||||
// data array directly, so TlsGetValue(index in ecx) collapses to a
|
||||
// single load that clobbers nothing but RAX.
|
||||
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
// Linux: call pthread_getspecific, preserving the registers that are
|
||||
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
||||
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
|
||||
{
|
||||
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
|
||||
// consumers only need a monotonically increasing counter, which rdtsc
|
||||
// provides without leaving Win64-safe registers.
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
|
||||
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
|
||||
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
|
||||
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitSwitchToThread(byte* page, ref int offset)
|
||||
{
|
||||
var schedYield = ResolveLibcExport("sched_yield");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||
var usleep = ResolveLibcExport("usleep");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitWaitForSingleObject(byte* page, ref int offset)
|
||||
{
|
||||
// DWORD WaitForSingleObject(worker event in rcx, timeout in edx): the
|
||||
// worker loop only ever waits forever, so the timeout is ignored.
|
||||
// macOS waits on a dispatch semaphore (needs DISPATCH_TIME_FOREVER in
|
||||
// rsi), Linux on a sem_t; both retry until the wait succeeds (EINTR).
|
||||
var wait = ResolveLibcExport(
|
||||
OperatingSystem.IsMacOS() ? "dispatch_semaphore_wait" : "sem_wait");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x53); // push rbx
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xCB); // mov rbx, rcx
|
||||
var retry = offset;
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xDF); // mov rdi, rbx
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Emit(page, ref offset, 0x48, 0xC7, 0xC6, 0xFF, 0xFF, 0xFF, 0xFF); // mov rsi, DISPATCH_TIME_FOREVER
|
||||
}
|
||||
EmitMovRaxImm64(page, ref offset, wait); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
|
||||
Emit(page, ref offset, 0x75, unchecked((byte)(retry - (offset + 2)))); // jnz retry
|
||||
Emit(page, ref offset, 0x31, 0xC0); // xor eax, eax (WAIT_OBJECT_0)
|
||||
Emit(page, ref offset, 0x5B); // pop rbx
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitSetEvent(byte* page, ref int offset)
|
||||
{
|
||||
// BOOL SetEvent(worker event in rcx) -> dispatch_semaphore_signal /
|
||||
// sem_post.
|
||||
var signal = ResolveLibcExport(
|
||||
OperatingSystem.IsMacOS() ? "dispatch_semaphore_signal" : "sem_post");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||
EmitMovRaxImm64(page, ref offset, signal); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint EmitExitThread(byte* page, ref int offset)
|
||||
{
|
||||
// void ExitThread(code in ecx) -> pthread_exit(NULL); never returns,
|
||||
// so no registers need preserving. pthread_exit runs the thread's TSD
|
||||
// destructors, which detaches the CLR if the thread lazily attached.
|
||||
var pthreadExit = ResolveLibcExport("pthread_exit");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x31, 0xFF); // xor edi, edi
|
||||
EmitMovRaxImm64(page, ref offset, pthreadExit); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0xCC); // int3 (never returns)
|
||||
return start;
|
||||
}
|
||||
|
||||
private static nint ResolveLibcExport(string name)
|
||||
{
|
||||
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
|
||||
return NativeLibrary.GetExport(libc, name);
|
||||
}
|
||||
|
||||
private static void Emit(byte* page, ref int offset, params byte[] bytes)
|
||||
{
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
page[offset++] = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
|
||||
{
|
||||
Emit(page, ref offset, 0x48, 0xB8);
|
||||
*(long*)(page + offset) = value;
|
||||
offset += sizeof(long);
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||
private static extern int pthread_key_create_linux(uint* key, nint destructor);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||
private static extern int pthread_key_delete_mac(nuint key);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||
private static extern int pthread_key_delete_linux(uint key);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||
private static extern int pthread_setspecific_mac(nuint key, nint value);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||
private static extern int pthread_setspecific_linux(uint key, nint value);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||
private static extern nint pthread_getspecific_mac(nuint key);
|
||||
|
||||
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||
private static extern nint pthread_getspecific_linux(uint key);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int gettid();
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_attr_init(byte* attr);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_attr_destroy(byte* attr);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_attr_setstacksize(byte* attr, nuint stackSize);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_create(nint* thread, byte* attr, nint startRoutine, nint arg);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_join(nint thread, nint* returnValue);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_detach(nint thread);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int pthread_kill(nint thread, int signal);
|
||||
|
||||
// macOS: dispatch semaphores back the worker events (unnamed sem_init is
|
||||
// unsupported on Darwin). libSystem reexports libdispatch, so "libc"
|
||||
// resolves these like the pthread imports above.
|
||||
[DllImport("libc")]
|
||||
private static extern nint dispatch_semaphore_create(long value);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern nint dispatch_semaphore_signal(nint semaphore);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern void dispatch_release(nint handle);
|
||||
|
||||
// Linux: unnamed POSIX semaphores.
|
||||
[DllImport("libc")]
|
||||
private static extern int sem_init(nint semaphore, int shared, uint value);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int sem_post(nint semaphore);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int sem_wait(nint semaphore);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int sem_trywait(nint semaphore);
|
||||
|
||||
[DllImport("libc")]
|
||||
private static extern int sem_destroy(nint semaphore);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
internal sealed class PosixHostSymbolResolver : IHostSymbolResolver
|
||||
{
|
||||
public nint GetAddress(HostRuntimeFunction function) => function switch
|
||||
{
|
||||
HostRuntimeFunction.TlsGetValue => PosixHostStubs.TlsGetValueStubAddress,
|
||||
HostRuntimeFunction.QueryPerformanceCounter => PosixHostStubs.QueryPerformanceCounterStubAddress,
|
||||
HostRuntimeFunction.SwitchToThread => PosixHostStubs.SwitchToThreadStubAddress,
|
||||
HostRuntimeFunction.Sleep => PosixHostStubs.SleepStubAddress,
|
||||
HostRuntimeFunction.WaitForSingleObject => PosixHostStubs.WaitForSingleObjectStubAddress,
|
||||
HostRuntimeFunction.SetEvent => PosixHostStubs.SetEventStubAddress,
|
||||
HostRuntimeFunction.ExitThread => PosixHostStubs.ExitThreadStubAddress,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
internal sealed class PosixHostThreading : IHostThreading
|
||||
{
|
||||
public uint AllocateTlsSlot() => PosixHostStubs.TlsAlloc();
|
||||
|
||||
public bool FreeTlsSlot(uint slot) => PosixHostStubs.TlsFree(slot);
|
||||
|
||||
public bool SetTlsValue(uint slot, nint value) => PosixHostStubs.TlsSetValue(slot, value);
|
||||
|
||||
public nint GetTlsValue(uint slot) => PosixHostStubs.TlsGetValue(slot);
|
||||
|
||||
public uint CurrentThreadId => PosixHostStubs.GetCurrentThreadId();
|
||||
|
||||
public void RequestTimerResolution()
|
||||
{
|
||||
// POSIX sleep primitives are already high-resolution; there is no
|
||||
// timeBeginPeriod equivalent to request.
|
||||
}
|
||||
|
||||
// Thread affinity is advisory on POSIX hosts (macOS offers no
|
||||
// pthread-level affinity API); callers treat false as "not applied".
|
||||
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
|
||||
{
|
||||
_ = affinityMask;
|
||||
return false;
|
||||
}
|
||||
|
||||
public nint CreateNativeThread(
|
||||
nint entry,
|
||||
nint parameter,
|
||||
nuint stackReserveBytes,
|
||||
out uint threadId)
|
||||
{
|
||||
return PosixHostStubs.CreateWorkerThread(entry, parameter, stackReserveBytes, out threadId);
|
||||
}
|
||||
|
||||
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
|
||||
{
|
||||
return PosixHostStubs.WaitForWorkerThreadExit(threadHandle, timeoutMilliseconds);
|
||||
}
|
||||
|
||||
public void CloseThreadHandle(nint threadHandle)
|
||||
{
|
||||
PosixHostStubs.CloseWorkerThreadHandle(threadHandle);
|
||||
}
|
||||
|
||||
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
|
||||
{
|
||||
_ = threadId;
|
||||
registers = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+51
-49
@@ -3,21 +3,21 @@
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DualSense controller over raw HID on a background thread.
|
||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
||||
/// </summary>
|
||||
internal static class DualSenseReader
|
||||
internal static class WindowsDualSenseReader
|
||||
{
|
||||
private const ushort SonyVendorId = 0x054C;
|
||||
private const ushort DualSenseProductId = 0x0CE6;
|
||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static PadState _state;
|
||||
private static HostGamepadState _state;
|
||||
private static bool _started;
|
||||
|
||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
||||
@@ -37,6 +37,8 @@ internal static class DualSenseReader
|
||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||
internal static void EnsureStarted()
|
||||
{
|
||||
// The GUI source-links this reader and calls it directly, without the
|
||||
// host-platform resolution that otherwise guarantees Windows.
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
@@ -59,7 +61,7 @@ internal static class DualSenseReader
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetState(out PadState state)
|
||||
internal static bool TryGetState(out HostGamepadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
@@ -69,7 +71,7 @@ internal static class DualSenseReader
|
||||
return state.Connected;
|
||||
}
|
||||
|
||||
private static void SetState(in PadState state)
|
||||
private static void SetState(in HostGamepadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
@@ -148,11 +150,11 @@ internal static class DualSenseReader
|
||||
{
|
||||
if (_outputStream is null)
|
||||
{
|
||||
var handle = HidNative.CreateFile(
|
||||
var handle = WindowsHidNative.CreateFile(
|
||||
_devicePath,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
@@ -262,7 +264,7 @@ internal static class DualSenseReader
|
||||
// to the full 0x31 input report. Harmless over USB.
|
||||
var feature = new byte[41];
|
||||
feature[0] = 0x05;
|
||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||
_ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||
|
||||
if (!announcedConnect)
|
||||
{
|
||||
@@ -320,18 +322,18 @@ internal static class DualSenseReader
|
||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
||||
{
|
||||
devicePath = null;
|
||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
||||
foreach (var path in WindowsHidNative.EnumerateHidDevicePaths())
|
||||
{
|
||||
// Open without access rights just to query VID/PID.
|
||||
using var probe = HidNative.CreateFile(
|
||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
||||
using var probe = WindowsHidNative.CreateFile(
|
||||
path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
|
||||
if (probe.IsInvalid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||
var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
|
||||
if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||
attributes.VendorId != SonyVendorId ||
|
||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
||||
{
|
||||
@@ -339,19 +341,19 @@ internal static class DualSenseReader
|
||||
}
|
||||
|
||||
// Read+write so feature reports work; fall back to read-only.
|
||||
var handle = HidNative.CreateFile(
|
||||
var handle = WindowsHidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
handle = HidNative.CreateFile(
|
||||
handle = WindowsHidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
WindowsHidNative.GenericRead,
|
||||
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||
}
|
||||
|
||||
if (!handle.IsInvalid)
|
||||
@@ -366,7 +368,7 @@ internal static class DualSenseReader
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
|
||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out HostGamepadState state)
|
||||
{
|
||||
// USB: report id 0x01, payload starts at [1].
|
||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
||||
@@ -395,43 +397,43 @@ internal static class DualSenseReader
|
||||
var buttons1 = report[offset + 8];
|
||||
var buttons2 = report[offset + 9];
|
||||
|
||||
uint buttons = 0;
|
||||
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
||||
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
||||
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
||||
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
||||
var buttons = HostGamepadButtons.None;
|
||||
buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
|
||||
buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
|
||||
buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
|
||||
buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
||||
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
||||
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
||||
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
||||
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
||||
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
||||
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
||||
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
||||
buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
|
||||
buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
|
||||
buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
|
||||
buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
|
||||
buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
|
||||
buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
|
||||
buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
|
||||
buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||
|
||||
state = new PadState(
|
||||
state = new HostGamepadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: leftX,
|
||||
LeftY: leftY,
|
||||
RightX: rightX,
|
||||
RightY: rightY,
|
||||
L2: l2,
|
||||
R2: r2);
|
||||
LeftTrigger: l2,
|
||||
RightTrigger: r2);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint HatToButtons(int hat) => hat switch
|
||||
private static HostGamepadButtons HatToButtons(int hat) => hat switch
|
||||
{
|
||||
0 => OrbisPadButton.Up,
|
||||
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
||||
2 => OrbisPadButton.Right,
|
||||
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
||||
4 => OrbisPadButton.Down,
|
||||
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
||||
6 => OrbisPadButton.Left,
|
||||
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
||||
0 => HostGamepadButtons.Up,
|
||||
1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
|
||||
2 => HostGamepadButtons.Right,
|
||||
3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
|
||||
4 => HostGamepadButtons.Down,
|
||||
5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
|
||||
6 => HostGamepadButtons.Left,
|
||||
7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
+23
-18
@@ -4,13 +4,13 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
||||
/// directly, without any external input library.
|
||||
/// </summary>
|
||||
internal static partial class HidNative
|
||||
internal static partial class WindowsHidNative
|
||||
{
|
||||
internal const int DigcfPresent = 0x02;
|
||||
internal const int DigcfDeviceInterface = 0x10;
|
||||
@@ -38,28 +38,32 @@ internal static partial class HidNative
|
||||
public ushort VersionNumber;
|
||||
}
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
|
||||
[LibraryImport("hid.dll")]
|
||||
internal static partial void HidD_GetHidGuid(out Guid hidGuid);
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
||||
[LibraryImport("hid.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
||||
[LibraryImport("hid.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
|
||||
|
||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
||||
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
|
||||
internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
||||
|
||||
[DllImport("setupapi.dll")]
|
||||
internal static extern bool SetupDiEnumDeviceInterfaces(
|
||||
[LibraryImport("setupapi.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetupDiEnumDeviceInterfaces(
|
||||
nint deviceInfoSet,
|
||||
nint deviceInfoData,
|
||||
ref Guid interfaceClassGuid,
|
||||
int memberIndex,
|
||||
ref SpDeviceInterfaceData deviceInterfaceData);
|
||||
|
||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern bool SetupDiGetDeviceInterfaceDetail(
|
||||
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetupDiGetDeviceInterfaceDetail(
|
||||
nint deviceInfoSet,
|
||||
ref SpDeviceInterfaceData deviceInterfaceData,
|
||||
nint deviceInterfaceDetailData,
|
||||
@@ -67,11 +71,12 @@ internal static partial class HidNative
|
||||
out int requiredSize,
|
||||
nint deviceInfoData);
|
||||
|
||||
[DllImport("setupapi.dll")]
|
||||
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
||||
[LibraryImport("setupapi.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern SafeFileHandle CreateFile(
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||
internal static partial SafeFileHandle CreateFile(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows input backend: DualSense over raw HID plus XInput controllers for gamepads,
|
||||
/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
|
||||
/// only exists on the DualSense.
|
||||
/// </summary>
|
||||
internal sealed partial class WindowsHostInput : IHostInput
|
||||
{
|
||||
public void EnsureStarted()
|
||||
{
|
||||
WindowsDualSenseReader.EnsureStarted();
|
||||
WindowsXInputReader.EnsureStarted();
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
var count = 0;
|
||||
if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
|
||||
{
|
||||
destination[count++] = dualSense;
|
||||
}
|
||||
|
||||
if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
|
||||
{
|
||||
destination[count++] = xinput;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public string? DescribeConnectedGamepad()
|
||||
{
|
||||
if (WindowsDualSenseReader.TryGetState(out _))
|
||||
{
|
||||
return "DualSense";
|
||||
}
|
||||
|
||||
return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
|
||||
}
|
||||
|
||||
public void SetRumble(byte largeMotor, byte smallMotor)
|
||||
{
|
||||
WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
|
||||
WindowsXInputReader.SetRumble(largeMotor, smallMotor);
|
||||
}
|
||||
|
||||
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
|
||||
WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
|
||||
|
||||
public void SetLightbar(byte red, byte green, byte blue) =>
|
||||
WindowsDualSenseReader.SetLightbar(red, green, blue);
|
||||
|
||||
public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
|
||||
|
||||
public bool IsHostWindowFocused()
|
||||
{
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
||||
return processId == (uint)Environment.ProcessId;
|
||||
}
|
||||
|
||||
public bool IsKeyDown(int virtualKey) =>
|
||||
(GetAsyncKeyState(virtualKey) & 0x8000) != 0;
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial short GetAsyncKeyState(int vKey);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial nint GetForegroundWindow();
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
}
|
||||
@@ -10,4 +10,8 @@ internal sealed class WindowsHostPlatform : IHostPlatform
|
||||
public IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||
|
||||
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
||||
|
||||
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
|
||||
|
||||
public IHostInput Input { get; } = new WindowsHostInput();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,31 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
||||
private const int CtxRbp = 160;
|
||||
private const int CtxRip = 248;
|
||||
|
||||
private static int _timerResolutionRequested;
|
||||
|
||||
public void RequestTimerResolution()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (TimeBeginPeriod(1) != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Host timer resolution request rejected; " +
|
||||
"timed waits keep the default ~15.6 ms granularity.");
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public uint AllocateTlsSlot() => TlsAlloc();
|
||||
|
||||
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||
@@ -162,4 +187,7 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool CloseHandle(nint hObject);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
|
||||
private static partial uint TimeBeginPeriod(uint uPeriod);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
internal sealed partial class WindowsWaveOutAudio : IHostAudioOutput
|
||||
{
|
||||
public string BackendName => "winmm";
|
||||
|
||||
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
|
||||
|
||||
private sealed partial class WaveOutStream : IHostAudioStream
|
||||
{
|
||||
private const uint WaveMapper = uint.MaxValue;
|
||||
private const uint CallbackEvent = 0x0005_0000;
|
||||
private const ushort WaveFormatPcm = 1;
|
||||
private const uint WaveHeaderDone = 0x0000_0001;
|
||||
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly AutoResetEvent _completion = new(false);
|
||||
private readonly Queue<NativeBuffer> _buffers = new();
|
||||
private IntPtr _device;
|
||||
private int _queuedPcmBytes;
|
||||
private bool _disposed;
|
||||
|
||||
public WaveOutStream(uint sampleRate)
|
||||
{
|
||||
var format = new WaveFormat
|
||||
{
|
||||
FormatTag = WaveFormatPcm,
|
||||
Channels = 2,
|
||||
SamplesPerSecond = sampleRate,
|
||||
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||
BlockAlign = 4,
|
||||
BitsPerSample = 16,
|
||||
ExtraSize = 0,
|
||||
};
|
||||
var result = WaveOutOpen(
|
||||
out _device,
|
||||
WaveMapper,
|
||||
ref format,
|
||||
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||
IntPtr.Zero,
|
||||
CallbackEvent);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReapCompletedBuffers();
|
||||
while (_queuedPcmBytes != 0 &&
|
||||
_queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
|
||||
{
|
||||
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReapCompletedBuffers();
|
||||
}
|
||||
|
||||
return QueueBuffer(stereoPcm16);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (_device != IntPtr.Zero)
|
||||
{
|
||||
WaveOutReset(_device);
|
||||
while (_buffers.TryDequeue(out var buffer))
|
||||
{
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
|
||||
WaveOutClose(_device);
|
||||
_device = IntPtr.Zero;
|
||||
}
|
||||
|
||||
_completion.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||
var headerAddress = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||
}
|
||||
|
||||
var header = new WaveHeader
|
||||
{
|
||||
Data = dataAddress,
|
||||
BufferLength = checked((uint)data.Length),
|
||||
};
|
||||
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||
Marshal.StructureToPtr(header, headerAddress, false);
|
||||
|
||||
var result = WaveOutPrepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = WaveOutWrite(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
return false;
|
||||
}
|
||||
|
||||
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||
_queuedPcmBytes += data.Length;
|
||||
dataAddress = IntPtr.Zero;
|
||||
headerAddress = IntPtr.Zero;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (headerAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(headerAddress);
|
||||
}
|
||||
|
||||
if (dataAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(dataAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReapCompletedBuffers()
|
||||
{
|
||||
while (_buffers.TryPeek(out var buffer))
|
||||
{
|
||||
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||
if ((header.Flags & WaveHeaderDone) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_buffers.Dequeue();
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseBuffer(NativeBuffer buffer)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
buffer.Header,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
_queuedPcmBytes -= buffer.Length;
|
||||
Marshal.FreeHGlobal(buffer.Header);
|
||||
Marshal.FreeHGlobal(buffer.Data);
|
||||
}
|
||||
|
||||
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct WaveFormat
|
||||
{
|
||||
public ushort FormatTag;
|
||||
public ushort Channels;
|
||||
public uint SamplesPerSecond;
|
||||
public uint AverageBytesPerSecond;
|
||||
public ushort BlockAlign;
|
||||
public ushort BitsPerSample;
|
||||
public ushort ExtraSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WaveHeader
|
||||
{
|
||||
public IntPtr Data;
|
||||
public uint BufferLength;
|
||||
public uint BytesRecorded;
|
||||
public nuint User;
|
||||
public uint Flags;
|
||||
public uint Loops;
|
||||
public IntPtr Next;
|
||||
public nuint Reserved;
|
||||
}
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||
private static partial uint WaveOutOpen(
|
||||
out IntPtr device,
|
||||
uint deviceId,
|
||||
ref WaveFormat format,
|
||||
IntPtr callback,
|
||||
IntPtr instance,
|
||||
uint flags);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||
private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||
private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||
private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||
private static partial uint WaveOutReset(IntPtr device);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||
private static partial uint WaveOutClose(IntPtr device);
|
||||
}
|
||||
}
|
||||
+36
-34
@@ -3,15 +3,15 @@
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
namespace SharpEmu.HLE.Host.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
||||
/// the Windows XInput API on a background thread, translated to the same
|
||||
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
|
||||
/// and hot-plug retry; the first connected slot (of four) is used.
|
||||
/// the Windows XInput API on a background thread, translated to
|
||||
/// <see cref="HostGamepadState"/> conventions. Supports rumble and hot-plug
|
||||
/// retry; the first connected slot (of four) is used.
|
||||
/// </summary>
|
||||
internal static class XInputReader
|
||||
internal static partial class WindowsXInputReader
|
||||
{
|
||||
private const uint ErrorSuccess = 0;
|
||||
private const int SlotCount = 4;
|
||||
@@ -34,7 +34,7 @@ internal static class XInputReader
|
||||
private const ushort XinputY = 0x8000;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static PadState _state;
|
||||
private static HostGamepadState _state;
|
||||
private static bool _started;
|
||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||
private static byte _motorLeft;
|
||||
@@ -45,6 +45,8 @@ internal static class XInputReader
|
||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||
internal static void EnsureStarted()
|
||||
{
|
||||
// The GUI source-links this reader and calls it directly, without the
|
||||
// host-platform resolution that otherwise guarantees Windows.
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
@@ -67,7 +69,7 @@ internal static class XInputReader
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetState(out PadState state)
|
||||
internal static bool TryGetState(out HostGamepadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
@@ -77,7 +79,7 @@ internal static class XInputReader
|
||||
return state.Connected;
|
||||
}
|
||||
|
||||
private static void SetState(in PadState state)
|
||||
private static void SetState(in HostGamepadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
@@ -204,40 +206,40 @@ internal static class XInputReader
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static PadState Translate(in XInputGamepad pad)
|
||||
private static HostGamepadState Translate(in XInputGamepad pad)
|
||||
{
|
||||
uint buttons = 0;
|
||||
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
|
||||
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
|
||||
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
|
||||
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
|
||||
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
|
||||
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
|
||||
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
|
||||
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
|
||||
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
|
||||
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
|
||||
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
|
||||
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
|
||||
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
|
||||
var buttons = HostGamepadButtons.None;
|
||||
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
|
||||
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
|
||||
buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
|
||||
buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
|
||||
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
|
||||
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
|
||||
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
|
||||
buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
|
||||
buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
|
||||
buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
|
||||
buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||
buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
|
||||
buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
|
||||
|
||||
return new PadState(
|
||||
return new HostGamepadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: AxisToByte(pad.ThumbLX),
|
||||
LeftY: AxisToByteInverted(pad.ThumbLY),
|
||||
RightX: AxisToByte(pad.ThumbRX),
|
||||
RightY: AxisToByteInverted(pad.ThumbRY),
|
||||
L2: pad.LeftTrigger,
|
||||
R2: pad.RightTrigger);
|
||||
LeftTrigger: pad.LeftTrigger,
|
||||
RightTrigger: pad.RightTrigger);
|
||||
}
|
||||
|
||||
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
||||
|
||||
// XInput Y grows upward, ORBIS pads report Y growing downward.
|
||||
// XInput Y grows upward, host pad conventions report Y growing downward.
|
||||
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
@@ -267,9 +269,9 @@ internal static class XInputReader
|
||||
}
|
||||
|
||||
// xinput1_4.dll ships with Windows 8 and later.
|
||||
[DllImport("xinput1_4.dll")]
|
||||
private static extern uint XInputGetState(uint userIndex, out XInputState state);
|
||||
[LibraryImport("xinput1_4.dll")]
|
||||
private static partial uint XInputGetState(uint userIndex, out XInputState state);
|
||||
|
||||
[DllImport("xinput1_4.dll")]
|
||||
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
||||
[LibraryImport("xinput1_4.dll")]
|
||||
private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Runs work on the real process main thread. GLFW windowing must live on
|
||||
/// that thread on macOS (AppKit) and Linux (X11's single event queue), so the
|
||||
/// CLI moves emulation onto a worker thread, parks the main thread in
|
||||
/// <see cref="Pump"/>, and the video presenter posts its window loop here. On
|
||||
/// Windows <see cref="IsAvailable"/> stays false and the window keeps its own
|
||||
/// thread.
|
||||
/// </summary>
|
||||
public static class HostMainThread
|
||||
{
|
||||
private static readonly BlockingCollection<Action> _work = new();
|
||||
private static Action? _shutdownRequestHandler;
|
||||
|
||||
public static bool IsAvailable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback invoked by <see cref="Shutdown"/> so a
|
||||
/// long-running posted work item (the presenter's window loop) can be
|
||||
/// asked to return to the pump.
|
||||
/// </summary>
|
||||
public static void SetShutdownRequestHandler(Action handler) =>
|
||||
_shutdownRequestHandler = handler;
|
||||
|
||||
/// <summary>Marks the pump as present. Call before guest code can run.</summary>
|
||||
public static void Enable() => IsAvailable = true;
|
||||
|
||||
public static void Post(Action work)
|
||||
{
|
||||
try
|
||||
{
|
||||
_work.Add(work);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Shutdown already requested; the process is exiting.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Services posted work on the calling (main) thread until
|
||||
/// <see cref="Shutdown"/> is called and the queue drains.
|
||||
/// </summary>
|
||||
public static void Pump()
|
||||
{
|
||||
foreach (var work in _work.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
work();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Main-thread work failed: {exception}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
IsAvailable = false;
|
||||
try
|
||||
{
|
||||
_shutdownRequestHandler?.Invoke();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] Main-thread shutdown handler failed: {exception.Message}");
|
||||
}
|
||||
|
||||
_work.CompleteAdding();
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
||||
public interface IGuestMemoryAllocator
|
||||
{
|
||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||
|
||||
bool TryFreeGuestMemory(ulong address);
|
||||
}
|
||||
|
||||
@@ -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,60 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||
</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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+333
-134
@@ -1,7 +1,10 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
@@ -156,13 +159,14 @@ public static class AgcExports
|
||||
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
||||
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
|
||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
|
||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
// Concurrent so the per-draw/per-dispatch hit path is lock-free (and no longer
|
||||
// shares _submitTraceGate with tracing).
|
||||
private static readonly ConcurrentDictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, ulong OutputLayout, uint OutputCount, uint Attributes),
|
||||
(IGuestCompiledShader Vertex, IGuestCompiledShader Pixel)> _graphicsShaderCache = new();
|
||||
private static readonly ConcurrentDictionary<
|
||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||
byte[]> _computeSpirvCache = new();
|
||||
IGuestCompiledShader> _computeShaderCache = new();
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
@@ -333,17 +337,20 @@ public static class AgcExports
|
||||
ulong ExportShaderAddress,
|
||||
ulong PixelShaderAddress,
|
||||
uint PrimitiveType,
|
||||
byte[] VertexSpirv,
|
||||
byte[] PixelSpirv,
|
||||
IGuestCompiledShader VertexShader,
|
||||
IGuestCompiledShader PixelShader,
|
||||
uint AttributeCount,
|
||||
uint VertexCount,
|
||||
uint InstanceCount,
|
||||
VulkanGuestIndexBuffer? IndexBuffer,
|
||||
GuestIndexBuffer? IndexBuffer,
|
||||
IReadOnlyList<TranslatedImageBinding> Textures,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs,
|
||||
IReadOnlyList<RenderTargetDescriptor> RenderTargets,
|
||||
VulkanGuestRenderState RenderState);
|
||||
// The seam-shaped view of RenderTargets, built once here so the per-frame
|
||||
// submit path does not rebuild it for every draw of a cached translation.
|
||||
IReadOnlyList<GuestRenderTarget> GuestTargets,
|
||||
GuestRenderState RenderState);
|
||||
|
||||
private sealed record TranslatedImageBinding(
|
||||
TextureDescriptor Descriptor,
|
||||
@@ -422,6 +429,8 @@ public static class AgcExports
|
||||
|
||||
private sealed record RegisterDefaultsAllocation(ulong Primary, ulong Internal);
|
||||
|
||||
// NID captured from shipped titles; 'sceAgcInit' is a working label that collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "23LRUSvYu1M",
|
||||
ExportName = "sceAgcInit",
|
||||
@@ -439,6 +448,7 @@ public static class AgcExports
|
||||
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2JtWUUiYBXs",
|
||||
@@ -618,6 +628,8 @@ public static class AgcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "HV4j+E0MBHE",
|
||||
ExportName = "sceAgcCreateInterpolantMapping",
|
||||
@@ -675,7 +687,10 @@ public static class AgcExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "V++UgBtQhn0",
|
||||
ExportName = "sceAgcGetDataPacketPayloadAddress",
|
||||
@@ -719,6 +734,7 @@ public static class AgcExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LtTouSCZjHM",
|
||||
@@ -2539,6 +2555,8 @@ public static class AgcExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "-KRzWekV120",
|
||||
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||
@@ -2553,6 +2571,7 @@ public static class AgcExports
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "h9z6+0hEydk",
|
||||
@@ -2566,6 +2585,8 @@ public static class AgcExports
|
||||
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 = "qj7QZpgr9Uw",
|
||||
ExportName = "sceAgcUnknownQj7QZpgr9Uw",
|
||||
@@ -2586,6 +2607,7 @@ public static class AgcExports
|
||||
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
|
||||
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
|
||||
@@ -2719,8 +2741,12 @@ public static class AgcExports
|
||||
ctx.TryReadUInt32(currentAddress + sizeof(uint), out var eventTypeRaw))
|
||||
{
|
||||
var eventType = eventTypeRaw & 0x3Fu;
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
|
||||
eventType,
|
||||
|
||||
// The guest registers AGC events with a full eventId, but the command buffer
|
||||
// only carries a 6-bit EVENT_TYPE. Those two values are not the same numbering
|
||||
// scheme, so exact ident matching never wakes anything. Trigger every graphics
|
||||
// event registration instead (workaround for issue #173).
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
eventType);
|
||||
if (tracePackets)
|
||||
@@ -2941,7 +2967,7 @@ public static class AgcExports
|
||||
handle,
|
||||
displayBufferIndex,
|
||||
out var cachedDisplayBuffer) &&
|
||||
VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
GuestGpu.Current.TrySubmitGuestImage(
|
||||
cachedDisplayBuffer.Address,
|
||||
cachedDisplayBuffer.Width,
|
||||
cachedDisplayBuffer.Height,
|
||||
@@ -2965,11 +2991,11 @@ public static class AgcExports
|
||||
displayBufferIndex,
|
||||
translatedDisplayBuffer,
|
||||
"draw-fallback");
|
||||
var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
|
||||
var textures = CreateGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
|
||||
var globalMemoryBuffers =
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDisplayBuffer.Width,
|
||||
@@ -2977,7 +3003,7 @@ public static class AgcExports
|
||||
translatedDraw.AttributeCount);
|
||||
TraceAgcShader(
|
||||
$"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " +
|
||||
$"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " +
|
||||
$"spirv={translatedDraw.PixelShader.Payload.Length} textures={textures.Count} " +
|
||||
$"global_buffers={globalMemoryBuffers.Count} " +
|
||||
$"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}");
|
||||
|
||||
@@ -3012,7 +3038,7 @@ public static class AgcExports
|
||||
displayBufferIndex,
|
||||
out var displayBuffer))
|
||||
{
|
||||
VulkanVideoPresenter.SubmitGuestDraw(
|
||||
GuestGpu.Current.SubmitGuestDraw(
|
||||
state.GuestDrawKind,
|
||||
displayBuffer.Width,
|
||||
displayBuffer.Height);
|
||||
@@ -3379,6 +3405,7 @@ public static class AgcExports
|
||||
$"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " +
|
||||
$"fmt={target.Format} tile={target.TileMode} " +
|
||||
$"size={target.Width}x{target.Height} vertices={vertexCount} " +
|
||||
$"prim=0x{primitiveType:X} indexed={indexed} " +
|
||||
$"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " +
|
||||
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}");
|
||||
}
|
||||
@@ -3407,27 +3434,23 @@ public static class AgcExports
|
||||
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
||||
if (firstTarget.Address != 0)
|
||||
{
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
var textures = CreateGuestDrawTextures(
|
||||
ctx,
|
||||
translatedDraw.Textures,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
var vertexBuffers =
|
||||
CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
TraceRectListVertices(translatedDraw, vertexBuffers);
|
||||
TraceGrassDrawVertices(translatedDraw, textures, vertexBuffers);
|
||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
translatedDraw.RenderTargets.Select(target =>
|
||||
new VulkanGuestRenderTarget(
|
||||
target.Address,
|
||||
target.Width,
|
||||
target.Height,
|
||||
target.Format,
|
||||
target.NumberType)).ToArray(),
|
||||
translatedDraw.VertexSpirv,
|
||||
translatedDraw.GuestTargets,
|
||||
translatedDraw.VertexShader,
|
||||
translatedDraw.VertexCount,
|
||||
translatedDraw.InstanceCount,
|
||||
translatedDraw.PrimitiveType,
|
||||
@@ -3441,14 +3464,14 @@ public static class AgcExports
|
||||
.FirstOrDefault(binding => binding.IsStorage);
|
||||
if (storageTarget is not null)
|
||||
{
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
var textures = CreateGuestDrawTextures(
|
||||
ctx,
|
||||
translatedDraw.Textures,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitStorageTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
@@ -3557,14 +3580,14 @@ public static class AgcExports
|
||||
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||
.OrderBy(target => target.Slot)
|
||||
.ToArray();
|
||||
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||
var renderTargetOutputKinds = new Gen5PixelOutputKind[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
var target = renderTargets[index];
|
||||
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
|
||||
if (!GuestGpu.Current.TryGetRenderTargetOutputKind(
|
||||
target.Format,
|
||||
target.NumberType,
|
||||
out renderTargetFormats[index]))
|
||||
out renderTargetOutputKinds[index]))
|
||||
{
|
||||
error =
|
||||
$"unsupported color target format={target.Format} number_type={target.NumberType}";
|
||||
@@ -3572,16 +3595,17 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
var pixelOutputs = renderTargets
|
||||
.Select((target, location) => new Gen5PixelOutputBinding(
|
||||
target.Slot,
|
||||
(uint)location,
|
||||
renderTargetFormats[location].OutputKind))
|
||||
.ToArray();
|
||||
var outputLayout = string.Join(
|
||||
';',
|
||||
pixelOutputs.Select(output =>
|
||||
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
|
||||
// Exact packed encoding of the output layout — guest slot (6 bits, CB targets are
|
||||
// 0-7) plus output kind (2 bits) per target, host locations being the sequential
|
||||
// byte positions. Replaces a per-draw LINQ + string build that allocated on every
|
||||
// draw, cache hit or not; the target count disambiguates trailing zero bytes.
|
||||
var outputLayout = 0UL;
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
outputLayout |= (ulong)(((renderTargets[index].Slot & 0x3Fu) << 2) |
|
||||
(uint)renderTargetOutputKinds[index]) << (index * 8);
|
||||
}
|
||||
|
||||
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||
@@ -3591,19 +3615,25 @@ public static class AgcExports
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
outputLayout,
|
||||
(uint)renderTargets.Length,
|
||||
attributeCount);
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
(byte[] Vertex, byte[] Pixel) compiled;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_graphicsSpirvCache.TryGetValue(shaderKey, out compiled);
|
||||
}
|
||||
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
|
||||
|
||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||
{
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
||||
for (var location = 0; location < renderTargets.Length; location++)
|
||||
{
|
||||
pixelOutputs[location] = new Gen5PixelOutputBinding(
|
||||
renderTargets[location].Slot,
|
||||
(uint)location,
|
||||
renderTargetOutputKinds[location]);
|
||||
}
|
||||
|
||||
if (!GuestGpu.Current.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
pixelOutputs,
|
||||
@@ -3613,7 +3643,7 @@ public static class AgcExports
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
!GuestGpu.Current.TryCompileVertexShader(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
@@ -3626,23 +3656,20 @@ public static class AgcExports
|
||||
return false;
|
||||
}
|
||||
|
||||
compiled = (vertexShader.Spirv, pixelShader.Spirv);
|
||||
DumpSpirv(
|
||||
compiled = (vertexShader!, pixelShader!);
|
||||
DumpCompiledShader(
|
||||
"vs",
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
compiled.Vertex,
|
||||
exportState.Program);
|
||||
DumpSpirv(
|
||||
DumpCompiledShader(
|
||||
"ps",
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
compiled.Pixel,
|
||||
pixelState.Program);
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_graphicsSpirvCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
|
||||
var imageBindings = pixelEvaluation.ImageBindings
|
||||
@@ -3679,6 +3706,17 @@ public static class AgcExports
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
guestTargets[index] = new GuestRenderTarget(
|
||||
renderTargets[index].Address,
|
||||
renderTargets[index].Width,
|
||||
renderTargets[index].Height,
|
||||
renderTargets[index].Format,
|
||||
renderTargets[index].NumberType);
|
||||
}
|
||||
|
||||
draw = new TranslatedGuestDraw(
|
||||
exportShaderAddress,
|
||||
pixelShaderAddress,
|
||||
@@ -3688,11 +3726,12 @@ public static class AgcExports
|
||||
attributeCount,
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
||||
textures,
|
||||
globalMemoryBindings,
|
||||
vertexInputs,
|
||||
renderTargets,
|
||||
guestTargets,
|
||||
ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||
textures,
|
||||
@@ -3713,8 +3752,8 @@ public static class AgcExports
|
||||
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||
/// attachment uses the same premultiplied blend pattern.
|
||||
/// </summary>
|
||||
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||
VulkanGuestRenderState renderState,
|
||||
private static GuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||
GuestRenderState renderState,
|
||||
IReadOnlyList<TranslatedImageBinding> textures,
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
||||
IReadOnlyList<uint> pixelUserData)
|
||||
@@ -3744,7 +3783,7 @@ public static class AgcExports
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
|
||||
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
|
||||
blend is
|
||||
{
|
||||
Enable: true,
|
||||
@@ -3753,7 +3792,7 @@ public static class AgcExports
|
||||
ColorFunc: 0,
|
||||
};
|
||||
|
||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||
private static GuestIndexBuffer? CreateGuestIndexBuffer(
|
||||
CpuContext ctx,
|
||||
SubmittedDcbState state,
|
||||
uint indexCount)
|
||||
@@ -3771,7 +3810,7 @@ public static class AgcExports
|
||||
var address = state.IndexBufferAddress + byteOffset;
|
||||
return (ctx.Memory.TryRead(address, data) ||
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data))
|
||||
? new VulkanGuestIndexBuffer(data, is32Bit)
|
||||
? new GuestIndexBuffer(data, is32Bit)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -3939,19 +3978,19 @@ public static class AgcExports
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static VulkanGuestRenderState CreateRenderState(
|
||||
private static GuestRenderState CreateRenderState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||
Gen5ShaderState pixelState)
|
||||
{
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return VulkanGuestRenderState.Default;
|
||||
return GuestRenderState.Default;
|
||||
}
|
||||
|
||||
var target = targets[0];
|
||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||
return new VulkanGuestRenderState(
|
||||
return new GuestRenderState(
|
||||
targets.Select(target =>
|
||||
{
|
||||
var blend = DecodeBlendState(registers, target.Slot);
|
||||
@@ -3964,7 +4003,7 @@ public static class AgcExports
|
||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||
}
|
||||
|
||||
private static VulkanGuestBlendState DecodeBlendState(
|
||||
private static GuestBlendState DecodeBlendState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint slot)
|
||||
{
|
||||
@@ -3975,7 +4014,7 @@ public static class AgcExports
|
||||
}
|
||||
|
||||
registers.TryGetValue(CbBlend0Control + slot, out var control);
|
||||
return new VulkanGuestBlendState(
|
||||
return new GuestBlendState(
|
||||
((control >> 30) & 1u) != 0,
|
||||
control & 0x1Fu,
|
||||
(control >> 8) & 0x1Fu,
|
||||
@@ -3987,14 +4026,14 @@ public static class AgcExports
|
||||
writeMask);
|
||||
}
|
||||
|
||||
private static VulkanGuestRect? DecodeScissor(
|
||||
private static GuestRect? DecodeScissor(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint targetWidth,
|
||||
uint targetHeight)
|
||||
{
|
||||
if (targetWidth == 0 || targetHeight == 0)
|
||||
{
|
||||
return new VulkanGuestRect(0, 0, 0, 0);
|
||||
return new GuestRect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
var left = 0;
|
||||
@@ -4059,22 +4098,22 @@ public static class AgcExports
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VulkanGuestRect(
|
||||
return new GuestRect(
|
||||
left,
|
||||
top,
|
||||
checked((uint)(right - left)),
|
||||
checked((uint)(bottom - top)));
|
||||
}
|
||||
|
||||
private static VulkanGuestViewport? DecodeViewport(
|
||||
private static GuestViewport? DecodeViewport(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint targetWidth,
|
||||
uint targetHeight,
|
||||
VulkanGuestRect? scissor)
|
||||
GuestRect? scissor)
|
||||
{
|
||||
if (targetWidth == 0 || targetHeight == 0)
|
||||
{
|
||||
return new VulkanGuestViewport(0, 0, 0, 0, 0, 1);
|
||||
return new GuestViewport(0, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
var minDepth = 0f;
|
||||
@@ -4100,7 +4139,7 @@ public static class AgcExports
|
||||
xScale > 0f &&
|
||||
yScale != 0f)
|
||||
{
|
||||
return new VulkanGuestViewport(
|
||||
return new GuestViewport(
|
||||
xOffset - xScale,
|
||||
yOffset - yScale,
|
||||
xScale * 2f,
|
||||
@@ -4113,10 +4152,10 @@ public static class AgcExports
|
||||
{
|
||||
return minDepth == 0f && maxDepth == 1f
|
||||
? null
|
||||
: new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
|
||||
: new GuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
|
||||
}
|
||||
|
||||
return new VulkanGuestViewport(
|
||||
return new GuestViewport(
|
||||
rect.X,
|
||||
rect.Y,
|
||||
rect.Width,
|
||||
@@ -4293,7 +4332,7 @@ public static class AgcExports
|
||||
var blend = draw.RenderState.Blend;
|
||||
TraceAgcShader(
|
||||
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
|
||||
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelSpirv.Length} " +
|
||||
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelShader.Payload.Length} " +
|
||||
$"primitive=0x{draw.PrimitiveType:X} " +
|
||||
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
|
||||
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
|
||||
@@ -4303,16 +4342,16 @@ public static class AgcExports
|
||||
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
|
||||
}
|
||||
|
||||
private static IReadOnlyList<VulkanGuestDrawTexture> CreateVulkanGuestDrawTextures(
|
||||
private static IReadOnlyList<GuestDrawTexture> CreateGuestDrawTextures(
|
||||
CpuContext ctx,
|
||||
IReadOnlyList<TranslatedImageBinding> bindings,
|
||||
out int fallbackTextureCount)
|
||||
{
|
||||
var textures = new List<VulkanGuestDrawTexture>(bindings.Count);
|
||||
var textures = new List<GuestDrawTexture>(bindings.Count);
|
||||
fallbackTextureCount = 0;
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (TryCreateVulkanGuestDrawTexture(
|
||||
if (TryCreateGuestDrawTexture(
|
||||
ctx,
|
||||
binding.Descriptor,
|
||||
binding.IsStorage,
|
||||
@@ -4331,13 +4370,13 @@ public static class AgcExports
|
||||
return textures;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<VulkanGuestMemoryBuffer> CreateVulkanGuestMemoryBuffers(
|
||||
private static IReadOnlyList<GuestMemoryBuffer> CreateGuestMemoryBuffers(
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
||||
{
|
||||
var buffers = new VulkanGuestMemoryBuffer[bindings.Count];
|
||||
var buffers = new GuestMemoryBuffer[bindings.Count];
|
||||
for (var index = 0; index < bindings.Count; index++)
|
||||
{
|
||||
buffers[index] = new VulkanGuestMemoryBuffer(
|
||||
buffers[index] = new GuestMemoryBuffer(
|
||||
bindings[index].BaseAddress,
|
||||
bindings[index].Data);
|
||||
}
|
||||
@@ -4345,14 +4384,14 @@ public static class AgcExports
|
||||
return buffers;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<VulkanGuestVertexBuffer> CreateVulkanGuestVertexBuffers(
|
||||
private static IReadOnlyList<GuestVertexBuffer> CreateGuestVertexBuffers(
|
||||
IReadOnlyList<Gen5VertexInputBinding> bindings)
|
||||
{
|
||||
var buffers = new VulkanGuestVertexBuffer[bindings.Count];
|
||||
var buffers = new GuestVertexBuffer[bindings.Count];
|
||||
for (var index = 0; index < bindings.Count; index++)
|
||||
{
|
||||
var binding = bindings[index];
|
||||
buffers[index] = new VulkanGuestVertexBuffer(
|
||||
buffers[index] = new GuestVertexBuffer(
|
||||
binding.Location,
|
||||
binding.ComponentCount,
|
||||
binding.DataFormat,
|
||||
@@ -4366,13 +4405,13 @@ public static class AgcExports
|
||||
return buffers;
|
||||
}
|
||||
|
||||
private static bool TryCreateVulkanGuestDrawTexture(
|
||||
private static bool TryCreateGuestDrawTexture(
|
||||
CpuContext ctx,
|
||||
TextureDescriptor descriptor,
|
||||
bool isStorage,
|
||||
uint mipLevel,
|
||||
IReadOnlyList<uint> samplerDescriptor,
|
||||
out VulkanGuestDrawTexture texture)
|
||||
out GuestDrawTexture texture)
|
||||
{
|
||||
texture = default!;
|
||||
if (descriptor.Type != Gen5TextureType2D ||
|
||||
@@ -4381,6 +4420,7 @@ public static class AgcExports
|
||||
descriptor.Width > 8192 ||
|
||||
descriptor.Height > 8192)
|
||||
{
|
||||
TraceTextureFallback(descriptor, "invalid-descriptor");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
@@ -4398,18 +4438,21 @@ public static class AgcExports
|
||||
sourceByteCount > MaxPresentedTextureBytes ||
|
||||
sourceByteCount > int.MaxValue)
|
||||
{
|
||||
TraceTextureFallback(
|
||||
descriptor,
|
||||
$"invalid-byte-count:{sourceByteCount}");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(
|
||||
GuestGpu.Current.IsGpuGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
{
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4423,7 +4466,7 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4433,14 +4476,17 @@ public static class AgcExports
|
||||
if (descriptor.Address != 0)
|
||||
{
|
||||
var storageSource = new byte[(int)sourceByteCount];
|
||||
if (ctx.Memory.TryRead(descriptor.Address, storageSource) &&
|
||||
if ((ctx.Memory.TryRead(descriptor.Address, storageSource) ||
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeapGpuAlias(
|
||||
descriptor.Address,
|
||||
storageSource)) &&
|
||||
storageSource.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
||||
{
|
||||
initialPixels = storageSource;
|
||||
}
|
||||
}
|
||||
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4454,13 +4500,19 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
var source = new byte[(int)sourceByteCount];
|
||||
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
||||
if (!ctx.Memory.TryRead(descriptor.Address, source) &&
|
||||
!KernelMemoryCompatExports.TryReadTrackedLibcHeapGpuAlias(
|
||||
descriptor.Address,
|
||||
source))
|
||||
{
|
||||
TraceTextureFallback(
|
||||
descriptor,
|
||||
$"guest-read-failed:{sourceByteCount}");
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
@@ -4486,9 +4538,10 @@ public static class AgcExports
|
||||
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
||||
$"dst=0x{descriptor.DstSelect:X3} " +
|
||||
$"bytes={source.Length} nonzero64={nonZero}");
|
||||
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
|
||||
|
||||
var rgba = source;
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
texture = new GuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4502,11 +4555,163 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
private static int _textureFallbackTraceCount;
|
||||
|
||||
private static void TraceTextureFallback(
|
||||
TextureDescriptor descriptor,
|
||||
string reason)
|
||||
{
|
||||
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||
if ((!string.Equals(mode, "1", StringComparison.Ordinal) &&
|
||||
!string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase)) ||
|
||||
Interlocked.Increment(ref _textureFallbackTraceCount) > 64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] agc.texture_fallback reason={reason} " +
|
||||
$"addr=0x{descriptor.Address:X16} type={descriptor.Type} " +
|
||||
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
||||
$"fmt={descriptor.Format} num={descriptor.NumberType} " +
|
||||
$"tile={descriptor.TileMode} mip={descriptor.MipLevels} " +
|
||||
$"dst=0x{descriptor.DstSelect:X3}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static int _grassTraceCount;
|
||||
|
||||
private static void TraceGrassDrawVertices(
|
||||
TranslatedGuestDraw draw,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
||||
{
|
||||
if (_grassTraceCount >= 6 ||
|
||||
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
|
||||
vertexBuffers.Count == 0 ||
|
||||
Interlocked.Increment(ref _grassTraceCount) > 6)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var text = new System.Text.StringBuilder();
|
||||
text.Append($"agc.grassdraw prim=0x{draw.PrimitiveType:X} verts={draw.VertexCount} ");
|
||||
text.Append($"indexed={draw.IndexBuffer is not null} buffers={vertexBuffers.Count}");
|
||||
foreach (var buffer in vertexBuffers)
|
||||
{
|
||||
text.Append(
|
||||
$"\n loc={buffer.Location} fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount} " +
|
||||
$"stride={buffer.Stride} offset={buffer.OffsetBytes} bytes={buffer.Data.Length}");
|
||||
var stride = Math.Max(buffer.Stride, 4u);
|
||||
var maxVerts = Math.Min(6, (int)((buffer.Data.Length - buffer.OffsetBytes) / stride));
|
||||
for (var vertex = 0; vertex < maxVerts; vertex++)
|
||||
{
|
||||
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
||||
var components = Math.Min(4, (int)((buffer.Data.Length - baseOffset) / 4));
|
||||
text.Append($"\n v{vertex}:");
|
||||
for (var c = 0; c < components; c++)
|
||||
{
|
||||
text.Append($" {BitConverter.ToSingle(buffer.Data, baseOffset + c * 4):0.#####}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TraceAgcShader(text.ToString());
|
||||
}
|
||||
|
||||
private static int _rectListTraceCount;
|
||||
|
||||
private static void TraceRectListVertices(
|
||||
TranslatedGuestDraw draw,
|
||||
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
||||
{
|
||||
if (draw.PrimitiveType != 0x11 ||
|
||||
draw.IndexBuffer is not null ||
|
||||
vertexBuffers.Count == 0 ||
|
||||
_rectListTraceCount >= 8 ||
|
||||
Interlocked.Increment(ref _rectListTraceCount) > 8)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = vertexBuffers[0];
|
||||
var stride = Math.Max(buffer.Stride, 4u);
|
||||
var text = new System.Text.StringBuilder();
|
||||
for (var vertex = 0; vertex < 3; vertex++)
|
||||
{
|
||||
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
||||
if (baseOffset + 16 > buffer.Data.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var x = BitConverter.ToSingle(buffer.Data, baseOffset);
|
||||
var y = BitConverter.ToSingle(buffer.Data, baseOffset + 4);
|
||||
var z = BitConverter.ToSingle(buffer.Data, baseOffset + 8);
|
||||
var w = BitConverter.ToSingle(buffer.Data, baseOffset + 12);
|
||||
text.Append($" v{vertex}=({x:0.###},{y:0.###},{z:0.###},{w:0.###})");
|
||||
}
|
||||
|
||||
TraceAgcShader(
|
||||
$"agc.rectlist verts={draw.VertexCount} stride={buffer.Stride} " +
|
||||
$"fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount}{text}");
|
||||
}
|
||||
|
||||
private static int _textureDumpCount;
|
||||
private static readonly ConcurrentDictionary<string, int> _textureDumpKeys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Writes raw sampled-texture bytes (as read from guest memory) when
|
||||
/// SHARPEMU_TEXTURE_DUMP_DIR is set, so upload-time content can be
|
||||
/// inspected offline. File name records size and effective pitch.
|
||||
/// </summary>
|
||||
private static void DumpTextureSourceIfRequested(
|
||||
in TextureDescriptor descriptor,
|
||||
uint sourcePitch,
|
||||
byte[] source)
|
||||
{
|
||||
var directory = Environment.GetEnvironmentVariable("SHARPEMU_TEXTURE_DUMP_DIR");
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = $"0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}";
|
||||
var occurrence = _textureDumpKeys.AddOrUpdate(key, 1, static (_, count) => count + 1);
|
||||
// First uses plus periodic later snapshots (the game reuses the same
|
||||
// allocation for successive full-screen images).
|
||||
if ((occurrence > 3 && occurrence % 500 >= 3) ||
|
||||
Interlocked.Increment(ref _textureDumpCount) > 200)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _textureDumpCount;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(
|
||||
directory,
|
||||
$"{index:D3}-0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}" +
|
||||
$"-p{sourcePitch}-f{descriptor.Format}-t{descriptor.TileMode}.bin");
|
||||
File.WriteAllBytes(path, source);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// A bad SHARPEMU_TEXTURE_DUMP_DIR (permissions, invalid path)
|
||||
// must not take the emulator down; the dump is a debug aid.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Texture dump failed: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
bool isStorage,
|
||||
uint format,
|
||||
uint numberType)
|
||||
@@ -4554,9 +4759,9 @@ public static class AgcExports
|
||||
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
|
||||
}
|
||||
|
||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
||||
descriptor.Count >= 4
|
||||
? new VulkanGuestSampler(
|
||||
? new GuestSampler(
|
||||
descriptor[0],
|
||||
descriptor[1],
|
||||
descriptor[2],
|
||||
@@ -4754,47 +4959,39 @@ public static class AgcExports
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ);
|
||||
byte[] computeSpirv;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!);
|
||||
}
|
||||
_computeShaderCache.TryGetValue(shaderKey, out var computeShader);
|
||||
|
||||
if (computeSpirv is null &&
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
if (computeShader is null &&
|
||||
GuestGpu.Current.TryCompileComputeShader(
|
||||
shaderState,
|
||||
evaluation,
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ,
|
||||
out var compiledCompute,
|
||||
out computeShader,
|
||||
out computeError))
|
||||
{
|
||||
computeSpirv = compiledCompute.Spirv;
|
||||
DumpSpirv(
|
||||
DumpCompiledShader(
|
||||
"cs",
|
||||
shaderAddress,
|
||||
shaderKey.Item2,
|
||||
computeSpirv,
|
||||
computeShader!,
|
||||
shaderState.Program);
|
||||
}
|
||||
|
||||
if (computeSpirv is not null)
|
||||
if (computeShader is not null)
|
||||
{
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_computeSpirvCache.TryAdd(shaderKey, computeSpirv);
|
||||
}
|
||||
_computeShaderCache.TryAdd(shaderKey, computeShader);
|
||||
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
var textures = CreateGuestDrawTextures(
|
||||
ctx,
|
||||
translatedBindings,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||
CreateGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitComputeDispatch(
|
||||
shaderAddress,
|
||||
computeSpirv,
|
||||
computeShader,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
dispatch.GroupCountX,
|
||||
@@ -4965,7 +5162,7 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
else if (source is { } cachedSourceTexture &&
|
||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||
GuestGpu.Current.TrySubmitGuestImageBlit(
|
||||
cachedSourceTexture.Address,
|
||||
cachedSourceTexture.Width,
|
||||
cachedSourceTexture.Height,
|
||||
@@ -5319,7 +5516,7 @@ public static class AgcExports
|
||||
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
|
||||
}
|
||||
|
||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
if (GuestGpu.Current.TryCompilePixelShader(
|
||||
pixelState,
|
||||
evaluation,
|
||||
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||
@@ -5328,7 +5525,7 @@ public static class AgcExports
|
||||
{
|
||||
TraceAgcShader(
|
||||
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
|
||||
$"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " +
|
||||
$"bytes={compiledPixel!.Payload.Length} bindings={evaluation.ImageBindings.Count} " +
|
||||
$"global_buffers={evaluation.GlobalMemoryBindings.Count}");
|
||||
}
|
||||
else
|
||||
@@ -6218,14 +6415,14 @@ public static class AgcExports
|
||||
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
|
||||
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
|
||||
|
||||
private static void DumpSpirv(
|
||||
private static void DumpCompiledShader(
|
||||
string stage,
|
||||
ulong shaderAddress,
|
||||
ulong stateFingerprint,
|
||||
byte[] spirv,
|
||||
IGuestCompiledShader shader,
|
||||
Gen5ShaderProgram program)
|
||||
{
|
||||
if (spirv.Length == 0 ||
|
||||
if (shader.Payload.Length == 0 ||
|
||||
!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"),
|
||||
"1",
|
||||
@@ -6237,7 +6434,9 @@ public static class AgcExports
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "shader-dumps");
|
||||
Directory.CreateDirectory(directory);
|
||||
var name = $"{shaderAddress:X16}-{stateFingerprint:X16}.{stage}";
|
||||
File.WriteAllBytes(Path.Combine(directory, $"{name}.spv"), spirv);
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(directory, $"{name}.{shader.PayloadFileExtension}"),
|
||||
shader.Payload);
|
||||
|
||||
var lines = new List<string>(program.Instructions.Count + 2)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
@@ -27,7 +28,7 @@ public static class AudioOutExports
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat,
|
||||
WinMmAudioPort? backend)
|
||||
IHostAudioStream? backend)
|
||||
{
|
||||
UserId = userId;
|
||||
Type = type;
|
||||
@@ -48,7 +49,7 @@ public static class AudioOutExports
|
||||
public int Channels { get; }
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public WinMmAudioPort? Backend { get; }
|
||||
public IHostAudioStream? Backend { get; }
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
|
||||
@@ -103,12 +104,13 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
WinMmAudioPort? backend = null;
|
||||
IHostAudioStream? backend = null;
|
||||
string backendName;
|
||||
try
|
||||
{
|
||||
backend = new WinMmAudioPort(frequency);
|
||||
backendName = "winmm";
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||
backendName = audio.BackendName;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -180,15 +182,31 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (port.Backend is null ||
|
||||
!port.Backend.Submit(
|
||||
source,
|
||||
port.BufferLength,
|
||||
port.Channels,
|
||||
port.BytesPerSample,
|
||||
port.IsFloat))
|
||||
if (port.Backend is null)
|
||||
{
|
||||
port.PaceSilence();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||
try
|
||||
{
|
||||
AudioPcmConversion.ConvertToStereoPcm16(
|
||||
source,
|
||||
output.AsSpan(0, outputLength),
|
||||
checked((int)port.BufferLength),
|
||||
port.Channels,
|
||||
port.BytesPerSample,
|
||||
port.IsFloat);
|
||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||
{
|
||||
port.PaceSilence();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(output);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
|
||||
/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
|
||||
/// device specifics live behind IHostAudioStream.
|
||||
/// </summary>
|
||||
internal static class AudioPcmConversion
|
||||
{
|
||||
/// <summary>Bytes per output frame: two 16-bit channels.</summary>
|
||||
public const int OutputFrameSize = 4;
|
||||
|
||||
public static void ConvertToStereoPcm16(
|
||||
ReadOnlySpan<byte> source,
|
||||
Span<byte> destination,
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
{
|
||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||
}
|
||||
}
|
||||
|
||||
private static short ReadSample(
|
||||
ReadOnlySpan<byte> frame,
|
||||
int channel,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||
if (!isFloat)
|
||||
{
|
||||
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||
}
|
||||
|
||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
internal sealed class WinMmAudioPort : IDisposable
|
||||
{
|
||||
private const uint WaveMapper = uint.MaxValue;
|
||||
private const uint CallbackEvent = 0x0005_0000;
|
||||
private const ushort WaveFormatPcm = 1;
|
||||
private const uint WaveHeaderDone = 0x0000_0001;
|
||||
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly AutoResetEvent _completion = new(false);
|
||||
private readonly Queue<NativeBuffer> _buffers = new();
|
||||
private IntPtr _device;
|
||||
private int _queuedPcmBytes;
|
||||
private bool _disposed;
|
||||
|
||||
public WinMmAudioPort(uint sampleRate)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
||||
}
|
||||
|
||||
var format = new WaveFormat
|
||||
{
|
||||
FormatTag = WaveFormatPcm,
|
||||
Channels = 2,
|
||||
SamplesPerSecond = sampleRate,
|
||||
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||
BlockAlign = 4,
|
||||
BitsPerSample = 16,
|
||||
ExtraSize = 0,
|
||||
};
|
||||
var result = WaveOutOpen(
|
||||
out _device,
|
||||
WaveMapper,
|
||||
ref format,
|
||||
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||
IntPtr.Zero,
|
||||
CallbackEvent);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
public bool Submit(
|
||||
ReadOnlySpan<byte> source,
|
||||
uint frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var outputLength = checked((int)frames * 4);
|
||||
ReapCompletedBuffers();
|
||||
while (_queuedPcmBytes != 0 &&
|
||||
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
||||
{
|
||||
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReapCompletedBuffers();
|
||||
}
|
||||
|
||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||
try
|
||||
{
|
||||
ConvertToStereoPcm16(
|
||||
source,
|
||||
output.AsSpan(0, outputLength),
|
||||
checked((int)frames),
|
||||
channels,
|
||||
bytesPerSample,
|
||||
isFloat);
|
||||
return QueueBuffer(output.AsSpan(0, outputLength));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (_device != IntPtr.Zero)
|
||||
{
|
||||
WaveOutReset(_device);
|
||||
while (_buffers.TryDequeue(out var buffer))
|
||||
{
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
|
||||
WaveOutClose(_device);
|
||||
_device = IntPtr.Zero;
|
||||
}
|
||||
|
||||
_completion.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||
var headerAddress = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||
}
|
||||
|
||||
var header = new WaveHeader
|
||||
{
|
||||
Data = dataAddress,
|
||||
BufferLength = checked((uint)data.Length),
|
||||
};
|
||||
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||
Marshal.StructureToPtr(header, headerAddress, false);
|
||||
|
||||
var result = WaveOutPrepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = WaveOutWrite(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
return false;
|
||||
}
|
||||
|
||||
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||
_queuedPcmBytes += data.Length;
|
||||
dataAddress = IntPtr.Zero;
|
||||
headerAddress = IntPtr.Zero;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (headerAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(headerAddress);
|
||||
}
|
||||
|
||||
if (dataAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(dataAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReapCompletedBuffers()
|
||||
{
|
||||
while (_buffers.TryPeek(out var buffer))
|
||||
{
|
||||
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||
if ((header.Flags & WaveHeaderDone) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_buffers.Dequeue();
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseBuffer(NativeBuffer buffer)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
buffer.Header,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
_queuedPcmBytes -= buffer.Length;
|
||||
Marshal.FreeHGlobal(buffer.Header);
|
||||
Marshal.FreeHGlobal(buffer.Data);
|
||||
}
|
||||
|
||||
private static void ConvertToStereoPcm16(
|
||||
ReadOnlySpan<byte> source,
|
||||
Span<byte> destination,
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
{
|
||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
||||
}
|
||||
}
|
||||
|
||||
private static short ReadSample(
|
||||
ReadOnlySpan<byte> frame,
|
||||
int channel,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||
if (!isFloat)
|
||||
{
|
||||
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||
}
|
||||
|
||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
|
||||
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct WaveFormat
|
||||
{
|
||||
public ushort FormatTag;
|
||||
public ushort Channels;
|
||||
public uint SamplesPerSecond;
|
||||
public uint AverageBytesPerSecond;
|
||||
public ushort BlockAlign;
|
||||
public ushort BitsPerSample;
|
||||
public ushort ExtraSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WaveHeader
|
||||
{
|
||||
public IntPtr Data;
|
||||
public uint BufferLength;
|
||||
public uint BytesRecorded;
|
||||
public nuint User;
|
||||
public uint Flags;
|
||||
public uint Loops;
|
||||
public IntPtr Next;
|
||||
public nuint Reserved;
|
||||
}
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||
private static extern uint WaveOutOpen(
|
||||
out IntPtr device,
|
||||
uint deviceId,
|
||||
ref WaveFormat format,
|
||||
IntPtr callback,
|
||||
IntPtr instance,
|
||||
uint flags);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||
private static extern uint WaveOutReset(IntPtr device);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||
private static extern uint WaveOutClose(IntPtr device);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,116 @@
|
||||
// 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 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);
|
||||
|
||||
/// <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);
|
||||
|
||||
internal sealed record GuestIndexBuffer(
|
||||
byte[] Data,
|
||||
bool Is32Bit);
|
||||
|
||||
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);
|
||||
|
||||
/// <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)
|
||||
{
|
||||
public static GuestRenderState Default { get; } = new(
|
||||
[GuestBlendState.Default],
|
||||
Scissor: null,
|
||||
Viewport: null);
|
||||
|
||||
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);
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// The guest-GPU backend seam: everything the AGC/VideoOut export layers need from a
|
||||
/// host renderer, expressed in guest-domain terms so Vulkan, Metal, and DX12 backends
|
||||
/// can each translate to their native API. Two rules keep it that way: no host-API
|
||||
/// value (formats, blend enums, barrier or pass concepts) may cross this interface,
|
||||
/// and submission is coarse-grained — synchronization is a backend-internal concern.
|
||||
///
|
||||
/// Shader compilation also lives behind the seam: the backend owns its codegen and
|
||||
/// returns opaque <see cref="IGuestCompiledShader"/> handles that only it can submit.
|
||||
/// </summary>
|
||||
internal interface IGuestGpuBackend
|
||||
{
|
||||
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
||||
void EnsureStarted(uint width, uint height);
|
||||
|
||||
// Shader compilation. The optional base/index parameters describe how a multi-stage
|
||||
// draw lays both stages' resources into one flat per-role slot space (buffers,
|
||||
// images, scalar-spill slots); each backend maps those slots to its own API binding
|
||||
// model. -1 keeps the emitter's single-stage defaults.
|
||||
|
||||
bool TryCompileVertexShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1);
|
||||
|
||||
bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1);
|
||||
|
||||
bool TryCompileComputeShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX,
|
||||
uint localSizeY,
|
||||
uint localSizeZ,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error);
|
||||
|
||||
void HideSplashScreen();
|
||||
|
||||
/// <summary>Presents one CPU-produced BGRA frame.</summary>
|
||||
void Submit(byte[] bgraFrame, uint width, uint height);
|
||||
|
||||
/// <summary>Presents a recognized fixed-function guest draw (see GuestDrawKind).</summary>
|
||||
void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height);
|
||||
|
||||
void SubmitTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null);
|
||||
|
||||
void SubmitOffscreenTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null);
|
||||
|
||||
void SubmitStorageTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
uint width,
|
||||
uint height);
|
||||
|
||||
void SubmitComputeDispatch(
|
||||
ulong shaderAddress,
|
||||
IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ);
|
||||
|
||||
bool TrySubmitGuestImage(
|
||||
ulong address,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitchInPixel);
|
||||
|
||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||
|
||||
/// <summary>Format/numberType are raw guest texture descriptor codes.</summary>
|
||||
bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType);
|
||||
|
||||
bool TrySubmitGuestImageBlit(
|
||||
ulong sourceAddress,
|
||||
uint sourceWidth,
|
||||
uint sourceHeight,
|
||||
uint sourceFormat,
|
||||
ulong destinationAddress,
|
||||
uint destinationWidth,
|
||||
uint destinationHeight,
|
||||
uint destinationFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the backend supports the guest render-target format, and how its pixel
|
||||
/// outputs are typed. Deliberately does not expose the backend's native format —
|
||||
/// the guest codes cross the seam and each backend maps them internally.
|
||||
/// </summary>
|
||||
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
/// <summary>The Vulkan backend's compiled shader: raw SPIR-V words.</summary>
|
||||
internal sealed record VulkanCompiledGuestShader(byte[] Spirv) : IGuestCompiledShader
|
||||
{
|
||||
public byte[] Payload => Spirv;
|
||||
|
||||
public string PayloadFileExtension => "spv";
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
/// <summary>
|
||||
/// Vulkan backend for the guest-GPU seam: SPIR-V codegen via
|
||||
/// SharpEmu.ShaderCompiler.Vulkan, rendering via a thin adapter over the existing
|
||||
/// VulkanVideoPresenter statics (folding the presenter into an instance type is
|
||||
/// follow-up work, not a seam concern).
|
||||
/// </summary>
|
||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||
{
|
||||
public bool TryCompileVertexShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
state,
|
||||
evaluation,
|
||||
out var compiled,
|
||||
out error,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
state,
|
||||
evaluation,
|
||||
outputs,
|
||||
out var compiled,
|
||||
out error,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCompileComputeShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX,
|
||||
uint localSizeY,
|
||||
uint localSizeZ,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ,
|
||||
out var compiled,
|
||||
out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void EnsureStarted(uint width, uint height) =>
|
||||
VulkanVideoPresenter.EnsureStarted(width, height);
|
||||
|
||||
public void HideSplashScreen() =>
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
|
||||
public void Submit(byte[] bgraFrame, uint width, uint height) =>
|
||||
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||
|
||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
|
||||
VulkanVideoPresenter.SubmitGuestDraw(drawKind, width, height);
|
||||
|
||||
public void SubmitTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null) =>
|
||||
VulkanVideoPresenter.SubmitTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
width,
|
||||
height,
|
||||
attributeCount,
|
||||
vertexShader is null ? null : Spirv(vertexShader),
|
||||
vertexCount,
|
||||
instanceCount,
|
||||
primitiveType,
|
||||
indexBuffer,
|
||||
vertexBuffers,
|
||||
renderState);
|
||||
|
||||
public void SubmitOffscreenTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null) =>
|
||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
attributeCount,
|
||||
targets,
|
||||
vertexShader is null ? null : Spirv(vertexShader),
|
||||
vertexCount,
|
||||
instanceCount,
|
||||
primitiveType,
|
||||
indexBuffer,
|
||||
vertexBuffers,
|
||||
renderState);
|
||||
|
||||
public void SubmitStorageTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
uint width,
|
||||
uint height) =>
|
||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
attributeCount,
|
||||
width,
|
||||
height);
|
||||
|
||||
public void SubmitComputeDispatch(
|
||||
ulong shaderAddress,
|
||||
IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ) =>
|
||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||
shaderAddress,
|
||||
Spirv(computeShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
groupCountX,
|
||||
groupCountY,
|
||||
groupCountZ);
|
||||
|
||||
public bool TrySubmitGuestImage(
|
||||
ulong address,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitchInPixel) =>
|
||||
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
|
||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(address, format, numberType);
|
||||
|
||||
public bool TrySubmitGuestImageBlit(
|
||||
ulong sourceAddress,
|
||||
uint sourceWidth,
|
||||
uint sourceHeight,
|
||||
uint sourceFormat,
|
||||
ulong destinationAddress,
|
||||
uint destinationWidth,
|
||||
uint destinationHeight,
|
||||
uint destinationFormat) =>
|
||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||
sourceAddress,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
sourceFormat,
|
||||
destinationAddress,
|
||||
destinationWidth,
|
||||
destinationHeight,
|
||||
destinationFormat);
|
||||
|
||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
||||
{
|
||||
if (VulkanVideoPresenter.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
|
||||
{
|
||||
outputKind = format.OutputKind;
|
||||
return true;
|
||||
}
|
||||
|
||||
outputKind = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
||||
shader is VulkanCompiledGuestShader vulkanShader
|
||||
? vulkanShader.Spirv
|
||||
: throw new InvalidOperationException(
|
||||
$"shader handle of type {shader.GetType().Name} was not compiled by the Vulkan backend");
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace SharpEmu.Libs;
|
||||
|
||||
public static class HostTimerResolution
|
||||
{
|
||||
private const uint TargetPeriodMilliseconds = 1;
|
||||
|
||||
private static int _requested;
|
||||
|
||||
public static void Request()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _requested, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Host timer resolution request rejected; " +
|
||||
"timed waits keep the default ~15.6 ms granularity.");
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
|
||||
private static extern uint TimeBeginPeriod(uint uPeriod);
|
||||
}
|
||||
@@ -34,9 +34,43 @@ public static class KernelEventFlagCompatExports
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class EventFlagWaiter
|
||||
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required EventFlagState State { get; init; }
|
||||
public required ulong Pattern { get; init; }
|
||||
public required uint WaitMode { get; init; }
|
||||
public required ulong ResultAddress { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Timed-wait completion state; unused when Timed is false.
|
||||
public ulong TimeoutAddress { get; init; }
|
||||
public long DeadlineTimestamp { get; init; }
|
||||
|
||||
public OrbisGen2Result? Result { get; set; }
|
||||
|
||||
// Untimed waits stash the prepared result here at wake and return it at resume.
|
||||
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public int Resume() => Timed
|
||||
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
||||
: (int)_blockedResult;
|
||||
|
||||
public bool TryWake()
|
||||
{
|
||||
if (Timed)
|
||||
{
|
||||
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
||||
}
|
||||
|
||||
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_blockedResult = preparedResult;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||
var timedWaiter = new EventFlagWaiter();
|
||||
var timedWaiter = new EventFlagWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
Pattern = pattern,
|
||||
WaitMode = waitMode,
|
||||
ResultAddress = resultAddress,
|
||||
Timed = true,
|
||||
TimeoutAddress = timeoutAddress,
|
||||
DeadlineTimestamp = deadline,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
timeoutAddress,
|
||||
deadline),
|
||||
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress),
|
||||
timedWaiter,
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
state.WaitingThreads++;
|
||||
@@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var managedThread = Environment.CurrentManagedThreadId;
|
||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
() => (int)blockedWaitResult,
|
||||
() =>
|
||||
new EventFlagWaiter
|
||||
{
|
||||
if (!TryPrepareBlockedWait(
|
||||
ctx,
|
||||
state,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
out var preparedResult))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
blockedWaitResult = preparedResult;
|
||||
return true;
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
Pattern = pattern,
|
||||
WaitMode = waitMode,
|
||||
ResultAddress = resultAddress,
|
||||
});
|
||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
private static readonly object _eventQueueGate = new();
|
||||
private static readonly HashSet<ulong> _eventQueues = new();
|
||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||
private static long _nextEventQueueHandle = 1;
|
||||
|
||||
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
|
||||
short Filter,
|
||||
ulong UserData);
|
||||
|
||||
// Grow-only ring buffer standing in for LinkedList<KernelQueuedEvent>, which
|
||||
// allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge
|
||||
// per registered queue. Mutated only under _eventQueueGate.
|
||||
private sealed class KernelEventDeque
|
||||
{
|
||||
private KernelQueuedEvent[] _items = new KernelQueuedEvent[4];
|
||||
private int _head;
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public KernelQueuedEvent this[int index]
|
||||
{
|
||||
get => _items[(_head + index) % _items.Length];
|
||||
set => _items[(_head + index) % _items.Length] = value;
|
||||
}
|
||||
|
||||
public void AddLast(in KernelQueuedEvent item)
|
||||
{
|
||||
if (Count == _items.Length)
|
||||
{
|
||||
var grown = new KernelQueuedEvent[_items.Length * 2];
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
grown[i] = this[i];
|
||||
}
|
||||
|
||||
_items = grown;
|
||||
_head = 0;
|
||||
}
|
||||
|
||||
_items[(_head + Count) % _items.Length] = item;
|
||||
Count++;
|
||||
}
|
||||
|
||||
public KernelQueuedEvent RemoveFirst()
|
||||
{
|
||||
var value = _items[_head];
|
||||
_head = (_head + 1) % _items.Length;
|
||||
Count--;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int FindIndex(ulong ident, short filter)
|
||||
{
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
var candidate = this[i];
|
||||
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong Handle { get; init; }
|
||||
public required ulong EventsAddress { get; init; }
|
||||
public required int EventCapacity { get; init; }
|
||||
public required ulong OutCountAddress { get; init; }
|
||||
|
||||
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
|
||||
|
||||
public bool TryWake() => HasPendingEvents(Handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D0OdFMjp46I",
|
||||
ExportName = "sceKernelCreateEqueue",
|
||||
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
_eventQueues.Add(handle);
|
||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = new KernelEventDeque();
|
||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||
}
|
||||
|
||||
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
|
||||
_registeredEvents.Remove(handle);
|
||||
}
|
||||
|
||||
_wakeKeys.TryRemove(handle, out _);
|
||||
|
||||
TraceEventQueue(ctx, "delete", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
GetEventQueueWakeKey(handle),
|
||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||
() => HasPendingEvents(handle)))
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
Handle = handle,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
}))
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -509,6 +589,66 @@ public static class KernelEventQueueCompatExports
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers every registered event on every queue that matches <paramref name="filter"/>
|
||||
/// regardless of the registration's <c>ident</c>. This is a workaround for PS5 AGC command
|
||||
/// buffers, where <c>IT_EVENT_WRITE</c> carries a hardware <c>EVENT_TYPE</c> that does not
|
||||
/// match the <c>eventId</c> the guest registered with <c>sceAgcDriverAddEqEvent</c>.
|
||||
/// See issue #173.
|
||||
/// </summary>
|
||||
public static int TriggerRegisteredEventsByFilter(
|
||||
short filter,
|
||||
ulong data)
|
||||
{
|
||||
List<ulong>? wakeHandles = null;
|
||||
var triggeredCount = 0;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
foreach (var (handle, registrations) in _registeredEvents)
|
||||
{
|
||||
foreach (var registration in registrations.Values)
|
||||
{
|
||||
if (registration.Filter != filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
QueueOrUpdateEvent(
|
||||
queue,
|
||||
new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
0,
|
||||
1,
|
||||
data,
|
||||
registration.UserData));
|
||||
(wakeHandles ??= new List<ulong>()).Add(handle);
|
||||
triggeredCount++;
|
||||
|
||||
// A single queue only needs to be woken once, even if multiple
|
||||
// registrations matched.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeHandles is not null)
|
||||
{
|
||||
foreach (var handle in wakeHandles)
|
||||
{
|
||||
WakeEventQueue(handle);
|
||||
}
|
||||
}
|
||||
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
private static bool TriggerRegisteredEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
@@ -527,7 +667,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -563,15 +703,15 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||
{
|
||||
events = new LinkedList<KernelQueuedEvent>();
|
||||
events = new KernelEventDeque();
|
||||
_pendingEvents[handle] = events;
|
||||
}
|
||||
|
||||
var count = 1UL;
|
||||
var pendingNode = FindPendingEvent(events, ident, filter);
|
||||
if (pendingNode is not null)
|
||||
var pendingIndex = events.FindIndex(ident, filter);
|
||||
if (pendingIndex >= 0)
|
||||
{
|
||||
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||
count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||
}
|
||||
|
||||
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||
@@ -584,9 +724,9 @@ public static class KernelEventQueueCompatExports
|
||||
eventData,
|
||||
userData);
|
||||
|
||||
if (pendingNode is not null)
|
||||
if (pendingIndex >= 0)
|
||||
{
|
||||
pendingNode.Value = triggeredEvent;
|
||||
events[pendingIndex] = triggeredEvent;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -631,40 +771,28 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
|
||||
private static void QueueOrUpdateEvent(
|
||||
LinkedList<KernelQueuedEvent> queue,
|
||||
KernelEventDeque queue,
|
||||
KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
||||
if (pendingNode is null)
|
||||
var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
|
||||
if (pendingIndex < 0)
|
||||
{
|
||||
queue.AddLast(queuedEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingNode.Value = queuedEvent with
|
||||
queue[pendingIndex] = queuedEvent with
|
||||
{
|
||||
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
||||
Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags),
|
||||
};
|
||||
}
|
||||
|
||||
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
||||
LinkedList<KernelQueuedEvent> queue,
|
||||
ulong ident,
|
||||
short filter)
|
||||
{
|
||||
for (var node = queue.First; node is not null; node = node.Next)
|
||||
{
|
||||
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||
|
||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||
$"sceKernelWaitEqueue:{handle:X16}";
|
||||
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||
|
||||
private static void WakeEventQueue(ulong handle)
|
||||
{
|
||||
@@ -678,7 +806,10 @@ public static class KernelEventQueueCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Engines wait on the vblank/flip equeue every frame, so the delivery buffer
|
||||
// (usually a single event) comes from the pool instead of a per-call array.
|
||||
KernelQueuedEvent[] events;
|
||||
int count;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||
@@ -686,24 +817,30 @@ public static class KernelEventQueueCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = Math.Min(eventCapacity, queue.Count);
|
||||
events = new KernelQueuedEvent[count];
|
||||
count = Math.Min(eventCapacity, queue.Count);
|
||||
events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
events[i] = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
events[i] = queue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < events.Length; i++)
|
||||
try
|
||||
{
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
return i;
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<KernelQueuedEvent>.Shared.Return(events);
|
||||
}
|
||||
|
||||
return events.Length;
|
||||
return count;
|
||||
}
|
||||
|
||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||
@@ -718,9 +855,12 @@ public static class KernelEventQueueCompatExports
|
||||
return ctx.Memory.TryWrite(address, eventBytes);
|
||||
}
|
||||
|
||||
private static readonly bool _logEqueue =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
||||
if (!_logEqueue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ public static class KernelMemoryCompatExports
|
||||
private const int MaxGuestStringLength = 4096;
|
||||
private const int WideCharSize = sizeof(ushort);
|
||||
private const int MemsetChunkSize = 16 * 1024;
|
||||
private const int MemcpyChunkSize = 256 * 1024;
|
||||
|
||||
// Shared all-zero scratch for chunked zero-fill loops; never written to.
|
||||
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||
private const int TlsModuleBlockSize = 0x10000;
|
||||
private const int O_WRONLY = 0x1;
|
||||
private const int O_RDWR = 0x2;
|
||||
@@ -121,6 +125,13 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
private static ulong _nextPhysicalAddress;
|
||||
private static ulong _nextVirtualAddress;
|
||||
// First guest virtual address handed out for direct/flexible mappings
|
||||
// when the game does not request one. 4GB is free on Windows, but on
|
||||
// POSIX hosts it belongs to the host image / runtime (the Mach-O image
|
||||
// base is 0x100000000 on macOS), so search from a guest-owned window
|
||||
// well clear of host mappings instead.
|
||||
private static readonly ulong DefaultMapSearchBase =
|
||||
OperatingSystem.IsWindows() ? 0x1_0000_0000UL : 0x20_0000_0000UL;
|
||||
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
||||
private static ulong _allocatedFlexibleBytes;
|
||||
private static ulong _threadAtexitCountCallback;
|
||||
@@ -237,7 +248,7 @@ public static class KernelMemoryCompatExports
|
||||
lock (_memoryGate)
|
||||
{
|
||||
var desiredAddress = AlignUp(
|
||||
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
||||
_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress,
|
||||
effectiveAlignment);
|
||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
||||
address == 0)
|
||||
@@ -255,11 +266,10 @@ public static class KernelMemoryCompatExports
|
||||
DirectStart: 0);
|
||||
}
|
||||
|
||||
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
||||
for (ulong offset = 0; offset < mappedLength;)
|
||||
{
|
||||
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
||||
var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -422,33 +432,50 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var chunk = new byte[MemsetChunkSize];
|
||||
Array.Fill(chunk, value);
|
||||
var remaining = length;
|
||||
var cursor = destination;
|
||||
while (remaining > 0)
|
||||
// Rent may hand back a larger array than requested; only the first chunkLength
|
||||
// bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
|
||||
var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
|
||||
var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||
if (value != 0)
|
||||
{
|
||||
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||
chunk.AsSpan(0, chunkLength).Fill(value);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var remaining = length;
|
||||
var cursor = destination;
|
||||
while (remaining > 0)
|
||||
{
|
||||
if (length <= 0x40)
|
||||
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||
{
|
||||
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||
if (recoveryIndex <= 8)
|
||||
if (length <= 0x40)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||
if (recoveryIndex <= 8)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
cursor += (ulong)take;
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (value != 0)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(chunk);
|
||||
}
|
||||
|
||||
cursor += (ulong)take;
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
@@ -1185,11 +1212,10 @@ public static class KernelMemoryCompatExports
|
||||
var rawCount = ctx[CpuRegister.Rdx];
|
||||
|
||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
||||
// exception. Reject anything above a sane bound before allocating.
|
||||
// upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
|
||||
// copy attempt from a guest-thread call context, which corrupted the CLR outright
|
||||
// ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code") instead of throwing a normal exception. Reject anything above a sane bound.
|
||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||
if (rawCount > maxSaneCount)
|
||||
{
|
||||
@@ -1198,15 +1224,52 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var count = (int)rawCount;
|
||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
if (rawCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Cap iterations at the requested chunk size, not chunk.Length: Rent may hand
|
||||
// back a larger array, and the copy granularity should not depend on pool
|
||||
// bucketing internals.
|
||||
var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize);
|
||||
var chunk = ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||
try
|
||||
{
|
||||
// memmove aliases this export, so overlapping ranges must survive the chunked
|
||||
// copy: when the destination starts inside the source range, copy high-to-low
|
||||
// so no source byte is overwritten before it has been read.
|
||||
var copyBackward = destination > source && destination - source < rawCount;
|
||||
var remaining = rawCount;
|
||||
ulong offset = copyBackward ? rawCount : 0;
|
||||
while (remaining > 0)
|
||||
{
|
||||
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||
if (copyBackward)
|
||||
{
|
||||
offset -= (ulong)take;
|
||||
}
|
||||
|
||||
var span = chunk.AsSpan(0, take);
|
||||
if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!copyBackward)
|
||||
{
|
||||
offset += (ulong)take;
|
||||
}
|
||||
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(chunk);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1753,6 +1816,8 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
// Not migratable to [GuestCString]: the local reader's TryReadCompat host-memory
|
||||
// fallback recovers paths in loader-mapped regions that ctx.Memory cannot see.
|
||||
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -3129,7 +3194,7 @@ public static class KernelMemoryCompatExports
|
||||
? requestedAddress
|
||||
: directMemoryStart != 0
|
||||
? AlignUp(directMemoryStart, effectiveAlignment)
|
||||
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, effectiveAlignment);
|
||||
: AlignUp(_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress, effectiveAlignment);
|
||||
|
||||
var reserved = false;
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
@@ -3235,7 +3300,7 @@ public static class KernelMemoryCompatExports
|
||||
var fixedMapping = (flags & 0x10UL) != 0;
|
||||
var desiredAddress = requestedAddress != 0
|
||||
? requestedAddress
|
||||
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, 0x1000UL);
|
||||
: AlignUp(_nextVirtualAddress == 0 ? DefaultMapSearchBase : _nextVirtualAddress, 0x1000UL);
|
||||
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
{
|
||||
@@ -6135,6 +6200,55 @@ public static class KernelMemoryCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryReadTrackedLibcHeapGpuAlias(
|
||||
ulong packedAddress,
|
||||
Span<byte> destination)
|
||||
{
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gen5 texture descriptors retain 46 bits of the byte address. Host
|
||||
// libc allocations can live at 0x7F... on Linux, so recover the full
|
||||
// tracked allocation address when the descriptor contains its packed
|
||||
// low-bit alias.
|
||||
const ulong textureAddressMask = (1UL << 46) - 1;
|
||||
var length = (ulong)destination.Length;
|
||||
ulong resolvedAddress = 0;
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
foreach (var (allocationAddress, allocation) in _libcAllocations)
|
||||
{
|
||||
var packedBase = allocationAddress & textureAddressMask;
|
||||
if (packedAddress < packedBase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offset = packedAddress - packedBase;
|
||||
var allocationSize = (ulong)allocation.Size;
|
||||
if (offset > allocationSize || length > allocationSize - offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = allocationAddress + offset;
|
||||
if (resolvedAddress != 0 && resolvedAddress != candidate)
|
||||
{
|
||||
// Do not guess if two live host allocations collide after
|
||||
// descriptor address packing.
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedAddress = candidate;
|
||||
}
|
||||
|
||||
return resolvedAddress != 0 &&
|
||||
TryReadHostMemory(resolvedAddress, destination);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
|
||||
@@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports
|
||||
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong MutexAddress { get; init; }
|
||||
public required ulong ResolvedAddress { get; init; }
|
||||
public required PthreadMutexState State { get; init; }
|
||||
public int Reserved;
|
||||
|
||||
public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||
|
||||
public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||
}
|
||||
|
||||
private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong CondAddress { get; init; }
|
||||
public required ulong MutexAddress { get; init; }
|
||||
public required PthreadCondState State { get; init; }
|
||||
public required ulong ObservedEpoch { get; init; }
|
||||
public required bool Timed { get; init; }
|
||||
public required int ReleasedRecursion { get; init; }
|
||||
public required bool PosixResult { get; init; }
|
||||
|
||||
public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult);
|
||||
|
||||
public bool TryWake() => State.SignalEpoch != ObservedEpoch;
|
||||
}
|
||||
|
||||
private sealed class PthreadCondState
|
||||
@@ -75,20 +99,6 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||
|
||||
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||
public int PendingSignals { get; set; }
|
||||
|
||||
public bool TryConsumePendingSignal()
|
||||
{
|
||||
if (PendingSignals <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingSignals--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||
@@ -629,6 +639,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
if (!InitializeMutexObject(ctx, handle, state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -641,6 +652,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
_mutexStates.TryRemove(handle, out _);
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -668,6 +680,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -724,7 +737,6 @@ public static class KernelPthreadCompatExports
|
||||
if (!acquired)
|
||||
{
|
||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
// Fibers retain the synchronous fallback to preserve switch state.
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||
@@ -736,8 +748,14 @@ public static class KernelPthreadCompatExports
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
state.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
new PthreadMutexWaiter
|
||||
{
|
||||
ThreadId = currentThreadId,
|
||||
Ctx = ctx,
|
||||
MutexAddress = mutexAddress,
|
||||
ResolvedAddress = resolvedAddress,
|
||||
State = state,
|
||||
}))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -895,6 +913,7 @@ public static class KernelPthreadCompatExports
|
||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -912,6 +931,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexAttrStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -935,6 +955,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1195,6 +1216,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = condAddress;
|
||||
state = raced;
|
||||
return true;
|
||||
@@ -1212,6 +1234,7 @@ public static class KernelPthreadCompatExports
|
||||
_condStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1231,7 +1254,22 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
Span<byte> initialData = stackalloc byte[size];
|
||||
initialData.Clear();
|
||||
return ctx.Memory.TryWrite(address, initialData);
|
||||
if (ctx.Memory.TryWrite(address, initialData))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
allocator.TryFreeGuestMemory(address);
|
||||
address = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (ctx.Memory is IGuestMemoryAllocator allocator)
|
||||
{
|
||||
allocator.TryFreeGuestMemory(address);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||
@@ -1269,6 +1307,7 @@ public static class KernelPthreadCompatExports
|
||||
_condStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -1293,6 +1332,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1321,9 +1361,8 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
state.Waiters++;
|
||||
var observedEpoch = state.SignalEpoch;
|
||||
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||
TracePthreadCond(
|
||||
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||
"wait-enter",
|
||||
condAddress,
|
||||
mutexAddress,
|
||||
state,
|
||||
@@ -1338,33 +1377,23 @@ public static class KernelPthreadCompatExports
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
if (consumedPendingSignal)
|
||||
{
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||
|
||||
// Relock outside SyncRoot to preserve lock ordering.
|
||||
Monitor.Exit(state.SyncRoot);
|
||||
try
|
||||
{
|
||||
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Balance the surrounding lock statement.
|
||||
Monitor.Enter(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||
state.WakeKey,
|
||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
||||
() => state.SignalEpoch != observedEpoch,
|
||||
new PthreadCondWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
CondAddress = condAddress,
|
||||
MutexAddress = mutexAddress,
|
||||
State = state,
|
||||
ObservedEpoch = observedEpoch,
|
||||
Timed = timed,
|
||||
ReleasedRecursion = releasedRecursion,
|
||||
PosixResult = posixResult,
|
||||
},
|
||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||
{
|
||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||
@@ -1479,10 +1508,6 @@ public static class KernelPthreadCompatExports
|
||||
Monitor.Pulse(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.PendingSignals++;
|
||||
}
|
||||
|
||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
@@ -1775,6 +1800,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = 0;
|
||||
state = null;
|
||||
return false;
|
||||
@@ -1784,12 +1810,14 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = mutexAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_mutexStates.TryGetValue(handle, out state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = handle;
|
||||
return true;
|
||||
}
|
||||
@@ -1803,6 +1831,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
_mutexStates.TryRemove(handle, out _);
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = 0;
|
||||
state = null;
|
||||
return false;
|
||||
|
||||
@@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required PthreadRwlockState Rwlock { get; init; }
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required bool Write { get; init; }
|
||||
|
||||
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||
}
|
||||
|
||||
private readonly record struct TlsKeyState(ulong Destructor);
|
||||
|
||||
private readonly record struct PthreadAttrState(
|
||||
@@ -1359,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
ctx,
|
||||
"pthread_rwlock_wrlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||
{
|
||||
transferredToScheduler = true;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -1396,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
ctx,
|
||||
"pthread_rwlock_rdlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1717,41 +1717,59 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
private static ulong ResolveKernelTscFrequency()
|
||||
{
|
||||
var (frequencyHz, source) = SelectKernelTscFrequency(
|
||||
_rdtscReader is not null,
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TSC_FREQ_HZ"),
|
||||
TryCalibrateHostTscFrequency,
|
||||
TryResolveCpuidTscFrequency,
|
||||
Stopwatch.Frequency);
|
||||
TraceKernelTscFrequency(source, frequencyHz);
|
||||
return frequencyHz;
|
||||
}
|
||||
|
||||
internal delegate bool TryGetFrequency(out ulong frequencyHz);
|
||||
|
||||
// sceKernelReadTsc only returns the CPU's RDTSC when the host RDTSC reader is available
|
||||
// (currently 64-bit Windows); otherwise it falls back to the QPC-based Stopwatch. The
|
||||
// calibrated and CPUID frequencies both describe RDTSC, so reporting either while ReadTsc is
|
||||
// actually returning Stopwatch ticks makes sceKernelGetTscFrequency disagree with the counter,
|
||||
// and a guest computing elapsed = readTscDelta / frequency gets the wrong time on Linux and
|
||||
// macOS. Gate those on rdtscAvailable; otherwise report Stopwatch's own frequency so the pair
|
||||
// stays consistent.
|
||||
internal static (ulong FrequencyHz, string Source) SelectKernelTscFrequency(
|
||||
bool rdtscAvailable,
|
||||
string? overrideHzText,
|
||||
TryGetFrequency tryCalibrate,
|
||||
TryGetFrequency tryResolveCpuid,
|
||||
long stopwatchFrequency)
|
||||
{
|
||||
const ulong minSane = 1_000_000UL;
|
||||
|
||||
var overrideHzText = Environment.GetEnvironmentVariable("SHARPEMU_TSC_FREQ_HZ");
|
||||
if (!string.IsNullOrWhiteSpace(overrideHzText) &&
|
||||
ulong.TryParse(overrideHzText, out var overrideHz) &&
|
||||
overrideHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("env", overrideHz);
|
||||
return overrideHz;
|
||||
return (overrideHz, "env");
|
||||
}
|
||||
|
||||
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||
if (rdtscAvailable)
|
||||
{
|
||||
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
|
||||
return calibratedHz;
|
||||
if (tryCalibrate(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||
{
|
||||
return (calibratedHz, "calibrated-rdtsc");
|
||||
}
|
||||
|
||||
if (tryResolveCpuid(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
return (cpuidHz, "cpuid");
|
||||
}
|
||||
}
|
||||
|
||||
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("cpuid", cpuidHz);
|
||||
return cpuidHz;
|
||||
}
|
||||
|
||||
var hostQpc = Stopwatch.Frequency > 0
|
||||
? unchecked((ulong)Stopwatch.Frequency)
|
||||
var hostQpc = stopwatchFrequency > 0
|
||||
? unchecked((ulong)stopwatchFrequency)
|
||||
: DefaultKernelTscFrequency;
|
||||
if (hostQpc >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("qpc", hostQpc);
|
||||
return hostQpc;
|
||||
}
|
||||
|
||||
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
|
||||
return DefaultKernelTscFrequency;
|
||||
return hostQpc >= minSane ? (hostQpc, "qpc") : (DefaultKernelTscFrequency, "default");
|
||||
}
|
||||
|
||||
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
|
||||
|
||||
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
|
||||
public required string WakeKey { get; init; }
|
||||
public required int InitialCount { get; init; }
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class SemaphoreWaiter
|
||||
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required KernelSemaphoreState Semaphore { get; init; }
|
||||
public required int NeedCount { get; init; }
|
||||
public required int CancelEpochAtBlock { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Timed-wait completion state; unused when Timed is false.
|
||||
public CpuContext? Ctx { get; init; }
|
||||
public ulong TimeoutAddress { get; init; }
|
||||
public long DeadlineTimestamp { get; init; }
|
||||
|
||||
// Written and read only under the owning semaphore's Gate.
|
||||
public int? Result { get; set; }
|
||||
|
||||
public int Resume() => Timed
|
||||
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
||||
: CompleteBlockedSemaWait(Semaphore, this);
|
||||
|
||||
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
||||
}
|
||||
|
||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
|
||||
var state = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
WakeKey = GetSemaphoreWakeKey(handle),
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
|
||||
state.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
|
||||
if (timeoutMicros == 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
pollTimedOut = true;
|
||||
}
|
||||
else
|
||||
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||
var timedWaiter = new SemaphoreWaiter
|
||||
{
|
||||
Semaphore = semaphore,
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
Timed = true,
|
||||
Ctx = ctx,
|
||||
TimeoutAddress = timeoutAddress,
|
||||
DeadlineTimestamp = deadline,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
||||
semaphore.WakeKey,
|
||||
timedWaiter,
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||
// immediate-timeout poll the callers already tolerate.
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
pollTimedOut = true;
|
||||
}
|
||||
}
|
||||
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
var waiter = new SemaphoreWaiter
|
||||
{
|
||||
Semaphore = semaphore,
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
semaphore.WakeKey,
|
||||
waiter))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -220,11 +259,8 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelPollSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelPollSema(CpuContext ctx)
|
||||
public static int KernelPollSema(CpuContext ctx, uint handle, int needCount)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
@@ -239,12 +275,18 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -254,11 +296,8 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelSignalSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSignalSema(CpuContext ctx)
|
||||
public static int KernelSignalSema(CpuContext ctx, uint handle, int signalCount)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
@@ -277,14 +316,17 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
}
|
||||
|
||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||
// waiter can proceed.
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -293,12 +335,8 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelCancelSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCancelSema(CpuContext ctx)
|
||||
public static int KernelCancelSema(CpuContext ctx, uint handle, int setCount, ulong waitingThreadsAddress)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
@@ -322,10 +360,13 @@ public static class KernelSemaphoreCompatExports
|
||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||
// and silently absorb the increment of a waiter that parks between this
|
||||
// gate release and the wake-all below.
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -349,8 +390,11 @@ public static class KernelSemaphoreCompatExports
|
||||
semaphore.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -376,7 +420,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -384,7 +431,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -393,7 +443,10 @@ public static class KernelSemaphoreCompatExports
|
||||
semaphore.Count -= waiter.NeedCount;
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -434,7 +487,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
}
|
||||
|
||||
result = waiter.Result!.Value;
|
||||
@@ -456,11 +512,13 @@ public static class KernelSemaphoreCompatExports
|
||||
return result;
|
||||
}
|
||||
|
||||
// Call sites must check this before building the interpolated message; the trace
|
||||
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||
private static readonly bool _traceSema =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,10 @@ internal static class KernelVirtualRangeAllocator
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw");
|
||||
// Expected when a fixed-address request cannot be satisfied on
|
||||
// this host; the caller falls back or reports the failure.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] {traceName}: no host mapping at 0x{desiredAddress:X16} len=0x{length:X}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class NpManagerExports
|
||||
{
|
||||
private const int NpTitleIdSize = 16;
|
||||
private const int NpTitleSecretSize = 128;
|
||||
private const int NpReachabilityStateUnavailable = 0;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3Zl8BePTh9Y",
|
||||
@@ -75,8 +76,15 @@ public static class NpManagerExports
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard and gamepad state sampled from the presenter's window, feeding
|
||||
/// the POSIX host input seam (macOS/Linux have no user32/XInput/raw-HID
|
||||
/// readers). The presenter attaches the window's input context once the
|
||||
/// window exists; input events arrive on the window thread and pad reads
|
||||
/// happen on guest threads, so all state is guarded.
|
||||
/// </summary>
|
||||
public static class HostWindowInput
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static readonly HashSet<Key> Pressed = new();
|
||||
private static volatile bool _connected;
|
||||
|
||||
// Latest window-gamepad snapshot in the host seam's conventions.
|
||||
private static bool _gamepadConnected;
|
||||
private static string? _gamepadName;
|
||||
private static HostGamepadButtons _gamepadButtons;
|
||||
private static byte _gamepadLeftX = 128;
|
||||
private static byte _gamepadLeftY = 128;
|
||||
private static byte _gamepadRightX = 128;
|
||||
private static byte _gamepadRightY = 128;
|
||||
private static byte _gamepadL2;
|
||||
private static byte _gamepadR2;
|
||||
|
||||
/// <summary>True once a window keyboard is delivering events.</summary>
|
||||
public static bool IsConnected => _connected;
|
||||
|
||||
public static void Attach(IInputContext input)
|
||||
{
|
||||
foreach (var keyboard in input.Keyboards)
|
||||
{
|
||||
keyboard.KeyDown += (_, key, _) =>
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Add(key);
|
||||
}
|
||||
};
|
||||
keyboard.KeyUp += (_, key, _) =>
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Remove(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (input.Keyboards.Count > 0)
|
||||
{
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
foreach (var gamepad in input.Gamepads)
|
||||
{
|
||||
AttachGamepad(gamepad);
|
||||
}
|
||||
|
||||
input.ConnectionChanged += (device, connected) =>
|
||||
{
|
||||
if (device is not IGamepad gamepad)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (connected)
|
||||
{
|
||||
AttachGamepad(gamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = false;
|
||||
_gamepadName = null;
|
||||
_gamepadButtons = HostGamepadButtons.None;
|
||||
_gamepadLeftX = 128;
|
||||
_gamepadLeftY = 128;
|
||||
_gamepadRightX = 128;
|
||||
_gamepadRightY = 128;
|
||||
_gamepadL2 = 0;
|
||||
_gamepadR2 = 0;
|
||||
}
|
||||
};
|
||||
|
||||
PosixHostInput.SetSource(new WindowInputSource());
|
||||
}
|
||||
|
||||
public static bool IsKeyDown(Key key)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Pressed.Contains(key);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WindowInputSource : IPosixWindowInputSource
|
||||
{
|
||||
public bool HasKeyboardFocus => _connected;
|
||||
|
||||
public bool IsKeyDown(int virtualKey)
|
||||
{
|
||||
return TryMapVirtualKey(virtualKey, out var key) && HostWindowInput.IsKeyDown(key);
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (!_gamepadConnected || destination.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
destination[0] = new HostGamepadState(
|
||||
Connected: true,
|
||||
Buttons: _gamepadButtons,
|
||||
LeftX: _gamepadLeftX,
|
||||
LeftY: _gamepadLeftY,
|
||||
RightX: _gamepadRightX,
|
||||
RightY: _gamepadRightY,
|
||||
LeftTrigger: _gamepadL2,
|
||||
RightTrigger: _gamepadR2);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public string? DescribeConnectedGamepad()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _gamepadConnected ? _gamepadName ?? "GLFW gamepad" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMapVirtualKey(int vk, out Key key)
|
||||
{
|
||||
key = vk switch
|
||||
{
|
||||
0x08 => Key.Backspace,
|
||||
0x09 => Key.Tab,
|
||||
0x0D => Key.Enter,
|
||||
0x1B => Key.Escape,
|
||||
0x25 => Key.Left,
|
||||
0x26 => Key.Up,
|
||||
0x27 => Key.Right,
|
||||
0x28 => Key.Down,
|
||||
>= 0x41 and <= 0x5A => Key.A + (vk - 0x41),
|
||||
_ => Key.Unknown,
|
||||
};
|
||||
return key != Key.Unknown;
|
||||
}
|
||||
|
||||
private static void AttachGamepad(IGamepad gamepad)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = true;
|
||||
_gamepadName = gamepad.Name;
|
||||
}
|
||||
|
||||
gamepad.ButtonDown += (_, button) =>
|
||||
{
|
||||
var bit = MapButton(button.Name);
|
||||
if (bit == HostGamepadButtons.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadButtons |= bit;
|
||||
}
|
||||
};
|
||||
gamepad.ButtonUp += (_, button) =>
|
||||
{
|
||||
var bit = MapButton(button.Name);
|
||||
if (bit == HostGamepadButtons.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadButtons &= ~bit;
|
||||
}
|
||||
};
|
||||
gamepad.ThumbstickMoved += (_, thumbstick) =>
|
||||
{
|
||||
// Silk's GLFW backend reports sticks -1..1 with +Y pointing down,
|
||||
// matching the seam's 0..255 down-growing convention after biasing.
|
||||
var x = ToStickByte(thumbstick.X);
|
||||
var y = ToStickByte(thumbstick.Y);
|
||||
lock (Gate)
|
||||
{
|
||||
if (thumbstick.Index == 0)
|
||||
{
|
||||
_gamepadLeftX = x;
|
||||
_gamepadLeftY = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadRightX = x;
|
||||
_gamepadRightY = y;
|
||||
}
|
||||
}
|
||||
};
|
||||
gamepad.TriggerMoved += (_, trigger) =>
|
||||
{
|
||||
// GLFW gamepad triggers rest at -1 and saturate at +1.
|
||||
var value = (byte)Math.Clamp((int)((trigger.Position + 1.0f) * 0.5f * 255.0f), 0, 255);
|
||||
lock (Gate)
|
||||
{
|
||||
if (trigger.Index == 0)
|
||||
{
|
||||
_gamepadL2 = value;
|
||||
if (value > 64)
|
||||
{
|
||||
_gamepadButtons |= HostGamepadButtons.L2;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadButtons &= ~HostGamepadButtons.L2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadR2 = value;
|
||||
if (value > 64)
|
||||
{
|
||||
_gamepadButtons |= HostGamepadButtons.R2;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadButtons &= ~HostGamepadButtons.R2;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static byte ToStickByte(float value)
|
||||
{
|
||||
return (byte)Math.Clamp((int)(128.0f + value * 127.0f), 0, 255);
|
||||
}
|
||||
|
||||
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
||||
{
|
||||
// GLFW reports the Xbox layout: A=Cross, B=Circle, X=Square, Y=Triangle.
|
||||
ButtonName.A => HostGamepadButtons.Cross,
|
||||
ButtonName.B => HostGamepadButtons.Circle,
|
||||
ButtonName.X => HostGamepadButtons.Square,
|
||||
ButtonName.Y => HostGamepadButtons.Triangle,
|
||||
ButtonName.LeftBumper => HostGamepadButtons.L1,
|
||||
ButtonName.RightBumper => HostGamepadButtons.R1,
|
||||
ButtonName.Back => HostGamepadButtons.TouchPad,
|
||||
ButtonName.Start => HostGamepadButtons.Options,
|
||||
ButtonName.LeftStick => HostGamepadButtons.L3,
|
||||
ButtonName.RightStick => HostGamepadButtons.R3,
|
||||
ButtonName.DPadUp => HostGamepadButtons.Up,
|
||||
ButtonName.DPadRight => HostGamepadButtons.Right,
|
||||
ButtonName.DPadDown => HostGamepadButtons.Down,
|
||||
ButtonName.DPadLeft => HostGamepadButtons.Left,
|
||||
_ => HostGamepadButtons.None,
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
@@ -38,8 +38,7 @@ public static class PadExports
|
||||
public static int PadInit(CpuContext ctx)
|
||||
{
|
||||
_initialized = true;
|
||||
DualSenseReader.EnsureStarted();
|
||||
XInputReader.EnsureStarted();
|
||||
HostPlatform.Current.Input.EnsureStarted();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
@@ -81,15 +80,13 @@ public static class PadExports
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
DualSenseReader.EnsureStarted();
|
||||
XInputReader.EnsureStarted();
|
||||
var input = HostPlatform.Current.Input;
|
||||
input.EnsureStarted();
|
||||
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||
: XInputReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
|
||||
? $"[LOADER][INFO] Controls: {gamepadName} connected (keyboard fallback also active)."
|
||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
@@ -282,7 +279,7 @@ public static class PadExports
|
||||
}
|
||||
|
||||
var triggerMask = parameter[0];
|
||||
XInputReader.SetTriggerRumble(
|
||||
HostPlatform.Current.Input.SetTriggerRumble(
|
||||
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
@@ -326,8 +323,7 @@ public static class PadExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
||||
XInputReader.SetRumble(parameter[0], parameter[1]);
|
||||
HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
@@ -357,7 +353,7 @@ public static class PadExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
|
||||
HostPlatform.Current.Input.SetLightbar(color[0], color[1], color[2]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
@@ -374,7 +370,7 @@ public static class PadExports
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
DualSenseReader.ResetLightbar();
|
||||
HostPlatform.Current.Input.ResetLightbar();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
@@ -420,37 +416,35 @@ public static class PadExports
|
||||
return _cachedInputState;
|
||||
}
|
||||
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
var input = HostPlatform.Current.Input;
|
||||
var acceptsKeyboardInput = input.IsHostWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
|
||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
|
||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
|
||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
|
||||
var l2 = acceptsKeyboardInput && input.IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
var r2 = acceptsKeyboardInput && input.IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
|
||||
if (DualSenseReader.TryGetState(out var pad))
|
||||
Span<HostGamepadState> gamepads = stackalloc HostGamepadState[2];
|
||||
var gamepadCount = input.GetGamepadStates(gamepads);
|
||||
for (var index = 0; index < gamepadCount; index++)
|
||||
{
|
||||
buttons |= pad.Buttons;
|
||||
var pad = gamepads[index];
|
||||
buttons |= ToOrbisButtons(pad.Buttons);
|
||||
// The controller stick wins whenever it is deflected past a
|
||||
// small deadzone; otherwise any keyboard value stays.
|
||||
leftX = MergeAxis(pad.LeftX, leftX);
|
||||
leftY = MergeAxis(pad.LeftY, leftY);
|
||||
rightX = MergeAxis(pad.RightX, rightX);
|
||||
rightY = MergeAxis(pad.RightY, rightY);
|
||||
l2 = Math.Max(l2, pad.L2);
|
||||
r2 = Math.Max(r2, pad.R2);
|
||||
l2 = Math.Max(l2, pad.LeftTrigger);
|
||||
r2 = Math.Max(r2, pad.RightTrigger);
|
||||
}
|
||||
|
||||
if (XInputReader.TryGetState(out var xpad))
|
||||
if (IsAutoCrossActive())
|
||||
{
|
||||
buttons |= xpad.Buttons;
|
||||
leftX = MergeAxis(xpad.LeftX, leftX);
|
||||
leftY = MergeAxis(xpad.LeftY, leftY);
|
||||
rightX = MergeAxis(xpad.RightX, rightX);
|
||||
rightY = MergeAxis(xpad.RightY, rightY);
|
||||
l2 = Math.Max(l2, xpad.L2);
|
||||
r2 = Math.Max(r2, xpad.R2);
|
||||
buttons |= 0x4000;
|
||||
}
|
||||
|
||||
_cachedInputState = new PadState(
|
||||
@@ -466,50 +460,94 @@ public static class PadExports
|
||||
return _cachedInputState;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
private static readonly long PadStartTimestamp = Stopwatch.GetTimestamp();
|
||||
private static readonly double[] AutoCrossTimes = ParseAutoCrossTimes();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
|
||||
private static bool IsKeyDown(int vk) =>
|
||||
(GetAsyncKeyState(vk) & 0x8000) != 0;
|
||||
|
||||
private static bool IsEmulatorWindowFocused()
|
||||
private static double[] ParseAutoCrossTimes()
|
||||
{
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == 0)
|
||||
// SHARPEMU_AUTO_CROSS="40,52,64": presses Cross for 0.4s at each
|
||||
// second offset from process start. Debug aid for unattended runs.
|
||||
var raw = Environment.GetEnvironmentVariable("SHARPEMU_AUTO_CROSS");
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var values = new List<double>();
|
||||
foreach (var token in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (double.TryParse(token, System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
return values.ToArray();
|
||||
}
|
||||
|
||||
private static bool IsAutoCrossActive()
|
||||
{
|
||||
var times = AutoCrossTimes;
|
||||
if (times.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
||||
return processId == (uint)Environment.ProcessId;
|
||||
var elapsed = (Stopwatch.GetTimestamp() - PadStartTimestamp) / (double)Stopwatch.Frequency;
|
||||
foreach (var time in times)
|
||||
{
|
||||
if (elapsed >= time && elapsed < time + 0.4)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static uint ReadKeyboardButtons()
|
||||
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
|
||||
private static uint ToOrbisButtons(HostGamepadButtons buttons)
|
||||
{
|
||||
uint result = 0;
|
||||
if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
|
||||
if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
|
||||
if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
|
||||
if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
|
||||
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
|
||||
if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
|
||||
if ((buttons & HostGamepadButtons.Square) != 0) result |= OrbisPadButton.Square;
|
||||
if ((buttons & HostGamepadButtons.Triangle) != 0) result |= OrbisPadButton.Triangle;
|
||||
if ((buttons & HostGamepadButtons.L1) != 0) result |= OrbisPadButton.L1;
|
||||
if ((buttons & HostGamepadButtons.R1) != 0) result |= OrbisPadButton.R1;
|
||||
if ((buttons & HostGamepadButtons.L2) != 0) result |= OrbisPadButton.L2;
|
||||
if ((buttons & HostGamepadButtons.R2) != 0) result |= OrbisPadButton.R2;
|
||||
if ((buttons & HostGamepadButtons.L3) != 0) result |= OrbisPadButton.L3;
|
||||
if ((buttons & HostGamepadButtons.R3) != 0) result |= OrbisPadButton.R3;
|
||||
if ((buttons & HostGamepadButtons.Options) != 0) result |= OrbisPadButton.Options;
|
||||
if ((buttons & HostGamepadButtons.TouchPad) != 0) result |= OrbisPadButton.TouchPad;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint ReadKeyboardButtons(IHostInput input)
|
||||
{
|
||||
uint buttons = 0;
|
||||
// D-pad
|
||||
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
|
||||
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
|
||||
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
|
||||
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
|
||||
if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
|
||||
if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
|
||||
if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
|
||||
if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
|
||||
// Face buttons
|
||||
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
|
||||
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
|
||||
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
|
||||
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
|
||||
if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
|
||||
if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
|
||||
if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
|
||||
if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
|
||||
// Shoulder buttons
|
||||
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
|
||||
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
|
||||
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
|
||||
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
|
||||
if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
|
||||
if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
|
||||
if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
|
||||
if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
|
||||
// Options (Start)
|
||||
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
|
||||
if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
|
||||
return buttons;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@ public static class RtcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// TryConvertTickToDateTime yields a Utc-kind DateTime, but here the tick is a local
|
||||
// wall-clock time. ConvertTimeToUtc throws ArgumentException when a Utc-kind value is
|
||||
// paired with a non-UTC source zone, so on any host not set to UTC this would always
|
||||
// fail. Re-tag as Unspecified so the value is interpreted as local time and converted.
|
||||
localDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
|
||||
|
||||
DateTime utcDateTime;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -6,6 +6,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
<!-- SysAbi export generator + analyzers (compile-time registry, NID validation). -->
|
||||
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The PS5 symbol catalog: lets the analyzer flag export names it doesn't know. -->
|
||||
<AdditionalFiles Include="..\..\scripts\ps5_names.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -13,6 +24,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Input" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
@@ -93,7 +94,7 @@ public static class SystemServiceExports
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||
{
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
GuestGpu.Current.HideSplashScreen();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,8 @@ public static class UserServiceExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "D-CzAxQL0XI",
|
||||
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||
@@ -184,6 +186,7 @@ public static class UserServiceExports
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "woNpu+45RLk",
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
internal static class PngSplashLoader
|
||||
{
|
||||
private const uint CrcPolynomial = 0xEDB88320;
|
||||
private static readonly uint[] CrcTable = BuildCrcTable();
|
||||
|
||||
private static ReadOnlySpan<byte> PngSignature =>
|
||||
[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
@@ -90,6 +93,13 @@ internal static class PngSplashLoader
|
||||
|
||||
var chunkType = png.Slice(offset + 4, 4);
|
||||
var chunkData = png.Slice(offset + 8, (int)chunkLength);
|
||||
var expectedCrc = BinaryPrimitives.ReadUInt32BigEndian(
|
||||
png.Slice(offset + 8 + (int)chunkLength, 4));
|
||||
if (CalculateCrc(chunkType, chunkData) != expectedCrc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chunkType.SequenceEqual("IHDR"u8))
|
||||
{
|
||||
if (chunkData.Length != 13)
|
||||
@@ -186,6 +196,41 @@ internal static class PngSplashLoader
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint CalculateCrc(ReadOnlySpan<byte> chunkType, ReadOnlySpan<byte> chunkData)
|
||||
{
|
||||
var crc = UpdateCrc(uint.MaxValue, chunkType);
|
||||
return ~UpdateCrc(crc, chunkData);
|
||||
}
|
||||
|
||||
private static uint UpdateCrc(uint crc, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
crc = CrcTable[(byte)(crc ^ value)] ^ (crc >> 8);
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static uint[] BuildCrcTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
for (var index = 0; index < table.Length; index++)
|
||||
{
|
||||
var value = (uint)index;
|
||||
for (var bit = 0; bit < 8; bit++)
|
||||
{
|
||||
value = (value & 1) != 0
|
||||
? CrcPolynomial ^ (value >> 1)
|
||||
: value >> 1;
|
||||
}
|
||||
|
||||
table[index] = value;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private static bool TryUnfilter(
|
||||
byte filter,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -67,7 +70,7 @@ public static class VideoOutExports
|
||||
return;
|
||||
}
|
||||
|
||||
HostTimerResolution.Request();
|
||||
HostPlatform.Current.Threading.RequestTimerResolution();
|
||||
|
||||
_vblankPumpThread = new Thread(VblankPumpLoop)
|
||||
{
|
||||
@@ -180,6 +183,10 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
// Only ever touched by the vblank pump thread; reused across edges so the 60 Hz
|
||||
// pump does not allocate a fresh snapshot per edge.
|
||||
private static readonly List<VideoOutPortState> _vblankPumpPorts = new();
|
||||
|
||||
private static void PumpVblanks()
|
||||
{
|
||||
lock (_vblankEdgeGate)
|
||||
@@ -188,7 +195,7 @@ public static class VideoOutExports
|
||||
Monitor.PulseAll(_vblankEdgeGate);
|
||||
}
|
||||
|
||||
VideoOutPortState[] ports;
|
||||
_vblankPumpPorts.Clear();
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_ports.Count == 0)
|
||||
@@ -198,10 +205,16 @@ public static class VideoOutExports
|
||||
|
||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||
foreach (var port in _ports.Values)
|
||||
{
|
||||
if (port.VblankEvents.Count != 0)
|
||||
{
|
||||
_vblankPumpPorts.Add(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var port in ports)
|
||||
foreach (var port in _vblankPumpPorts)
|
||||
{
|
||||
SignalVblank(port);
|
||||
}
|
||||
@@ -221,6 +234,12 @@ public static class VideoOutExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
// Call sites must check this before building the interpolated message; the trace
|
||||
// strings would otherwise be allocated on the per-frame flip path even with tracing off.
|
||||
private static readonly bool _logVideoOut = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _dumpVideoOut = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||
"1",
|
||||
@@ -586,7 +605,10 @@ public static class VideoOutExports
|
||||
// Some engines wait on this queue before issuing their first flip. Provide a first
|
||||
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||
SignalVblank(port);
|
||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -623,7 +645,10 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -684,7 +709,10 @@ public static class VideoOutExports
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||
|
||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -785,7 +813,7 @@ public static class VideoOutExports
|
||||
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
|
||||
}
|
||||
|
||||
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||
GuestGpu.Current.Submit(bgraFrame, width, height);
|
||||
}
|
||||
|
||||
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
|
||||
@@ -1077,33 +1105,53 @@ public static class VideoOutExports
|
||||
|
||||
private static void SignalVblank(VideoOutPortState port)
|
||||
{
|
||||
List<FlipEventRegistration> vblankEvents;
|
||||
// Snapshot the registrations into a pooled rental so the triggers can run outside
|
||||
// _stateGate without copying the list into a fresh allocation on every edge.
|
||||
// A per-port reusable buffer would race: the pump thread and a guest thread's
|
||||
// first-edge signal (AddVblankEvent) can signal the same port concurrently.
|
||||
FlipEventRegistration[]? vblankEvents = null;
|
||||
int vblankEventCount;
|
||||
ulong eventHint;
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.VblankCount++;
|
||||
eventHint = SceVideoOutInternalEventVblank |
|
||||
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
|
||||
vblankEventCount = port.VblankEvents.Count;
|
||||
if (vblankEventCount != 0)
|
||||
{
|
||||
vblankEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(vblankEventCount);
|
||||
port.VblankEvents.CopyTo(vblankEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||
|
||||
foreach (var vblankEvent in vblankEvents)
|
||||
if (vblankEvents is not null)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
vblankEvent.Equeue,
|
||||
SceVideoOutInternalEventVblank,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
vblankEvent.UserData);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < vblankEventCount; i++)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
vblankEvents[i].Equeue,
|
||||
SceVideoOutInternalEventVblank,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
vblankEvents[i].UserData);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<FlipEventRegistration>.Shared.Return(vblankEvents);
|
||||
}
|
||||
}
|
||||
|
||||
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
||||
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
|
||||
$"queues={vblankEventCount} hint=0x{eventHint:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1125,8 +1173,11 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
// Pooled snapshot for the same reason as SignalVblank: triggers run outside
|
||||
// _stateGate, and SubmitFlip is per-frame so a fresh List copy is steady churn.
|
||||
ulong eventHint;
|
||||
List<FlipEventRegistration> flipEvents;
|
||||
FlipEventRegistration[]? flipEvents = null;
|
||||
int flipEventCount;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||
@@ -1138,7 +1189,12 @@ public static class VideoOutExports
|
||||
port.FlipCount++;
|
||||
eventHint = SceVideoOutInternalEventFlip |
|
||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||
flipEventCount = port.FlipEvents.Count;
|
||||
if (flipEventCount != 0)
|
||||
{
|
||||
flipEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(flipEventCount);
|
||||
port.FlipEvents.CopyTo(flipEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var guestImageSubmitted = false;
|
||||
@@ -1148,7 +1204,7 @@ public static class VideoOutExports
|
||||
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
|
||||
{
|
||||
guestImageAddress = displayBuffer.Address;
|
||||
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
guestImageSubmitted = GuestGpu.Current.TrySubmitGuestImage(
|
||||
displayBuffer.Address,
|
||||
displayBuffer.Width,
|
||||
displayBuffer.Height,
|
||||
@@ -1160,14 +1216,24 @@ public static class VideoOutExports
|
||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
foreach (var flipEvent in flipEvents)
|
||||
if (flipEvents is not null)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
flipEvent.Equeue,
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
flipEvent.UserData);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < flipEventCount; i++)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
flipEvents[i].Equeue,
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
flipEvents[i].UserData);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<FlipEventRegistration>.Shared.Return(flipEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||
@@ -1176,10 +1242,13 @@ public static class VideoOutExports
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
||||
$"flipQueues={flipEvents.Count}");
|
||||
$"flipQueues={flipEventCount}");
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEventCount}");
|
||||
}
|
||||
ReportFrameRate(presented: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1261,16 +1330,19 @@ public static class VideoOutExports
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
}
|
||||
GuestGpu.Current.EnsureStarted(attribute.Width, attribute.Height);
|
||||
|
||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||
if (guestFormat != 0)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
GuestGpu.Current.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1428,7 +1500,10 @@ public static class VideoOutExports
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1467,7 +1542,10 @@ public static class VideoOutExports
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1496,7 +1574,7 @@ public static class VideoOutExports
|
||||
: 0u;
|
||||
|
||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
|
||||
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
|
||||
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||
NormalizePixelFormat(pixelFormat) switch
|
||||
@@ -1694,11 +1772,6 @@ public static class VideoOutExports
|
||||
|
||||
private static void TraceVideoOut(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,111 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "Direct",
|
||||
"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": "Direct",
|
||||
"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": "Direct",
|
||||
"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": "Direct",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace SharpEmu.Logging;
|
||||
/// </summary>
|
||||
public static class BuildInfo
|
||||
{
|
||||
private const string ProjectUrl = "https://github.com/par274/sharpemu";
|
||||
private const string CanonicalRepository = "par274/sharpemu";
|
||||
private const string ProjectUrl = "https://github.com/sharpemu/sharpemu";
|
||||
private const string CanonicalRepository = "sharpemu/sharpemu";
|
||||
|
||||
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
|
||||
public static string? CommitSha { get; }
|
||||
@@ -76,9 +76,9 @@ public static class BuildInfo
|
||||
/// <summary>
|
||||
/// The multi-line banner, e.g.
|
||||
/// <code>
|
||||
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
|
||||
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/sharpemu/sharpemu
|
||||
///
|
||||
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
|
||||
/// Built from branch "main" of "sharpemu/sharpemu" by GitHub Actions workflow run https://github.com/sharpemu/sharpemu/actions/runs/123.
|
||||
/// </code>
|
||||
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
|
||||
/// local-build line when no CI provenance is present.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user