Compare commits
19 Commits
1016_part3
...
render-bac
| Author | SHA1 | Date |
|---|---|---|
|
|
1aa084704a | |
|
|
0bd63c5a74 | |
|
|
25ea376d41 | |
|
|
94463576f8 | |
|
|
c707f4d2ce | |
|
|
0c750ffa61 | |
|
|
cd27d3824b | |
|
|
74c18837f3 | |
|
|
6bb7cd5192 | |
|
|
7d4b6c1187 | |
|
|
f90b5fa82c | |
|
|
bd33ce464e | |
|
|
3ac4c7a0bb | |
|
|
3983c70475 | |
|
|
d0e7adec3b | |
|
|
30d6e9eda6 | |
|
|
919b9d92bb | |
|
|
8c0903f297 | |
|
|
5594d89cbd |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 693 KiB |
|
Before Width: | Height: | Size: 393 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
|
@ -1,27 +0,0 @@
|
|||
## Before submitting
|
||||
|
||||
Please read our contribution guidelines before opening a pull request:
|
||||
|
||||
➡️ [**CONTRIBUTING.md**](https://github.com/sharpemu/sharpemu/blob/main/CONTRIBUTING.md)
|
||||
|
||||
By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines.
|
||||
|
||||
## Testing
|
||||
|
||||
If applicable, list the game(s) you tested and briefly describe the results.
|
||||
|
||||
Example:
|
||||
|
||||
- Demon's Souls (PPSA01341) – Boots to splash screen.
|
||||
- Dreaming Sarah – Save/load works correctly.
|
||||
|
||||
If your changes do not affect runtime behavior (e.g. documentation, tooling, CI), write `N/A`.
|
||||
|
||||
## Checklist
|
||||
|
||||
By submitting this pull request, you confirm that:
|
||||
|
||||
- [ ] I have read and followed `CONTRIBUTING.md`.
|
||||
- [ ] I tested my changes or marked the testing section as `N/A`.
|
||||
- [ ] I wrote this pull request description myself and did not paste AI-generated explanations.
|
||||
- [ ] I listed the game(s) I tested (or marked the testing section as `N/A`).
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Tells the website repo to rebuild when a release is published, so
|
||||
# sharpemu.app/downloads lists the new build within a minute.
|
||||
name: Notify website
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, released, edited, deleted]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Trigger sharpemu-site rebuild
|
||||
env:
|
||||
TOKEN: ${{ secrets.SITE_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "SITE_DISPATCH_TOKEN is not set — skipping website rebuild."
|
||||
exit 0
|
||||
fi
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/sharpemu/sharpemu-site/dispatches \
|
||||
-d '{"event_type":"release-published"}'
|
||||
echo "Website rebuild requested."
|
||||
|
|
@ -26,7 +26,7 @@ permissions:
|
|||
jobs:
|
||||
comment:
|
||||
name: Post artifact links
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# Only for successful PR builds — artifacts exist only when the build passed.
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "**"
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
|
|
@ -34,7 +32,7 @@ concurrency:
|
|||
jobs:
|
||||
init:
|
||||
name: Init
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release-name: ${{ steps.vars.outputs.release-name }}
|
||||
release-tag: ${{ steps.vars.outputs.release-tag }}
|
||||
|
|
@ -55,11 +53,6 @@ jobs:
|
|||
release_tag="v${version}"
|
||||
release_name="SharpEmu v${version}"
|
||||
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "${GITHUB_REF_NAME}" != "${release_tag}" ]; then
|
||||
echo "Release tag ${GITHUB_REF_NAME} does not match project version ${release_tag}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "short-sha=${short_sha}"
|
||||
echo "safe-ref=${safe_ref}"
|
||||
|
|
@ -71,7 +64,7 @@ jobs:
|
|||
reuse:
|
||||
name: REUSE Compliance
|
||||
needs: init
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -80,7 +73,7 @@ jobs:
|
|||
uses: fsfe/reuse-action@v6
|
||||
|
||||
build:
|
||||
name: Build win-x64
|
||||
name: Build Archive
|
||||
needs:
|
||||
- init
|
||||
- reuse
|
||||
|
|
@ -89,6 +82,7 @@ jobs:
|
|||
DOTNET_NOLOGO: true
|
||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
||||
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -120,13 +114,24 @@ jobs:
|
|||
- name: Publish win-x64 CLI
|
||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||
|
||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||
if (Test-Path $archivePath) {
|
||||
Remove-Item $archivePath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.PUBLISH_DIR }}
|
||||
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
|
||||
build-posix:
|
||||
name: Build ${{ matrix.rid }}
|
||||
|
|
@ -138,7 +143,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-26.04
|
||||
- os: ubuntu-latest
|
||||
rid: linux-x64
|
||||
- os: macos-latest
|
||||
rid: osx-x64
|
||||
|
|
@ -146,11 +151,7 @@ jobs:
|
|||
DOTNET_NOLOGO: true
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
||||
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
||||
SPIRV_TARGET_ENV: vulkan1.2
|
||||
SPIRV_TOOLS_COMMIT: 0539c81f69a3daeb706fd3477dca61435b475156
|
||||
SPIRV_TOOLS_VERSION: v2026.2
|
||||
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -175,34 +176,6 @@ jobs:
|
|||
- name: Run tests
|
||||
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
|
||||
|
||||
- name: Build pinned SPIRV-Tools
|
||||
if: matrix.rid == 'linux-x64'
|
||||
run: |
|
||||
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Tools.git "$RUNNER_TEMP/spirv-tools"
|
||||
git -C "$RUNNER_TEMP/spirv-tools" checkout --detach "$SPIRV_TOOLS_COMMIT"
|
||||
test "$(git -C "$RUNNER_TEMP/spirv-tools" rev-parse HEAD)" = "$SPIRV_TOOLS_COMMIT"
|
||||
|
||||
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Headers.git "$RUNNER_TEMP/spirv-tools/external/spirv-headers"
|
||||
git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" checkout --detach "$SPIRV_HEADERS_COMMIT"
|
||||
test "$(git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" rev-parse HEAD)" = "$SPIRV_HEADERS_COMMIT"
|
||||
|
||||
cmake -S "$RUNNER_TEMP/spirv-tools" -B "$RUNNER_TEMP/spirv-tools-build" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DSPIRV_SKIP_TESTS=ON \
|
||||
-DSPIRV_WERROR=OFF
|
||||
cmake --build "$RUNNER_TEMP/spirv-tools-build" --target spirv-val
|
||||
|
||||
- name: Generate and validate synthetic SPIR-V
|
||||
if: matrix.rid == 'linux-x64'
|
||||
run: |
|
||||
dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||
scripts/validate-synthetic-spirv.sh \
|
||||
"$RUNNER_TEMP/spirv-tools-build/tools/spirv-val" \
|
||||
"$SPIRV_TOOLS_VERSION" \
|
||||
"$SPIRV_TARGET_ENV" \
|
||||
artifacts/shader-dump
|
||||
|
||||
- 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"
|
||||
|
||||
|
|
@ -210,13 +183,19 @@ jobs:
|
|||
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.PUBLISH_DIR }}
|
||||
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
|
|
@ -224,120 +203,54 @@ jobs:
|
|||
- init
|
||||
- build
|
||||
- build-posix
|
||||
# Versioned releases are immutable and tag-driven. Branch and manual runs
|
||||
# still produce Actions artifacts without modifying a published release.
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-26.04
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: release
|
||||
|
||||
- name: Package release assets
|
||||
shell: bash
|
||||
env:
|
||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
|
||||
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
|
||||
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
|
||||
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
|
||||
test -d "${package_dir}"
|
||||
done
|
||||
|
||||
mkdir -p release-assets
|
||||
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
|
||||
|
||||
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
|
||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
||||
|
||||
- name: Build release notes
|
||||
shell: bash
|
||||
env:
|
||||
MAX_COMMITS: 200
|
||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
previous_tag="$(git describe --tags --abbrev=0 --match 'v*' "${RELEASE_TAG}^" 2>/dev/null || true)"
|
||||
|
||||
if [ -n "${previous_tag}" ]; then
|
||||
range="${previous_tag}..${RELEASE_TAG}"
|
||||
else
|
||||
range="${RELEASE_TAG}"
|
||||
fi
|
||||
|
||||
git log --no-merges --reverse --pretty=tformat:"%H%x1f%h%x1f%s" "${range}" > commits.txt
|
||||
total="$(wc -l < commits.txt)"
|
||||
|
||||
{
|
||||
printf 'Automated SharpEmu v%s build for commit [`%s`](%s/commit/%s).\n\n' \
|
||||
"${VERSION}" "${SHORT_SHA}" "${REPO_URL}" "${GITHUB_SHA}"
|
||||
|
||||
if [ -n "${previous_tag}" ]; then
|
||||
printf '## Changes since %s\n\n' "${previous_tag}"
|
||||
else
|
||||
printf '## Changes\n\n'
|
||||
fi
|
||||
|
||||
if [ "${total}" -eq 0 ]; then
|
||||
printf '_No commits since %s._\n' "${previous_tag}"
|
||||
else
|
||||
head -n "${MAX_COMMITS}" commits.txt | while IFS=$'\x1f' read -r sha short subject; do
|
||||
printf -- '- %s ([`%s`](%s/commit/%s))\n' "${subject}" "${short}" "${REPO_URL}" "${sha}"
|
||||
done
|
||||
|
||||
if [ "${total}" -gt "${MAX_COMMITS}" ]; then
|
||||
printf '\n_…and %s more commits._\n' "$((total - MAX_COMMITS))"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
|
||||
if [ -n "${previous_tag}" ]; then
|
||||
printf '**Full changelog**: %s/compare/%s...%s\n' "${REPO_URL}" "${previous_tag}" "${RELEASE_TAG}"
|
||||
else
|
||||
printf '**Full changelog**: %s/commits/%s\n' "${REPO_URL}" "${RELEASE_TAG}"
|
||||
fi
|
||||
} > release-notes.md
|
||||
|
||||
cat release-notes.md
|
||||
|
||||
- name: Create release
|
||||
- name: Create or update release
|
||||
shell: bash
|
||||
env:
|
||||
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: |
|
||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||
if [ "${#assets[@]}" -ne 3 ]; then
|
||||
echo "Expected 3 release assets, found ${#assets[@]}." >&2
|
||||
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
|
||||
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
||||
exit 1
|
||||
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
||||
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
||||
else
|
||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes "${notes}" \
|
||||
--target "${GITHUB_SHA}"
|
||||
fi
|
||||
|
||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||
--verify-tag \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes-file release-notes.md
|
||||
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
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ packages/
|
|||
.nuget/
|
||||
.dotnet-home/
|
||||
.cache/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -42,6 +40,3 @@ ehthumbs.db
|
|||
|
||||
.vs/
|
||||
.idea/
|
||||
.vscode/
|
||||
clean_test/
|
||||
.debug/
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
||||
# Contributing
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The pull request template is mandatory.
|
||||
>
|
||||
> Pull requests that do not follow the template or leave the required checklist incomplete will be closed without review, even if the proposed code is technically correct or beneficial. Please review these contribution guidelines before submitting a pull request.
|
||||
|
||||
Contributions are always welcome!
|
||||
|
||||
Before opening a pull request, please keep the following in mind:
|
||||
|
|
@ -26,25 +21,6 @@ Before opening a pull request, please keep the following in mind:
|
|||
|
||||
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
Pull requests should provide real, observable emulator behavior rather than only suppressing errors or unresolved imports.
|
||||
|
||||
Changes that only return success, zero, or fabricated handles without implementing the expected state, output, or side effects will generally not be accepted. Functions that create resources, write output structures, register callbacks, or expose runtime state should model the behavior required by the guest.
|
||||
|
||||
When applicable, PRs should include:
|
||||
|
||||
- The affected game or application.
|
||||
- Relevant logs or failing imports.
|
||||
- Behavior before and after the change.
|
||||
- Real game testing and known limitations.
|
||||
|
||||
Avoid submitting large collections of speculative NIDs or unrelated exports. Keep each PR focused on one problem or a closely related set of changes.
|
||||
|
||||
Large architectural changes should be discussed with the maintainers before implementation. Contributors are encouraged to ask first when they are uncertain whether a proposed direction fits the project.
|
||||
|
||||
Opening a PR does not guarantee that it will be merged. Maintainers evaluate changes based on correctness, evidence, testing, scope, maintenance cost, and the long-term direction of the project.
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
|
||||
|
|
|
|||
|
|
@ -9,18 +9,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.1</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx</_HostRidOSPrefix>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64</_HostRidArch>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64</_HostRidArch>
|
||||
<RuntimeIdentifier Condition="'$(_HostRidOSPrefix)' != ''">$(_HostRidOSPrefix)-$(_HostRidArch)</RuntimeIdentifier>
|
||||
|
||||
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,27 +7,22 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="12.1.0" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
|
||||
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
|
||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<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="NLayer" Version="1.14.0" />
|
||||
<PackageVersion Include="ppy.SDL3-CS" Version="2026.629.0" />
|
||||
<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" />
|
||||
<!-- Reuse the font rasterizer already shipped with Avalonia.Skia. -->
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
<PackageVersion Include="System.IO.Hashing" Version="10.0.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
133
README.md
|
|
@ -13,36 +13,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
An experimental PlayStation 5 emulator for Windows, Linux and macOS.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/zdTuUU9Uwn">
|
||||
<a href="https://discord.gg/6GejPEDqpc">
|
||||
<img src="https://img.shields.io/badge/Discord-Join%20our%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Join our official Discord server for development updates, compatibility discussions, support, and community chat.</strong>
|
||||
<strong>Join our Discord for development updates, compatibility discussions, support, and community chat.</strong>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<a href="#support">
|
||||
<img src="https://img.shields.io/badge/Support-GitHub%20Sponsors%20%26%20Crypto-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Support SharpEmu">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Official channels:** Only **sharpemu.app** and the links listed in this GitHub repository are affiliated with SharpEmu. Any other websites, accounts, or donation pages are unofficial and unauthorized.
|
||||
|
||||
> [!NOTE]
|
||||
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||
> can run the macOS x64 build through Rosetta 2, and Windows on ARM devices
|
||||
> (e.g. Snapdragon) can run the Windows x64 build through Windows' built-in
|
||||
> x64 emulation.
|
||||
---
|
||||
|
||||
> [!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.
|
||||
|
|
@ -56,55 +41,71 @@ This project is developed purely for research and educational purposes. There ar
|
|||
SharpEmu focuses exclusively on the PlayStation 5.
|
||||
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
|
||||
|
||||
## Games Tested
|
||||
|
||||
| Demon's Souls Remake | ASTRO BOT |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| ASTRO's PLAYROOM | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| God of War: Sons of Sparta | Tomb Raider V Remastered |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
## Status
|
||||
|
||||
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle gpu-related functionality. Included 3D games.
|
||||
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
|
||||
|
||||
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.
|
||||
Current capabilities include:
|
||||
|
||||
## Using
|
||||
* Loading `eboot.bin` and `.elf` files
|
||||
* Executing native CPU instructions
|
||||
* Reading basic game metadata (title, version, etc.)
|
||||
* Loading system modules (`prx` / `sys_module`)
|
||||
* Partial support for some kernel functions
|
||||
* `Fiber` and `AMPR` exports
|
||||
* PlayGo scenarios
|
||||
* Initial loading game files
|
||||
* Shader/resource submits and AGC initial
|
||||
* Video outputs in some games
|
||||
|
||||
Download the release archive for your operating system, extract it, and launch
|
||||
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||
Some games have reached like `sceVideoOut` and AGC stages.
|
||||
|
||||
Or command line;
|
||||
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.
|
||||
|
||||
Windows PowerShell:
|
||||
## Games Tested
|
||||
|
||||
```powershell
|
||||
.\SharpEmu.exe "C:\path\to\game\eboot.bin" --log-to-file
|
||||
```
|
||||
* **Demon's Souls Remake**
|
||||
* [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.
|
||||

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

|
||||
|
||||
SharpEmu supports environment variables that can be configured from the GUI. Undocumented variables are listed here: [docs/sharpemu-gui-undocumented-env-vars.md](docs/sharpemu-gui-undocumented-env-vars.md)
|
||||
|
||||
You can set them per game or globally in the GUI, or pass them directly through the CLI.
|
||||
|
||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||
release includes the MoltenVK Vulkan implementation.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project does **not** support or condone piracy.
|
||||
|
|
@ -113,7 +114,7 @@ release includes the MoltenVK Vulkan implementation.
|
|||
|
||||
## Build
|
||||
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
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`
|
||||
|
|
@ -142,18 +143,6 @@ Provided valuable references for filesystem handling and low-level C# implementa
|
|||
|
||||
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
|
||||
|
||||
## Support
|
||||
|
||||
Support SharpEmu via GitHub Sponsors or cryptocurrency. Every contribution helps fund ongoing development and long-term maintenance. GitHub Sponsors is the preferred way to support the project, but cryptocurrency donations are also appreciated.
|
||||
|
||||
### ETH/USDT
|
||||
|
||||
`0xF315F5d986c790bB3A58DbE60F1B2760997dEd82`
|
||||
|
||||
### BTC
|
||||
|
||||
`bc1qmr9k8899njys5ny63xsues4jgmkk96erslrkmv`
|
||||
|
||||
## Contributing
|
||||
|
||||
Before opening an issue or pull request, please read our contribution guidelines:
|
||||
|
|
|
|||
12
REUSE.toml
|
|
@ -5,14 +5,10 @@ path = [
|
|||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"src/SharpEmu.ShaderCompiler.Metal/Templates/**",
|
||||
"tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
".github/pull_request_template.md",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
|
|
@ -20,13 +16,7 @@ SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
|||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/SharpEmu.GUI/Assets/Fonts/MaterialSymbolsRounded-*.ttf"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2026 Google LLC"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/SharpEmu.LibAtrac9/**"
|
||||
path = "src/SharpEmu.GUI/Atrac9/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2018 Alex Barney"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
|
|
|||
|
|
@ -5,24 +5,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpEmu.LibAtrac9/SharpEmu.LibAtrac9.csproj" />
|
||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||
<Project Path="src/SharpEmu.DebugClient/SharpEmu.DebugClient.csproj" />
|
||||
<Project Path="src/SharpEmu.Debugger/SharpEmu.Debugger.csproj" />
|
||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
||||
<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.Metal/SharpEmu.ShaderCompiler.Metal.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.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 3.9 MiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Aerolib Catalog
|
||||
|
||||
```bash
|
||||
# NID to export name
|
||||
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
|
||||
|
||||
# Export name to NID
|
||||
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
|
||||
|
||||
# A comma can be used for multiple query strings.
|
||||
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk,SaKib2Ug0yI,sceVideoOutOpen,amuBfI-AQc4
|
||||
|
||||
# Search export names
|
||||
python scripts/aerolib_catalog.py search VideoOut --limit 20
|
||||
|
||||
# Export all NID/name pairs to artifacts/aerolib.txt
|
||||
python scripts/aerolib_catalog.py export
|
||||
```
|
||||
|
|
@ -9,58 +9,38 @@ Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
|
|||
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||
decoder cannot observe or replace those frames.
|
||||
|
||||
By default, SharpEmu leaves Bink decoding to the implementation linked into
|
||||
the game (`SHARPEMU_BINK_MODE=guest`).
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||
|
||||
Set `SHARPEMU_BINK_MODE=native` to use the optional host bridge. It observes
|
||||
guest .bk2 opens and presents decoded frames at the guest-flip boundary. The
|
||||
host path decodes by calling FFmpeg's C API directly from managed
|
||||
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
|
||||
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
|
||||
bindings) against a custom FFmpeg build
|
||||
(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
|
||||
FFmpeg 7.1.2; see "Supplying the FFmpeg libraries" below for where those
|
||||
libraries come from. No proprietary RAD SDK is needed to build or run
|
||||
SharpEmu, and there is no C/C++ code of SharpEmu's own involved in decoding
|
||||
-- SharpEmu.CLI.csproj only downloads a prebuilt release archive.
|
||||
|
||||
Set `SHARPEMU_BINK_MODE=skip` only when explicitly testing a title whose
|
||||
cinematics are optional.
|
||||
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||
not-found so games that mark cinematics as optional progress to their next
|
||||
state instead of waiting on an empty Bink GPU texture.
|
||||
|
||||
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||
only; it does not decode the movie or alter its game logic.
|
||||
`SHARPEMU_BINK_MODE=ffmpeg` is an alias for `native`.
|
||||
only; it does not decode the movie or alter its game logic. Set
|
||||
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||
|
||||
## Supplying the FFmpeg libraries
|
||||
## Supplying the adapter
|
||||
|
||||
Both `dotnet build` and `dotnet publish` fetch a prebuilt release of
|
||||
`github.com/sharpemu/ffmpeg-core` (the tag is pinned in
|
||||
`SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to the `FFmpeg.AutoGen`
|
||||
package version in `Directory.Packages.props` -- both need to agree on the
|
||||
same FFmpeg ABI) and copy its dynamically linked libraries into a `plugins`
|
||||
folder next to the resulting executable (`artifacts/bin/...` for build,
|
||||
`artifacts/publish/...` for publish). No C toolchain is required to build
|
||||
SharpEmu; both just download a zip once (cached under
|
||||
`$(BaseIntermediateOutputPath)ffmpeg-runtime/`, so later builds/publishes
|
||||
reuse it instead of re-fetching). `plugins` is a loose, unpacked folder
|
||||
rather than something embedded in the single-file bundle, so the OS loader
|
||||
can resolve the libraries' own inter-dependencies (`avcodec` depends on
|
||||
`avutil`, etc.) itself.
|
||||
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||
The adapter deliberately contains only a three-function C ABI so the managed
|
||||
emulator never depends on RAD's private binary ABI.
|
||||
|
||||
A plain `dotnet build`/`dotnet publish` with no `-r` still works: it defaults
|
||||
to the host machine's own RID (see `Directory.Build.props`), so it fetches
|
||||
the matching `ffmpeg-core` archive and populates `plugins` without any extra
|
||||
flags. Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
||||
Windows) still overrides that default normally.
|
||||
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||
executable, or point to it explicitly:
|
||||
|
||||
To use a different set of FFmpeg libraries, drop them into the build or
|
||||
published `plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||
folder and does not otherwise care where the files came from.
|
||||
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||
./SharpEmu /path/to/eboot.bin
|
||||
|
||||
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
||||
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
|
||||
could not open movie ...") and leaves the guest's own rendering path
|
||||
untouched, rather than crashing.
|
||||
The expected exports are sharpemu_bink2_open_utf8,
|
||||
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||
frame. The managed side validates dimensions and retains ownership of the
|
||||
destination buffer.
|
||||
|
||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||
and retains the existing guest rendering path.
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Shader rejection diagnostics
|
||||
|
||||
`SHARPEMU_STRICT_COMPUTE` controls the rejections listed below for compute dispatches
|
||||
and graphics draws. The existing variable name is retained; there is no separate
|
||||
`SHARPEMU_STRICT_DRAW` switch. Restart the game after a change. The pipeline cache and
|
||||
draw executor read the value when created.
|
||||
|
||||
- Unset or `1`: stop on the rejection so that the failure can be diagnosed (default).
|
||||
- `0`: skip the affected dispatch or draw. Only this value enables skipping.
|
||||
|
||||
The GUI provides this switch under **Environment > Debug**, globally and per game.
|
||||
It is on by default. Turning it off saves an explicit `0`; an inherited process
|
||||
environment value does not override the GUI choice. Other values retain strict mode.
|
||||
|
||||
## Complete skip list
|
||||
|
||||
With the switch set to `0`, these are the only handled rejection categories:
|
||||
|
||||
| Rejection | Detection point | Operation skipped |
|
||||
| --- | --- | --- |
|
||||
| Resource-plan extraction throws `ResourcePlanException`, including an unsupported resource expression or a value graph that does not converge | `ShaderProgramCache.CreateEntry` | The dispatch or draw that needs the rejected shader |
|
||||
| Shader compilation returns failure or no compiled program, including an unsupported opcode reported by the compiler | `ShaderProgramCache.CompilePermutation` | The dispatch or draw that needs the rejected shader |
|
||||
| Indirect image candidates have incompatible image classes (`IncompatibleImageCandidates`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
|
||||
| Indirect image expansion exceeds the dense image resource limit (`ImageCapacityExceeded`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
|
||||
| A graphics texture's cached image type cannot support the requested view type, such as a 2D view of a 1D image | Vulkan `ValidateDrawImageTypes`, handled by `RenderExecutor.RecordDraw` | The whole indexed or automatic draw |
|
||||
|
||||
The last category applies to vertex and pixel resource preparation, not compute
|
||||
image binding. It checks available, non-stale cache entries and excludes host movie
|
||||
textures. A mismatch detected later in binding remains fatal.
|
||||
|
||||
The draw image-type check runs before resource binding and draw recording. Skip mode
|
||||
does not create an invalid view or issue the rejected draw. The preparation scope is
|
||||
released, and the caller resets the image bindings before the next operation.
|
||||
|
||||
## Failures that remain fatal
|
||||
|
||||
- Shader decoding failures and invalid shader source reads.
|
||||
- Unreadable resource memory, invalid indirect table state, and materialization failures
|
||||
other than the two named categories above.
|
||||
- Invalid binding layouts or invalid resource snapshot state.
|
||||
- Other image checks, including format, usage, aspect, component mapping, and range
|
||||
checks reached during binding or view creation. The image cache checks are unchanged.
|
||||
- Image-type mismatches in compute binding or outside the graphics preparation check.
|
||||
- Host allocation failures, device loss, synchronization failures, and unexpected exceptions.
|
||||
|
||||
This is not a general exception bypass. Setting the switch to `1` also makes every
|
||||
category in the skip list fatal. Supported operations use the same path in both modes.
|
||||
|
||||
## Warnings and test limits
|
||||
|
||||
Program rejections log `COMPUTE_SKIPPED` or `DRAW_SKIPPED` once per stage, shader hash,
|
||||
and code size in each pipeline cache. Draw image-type rejections log `DRAW_SKIPPED`
|
||||
once per shader hash, image type, and view type in each draw executor. Warning counts
|
||||
are not skipped-operation counts. No rejected permutation is recorded as compiled;
|
||||
a later request can retry it.
|
||||
|
||||
A skipped operation does not produce its outputs. Missing images, incorrect results,
|
||||
later errors, and stalls are possible. FPS can rise because less work is executed.
|
||||
Skip-mode smoke tests can check progress and expose other defects, but must record
|
||||
the skip warnings. They do not close the skipped defects or establish a final visual
|
||||
or performance baseline. This switch does not restore the old renderer.
|
||||
|
||||
## PowerShell
|
||||
|
||||
Enable strict testing in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:SHARPEMU_STRICT_COMPUTE = '1'
|
||||
```
|
||||
|
||||
Explicitly enable skip mode:
|
||||
|
||||
```powershell
|
||||
$env:SHARPEMU_STRICT_COMPUTE = '0'
|
||||
```
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Live debug server
|
||||
|
||||
SharpEmu can expose a **live debug server** so an external process can inspect
|
||||
and control a running guest over TCP. The server lives in the emulator; the
|
||||
companion `SharpEmu.DebugClient` executable is one client, and the wire protocol
|
||||
is simple enough to script against directly.
|
||||
|
||||
This document describes the moving parts and the wire protocol. For day-to-day
|
||||
client usage, see
|
||||
[`src/SharpEmu.DebugClient/DEVELOPER_READ.md`](../src/SharpEmu.DebugClient/DEVELOPER_READ.md).
|
||||
|
||||
## Layering
|
||||
|
||||
| Assembly | Role |
|
||||
| -------- | ---- |
|
||||
| `SharpEmu.Core` | Defines the dispatcher seam `ICpuDebugHook` / `ICpuDebugFrame` (namespace `SharpEmu.Core.Cpu.Debug`) and the `CpuExecutionOptions.DebugHook` slot. Core has **no** reference to the debugger. |
|
||||
| `SharpEmu.Debugger` | The debugger: `DebuggerSession` (implements the hook), `BreakpointStore`, the TCP `DebuggerServer`, the pluggable `IDebugProtocol` with a JSON-lines implementation, and the `DebuggerServerHost` one-call wiring. |
|
||||
| `SharpEmu.CLI` | Parses `--debug-server`, builds a `DebuggerServerHost`, hands its `Hook` to `SharpEmuRuntimeOptions.DebugHook`, and manages its lifetime. |
|
||||
| `SharpEmu.DebugClient` | A standalone client executable. Depends only on the BCL. |
|
||||
|
||||
The dependency direction is important: Core stays debugger-agnostic and only
|
||||
publishes the seam. Anything that observes execution implements
|
||||
`ICpuDebugHook` and is injected through the options, so the debugger can evolve
|
||||
without touching the CPU core.
|
||||
|
||||
## Execution model
|
||||
|
||||
`CpuDispatcher` enters a fresh frame for the process entry point and for each
|
||||
module initializer. When a `DebugHook` is attached it is notified at those
|
||||
boundaries:
|
||||
|
||||
- `OnFrameEnter(frame)` — before the native backend runs the frame. The
|
||||
`DebuggerSession` decides whether to stop (pause request, breakpoint on the
|
||||
entry address, single-step, or stop-at-entry). To stop, it **parks the
|
||||
emulation thread** inside this call on a gate; the frame stays live, so a
|
||||
client can read and write registers and memory while parked. `continue` /
|
||||
`step` release the gate.
|
||||
- `OnFrameExit(frame, result)` — after the frame completes.
|
||||
|
||||
Because pausing parks the one thread that owns the guest context, register and
|
||||
memory accessors are only served while the session reports `Paused`; otherwise
|
||||
they return "not paused" so a client never observes torn state.
|
||||
|
||||
### What is and isn't live yet
|
||||
|
||||
- **Live:** attach/handshake, run-state tracking, register read/write, memory
|
||||
read/write, breakpoint management, execution breakpoints at frame entry,
|
||||
pause, frame-level step, continue, and stop/resume/terminate events.
|
||||
- **Surface only (armed as the backend grows hooks):** per-instruction
|
||||
stepping and data watchpoints (`readwatch` / `writewatch` / `accesswatch`).
|
||||
The verbs and types exist so clients and tooling can be written now.
|
||||
|
||||
## Enabling the server
|
||||
|
||||
```bash
|
||||
SharpEmu --debug-server "/path/to/eboot.bin" # 127.0.0.1:5714
|
||||
SharpEmu --debug-server=0.0.0.0:5714 "/path/to/eboot.bin"
|
||||
```
|
||||
|
||||
The bind address defaults to loopback; a routable address must be given
|
||||
explicitly. With stop-at-entry (the default `DebuggerSessionOptions.StopAtEntry`),
|
||||
the guest parks at its first frame until a client connects and issues
|
||||
`continue`, giving you a window to set breakpoints before any guest code runs.
|
||||
|
||||
## Browser frontend
|
||||
|
||||
The dependency-free Python frontend can choose and launch an `eboot.bin`, attach
|
||||
to its debugger automatically, and provides execution controls, registers,
|
||||
memory inspection, breakpoint management, process output, and a live protocol
|
||||
activity stream:
|
||||
|
||||
```bash
|
||||
./tools/SharpEmu.DebuggerFrontend/run.sh
|
||||
```
|
||||
|
||||
It connects to `127.0.0.1:5714` and opens `http://127.0.0.1:8765/` by default.
|
||||
See [`tools/SharpEmu.DebuggerFrontend/README.md`](../tools/SharpEmu.DebuggerFrontend/README.md)
|
||||
for configuration and testing options.
|
||||
|
||||
## Wire protocol (json-lines/1)
|
||||
|
||||
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
|
||||
|
||||
### Requests
|
||||
|
||||
A `command` string plus command-specific fields. Numeric fields accept a JSON
|
||||
number or a `0x`-prefixed hex string.
|
||||
|
||||
| `command` | Fields | Reply `data` |
|
||||
| --------- | ------ | ------------ |
|
||||
| `ping` | — | — |
|
||||
| `status` (`info`) | — | `state`, `breakpoints`, `lastStop?` |
|
||||
| `state` | — | `state` |
|
||||
| `registers` (`regs`) | — | `registers` (rax..r15, rip, rflags, fs_base, gs_base) |
|
||||
| `set-register` | `register`, `value` | — |
|
||||
| `read-memory` | `address`, `length` (≤ 65536) | `address`, `length`, `bytes` (hex) |
|
||||
| `write-memory` | `address`, `bytes` (hex) | `written` |
|
||||
| `list-breakpoints` (`breakpoints`) | — | `breakpoints[]` |
|
||||
| `add-breakpoint` (`break`) | `address`, `kind?`, `length?` | `breakpoint` |
|
||||
| `remove-breakpoint` (`delete-breakpoint`) | `id` | — |
|
||||
| `enable-breakpoint` | `id`, `enabled?` (default true) | — |
|
||||
| `continue` (`cont`, `c`) | — | — |
|
||||
| `step` (`s`) | — | — |
|
||||
| `pause` | — | — |
|
||||
|
||||
### Replies
|
||||
|
||||
```json
|
||||
{"ok":true,"command":"registers","data":{ "registers": { "rax":"0x…", … } }}
|
||||
{"ok":false,"command":"read-memory","error":"Target is not paused."}
|
||||
```
|
||||
|
||||
### Events (unsolicited)
|
||||
|
||||
```json
|
||||
{"event":"hello","protocol":"json-lines/1","state":"Paused"}
|
||||
{"event":"stopped","reason":"Breakpoint","address":"0x…","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{…},"breakpoint":{…}}
|
||||
{"event":"resumed"}
|
||||
{"event":"terminated"}
|
||||
```
|
||||
|
||||
`reason` is one of `EntryPoint`, `Breakpoint`, `Watchpoint`, `Step`, `Pause`,
|
||||
`Fault`, or `Stall`.
|
||||
|
||||
Stall stops include structured evidence in addition to the human-readable
|
||||
detail. Import-loop evidence identifies the NID, resolved HLE export, repeating
|
||||
guest return site, dispatch count, and first two ABI arguments:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "stopped",
|
||||
"reason": "Stall",
|
||||
"stall": {
|
||||
"kind": "ImportLoop",
|
||||
"nid": "9UK1vLZQft4",
|
||||
"instructionPointer": "0x0000000801CE2418",
|
||||
"dispatchIndex": 40667904,
|
||||
"argument0": "0x0000000812345000",
|
||||
"argument1": "0x0000000000000000",
|
||||
"resolved": true,
|
||||
"library": "libKernel",
|
||||
"function": "scePthreadMutexLock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Python frontend uses this evidence to explain the likely failure class and
|
||||
rank concrete checks/fixes. Its diagnosis is intentionally labelled heuristic:
|
||||
it helps locate the responsible HLE/scheduler path but does not replace tracing.
|
||||
|
||||
## Swapping the protocol
|
||||
|
||||
`DebuggerServer` takes an `IDebugProtocol` factory. The default is
|
||||
`JsonLineDebugProtocol`; a GDB remote serial stub (or any other framing) can be
|
||||
dropped in without changing the session or command semantics, which live in
|
||||
`DebugCommandDispatcher`.
|
||||
|
||||
## Embedding the server
|
||||
|
||||
```csharp
|
||||
using SharpEmu.Debugger;
|
||||
using SharpEmu.Core.Runtime;
|
||||
|
||||
await using var host = new DebuggerServerHost();
|
||||
host.Start();
|
||||
|
||||
var options = new SharpEmuRuntimeOptions { DebugHook = host.Hook };
|
||||
using var runtime = SharpEmuRuntime.CreateDefault(options);
|
||||
var result = runtime.Run(ebootPath);
|
||||
|
||||
host.NotifyRunCompleted();
|
||||
```
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Guest write watch
|
||||
|
||||
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
|
||||
code and HLE code that damage guest memory. The tool starts only if you set one
|
||||
or more `SHARPEMU_WATCH_*` environment variables.
|
||||
|
||||
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
|
||||
does not monitor stores that native guest code makes directly. Use a platform
|
||||
debugger or a hardware watchpoint to monitor these stores.
|
||||
|
||||
## Watch modes
|
||||
|
||||
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
|
||||
block at the specified guest address.
|
||||
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
|
||||
monitors the first 64 direct mappings that have a size of 64 KiB and
|
||||
protection value `0xF2`.
|
||||
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
|
||||
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
|
||||
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
|
||||
memory range. The tool logs a maximum of 128 entries for each process.
|
||||
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
|
||||
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
|
||||
logs a maximum of 64 entries for each process.
|
||||
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
|
||||
specified upper 32 bits in the destination address.
|
||||
|
||||
For each match, the tool logs the destination address, the data pattern, and the
|
||||
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
|
||||
tag.
|
||||
|
||||
Use these variables together to scan bulk writes in the
|
||||
`0x00000080xxxxxxxx` region.
|
||||
|
||||
macOS and Linux:
|
||||
|
||||
```sh
|
||||
SHARPEMU_WATCH_BULK_TORN=1 \
|
||||
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
|
||||
SharpEmu /path/to/eboot.bin
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:SHARPEMU_WATCH_BULK_TORN = "1"
|
||||
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
|
||||
& .\SharpEmu.exe C:\path\to\game\eboot.bin
|
||||
```
|
||||
|
||||
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
|
||||
address from a crash dump.
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Performance display
|
||||
|
||||
Open **Options → Rendering**, globally or for one game.
|
||||
|
||||
- **Performance display:** start with statistics on or off.
|
||||
- **Display mode:** Full shows all counters and the frame graph. Minimal shows
|
||||
guest FPS, process CPU usage, process GPU usage, and elapsed session time.
|
||||
Title bar shows the same minimal values in the window title.
|
||||
- **Screen corner:** choose any of the four corners. This setting applies only
|
||||
to the on-screen panel.
|
||||
|
||||
F1 toggles the display for the current run. F2 cycles clockwise through the
|
||||
screen corners. Neither key changes saved settings. The next run uses the saved
|
||||
options again. Command+F1 retains its separate Metal HUD shortcut.
|
||||
|
||||
`SHARPEMU_OVERLAY=0` starts the display hidden, even when the saved option is on.
|
||||
F1 can still show it. RenderDoc's F12 hint remains in the title when available.
|
||||
The operating system does not show a title bar in borderless/fullscreen modes.
|
||||
|
||||
The launcher passes these options to the game process, including when a separate
|
||||
emulator path is configured:
|
||||
|
||||
```text
|
||||
--overlay=on|off
|
||||
--overlay-mode=full|minimal|titlebar
|
||||
--overlay-corner=topleft|topright|bottomright|bottomleft
|
||||
```
|
||||
|
||||
## Usage values
|
||||
|
||||
The panel dimensions are separate constants in `PerfOverlay.cs`:
|
||||
`PanelWidth` / `PanelHeight` for Full, and
|
||||
`MinimalPanelWidth` / `MinimalPanelHeight` for Minimal. Both heights are fixed;
|
||||
the renderer clips the panel only when the window is too small.
|
||||
|
||||
CPU usage is the emulator process's CPU time divided by wall time and the
|
||||
number of logical processors. GPU usage is the busiest GPU engine used by the
|
||||
emulator process, not GPU memory usage or total usage by all applications.
|
||||
|
||||
Windows supplies the GPU Engine performance counters. Collection runs off the
|
||||
render thread and uses the existing statistics refresh interval. Rate counters
|
||||
need two samples. Missing counters, unavailable samples, and other platforms
|
||||
show **N/A**, not zero. No GPU waits or presentation delays are added.
|
||||
|
||||
The full panel keeps cache memory counters and puts CPU/GPU usage on a separate
|
||||
line above elapsed time and the Vulkan allocation counters.
|
||||
|
||||
The counter interpretation follows [Microsoft's GPU utilization description](https://devblogs.microsoft.com/directx/gpus-in-the-task-manager/).
|
||||
Wildcard samples use [PdhGetFormattedCounterArray](https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhgetformattedcounterarrayw).
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
## Release Script
|
||||
|
||||
SharpEmu releases are prepared and published using `scripts/release.py`.
|
||||
|
||||
The release process consists of two steps:
|
||||
|
||||
1. Prepare the version bump through a pull request.
|
||||
2. Create and push the release tag after the pull request has been merged.
|
||||
|
||||
### Preparing a Release
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python scripts/release.py prepare 0.0.2-beta.2
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
- Verify that the working tree is clean.
|
||||
- Update the local `main` branch.
|
||||
- Create a new branch named `release/0.0.2-beta.2`.
|
||||
- Update `SharpEmuVersion` in `Directory.Build.props`.
|
||||
- Create a version bump commit.
|
||||
- Push the release branch to the remote repository.
|
||||
|
||||
Afterwards, open a pull request from:
|
||||
|
||||
```text
|
||||
release/0.0.2-beta.2
|
||||
```
|
||||
|
||||
into:
|
||||
|
||||
```text
|
||||
main
|
||||
```
|
||||
|
||||
### Publishing a Release
|
||||
|
||||
Once the pull request has been merged, update your local repository:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git pull --ff-only
|
||||
```
|
||||
|
||||
Then create and push the release tag:
|
||||
|
||||
```bash
|
||||
python scripts/release.py tag 0.0.2-beta.2
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
- Verify that the working tree is clean.
|
||||
- Confirm that `Directory.Build.props` contains the requested version.
|
||||
- Create an annotated Git tag (`v0.0.2-beta.2`).
|
||||
- Push the tag to the remote repository.
|
||||
|
||||
Pushing the tag automatically triggers the GitHub Release workflow.
|
||||
|
||||
### Version Format
|
||||
|
||||
Specify the version **without** the `v` prefix.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
0.0.2
|
||||
0.0.2-alpha.1
|
||||
0.0.2-beta.1
|
||||
0.0.2-beta.2
|
||||
0.0.2-rc.1
|
||||
```
|
||||
|
||||
The script automatically prefixes the Git tag with `v`.
|
||||
|
||||
### Notes
|
||||
|
||||
- Run `prepare` only from the `main` branch.
|
||||
- Run `tag` only after the version bump pull request has been merged.
|
||||
- Do not create release tags manually before merging the version bump.
|
||||
- Both commands require a clean working tree.
|
||||
- The version in `Directory.Build.props` must exactly match the version passed to the `tag` command.
|
||||
|
|
@ -1,433 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# SharpEmu: environment variables that are not on the GUI options pages
|
||||
|
||||
## Scope
|
||||
|
||||
This list contains each `SHARPEMU_*` environment variable in the repository that the GUI does not show.
|
||||
The GUI shows 14 variables on its **Options** and **Game options** pages. This list does not contain them:
|
||||
`SHARPEMU_BTHID_UNAVAILABLE`, `SHARPEMU_CRASH_CAPTURE`, `SHARPEMU_DEFAULT_PROFILE`,
|
||||
`SHARPEMU_DISABLE_IMPORT_LOOP_GUARD`, `SHARPEMU_DUMP_SPIRV`, `SHARPEMU_LOG_DIRECT_MEMORY`, `SHARPEMU_LOG_IO`,
|
||||
`SHARPEMU_LOG_NP`, `SHARPEMU_PROFILE_PERFORMANCE`, `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE`,
|
||||
`SHARPEMU_RENDERDOC`, `SHARPEMU_VK_DISABLE_IMPLICITS`, `SHARPEMU_STRICT_COMPUTE`, `SHARPEMU_VK_VALIDATION`, `SHARPEMU_WRITABLE_APP0`.
|
||||
|
||||
The list contains 269 variables. The source was examined on 2026-09-19, branch `dev`.
|
||||
The descriptions come from the code that reads each variable. The emulator was not started for this list.
|
||||
|
||||
## How to use a variable
|
||||
|
||||
1. Set the variable in the shell before you start the emulator or the GUI.
|
||||
2. You can also enter additional env vars using the button next to the "Play" button from GUI.
|
||||
3. The GUI also accepts a variable in the `EnvironmentToggles` list of its settings file. Write `NAME` or `NAME=value`. An entry without a value gets the value `1`.
|
||||
4. The emulator reads most variables one time, when it starts. Start the game again after a change.
|
||||
5. If the **Value** column shows `1`, only the value `1` turns the function on, unless the row gives other values.
|
||||
|
||||
## Number of variables in each group
|
||||
|
||||
| Group | Variables |
|
||||
| --- | --- |
|
||||
| Behavior switches | 19 |
|
||||
| GPU and Vulkan | 59 |
|
||||
| Metal | 10 |
|
||||
| Audio and video | 6 |
|
||||
| Input | 5 |
|
||||
| Memory and CPU | 19 |
|
||||
| Loader | 11 |
|
||||
| Debugger | 1 |
|
||||
| Log channels | 72 |
|
||||
| Traces | 25 |
|
||||
| Data dumps | 12 |
|
||||
| Performance measurement | 17 |
|
||||
| Internal | 1 |
|
||||
| Tests and tools | 12 |
|
||||
|
||||
## Behavior switches
|
||||
|
||||
These variables change how the emulator operates.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_AGC_VERSION13_DEFAULTS` | text (`legacy`, `generic` or `0`) | Selects the register defaults for AGC version 13 requests. The default uses the version 11 compact tables. The values `legacy`, `generic` and `0` use the older generic layout. | `AgcExports.Init.cs` |
|
||||
| `SHARPEMU_DISABLE_LLE_LIBC` | `1` | Stops the use of the guest libc module for libc exports. The emulator then uses its HLE handlers for these exports. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_HLE_FAST_MEMCPY` | `0` | Set to `0` to stop the native fast-path stub for the guest libc `memcpy` and `memmove`, so those imports use the managed HLE gateway again. | `DirectExecutionBackend.FastPath.cs` |
|
||||
| `SHARPEMU_HLE_FAST_PATH` | `0` | Set to `0` to stop the native fast-path stubs for `pthread_self` and `pthread_getspecific`, so those imports use the managed HLE gateway again. The stubs are on by default on macOS x64 and are off on any run that traces imports or pthreads. | `DirectExecutionBackend.FastPath.cs` |
|
||||
| `SHARPEMU_HOLD_FIRST_FLIP_MS` | number | Stops the thread that submits the flip for this number of milliseconds on one guest flip. The range is 0 to 60000 and the default is 0 (off). `SHARPEMU_HOLD_FLIP_NUMBER` selects the flip. | `VideoOutExports.cs` |
|
||||
| `SHARPEMU_HOLD_FLIP_NUMBER` | number | Selects the guest flip that `SHARPEMU_HOLD_FIRST_FLIP_MS` holds. The minimum is 1 and the default is 1 (the first flip). | `VideoOutExports.cs` |
|
||||
| `SHARPEMU_IGNORE_GUEST_EXCEPTIONS` | `1` | `sceKernelRaiseException` returns OK and does not call the installed guest exception handler. Default is off. | `KernelExceptionCompatExports.cs` |
|
||||
| `SHARPEMU_IMPORT_LOOP_GUARD_SECONDS` | number | When `SHARPEMU_DISABLE_IMPORT_LOOP_GUARD=0` enables the guard, sets how long an import call pattern can repeat before a guest exit. The default is 5 seconds; 0 turns the guard off. | `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_NET_REDIRECT` | IP address | Sends guest `connect` and `sendto` traffic to this host IP address. When unset, the emulator permits outbound guest traffic only to loopback addresses. If the value is not a valid IP address, the emulator keeps the original destination and permits the traffic. | `KernelSocketCompatExports.cs` |
|
||||
| `SHARPEMU_NO_FLIP_PACING` | `1` | Stops flip pacing. The emulator completes each flip immediately and does not wait for the display refresh time. The default is off. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
|
||||
| `SHARPEMU_OVERLAY` | `0` | Hides the performance overlay at start, even when the video options have the overlay on. All other values use the video options. | `PerfOverlay.cs`, `PerformanceOverlayState.cs` |
|
||||
| `SHARPEMU_POSIX_GPU_FAULT_FAST_PATH` | `0` | Set to `0` to stop the POSIX fault bridge from offering GPU-tracked page faults to the GPU memory tracker before it builds the Win64 exception context. The fault then takes the full handler order again. | `DirectExecutionBackend.PosixSignals.cs` |
|
||||
| `SHARPEMU_RETAIN_SUBMITTED_INDEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted index data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
|
||||
| `SHARPEMU_RETAIN_SUBMITTED_VERTEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted vertex data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
|
||||
| `SHARPEMU_SAVEDATA_DIR` | path | Set the root directory for save data. The default is `user/savedata` next to the executable. When this variable is set, the emulator does not move save data from the old layout. | `SaveDataStorage.cs`, `SaveDataExports.cs` |
|
||||
| `SHARPEMU_STALL_WATCHDOG_SECONDS` | number (seconds) | Sets the time without import progress before the stall watchdog operates. The default is 20. The watchdog writes a snapshot and stops the process with exit code 4. It does not operate when a guest thread is ready or a known wait is in progress. `0` stops the watchdog. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE` | `1` | Set to `1` to make a guest thread take a free pthread rwlock as the exclusive writer. By default, the thread gets a compatibility writer record and the lock has no writer thread. | `KernelPthreadExtendedCompatExports.cs` |
|
||||
| `SHARPEMU_TEMP0_DIR` | path | Set the host directory for the guest `temp0` mount. The default is `user/temp/<app name>/temp0` next to the executable. When unset, the kernel code sets this variable to the default path. | `AppContentExports.cs`, `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_TRACE_TITLE_SHADER_STATE` | `1` | The SPIR-V translator changes the pixel shader at address 0x0000000500781200 for diagnosis. The first float output then shows the execution state, the wave mask state, and the export state as colors. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
|
||||
## GPU and Vulkan
|
||||
|
||||
These variables change the GPU path or the Vulkan presenter.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_ASYNC_GUEST_READBACK` | `0` | `0` makes a guest thread that reads GPU-written buffer memory wait until the render thread has copied the data back. The default starts the copy on the render thread and lets the guest thread wait for it on a separate readback slot, so the render thread continues. | `GuestBufferCache.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_EXEC_PCS` | list (`pc:register` pairs) | At each given instruction PC, the pixel shader writes the execution flag to the given VGPR register. The value is 1.0 when the flag is set and 0.0 when it is not. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` | hex address | Selects the pixel shader for the image capture by its program address. The default is no capture. Use it with `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC`. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC` | number (instruction PC) | Sets the image instruction whose result the SPIR-V translator copies to four VGPR registers. `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_VGPR_BASE` | number (0 to 252) | Sets the first VGPR register that receives the captured image components. The default is 248. Values above 252 use the default. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` | hex address | Selects the pixel shader for the VGPR capture, execution capture and path marker functions by its program address. The default is no capture. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_DEST_BASE` | number (0 to 252) | Sets the first destination VGPR register for `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES`. The default is 248. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_IGNORE_EXEC` | `1` | Makes the VGPR capture copies occur without the execution flag guard. The default applies the guard. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_PC` | number (instruction PC) | Sets the instruction PC at which the translator copies the source VGPR registers. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_POINTS` | list (`pc:source:destination` triples) | At each given instruction PC, the pixel shader copies the source VGPR register to the destination register. Register numbers must be less than 256. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES` | list (one to four register numbers) | Gives the source VGPR registers that the translator copies at `SHARPEMU_CAPTURE_PIXEL_VGPR_PC`. The copies go to the registers that start at the destination base. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_DETILE` | `1` or `0` | Controls the detile step for tiled surfaces. The default detiles only the trusted swizzle modes. `1` also detiles the approximate modes and `0` uploads all surfaces as raw data. | `GnmTiling.cs` |
|
||||
| `SHARPEMU_DISABLE_FILL_CLEAR` | `1` | Stops the adaptation that treats a transparent premultiplied fill draw as a target overwrite. The default keeps the adaptation on. | `ShaderPipelineCache.Adaptations.cs` |
|
||||
| `SHARPEMU_ENABLE_CHUNKED_DRAWS` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT` | number (attribute location) | The Vulkan presenter reads the number into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_FULLSCREEN_PIPELINE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_PACKED_EXPORT_ONE` | `1` | The SPIR-V translator replaces each packed half-float export component with the constant 1.0. This applies to all shaders. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PACKED_EXPORT_STORE_ONE` | `1` | The SPIR-V translator writes (1.0, 1.0) to the packed half-float pair before the export reads it. This applies only to the pixel shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES` | `1` | Each packed half-float store writes 1.0 when the EXEC mask is active and 0.5 when it is not. This shows the EXEC state as a color. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` | hex address | Sets the shader address filter for the pixel export debug overrides. The value is a hex address, and the `0x` prefix is optional. When unset, the overrides apply to all pixel shaders. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PIXEL_EXPORT_PACK_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. The translator packs the values to half-float and unpacks them again. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_ADDRESS` | hex address | Sets the shader address filter for the two VGPR base overrides. The `0x` prefix is optional. When unset, the filter from `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` applies. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. These values replace the decoded export sources. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_PIXEL_MAGENTA` | `1` | Each selected pixel shader export writes opaque magenta. The control flow, EXEC mask, geometry and raster state stay the same. Use `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` to select one shader. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_COMPARE_4D4` | `1` | The compare instruction at shader PC 0x4D4 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_COMPARE_540` | `1` | The compare instruction at shader PC 0x540 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_EARLY_COLOR` | `1` | The pixel shader at guest address 0x0000000500781200 writes a constant to its first output and returns at once. A float output gets opaque magenta. An integer output gets zero. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_EXPORT_EXEC` | `1` | The translator sets the EXEC mask to true and writes 1 to 64-bit register 126 before each pixel export store. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_SINGLE_MRT` | `1` | The translator declares only the first pixel output for the pixel shader at guest address 0x0000000500781200. It does not declare the other render target outputs. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_FORCE_TITLE_VERTEX_OUTPUTS_ONE` | `1` | Vertex export targets 32 to 35 write the vector (1, 1, 1, 1). This applies only to the vertex shader at guest address 0x0000000500780000. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_GPU_BACKEND` | `vulkan` or `metal` | Selects the guest GPU backend. The default is Vulkan. The value `metal` works only on macOS, and other hosts show a warning and use Vulkan. | `GuestGpu.cs` |
|
||||
| `SHARPEMU_GPU_WAIT_SPIN_US` | number (microseconds) | Sets how long a wait for GPU work polls before it blocks. A short poll saves the wake-up delay when the GPU finishes soon. `0` blocks at once. The default is 150. | `GpuWaitSpin.cs` |
|
||||
| `SHARPEMU_GRAPHICS_SUBGROUPS` | `0` or `1` | Set `1` to use native Vulkan subgroup operations in graphics shaders. Set `0` to use the fallback path. When unset, the presenter uses them only if the subgroup size is 32 and the vertex and fragment stages support subgroups. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
|
||||
| `SHARPEMU_IMAGE_LOOKUP_MEMO` | `0` | `0` turns off the image lookup memo. The default keeps the result of each image lookup that found an image with the same backing, and gives it again for the same request until an image is registered or removed. | `GuestImageCache.Lookup.cs` |
|
||||
| `SHARPEMU_MARK_PIXEL_PCS` | list of `pc:register` pairs | Makes the SPIR-V pixel shader write the float value 1.0 to a shader register when execution reaches the given instruction PC. Use decimal numbers, a register below 256, and a comma between pairs. It has an effect only for the shader address in `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS`. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_MAX_GUEST_WORK_PER_RENDER` | number | Sets the maximum number of guest command stream slices that the Vulkan presenter runs in one render pass. The default is 1024, or 256 on macOS. The value must be more than 0. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.CommandStream.cs` |
|
||||
| `SHARPEMU_RENDERDOC_CAPTURE_TIMEOUT_SECONDS` | number (seconds) | Set the maximum time for a RenderDoc frame capture. The emulator discards a capture that does not end in this time. The range is 1 to 120. The default is 15. | `RenderDocCapture.cs` |
|
||||
| `SHARPEMU_RENDERDOC_DLL` | path | Set the path of the RenderDoc library file. The emulator tries this path first. When unset or when the load fails, it searches the default library name and the known install paths. | `RenderDocCapture.cs` |
|
||||
| `SHARPEMU_RENDERDOC_WAIT` | number (seconds) or `enter` | Set to make the emulator wait before it starts Vulkan, so that you can attach RenderDoc. `enter` waits until you press Enter. A number waits that many seconds, from 1 to 300. Other text waits 15 seconds. | `VulkanVideoPresenter.Device.Setup.cs` |
|
||||
| `SHARPEMU_RENDER_WORK_BUDGET_MS` | number (milliseconds) | Set the maximum time that one render call uses for queued guest work in the Vulkan presenter. Work that remains stays in the queue for the next frame. `0` removes the limit. The default is 12 on macOS and 0 on other hosts. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
|
||||
| `SHARPEMU_RESOURCE_PREFETCH` | `0` | `0` makes the shader resource materialization read each guest word one at a time. The default reads adjacent words of descriptors and the flattened resource table in one read of resident guest memory. A range that the GPU may still own is read word by word as before. | `ShaderProgramCache.cs`, `RawReadPrefetch.cs` |
|
||||
| `SHARPEMU_SHADER_MAX_STEPS` | number | Set the maximum number of dispatcher loop steps in each translated shader invocation. The limit makes sure that an incorrect loop stops. `0` removes the limit. The default is 100000. | `Gen5SpirvTranslator.cs`, `Gen5MslTranslator.cs` |
|
||||
| `SHARPEMU_SHADER_PREWARM` | `0` | `0` turns off the shader prewarm list. By default, each compute program that a game compiles is added at once to `shader-prewarm.bin` in `user/pipeline_cache/<title id>/`, so a stopped game keeps the list. At the next launch after a change of the shader translator or of the GPU driver, background threads translate the listed programs again and create their pipelines, so that the driver compiles them before the game asks for them. The `shader-prewarm.stamp` file names the build and driver that last did this; with the same build and driver, the launch does no prewarm work. The log shows `Shader prewarm done` and `Shader prewarm hits`. On Windows it replaces the cache file by default; on Linux it works beside the cache file. It has no effect with the per-shader cache files of `SHARPEMU_VK_PIPELINE_CACHE_SHARDS`. | `VulkanVideoPresenter.ShaderPrewarm.cs`, `ShaderPrewarmList.cs`, `ShaderProgramCache.cs` |
|
||||
| `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` |
|
||||
| `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` |
|
||||
| `SHARPEMU_SRT_NATIVE` | `0` | `0` turns off the compiled shader resource (SRT) evaluator. By default, the resource plan of each shader is compiled once to .NET code (`DynamicMethod`) on a low-priority background thread named `SRT compiler`; until it is ready, the plan is evaluated by the interpreter, so the first use of a new shader does not wait for the compilation. With `0`, every plan always uses the interpreter. The compiled path is also off when the runtime does not support dynamic code, for example in a Native AOT build. | `CompiledResourceEvaluator.cs`, `ShaderResourcePlan.cs`, `RuntimeValueEvaluator.cs` |
|
||||
| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` |
|
||||
| `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` |
|
||||
| `SHARPEMU_VK_PIPELINE_CACHE_PATH` | path | Sets the path of the Vulkan pipeline cache file. The path can contain environment variables. Default is `user/pipeline_cache/<title id>/` below the application directory. The shader prewarm list goes to the same directory. With the per-shader cache, the files go to a `.shards` directory next to this path. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` |
|
||||
| `SHARPEMU_VK_PIPELINE_CACHE` | `0`, `1` or `file` | Selects the Vulkan pipeline cache file, which keeps the driver's compiled pipelines. `1` or `file` saves and loads one cache file. `0` keeps the cache in memory only on every system. The default is memory only on Windows: the GPU driver keeps its own shader cache on disk, and the shader prewarm list (`SHARPEMU_SHADER_PREWARM`) rebuilds it after an update. Linux keeps a cache file by default, and macOS keeps per-shader files. | `VulkanVideoPresenter.Device.Setup.cs` |
|
||||
| `SHARPEMU_VK_PIPELINE_CACHE_SHARDS` | `1` or `0` | `1` keeps one Vulkan pipeline cache file for each compute shader and for each vertex and pixel shader pair, in `vulkan-pipeline-cache.bin.shards`, also on Windows. `0` keeps the whole cache in one file. The default is `1` on macOS and `0` on other systems: NVIDIA adds about 2 MB of driver data to every cache file, so a file for each shader uses much more disk. A shard file loads when its pipeline is first created, and a save writes only the files that got new pipelines. The shard mode removes the old single file when `SHARPEMU_VK_PIPELINE_CACHE_PATH` is not set. A cache file that grows over 256 MiB is emptied, so the next launch starts it again. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.PipelineCacheShards.cs` |
|
||||
| `SHARPEMU_VK_PIPELINE_CACHE_LEGACY_SHARDS` | `1` | `1` names the per-shader cache files by the guest shader hash. The default names them by a hash of the translated SPIR-V, so a translator change does not load records that no longer match. | `VulkanVideoPresenter.PipelineCacheShards.cs` |
|
||||
|
||||
## Metal
|
||||
|
||||
These variables apply to the Metal backend.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_GPU_DETILE` | `0` | The Metal backend decodes supported tiled texture data on the GPU by default. Set `0` to decode all tiled textures on the CPU. | `MetalTextureSnapshots.cs`, `GuestGpuTypes.cs` |
|
||||
| `SHARPEMU_LOG_GPU_DETILE` | `1` | Writes each texture tile mode and each GPU detile decision one time to stderr. It applies to the Metal texture path only. The emulator reads the value one time at start. | `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_METAL_CAP_DRAWABLE` | `1` | Limits the long edge of the Metal drawable to 1920 pixels and keeps the aspect ratio. The default is off, so the drawable uses the full window pixel size. | `MetalVideoPresenter.cs` |
|
||||
| `SHARPEMU_METAL_DBG` | `solid` or `uv` | Replaces the Metal present fragment shader with a test shader. `solid` shows a green frame. `uv` shows the value of vertex attribute 0. All other values use the normal present shader. | `MetalVideoPresenter.cs` |
|
||||
| `SHARPEMU_METAL_FULL_DRAWABLE` | `1` | Cancels `SHARPEMU_METAL_CAP_DRAWABLE=1`, so the Metal drawable uses the full window pixel size. It has no effect when the cap is off. | `MetalVideoPresenter.cs` |
|
||||
| `SHARPEMU_NO_TEXTURE_SKIP` | `1` | Stops the reuse of cached texture snapshots in the Metal backend, so each texture is copied again. Use it to make sure that a snapshot is not stale. The default is off. | `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_REUSE_GUEST_TEXTURE_SNAPSHOTS` | `0` | The default is on. The Metal backend uses a guest texture snapshot again when the snapshot key is the same. Set to `0` to make a new snapshot each time. | `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_TEXTURE_DUMP_DIR` | path | Set a directory to make the Metal backend write raw texture source bytes to `.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_TEXTURE_LINEAR_DUMP_DIR` | path | Set a directory to make the Metal backend write the linear texture bytes to `.linear.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_TRACE_STORAGE_IMAGE_INIT_ADDRESS` | hex address | The Metal backend writes an `agc.storage_initial_data` trace line when it makes the texture at this guest address. The line shows the upload state and the read result of the initial data. | `MetalTextureSnapshots.cs` |
|
||||
|
||||
## Audio and video
|
||||
|
||||
These variables apply to audio output and video playback.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_ALSA_DEVICE` | text (ALSA device name) | Sets the ALSA playback device on Linux. The default is `default`. | `PosixAlsaAudioStream.cs` |
|
||||
| `SHARPEMU_AUDIO_LATENCY_MS` | number (milliseconds) | Sets the target depth of the SDL audio queue, which is the playback latency. The default is 60. Values of zero or less use the default. | `SdlHostAudio.cs` |
|
||||
| `SHARPEMU_BINK_MODE` | text (`native`, `ffmpeg`, `dummy`, `skip`, `guest`) | Selects how the emulator handles Bink movies. `native` or `ffmpeg` decodes on the host and is the default. `dummy` shows a placeholder frame, `skip` skips the movie, and `guest` lets the guest decode. | `HostMovieBridge.cs` |
|
||||
| `SHARPEMU_LOG_MOVIE_SYNC` | `1` | Records the difference between the movie clock and the guest audio position during movie playback. The emulator reads the value one time at start. | `MediaFramePlayback.cs` |
|
||||
| `SHARPEMU_MOVIE_CLOCK` | `wall` | Sets the time base for host-decoded movie playback. By default, playback follows the guest audio clock and uses the wall clock when no guest audio flows. `wall` makes playback always use the wall clock. | `MediaFramePlayback.cs` |
|
||||
| `SHARPEMU_TRACE_AVPLAYER_IMAGES` | `1` | Set to `1` to write `[AVPLAYER][TRACE]` lines for video buffer addresses and the first 16 video frame payloads. It also marks the video buffer ranges for image traces in other components. The default is off. | `AvPlayerExports.cs` |
|
||||
|
||||
## Input
|
||||
|
||||
These variables apply to the controller and input libraries.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_AUTO_CROSS` | list (seconds, for example `40,52,64`) | Presses the Cross button for 0.4 seconds at each time in the list. The times are seconds from process start. The default is no automatic press. | `PadExports.cs` |
|
||||
| `SHARPEMU_BTHID_CB_FAIL` | text (`nf` or `ni`) | Makes `sceBluetoothHidRegisterCallback` return an error. `nf` returns NOT_FOUND and `ni` returns NOT_IMPLEMENTED. The default returns the usual result. | `BluetoothHidExports.cs` |
|
||||
| `SHARPEMU_BTHID_EVENT_CODE` | number (decimal or `0x` hex) | Sets the event code of the synthetic Bluetooth HID callback event. The default is 0. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
|
||||
| `SHARPEMU_BTHID_EVENT_SIZE` | number (bytes, decimal or `0x` hex) | Sets the size of the zeroed event structure for the synthetic Bluetooth HID callback. The default is 256. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
|
||||
| `SHARPEMU_BTHID_FIRE_CALLBACK` | `1` | Calls the registered Bluetooth HID guest callback one time with a zeroed event structure. The call occurs at the first `sceBluetoothHidRegisterDevice`. The default is off. | `BluetoothHidExports.cs` |
|
||||
|
||||
## Memory and CPU
|
||||
|
||||
These variables apply to guest memory and guest code execution.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY` | `1` | Stops the Windows fault recovery for a guest allocator that reads an empty pool node. The default recovers the fault and continues at the allocator fallback code. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_DISABLE_MITIGATION_RELAUNCH` | `1` | Stops the Windows relaunch of the emulator as a child process with Control Flow Guard and CET shadow stacks off. The default starts the child process. | `Program.cs`, `SelfLoader.cs` |
|
||||
| `SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS` | `1` | Stops the use of native worker threads for guest thread execution on Windows. The default uses the native workers. | `DirectExecutionBackend.NativeWorker.cs` |
|
||||
| `SHARPEMU_DISABLE_POSIX_SIGNALS` | `1` | Does not install the POSIX signal handlers on Linux and macOS. The emulator then cannot recover guest faults. | `DirectExecutionBackend.PosixSignals.cs` |
|
||||
| `SHARPEMU_DISABLE_RAW_HANDLER` | `1` | Does not install the raw vectored exception handler on Windows. On POSIX hosts it stops the raw sentinel recovery. The default keeps the two functions on. | `DirectExecutionBackend.Exceptions.cs`, `DirectExecutionBackend.PosixSignals.cs` |
|
||||
| `SHARPEMU_IGNORE_INT41` | `0` or `false` | The fault handler skips guest `int 0x41` instructions and continues at the next instruction by default. Set `0` or `false` to stop this recovery. The emulator shows a warning for the first 16 traps and then for each 65536th trap. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_IGNORE_STACK_CHK` | `1` | Recovers from a guest stack-protector failure call (NID `Ou3iL1abvng`) when a UD2 instruction follows the call. The emulator sets the return address to the function epilogue 20 bytes before. It does this only when the code bytes match the known pattern. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LAZY_RESERVE_PRIME_MB` | number | Sets the number of megabytes that the emulator commits at the start of each reserve-only memory region. The default is 64 and the maximum is 4096. The value 0 turns the initial commit off. | `PhysicalVirtualMemory.cs` |
|
||||
| `SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT` | number | Sets the maximum number of native guest worker runs that execute at the same time. The default is 2. The emulator limits the value to the range 1 to 64. | `DirectExecutionBackend.NativeWorker.cs`, `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_GUEST_AFFINITY` | `1` | Applies the guest thread affinity to the host threads. The default leaves host threads unpinned, because pinning let spinning guest workers starve the render thread. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_RENDER_CORE` | `0` | `0` turns off the render core reservation. On Windows hosts with 8 to 64 logical processors, the default gives the render thread the last physical core of the fastest efficiency class with a high priority, and keeps guest threads off that core. | `HostLaneReservation.cs`, `VulkanVideoPresenter.Present.cs`, `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_RESERVED_HOST_LANES` | number | Set the number of host logical processors that guest thread affinity does not use. The emulator keeps these processors for its own threads. The default is three eighths of the processor count, with a minimum of 2. It has an effect only with `SHARPEMU_GUEST_AFFINITY=1`. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_TSC_FREQ_HZ` | number (Hz, minimum 1000000) | Sets the TSC frequency that the kernel library reports to the guest. When unset, the emulator uses the calibrated RDTSC frequency, then the CPUID frequency, then the host stopwatch frequency. | `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_WATCH_BULK_DEST_HI` | hex number | Limits the `SHARPEMU_WATCH_BULK_TORN` scan to writes whose upper 32 address bits equal this value. When unset, the scan uses the direct-memory address band. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_BULK_TORN` | `1` | Examines each aligned 8-byte value in managed bulk writes to guest memory. It reports torn values and shifted pointer values with a stack trace. It writes a maximum of 64 reports for each kind. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_POOL_HEADER` | `1` | Monitors the header slot at offset 0x40 of each pool mapping with size 0x10000 and protection 0xF2. It reports each managed write to a monitored slot with a stack trace. It monitors a maximum of 64 slots. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_VALUE1` | `1` | Reports managed writes of 1 to 8 bytes that have the value 1 and go to the direct-memory address band. Each report has a stack trace. It writes a maximum of 128 reports. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_VALUE_PATTERN` | `1` | Reports each managed 8-byte write whose low half is 1 and whose high half is between 1 and 0xFFFF. Each report has a stack trace. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_WRITE` | hex address | Monitors the 8 bytes at this guest address. It reports each managed write that touches these bytes with the address, the length, the first value, and a stack trace. Default is off. | `GuestWriteWatch.cs` |
|
||||
|
||||
## Loader
|
||||
|
||||
These variables apply to the program loader and the module system.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_AMPR_INDEX_CACHE` | path (directory) | Sets the directory for the AMPR file index cache. The default is `SharpEmu/ampr-index` in the local application data folder. | `AmprFileRegistry.cs` |
|
||||
| `SHARPEMU_AMPR_REINDEX` | `1` | Ignores the AMPR index cache on disk and scans the application file tree again. Use it after manual changes to the application files. | `AmprFileRegistry.cs` |
|
||||
| `SHARPEMU_APP0_DIR` | path (directory) | Sets the host directory for the guest `/app0` mount. The default is the directory of the loaded executable. The directory name also identifies the per-application data folders. | `SharpEmuRuntime.cs`, `KernelMemoryCompatExports.cs`, `AppContentExports.cs` |
|
||||
| `SHARPEMU_DEVLOG_APP_DIR` | path (directory) | Sets the host directory for the guest devlog application path. The default is `user/game_logs/<title id>/devlog/app` below the emulator directory. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_DISABLE_RED_ZONE_PATCH` | `1` | Stops the load-time patch that protects the guest stack red zone on Windows and macOS. The default applies the patch. The vector store split for Rosetta stays on when it is necessary. | `GuestRedZonePatcher.cs` |
|
||||
| `SHARPEMU_DOWNLOAD0_DIR` | path (directory) | Sets the host directory for the guest `/download0` mount. The default is a `download0` folder in the per-application writable directory. The emulator sets the variable to the default when it is not set. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_GUEST_ARGS` | text | Adds arguments to the guest process entry frame after the image name. White space divides the arguments. The emulator uses only the first two. | `CpuDispatcher.cs` |
|
||||
| `SHARPEMU_HOSTAPP_DIR` | path | Sets the host directory for the guest `/hostapp` mount. The default is `user/game_logs/<title id>/hostapp` below the emulator directory. The emulator makes the directory if it is absent. | `KernelMemoryCompatExports.cs`, `KernelGameLogPathTests.cs` |
|
||||
| `SHARPEMU_LLE_LIBC_ALL` | `1` | Sends all libc imports to the guest libc module and not to the HLE handlers. `SHARPEMU_DISABLE_LLE_LIBC=1` and `SHARPEMU_LLE_LIBC_SAFE_ONLY=off` have priority. Default is off. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_LLE_LIBC_SAFE_ONLY` | `1`, `0`, `off`, `false` or `none` | Controls which libc imports use the guest libc module. Unset or `1` uses guest code only for exports on the safe list. `0` uses guest code for all libc exports, and `off`, `false` or `none` uses HLE for all of them. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_PRELOAD_ALL_SCE_MODULES` | `1` | Set to `1` to preload all system modules. When unset, the runtime does not preload the modules in its internal skip list. | `SharpEmuRuntime.cs` |
|
||||
|
||||
## Debugger
|
||||
|
||||
These variables apply to the debug server.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_SENTINEL_PROBE` | `1` | Set to `1` to call the native entry stub with the unresolved sentinel address 0xFFFE before the guest entry. The emulator writes a log line before and after the call. The default is off. | `DirectExecutionBackend.cs`, `Gen5NativeReturnSmokeTests.cs` |
|
||||
|
||||
## Log channels
|
||||
|
||||
Each variable adds one group of messages to the log. The default is off.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_LOG_ACM` | `1` | Writes `acm.` trace lines from the Acm library handlers to stderr. Default is off. | `AcmExports.cs` |
|
||||
| `SHARPEMU_LOG_AGC_SHADER` | `1` | Writes only the AGC shader trace lines and the Vulkan shader trace lines. `SHARPEMU_LOG_AGC=1` also turns this on. Default is off. | `AgcExports.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_LOG_AGC` | `1` | Starts the full AGC trace: command submissions, shader creation, draws and dispatches. It also turns on the AGC shader trace and the Vulkan shader trace. Default is off. | `AgcExports.cs`, `RenderExecutor.Trace.cs`, `AgcExports.ShaderState.cs` |
|
||||
| `SHARPEMU_LOG_AJM` | `1` | Writes `ajm.` trace lines to stderr: context initialization, codec registration, split buffer descriptors and ATRAC9 decode state. Default is off. | `AjmExports.cs`, `Atrac9DecodeState.cs` |
|
||||
| `SHARPEMU_LOG_ALLOC_IMPORTS` | `1` | Writes more trace lines for `sceKernelGetGPI` (the returned mask) and for the PRT aperture set call (the raw register values). Default is off. | `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_ALL_IMPORTS` | `1` | Writes a debug line for each import stub during setup. The lines show if the import uses a direct bridge, a trampoline, HLE or a runtime symbol. Default is off. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_LOG_AMPR_READS` | `1` | Writes only the AMPR file read trace lines and the pak directory tracker lines. `SHARPEMU_LOG_AMPR=1` also turns this on. Default is off. | `AmprExports.cs`, `PakDirectoryTracker.cs` |
|
||||
| `SHARPEMU_LOG_AMPR` | `1` | Writes trace lines for AMPR command buffers, APR submissions and waits, and the pak directory tracker. It also turns on the AMPR read trace. Default is off. | `AmprExports.cs`, `KernelAprCompatExports.cs`, `PakDirectoryTracker.cs` |
|
||||
| `SHARPEMU_LOG_APP_CONTENT` | `1` | Writes an `app_content` trace line to stderr for each AppContent call. The default is off. | `AppContentExports.cs` |
|
||||
| `SHARPEMU_LOG_AUDIO_OUT2` | `1` | Writes an `audio_out2` trace line to stderr for each AudioOut2 operation. The default is off. | `AudioOut2Exports.cs` |
|
||||
| `SHARPEMU_LOG_AUDIO_OUT` | `1` | Records `sceAudioOutOutput` calls and the peak amplitude of the submitted samples. It writes the first 8 calls and then every 200th call. The emulator reads the value one time at start. | `AudioOutExports.cs` |
|
||||
| `SHARPEMU_LOG_BOOTSTRAP` | `1` | Writes a trace line for each call through the bootstrap bridge import. The line shows the handle, the symbol name and the output address. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_CONTEXT` | `1` | Writes `guest_context` trace lines to stderr. Each import dispatch shows the NID, the return address, the managed thread, the guest thread and the fiber. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_DATA_REBIND` | `1` | Writes one `[RUNTIME]` line for each imported data relocation. The line shows if the relocation is rebound, unresolved or write-failed. | `SharpEmuRuntime.cs` |
|
||||
| `SHARPEMU_LOG_DISCMAP` | `1` | Writes a trace line for each DiscMap call. The line shows the export name, the path, the offset and the size. | `DiscMapExports.cs` |
|
||||
| `SHARPEMU_LOG_DISCORD` | `1` | Writes `[DISCORD]` messages from the rich presence client of the GUI to stderr. The default is off. | `DiscordRichPresence.cs` |
|
||||
| `SHARPEMU_LOG_DLSYM` | `1` | Writes a trace line for each successful `sceKernelDlsym` call. The line shows the module handle, the symbol name and the resolved address. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_EQUEUE` | `1` | Writes trace lines for kernel event queue operations. The emulator reads the value one time at start. | `KernelEventQueueCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_EVENT_FLAG` | `1` | Writes trace lines for kernel event flag operations. The emulator reads the value one time at start. | `KernelEventFlagCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS` | `1` | Shows import error results that the emulator knows as usual, for example file-not-found probes, timeouts and busy trylocks. It shows the first 8 results for each NID and result, and then every 10000th. The default hides these results. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_FIBER` | `1` | Writes `fiber` trace lines for the Fiber library calls. It also writes a line for each fiber context transfer in the CPU backend. | `FiberExports.cs`, `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_FILE` | path | Writes all log entries to this file with timestamps and appends to an existing file. The file receives every log level. `SHARPEMU_LOG_LEVEL` then limits only the console output. | `SharpEmuLog.cs` |
|
||||
| `SHARPEMU_LOG_FMOD` | `1` | Writes a trace line for the FMOD `system_set_output` compatibility call. The line shows the system address, the output type and the call count. | `FmodCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_GUARDS` | `1` | Writes a trace line for each C++ static guard operation. The line shows the guard address, the guard word, the state flags and the owner thread. | `CxxAbiExports.cs` |
|
||||
| `SHARPEMU_LOG_GUEST_EXCEPTIONS` | `1` | Writes `guest_exception` trace lines when the emulator raises and delivers a guest exception. The lines show the target thread and the exception type. | `DirectExecutionBackend.cs`, `KernelExceptionCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_GUEST_THREADS` | `1` | Writes guest thread scheduler messages to stderr. The messages show thread state changes, yields, wakes, exits, nested callbacks and affinity failures. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_GUEST_THREAD_SNAPSHOTS` | `1` | Writes one snapshot line for each guest thread every second. The line shows the state, the import count, the last NID, the last return address and the block reason. The ready-dispatch thread then wakes every second. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_LOG_HTTP2` | `1` | Writes an `http2` trace line for each Http2 library call. The line shows the operation, the id and four arguments. | `Http2Exports.cs` |
|
||||
| `SHARPEMU_LOG_HTTP` | `1` | Writes an `http` trace line for each Http library call. The line shows the operation, the id and four arguments. | `HttpExports.cs` |
|
||||
| `SHARPEMU_LOG_IO_FILTER` | text | Limits the `SHARPEMU_LOG_IO` output to paths that contain this text. The comparison ignores case. When unset, the emulator shows all paths. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_JSON` | `1` | Writes a `json` trace line for each Json library call. Text values show a maximum of 128 characters. | `JsonExports.cs` |
|
||||
| `SHARPEMU_LOG_LAZY_COMMIT` | `1` | Writes a trace line for every lazy memory commit fault. The default shows only the first 16 faults and then every 256th fault. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_LEVEL` | text | Sets the minimum log level. The values are `Trace`, `Debug`, `Info`, `Warning` or `Warn`, `Error`, `Critical` or `Fatal`, and `None`. The default is `Info`, and the comparison ignores case. | `SharpEmuLog.cs` |
|
||||
| `SHARPEMU_LOG_LIBC_ALLOC` | `1` | Writes a trace line for each guest libc allocation call. The line shows the size, the alignment, the result address and the return address of the caller. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_NET` | `1` | Writes `net` trace lines for Net library calls and debug lines for kernel socket operations. It also writes each payload that the guest sends on a connected socket to stdout as text. | `NetExports.cs`, `KernelSocketCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_NGS2` | `1` | Writes trace lines for Ngs2 audio library calls. The default is off. | `Ngs2Exports.cs` |
|
||||
| `SHARPEMU_LOG_NO_COLOR` | `1` | Stops the use of colors in the console log. The values `1`, `true`, `yes` and `on` are accepted. The console log has no colors when the output is redirected. | `SharpEmuLog.cs` |
|
||||
| `SHARPEMU_LOG_NP_WEB_API2` | `1` | Writes an `npwebapi2` trace line for each NpWebApi2 call. The line shows the operation, the id, the first argument and the initialized state. | `NpWebApi2Exports.cs` |
|
||||
| `SHARPEMU_LOG_OPEN` | `1` | Writes trace lines for guest file open operations to stderr. The default is off. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_PLAYGO` | `1` | Writes `playgo` trace lines for PlayGo calls. For single-entry locus queries it shows the first 32 calls and then every 1000th call. | `PlayGoExports.cs` |
|
||||
| `SHARPEMU_LOG_POINTER_WINDOW_SIZE` | hex number | Sets the byte size of each memory window that `SHARPEMU_LOG_POINTER_WINDOWS` shows after a guest fault. The value is hexadecimal, with or without `0x`. The default is 0x80. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_POSIX_SIGNALS` | `1` | Writes a trace line for every POSIX signal that the fault handler receives. When unset, the log shows only the first 16 signals and then every 1024th signal. | `DirectExecutionBackend.PosixSignals.cs` |
|
||||
| `SHARPEMU_LOG_PROC_PARAM_PTRS` | `1` | Shows the pointer fields in the process parameter block. This dump is part of the `SHARPEMU_LOG_PROC_PARAM` trace, so set that variable also. | `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_PROC_PARAM` | `1` | Shows the address and the contents of the process parameter block of the guest program. The default is off. | `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_PS5_USER_SLOTS` | `1` | Adds four 0x60-byte memory dumps to the IL2CPP exception diagnostic. The dumps start at the fixed guest address 0x801A73110 and have a step of 0x51C8 bytes. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_PSML` | `1` | Writes `psml.` trace lines for the PSML library calls to stderr. The default is off. | `PsmlExports.cs` |
|
||||
| `SHARPEMU_LOG_PTHREADS` | `1` | Writes trace lines for the pthread calls of the guest. It also starts the condition variable trace of `SHARPEMU_LOG_PTHREAD_CONDS`. The default is off. | `KernelPthreadCompatExports.cs`, `KernelExports.cs` |
|
||||
| `SHARPEMU_LOG_PTHREAD_CONDS` | `1` | Writes a `pthread_cond_` trace line for each condition variable operation. The line shows the waiter count, the signal epoch, and the result. | `KernelPthreadCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_PTHREAD_FASTPATH` | `1` | Writes trace lines for the mutex fast path. It shows the first 16 fast-path unlocks with the mutex object words. It shows one busy result for each mutex address. | `KernelPthreadCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_PTHREAD_MUTEX_FILTER` | list of hex addresses | Limits the mutex trace to the given mutex addresses. Use a comma, a semicolon, or a space between the hexadecimal addresses. The listed mutexes are in the trace even if `SHARPEMU_LOG_PTHREADS` is not set. | `KernelPthreadCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_REFSCAN_ADDRS` | list of hex addresses | After a guest fault, scans executable guest memory from 0x800000000 to 0x810000000 for instructions that refer to the given addresses. It shows a maximum of 24 hits for each address. Use a comma between the hexadecimal addresses. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_REGISTER_WINDOWS` | `1` | After a guest fault, shows a 0x80-byte memory window at each general register value that is 0x10000 or more. The default is off. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_SAVEDATA` | `1` | Writes `savedata.` and `save_data_dialog.` trace lines for the save data calls to stderr. The default is off. | `SaveDataExports.cs`, `SaveDataDialogExports.cs` |
|
||||
| `SHARPEMU_LOG_SEMA` | `1` | Writes trace lines for the kernel semaphore operations. The emulator reads the value one time at start. The default is off. | `KernelSemaphoreCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_SHARE` | `1` | Writes `share.` trace lines for the Share library calls to stderr. The default is off. | `ShareExports.cs` |
|
||||
| `SHARPEMU_LOG_SSL` | `1` | Writes an `ssl.` trace line for each SSL library call. The line shows the operation, the identifier, and the first argument. | `SslExports.cs` |
|
||||
| `SHARPEMU_LOG_STACK_CHK` | `1` | Writes diagnostic lines when the guest calls the stack check failure import. The lines show the return address, the guard registers, and the stack slots from rbp-0x10 to rbp-0x80. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_STDIO` | `1` | Writes trace lines for the libc stdio calls of the guest. The emulator reads the value one time at start. The default is off. | `LibcStdioExports.cs` |
|
||||
| `SHARPEMU_LOG_STRLEN_BURSTS` | `1` | Writes a warning when the guest calls `strlen` 24 times in sequence. The warning shows the return address and the last five different imports before the burst. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_STRLEN` | `1` | Keeps `strlen` calls in the import trace. When unset, the import trace does not show `strlen` calls. It also starts the burst detection of `SHARPEMU_LOG_STRLEN_BURSTS`. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_THREAD_MODE` | `1` | Writes `[THREADMODE]` lines when a native guest worker thread is created, starts a guest run, and stops a guest run. Each line shows the host thread identifiers. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.NativeWorker.cs` |
|
||||
| `SHARPEMU_LOG_USER_SERVICE` | `1` | Writes trace lines for the user service library calls. Each line includes the guest return address of the call. | `UserServiceExports.cs` |
|
||||
| `SHARPEMU_LOG_USLEEP` | `1` | Writes trace lines for `usleep` calls: the first 32 calls and then every 10000th call. It also stops the native inline `usleep` stub and the leaf import path, so each call goes through the traced handler. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_VIDEOOUT_FPS` | `1` | Writes periodic frame rate lines with flip, present, draw, pipeline, and buffer pool counts. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the same output. | `VideoOutExports.cs` |
|
||||
| `SHARPEMU_LOG_VIDEOOUT` | `1` | Writes `videoout.` trace lines for the video output calls, such as flip events and frame dumps. The default is off. | `VideoOutExports.cs` |
|
||||
| `SHARPEMU_LOG_VK_RESOURCES` | `1` | Writes a `vk.global_buffer` trace line one time for each new guest buffer that the Vulkan presenter binds. The line shows the base address and the size. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.GuestBuffers.cs` |
|
||||
| `SHARPEMU_LOG_VMEM` | `1` | Writes debug lines for guest virtual memory operations and `[HOSTMEM]` lines for host memory operations. The default is off. | `PhysicalVirtualMemory.cs`, `HostMemory.cs`, `PosixHostMemory.cs` |
|
||||
| `SHARPEMU_LOG_WIDE_PRINTF_ARGS` | `1` | Writes trace lines for the arguments of printf format calls, such as string arguments. Use `SHARPEMU_LOG_WIDE_PRINTF_FILTER` to limit the output. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_WIDE_PRINTF_FILTER` | text | Limits the `SHARPEMU_LOG_WIDE_PRINTF_ARGS` trace to format strings that contain this text. The comparison is case-sensitive. When unset, the trace includes all format strings. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_WIDE_PRINTF` | `1` | Writes a trace line for each wide-character printf call. The line shows the first 160 characters of the format and of the result. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_LOG_WIDE` | `1` | Shows the first 32 bytes at the string address for each `wcslen` call. The default is off. | `KernelMemoryCompatExports.cs` |
|
||||
| `SHARPEMU_PERIODIC_SNAPSHOT_SECONDS` | number (seconds) | Set a number of seconds to write the stall watchdog snapshot at that interval. The default is 0, which writes no periodic snapshot. The stall watchdog must be on. | `DirectExecutionBackend.cs` |
|
||||
|
||||
## Traces
|
||||
|
||||
Each variable records a detailed sequence of events. Traces can be large.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_LOG_IMPORT_FILTER` | text | Writes a trace line for each import that contains this text. The emulator compares the text with the library name, the export name and the NID, and ignores case. When unset, the filter selects no imports. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_IMPORT_FRAMES` | `1` | Adds the guest frame chain to each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_IMPORT_PERIODIC` | `1` | Starts the periodic import trace. It shows the early import bands and every 100000th import dispatch. The default is off because the stderr output decreases the speed. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_IMPORT_RECENT` | `1` | Writes the recent import history after each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_PROBE_IMPORT_RET_ADDRESS` | hex address | Set a guest return address. When an import call returns to this address, the emulator writes an `import-return-address-probe` line with the thread, NID, stack and frame values. The limit is 2048 lines. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_PROBE_IMPORT_RET` | text (NID or `*`) | Set an import NID, or `*` for all imports. For the first 8 applicable import calls, the emulator writes the bytes and the disassembly before the return address. The default is off. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.Diagnostics.cs` |
|
||||
| `SHARPEMU_RTC_PROBE_RANGE` | hex address range (`start-end`) | Set a range of guest addresses. When a caller of `sceRtcGetCurrentTick` returns into this range, the emulator writes 0x100 bytes of code near that return address. It does this one time only. | `RtcExports.cs` |
|
||||
| `SHARPEMU_TRACE_AGC_EQ_ACCESSORS` | `1` | Set to `1` to write an `agc.eq_accessor` line for calls to the AGC event accessor functions. Each line shows the accessor, the event address, the result and the event bytes. After 64 lines, the emulator writes only at power-of-two counts. | `AgcExports.Events.cs` |
|
||||
| `SHARPEMU_TRACE_DCC_WRITERS` | `1` | Writes a `[GPU][DCC_WRITER]` line with the shader program and a call stack for each GPU buffer write that overlaps DCC metadata. It also writes `[GPU][BOUNDED_FILL_REFUSED]` lines when a fill shader is not changed into a buffer fill. Each kind writes a maximum of 64 lines. | `DccWriterTrace.cs` |
|
||||
| `SHARPEMU_TRACE_FOCUSED_CONTINUATION` | `1` | Set to `1` to write `focused_continuation` lines for guest thread continuations. The emulator writes a line only when the stack pointer is in the range 0x6FFFAC000000 to 0x6FFFAC200000. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_TRACE_FRAME_PACKETS` | `1` | Writes a `[FRAMEPKT]` line at a flip with the draw count and the dispatch count of the frame. It shows the first 8 flips, each 60th flip, and each flip that has no draws. Default is off. | `AgcExports.CommandStream.cs` |
|
||||
| `SHARPEMU_TRACE_GPU_MEMORY_ADDRESS` | hex address or `auto` | Writes `[GPU][MEMORY_TRACE]` lines for GPU memory events that touch the page of the given address. The value `auto` selects the page of the first device-address fault. Default is off. | `GuestGpuMemoryHook.cs` |
|
||||
| `SHARPEMU_TRACE_GPU_READS` | `1` | Writes a `[GPU][SYNC_READ]` line with the shader hash, the stage and the address when a shader resource read must wait for GPU-written memory. It writes one line for each shader and page, and a maximum of 64 lines. | `GpuReadTrace.cs`, `VulkanVideoPresenter.Pipelines.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_IMAGES` | `1`, `present` or `every:N[@M]` | `1` and `present` write Vulkan present trace lines (`vk.present_taken`, `vk.present_dropped`, `vk.present_sample`) and up to 64 Metal `agc.texture_fallback` lines. The code reads the `every:N[@M]` form, but no code uses the result. Default is off. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.Present.cs`, `MetalTextureSnapshots.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that contains a listed address. `*` selects all ranges. The Vulkan presenter also reads the list, but no code uses that filter. | `GuestImageWriteTracker.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT` | text (Vulkan format name) | Effect not clear from the code. The Vulkan presenter keeps the name for an image filter that compares the image format, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image height, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image width, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME` | list of source names or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that has a listed source name. Use a comma or a semicolon between names. `*` selects all sources. | `GuestImageWriteTracker.cs` |
|
||||
| `SHARPEMU_TRACE_GUEST_WRITES` | list of hex addresses or `*` | Effect not clear from the code. The Vulkan presenter reads the list in an address filter for guest image writes, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_PACKED_EXPORT` | `1` | The SPIR-V translator writes `[AGC][PACKED-EXPORT]` and `[AGC][TITLE-IR]` lines for packed export analysis. It does this only for the shader program at address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | Writes a `vk.present_sample` trace line when the presenter shows a guest image at a listed address. It records the first presentation of each address unless `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` is set. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
|
||||
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` | number | Selects which presentation of an address in `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` the presenter records. Default is 0, which selects the first presentation. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_TRACE_TITLE_INTERFACE` | `1` | The SPIR-V translator writes `[AGC][TITLE-INTERFACE]` lines with the export and interpolation instructions of a shader. It does this only for the programs at addresses 0x0000000500780000 and 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
|
||||
| `SHARPEMU_TRACE_WRITER_ADDRESS` | hex address | Writes a `[GPU][TARGET_WRITER]` line with the shader program and a call stack for each GPU buffer write that covers this guest address. It writes a maximum of 64 lines. Default is off. | `DccWriterTrace.cs` |
|
||||
|
||||
## Data dumps
|
||||
|
||||
Each variable writes data to files.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_DUMP_FAULT_STACK_WINDOW` | `1` | Writes the full stack window from RSP-0x300 to RSP+0x100 to the error output when a guest fault report occurs. The default writes only the short stack list. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT` | path | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_DUMP_SPIRV_ADDRESS` | hex address | Limits the shader dump of `SHARPEMU_DUMP_SPIRV=1` to the shader at this guest address. The default writes all shaders. | `ShaderProgramCache.cs` |
|
||||
| `SHARPEMU_DUMP_VIDEOOUT` | `1` | Writes submitted VideoOut frame buffers as BMP files with a text metadata file. It writes a maximum of 8 frames and skips a frame that did not change. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
|
||||
| `SHARPEMU_GUEST_IMAGE_DUMP_CONTINUOUS` | `1` | Writes a swapchain dump for each presented frame, not only for the first one. It needs `SHARPEMU_GUEST_IMAGE_DUMP_DIR` and an active presented-image trace. Each dump waits for the GPU, so the frame rate decreases. | `VulkanVideoPresenter.Present.Frames.cs` |
|
||||
| `SHARPEMU_GUEST_IMAGE_DUMP_DIR` | path | Sets the directory for raw swapchain dumps. The presenter writes `present-NNNN-WxH-format.bgra` files there when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The presenter makes the directory if it is absent. | `VulkanVideoPresenter.Present.Frames.cs` |
|
||||
| `SHARPEMU_LOG_DISASM_ADDRS` | list of hex addresses | Adds more addresses to the fault disassembly dump. Use a comma between the addresses. The emulator uses this list only when `SHARPEMU_LOG_DISASM` is `1`, and it shows 48 instructions for each address. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_DISASM` | `1` | Writes guest disassembly to stderr when a guest fault, an abort or an illegal instruction occurs. It shows the code before the fault address, before the stack return address and before three frame return addresses. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_LOG_IL2CPP_EXCEPTION` | `1` | Writes a diagnostic dump when the guest calls the unresolved import with NID `cfwBSQyr5Ys`. The dump shows the registers, the code pointers on the stack and the frame pointer chain. The emulator writes a maximum of 4 dumps. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LOG_POINTER_WINDOWS` | list of hex addresses | Writes a memory window for each address when a guest access fault occurs. Use a comma between the addresses. The window size is 0x80 bytes unless `SHARPEMU_LOG_POINTER_WINDOW_SIZE` sets a different size. | `DirectExecutionBackend.Exceptions.cs` |
|
||||
| `SHARPEMU_SHADER_SPIRV_DUMP_DIR` | path | Set a directory for shader dumps. When set, the Vulkan presenter writes each SPIR-V module to a numbered `.spv` file. The `SHARPEMU_DUMP_SPIRV=1` dumps also use this directory. Their default is `shader-dumps` next to the executable. | `VulkanVideoPresenter.Pipelines.cs`, `ShaderProgramCache.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
|
||||
| `SHARPEMU_SWAPCHAIN_DUMP_EVERY` | number (frames) | Set a number N to trace the presented swapchain image at each Nth present. This operates only when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The default is 0, which traces only the first presented image. | `VulkanVideoPresenter.Present.Frames.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
|
||||
## Performance measurement
|
||||
|
||||
Each variable records timings or counts.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_LOG_AUDIO_QUEUE` | `1` | Writes one `[PERF][AUDIO]` line per second for each SDL audio stream. The line shows the queue depth in milliseconds, the submit rate, the fill percentage and the drops. The emulator reads the value one time at start. | `SdlHostAudio.cs` |
|
||||
| `SHARPEMU_PERF_HLE_NODICT` | `1` | Use with `SHARPEMU_PERF_HLE=1`. Set to `1` to stop the call count dictionary and the top call count report. The dispatch time report stays on. | `DirectExecutionBackend.Diagnostics.cs` |
|
||||
| `SHARPEMU_PERF_HLE` | `1` | Set to `1` to measure the time of each HLE import dispatch. After each 500000 calls, the emulator writes `[PERF][HLE]` lines with the call counts and the cost of the top exports. The default is off. | `DirectExecutionBackend.Diagnostics.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_PERF_MEM` | `1` | Set to `1` to count the POSIX fault signals that the signal handler receives. The emulator writes a `[PERF][MEM] posix_faults` line after each 100000 signals. This has an effect only on POSIX hosts. | `DirectExecutionBackend.PosixSignals.cs` |
|
||||
| `SHARPEMU_PROFILE_DCB_PARSE` | `1` | Set to `1` to measure the time to parse draw command buffers. The emulator writes `[PERF][DCB_PARSE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbParseProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_DCB_SUBMISSION` | `1` | Set to `1` to measure command buffer submission and geometry snapshot work. The emulator writes `[PERF][DCB_SUBMIT]`, `[PERF][VERTEX_SNAPSHOT]` and `[PERF][SNAPSHOT_PREPASS]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbSubmissionProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_GPU_DRAWS` | `1` | Use with the render profile. Writes a GPU timestamp after each draw and dispatch, so that the `[PERF][GPU_INTERVAL]` lines give the GPU time of each pipeline. The default writes only two timestamps for each submission for the `[PERF][GPU_BUSY]` lines, because one timestamp for each draw can change the GPU time. | `VulkanCommandProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_IMAGE_TRACKER` | `1` | Set to `1` to record counters for the guest image write tracker. The emulator writes `[PERF][GUEST_IMAGE_TRACKER]` and `[PERF][GUEST_IMAGE_TRACKER_RANGE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `GuestImageWriteTracker.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_RIP_INTERVAL_MS` | number (milliseconds) | Set the time between samples of the guest code profiler. The value must be more than 0. The default is 2. | `DirectExecutionBackend.GuestSampler.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_RIP_REPORT_S` | number (seconds) | Set the time between reports of the guest code profiler. The value must be more than 0. The default is 15. | `DirectExecutionBackend.GuestSampler.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_RIP_THREAD` | text | Set a text filter for the guest code profiler. The profiler samples only guest threads whose name contains this text. The comparison ignores case. When unset, the profiler samples all guest threads. | `DirectExecutionBackend.GuestSampler.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_RIP` | `1` | Set to `1` to start a sample profiler for guest code. The profiler records the instruction address of each guest thread and writes `[PERF][GUEST]` reports. The default is off. | `DirectExecutionBackend.GuestSampler.cs`, `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_PROFILE_GUEST_SCHEDULER` | `1` | Set to `1` to record guest thread scheduler statistics. The emulator writes `[PERF][GUEST_SCHED]` lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records guest thread flow events. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DirectExecutionBackend.GuestSchedulerProfile.cs`, `GuestThreadFlowProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_RENDER_REPORT_S` | number (seconds) | Set the time between render profile reports. The value can be a decimal number and must be more than 0. The default is 5. | `RenderPhaseProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_RENDER` | `1` | Set to `1` to measure the time of each render thread phase. The emulator writes `[PERF][RENDER]`, `[PERF][RENDER_MS]` and related lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records GPU memory access counters. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `RenderPhaseProfile.cs`, `GpuMemoryAccessProfile.cs` |
|
||||
| `SHARPEMU_PROFILE_SYNC_ON_ADDRESS` | `1` | Set to `1` to start the detailed sync-on-address profile. The emulator also records the guest call stack for each wait and wake call. It writes `[PERF][SYNC_ADDR]` and `[PERF][SYNC_THREAD]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the profile without the call stack. | `KernelSyncOnAddressProfile.cs`, `KernelSyncOnAddressCompatExports.cs` |
|
||||
| `SHARPEMU_PROFILE_TEXTURE_PREPARATION` | `1` | Set to `1` to measure texture preparation work. The emulator writes `[PERF][TEXTURE_PREP]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `TexturePreparationProfile.cs` |
|
||||
|
||||
## Internal
|
||||
|
||||
The emulator sets these variables for its own processes. Do not set them manually.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_MITIGATED_CHILD` | `1` | The Windows launcher sets this variable to 1 when it starts the child process with CFG and CET mitigations off. The child accepts the `--sharpemu-mitigated-child` argument only when the value is 1. Do not set it manually. | `Program.cs`, `EmulatorProcess.cs` |
|
||||
|
||||
## Tests and tools
|
||||
|
||||
Only the tests, the scripts or the build tools read these variables.
|
||||
|
||||
| Variable | Value | Function | Code that reads it |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARPEMU_BACKING_VIEW_FAULT_WORKER` | `1` | The backing view fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `BackingViewFaultTests.cs`, `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_BUFFER_FAULT_WORKER` | `1` | The guest buffer fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestBufferFaultTests.cs`, `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_FAULT_WORKER_REPORT` | path | The fault test parent sets this to a temporary file path for its child process. The child writes its result (`completed` or `skipped`) to this file. When unset, the child writes the result to the error stream. | `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_IMAGE_FAULT_WORKER` | `1` | The guest image fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestImageFaultTests.cs`, `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_NATIVE_RETURN_SMOKE_WORKER` | `1` | The native return smoke test sets this for its child process. The value `1` tells the child to run the synthetic guest entry. Do not set it manually. | `Gen5NativeReturnSmokeTests.cs` |
|
||||
| `SHARPEMU_SOCKET_CONNECTION_TEST_WORKER` | `1` | The kernel socket connection test sets this for its child process. The value `1` tells the child to do the connect sequence. Do not set it manually. | `KernelSocketConnectionTests.cs` |
|
||||
| `SHARPEMU_TEST_REFERENCE_SPIRV_DIR` | path | Sets the directory that contains the reference compiled shader files for the GPU image tests. The tests make sure that each file has the recorded SHA-256 hash. When unset, these tests skip the reference comparison. | `ImageTestHarness.cs` |
|
||||
| `SHARPEMU_TEST_REQUIRE_DEVICE` | `1` | Makes a GPU test fail when a Vulkan or Metal device or a device feature is missing. Default is to skip the test. | `ImageTestHarness.cs`, `MetalRuntimeTests.cs` |
|
||||
| `SHARPEMU_TEST_REQUIRE_REFERENCE_SPIRV` | `1` | Makes a test fail when a reference shader file, its directory, or a necessary device is missing. Default is to skip the test. | `ImageTestHarness.cs`, `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_TEST_VK_VALIDATION` | `1` | Loads the Vulkan validation layer in the headless test device and prints the validation messages. Default is off. | `HeadlessVulkan.cs` |
|
||||
| `SHARPEMU_THREAD_CONTEXT_TEST_WORKER` | `1` | The Windows thread context capture test sets this for its child process. The value `1` tells the child to do the capture sequence. Do not set it manually. | `WindowsThreadContextCaptureTests.cs`, `GuestFaultWorker.cs` |
|
||||
| `SHARPEMU_UPDATE_GOLDENS` | `1` | Makes the Metal golden tests write the generated shader text to the golden files in the source tree. Default is to compare the generated text with the golden files. | `MslGoldenTests.cs` |
|
||||
|
||||
## Variables that the code reads but does not use
|
||||
|
||||
The code reads these variables into a field or a filter. No other code uses that field or filter.
|
||||
Thus they have no effect, or only a part of their function operates. The rows above give the details.
|
||||
|
||||
- `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT`
|
||||
- `SHARPEMU_ENABLE_CHUNKED_DRAWS`
|
||||
- `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT`
|
||||
- `SHARPEMU_FORCE_DEFAULT_RASTER_STATE`
|
||||
- `SHARPEMU_FORCE_FULLSCREEN_PIPELINE`
|
||||
- `SHARPEMU_FORCE_FULLSCREEN_VERTEX`
|
||||
- `SHARPEMU_FORCE_SOLID_FRAGMENT`
|
||||
- `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE`
|
||||
- `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX`
|
||||
- `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT`
|
||||
- `SHARPEMU_SKIP_TALL_COMPUTE_Z`
|
||||
- `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT`
|
||||
- `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT`
|
||||
- `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH`
|
||||
- `SHARPEMU_TRACE_GUEST_WRITES`
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Windows crash capture
|
||||
|
||||
In the launcher, open **Options > Logging** and enable **Crash dumps** before
|
||||
you start a game. The same toggle is available in each game's Logging settings.
|
||||
It sets `SHARPEMU_CRASH_CAPTURE` and is off by default.
|
||||
|
||||
For a diagnostic run on Windows x64, you can also set it from PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:SHARPEMU_CRASH_CAPTURE = '1'
|
||||
.\SharpEmu.exe
|
||||
```
|
||||
|
||||
Start a new launcher from that PowerShell window. The game process starts a
|
||||
hidden helper before runtime initialization. No SDK or debugger install is
|
||||
required. Look for `[CRASH][INFO] Crash capture ready` in the game log.
|
||||
|
||||
The helper writes a full dump beside the run log. Without a log path, it uses
|
||||
`user/logs` beside the executable. Names include the game process ID:
|
||||
|
||||
- `<run>.crash-<pid>.dmp`: the memory dump.
|
||||
- `<run>.crash-<pid>.dmp.capture.log`: capture status and errors.
|
||||
|
||||
Full dumps can use many GB of disk space and contain private process data.
|
||||
Keep them local until you choose to share them. There is no automatic upload.
|
||||
|
||||
The helper uses Windows debugger events. It passes first-chance exceptions to
|
||||
the application and captures only second-chance, unhandled exceptions. Normal
|
||||
handled page faults do not create dumps. A normal exit or a forced process
|
||||
termination does not create a dump either. The original crash is not suppressed.
|
||||
|
||||
This option changes debugger presence and can change timing. Do not use it for
|
||||
FPS comparisons. Do not attach WinDbg, ProcDump, or another native debugger at
|
||||
the same time. Capture failures are recorded when possible; no capture method
|
||||
can cover every process termination. A failed write can leave an incomplete dump.
|
||||
|
||||
This is separate from the `DOTNET_Dbg*` settings. It does not change them.
|
||||
To disable a GUI selection, turn off **Crash dumps** before the next game launch.
|
||||
If you set the option in PowerShell, close the launcher and run:
|
||||
|
||||
```powershell
|
||||
Remove-Item Env:SHARPEMU_CRASH_CAPTURE -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "latestFeature"
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*
|
||||
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "bink.h"
|
||||
|
||||
typedef struct sharpemu_bink2_info {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t frames_per_second_numerator;
|
||||
uint32_t frames_per_second_denominator;
|
||||
} sharpemu_bink2_info;
|
||||
|
||||
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||
HBINK bink;
|
||||
if (!path || !movie || !info) return 0;
|
||||
|
||||
bink = BinkOpen(path, 0);
|
||||
if (!bink) return 0;
|
||||
|
||||
*movie = bink;
|
||||
info->width = bink->Width;
|
||||
info->height = bink->Height;
|
||||
info->frames_per_second_numerator = bink->FrameRate;
|
||||
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||
uint32_t stride, uint32_t destination_bytes) {
|
||||
uint64_t needed;
|
||||
if (!movie || !destination || stride < movie->Width * 4) return 0;
|
||||
needed = (uint64_t)stride * movie->Height;
|
||||
if (needed > destination_bytes) return 0;
|
||||
|
||||
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||
if (BinkWait(movie)) return 0;
|
||||
BinkDoFrame(movie);
|
||||
BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
|
||||
BinkNextFrame(movie);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void sharpemu_bink2_close(HBINK movie) {
|
||||
if (movie) BinkClose(movie);
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
|
||||
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
|
||||
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
|
||||
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
|
||||
|
||||
|
||||
def compute_nid(export_name: str) -> str:
|
||||
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
|
||||
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
|
||||
return encoded.rstrip("=").replace("/", "-")
|
||||
|
||||
|
||||
def read_names(path: Path) -> list[str]:
|
||||
try:
|
||||
return [
|
||||
line.strip()
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
except OSError as error:
|
||||
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
|
||||
|
||||
|
||||
def write_pair(nid: str, export_name: str) -> None:
|
||||
print(f"{nid}\t{export_name}")
|
||||
|
||||
|
||||
def lookup(args: argparse.Namespace) -> int:
|
||||
values = [value.strip() for value in args.value.split(",") if value.strip()]
|
||||
names = read_names(args.names)
|
||||
name_set = set(names)
|
||||
|
||||
result = 0
|
||||
|
||||
for value in values:
|
||||
if NID_PATTERN.fullmatch(value):
|
||||
found = False
|
||||
|
||||
for export_name in names:
|
||||
if compute_nid(export_name) == value:
|
||||
write_pair(value, export_name)
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
print(f"NID not found in catalog: {value}", file=sys.stderr)
|
||||
result = 1
|
||||
|
||||
continue
|
||||
|
||||
write_pair(compute_nid(value), value)
|
||||
|
||||
if value not in name_set:
|
||||
print(
|
||||
f"Warning: export name is not present in the catalog: {value}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def search(args: argparse.Namespace) -> int:
|
||||
names = read_names(args.names)
|
||||
if args.regex:
|
||||
try:
|
||||
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
|
||||
except re.error as error:
|
||||
print(f"Invalid regular expression: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
matches = (name for name in names if pattern.search(name))
|
||||
elif args.case_sensitive:
|
||||
matches = (name for name in names if args.query in name)
|
||||
else:
|
||||
query = args.query.casefold()
|
||||
matches = (name for name in names if query in name.casefold())
|
||||
|
||||
count = 0
|
||||
for export_name in matches:
|
||||
write_pair(compute_nid(export_name), export_name)
|
||||
count += 1
|
||||
if args.limit and count >= args.limit:
|
||||
break
|
||||
|
||||
if count == 0:
|
||||
print(f"No catalog names matched: {args.query}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def export_catalog(args: argparse.Namespace) -> int:
|
||||
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
|
||||
if args.sort == "nid":
|
||||
pairs.sort(key=lambda pair: (pair[0], pair[1]))
|
||||
elif args.sort == "name":
|
||||
pairs.sort(key=lambda pair: pair[1])
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as output:
|
||||
output.write("# NID\tExportName\n")
|
||||
for nid, export_name in pairs:
|
||||
output.write(f"{nid}\t{export_name}\n")
|
||||
except OSError as error:
|
||||
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Wrote {len(pairs)} entries to {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
|
||||
epilog=(
|
||||
"Examples:\n"
|
||||
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
|
||||
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
|
||||
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
|
||||
" python scripts/aerolib_catalog.py export"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--names",
|
||||
type=Path,
|
||||
default=DEFAULT_NAMES_FILE,
|
||||
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
lookup_parser = subparsers.add_parser(
|
||||
"lookup", help="resolve a NID or calculate the NID for an export name"
|
||||
)
|
||||
lookup_parser.add_argument(
|
||||
"value", help="11-character NID or exact export name; comma-separated values are supported"
|
||||
)
|
||||
lookup_parser.set_defaults(handler=lookup)
|
||||
|
||||
search_parser = subparsers.add_parser(
|
||||
"search", help="find export names and print matching NID/name pairs"
|
||||
)
|
||||
search_parser.add_argument("query", help="name substring or regular expression")
|
||||
search_parser.add_argument(
|
||||
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--case-sensitive", action="store_true", help="match case exactly"
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--regex", action="store_true", help="treat the query as a regular expression"
|
||||
)
|
||||
search_parser.set_defaults(handler=search)
|
||||
|
||||
export_parser = subparsers.add_parser(
|
||||
"export", help="write every NID/name pair to a tab-separated text file"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"output",
|
||||
type=Path,
|
||||
nargs="?",
|
||||
default=DEFAULT_EXPORT_FILE,
|
||||
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--sort",
|
||||
choices=("source", "nid", "name"),
|
||||
default="nid",
|
||||
help="output ordering (default: nid)",
|
||||
)
|
||||
export_parser.set_defaults(handler=export_catalog)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
return args.handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -133845,7 +133845,6 @@ sceKernelWriteRemapIntoPrtCommand
|
|||
sceKernelWriteRemapWithGpuMaskIdCommand
|
||||
sceKernelWriteSdkEventLog
|
||||
sceKernelWriteSdkEventLogV
|
||||
sceKernelWriteThrottlingStatus
|
||||
sceKernelWriteUnmapCommand
|
||||
sceKernelWriteUnmapToPrtCommand
|
||||
sceKernelWriteWaitOnAddressCommand
|
||||
|
|
@ -153134,7 +153133,6 @@ scePsmlMfsrGetContextBufferRequirement800M3_2
|
|||
scePsmlMfsrGetDispatchMfsrPacket1000
|
||||
scePsmlMfsrGetDispatchMfsrPacket1100
|
||||
scePsmlMfsrGetDispatchMfsrPacketSizeInDwords
|
||||
scePsmlMfsrGetDispatchMfsrPacket900
|
||||
scePsmlMfsrGetMipmapBias
|
||||
scePsmlMfsrGetSharedResourcesInitRequirement
|
||||
scePsmlMfsrInit
|
||||
|
|
@ -154457,6 +154455,3 @@ sceNpEAAccessInitialize
|
|||
sceNpEAAccessTerminate
|
||||
sceNpHasEAAccessSubscription
|
||||
sceNpHasEAAccessSubscriptionAbortRequest
|
||||
sceAgcAddPrimStateRegisters
|
||||
sceVoiceQoSSetMode
|
||||
sceVoiceQoSTerminate
|
||||
|
|
|
|||
|
|
@ -1,466 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
|
||||
VERSION_ELEMENT_PATTERN = re.compile(
|
||||
r"(<SharpEmuVersion>)([^<]+)(</SharpEmuVersion>)"
|
||||
)
|
||||
|
||||
|
||||
class ReleaseError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def run_git(
|
||||
*args: str,
|
||||
cwd: Path,
|
||||
capture_output: bool = False,
|
||||
) -> str:
|
||||
command = ["git", *args]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=capture_output,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise ReleaseError("Git was not found in PATH.") from None
|
||||
except subprocess.CalledProcessError as error:
|
||||
stderr = error.stderr.strip() if error.stderr else ""
|
||||
detail = f"\n{stderr}" if stderr else ""
|
||||
|
||||
raise ReleaseError(
|
||||
f"Git command failed: {' '.join(command)}{detail}"
|
||||
) from error
|
||||
|
||||
return result.stdout.strip() if capture_output else ""
|
||||
|
||||
|
||||
def find_repository_root(script_path: Path) -> Path:
|
||||
root = run_git(
|
||||
"rev-parse",
|
||||
"--show-toplevel",
|
||||
cwd=script_path.resolve().parent,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
return Path(root)
|
||||
|
||||
|
||||
def get_status(repository_root: Path) -> str:
|
||||
return run_git(
|
||||
"status",
|
||||
"--porcelain",
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def ensure_clean_worktree(repository_root: Path) -> None:
|
||||
status = get_status(repository_root)
|
||||
|
||||
if status:
|
||||
raise ReleaseError(
|
||||
"The working tree is not clean.\n\n"
|
||||
f"{status}\n\n"
|
||||
"Commit, stash, or remove these changes first."
|
||||
)
|
||||
|
||||
|
||||
def get_current_branch(repository_root: Path) -> str:
|
||||
branch = run_git(
|
||||
"branch",
|
||||
"--show-current",
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if not branch:
|
||||
raise ReleaseError(
|
||||
"HEAD is detached. Switch to a branch before continuing."
|
||||
)
|
||||
|
||||
return branch
|
||||
|
||||
|
||||
def ensure_branch_does_not_exist(
|
||||
repository_root: Path,
|
||||
branch: str,
|
||||
remote: str,
|
||||
) -> None:
|
||||
local_branch = run_git(
|
||||
"branch",
|
||||
"--list",
|
||||
branch,
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if local_branch:
|
||||
raise ReleaseError(f"Branch {branch} already exists locally.")
|
||||
|
||||
remote_branch = run_git(
|
||||
"ls-remote",
|
||||
"--heads",
|
||||
remote,
|
||||
branch,
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if remote_branch:
|
||||
raise ReleaseError(
|
||||
f"Branch {branch} already exists on {remote}."
|
||||
)
|
||||
|
||||
|
||||
def ensure_tag_does_not_exist(
|
||||
repository_root: Path,
|
||||
tag: str,
|
||||
remote: str,
|
||||
) -> None:
|
||||
local_tag = run_git(
|
||||
"tag",
|
||||
"--list",
|
||||
tag,
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if local_tag:
|
||||
raise ReleaseError(f"Tag {tag} already exists locally.")
|
||||
|
||||
remote_tag = run_git(
|
||||
"ls-remote",
|
||||
"--tags",
|
||||
remote,
|
||||
f"refs/tags/{tag}",
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if remote_tag:
|
||||
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
||||
|
||||
|
||||
def get_previous_tag(repository_root: Path) -> str:
|
||||
try:
|
||||
return run_git(
|
||||
"describe",
|
||||
"--tags",
|
||||
"--abbrev=0",
|
||||
"--match",
|
||||
"v*",
|
||||
"HEAD",
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
except ReleaseError:
|
||||
return ""
|
||||
|
||||
|
||||
def get_release_commits(
|
||||
repository_root: Path,
|
||||
previous_tag: str,
|
||||
) -> list[str]:
|
||||
revision_range = f"{previous_tag}..HEAD" if previous_tag else "HEAD"
|
||||
|
||||
output = run_git(
|
||||
"log",
|
||||
"--no-merges",
|
||||
"--reverse",
|
||||
"--pretty=tformat:%h %s",
|
||||
revision_range,
|
||||
cwd=repository_root,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
return output.splitlines()
|
||||
|
||||
|
||||
def print_release_notes_preview(repository_root: Path) -> None:
|
||||
previous_tag = get_previous_tag(repository_root)
|
||||
commits = get_release_commits(repository_root, previous_tag)
|
||||
|
||||
print()
|
||||
|
||||
if previous_tag:
|
||||
print(f"Commits since {previous_tag} ({len(commits)}):")
|
||||
else:
|
||||
print(f"Commits in this release ({len(commits)}):")
|
||||
|
||||
if not commits:
|
||||
print(" (none)")
|
||||
|
||||
for commit in commits:
|
||||
print(f" {commit}")
|
||||
|
||||
print()
|
||||
print("These commits go into the generated release notes.")
|
||||
|
||||
|
||||
def read_version(props_path: Path) -> str:
|
||||
if not props_path.exists():
|
||||
raise ReleaseError(f"Version file not found: {props_path}")
|
||||
|
||||
content = props_path.read_text(encoding="utf-8")
|
||||
match = VERSION_ELEMENT_PATTERN.search(content)
|
||||
|
||||
if match is None:
|
||||
raise ReleaseError(
|
||||
f"SharpEmuVersion was not found in {props_path.name}."
|
||||
)
|
||||
|
||||
return match.group(2).strip()
|
||||
|
||||
|
||||
def update_version(props_path: Path, version: str) -> str:
|
||||
content = props_path.read_text(encoding="utf-8")
|
||||
current_version = read_version(props_path)
|
||||
|
||||
if current_version == version:
|
||||
raise ReleaseError(
|
||||
f"SharpEmuVersion is already set to {version}."
|
||||
)
|
||||
|
||||
updated_content, replacement_count = VERSION_ELEMENT_PATTERN.subn(
|
||||
rf"\g<1>{version}\g<3>",
|
||||
content,
|
||||
count=1,
|
||||
)
|
||||
|
||||
if replacement_count != 1:
|
||||
raise ReleaseError(
|
||||
"Expected exactly one SharpEmuVersion element."
|
||||
)
|
||||
|
||||
props_path.write_text(
|
||||
updated_content,
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
return current_version
|
||||
|
||||
|
||||
def prepare_release(
|
||||
repository_root: Path,
|
||||
props_path: Path,
|
||||
version: str,
|
||||
remote: str,
|
||||
) -> None:
|
||||
ensure_clean_worktree(repository_root)
|
||||
|
||||
current_branch = get_current_branch(repository_root)
|
||||
|
||||
if current_branch != "main":
|
||||
raise ReleaseError(
|
||||
f"Prepare must be run from main, not {current_branch}."
|
||||
)
|
||||
|
||||
run_git(
|
||||
"pull",
|
||||
"--ff-only",
|
||||
remote,
|
||||
"main",
|
||||
cwd=repository_root,
|
||||
)
|
||||
|
||||
branch = f"release/{version}"
|
||||
ensure_branch_does_not_exist(
|
||||
repository_root,
|
||||
branch,
|
||||
remote,
|
||||
)
|
||||
|
||||
previous_version = read_version(props_path)
|
||||
|
||||
run_git(
|
||||
"switch",
|
||||
"-c",
|
||||
branch,
|
||||
cwd=repository_root,
|
||||
)
|
||||
|
||||
try:
|
||||
update_version(props_path, version)
|
||||
|
||||
relative_props_path = props_path.relative_to(repository_root)
|
||||
|
||||
run_git(
|
||||
"add",
|
||||
relative_props_path.as_posix(),
|
||||
cwd=repository_root,
|
||||
)
|
||||
run_git(
|
||||
"commit",
|
||||
"-m",
|
||||
f"chore: bump version to {version}",
|
||||
cwd=repository_root,
|
||||
)
|
||||
run_git(
|
||||
"push",
|
||||
"-u",
|
||||
remote,
|
||||
branch,
|
||||
cwd=repository_root,
|
||||
)
|
||||
except Exception:
|
||||
print(
|
||||
"\nPrepare failed. The release branch may still exist locally.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
print()
|
||||
print(f"Prepared release {previous_version} -> {version}")
|
||||
print(f"Branch pushed: {branch}")
|
||||
print()
|
||||
print("Open a pull request from:")
|
||||
print(f" {branch}")
|
||||
print("into:")
|
||||
print(" main")
|
||||
print()
|
||||
print("After merging the PR, run:")
|
||||
print(f" python scripts/release.py tag {version}")
|
||||
|
||||
|
||||
def create_release_tag(
|
||||
repository_root: Path,
|
||||
props_path: Path,
|
||||
version: str,
|
||||
remote: str,
|
||||
) -> None:
|
||||
ensure_clean_worktree(repository_root)
|
||||
|
||||
current_branch = get_current_branch(repository_root)
|
||||
|
||||
if current_branch != "main":
|
||||
raise ReleaseError(
|
||||
f"Tagging must be run from main, not {current_branch}."
|
||||
)
|
||||
|
||||
run_git(
|
||||
"pull",
|
||||
"--ff-only",
|
||||
remote,
|
||||
"main",
|
||||
cwd=repository_root,
|
||||
)
|
||||
|
||||
current_version = read_version(props_path)
|
||||
|
||||
if current_version != version:
|
||||
raise ReleaseError(
|
||||
"Version mismatch:\n"
|
||||
f" Directory.Build.props: {current_version}\n"
|
||||
f" Requested tag: {version}"
|
||||
)
|
||||
|
||||
tag = f"v{version}"
|
||||
|
||||
ensure_tag_does_not_exist(
|
||||
repository_root,
|
||||
tag,
|
||||
remote,
|
||||
)
|
||||
|
||||
print_release_notes_preview(repository_root)
|
||||
|
||||
run_git(
|
||||
"tag",
|
||||
"-a",
|
||||
tag,
|
||||
"-m",
|
||||
f"SharpEmu {version}",
|
||||
cwd=repository_root,
|
||||
)
|
||||
run_git(
|
||||
"push",
|
||||
remote,
|
||||
tag,
|
||||
cwd=repository_root,
|
||||
)
|
||||
|
||||
print()
|
||||
print(f"Successfully pushed tag {tag}.")
|
||||
print("The release workflow should start automatically.")
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prepare or tag a SharpEmu release."
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command",
|
||||
required=True,
|
||||
)
|
||||
|
||||
for command in ("prepare", "tag"):
|
||||
subparser = subparsers.add_parser(command)
|
||||
subparser.add_argument(
|
||||
"version",
|
||||
help="Version without the v prefix, e.g. 0.0.2-beta.2.",
|
||||
)
|
||||
subparser.add_argument(
|
||||
"--remote",
|
||||
default="origin",
|
||||
help="Git remote. Default: origin.",
|
||||
)
|
||||
|
||||
arguments = parser.parse_args()
|
||||
|
||||
if not VERSION_PATTERN.fullmatch(arguments.version):
|
||||
parser.error(
|
||||
"Version must look like 0.0.2, "
|
||||
"0.0.2-beta.2, or 0.0.2-rc.1."
|
||||
)
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = parse_arguments()
|
||||
|
||||
try:
|
||||
repository_root = find_repository_root(Path(__file__))
|
||||
props_path = repository_root / "Directory.Build.props"
|
||||
|
||||
if arguments.command == "prepare":
|
||||
prepare_release(
|
||||
repository_root,
|
||||
props_path,
|
||||
arguments.version,
|
||||
arguments.remote,
|
||||
)
|
||||
else:
|
||||
create_release_tag(
|
||||
repository_root,
|
||||
props_path,
|
||||
arguments.version,
|
||||
arguments.remote,
|
||||
)
|
||||
except ReleaseError as error:
|
||||
print(f"Error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 4 ]; then
|
||||
echo "usage: $0 <spirv-val> <expected-version> <target-env> <module-directory>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
validator=$1
|
||||
expected_version=$2
|
||||
target_env=$3
|
||||
module_directory=$4
|
||||
|
||||
if [ ! -x "$validator" ]; then
|
||||
echo "SPIR-V validator is not executable: $validator" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$module_directory" ]; then
|
||||
echo "SPIR-V module directory does not exist: $module_directory" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
validator_version="$("$validator" --version | head -n 1)"
|
||||
if [[ "$validator_version" != *"SPIRV-Tools $expected_version"* ]]; then
|
||||
echo "unexpected SPIRV-Tools version: $validator_version (expected $expected_version)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Validator: $validator_version"
|
||||
echo "Target environment: $target_env"
|
||||
|
||||
mapfile -d '' modules < <(find "$module_directory" -type f -name '*.spv' -print0 | sort -z)
|
||||
if [ "${#modules[@]}" -eq 0 ]; then
|
||||
echo "no SPIR-V modules found in $module_directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
failures=0
|
||||
for module in "${modules[@]}"; do
|
||||
echo "Validating module: $module"
|
||||
if ! "$validator" --target-env "$target_env" "$module"; then
|
||||
echo "SPIR-V validation failed: $module" >&2
|
||||
failures=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$failures" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validated ${#modules[@]} synthetic SPIR-V modules."
|
||||
|
|
@ -8,7 +8,6 @@ using SharpEmu.HLE;
|
|||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
@ -32,6 +31,7 @@ internal static partial class Program
|
|||
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_INPUT_HANDLE = -10;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
|
|
@ -45,13 +45,10 @@ internal static partial class Program
|
|||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
ConfigureManagedPluginResolution();
|
||||
|
||||
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
|
||||
return SharpEmu.Core.Diagnostics.WindowsCrashCapture.RunHelper(args);
|
||||
|
||||
RenderDocCapture.ApplyVulkanLoaderEnvironment();
|
||||
SharpEmu.Libs.VideoOut.RenderDocCapture.Initialize();
|
||||
// Avoid blocking full collections while guest and render threads are
|
||||
// running, and establish the GC mode before the runtime reserves the
|
||||
// fixed guest address-space window.
|
||||
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -64,36 +61,13 @@ internal static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
private static void ConfigureManagedPluginResolution()
|
||||
{
|
||||
AssemblyLoadContext.Default.Resolving += static (loadContext, assemblyName) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assemblyName.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var assemblyPath = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"plugins",
|
||||
assemblyName.Name + ".dll");
|
||||
return File.Exists(assemblyPath)
|
||||
? loadContext.LoadFromAssemblyPath(assemblyPath)
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
if (Updater.TryApply(args, out var updateExitCode))
|
||||
{
|
||||
return updateExitCode;
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
|
||||
if (args.Length == 0)
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
// No arguments: open the desktop frontend. Any argument selects
|
||||
// the classic CLI behavior below.
|
||||
return GuiLauncher.Run();
|
||||
}
|
||||
|
||||
|
|
@ -119,9 +93,14 @@ internal static partial class Program
|
|||
PreloadMacVulkanLoader();
|
||||
}
|
||||
|
||||
// SDL/AppKit window work belongs on the process main thread on
|
||||
// macOS. Linux uses the same model for consistent X11/Wayland
|
||||
// event ownership. Emulation remains on a worker thread.
|
||||
// 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(() =>
|
||||
|
|
@ -152,9 +131,10 @@ internal static partial class Program
|
|||
/// 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). Failing up front on
|
||||
/// any other process architecture distinguishes that from MoltenVK,
|
||||
/// signal-handler, or guest-memory startup problems.
|
||||
/// 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()
|
||||
{
|
||||
|
|
@ -198,11 +178,11 @@ internal static partial class Program
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a Vulkan loader visible before SDL creates its Vulkan surface.
|
||||
/// 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 can then resolve the loader for SDL and Silk.NET.
|
||||
/// dyld then resolves GLFW's bare-name dlopen to the loaded image.
|
||||
/// </summary>
|
||||
private static void PreloadMacVulkanLoader()
|
||||
{
|
||||
|
|
@ -243,13 +223,7 @@ internal static partial class Program
|
|||
return childExitCode;
|
||||
}
|
||||
|
||||
if (!TryParseArguments(
|
||||
args,
|
||||
out var ebootPath,
|
||||
out var runtimeOptions,
|
||||
out var videoOptions,
|
||||
out var logLevel,
|
||||
out var logFilePath))
|
||||
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
|
|
@ -261,11 +235,6 @@ internal static partial class Program
|
|||
}
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
if (!HostVideoHost.TryConfigureVideo(videoOptions))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Video options cannot change while a presenter is active.");
|
||||
return 3;
|
||||
}
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
Log.Info(HostSystemInfo.Summary);
|
||||
|
|
@ -279,118 +248,68 @@ internal static partial class Program
|
|||
return 2;
|
||||
}
|
||||
|
||||
if (!TryGetDebugServerOptions(args, out var debugServerEnabled, out var debugServerOptions, out var debugServerError))
|
||||
{
|
||||
Log.Error($"Invalid --debug-server endpoint: {debugServerError}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
SharpEmu.Debugger.DebuggerServerHost? debugHost = null;
|
||||
if (debugServerEnabled)
|
||||
{
|
||||
debugHost = new SharpEmu.Debugger.DebuggerServerHost(debugServerOptions);
|
||||
try
|
||||
{
|
||||
debugHost.Start();
|
||||
Log.Info($"Live debug server listening on {debugHost.Endpoint}. Attach with SharpEmu.DebugClient.");
|
||||
// With StopAtEntry, the guest parks at its first frame until a
|
||||
// client connects and continues.
|
||||
runtimeOptions = runtimeOptions with { DebugHook = debugHost.Hook };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Failed to start the debug server.", ex);
|
||||
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[DEBUG] Creating runtime...");
|
||||
|
||||
SharpEmu.Core.Diagnostics.WindowsCrashCapture.StartIfEnabled(logFilePath);
|
||||
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||
|
||||
OrbisGen2Result result;
|
||||
ConsoleCancelEventHandler? cancelHandler = null;
|
||||
try
|
||||
{
|
||||
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||
|
||||
OrbisGen2Result result;
|
||||
ConsoleCancelEventHandler? cancelHandler = null;
|
||||
try
|
||||
cancelHandler = (_, eventArgs) =>
|
||||
{
|
||||
cancelHandler = (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
VideoOutExports.NotifyHostInterrupt();
|
||||
};
|
||||
Console.CancelKeyPress += cancelHandler;
|
||||
// SIGTERM (launchd, process managers, `kill`) takes the same cooperative shutdown: the
|
||||
// runtime's default exits with GPU work in flight, which leaves the host driver to reclaim
|
||||
// resources the device still owns.
|
||||
using var terminateRegistration = OperatingSystem.IsWindows()
|
||||
? null
|
||||
: PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
|
||||
{
|
||||
context.Cancel = true;
|
||||
VideoOutExports.NotifyHostInterrupt();
|
||||
});
|
||||
eventArgs.Cancel = true;
|
||||
VideoOutExports.NotifyHostInterrupt();
|
||||
};
|
||||
Console.CancelKeyPress += cancelHandler;
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||
result = runtime.Run(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
|
||||
Log.Error("SharpEmu failed to run.", ex);
|
||||
return 3;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (cancelHandler is not null)
|
||||
{
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||
{
|
||||
Log.Info(runtime.LastSessionSummary);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
|
||||
{
|
||||
Log.Info("BB trace:");
|
||||
Log.Info(runtime.LastBasicBlockTrace);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
|
||||
{
|
||||
Log.Info(runtime.LastMilestoneLog);
|
||||
}
|
||||
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
|
||||
{
|
||||
Log.Warn(runtime.LastExecutionDiagnostics);
|
||||
}
|
||||
|
||||
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
|
||||
{
|
||||
Log.Info("Import trace:");
|
||||
Log.Info(runtime.LastExecutionTrace);
|
||||
}
|
||||
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||
result = runtime.Run(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
|
||||
Log.Error("SharpEmu failed to run.", ex);
|
||||
return 3;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (debugHost is not null)
|
||||
if (cancelHandler is not null)
|
||||
{
|
||||
debugHost.NotifyRunCompleted();
|
||||
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||
{
|
||||
Log.Info(runtime.LastSessionSummary);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
|
||||
{
|
||||
Log.Info("BB trace:");
|
||||
Log.Info(runtime.LastBasicBlockTrace);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
|
||||
{
|
||||
Log.Info(runtime.LastMilestoneLog);
|
||||
}
|
||||
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
|
||||
{
|
||||
Log.Warn(runtime.LastExecutionDiagnostics);
|
||||
}
|
||||
|
||||
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
|
||||
{
|
||||
Log.Info("Import trace:");
|
||||
Log.Info(runtime.LastExecutionTrace);
|
||||
}
|
||||
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
}
|
||||
|
||||
private static void EnsureCliConsole()
|
||||
|
|
@ -487,9 +406,7 @@ internal static partial class Program
|
|||
return handle != 0 && handle != -1;
|
||||
}
|
||||
|
||||
private static string[] NormalizeInternalArguments(
|
||||
string[] args,
|
||||
out bool isMitigatedChild)
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
var trustedMitigatedChild = string.Equals(
|
||||
|
|
@ -535,15 +452,14 @@ internal static partial class Program
|
|||
return false;
|
||||
}
|
||||
|
||||
var commandLineArguments = Environment.GetCommandLineArgs();
|
||||
var entryAssemblyPath = commandLineArguments.Length > 0 ? commandLineArguments[0] : null;
|
||||
string[] childArguments =
|
||||
Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(entryAssemblyPath)
|
||||
? [entryAssemblyPath, MitigatedChildFlag, .. args]
|
||||
: [MitigatedChildFlag, .. args];
|
||||
var childArgs = new string[args.Length + 1];
|
||||
childArgs[0] = MitigatedChildFlag;
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
childArgs[i + 1] = args[i];
|
||||
}
|
||||
|
||||
var commandLine = BuildCommandLine(processPath, childArguments);
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
|
||||
|
|
@ -592,7 +508,7 @@ internal static partial class Program
|
|||
nint jobHandle = 0;
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
var created = CreateProcessW(
|
||||
null,
|
||||
processPath,
|
||||
cmdLineBuilder,
|
||||
0,
|
||||
0,
|
||||
|
|
@ -1010,52 +926,14 @@ internal static partial class Program
|
|||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects the <c>--debug-server</c> flag and parses its optional
|
||||
/// <c>host:port</c> endpoint. Returns false only when the flag is present but
|
||||
/// its endpoint is malformed, so the caller can abort with a clear error.
|
||||
/// </summary>
|
||||
private static bool TryGetDebugServerOptions(
|
||||
string[] args,
|
||||
out bool enabled,
|
||||
out SharpEmu.Debugger.Server.DebuggerServerOptions options,
|
||||
out string error)
|
||||
{
|
||||
enabled = false;
|
||||
options = new SharpEmu.Debugger.Server.DebuggerServerOptions();
|
||||
error = string.Empty;
|
||||
foreach (var argument in args)
|
||||
{
|
||||
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const string prefix = "--debug-server=";
|
||||
if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
enabled = true;
|
||||
if (!SharpEmu.Debugger.Server.DebuggerServerOptions.TryParseEndpoint(argument[prefix.Length..], out options, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseArguments(
|
||||
string[] args,
|
||||
out string ebootPath,
|
||||
out SharpEmuRuntimeOptions runtimeOptions,
|
||||
out HostVideoOptions videoOptions,
|
||||
out LogLevel logLevel,
|
||||
out string? logFilePath)
|
||||
{
|
||||
|
|
@ -1063,7 +941,6 @@ internal static partial class Program
|
|||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
videoOptions = HostVideoOptions.Default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
|
|
@ -1072,173 +949,18 @@ internal static partial class Program
|
|||
var strictDynlibResolution = false;
|
||||
var importTraceLimit = 0;
|
||||
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||
var systemLanguage = 1;
|
||||
HostWindowMode? windowModeOverride = null;
|
||||
HostScalingMode? scalingModeOverride = null;
|
||||
int? windowWidthOverride = null;
|
||||
int? windowHeightOverride = null;
|
||||
int? displayIndexOverride = null;
|
||||
int? refreshRateOverride = null;
|
||||
bool? vsyncOverride = null;
|
||||
HostHdrMode? hdrModeOverride = null;
|
||||
bool? overlayEnabledOverride = null;
|
||||
PerformanceOverlayMode? overlayModeOverride = null;
|
||||
PerformanceOverlayCorner? overlayCornerOverride = null;
|
||||
videoOptions = HostVideoOptions.Default;
|
||||
logFilePath = null;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
var pathTokens = new List<string>(args.Length);
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var argument = args[i];
|
||||
if (TrySplitOption(argument, "--console", out var consoleText))
|
||||
{
|
||||
if (!string.Equals(consoleText, "ps5", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--console-language", out var consoleLanguageText))
|
||||
{
|
||||
if (!TryParseSystemLanguage(consoleLanguageText, out systemLanguage))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--window-mode", out var windowModeText))
|
||||
{
|
||||
if (!TryParseWindowMode(windowModeText, out var windowMode))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
windowModeOverride = windowMode;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--resolution", out var resolutionText))
|
||||
{
|
||||
if (!TryParseResolution(resolutionText, out var windowWidth, out var windowHeight))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
windowWidthOverride = windowWidth;
|
||||
windowHeightOverride = windowHeight;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--display", out var displayText))
|
||||
{
|
||||
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
displayIndexOverride = displayIndex;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--refresh-rate", out var refreshText))
|
||||
{
|
||||
if (!int.TryParse(refreshText, out var refreshRate) || refreshRate < 0)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
refreshRateOverride = refreshRate;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--scaling", out var scalingText))
|
||||
{
|
||||
if (!TryParseScalingMode(scalingText, out var scalingMode))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
scalingModeOverride = scalingMode;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--vsync", out var vsyncText))
|
||||
{
|
||||
if (!TryParseSwitch(vsyncText, out var vsync))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
vsyncOverride = vsync;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--overlay", out var overlayText))
|
||||
{
|
||||
if (!TryParseSwitch(overlayText, out var overlayEnabled))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
overlayEnabledOverride = overlayEnabled;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--overlay-mode", out var overlayModeText))
|
||||
{
|
||||
if (!Enum.TryParse<PerformanceOverlayMode>(overlayModeText, true, out var overlayMode) ||
|
||||
!Enum.IsDefined(overlayMode))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
overlayModeOverride = overlayMode;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--overlay-corner", out var overlayCornerText))
|
||||
{
|
||||
if (!Enum.TryParse<PerformanceOverlayCorner>(overlayCornerText, true, out var overlayCorner) ||
|
||||
!Enum.IsDefined(overlayCorner))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
overlayCornerOverride = overlayCorner;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--hdr", out var hdrText))
|
||||
{
|
||||
if (!TryParseHdrMode(hdrText, out var hdrMode))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
hdrModeOverride = hdrMode;
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(argument, "--strict", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
strictDynlibResolution = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The debug-server endpoint is parsed separately (see
|
||||
// TryGetDebugServerOptions); accept the flag here so it is not
|
||||
// rejected as an unknown option or mistaken for the eboot path.
|
||||
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase) ||
|
||||
argument.StartsWith("--debug-server=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
importTraceLimit = DefaultImportTraceLimit;
|
||||
|
|
@ -1382,199 +1104,12 @@ internal static partial class Program
|
|||
runtimeOptions = new SharpEmuRuntimeOptions
|
||||
{
|
||||
CpuEngine = cpuEngine,
|
||||
SystemLanguage = systemLanguage,
|
||||
StrictDynlibResolution = strictDynlibResolution,
|
||||
ImportTraceLimit = importTraceLimit,
|
||||
};
|
||||
var configuredVideoOptions = LoadConfiguredVideoOptions(ebootPath);
|
||||
videoOptions = (configuredVideoOptions with
|
||||
{
|
||||
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
|
||||
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
|
||||
Width = windowWidthOverride ?? configuredVideoOptions.Width,
|
||||
Height = windowHeightOverride ?? configuredVideoOptions.Height,
|
||||
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
|
||||
RefreshRate = refreshRateOverride ?? configuredVideoOptions.RefreshRate,
|
||||
VSync = vsyncOverride ?? configuredVideoOptions.VSync,
|
||||
HdrMode = hdrModeOverride ?? configuredVideoOptions.HdrMode,
|
||||
OverlayEnabled = overlayEnabledOverride ?? configuredVideoOptions.OverlayEnabled,
|
||||
OverlayMode = overlayModeOverride ?? configuredVideoOptions.OverlayMode,
|
||||
OverlayCorner = overlayCornerOverride ?? configuredVideoOptions.OverlayCorner,
|
||||
}).Normalize();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HostVideoOptions LoadConfiguredVideoOptions(string ebootPath)
|
||||
{
|
||||
var defaults = HostVideoOptions.Default;
|
||||
try
|
||||
{
|
||||
var effective = EffectiveLaunchSettings.Resolve(
|
||||
GuiSettings.Load(),
|
||||
PerGameSettings.Load(TryReadTitleId(ebootPath)));
|
||||
|
||||
var windowMode = TryParseWindowMode(effective.WindowMode, out var parsedWindowMode)
|
||||
? parsedWindowMode
|
||||
: defaults.WindowMode;
|
||||
var scalingMode = TryParseScalingMode(effective.ScalingMode, out var parsedScalingMode)
|
||||
? parsedScalingMode
|
||||
: defaults.ScalingMode;
|
||||
var hasResolution = TryParseResolution(
|
||||
effective.Resolution,
|
||||
out var configuredWidth,
|
||||
out var configuredHeight);
|
||||
var hdrMode = TryParseHdrMode(effective.HdrMode, out var parsedHdrMode)
|
||||
? parsedHdrMode
|
||||
: defaults.HdrMode;
|
||||
|
||||
return new HostVideoOptions
|
||||
{
|
||||
WindowMode = windowMode,
|
||||
ScalingMode = scalingMode,
|
||||
Width = hasResolution ? configuredWidth : defaults.Width,
|
||||
Height = hasResolution ? configuredHeight : defaults.Height,
|
||||
DisplayIndex = effective.DisplayIndex,
|
||||
RefreshRate = effective.RefreshRate,
|
||||
VSync = effective.VSync,
|
||||
HdrMode = hdrMode,
|
||||
OverlayEnabled = effective.OverlayEnabled,
|
||||
OverlayCorner = Enum.TryParse<PerformanceOverlayCorner>(effective.OverlayCorner, true, out var corner)
|
||||
? corner : defaults.OverlayCorner,
|
||||
OverlayMode = Enum.TryParse<PerformanceOverlayMode>(effective.OverlayMode, true, out var mode)
|
||||
? mode : defaults.OverlayMode,
|
||||
}.Normalize();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] GUI video settings could not be loaded; using defaults: {exception.Message}");
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySplitOption(string argument, string name, out string value)
|
||||
{
|
||||
var prefix = name + "=";
|
||||
if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = argument[prefix.Length..];
|
||||
return true;
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseWindowMode(string value, out HostWindowMode mode)
|
||||
{
|
||||
mode = value.ToLowerInvariant() switch
|
||||
{
|
||||
"windowed" => HostWindowMode.Windowed,
|
||||
"borderless" => HostWindowMode.Borderless,
|
||||
"exclusive" or "fullscreen" => HostWindowMode.ExclusiveFullscreen,
|
||||
_ => (HostWindowMode)(-1),
|
||||
};
|
||||
return Enum.IsDefined(mode);
|
||||
}
|
||||
|
||||
private static bool TryParseSystemLanguage(string value, out int language)
|
||||
{
|
||||
language = value.ToLowerInvariant() switch
|
||||
{
|
||||
"japanese" => 0,
|
||||
"englishus" => 1,
|
||||
"french" => 2,
|
||||
"spanishspain" => 3,
|
||||
"german" => 4,
|
||||
"italian" => 5,
|
||||
"dutch" => 6,
|
||||
"portugueseportugal" => 7,
|
||||
"russian" => 8,
|
||||
"korean" => 9,
|
||||
"chinesetraditional" => 10,
|
||||
"chinesesimplified" => 11,
|
||||
"finnish" => 12,
|
||||
"swedish" => 13,
|
||||
"danish" => 14,
|
||||
"norwegian" => 15,
|
||||
"polish" => 16,
|
||||
"portuguesebrazil" => 17,
|
||||
"englishuk" => 18,
|
||||
"turkish" => 19,
|
||||
"spanishlatinamerica" => 20,
|
||||
"arabic" => 21,
|
||||
"frenchcanada" => 22,
|
||||
"czech" => 23,
|
||||
"hungarian" => 24,
|
||||
"greek" => 25,
|
||||
"romanian" => 26,
|
||||
"thai" => 27,
|
||||
"vietnamese" => 28,
|
||||
"indonesian" => 29,
|
||||
"ukrainian" => 30,
|
||||
_ => -1,
|
||||
};
|
||||
return language >= 0;
|
||||
}
|
||||
|
||||
private static bool TryParseScalingMode(string value, out HostScalingMode mode)
|
||||
{
|
||||
mode = value.ToLowerInvariant() switch
|
||||
{
|
||||
"fit" => HostScalingMode.Fit,
|
||||
"cover" => HostScalingMode.Cover,
|
||||
"stretch" => HostScalingMode.Stretch,
|
||||
"integer" => HostScalingMode.Integer,
|
||||
_ => (HostScalingMode)(-1),
|
||||
};
|
||||
return Enum.IsDefined(mode);
|
||||
}
|
||||
|
||||
private static bool TryParseHdrMode(string value, out HostHdrMode mode)
|
||||
{
|
||||
mode = value.ToLowerInvariant() switch
|
||||
{
|
||||
"auto" => HostHdrMode.Auto,
|
||||
"on" or "true" or "1" => HostHdrMode.On,
|
||||
"off" or "false" or "0" => HostHdrMode.Off,
|
||||
_ => (HostHdrMode)(-1),
|
||||
};
|
||||
return Enum.IsDefined(mode);
|
||||
}
|
||||
|
||||
private static bool TryParseResolution(string value, out int width, out int height)
|
||||
{
|
||||
var parts = value.Split('x', 'X');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out width) && int.TryParse(parts[1], out height) &&
|
||||
width >= 640 && height >= 360)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
width = 0;
|
||||
height = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseSwitch(string value, out bool enabled)
|
||||
{
|
||||
if (value is "1" || value.Equals("on", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
enabled = true;
|
||||
return true;
|
||||
}
|
||||
if (value is "0" || value.Equals("off", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
enabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
enabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCpuEngine(string valueText, out CpuExecutionEngine engine)
|
||||
{
|
||||
if (string.Equals(valueText, "native", StringComparison.OrdinalIgnoreCase) ||
|
||||
|
|
@ -1753,7 +1288,7 @@ internal static partial class Program
|
|||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(
|
||||
string? applicationName,
|
||||
string applicationName,
|
||||
StringBuilder commandLine,
|
||||
nint processAttributes,
|
||||
nint threadAttributes,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Debugger\SharpEmu.Debugger.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
|
@ -20,11 +19,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<!-- 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>
|
||||
<!-- A plain "dotnet publish" with no -r defaults $(RuntimeIdentifier) to
|
||||
the host's own RID; see Directory.Build.props, which is where that
|
||||
default actually has to live (PublishDir's RID suffix is decided
|
||||
there, evaluated before this file, so a default set only here would
|
||||
be too late for it). -->
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
|
|
@ -54,18 +48,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
|
@ -77,107 +71,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.LibAtrac9\LICENSE.txt">
|
||||
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
|
||||
<TargetPath>licenses\LibAtrac9.txt</TargetPath>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="KeepLibAtrac9External" BeforeTargets="_ComputeFilesToBundle">
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<ResolvedFileToPublish Update="@(ResolvedFileToPublish)"
|
||||
Condition="'%(Filename)%(Extension)' == 'SharpEmu.LibAtrac9.dll'">
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
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>
|
||||
<RelativePath>plugins\SharpEmu.LibAtrac9.dll</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- These are native debug symbols emitted by Skia/HarfBuzz, not managed
|
||||
symbols that single-file publish can bundle. They are not needed at
|
||||
runtime and would otherwise add more than 100 MB to every release. -->
|
||||
<Target Name="RemoveNativeDebugSymbols" AfterTargets="Publish">
|
||||
<ItemGroup>
|
||||
<_NativeDebugSymbols Include="$(PublishDir)**\*.pdb" />
|
||||
</ItemGroup>
|
||||
<Delete Files="@(_NativeDebugSymbols)" />
|
||||
</Target>
|
||||
|
||||
<!-- Native FFmpeg libraries publish into a subfolder next to the
|
||||
executable instead of sitting loose beside it, so the publish
|
||||
directory stays uncluttered as more native deps get added. The folder
|
||||
name is a fixed constant, not derived from the RID/architecture: each
|
||||
publish output only ever holds one architecture's binaries anyway, so
|
||||
varying the name added a class of bugs (RID resolution timing, host-OS
|
||||
vs. target-RID mixups) for no benefit. Runtime code (FfmpegRuntime)
|
||||
uses the same literal "plugins" folder name. -->
|
||||
<PropertyGroup>
|
||||
<NativeLibraryFolderName>plugins</NativeLibraryFolderName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FfmpegRuntimeTag>3b502d4</FfmpegRuntimeTag>
|
||||
<FfmpegRuntimeDir>
|
||||
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'linux-x64'">ffmpeg-linux-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-x64'">ffmpeg-macos-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">ffmpeg-macos-arm64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimeArchive>$(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)</FfmpegRuntimeArchive>
|
||||
<FfmpegRuntimeExtractDir>$(FfmpegRuntimeDir)/extracted</FfmpegRuntimeExtractDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="FetchFfmpegRuntime"
|
||||
BeforeTargets="Publish;Build"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||
<Unzip
|
||||
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishFfmpegRuntime"
|
||||
AfterTargets="Publish"
|
||||
DependsOnTargets="FetchFfmpegRuntime"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
|
||||
e.g. linux-x64 from a Windows machine is a supported cross-publish,
|
||||
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
|
||||
depends on which platform's ffmpeg-core package was fetched. -->
|
||||
<ItemGroup>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<!-- Mirrors PublishFfmpegRuntime for plain `dotnet build`: devs running
|
||||
straight out of the build output directory (no publish step) still
|
||||
need the FFmpeg plugins present, otherwise AvPlayer/Bink video probing
|
||||
throws NotSupportedException the first time a guest opens a movie. -->
|
||||
<Target Name="BuildFfmpegRuntime"
|
||||
AfterTargets="Build"
|
||||
DependsOnTargets="FetchFfmpegRuntime"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<ItemGroup>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(OutDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="SharpEmu" />
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Required by Avalonia NativeControlHost on Windows 10 and 11. -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
|
|
@ -133,20 +132,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryInstallAdditionalModule(
|
||||
IReadOnlyDictionary<ulong, string> importStubs,
|
||||
IReadOnlyDictionary<string, ulong> runtimeSymbols,
|
||||
out string? error)
|
||||
{
|
||||
if (_nativeCpuBackend is not DirectExecutionBackend backend)
|
||||
{
|
||||
error = "the active CPU backend does not support runtime modules";
|
||||
return false;
|
||||
}
|
||||
|
||||
return backend.TryInstallAdditionalModule(importStubs, runtimeSymbols, out error);
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchEntryCore(
|
||||
ulong entryPoint,
|
||||
Generation generation,
|
||||
|
|
@ -287,23 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
entryFrameDiagnostic,
|
||||
Environment.NewLine,
|
||||
"CpuEngine: native-only");
|
||||
// Frame boundaries an attached debugger observes; null hook = a branch.
|
||||
var debugHook = executionOptions.DebugHook;
|
||||
var debugFrame = debugHook is null
|
||||
? null
|
||||
: new CpuContextDebugFrame(
|
||||
frameKind == EntryFrameKind.ProcessEntry
|
||||
? CpuDebugFrameKind.ProcessEntry
|
||||
: CpuDebugFrameKind.ModuleInitializer,
|
||||
entryPoint,
|
||||
processImageName,
|
||||
context,
|
||||
effectiveImportStubs);
|
||||
debugHook?.OnFrameEnter(debugFrame!);
|
||||
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||
// Let backend stall reports reference the same frame as entry.
|
||||
(_nativeCpuBackend as DirectExecutionBackend)?.SetActiveDebugFrame(debugFrame);
|
||||
if (_nativeCpuBackend.TryExecute(
|
||||
context,
|
||||
entryPoint,
|
||||
|
|
@ -313,7 +282,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
executionOptions,
|
||||
out var nativeResult))
|
||||
{
|
||||
debugHook?.OnFrameExit(debugFrame!, nativeResult);
|
||||
LastSessionSummary = new CpuSessionSummary(
|
||||
nativeResult,
|
||||
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
|
||||
|
|
@ -328,8 +296,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
return nativeResult;
|
||||
}
|
||||
|
||||
debugHook?.OnFrameExit(debugFrame!, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
|
||||
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
|
||||
? "native-backend"
|
||||
: _nativeCpuBackend.BackendName;
|
||||
|
|
@ -727,13 +693,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
return _virtualMemory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the disposed native backend left its session state alive
|
||||
/// because guest workers were still executing guest code. The guest
|
||||
/// address space must then stay mapped as well.
|
||||
/// </summary>
|
||||
internal bool NativeSessionLeaked { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_nativeCpuBackend is IDisposable disposableBackend)
|
||||
|
|
@ -741,7 +700,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||
disposableBackend.Dispose();
|
||||
}
|
||||
|
||||
NativeSessionLeaked = _nativeCpuBackend is DirectExecutionBackend { GuestSessionLeaked: true };
|
||||
_nativeCpuBackend = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,15 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public readonly struct CpuExecutionOptions
|
||||
{
|
||||
public bool EnableDisasmDiagnostics { get; init; }
|
||||
|
||||
|
||||
public CpuExecutionEngine CpuEngine { get; init; }
|
||||
|
||||
public bool StrictDynlibResolution { get; init; }
|
||||
|
||||
public int ImportTraceLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional debugger attached to this execution session. When set, the
|
||||
/// dispatcher notifies it at each frame boundary via
|
||||
/// <see cref="ICpuDebugHook.OnFrameEnter"/> / <see cref="ICpuDebugHook.OnFrameExit"/>.
|
||||
/// Null when no debugger is attached, which is the default and imposes no
|
||||
/// runtime cost.
|
||||
/// </summary>
|
||||
public ICpuDebugHook? DebugHook { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts a live <see cref="CpuContext"/> to <see cref="ICpuDebugFrame"/>. The
|
||||
/// dispatcher creates one of these around the guest context it is about to run
|
||||
/// and passes it to the attached <see cref="ICpuDebugHook"/>; every accessor
|
||||
/// forwards directly to the underlying context.
|
||||
/// </summary>
|
||||
internal sealed class CpuContextDebugFrame : ICpuDebugFrame
|
||||
{
|
||||
private readonly CpuContext _context;
|
||||
|
||||
internal CpuContextDebugFrame(
|
||||
CpuDebugFrameKind kind,
|
||||
ulong entryPoint,
|
||||
string label,
|
||||
CpuContext context,
|
||||
IReadOnlyDictionary<ulong, string> importStubs)
|
||||
{
|
||||
Kind = kind;
|
||||
EntryPoint = entryPoint;
|
||||
Label = label ?? string.Empty;
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
|
||||
}
|
||||
|
||||
public CpuDebugFrameKind Kind { get; }
|
||||
|
||||
public Generation Generation => _context.TargetGeneration;
|
||||
|
||||
public ulong EntryPoint { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public ICpuMemory Memory => _context.Memory;
|
||||
|
||||
public ulong GetRegister(CpuRegister register) => _context[register];
|
||||
|
||||
public void SetRegister(CpuRegister register, ulong value) => _context[register] = value;
|
||||
|
||||
public ulong Rip
|
||||
{
|
||||
get => _context.Rip;
|
||||
set => _context.Rip = value;
|
||||
}
|
||||
|
||||
public ulong Rflags
|
||||
{
|
||||
get => _context.Rflags;
|
||||
set => _context.Rflags = value;
|
||||
}
|
||||
|
||||
public ulong FsBase => _context.FsBase;
|
||||
|
||||
public ulong GsBase => _context.GsBase;
|
||||
|
||||
public void GetXmm(int registerIndex, out ulong low, out ulong high)
|
||||
=> _context.GetXmmRegister(registerIndex, out low, out high);
|
||||
|
||||
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the kind of guest entry frame a debugger is observing. The
|
||||
/// dispatcher enters a fresh frame for the process entry point and for every
|
||||
/// module initializer, so the debug layer can label stops accordingly.
|
||||
/// </summary>
|
||||
public enum CpuDebugFrameKind
|
||||
{
|
||||
/// <summary>The guest process entry point (<c>eboot.bin</c> start).</summary>
|
||||
ProcessEntry,
|
||||
|
||||
/// <summary>A module DT_INIT / initializer routine.</summary>
|
||||
ModuleInitializer,
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>The kind of execution stall the backend detected.</summary>
|
||||
public enum CpuStallKind
|
||||
{
|
||||
/// <summary>
|
||||
/// The guest is repeatedly re-dispatching the same import with no forward
|
||||
/// progress — most commonly a spin on a mutex lock/unlock pair.
|
||||
/// </summary>
|
||||
ImportLoop,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details of a detected stall handed to <see cref="ICpuDebugHook.OnStall"/>.
|
||||
/// Reported from the emulation thread at the point the backend recognises the
|
||||
/// livelock, before it forces the guest out of the loop.
|
||||
/// </summary>
|
||||
public readonly struct CpuStallInfo
|
||||
{
|
||||
public CpuStallInfo(
|
||||
CpuStallKind kind,
|
||||
string? nid,
|
||||
ulong instructionPointer,
|
||||
long dispatchIndex,
|
||||
ulong argument0,
|
||||
ulong argument1,
|
||||
string detail,
|
||||
string? libraryName = null,
|
||||
string? functionName = null)
|
||||
{
|
||||
Kind = kind;
|
||||
Nid = nid;
|
||||
InstructionPointer = instructionPointer;
|
||||
DispatchIndex = dispatchIndex;
|
||||
Argument0 = argument0;
|
||||
Argument1 = argument1;
|
||||
Detail = detail ?? string.Empty;
|
||||
LibraryName = libraryName;
|
||||
FunctionName = functionName;
|
||||
}
|
||||
|
||||
public CpuStallKind Kind { get; }
|
||||
|
||||
/// <summary>The NID of the import being spun on, when known.</summary>
|
||||
public string? Nid { get; }
|
||||
|
||||
/// <summary>The guest return address of the looping import dispatch.</summary>
|
||||
public ulong InstructionPointer { get; }
|
||||
|
||||
/// <summary>The import dispatch counter at detection time.</summary>
|
||||
public long DispatchIndex { get; }
|
||||
|
||||
/// <summary>The first two guest ABI arguments at stall detection.</summary>
|
||||
public ulong Argument0 { get; }
|
||||
|
||||
public ulong Argument1 { get; }
|
||||
|
||||
/// <summary>The resolved HLE export, when the NID is registered.</summary>
|
||||
public string? LibraryName { get; }
|
||||
|
||||
public string? FunctionName { get; }
|
||||
|
||||
public bool IsResolved => !string.IsNullOrWhiteSpace(FunctionName);
|
||||
|
||||
/// <summary>A human-readable one-line summary of the stall.</summary>
|
||||
public string Detail { get; }
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// A live view of the guest CPU state at a dispatch boundary, handed to an
|
||||
/// <see cref="ICpuDebugHook"/> so a debugger can read and mutate registers and
|
||||
/// guest memory without taking a dependency on the concrete
|
||||
/// <c>CpuContext</c>/<c>CpuDispatcher</c> types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The frame instance is only valid for the duration of the hook call that
|
||||
/// receives it (between <see cref="ICpuDebugHook.OnFrameEnter"/> and the
|
||||
/// matching <see cref="ICpuDebugHook.OnFrameExit"/>). Reads and writes are
|
||||
/// forwarded straight to the underlying guest context, so mutations made from
|
||||
/// a hook are observed by the CPU backend when it resumes the frame.
|
||||
/// </remarks>
|
||||
public interface ICpuDebugFrame
|
||||
{
|
||||
/// <summary>The kind of frame being executed.</summary>
|
||||
CpuDebugFrameKind Kind { get; }
|
||||
|
||||
/// <summary>The guest ABI generation this frame targets.</summary>
|
||||
Generation Generation { get; }
|
||||
|
||||
/// <summary>The guest virtual address the frame begins executing at.</summary>
|
||||
ulong EntryPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable label for the frame (process image name or module name).
|
||||
/// </summary>
|
||||
string Label { get; }
|
||||
|
||||
/// <summary>Guest-addressable memory for this frame.</summary>
|
||||
ICpuMemory Memory { get; }
|
||||
|
||||
/// <summary>Reads a general-purpose register.</summary>
|
||||
ulong GetRegister(CpuRegister register);
|
||||
|
||||
/// <summary>Overwrites a general-purpose register.</summary>
|
||||
void SetRegister(CpuRegister register, ulong value);
|
||||
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
ulong Rip { get; set; }
|
||||
|
||||
/// <summary>The flags register.</summary>
|
||||
ulong Rflags { get; set; }
|
||||
|
||||
/// <summary>The FS segment base (guest TLS pointer).</summary>
|
||||
ulong FsBase { get; }
|
||||
|
||||
/// <summary>The GS segment base.</summary>
|
||||
ulong GsBase { get; }
|
||||
|
||||
/// <summary>Reads the 128-bit value of an XMM register.</summary>
|
||||
void GetXmm(int registerIndex, out ulong low, out ulong high);
|
||||
|
||||
/// <summary>
|
||||
/// The import stubs (guest address to NID) resolved for this frame, so a
|
||||
/// debugger can annotate calls into HLE exports.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<ulong, string> ImportStubs { get; }
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// The seam the CPU dispatcher uses to notify an attached debugger when guest
|
||||
/// execution crosses a frame boundary. Implemented outside of Core (for
|
||||
/// example by <c>SharpEmu.Debugger</c>) and supplied through
|
||||
/// <see cref="CpuExecutionOptions.DebugHook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is intentionally coarse-grained: it exposes the entry and exit of each
|
||||
/// dispatched frame rather than per-instruction stepping. Per-instruction
|
||||
/// control requires cooperation from the native execution backend and is layered
|
||||
/// on top of this seam as the backend gains support; keeping the dispatcher-level
|
||||
/// contract stable lets the debugger infrastructure exist independently of that
|
||||
/// work. Implementations must be thread-safe: frames may be dispatched from the
|
||||
/// dedicated emulation thread while a debug server services clients on its own
|
||||
/// threads.
|
||||
/// </remarks>
|
||||
public interface ICpuDebugHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked immediately before the native backend begins executing a frame.
|
||||
/// The debugger may inspect or mutate <paramref name="frame"/> and may block
|
||||
/// the calling thread (for example, to honour a pause request) before
|
||||
/// returning to allow execution to proceed.
|
||||
/// </summary>
|
||||
void OnFrameEnter(ICpuDebugFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a frame completes, whether it returned to the host or
|
||||
/// terminated with an error. <paramref name="frame"/> reflects the final
|
||||
/// guest state.
|
||||
/// </summary>
|
||||
void OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked from the emulation thread when the backend detects an execution
|
||||
/// stall (for example a mutex spin loop) in the running frame, before it
|
||||
/// forces the guest out of the loop. As with <see cref="OnFrameEnter"/>, the
|
||||
/// implementation may inspect <paramref name="frame"/> and block to honour a
|
||||
/// break before returning to let the backend proceed.
|
||||
/// </summary>
|
||||
void OnStall(ICpuDebugFrame frame, CpuStallInfo info);
|
||||
}
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
|
||||
/// bit-manipulation instructions.
|
||||
///
|
||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
|
||||
/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
|
||||
/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
|
||||
/// register-only arithmetic so the exception handler can finish the instruction in software and
|
||||
/// resume, instead of aborting the title.
|
||||
///
|
||||
/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
|
||||
/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
|
||||
/// backend adapter. Each method returns the result already masked to the operand width and, where
|
||||
/// the instruction is defined to touch flags, updates <paramref name="eflags"/> in place. Flags
|
||||
/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
|
||||
/// deterministic across hosts.
|
||||
/// </summary>
|
||||
public static class BmiInstructionEmulator
|
||||
{
|
||||
private const uint FlagCarry = 1u << 0;
|
||||
private const uint FlagZero = 1u << 6;
|
||||
private const uint FlagSign = 1u << 7;
|
||||
private const uint FlagOverflow = 1u << 11;
|
||||
|
||||
private static ulong WidthMask(GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
|
||||
|
||||
private static int WidthBits(GprOperandSize size) => (int)size;
|
||||
|
||||
private static bool SignSet(ulong value, GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
|
||||
|
||||
private static uint WithFlag(uint eflags, uint flag, bool set) =>
|
||||
set ? eflags | flag : eflags & ~flag;
|
||||
|
||||
// Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
|
||||
// the result, and the caller supplies CF because each instruction defines it differently.
|
||||
private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
|
||||
{
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return eflags;
|
||||
}
|
||||
|
||||
/// <summary>ANDN: <c>dest = (~src1) & src2</c>. CF and OF are cleared.</summary>
|
||||
public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var result = (~src1 & src2) & WidthMask(size);
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSI: isolate the lowest set bit, <c>dest = (-src) & src</c>. CF = (src != 0).</summary>
|
||||
public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((0UL - s) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSMSK: mask up to and including the lowest set bit, <c>dest = (src - 1) ^ src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) ^ s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSR: reset the lowest set bit, <c>dest = (src - 1) & src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BEXTR: extract <c>len</c> bits of <paramref name="src"/> starting at bit <c>start</c>, where
|
||||
/// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
|
||||
/// </summary>
|
||||
public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var start = (int)(control & 0xFF);
|
||||
var length = (int)((control >> 8) & 0xFF);
|
||||
|
||||
ulong result;
|
||||
if (start >= bits)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shifted = (src & WidthMask(size)) >> start;
|
||||
if (length == 0)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else if (length >= bits)
|
||||
{
|
||||
result = shifted;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = shifted & ((1UL << length) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
result &= WidthMask(size);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagCarry, false);
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BZHI: zero the bits of <paramref name="src"/> from bit position <c>index[7:0]</c> upward.
|
||||
/// CF is set when the requested position is at or beyond the operand width.
|
||||
/// </summary>
|
||||
public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var n = (int)(index & 0xFF);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (n >= bits)
|
||||
{
|
||||
result = s;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = s & ((1UL << n) - 1);
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.TrailingZeroCount(s)
|
||||
: BitOperations.TrailingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.LeadingZeroCount(s)
|
||||
: BitOperations.LeadingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>RORX: rotate <paramref name="src"/> right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Rorx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var rotate = count & (bits - 1);
|
||||
if (rotate == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
return ((s >> rotate) | (s << (bits - rotate))) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SARX: arithmetic shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Sarx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
if (size == GprOperandSize.Bits64)
|
||||
{
|
||||
return (ulong)((long)src >> shift);
|
||||
}
|
||||
|
||||
return (ulong)(uint)((int)(uint)src >> shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHLX: logical shift left by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shlx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
return (src << shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHRX: logical shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shrx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var shift = count & (bits - 1);
|
||||
return s >> shift;
|
||||
}
|
||||
|
||||
/// <summary>PDEP: deposit contiguous low bits of <paramref name="src"/> into the positions selected by <paramref name="mask"/>. No flags.</summary>
|
||||
public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
var position = 1UL << i;
|
||||
if ((selector & position) != 0)
|
||||
{
|
||||
if (((src >> bit) & 1UL) != 0)
|
||||
{
|
||||
result |= position;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
|
||||
/// <summary>PEXT: gather the bits of <paramref name="src"/> selected by <paramref name="mask"/> into contiguous low bits. No flags.</summary>
|
||||
public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
if ((selector & (1UL << i)) != 0)
|
||||
{
|
||||
if (((src >> i) & 1UL) != 0)
|
||||
{
|
||||
result |= 1UL << bit;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
|
||||
/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
|
||||
/// </summary>
|
||||
public enum GprOperandSize
|
||||
{
|
||||
Bits32 = 32,
|
||||
Bits64 = 64,
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the results of the SHA extension instructions for instruction recovery.
|
||||
/// Dword 0 of a value is bits 31:0 and dword 3 is bits 127:96, as in the Intel SDM.
|
||||
/// </summary>
|
||||
public static class ShaInstructionEmulator
|
||||
{
|
||||
public static UInt128 Sha1Rnds4(UInt128 source1, UInt128 source2, int function)
|
||||
{
|
||||
var a = Dword(source1, 3);
|
||||
var b = Dword(source1, 2);
|
||||
var c = Dword(source1, 1);
|
||||
var d = Dword(source1, 0);
|
||||
var e = 0u;
|
||||
var k = (function & 3) switch
|
||||
{
|
||||
0 => 0x5A827999u,
|
||||
1 => 0x6ED9EBA1u,
|
||||
2 => 0x8F1BBCDCu,
|
||||
_ => 0xCA62C1D6u,
|
||||
};
|
||||
|
||||
// The first message dword already carries E (SHA1NEXTE adds it).
|
||||
for (var round = 0; round < 4; round++)
|
||||
{
|
||||
var next = Sha1Function(function, b, c, d) + BitOperations.RotateLeft(a, 5) + Dword(source2, 3 - round) + e + k;
|
||||
e = d;
|
||||
d = c;
|
||||
c = BitOperations.RotateLeft(b, 30);
|
||||
b = a;
|
||||
a = next;
|
||||
}
|
||||
|
||||
return FromDwords(d, c, b, a);
|
||||
}
|
||||
|
||||
public static UInt128 Sha1Nexte(UInt128 source1, UInt128 source2)
|
||||
{
|
||||
var e = BitOperations.RotateLeft(Dword(source1, 3), 30);
|
||||
return FromDwords(Dword(source2, 0), Dword(source2, 1), Dword(source2, 2), Dword(source2, 3) + e);
|
||||
}
|
||||
|
||||
public static UInt128 Sha1Msg1(UInt128 source1, UInt128 source2)
|
||||
{
|
||||
var w0 = Dword(source1, 3);
|
||||
var w1 = Dword(source1, 2);
|
||||
var w2 = Dword(source1, 1);
|
||||
var w3 = Dword(source1, 0);
|
||||
var w4 = Dword(source2, 3);
|
||||
var w5 = Dword(source2, 2);
|
||||
return FromDwords(w5 ^ w3, w4 ^ w2, w3 ^ w1, w2 ^ w0);
|
||||
}
|
||||
|
||||
public static UInt128 Sha1Msg2(UInt128 source1, UInt128 source2)
|
||||
{
|
||||
var w13 = Dword(source2, 2);
|
||||
var w14 = Dword(source2, 1);
|
||||
var w15 = Dword(source2, 0);
|
||||
var w16 = BitOperations.RotateLeft(Dword(source1, 3) ^ w13, 1);
|
||||
var w17 = BitOperations.RotateLeft(Dword(source1, 2) ^ w14, 1);
|
||||
var w18 = BitOperations.RotateLeft(Dword(source1, 1) ^ w15, 1);
|
||||
var w19 = BitOperations.RotateLeft(Dword(source1, 0) ^ w16, 1);
|
||||
return FromDwords(w19, w18, w17, w16);
|
||||
}
|
||||
|
||||
// SHA256RNDS2 takes its two round-constant-plus-message dwords from XMM0[63:0].
|
||||
public static UInt128 Sha256Rnds2(UInt128 source1, UInt128 source2, UInt128 xmm0)
|
||||
{
|
||||
var a = Dword(source2, 3);
|
||||
var b = Dword(source2, 2);
|
||||
var c = Dword(source1, 3);
|
||||
var d = Dword(source1, 2);
|
||||
var e = Dword(source2, 1);
|
||||
var f = Dword(source2, 0);
|
||||
var g = Dword(source1, 1);
|
||||
var h = Dword(source1, 0);
|
||||
|
||||
for (var round = 0; round < 2; round++)
|
||||
{
|
||||
var t1 = Ch(e, f, g) + BigSigma1(e) + Dword(xmm0, round) + h;
|
||||
var t2 = Maj(a, b, c) + BigSigma0(a);
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + t1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = t1 + t2;
|
||||
}
|
||||
|
||||
return FromDwords(f, e, b, a);
|
||||
}
|
||||
|
||||
public static UInt128 Sha256Msg1(UInt128 source1, UInt128 source2)
|
||||
{
|
||||
var w4 = Dword(source2, 0);
|
||||
var w3 = Dword(source1, 3);
|
||||
var w2 = Dword(source1, 2);
|
||||
var w1 = Dword(source1, 1);
|
||||
var w0 = Dword(source1, 0);
|
||||
return FromDwords(w0 + SmallSigma0(w1), w1 + SmallSigma0(w2), w2 + SmallSigma0(w3), w3 + SmallSigma0(w4));
|
||||
}
|
||||
|
||||
public static UInt128 Sha256Msg2(UInt128 source1, UInt128 source2)
|
||||
{
|
||||
var w14 = Dword(source2, 2);
|
||||
var w15 = Dword(source2, 3);
|
||||
var w16 = Dword(source1, 0) + SmallSigma1(w14);
|
||||
var w17 = Dword(source1, 1) + SmallSigma1(w15);
|
||||
var w18 = Dword(source1, 2) + SmallSigma1(w16);
|
||||
var w19 = Dword(source1, 3) + SmallSigma1(w17);
|
||||
return FromDwords(w16, w17, w18, w19);
|
||||
}
|
||||
|
||||
public static uint Dword(UInt128 value, int index) => (uint)(value >> (32 * index));
|
||||
|
||||
public static UInt128 FromDwords(uint dword0, uint dword1, uint dword2, uint dword3) =>
|
||||
((UInt128)dword3 << 96) | ((UInt128)dword2 << 64) | ((UInt128)dword1 << 32) | dword0;
|
||||
|
||||
private static uint Sha1Function(int function, uint b, uint c, uint d) => (function & 3) switch
|
||||
{
|
||||
0 => (b & c) ^ (~b & d),
|
||||
2 => (b & c) ^ (b & d) ^ (c & d),
|
||||
_ => b ^ c ^ d,
|
||||
};
|
||||
|
||||
private static uint Ch(uint e, uint f, uint g) => (e & f) ^ (~e & g);
|
||||
|
||||
private static uint Maj(uint a, uint b, uint c) => (a & b) ^ (a & c) ^ (b & c);
|
||||
|
||||
private static uint BigSigma0(uint x) =>
|
||||
BitOperations.RotateRight(x, 2) ^ BitOperations.RotateRight(x, 13) ^ BitOperations.RotateRight(x, 22);
|
||||
|
||||
private static uint BigSigma1(uint x) =>
|
||||
BitOperations.RotateRight(x, 6) ^ BitOperations.RotateRight(x, 11) ^ BitOperations.RotateRight(x, 25);
|
||||
|
||||
private static uint SmallSigma0(uint x) =>
|
||||
BitOperations.RotateRight(x, 7) ^ BitOperations.RotateRight(x, 18) ^ (x >> 3);
|
||||
|
||||
private static uint SmallSigma1(uint x) =>
|
||||
BitOperations.RotateRight(x, 17) ^ BitOperations.RotateRight(x, 19) ^ (x >> 10);
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the bit-field results for SSE4a instruction recovery.
|
||||
/// </summary>
|
||||
public static class Sse4aBitFieldEmulator
|
||||
{
|
||||
public static bool IsValidBitField(int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
|
||||
}
|
||||
|
||||
public static ulong ExtractBitField(ulong value, int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
if (len == 0)
|
||||
{
|
||||
len = 64;
|
||||
}
|
||||
|
||||
// Results past bit 63 are undefined; limit the field to the remaining bits.
|
||||
len = Math.Min(len, 64 - idx);
|
||||
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
||||
return (value >> idx) & mask;
|
||||
}
|
||||
|
||||
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
|
||||
{
|
||||
var len = length & 0x3F;
|
||||
var idx = index & 0x3F;
|
||||
if (len == 0)
|
||||
{
|
||||
len = 64;
|
||||
}
|
||||
|
||||
len = Math.Min(len, 64 - idx);
|
||||
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
||||
var destinationClearMask = fieldMask << idx;
|
||||
var sourceField = (source & fieldMask) << idx;
|
||||
return (destination & ~destinationClearMask) | sourceField;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,21 +26,22 @@ public sealed unsafe class CpuPatcher : IDisposable
|
|||
|
||||
internal unsafe class UnsafeCodeReader : Iced.Intel.CodeReader
|
||||
{
|
||||
private readonly byte* _start;
|
||||
private readonly ulong _length;
|
||||
public ulong Position { get; private set; }
|
||||
private byte* _start;
|
||||
private byte* _current;
|
||||
private int _length;
|
||||
|
||||
public UnsafeCodeReader(byte* start, ulong length)
|
||||
public UnsafeCodeReader(byte* start, int length)
|
||||
{
|
||||
_start = start;
|
||||
_current = start;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (Position >= _length)
|
||||
if (_current >= _start + _length)
|
||||
return -1;
|
||||
return _start[Position++];
|
||||
return *_current++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Recover supported instructions only after an illegal-instruction fault.
|
||||
// Leave native instructions and unrelated register values unchanged.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
|
||||
// image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that
|
||||
// area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross-
|
||||
// checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT
|
||||
// layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen
|
||||
// if this offset (or anything before it) were wrong.
|
||||
private const int Win64ContextXmm0Offset = 0x1A0;
|
||||
|
||||
private static int _sse4aSoftwareFallbackAnnounced;
|
||||
private static long _sse4aInstructionsEmulated;
|
||||
private static int _monitorxSoftwareFallbackAnnounced;
|
||||
private static long _monitorxInstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (TryRecoverMonitorxMwaitx(contextRecord, rip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// EXTRQ and INSERTQ need the current vector register values.
|
||||
// Use recovery only when the signal context contains these values.
|
||||
return (OperatingSystem.IsWindows() || _posixXmmContextBridged) &&
|
||||
TryRecoverSse4aExtractInsert(contextRecord, rip);
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
|
||||
{
|
||||
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
|
||||
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
|
||||
var opcode = new byte[3];
|
||||
if (!TryReadExecutableBytes(rip, opcode) ||
|
||||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line
|
||||
// and MWAITX blocks until that line is written (or a timeout elapses). Hosts without
|
||||
// the extension raise #UD on either one. We do not model the monitor itself, only its
|
||||
// observable effect on guest forward progress: MONITORX becomes a no-op (arming a
|
||||
// watch we never honour has no side effect of its own) and MWAITX becomes a plain
|
||||
// thread yield, i.e. treat the awaited condition as already satisfied so the guest
|
||||
// loop keeps making progress instead of executing an illegal opcode forever.
|
||||
if (opcode[2] == 0xFB)
|
||||
{
|
||||
Thread.Yield();
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + 3);
|
||||
|
||||
Interlocked.Increment(ref _monitorxInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
|
||||
!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
|
||||
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
|
||||
if (!isExtrq && !isInsertq)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isImmediateExtrq = isExtrq && instruction.OpCount == 3;
|
||||
var isRegisterExtrq = isExtrq && instruction.OpCount == 2;
|
||||
var isImmediateInsertq = isInsertq && instruction.OpCount == 4;
|
||||
if (!isImmediateExtrq && !isRegisterExtrq && !isImmediateInsertq)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.GetOpKind(0) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destLow = ReadCtxU64(contextRecord, destOffset);
|
||||
if (isExtrq)
|
||||
{
|
||||
int length;
|
||||
int index;
|
||||
if (isRegisterExtrq)
|
||||
{
|
||||
if (instruction.GetOpKind(1) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var controlOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// AMD defines the register form's field length in xmm2[5:0] and its start
|
||||
// index in xmm2[13:8]. Other control bits do not affect the instruction.
|
||||
var control = ReadCtxU64(contextRecord, controlOffset);
|
||||
length = (int)(control & 0x3F);
|
||||
index = (int)((control >> 8) & 0x3F);
|
||||
}
|
||||
else
|
||||
{
|
||||
length = (int)instruction.GetImmediate(1);
|
||||
index = (int)instruction.GetImmediate(2);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
|
||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (instruction.GetOpKind(1) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = (int)instruction.GetImmediate(2);
|
||||
var index = (int)instruction.GetImmediate(3);
|
||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
|
||||
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
|
||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _sse4aInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
|
||||
// explicit switch (rather than arithmetic on the Register enum) to match the style already
|
||||
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
|
||||
private static bool TryGetXmmOffset(Register register, out int offset)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
|
||||
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
|
||||
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
|
||||
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
|
||||
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
|
||||
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
|
||||
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
|
||||
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
|
||||
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
|
||||
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
|
||||
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
|
||||
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
|
||||
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
|
||||
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
|
||||
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
|
||||
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
|
||||
default:
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,79 +23,14 @@ public sealed partial class DirectExecutionBackend
|
|||
private static long _perfHleTotal;
|
||||
private static long _perfHleDispatchTicks;
|
||||
|
||||
private sealed class PerfHleExportCost
|
||||
{
|
||||
public long Calls;
|
||||
public long Ticks;
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, PerfHleExportCost> _perfHleCosts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Name of the export currently being dispatched on this thread, so the
|
||||
/// gateway can attribute its elapsed time once the call returns. Answering
|
||||
/// "which export is worth optimising" needs cost per export, not just call
|
||||
/// counts — a rare expensive call and a hot cheap one look identical in a
|
||||
/// frequency histogram.
|
||||
/// </summary>
|
||||
[System.ThreadStatic]
|
||||
private static string? _perfHleCurrentExport;
|
||||
|
||||
private static long _perfHleFirstTimestamp;
|
||||
|
||||
private static void RecordPerfHleDispatchTime(long ticks)
|
||||
{
|
||||
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
|
||||
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
|
||||
|
||||
var name = _perfHleCurrentExport;
|
||||
if (name is not null)
|
||||
{
|
||||
var cost = _perfHleCosts.GetOrAdd(name, static _ => new PerfHleExportCost());
|
||||
System.Threading.Interlocked.Increment(ref cost.Calls);
|
||||
System.Threading.Interlocked.Add(ref cost.Ticks, ticks);
|
||||
}
|
||||
|
||||
if (calls > 0 && calls % 500000 == 0)
|
||||
{
|
||||
var frequency = (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
var avgUs = (double)total / frequency * 1_000_000.0 / calls;
|
||||
var first = System.Threading.Interlocked.CompareExchange(ref _perfHleFirstTimestamp, 0, 0);
|
||||
var wallSeconds = first == 0
|
||||
? 0
|
||||
: (double)(System.Diagnostics.Stopwatch.GetTimestamp() - first) / frequency;
|
||||
System.Console.Error.WriteLine(
|
||||
$"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us " +
|
||||
$"total_managed_s={(double)total / frequency:F2} " +
|
||||
$"wall_s={wallSeconds:F2} " +
|
||||
$"cores={(wallSeconds > 0 ? total / frequency / wallSeconds : 0):F2}");
|
||||
|
||||
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, PerfHleExportCost>>(_perfHleCosts.Count + 16);
|
||||
foreach (var kvp in _perfHleCosts)
|
||||
{
|
||||
snapshot.Add(kvp);
|
||||
}
|
||||
|
||||
var top = snapshot
|
||||
.OrderByDescending(kvp => System.Threading.Interlocked.Read(ref kvp.Value.Ticks))
|
||||
.Take(12)
|
||||
.Select(kvp =>
|
||||
{
|
||||
var seconds = System.Threading.Interlocked.Read(ref kvp.Value.Ticks) / frequency;
|
||||
var callCount = System.Threading.Interlocked.Read(ref kvp.Value.Calls);
|
||||
var cores = wallSeconds > 0 ? seconds / wallSeconds : 0;
|
||||
var perCallUs = callCount > 0 ? seconds * 1_000_000.0 / callCount : 0;
|
||||
return $"{kvp.Key}: {cores:F2}cores {seconds:F1}s n={callCount} {perCallUs:F2}us/call";
|
||||
});
|
||||
System.Console.Error.WriteLine($"[PERF][HLE] cost: {string.Join(" | ", top)}");
|
||||
|
||||
if (SharpEmu.HLE.GuestFastPath.Enabled)
|
||||
{
|
||||
var fastPath = SharpEmu.HLE.GuestFastPath.SnapshotCounters();
|
||||
System.Console.Error.WriteLine(
|
||||
$"[PERF][HLE] fast_path: self_hits={fastPath.SelfHits} " +
|
||||
$"getspecific_hits={fastPath.GetspecificHits} blocks={fastPath.Blocks}");
|
||||
}
|
||||
var avgUs = (double)total / System.Diagnostics.Stopwatch.Frequency * 1_000_000.0 / calls;
|
||||
System.Console.Error.WriteLine($"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us total_managed_s={(double)total / System.Diagnostics.Stopwatch.Frequency:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,16 +39,7 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
private static void RecordPerfHleCall(string name)
|
||||
{
|
||||
_perfHleCurrentExport = name;
|
||||
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
|
||||
if (total == 1)
|
||||
{
|
||||
System.Threading.Interlocked.CompareExchange(
|
||||
ref _perfHleFirstTimestamp,
|
||||
System.Diagnostics.Stopwatch.GetTimestamp(),
|
||||
0);
|
||||
}
|
||||
|
||||
if (!_perfHleNoDict)
|
||||
{
|
||||
_perfHleCounts.AddOrUpdate(name, 1, static (_, v) => v + 1);
|
||||
|
|
@ -471,29 +397,6 @@ public sealed partial class DirectExecutionBackend
|
|||
return false;
|
||||
}
|
||||
|
||||
// The committed readable run of host pages that contains address.
|
||||
private unsafe static bool TryQueryReadableRange(ulong address, out ulong start, out ulong end)
|
||||
{
|
||||
start = 0;
|
||||
end = 0;
|
||||
if (address <= 65536 || address >= 140737488355328L ||
|
||||
VirtualQuery((void*)address, out var info, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regionEnd = info.BaseAddress + info.RegionSize;
|
||||
if (regionEnd < info.BaseAddress || address > regionEnd - 8 ||
|
||||
info.State != 4096 || !IsReadableProtection(info.Protect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
start = info.BaseAddress;
|
||||
end = regionEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe static bool TryReadStackU64(ulong address, out ulong value)
|
||||
{
|
||||
value = 0uL;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ using System.Runtime.InteropServices;
|
|||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
|
|
@ -20,9 +19,6 @@ public sealed partial class DirectExecutionBackend
|
|||
private static int _lazyCommitTraceCount;
|
||||
private static int _guestAllocatorHoleRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultSkips;
|
||||
private nint _workerAbortStack;
|
||||
private const uint WorkerAbortStackSize = 0x10000u;
|
||||
|
||||
private unsafe void SetupExceptionHandler()
|
||||
{
|
||||
|
|
@ -114,19 +110,15 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
if (MayBeGuestInt41(exceptionRecord) && TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (!OperatingSystem.IsWindows() &&
|
||||
exceptionCode == 3221225477u &&
|
||||
exceptionRecord->NumberParameters >= 2 &&
|
||||
exceptionRecord->ExceptionInformation[0] == 1uL &&
|
||||
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||
exceptionRecord->ExceptionInformation[1]))
|
||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
|
@ -136,32 +128,6 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == 3221225477u && TryResolveGpuFault(exceptionRecord))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
// Only a fault none of the handlers above resolves aborts a TBB worker: a write
|
||||
// to a lazily committed or GPU-tracked page is routine, and aborting the worker
|
||||
// there strands the guest allocator lock it may hold.
|
||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverAmdCompatInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverShaInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
|
|
@ -284,7 +250,7 @@ public sealed partial class DirectExecutionBackend
|
|||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||
if (!TryReadDiagnosticHostQword(stackAddr, out ulong value))
|
||||
if (!TryReadHostQword(stackAddr, out ulong value))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||
break;
|
||||
|
|
@ -301,7 +267,7 @@ public sealed partial class DirectExecutionBackend
|
|||
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
|
||||
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
|
||||
{
|
||||
if (!TryReadDiagnosticHostQword(stackAddr, out var value))
|
||||
if (!TryReadHostQword(stackAddr, out var value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -321,7 +287,6 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
DumpPointerWindow("fault-register-rbx", rbx, 0x60);
|
||||
DumpPointerWindow("fault-register-rsi", rsi, 0x60);
|
||||
DumpPointerWindow("fault-register-rcx", rcx, 0x80);
|
||||
DumpPointerWindow("fault-register-rdi", rdi, 0x60);
|
||||
DumpPointerWindow("fault-register-r13", r13, 0x60);
|
||||
DumpPointerWindow("fault-register-r14", r14, 0x60);
|
||||
|
|
@ -332,12 +297,11 @@ public sealed partial class DirectExecutionBackend
|
|||
ulong frame = rbp;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
if (!IsCanonicalUserAddress(frame) || (frame & 7) != 0)
|
||||
if (frame < 0x10000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!TryReadDiagnosticHostQword(frame, out ulong next) ||
|
||||
!TryReadDiagnosticHostQword(frame + 8, out ulong ret))
|
||||
if (!TryReadHostQword(frame, out ulong next) || !TryReadHostQword(frame + 8, out ulong ret))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
|
||||
break;
|
||||
|
|
@ -365,13 +329,7 @@ public sealed partial class DirectExecutionBackend
|
|||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
||||
byte[] code = new byte[16];
|
||||
DumpHostRegion("RIP", rip);
|
||||
// Guest code can be mapped execute-only, which makes every
|
||||
// host-side read of it fail. The guest memory view still
|
||||
// resolves those pages, so prefer it here: otherwise the
|
||||
// whole call-site report below is silently skipped on
|
||||
// exactly the faults that need it.
|
||||
if (TryReadGuestOrHostBytes(rip, code))
|
||||
if (TryReadHostBytes(rip, code))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
||||
if (code[0] == 100)
|
||||
|
|
@ -389,12 +347,12 @@ public sealed partial class DirectExecutionBackend
|
|||
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
||||
}
|
||||
byte[] before = new byte[16];
|
||||
if (rip > 16 && TryReadGuestOrHostBytes(rip - 16, before))
|
||||
if (rip > 16 && TryReadHostBytes(rip - 16, before))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
||||
}
|
||||
byte[] window = new byte[64];
|
||||
if (rip > 32 && TryReadGuestOrHostBytes(rip - 32, window))
|
||||
if (rip > 32 && TryReadHostBytes(rip - 32, window))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||
}
|
||||
|
|
@ -411,7 +369,7 @@ public sealed partial class DirectExecutionBackend
|
|||
continue;
|
||||
}
|
||||
byte[] callSiteWindow = new byte[48];
|
||||
if (TryReadGuestOrHostBytes(candidate - 24, callSiteWindow))
|
||||
if (TryReadHostBytes(candidate - 24, callSiteWindow))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
|
||||
|
|
@ -423,11 +381,6 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||
}
|
||||
// The faulting instruction is the first thing an access
|
||||
// violation report has to answer, so decode it through guest
|
||||
// memory as well: that path still works when the host code
|
||||
// page refuses a plain read.
|
||||
DumpGuestInstructionStream("fault", rip, 4);
|
||||
DumpRecentImportTrace();
|
||||
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||
DumpGuestRegisterWindowDiagnostics(
|
||||
|
|
@ -472,91 +425,18 @@ public sealed partial class DirectExecutionBackend
|
|||
void* contextRecord,
|
||||
ulong rip)
|
||||
{
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||
rip >= 0x0000000800000000UL ||
|
||||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer ThreadStatic active state; fall back to host-thread name when
|
||||
// concurrent TBB AVs race logging (tLT61: recover skipped, then Fatal).
|
||||
GuestThreadState? activeThread = _activeGuestThreadState;
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var hostName = Thread.CurrentThread.Name;
|
||||
if (hostName is null ||
|
||||
!hostName.StartsWith("SharpEmu-tbb_thead", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activeThread = FindGuestThreadStateByHostThreadId(unchecked((int)GetCurrentThreadId()));
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var skip = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultSkips);
|
||||
if (skip <= 8 || skip % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] tbb_recover skip #{skip}: rip=0x{rip:X16} " +
|
||||
$"host='{hostName}' active={(activeThread?.Name ?? "null")}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var hostExit = ActiveEntryReturnSentinelRip;
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
hostExit = unchecked((ulong)_guestReturnStub);
|
||||
}
|
||||
|
||||
// Prefer worker-abort (SetEvent + ExitThread) over host_exit→RunEpilogue:
|
||||
// the latter FailFasts the process after TBB recover (tLT28/30 silent die).
|
||||
// Do NOT abandon mutexes here — managed HLE from inside VEH can re-enter
|
||||
// and Fatal (tLT73). NativeGuestExecutor.Run abandons after detecting abort.
|
||||
var abortRip = unchecked((ulong)_workerAbortStub);
|
||||
if (abortRip >= 0x10000)
|
||||
{
|
||||
// Do NOT SetEvent from managed VEH: that wakes the renter which may
|
||||
// TerminateThread while this thread is still inside VEH return
|
||||
// (tLTA2: recover logged, no respawning, process die). Abort stub
|
||||
// SetEvent's only after CONTINUE_EXECUTION resumes at park.
|
||||
|
||||
// Prefer the entry-stub-saved host RSP (real CreateThread stack).
|
||||
// Do not treat mid-range host stacks as guest — Astro worker stacks
|
||||
// often sit in 0x02xxxxxx_xxxx and were wrongly replaced with a
|
||||
// shared VirtualAlloc abort stack (concurrent TBB AV → die).
|
||||
var hostRspSlot = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
ulong hostRsp = 0;
|
||||
if (hostRspSlot != 0)
|
||||
{
|
||||
hostRsp = *(ulong*)hostRspSlot;
|
||||
}
|
||||
|
||||
if (hostRsp < 0x10000)
|
||||
{
|
||||
hostRsp = EnsureWorkerAbortStackRsp();
|
||||
}
|
||||
|
||||
if (hostRsp >= 0x10000)
|
||||
{
|
||||
WriteCtxU64(contextRecord, 152, hostRsp & ~0xFUL);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, abortRip);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} " +
|
||||
$"host_rsp=0x{hostRsp:X16} -> worker_abort=0x{abortRip:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: parking native worker (SetEvent+park); " +
|
||||
"renter will TerminateThread+respawn — avoids ExitThread after VEH");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -568,67 +448,13 @@ public sealed partial class DirectExecutionBackend
|
|||
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, hostExit);
|
||||
var recoveryFallback = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recoveryFallback}: " +
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: resumed at host_exit (abort stub unavailable); " +
|
||||
"subsequent FastFail/CLR must not re-enter managed VEH " +
|
||||
"(live trampoline pre-filters 0xC0000409 / 0xE0434352)");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
private GuestThreadState? FindGuestThreadStateByHostThreadId(int hostThreadId)
|
||||
{
|
||||
if (hostThreadId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var thread in SnapshotGuestThreads())
|
||||
{
|
||||
if (Volatile.Read(ref thread.HostThreadId) == hostThreadId)
|
||||
{
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private unsafe ulong EnsureWorkerAbortStackRsp()
|
||||
{
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
_workerAbortStack = (nint)VirtualAlloc(null, WorkerAbortStackSize, 12288u, 4u);
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Grow-down stack: hand out near the top with alignment headroom.
|
||||
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
|
||||
}
|
||||
|
||||
// On Windows an INT n the guest may not execute raises a general-protection fault,
|
||||
// reported as an access violation whose fault address is all ones; a page fault
|
||||
// carries the data address instead. GPU-tracked and lazily committed pages fault
|
||||
// thousands of times per frame, so skip the opcode probe (a VirtualQuery and a
|
||||
// read of the faulting instruction) for them.
|
||||
private static unsafe bool MayBeGuestInt41(EXCEPTION_RECORD* exceptionRecord) =>
|
||||
!OperatingSystem.IsWindows() ||
|
||||
exceptionRecord->NumberParameters < 2 ||
|
||||
exceptionRecord->ExceptionInformation[1] == ulong.MaxValue;
|
||||
|
||||
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||
|
|
@ -637,7 +463,7 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
byte[] opcode = new byte[2];
|
||||
if (!TryReadExecutableBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
|
||||
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -647,31 +473,12 @@ public sealed partial class DirectExecutionBackend
|
|||
if (count <= 16 || count % 65536 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (default-on; set SHARPEMU_IGNORE_INT41=0 to disable)");
|
||||
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Runs after the lazy-commit branch. That branch commits the page first.
|
||||
private unsafe static bool TryResolveGpuFault(EXCEPTION_RECORD* exceptionRecord)
|
||||
{
|
||||
if (exceptionRecord->NumberParameters < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var kind = exceptionRecord->ExceptionInformation[0] switch
|
||||
{
|
||||
0 => FaultKind.Read,
|
||||
1 => FaultKind.Write,
|
||||
8 => FaultKind.Execute,
|
||||
_ => FaultKind.Unknown,
|
||||
};
|
||||
return kind != FaultKind.Unknown
|
||||
&& GuestGpuMemoryHook.TryResolveFault(kind, exceptionRecord->ExceptionInformation[1]);
|
||||
}
|
||||
|
||||
private unsafe static bool TryRecoverGuestAllocatorHole(
|
||||
EXCEPTION_RECORD* exceptionRecord,
|
||||
void* contextRecord,
|
||||
|
|
@ -809,7 +616,7 @@ public sealed partial class DirectExecutionBackend
|
|||
// Optimized guest code frequently omits frame pointers. The return
|
||||
// address at RSP is then more useful than an RBP walk and identifies the
|
||||
// exact call site that supplied the faulting arguments.
|
||||
if (TryReadDiagnosticHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
|
||||
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
|
||||
{
|
||||
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
|
||||
}
|
||||
|
|
@ -1238,7 +1045,7 @@ public sealed partial class DirectExecutionBackend
|
|||
for (int offset = 0; offset < size; offset += 8)
|
||||
{
|
||||
ulong slotAddress = baseAddress + (ulong)offset;
|
||||
if (slotAddress < baseAddress || !TryReadDiagnosticHostQword(slotAddress, out var value))
|
||||
if (!TryReadQword(slotAddress, out var value))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: <unreadable>");
|
||||
break;
|
||||
|
|
@ -1281,11 +1088,6 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
private static bool TryReadHostQword(ulong address, out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
if (address < 0x10000)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// A stray read inside the signal handler would raise a nested
|
||||
|
|
@ -1294,6 +1096,7 @@ public sealed partial class DirectExecutionBackend
|
|||
return TryReadStackU64(address, out value);
|
||||
}
|
||||
|
||||
value = 0;
|
||||
try
|
||||
{
|
||||
value = (ulong)Marshal.ReadInt64((nint)address);
|
||||
|
|
@ -1305,90 +1108,25 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe static bool TryReadDiagnosticHostQword(ulong address, out ulong value)
|
||||
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
if (address < 65536)
|
||||
{
|
||||
ulong result = 0;
|
||||
bool success = TryReadLinuxMemory(address, (byte*)&result, sizeof(ulong));
|
||||
value = success ? result : 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
ulong result = 0;
|
||||
bool success = TryReadMacOsMemory(address, (byte*)&result, sizeof(ulong));
|
||||
value = success ? result : 0;
|
||||
return success;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return TryReadStackU64(address, out value);
|
||||
}
|
||||
|
||||
value = 0;
|
||||
if (!IsReadableHostRange(address, sizeof(ulong)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong readValue = 0;
|
||||
if (ReadProcessMemory(
|
||||
(nint)(-1),
|
||||
(nint)address,
|
||||
&readValue,
|
||||
sizeof(ulong),
|
||||
out var bytesRead) == 0 ||
|
||||
bytesRead != sizeof(ulong))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = readValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
fixed (byte* destination = buffer)
|
||||
// See TryReadHostQword: probe every touched page before reading.
|
||||
ulong end = address + (ulong)buffer.Length;
|
||||
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
||||
{
|
||||
return TryReadLinuxMemory(address, destination, buffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
fixed (byte* destination = buffer)
|
||||
{
|
||||
return TryReadMacOsMemory(address, destination, buffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsReadableHostRange(address, buffer.Length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
fixed (byte* destination = buffer)
|
||||
{
|
||||
return ReadProcessMemory(
|
||||
(nint)(-1),
|
||||
(nint)address,
|
||||
destination,
|
||||
(nuint)buffer.Length,
|
||||
out var bytesRead) != 0 &&
|
||||
bytesRead == (nuint)buffer.Length;
|
||||
if (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
mbi.State != MEM_COMMIT ||
|
||||
!IsReadableProtection(mbi.Protect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1403,176 +1141,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe static bool TryReadLinuxMemory(ulong address, byte* destination, int byteCount)
|
||||
{
|
||||
if (byteCount < 0 || !IsCanonicalUserAddress(address) ||
|
||||
address > 0x0000_8000_0000_0000UL - (ulong)byteCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byteCount == 0) return true;
|
||||
|
||||
// Read through the kernel so untracked or inaccessible pages cannot fault the reader.
|
||||
var local = new DiagnosticMemoryVector { Address = (nint)destination, Length = (nuint)byteCount };
|
||||
var remote = new DiagnosticMemoryVector { Address = (nint)address, Length = (nuint)byteCount };
|
||||
return ReadLinuxProcessMemory(Environment.ProcessId, &local, 1, &remote, 1, 0) == byteCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct DiagnosticMemoryVector
|
||||
{
|
||||
public nint Address;
|
||||
public nuint Length;
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "process_vm_readv")]
|
||||
private unsafe static extern nint ReadLinuxProcessMemory(
|
||||
int processId, DiagnosticMemoryVector* local, nuint localCount,
|
||||
DiagnosticMemoryVector* remote, nuint remoteCount, nuint flags);
|
||||
|
||||
private unsafe static bool TryReadMacOsMemory(ulong address, byte* destination, int byteCount)
|
||||
{
|
||||
if (byteCount < 0 || !IsCanonicalUserAddress(address) ||
|
||||
address > 0x0000_8000_0000_0000UL - (ulong)byteCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byteCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// The host memory table does not contain all guest code mappings.
|
||||
// Use Mach to read the memory and report access errors without another fault.
|
||||
return ReadMachVirtualMemory(GetCurrentMachTask(), address, (ulong)byteCount,
|
||||
(ulong)destination, out var bytesRead) == 0 && bytesRead == (ulong)byteCount;
|
||||
}
|
||||
|
||||
[DllImport("libSystem.B.dylib", EntryPoint = "mach_task_self")]
|
||||
private static extern uint GetCurrentMachTask();
|
||||
|
||||
[DllImport("libSystem.B.dylib", EntryPoint = "mach_vm_read_overwrite")]
|
||||
private static extern int ReadMachVirtualMemory(
|
||||
uint taskHandle, ulong sourceAddress, ulong byteCount, ulong destinationAddress, out ulong bytesRead);
|
||||
|
||||
private unsafe static bool TryReadExecutableBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return TryReadHostBytes(address, buffer);
|
||||
}
|
||||
|
||||
// Faulting x64 code can have execute-only protection. Data probes still require read access.
|
||||
if (!IsReadableHostRange(address, buffer.Length, allowExecuteOnly: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// RIP identifies code that Windows was already executing when the
|
||||
// exception occurred. These mappings are stable for the duration of
|
||||
// dispatch, so read them directly. Arbitrary diagnostic and operand
|
||||
// addresses continue to use ReadProcessMemory above because they can
|
||||
// become invalid while an exception is being reported.
|
||||
new ReadOnlySpan<byte>((void*)address, buffer.Length).CopyTo(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads guest bytes for a crash report, preferring the guest memory view
|
||||
/// over a raw host read. Guest code pages are commonly committed
|
||||
/// PAGE_EXECUTE, which no host read can satisfy.
|
||||
/// </summary>
|
||||
private bool TryReadGuestOrHostBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (_cpuContext is { } context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.Memory.TryRead(address, buffer))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to the host reader.
|
||||
}
|
||||
}
|
||||
|
||||
return TryReadHostBytes(address, buffer);
|
||||
}
|
||||
|
||||
private unsafe static void DumpHostRegion(string name, ulong address)
|
||||
{
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {name} region: <VirtualQuery failed> address=0x{address:X16}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] {name} region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} " +
|
||||
$"state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08} type=0x{mbi.Type:X08}");
|
||||
}
|
||||
|
||||
private unsafe static bool IsReadableHostRange(ulong address, int byteCount, bool allowExecuteOnly = false)
|
||||
{
|
||||
if (byteCount < 0 || !IsCanonicalUserAddress(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = (ulong)byteCount;
|
||||
if (address > 0x0000_8000_0000_0000UL - length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var end = address + length;
|
||||
var cursor = address;
|
||||
while (cursor < end)
|
||||
{
|
||||
if (VirtualQuery(
|
||||
(void*)cursor,
|
||||
out var mbi,
|
||||
(nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
mbi.State != MEM_COMMIT ||
|
||||
(!IsReadableProtection(mbi.Protect) &&
|
||||
!(allowExecuteOnly && mbi.Protect == 0x10)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (regionEnd < mbi.BaseAddress || regionEnd <= cursor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cursor = Math.Min(regionEnd, end);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsCanonicalUserAddress(ulong address) =>
|
||||
address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = false)]
|
||||
private unsafe static extern int ReadProcessMemory(
|
||||
nint process,
|
||||
nint baseAddress,
|
||||
void* buffer,
|
||||
nuint byteCount,
|
||||
out nuint bytesRead);
|
||||
|
||||
private string FormatPointerWithNearestSymbol(ulong value)
|
||||
{
|
||||
string text = $"0x{value:X16}";
|
||||
|
|
@ -1586,10 +1154,9 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
private void InitializeRuntimeSymbolIndex(IReadOnlyDictionary<string, ulong> runtimeSymbols)
|
||||
{
|
||||
var symbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
|
||||
_runtimeSymbolsByName.Clear();
|
||||
if (runtimeSymbols.Count == 0)
|
||||
{
|
||||
Volatile.Write(ref _runtimeSymbolsByName, symbolsByName);
|
||||
_runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1600,12 +1167,11 @@ public sealed partial class DirectExecutionBackend
|
|||
if (runtimeSymbol.Value != 0L && !string.IsNullOrWhiteSpace(runtimeSymbol.Key))
|
||||
{
|
||||
list.Add(runtimeSymbol);
|
||||
symbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
|
||||
_runtimeSymbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
|
||||
}
|
||||
}
|
||||
|
||||
list.Sort((a, b) => a.Value.CompareTo(b.Value));
|
||||
Volatile.Write(ref _runtimeSymbolsByName, symbolsByName);
|
||||
_runtimeSymbolsByAddress = list.ToArray();
|
||||
}
|
||||
|
||||
|
|
@ -1740,7 +1306,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
|
|
@ -1800,7 +1365,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
|
|
@ -1902,25 +1466,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
}
|
||||
|
||||
// Re-scans a just-committed window for FS:[0] TLS loads; skips non-executable commits.
|
||||
private unsafe void RescanTlsPatternsIfExecutable(ulong committedBase, ulong committedSize, uint commitProtect)
|
||||
{
|
||||
const uint executableProtectionMask = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
if ((commitProtect & executableProtectionMask) == 0 || committedSize == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var counts = PatchTlsPatternsInRange(committedBase, committedBase + committedSize);
|
||||
if (counts.Total != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Patched {counts.Loads} TLS loads, {counts.Stores} TLS stores, " +
|
||||
$"{counts.StackCanaries} stack-canary accesses " +
|
||||
$"(lazy-commit rescan 0x{committedBase:X16}-0x{committedBase + committedSize:X16})");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldTraceLazyCommit(int traceIndex)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
||||
|
|
|
|||
|
|
@ -1,286 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Native fast-path stubs for the trivial per-thread imports that dominate the
|
||||
/// HLE call count. Each stub answers from <see cref="GuestFastPath"/>'s
|
||||
/// per-host-thread block and jumps to the normal import trampoline whenever it
|
||||
/// cannot, so the managed implementation stays the single source of truth for
|
||||
/// every case the stub does not handle.
|
||||
/// </summary>
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
internal enum GuestFastPathStub
|
||||
{
|
||||
None,
|
||||
PthreadSelf,
|
||||
PthreadGetspecific,
|
||||
MemoryCopy,
|
||||
}
|
||||
|
||||
// scePthreadSelf / pthread_self.
|
||||
private const string PthreadSelfNid = "aI+OeCz8xrQ";
|
||||
private const string PosixPthreadSelfNid = "EotR8a3ASf4";
|
||||
|
||||
// scePthreadGetspecific / pthread_getspecific.
|
||||
private const string PthreadGetspecificNid = "eoht7mQOCmo";
|
||||
private const string PosixPthreadGetspecificNid = "0-KXaS70xy4";
|
||||
|
||||
// memcpy / memmove.
|
||||
private const string MemcpyNid = "Q3VBxCXhUHs";
|
||||
private const string MemmoveNid = "+P6FRGH4LfA";
|
||||
|
||||
// Guest memory is identity-mapped and every fault the copy can take (write tracking, lazy
|
||||
// commit) is resolved from the fault address alone, so the copy may run as guest-side code.
|
||||
// SHARPEMU_HLE_FAST_MEMCPY=0 keeps it on the managed export.
|
||||
private static readonly bool GuestMemoryCopyStubEnabled =
|
||||
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_MEMCPY"), "0", StringComparison.Ordinal);
|
||||
|
||||
private uint _fastPathBlockTlsIndex = uint.MaxValue;
|
||||
private bool _guestFastPathEnabled;
|
||||
private int _guestFastPathStubCount;
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether native fast-path stubs may be installed for this run and
|
||||
/// arms <see cref="GuestFastPath"/> if so. Any diagnostic mode that must
|
||||
/// observe every import call keeps the fast path off, because a stub answers
|
||||
/// without ever reaching the managed gateway that does the tracing.
|
||||
/// </summary>
|
||||
private void ConfigureGuestFastPath()
|
||||
{
|
||||
_guestFastPathEnabled = false;
|
||||
_guestFastPathStubCount = 0;
|
||||
|
||||
// Emission uses the macOS pthread gs-indexed key array. Windows and
|
||||
// Linux keep the managed gateway until their TLS read is emitted too.
|
||||
if (!OperatingSystem.IsMacOS() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_PATH"), "0", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] HLE native fast path disabled by SHARPEMU_HLE_FAST_PATH=0.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_logAllImports ||
|
||||
!string.IsNullOrEmpty(_importFilter) ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] HLE native fast path disabled: import/pthread tracing needs every call in the gateway.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fastPathBlockTlsIndex == uint.MaxValue)
|
||||
{
|
||||
_fastPathBlockTlsIndex = TlsAlloc();
|
||||
if (_fastPathBlockTlsIndex == uint.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tlsIndex = _fastPathBlockTlsIndex;
|
||||
GuestFastPath.Enable(block => TlsSetValue(tlsIndex, block));
|
||||
_guestFastPathEnabled = true;
|
||||
}
|
||||
|
||||
private static GuestFastPathStub ClassifyGuestFastPathStub(string nid) => nid switch
|
||||
{
|
||||
PthreadSelfNid or PosixPthreadSelfNid => GuestFastPathStub.PthreadSelf,
|
||||
PthreadGetspecificNid or PosixPthreadGetspecificNid => GuestFastPathStub.PthreadGetspecific,
|
||||
MemcpyNid or MemmoveNid => GuestFastPathStub.MemoryCopy,
|
||||
_ => GuestFastPathStub.None,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds the native stub for <paramref name="nid"/>, falling back to
|
||||
/// <paramref name="fallbackTrampoline"/> (the import's managed gateway
|
||||
/// trampoline) for every case the stub declines.
|
||||
/// </summary>
|
||||
private unsafe bool TryCreateGuestFastPathStub(string nid, nint fallbackTrampoline, out nint address)
|
||||
{
|
||||
address = 0;
|
||||
if (!_guestFastPathEnabled || fallbackTrampoline == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var kind = ClassifyGuestFastPathStub(nid);
|
||||
if (kind == GuestFastPathStub.None ||
|
||||
// The write watch inspects every managed guest write, which a native copy never reaches.
|
||||
(kind == GuestFastPathStub.MemoryCopy && (!GuestMemoryCopyStubEnabled || GuestWriteWatch.Armed)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var code = EmitGuestFastPathStub(kind, _fastPathBlockTlsIndex, fallbackTrampoline);
|
||||
const uint stubAllocationSize = 128u;
|
||||
if (code.Count > stubAllocationSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void* memory = VirtualAlloc(null, stubAllocationSize, 12288u, 64u);
|
||||
if (memory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < code.Count; i++)
|
||||
{
|
||||
((byte*)memory)[i] = code[i];
|
||||
}
|
||||
|
||||
uint oldProtect = 0;
|
||||
if (!VirtualProtect(memory, stubAllocationSize, 32u, &oldProtect))
|
||||
{
|
||||
VirtualFree(memory, 0u, 32768u);
|
||||
return false;
|
||||
}
|
||||
|
||||
FlushInstructionCache(GetCurrentProcess(), memory, (nuint)code.Count);
|
||||
address = (nint)memory;
|
||||
_importHandlerTrampolines.Add(address);
|
||||
_guestFastPathStubCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits one fast-path stub. The stubs obey the SysV ABI the guest calls
|
||||
/// them with: the answer goes in RAX, only RAX and the arithmetic flags are
|
||||
/// clobbered (both caller-saved across a call), and on the slow path every
|
||||
/// register - including the incoming RAX that carries the variadic vector
|
||||
/// count - is restored before jumping to the trampoline.
|
||||
/// </summary>
|
||||
internal static List<byte> EmitGuestFastPathStub(
|
||||
GuestFastPathStub kind,
|
||||
uint blockTlsIndex,
|
||||
nint fallbackTrampoline)
|
||||
{
|
||||
if (kind == GuestFastPathStub.MemoryCopy)
|
||||
{
|
||||
return EmitGuestMemoryCopyStub(fallbackTrampoline);
|
||||
}
|
||||
|
||||
var code = new List<byte>(48);
|
||||
var slowPathFixups = new List<int>();
|
||||
|
||||
void Emit(params byte[] bytes) => code.AddRange(bytes);
|
||||
|
||||
// Emits a short conditional branch to the slow path; 0x74 is JE, 0x73 is JAE.
|
||||
void EmitBranchToSlowPath(byte opcode)
|
||||
{
|
||||
Emit(opcode, 0x00); // rel8, patched once the slow-path offset is known
|
||||
slowPathFixups.Add(code.Count - 1);
|
||||
}
|
||||
|
||||
void EmitJumpToSlowPath() => EmitBranchToSlowPath(0x74);
|
||||
|
||||
// push rax: the guest's incoming RAX/AL must survive a slow-path bail.
|
||||
Emit(0x50);
|
||||
|
||||
// mov rax, gs:[blockTlsIndex * 8] - the macOS pthread key array is the
|
||||
// gs-based thread specific data array, so a key read is a single load.
|
||||
Emit(0x65, 0x48, 0x8B, 0x04, 0x25);
|
||||
code.AddRange(BitConverter.GetBytes(checked(blockTlsIndex * 8u)));
|
||||
|
||||
// test rax, rax / je slow: no block published on this host thread.
|
||||
Emit(0x48, 0x85, 0xC0);
|
||||
EmitJumpToSlowPath();
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case GuestFastPathStub.PthreadSelf:
|
||||
// cmp qword [rax + SelfHandleOffset], 0 / je slow: no guest
|
||||
// thread bound here, and a host thread's pthread_self has to
|
||||
// reach the managed export for its scheduler registration.
|
||||
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockSelfHandleOffset), 0x00);
|
||||
EmitJumpToSlowPath();
|
||||
// inc qword [rax + SelfHitsOffset]
|
||||
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockSelfHitsOffset));
|
||||
// mov rax, [rax + SelfHandleOffset]
|
||||
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockSelfHandleOffset));
|
||||
break;
|
||||
|
||||
case GuestFastPathStub.PthreadGetspecific:
|
||||
// cmp qword [rax + TlsValuesOffset], 0 / je slow: this thread
|
||||
// has no value table published.
|
||||
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockTlsValuesOffset), 0x00);
|
||||
EmitJumpToSlowPath();
|
||||
// cmp rdi, TlsSlotCount / jae slow. An out-of-range or
|
||||
// sign-extended negative key is answered by the managed path,
|
||||
// which truncates it to int and consults the key registry.
|
||||
Emit(0x48, 0x81, 0xFF);
|
||||
code.AddRange(BitConverter.GetBytes(GuestFastPath.TlsSlotCount));
|
||||
EmitBranchToSlowPath(0x73);
|
||||
// inc qword [rax + GetspecificHitsOffset]
|
||||
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockGetspecificHitsOffset));
|
||||
// mov rax, [rax + TlsValuesOffset] / mov rax, [rax + rdi*8]
|
||||
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockTlsValuesOffset));
|
||||
Emit(0x48, 0x8B, 0x04, 0xF8);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown fast-path stub kind.");
|
||||
}
|
||||
|
||||
// add rsp, 8 (drop the saved RAX - the result replaces it) / ret
|
||||
Emit(0x48, 0x83, 0xC4, 0x08);
|
||||
Emit(0xC3);
|
||||
|
||||
var slowPathOffset = code.Count;
|
||||
|
||||
// pop rax / jmp qword [rip+0] - the absolute target follows the
|
||||
// instruction, so nothing else is clobbered on the way out.
|
||||
Emit(0x58);
|
||||
Emit(0xFF, 0x25, 0x00, 0x00, 0x00, 0x00);
|
||||
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
|
||||
|
||||
foreach (var fixup in slowPathFixups)
|
||||
{
|
||||
var delta = slowPathOffset - (fixup + 1);
|
||||
if (delta is < 0 or > 127)
|
||||
{
|
||||
throw new InvalidOperationException("Fast-path stub slow-path branch is out of rel8 range.");
|
||||
}
|
||||
|
||||
code[fixup] = (byte)delta;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
|
||||
/// export copies with memmove semantics, so a destination that starts inside the source
|
||||
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
|
||||
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
|
||||
/// flag is clear on entry per the SysV ABI.
|
||||
/// </summary>
|
||||
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
|
||||
{
|
||||
var code = new List<byte>(40);
|
||||
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
|
||||
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
|
||||
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
|
||||
var slowPathFixup = code.Count - 1;
|
||||
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
|
||||
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
|
||||
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
|
||||
// jmp qword [rip+0] with the absolute trampoline address after it.
|
||||
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
|
||||
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Sampling profiler for guest code. Managed profilers only see the emulator's
|
||||
/// own frames — once a guest thread is running translated code it is opaque to
|
||||
/// them, so a title that burns its cores inside its own spin loops looks like
|
||||
/// unattributed native time. This walks the guest thread registry and samples
|
||||
/// each thread's host RIP, which lands directly on the guest instruction being
|
||||
/// executed.
|
||||
/// </summary>
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private static readonly bool _profileGuestRip =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly int _profileGuestRipIntervalMs =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_INTERVAL_MS"),
|
||||
out var interval) && interval > 0
|
||||
? interval
|
||||
: 2;
|
||||
|
||||
private static readonly int _profileGuestRipReportSeconds =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_REPORT_S"),
|
||||
out var report) && report > 0
|
||||
? report
|
||||
: 15;
|
||||
|
||||
private static readonly string? _profileGuestRipThreadFilter =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_THREAD");
|
||||
|
||||
private const ulong GuestImageBase = 0x0000_0008_0000_0000UL;
|
||||
private const ulong GuestImageLimit = 0x0000_0009_0000_0000UL;
|
||||
|
||||
private int _guestRipSamplerStarted;
|
||||
private readonly ConcurrentDictionary<ulong, long> _guestRipSamples = new();
|
||||
private readonly ConcurrentDictionary<string, long> _guestRipThreadSamples = new();
|
||||
private readonly ConcurrentDictionary<string, long> _guestWaitSamples = new();
|
||||
private readonly ConcurrentDictionary<string, long> _guestThreadWaitSamples = new();
|
||||
private readonly ConcurrentDictionary<string, long> _guestThreadWaitReasonSamples = new();
|
||||
private long _guestRipTotalSamples;
|
||||
private long _guestWaitTotalSamples;
|
||||
private long _guestRipCaptureFailures;
|
||||
private long _guestRipSamplerErrors;
|
||||
|
||||
private readonly record struct GuestRipSampleTarget(
|
||||
string Name,
|
||||
CpuContext Context,
|
||||
int HostThreadId,
|
||||
string? BlockReason);
|
||||
|
||||
/// <summary>
|
||||
/// Names the HLE call a thread is parked in, using the guest RIP the import
|
||||
/// dispatcher left on its context.
|
||||
/// </summary>
|
||||
private string ResolveWaitLabel(CpuContext context, string? blockReason)
|
||||
{
|
||||
var importIndex = context.ActiveImportIndex;
|
||||
if ((uint)importIndex >= (uint)_importEntries.Length)
|
||||
{
|
||||
// Host code with no import in flight: the thread is parked by the
|
||||
// emulator's own scheduler. The cooperative block records why, which
|
||||
// is the part that actually identifies what the frame is waiting on.
|
||||
return string.IsNullOrEmpty(blockReason)
|
||||
? "<idle-or-scheduler>"
|
||||
: $"blocked:{blockReason}";
|
||||
}
|
||||
|
||||
var entry = _importEntries[importIndex];
|
||||
return entry.Export?.Name ?? entry.Nid;
|
||||
}
|
||||
|
||||
internal void ClearActiveImportIndex()
|
||||
{
|
||||
if (!_profileGuestRip)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var context = ActiveCpuContext;
|
||||
if (context is not null)
|
||||
{
|
||||
context.ActiveImportIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureGuestRipSampler()
|
||||
{
|
||||
if (!_profileGuestRip ||
|
||||
!OperatingSystem.IsWindows() ||
|
||||
Interlocked.Exchange(ref _guestRipSamplerStarted, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sampler = new Thread(GuestRipSampleLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu guest RIP sampler",
|
||||
// Sampling suspends guest threads briefly. Keep this diagnostic below
|
||||
// the title workers so it observes them without becoming the bottleneck.
|
||||
Priority = ThreadPriority.BelowNormal,
|
||||
};
|
||||
sampler.Start();
|
||||
Console.Error.WriteLine(
|
||||
$"[PERF][GUEST] RIP sampler started: interval={_profileGuestRipIntervalMs}ms " +
|
||||
$"report={_profileGuestRipReportSeconds}s " +
|
||||
$"thread={_profileGuestRipThreadFilter ?? "<all>"}");
|
||||
}
|
||||
|
||||
private void GuestRipSampleLoop()
|
||||
{
|
||||
var clock = Stopwatch.StartNew();
|
||||
var lastReportMs = 0L;
|
||||
var lastReportSamples = 0L;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var guestThreads = SnapshotGuestRipTargets();
|
||||
if (!string.IsNullOrWhiteSpace(_profileGuestRipThreadFilter))
|
||||
{
|
||||
guestThreads = guestThreads
|
||||
.Where(thread => thread.Name.Contains(
|
||||
_profileGuestRipThreadFilter,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
var sampledHostThreads = new HashSet<int>();
|
||||
foreach (var thread in guestThreads)
|
||||
{
|
||||
var hostThreadId = thread.HostThreadId;
|
||||
if (hostThreadId == 0 || !sampledHostThreads.Add(hostThreadId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryCaptureHostThreadContext(hostThreadId, out var snapshot) ||
|
||||
!snapshot.IsValid)
|
||||
{
|
||||
Interlocked.Increment(ref _guestRipCaptureFailures);
|
||||
continue;
|
||||
}
|
||||
|
||||
_guestRipSamples.AddOrUpdate(snapshot.Rip, 1, static (_, value) => value + 1);
|
||||
_guestRipThreadSamples.AddOrUpdate(
|
||||
string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name,
|
||||
1,
|
||||
static (_, value) => value + 1);
|
||||
Interlocked.Increment(ref _guestRipTotalSamples);
|
||||
|
||||
// A host RIP means the thread is inside the emulator rather
|
||||
// than running translated code. DispatchImport parks the
|
||||
// guest RIP on the import stub for the call being serviced,
|
||||
// so the stub address names what the thread is waiting on —
|
||||
// no hot-path bookkeeping needed to find out.
|
||||
if (snapshot.Rip >= GuestImageBase && snapshot.Rip < GuestImageLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var threadName = string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name;
|
||||
var waitLabel = ResolveWaitLabel(thread.Context, thread.BlockReason);
|
||||
_guestWaitSamples.AddOrUpdate(
|
||||
waitLabel,
|
||||
1,
|
||||
static (_, value) => value + 1);
|
||||
_guestThreadWaitSamples.AddOrUpdate(
|
||||
threadName,
|
||||
1,
|
||||
static (_, value) => value + 1);
|
||||
_guestThreadWaitReasonSamples.AddOrUpdate(
|
||||
$"{threadName}\u001F{waitLabel}",
|
||||
1,
|
||||
static (_, value) => value + 1);
|
||||
Interlocked.Increment(ref _guestWaitTotalSamples);
|
||||
}
|
||||
|
||||
Thread.Sleep(_profileGuestRipIntervalMs);
|
||||
|
||||
var elapsedMs = clock.ElapsedMilliseconds;
|
||||
if (elapsedMs - lastReportMs < _profileGuestRipReportSeconds * 1000L)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var samples = Interlocked.Read(ref _guestRipTotalSamples);
|
||||
ReportGuestRipSamples(samples - lastReportSamples, (elapsedMs - lastReportMs) / 1000.0);
|
||||
lastReportMs = elapsedMs;
|
||||
lastReportSamples = samples;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// A title can tear down a thread or its context during a capture.
|
||||
// The profiler must never silently die or affect guest execution.
|
||||
if (Interlocked.Increment(ref _guestRipSamplerErrors) == 1)
|
||||
{
|
||||
Console.Error.WriteLine($"[PERF][GUEST] sampler recovery: {exception.GetType().Name}: {exception.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GuestRipSampleTarget[] SnapshotGuestRipTargets()
|
||||
{
|
||||
using (LockGate("SnapshotGuestRipTargets"))
|
||||
{
|
||||
var targets = new GuestRipSampleTarget[_guestThreads.Count + _externalGuestThreads.Count];
|
||||
var index = 0;
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
targets[index++] = new GuestRipSampleTarget(
|
||||
thread.Name,
|
||||
thread.Context,
|
||||
Volatile.Read(ref thread.HostThreadId),
|
||||
thread.BlockReason);
|
||||
}
|
||||
|
||||
foreach (var pair in _externalGuestThreads)
|
||||
{
|
||||
var thread = pair.Value;
|
||||
targets[index++] = new GuestRipSampleTarget(
|
||||
thread.Name,
|
||||
thread.Context,
|
||||
Volatile.Read(ref thread.HostThreadId),
|
||||
null);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportGuestRipSamples(long windowSamples, double windowSeconds)
|
||||
{
|
||||
if (windowSamples == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var byRip = new List<KeyValuePair<ulong, long>>(_guestRipSamples.Count + 16);
|
||||
foreach (var pair in _guestRipSamples)
|
||||
{
|
||||
byRip.Add(pair);
|
||||
}
|
||||
|
||||
// A tight spin lands on a handful of instructions; grouping by 4 KB page
|
||||
// as well shows which routine those instructions belong to.
|
||||
var byPage = new Dictionary<ulong, long>();
|
||||
foreach (var pair in byRip)
|
||||
{
|
||||
var page = pair.Key & ~0xFFFUL;
|
||||
byPage[page] = byPage.TryGetValue(page, out var existing)
|
||||
? existing + pair.Value
|
||||
: pair.Value;
|
||||
}
|
||||
|
||||
var byThread = new List<KeyValuePair<string, long>>(_guestRipThreadSamples.Count + 16);
|
||||
foreach (var pair in _guestRipThreadSamples)
|
||||
{
|
||||
byThread.Add(pair);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[PERF][GUEST] window={windowSamples} in {windowSeconds:F1}s " +
|
||||
$"capture_failures={Interlocked.Read(ref _guestRipCaptureFailures)}");
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[PERF][GUEST] top_rip: " +
|
||||
string.Join(
|
||||
" | ",
|
||||
byRip.OrderByDescending(pair => pair.Value)
|
||||
.Take(12)
|
||||
.Select(pair =>
|
||||
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[PERF][GUEST] top_page: " +
|
||||
string.Join(
|
||||
" | ",
|
||||
byPage.OrderByDescending(pair => pair.Value)
|
||||
.Take(8)
|
||||
.Select(pair =>
|
||||
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
|
||||
|
||||
var byWait = new List<KeyValuePair<string, long>>(_guestWaitSamples.Count + 16);
|
||||
foreach (var pair in _guestWaitSamples)
|
||||
{
|
||||
byWait.Add(pair);
|
||||
}
|
||||
|
||||
var waitTotal = Interlocked.Read(ref _guestWaitTotalSamples);
|
||||
Console.Error.WriteLine(
|
||||
$"[PERF][GUEST] waiting={waitTotal * 100.0 / windowSamples:F1}% of guest thread-time; top_wait: " +
|
||||
string.Join(
|
||||
" | ",
|
||||
byWait.OrderByDescending(pair => pair.Value)
|
||||
.Take(12)
|
||||
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[PERF][GUEST] thread_wait: " +
|
||||
string.Join(
|
||||
" | ",
|
||||
_guestThreadWaitReasonSamples.OrderByDescending(pair => pair.Value)
|
||||
.Take(16)
|
||||
.Select(pair =>
|
||||
{
|
||||
var separator = pair.Key.IndexOf('\u001F');
|
||||
var threadName = separator >= 0 ? pair.Key[..separator] : pair.Key;
|
||||
var waitLabel = separator >= 0 ? pair.Key[(separator + 1)..] : "<unknown>";
|
||||
return $"{threadName}->{waitLabel}={pair.Value * 100.0 / windowSamples:F1}%";
|
||||
})));
|
||||
|
||||
// Per-thread spin/park split. The global wait share mixes the job pool in
|
||||
// with a dozen dormant threads, which hides the number that matters:
|
||||
// how much of a core each worker actually burns.
|
||||
Console.Error.WriteLine(
|
||||
"[PERF][GUEST] thread_split (running/parked): " +
|
||||
string.Join(
|
||||
" | ",
|
||||
byThread.OrderByDescending(pair => pair.Value)
|
||||
.Take(10)
|
||||
.Select(pair =>
|
||||
{
|
||||
var parked = _guestThreadWaitSamples.TryGetValue(pair.Key, out var wait) ? wait : 0;
|
||||
var running = pair.Value - parked;
|
||||
return $"{pair.Key}={running * 100.0 / pair.Value:F0}%/{parked * 100.0 / pair.Value:F0}%";
|
||||
})));
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[PERF][GUEST] top_thread: " +
|
||||
string.Join(
|
||||
" | ",
|
||||
byThread.OrderByDescending(pair => pair.Value)
|
||||
.Take(10)
|
||||
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
|
||||
|
||||
_guestRipSamples.Clear();
|
||||
_guestRipThreadSamples.Clear();
|
||||
_guestWaitSamples.Clear();
|
||||
_guestThreadWaitSamples.Clear();
|
||||
_guestThreadWaitReasonSamples.Clear();
|
||||
Interlocked.Exchange(ref _guestWaitTotalSamples, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tags a sampled address with the region it belongs to. Guest module code
|
||||
/// lives above the image base; anything else is emulator or system code that
|
||||
/// the managed profiler already covers.
|
||||
/// </summary>
|
||||
private string DescribeGuestAddress(ulong address)
|
||||
{
|
||||
|
||||
|
||||
|
||||
if (address >= GuestImageBase && address < GuestImageLimit)
|
||||
{
|
||||
return $"(app+0x{address - GuestImageBase:X})";
|
||||
}
|
||||
|
||||
for (var index = 0; index < _importEntries.Length; index++)
|
||||
{
|
||||
if (_importEntries[index].Address == (address & ~0xFUL))
|
||||
{
|
||||
return $"(stub:{_importEntries[index].Nid})";
|
||||
}
|
||||
}
|
||||
|
||||
return "(host)";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
private sealed class GuestSchedulerThreadStats
|
||||
{
|
||||
public string Name = string.Empty;
|
||||
public long ReadyTimestamp;
|
||||
public long BlockTimestamp;
|
||||
public long ScheduledTimestamp;
|
||||
public long RunTimestamp;
|
||||
public long ReadyCount;
|
||||
public long ClaimCount;
|
||||
public long ScheduleCount;
|
||||
public long RunCount;
|
||||
public long BlockCount;
|
||||
public long ResumeCount;
|
||||
public long ReadyTicks;
|
||||
public long BlockTicks;
|
||||
public long ScheduleTicks;
|
||||
public long RunTicks;
|
||||
public long MaxReadyTicks;
|
||||
public long MaxBlockTicks;
|
||||
public long MaxScheduleTicks;
|
||||
public long MaxRunTicks;
|
||||
public string? LastBlockReason;
|
||||
public int Completed;
|
||||
}
|
||||
|
||||
private static readonly bool _profileGuestScheduler =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER"),
|
||||
"1",
|
||||
StringComparison.Ordinal) ||
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, GuestSchedulerThreadStats> _guestSchedulerStats = new();
|
||||
private readonly GuestThreadFlowProfile _guestThreadFlowProfile = new();
|
||||
private readonly object _guestTraceOutputGate = new();
|
||||
private long _guestSchedulerProfileWindowStart = Stopwatch.GetTimestamp();
|
||||
private long _guestSchedulerProfileNextReport =
|
||||
Stopwatch.GetTimestamp() + (Stopwatch.Frequency * 5L);
|
||||
private int _guestSchedulerProfileReporting;
|
||||
|
||||
private void StartGuestFlowTraces()
|
||||
{
|
||||
lock (_guestTraceOutputGate)
|
||||
{
|
||||
_guestThreadFlowProfile.StartSession();
|
||||
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.StartSession();
|
||||
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.StartSession();
|
||||
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.StartSession();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteGuestFlowTraces(TextWriter? output = null)
|
||||
{
|
||||
output ??= Console.Error;
|
||||
lock (_guestTraceOutputGate)
|
||||
{
|
||||
var signals = SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.Close();
|
||||
var mutexes = SharpEmu.Libs.Diagnostics.MutexHandoffProfile.Close();
|
||||
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.WriteTrace(output);
|
||||
try
|
||||
{
|
||||
_guestThreadFlowProfile.WriteTrace(output);
|
||||
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.WriteTrace(output, signals);
|
||||
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.WriteTrace(output, mutexes);
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or ObjectDisposedException)
|
||||
{
|
||||
// A closed trace output must not prevent native session cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordGuestThreadFlow(GuestThreadState thread, GuestThreadFlowProfile.EventKind kind, string? reason = null)
|
||||
{
|
||||
if (!GuestThreadFlowProfile.Enabled) return;
|
||||
_guestThreadFlowProfile.Record(new GuestThreadFlowProfile.TraceEvent(
|
||||
Stopwatch.GetTimestamp(), Environment.CurrentManagedThreadId, thread.ThreadHandle,
|
||||
thread.Name, kind, reason, thread.BlockWakeKey, Volatile.Read(ref thread.LastImportNid),
|
||||
Volatile.Read(ref thread.LastImportRdi), Volatile.Read(ref thread.LastImportRsi), thread.BlockDeadlineTimestamp));
|
||||
}
|
||||
|
||||
private GuestSchedulerThreadStats GetGuestSchedulerStats(GuestThreadState thread)
|
||||
{
|
||||
var stats = _guestSchedulerStats.GetOrAdd(
|
||||
thread.ThreadHandle,
|
||||
static _ => new GuestSchedulerThreadStats());
|
||||
stats.Name = thread.Name;
|
||||
return stats;
|
||||
}
|
||||
|
||||
private void ProfileGuestThreadReady(GuestThreadState thread)
|
||||
{
|
||||
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Ready);
|
||||
if (!_profileGuestScheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stats = GetGuestSchedulerStats(thread);
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
Volatile.Write(ref stats.ReadyTimestamp, now);
|
||||
Interlocked.Increment(ref stats.ReadyCount);
|
||||
var blockTimestamp = Interlocked.Exchange(ref stats.BlockTimestamp, 0);
|
||||
if (blockTimestamp != 0 && now >= blockTimestamp)
|
||||
{
|
||||
var ticks = now - blockTimestamp;
|
||||
Interlocked.Increment(ref stats.ResumeCount);
|
||||
Interlocked.Add(ref stats.BlockTicks, ticks);
|
||||
UpdateGuestSchedulerMaximum(ref stats.MaxBlockTicks, ticks);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileGuestThreadClaimed(GuestThreadState thread)
|
||||
{
|
||||
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Claimed);
|
||||
if (!_profileGuestScheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var stats = GetGuestSchedulerStats(thread);
|
||||
var readyTimestamp = Interlocked.Exchange(ref stats.ReadyTimestamp, 0);
|
||||
Interlocked.Increment(ref stats.ClaimCount);
|
||||
if (readyTimestamp != 0 && now >= readyTimestamp)
|
||||
{
|
||||
var ticks = now - readyTimestamp;
|
||||
Interlocked.Add(ref stats.ReadyTicks, ticks);
|
||||
UpdateGuestSchedulerMaximum(ref stats.MaxReadyTicks, ticks);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileGuestThreadScheduled(GuestThreadState thread)
|
||||
{
|
||||
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Scheduled);
|
||||
if (!_profileGuestScheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stats = GetGuestSchedulerStats(thread);
|
||||
Volatile.Write(ref stats.ScheduledTimestamp, Stopwatch.GetTimestamp());
|
||||
Interlocked.Increment(ref stats.ScheduleCount);
|
||||
}
|
||||
|
||||
private void ProfileGuestThreadRunStarted(GuestThreadState thread)
|
||||
{
|
||||
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.RunStarted);
|
||||
if (!_profileGuestScheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var stats = GetGuestSchedulerStats(thread);
|
||||
var scheduledTimestamp = Interlocked.Exchange(ref stats.ScheduledTimestamp, 0);
|
||||
Volatile.Write(ref stats.RunTimestamp, now);
|
||||
Interlocked.Increment(ref stats.RunCount);
|
||||
if (scheduledTimestamp != 0 && now >= scheduledTimestamp)
|
||||
{
|
||||
var ticks = now - scheduledTimestamp;
|
||||
Interlocked.Add(ref stats.ScheduleTicks, ticks);
|
||||
UpdateGuestSchedulerMaximum(ref stats.MaxScheduleTicks, ticks);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileGuestThreadRunStopped(GuestThreadState thread)
|
||||
{
|
||||
if (!_profileGuestScheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var stats = GetGuestSchedulerStats(thread);
|
||||
var runTimestamp = Interlocked.Exchange(ref stats.RunTimestamp, 0);
|
||||
if (runTimestamp != 0 && now >= runTimestamp)
|
||||
{
|
||||
var ticks = now - runTimestamp;
|
||||
Interlocked.Add(ref stats.RunTicks, ticks);
|
||||
UpdateGuestSchedulerMaximum(ref stats.MaxRunTicks, ticks);
|
||||
}
|
||||
|
||||
if (thread.State == GuestThreadRunState.Blocked)
|
||||
{
|
||||
Interlocked.Increment(ref stats.BlockCount);
|
||||
Volatile.Write(ref stats.BlockTimestamp, now);
|
||||
Volatile.Write(ref stats.LastBlockReason, thread.BlockReason);
|
||||
}
|
||||
else if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
|
||||
{
|
||||
Volatile.Write(ref stats.Completed, 1);
|
||||
}
|
||||
|
||||
TryReportGuestSchedulerProfile(now);
|
||||
}
|
||||
|
||||
private void TryReportGuestSchedulerProfile(long now)
|
||||
{
|
||||
var deadline = Volatile.Read(ref _guestSchedulerProfileNextReport);
|
||||
if (now < deadline ||
|
||||
Interlocked.CompareExchange(ref _guestSchedulerProfileReporting, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Volatile.Write(
|
||||
ref _guestSchedulerProfileNextReport,
|
||||
now + (Stopwatch.Frequency * 5L));
|
||||
var windowStart = Interlocked.Exchange(ref _guestSchedulerProfileWindowStart, now);
|
||||
var seconds = Math.Max(
|
||||
(now - windowStart) / (double)Stopwatch.Frequency,
|
||||
double.Epsilon);
|
||||
|
||||
var rows = new List<(long Rank, string Text)>();
|
||||
foreach (var pair in _guestSchedulerStats)
|
||||
{
|
||||
var stats = pair.Value;
|
||||
var readyCount = Interlocked.Exchange(ref stats.ReadyCount, 0);
|
||||
var claimCount = Interlocked.Exchange(ref stats.ClaimCount, 0);
|
||||
var scheduleCount = Interlocked.Exchange(ref stats.ScheduleCount, 0);
|
||||
var runCount = Interlocked.Exchange(ref stats.RunCount, 0);
|
||||
var blockCount = Interlocked.Exchange(ref stats.BlockCount, 0);
|
||||
var resumeCount = Interlocked.Exchange(ref stats.ResumeCount, 0);
|
||||
var readyTicks = Interlocked.Exchange(ref stats.ReadyTicks, 0);
|
||||
var blockTicks = Interlocked.Exchange(ref stats.BlockTicks, 0);
|
||||
var scheduleTicks = Interlocked.Exchange(ref stats.ScheduleTicks, 0);
|
||||
var runTicks = Interlocked.Exchange(ref stats.RunTicks, 0);
|
||||
var maxReadyTicks = Interlocked.Exchange(ref stats.MaxReadyTicks, 0);
|
||||
var maxBlockTicks = Interlocked.Exchange(ref stats.MaxBlockTicks, 0);
|
||||
var maxScheduleTicks = Interlocked.Exchange(ref stats.MaxScheduleTicks, 0);
|
||||
var maxRunTicks = Interlocked.Exchange(ref stats.MaxRunTicks, 0);
|
||||
if (readyCount == 0 && claimCount == 0 && scheduleCount == 0 && runCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.Add((Math.Max(blockTicks, Math.Max(readyTicks, runTicks)),
|
||||
$"name='{stats.Name}' handle=0x{pair.Key:X16} " +
|
||||
$"ready={readyCount} claimed={claimCount} scheduled={scheduleCount} " +
|
||||
$"runs={runCount} blocked={blockCount} resumed={resumeCount} " +
|
||||
$"blocked_ms={GuestSchedulerMilliseconds(blockTicks):F2} " +
|
||||
$"blocked_avg_ms={(resumeCount == 0 ? 0.0 : GuestSchedulerMilliseconds(blockTicks) / resumeCount):F3} " +
|
||||
$"blocked_max_ms={GuestSchedulerMilliseconds(maxBlockTicks):F3} " +
|
||||
$"ready_ms={GuestSchedulerMilliseconds(readyTicks):F2} " +
|
||||
$"ready_avg_ms={(claimCount == 0 ? 0.0 : GuestSchedulerMilliseconds(readyTicks) / claimCount):F3} " +
|
||||
$"ready_max_ms={GuestSchedulerMilliseconds(maxReadyTicks):F3} " +
|
||||
$"dispatch_ms={GuestSchedulerMilliseconds(scheduleTicks):F2} " +
|
||||
$"dispatch_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(scheduleTicks) / runCount):F3} " +
|
||||
$"dispatch_max_ms={GuestSchedulerMilliseconds(maxScheduleTicks):F3} " +
|
||||
$"run_ms={GuestSchedulerMilliseconds(runTicks):F2} " +
|
||||
$"run_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(runTicks) / runCount):F3} " +
|
||||
$"run_max_ms={GuestSchedulerMilliseconds(maxRunTicks):F3} " +
|
||||
$"last_block={Volatile.Read(ref stats.LastBlockReason) ?? "none"}"));
|
||||
|
||||
if (Volatile.Read(ref stats.Completed) != 0)
|
||||
{
|
||||
_guestSchedulerStats.TryRemove(pair.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[PERF][GUEST_SCHED] window={seconds:F1}s active_threads={rows.Count}");
|
||||
foreach (var row in rows
|
||||
.OrderByDescending(static item => item.Rank)
|
||||
.Take(32))
|
||||
{
|
||||
Console.Error.WriteLine($"[PERF][GUEST_SCHED] {row.Text}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref _guestSchedulerProfileReporting, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static double GuestSchedulerMilliseconds(long ticks) =>
|
||||
ticks * 1000.0 / Stopwatch.Frequency;
|
||||
|
||||
private static void UpdateGuestSchedulerMaximum(ref long target, long candidate)
|
||||
{
|
||||
var current = Volatile.Read(ref target);
|
||||
while (candidate > current)
|
||||
{
|
||||
var observed = Interlocked.CompareExchange(ref target, candidate, current);
|
||||
if (observed == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
current = observed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
|
||||
//
|
||||
// Guest code runs natively, so the guest and host share the same virtual address space and the
|
||||
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
|
||||
// one of these extensions it raises #UD instead of executing the opcode; without this the title
|
||||
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
|
||||
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
|
||||
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
|
||||
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
|
||||
// does not fully model.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
|
||||
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
|
||||
private const int CTX_EFLAGS = 68;
|
||||
|
||||
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
|
||||
private const uint StatusIllegalInstruction = 0xC000001Du;
|
||||
|
||||
private const int MaxInstructionBytes = 15;
|
||||
|
||||
// Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
|
||||
private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
|
||||
|
||||
private static int _bmiSoftwareFallbackAnnounced;
|
||||
private static long _bmiInstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Op0Kind != OpKind.Register ||
|
||||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, result);
|
||||
if (flagsChanged)
|
||||
{
|
||||
WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _bmiInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryEvaluate(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
GprOperandSize size,
|
||||
out ulong result,
|
||||
out bool flagsChanged,
|
||||
out uint eflags)
|
||||
{
|
||||
result = 0;
|
||||
flagsChanged = false;
|
||||
eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
|
||||
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Andn:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Blsi:
|
||||
case Mnemonic.Blsmsk:
|
||||
case Mnemonic.Blsr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
|
||||
Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
|
||||
_ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
|
||||
};
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bextr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bzhi:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Tzcnt:
|
||||
case Mnemonic.Lzcnt:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Tzcnt
|
||||
? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
|
||||
: BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Rorx:
|
||||
if (instruction.Op2Kind != OpKind.Immediate8 ||
|
||||
!TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
|
||||
return true;
|
||||
|
||||
case Mnemonic.Sarx:
|
||||
case Mnemonic.Shlx:
|
||||
case Mnemonic.Shrx:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
|
||||
Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
|
||||
_ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
|
||||
};
|
||||
return true;
|
||||
|
||||
case Mnemonic.Pdep:
|
||||
case Mnemonic.Pext:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Pdep
|
||||
? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
|
||||
: BmiInstructionEmulator.Pext(packSrc, packMask, size);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
|
||||
{
|
||||
// Try the full instruction window first, then shrink so a fault near the end of a mapped
|
||||
// page (where fewer than 15 bytes are readable) still decodes.
|
||||
foreach (var attempt in DecodeWindowSizes)
|
||||
{
|
||||
var buffer = new byte[attempt];
|
||||
if (!TryReadExecutableBytes(rip, buffer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
|
||||
decoder.IP = rip;
|
||||
decoder.Decode(out instruction);
|
||||
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
instruction = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe bool TryReadOperand(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
int operandIndex,
|
||||
GprOperandSize size,
|
||||
out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
switch (instruction.GetOpKind(operandIndex))
|
||||
{
|
||||
case OpKind.Register:
|
||||
if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var raw = ReadCtxU64(contextRecord, offset);
|
||||
value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
|
||||
return true;
|
||||
|
||||
case OpKind.Memory:
|
||||
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
|
||||
var buffer = new byte[byteCount];
|
||||
if (!TryReadHostBytes(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = byteCount == 8
|
||||
? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
|
||||
// FS/GS-relative operands need the guest segment base, which is not modelled here.
|
||||
if (instruction.SegmentPrefix != Register.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.IsIPRelativeMemoryOperand)
|
||||
{
|
||||
address = instruction.IPRelativeMemoryAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
var effective = instruction.MemoryDisplacement64;
|
||||
if (instruction.MemoryBase != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, baseOffset);
|
||||
}
|
||||
|
||||
if (instruction.MemoryIndex != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
|
||||
}
|
||||
|
||||
address = effective;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
|
||||
private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
|
||||
default:
|
||||
if (TryGetGpr64Offset(register, out offset))
|
||||
{
|
||||
size = GprOperandSize.Bits64;
|
||||
return true;
|
||||
}
|
||||
|
||||
size = GprOperandSize.Bits32;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetGpr64Offset(Register register, out int offset)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.RAX: offset = CTX_RAX; return true;
|
||||
case Register.RCX: offset = CTX_RCX; return true;
|
||||
case Register.RDX: offset = CTX_RDX; return true;
|
||||
case Register.RBX: offset = CTX_RBX; return true;
|
||||
case Register.RSP: offset = CTX_RSP; return true;
|
||||
case Register.RBP: offset = CTX_RBP; return true;
|
||||
case Register.RSI: offset = CTX_RSI; return true;
|
||||
case Register.RDI: offset = CTX_RDI; return true;
|
||||
case Register.R8: offset = CTX_R8; return true;
|
||||
case Register.R9: offset = CTX_R9; return true;
|
||||
case Register.R10: offset = CTX_R10; return true;
|
||||
case Register.R11: offset = CTX_R11; return true;
|
||||
case Register.R12: offset = CTX_R12; return true;
|
||||
case Register.R13: offset = CTX_R13; return true;
|
||||
case Register.R14: offset = CTX_R14; return true;
|
||||
case Register.R15: offset = CTX_R15; return true;
|
||||
default: offset = 0; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ using System.Runtime.InteropServices;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
|
@ -54,13 +53,10 @@ public sealed partial class DirectExecutionBackend
|
|||
var startTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
var r = directExecutionBackend.DispatchImport(importIndex, argPackPtr);
|
||||
RecordPerfHleDispatchTime(System.Diagnostics.Stopwatch.GetTimestamp() - startTicks);
|
||||
directExecutionBackend.ClearActiveImportIndex();
|
||||
return r;
|
||||
}
|
||||
|
||||
var result = directExecutionBackend.DispatchImport(importIndex, argPackPtr);
|
||||
directExecutionBackend.ClearActiveImportIndex();
|
||||
return result;
|
||||
return directExecutionBackend.DispatchImport(importIndex, argPackPtr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -164,32 +160,16 @@ public sealed partial class DirectExecutionBackend
|
|||
return 18446744071562199042uL;
|
||||
}
|
||||
ImportStubEntry importStubEntry = _importEntries[importIndex];
|
||||
using var registerPacketImport = SharpEmu.Libs.Diagnostics.AgcRegisterPacketProfile.MeasureImport(importStubEntry.Nid);
|
||||
if (_perfHleHistogram)
|
||||
{
|
||||
RecordPerfHleCall(importStubEntry.Export?.Name ?? importStubEntry.Nid);
|
||||
}
|
||||
if (_profileGuestRip)
|
||||
{
|
||||
EnsureGuestRipSampler();
|
||||
}
|
||||
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
||||
if (num2 != _lastReportedRawSentinelRecoveries)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||
_lastReportedRawSentinelRecoveries = num2;
|
||||
}
|
||||
if (importStubEntry.IsLeaf &&
|
||||
TryDispatchHotMemoryLeaf(cpuContext, importStubEntry, argPackPtr, out var hotMemoryResult))
|
||||
{
|
||||
return hotMemoryResult;
|
||||
}
|
||||
|
||||
if (importStubEntry.IsTrivialLeaf && !_disableTrivialLeafDispatch)
|
||||
{
|
||||
return DispatchTrivialLeaf(cpuContext, in importStubEntry, argPackPtr, num);
|
||||
}
|
||||
|
||||
if (importStubEntry.IsLeaf &&
|
||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||
{
|
||||
|
|
@ -197,10 +177,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
cpuContext.Rip = importStubEntry.Address;
|
||||
if (_profileGuestRip)
|
||||
{
|
||||
cpuContext.ActiveImportIndex = importIndex;
|
||||
}
|
||||
LoadImportVolatileArguments(cpuContext, argPackPtr);
|
||||
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
||||
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||
|
|
@ -215,13 +191,6 @@ public sealed partial class DirectExecutionBackend
|
|||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||
cpuContext.SetImportStackArguments(
|
||||
ReadImportStackArgument(argPackPtr, 0),
|
||||
ReadImportStackArgument(argPackPtr, 1),
|
||||
ReadImportStackArgument(argPackPtr, 2),
|
||||
ReadImportStackArgument(argPackPtr, 3),
|
||||
ReadImportStackArgument(argPackPtr, 4),
|
||||
ReadImportStackArgument(argPackPtr, 5));
|
||||
ulong value = cpuContext[CpuRegister.Rdi];
|
||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||
ulong num3 = cpuContext[CpuRegister.Rdx];
|
||||
|
|
@ -243,8 +212,8 @@ public sealed partial class DirectExecutionBackend
|
|||
if (probeTarget &&
|
||||
Interlocked.Increment(ref _probeImportReturnAddressCount) <= 2048)
|
||||
{
|
||||
var frameValue = TryReadImportStackU64(value4, out var savedRbp) ? savedRbp : 0;
|
||||
var frameReturn = TryReadImportStackU64(value4 + sizeof(ulong), out var savedReturn)
|
||||
var frameValue = TryReadStackU64(value4, out var savedRbp) ? savedRbp : 0;
|
||||
var frameReturn = TryReadStackU64(value4 + sizeof(ulong), out var savedReturn)
|
||||
? savedReturn
|
||||
: 0;
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -345,26 +314,14 @@ public sealed partial class DirectExecutionBackend
|
|||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
// Once the host is shutting down, a guest thread still running reaches no more HLE: the
|
||||
// services behind the imports are being torn down, and a title that sees them fail (a null
|
||||
// command buffer from the GPU library, say) crashes the process on its way out.
|
||||
if (_forcedGuestExit && !ActiveForcedGuestExit && TryEndGuestSliceForShutdown(argPackPtr))
|
||||
if (!isGuestWorker &&
|
||||
!ActiveForcedGuestExit &&
|
||||
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2) &&
|
||||
TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
if (!isGuestWorker &&
|
||||
!ActiveForcedGuestExit &&
|
||||
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2))
|
||||
{
|
||||
// Break before the forced exit so the loop state is still live.
|
||||
NotifyDebuggerStall(CpuStallKind.ImportLoop, in importStubEntry, num7, num, value, value2);
|
||||
if (TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
}
|
||||
bool flag0 = importStubEntry.SuppressStrlenTrace;
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
|
|
@ -372,8 +329,7 @@ public sealed partial class DirectExecutionBackend
|
|||
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
|
||||
bool flag5 = false;
|
||||
ExportedFunction? matchedExport = importStubEntry.Export;
|
||||
bool periodicTrace = _logImportPeriodic &&
|
||||
(num <= 128 ||
|
||||
bool periodicTrace = num <= 128 ||
|
||||
(num >= 240 && num <= 400) ||
|
||||
(num >= 900 && num <= 1300) ||
|
||||
num % 100000 == 0L ||
|
||||
|
|
@ -381,7 +337,7 @@ public sealed partial class DirectExecutionBackend
|
|||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
|
||||
flag ||
|
||||
flag2 ||
|
||||
flag3);
|
||||
flag3;
|
||||
if (matchedExport is not null)
|
||||
{
|
||||
if (flag4)
|
||||
|
|
@ -569,12 +525,9 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
}
|
||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||
{
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||
}
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||
if (dispatchResolved &&
|
||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||
|
|
@ -645,7 +598,7 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
*(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
|
||||
if (_logFiber)
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
|
||||
|
|
@ -1230,56 +1183,10 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
}
|
||||
|
||||
// Every import reads its stack arguments. On POSIX a region query takes the global region lock,
|
||||
// so keep the last readable range per thread until any mapping changes. The signal handlers
|
||||
// use TryReadStackU64 instead: they must not touch thread-static storage.
|
||||
[ThreadStatic] private static ulong _importReadableStart;
|
||||
[ThreadStatic] private static ulong _importReadableEnd;
|
||||
[ThreadStatic] private static long _importReadableGeneration;
|
||||
|
||||
private unsafe static bool TryReadImportStackU64(ulong address, out ulong value)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return TryReadHostQword(address, out value);
|
||||
}
|
||||
|
||||
// Stack arguments sit on the calling guest thread's own stack, which the backend mapped read-write
|
||||
// for the thread's lifetime and no tracker ever protects. Reading inside it needs no query: the
|
||||
// cached range below is keyed by a mapping generation that every tracker protection change bumps,
|
||||
// so falling through here cost a locked host region query per argument on nearly every import.
|
||||
if (_activeGuestThreadState is { StackSize: > 0 } thread &&
|
||||
address >= thread.StackBase && address <= thread.StackBase + thread.StackSize - sizeof(ulong))
|
||||
{
|
||||
value = *(ulong*)address;
|
||||
return true;
|
||||
}
|
||||
|
||||
var generation = HostMemory.MappingGeneration;
|
||||
if (generation == _importReadableGeneration &&
|
||||
address >= _importReadableStart && address <= _importReadableEnd - 8)
|
||||
{
|
||||
value = *(ulong*)address;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryQueryReadableRange(address, out var start, out var end))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
_importReadableStart = start;
|
||||
_importReadableEnd = end;
|
||||
_importReadableGeneration = generation;
|
||||
value = *(ulong*)address;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong ReadImportStackArgument(nint argPackPtr, int index)
|
||||
{
|
||||
var address = checked((ulong)argPackPtr + 104UL + (ulong)index * sizeof(ulong));
|
||||
return TryReadImportStackU64(address, out var value) ? value : 0;
|
||||
return TryReadHostQword(address, out var value) ? value : 0;
|
||||
}
|
||||
|
||||
private static GuestCpuContinuation CaptureImportBoundaryContinuation(
|
||||
|
|
@ -1312,41 +1219,6 @@ public sealed partial class DirectExecutionBackend
|
|||
Mxcsr: context.Mxcsr,
|
||||
RestoreFullFpuState: false);
|
||||
|
||||
/// <summary>
|
||||
/// Ultra-thin path for hot memcpy/memmove leaf imports: skip
|
||||
/// CpuContext register marshalling, import-call frames, and vector return
|
||||
/// stores when guest memory can satisfy the copy directly.
|
||||
/// </summary>
|
||||
private unsafe bool TryDispatchHotMemoryLeaf(
|
||||
CpuContext cpuContext,
|
||||
ImportStubEntry importStubEntry,
|
||||
nint argPackPtr,
|
||||
out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
var nid = importStubEntry.Nid;
|
||||
if (nid is not ("Q3VBxCXhUHs" or "+P6FRGH4LfA"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = *(ulong*)argPackPtr;
|
||||
var source = *(ulong*)(argPackPtr + 8);
|
||||
var count = *(ulong*)(argPackPtr + 16);
|
||||
if (count > (ulong)int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count != 0 && !cpuContext.Memory.TryCopy(destination, source, count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = destination;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryDispatchLeafImport(
|
||||
CpuContext cpuContext,
|
||||
ImportStubEntry importStubEntry,
|
||||
|
|
@ -1390,13 +1262,6 @@ public sealed partial class DirectExecutionBackend
|
|||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||
cpuContext.SetImportStackArguments(
|
||||
ReadImportStackArgument(argPackPtr, 0),
|
||||
ReadImportStackArgument(argPackPtr, 1),
|
||||
ReadImportStackArgument(argPackPtr, 2),
|
||||
ReadImportStackArgument(argPackPtr, 3),
|
||||
ReadImportStackArgument(argPackPtr, 4),
|
||||
ReadImportStackArgument(argPackPtr, 5));
|
||||
|
||||
if (_activeGuestThreadState is { } activeGuestThreadState)
|
||||
{
|
||||
|
|
@ -1417,7 +1282,7 @@ public sealed partial class DirectExecutionBackend
|
|||
Volatile.Write(ref activeGuestThreadState.LastReturnRip, returnRip);
|
||||
Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
|
||||
}
|
||||
if (_logImportPeriodic && dispatchIndex % 100000 == 0)
|
||||
if (dispatchIndex % 100000 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid}) " +
|
||||
|
|
@ -1427,7 +1292,7 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
int returnValue;
|
||||
if (importStubEntry.IsNoBlockLeaf)
|
||||
if (IsNoBlockLeafImport(importStubEntry.Nid))
|
||||
{
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
returnValue = export.Function(cpuContext);
|
||||
|
|
@ -1456,12 +1321,9 @@ public sealed partial class DirectExecutionBackend
|
|||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
}
|
||||
}
|
||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||
{
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||
}
|
||||
DeliverPendingGuestExceptionAtSafePoint(
|
||||
cpuContext,
|
||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||
|
||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
|
|
@ -1483,7 +1345,8 @@ public sealed partial class DirectExecutionBackend
|
|||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockWaiter,
|
||||
out var blockResumeHandler,
|
||||
out var blockWakeHandler,
|
||||
out var blockDeadlineTimestamp);
|
||||
if (consumedThreadBlock &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||
|
|
@ -1494,7 +1357,8 @@ public sealed partial class DirectExecutionBackend
|
|||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockWaiter,
|
||||
blockResumeHandler,
|
||||
blockWakeHandler,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
|
|
@ -1518,68 +1382,6 @@ public sealed partial class DirectExecutionBackend
|
|||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool _disableTrivialLeafDispatch = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRIVIAL_LEAF"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Tiny exports called millions of times per second, where the ~250ns of full
|
||||
// import bookkeeping costs more than the export itself. Each one reads only
|
||||
// argument registers (RDI..R9) and thread-local state, returns in RAX, never
|
||||
// blocks, yields, calls back into the guest or reads the guest stack.
|
||||
private static bool IsTrivialLeafImport(string nid) =>
|
||||
nid is
|
||||
"0-KXaS70xy4" or // pthread_getspecific
|
||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||
"EI-5-jlq2dE" or // scePthreadGetthreadid
|
||||
"3eqs37G74-s" or // pthread_getthreadid_np
|
||||
"BNowx2l588E" or // sceKernelGetProcessTimeCounterFrequency
|
||||
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
|
||||
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
|
||||
"-2IRUCO--PM" or // sceKernelReadTsc
|
||||
"1j3S3n-tTW4" or // sceKernelGetTscFrequency
|
||||
"ob5xAW4ln-0" or // strchr
|
||||
"9yDWMxEFdJU" or // strrchr
|
||||
"V++UgBtQhn0"; // sceAgcGetDataPacketPayloadAddress
|
||||
|
||||
private unsafe ulong DispatchTrivialLeaf(
|
||||
CpuContext cpuContext,
|
||||
in ImportStubEntry importStubEntry,
|
||||
nint argPackPtr,
|
||||
long dispatchIndex)
|
||||
{
|
||||
// Polling loops on the host main thread use the time queries as loop-guard
|
||||
// boundaries; keep resetting the pattern so skipping the guard here cannot
|
||||
// turn a legitimate time-polling loop into a forced exit.
|
||||
if (importStubEntry.IsLoopGuardBoundary && !GuestThreadExecution.IsGuestThread)
|
||||
{
|
||||
ResetImportLoopPattern();
|
||||
}
|
||||
|
||||
cpuContext.Rip = importStubEntry.Address;
|
||||
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
||||
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
|
||||
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
|
||||
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
|
||||
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
var returnValue = importStubEntry.Export!.Function(cpuContext);
|
||||
if (!cpuContext.WasRaxWritten)
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||
}
|
||||
|
||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||
ShouldLogImportResult(importStubEntry.Nid, (OrbisGen2Result)returnValue))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Import#{dispatchIndex} result: {(OrbisGen2Result)returnValue} ({importStubEntry.Nid}) " +
|
||||
$"rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
|
||||
$"ret=0x{*(ulong*)(argPackPtr + 96):X16}");
|
||||
}
|
||||
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
private static bool IsNoBlockLeafImport(string nid) =>
|
||||
nid is
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
|
|
@ -1593,13 +1395,11 @@ public sealed partial class DirectExecutionBackend
|
|||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||
"4fgtGfXDrFc" or // sceAmprMeasureCommandSizeWriteAddress_04_00
|
||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||
"gzndltBEzWc" or // sceAmprCommandBufferGetNumCommands
|
||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||
"j0+3uJMxYJY" or // sceAmprCommandBufferWriteAddress_04_00
|
||||
"mPpPxv5CZt4" or // sceSystemServiceGetHdrToneMapLuminance
|
||||
"1FZBKy8HeNU" or // sceVideoOutGetVblankStatus
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
|
|
@ -1607,18 +1407,11 @@ public sealed partial class DirectExecutionBackend
|
|||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"viiwFMaNamA" or // strstr
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||
"K-jXhbt2gn4" or // scePthreadMutexTrylock
|
||||
// Hot memory leaves: skip non-NoBlock call-frame bookkeeping on top of TryCopy.
|
||||
"Q3VBxCXhUHs" or // memcpy
|
||||
"+P6FRGH4LfA" or // memmove
|
||||
"DfivPArhucg" or // memcmp
|
||||
"8zTFvBIAIN8"; // memset
|
||||
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
||||
|
||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||
{
|
||||
|
|
@ -1637,27 +1430,8 @@ public sealed partial class DirectExecutionBackend
|
|||
var expectedEqueueTimeout =
|
||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
// scePthreadCondTimedwait and sceKernelWaitEventFlag report an elapsed timeout.
|
||||
var expectedWaitTimeout =
|
||||
(nid is "BmMjYxmew1w" or "JTvBflhYazQ") &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
// scePthreadMutexLock on an error-checking mutex the caller already owns; titles use it
|
||||
// to build their own recursive locks.
|
||||
var expectedErrorCheckRelock =
|
||||
string.Equals(nid, "9UK1vLZQft4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
// scePadReadState on a handle that is not open; titles poll every pad slot.
|
||||
var expectedPadNotOpen =
|
||||
string.Equals(nid, "YndgXqQVV7c", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80920003);
|
||||
var expectedMutexTrylockBusy =
|
||||
(nid is "K-jXhbt2gn4" or "upoVrzMHFeE") &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
var expectedSemaphoreTrywaitAgain =
|
||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
var expectedPollSemaBusy =
|
||||
string.Equals(nid, "12wOHk8ywb0", StringComparison.Ordinal) &&
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
var expectedNetAcceptWouldBlock =
|
||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||
|
|
@ -1668,22 +1442,13 @@ public sealed partial class DirectExecutionBackend
|
|||
var expectedPrivacyInvalidParameter =
|
||||
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80960009);
|
||||
var expectedPlayGoChunkEnumerationEnd =
|
||||
string.Equals(nid, "uWIYLFkkwqk", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80B2000C);
|
||||
if (!expectedFileProbeMiss &&
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedWaitTimeout &&
|
||||
!expectedErrorCheckRelock &&
|
||||
!expectedPadNotOpen &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedSemaphoreTrywaitAgain &&
|
||||
!expectedPollSemaBusy &&
|
||||
!expectedNetAcceptWouldBlock &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter &&
|
||||
!expectedPlayGoChunkEnumerationEnd)
|
||||
!expectedPrivacyInvalidParameter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1735,7 +1500,7 @@ public sealed partial class DirectExecutionBackend
|
|||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
"qj7QZpgr9Uw" or // Graphics context-state operation
|
||||
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
|
||||
"LtTouSCZjHM" or // sceAgcCbNop
|
||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||
|
|
@ -1774,13 +1539,11 @@ public sealed partial class DirectExecutionBackend
|
|||
"vWU-odnS+fU" or
|
||||
"sSAUCCU1dv4" or
|
||||
"C+IEj+BsAFM" or
|
||||
"4fgtGfXDrFc" or
|
||||
"tZDDEo2tE5k" or
|
||||
"GnxKOHEawhk" or
|
||||
"gzndltBEzWc" or
|
||||
"H896Pt-yB4I" or
|
||||
"sJXyWHjP-F8" or
|
||||
"j0+3uJMxYJY" or
|
||||
"mPpPxv5CZt4" or
|
||||
"1FZBKy8HeNU" or
|
||||
"ASoW5WE-UPo" or
|
||||
|
|
@ -1805,8 +1568,6 @@ public sealed partial class DirectExecutionBackend
|
|||
"WkkeywLJcgU" or // wcslen
|
||||
"Ovb2dSJOAuE" or // strcmp
|
||||
"aesyjrHVWy4" or // strncmp
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"viiwFMaNamA" or // strstr
|
||||
"pNtJdE3x49E" or // wcscmp
|
||||
"fV2xHER+bKE" or // wcscoll
|
||||
"E8wCoUEbfzk" or // wcsncmp
|
||||
|
|
@ -1869,20 +1630,6 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the import straight to the host entry stub, which ends this guest slice.
|
||||
private unsafe bool TryEndGuestSliceForShutdown(nint argPackPtr)
|
||||
{
|
||||
ulong sentinel = ActiveEntryReturnSentinelRip;
|
||||
if (sentinel < 65536 || !TryPatchActiveGuestReturnSlot(sentinel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*(ulong*)(argPackPtr + 96) = sentinel;
|
||||
ActiveForcedGuestExit = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
|
||||
{
|
||||
ulong num = ActiveEntryReturnSentinelRip;
|
||||
|
|
@ -2013,18 +1760,10 @@ public sealed partial class DirectExecutionBackend
|
|||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
nid is
|
||||
"1jfXLRVzisc" or // sceKernelUsleep
|
||||
"1j3S3n-tTW4" or // sceKernelGetTscFrequency (polling/time base query)
|
||||
"WKAXJ4XBPQ4" or // scePthreadCondWait
|
||||
"BmMjYxmew1w" or // scePthreadCondTimedwait
|
||||
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"Zxa0VhQVTsk" or // sceKernelWaitSema
|
||||
"n88vx3C5nW8" or // gettimeofday
|
||||
"lLMT9vJAck0" or // clock_gettime
|
||||
"-2IRUCO--PM" or // sceKernelReadTsc
|
||||
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
|
||||
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
|
||||
"yH17Q6NWtVg"; // sceUserServiceGetEvent (non-blocking event poll)
|
||||
"27bAgiJmOh0"; // pthread_cond_timedwait
|
||||
|
||||
private void ResetImportLoopPattern()
|
||||
{
|
||||
|
|
@ -2423,18 +2162,17 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return false;
|
||||
}
|
||||
var runtimeSymbolsByName = Volatile.Read(ref _runtimeSymbolsByName);
|
||||
if (runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
|
||||
if (_runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
|
||||
{
|
||||
address = value;
|
||||
return true;
|
||||
}
|
||||
if (symbolName.StartsWith("_", StringComparison.Ordinal) && runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
|
||||
if (symbolName.StartsWith("_", StringComparison.Ordinal) && _runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
|
||||
{
|
||||
address = value;
|
||||
return true;
|
||||
}
|
||||
if (runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
|
||||
if (_runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
|
||||
{
|
||||
address = value;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// On macOS the runtime receives EXC_BAD_ACCESS on each of its threads through a Mach exception
|
||||
// port before the kernel raises SIGSEGV or SIGBUS. It calls any fault in the page holding RSP, or
|
||||
// the page below it, a stack overflow and aborts, assuming RSP is on a stack it owns. Guest code
|
||||
// runs on guest stacks, and those pages fault whenever GPU or image write tracking has protected
|
||||
// them, so a thread running guest code must leave its access faults to the signal handler. The
|
||||
// handler resolves guest faults and chains every other one to the runtime's own signal handler,
|
||||
// the same path the runtime uses on Linux.
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
private const uint ExceptionMaskBadAccess = 1u << 1;
|
||||
private const int ExceptionDefault = 1;
|
||||
private const int ThreadStateNone = 13;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _guestFaultsUseSignals;
|
||||
|
||||
private static int _machExceptionPortWarning;
|
||||
|
||||
[DllImport("libSystem.dylib", EntryPoint = "mach_thread_self")]
|
||||
private static extern uint MachThreadSelf();
|
||||
|
||||
[DllImport("libSystem.dylib", EntryPoint = "thread_set_exception_ports")]
|
||||
private static extern int ThreadSetExceptionPorts(uint thread, uint exceptionMask, uint newPort, int behavior, int newFlavor);
|
||||
|
||||
[DllImport("libSystem.dylib", EntryPoint = "mach_port_deallocate")]
|
||||
private static extern int MachPortDeallocate(uint task, uint name);
|
||||
|
||||
private static readonly uint _machTaskSelf = OperatingSystem.IsMacOS()
|
||||
? (uint)Marshal.ReadInt32(NativeLibrary.GetExport(NativeLibrary.Load("libSystem.dylib"), "mach_task_self_"))
|
||||
: 0;
|
||||
|
||||
private static void RouteGuestAccessFaultsToSignals()
|
||||
{
|
||||
if (_guestFaultsUseSignals || !OperatingSystem.IsMacOS())
|
||||
return;
|
||||
_guestFaultsUseSignals = true;
|
||||
var thread = MachThreadSelf();
|
||||
var result = ThreadSetExceptionPorts(thread, ExceptionMaskBadAccess, 0, ExceptionDefault, ThreadStateNone);
|
||||
_ = MachPortDeallocate(_machTaskSelf, thread);
|
||||
if (result != 0 && System.Threading.Interlocked.Exchange(ref _machExceptionPortWarning, 1) == 0)
|
||||
Console.Error.WriteLine($"[LOADER][WARN] Cannot hand guest access faults to the signal handler: thread_set_exception_ports={result}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -29,29 +29,9 @@ public sealed partial class DirectExecutionBackend
|
|||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Cap concurrent native-worker Runs. Astro's tbb_thead burst overlaps many
|
||||
// UnmanagedCallersOnly prologues; a large prewarm + unbounded concurrency
|
||||
// FailFasts (0xC0000409) mid-storm with no VEH breadcrumb. Pool size and
|
||||
// in-flight Runs are separate knobs.
|
||||
private static readonly int NativeWorkerMaxConcurrent = ReadNativeWorkerMaxConcurrent();
|
||||
|
||||
private static int ReadNativeWorkerMaxConcurrent()
|
||||
{
|
||||
if (int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT"),
|
||||
out var parsed) &&
|
||||
parsed > 0)
|
||||
{
|
||||
return Math.Clamp(parsed, 1, 64);
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent);
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
|
|
@ -69,9 +49,6 @@ public sealed partial class DirectExecutionBackend
|
|||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool TerminateThread(nint hThread, uint dwExitCode);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
|
|
@ -79,148 +56,40 @@ public sealed partial class DirectExecutionBackend
|
|||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe ulong RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
{
|
||||
// Limit in-flight native Runs before renting so the idle pool is not
|
||||
// drained by threads blocked on the concurrency gate.
|
||||
_nativeWorkerRunLimiter.Wait();
|
||||
NativeGuestExecutor? worker = null;
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
try
|
||||
{
|
||||
// Astro can spawn a burst of tbb_thead while workers are still in
|
||||
// TerminateThread+respawn. Wait for a native worker — never fall back
|
||||
// to managed inline (FailFast) and never throw (uncaught throw mid-
|
||||
// storm was a silent process die).
|
||||
var maxAttempts = requireNativeWorker ? 500 : 48;
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
worker = RentNativeGuestExecutor();
|
||||
if (worker is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requireNativeWorker)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(attempt < 32 ? 1 : 4);
|
||||
}
|
||||
|
||||
if (worker is null)
|
||||
{
|
||||
if (requireNativeWorker)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeWorkerRefuseCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_native_worker unavailable #{n} after {maxAttempts} attempts; " +
|
||||
"skipping run (no managed inline, no throw)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
_activeGuestThreadYieldRequested = true;
|
||||
_activeGuestThreadYieldReason = "tbb_native_worker_unavailable";
|
||||
_activeForcedGuestExit = true;
|
||||
return 0x80020012UL;
|
||||
}
|
||||
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
if (LogThreadMode && state is { Name: "tbb_thead" })
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
|
||||
if (n <= 12 || n % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] tbb_run_enter #{n} native_tid_pending handle=0x{state.ThreadHandle:X16} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
var state = _activeGuestThreadState;
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nativeWorkerRunLimiter.Release();
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private static int _tbbNativeRunEnterCount;
|
||||
private static int _tbbNativeWorkerRefuseCount;
|
||||
internal static int _tbbWorkerPrologueFaultCount;
|
||||
|
||||
private void PrewarmNativeGuestWorkers(int count)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled || count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var warmed = new List<NativeGuestExecutor>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
warmed.Add(worker);
|
||||
}
|
||||
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
_allNativeWorkers.Add(worker);
|
||||
_idleNativeWorkers.Push(worker);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Native guest workers prewarmed: {warmed.Count}/{count} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||
|
|
@ -302,8 +171,8 @@ public sealed partial class DirectExecutionBackend
|
|||
// loop: WaitForSingleObject(work);
|
||||
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
|
||||
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
|
||||
// if (rax != 0) rax = rax(); // guest entry stub — zero managed frames below
|
||||
// RunEpilogue(self, rax); // managed: captures outcome, restores ambient
|
||||
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
|
||||
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
|
||||
// SetEvent(done); goto loop;
|
||||
//
|
||||
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
|
||||
|
|
@ -348,7 +217,7 @@ public sealed partial class DirectExecutionBackend
|
|||
private nint _runHostRspSlot;
|
||||
private nint _runEntryStub;
|
||||
private ulong _runAffinityMask;
|
||||
private ulong _runNativeResult;
|
||||
private int _runNativeResult;
|
||||
private bool _runYieldRequested;
|
||||
private string? _runYieldReason;
|
||||
private bool _runForcedExit;
|
||||
|
|
@ -425,7 +294,7 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, ulong, void>)&RunEpilogue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||
nint workHandle;
|
||||
nint doneHandle;
|
||||
|
|
@ -504,9 +373,9 @@ public sealed partial class DirectExecutionBackend
|
|||
Emit(0x0F); Emit(0x84); // je skipEntry
|
||||
int skipJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitCallRax(); // guest entry stub -> rax
|
||||
EmitCallRax(); // guest entry stub -> eax
|
||||
int skipEntryOffset = offset;
|
||||
Emit(0x48); Emit(0x89); Emit(0xC2); // mov rdx, rax
|
||||
Emit(0x89); Emit(0xC2); // mov edx, eax
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)epiloguePtr);
|
||||
EmitCallRax();
|
||||
|
|
@ -531,22 +400,6 @@ public sealed partial class DirectExecutionBackend
|
|||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool RestartWorkerThread()
|
||||
{
|
||||
if (_loopStub == null || _controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*(int*)_controlBlock = 0;
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool StartWorkerThread()
|
||||
{
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
|
|
@ -565,7 +418,7 @@ public sealed partial class DirectExecutionBackend
|
|||
return true;
|
||||
}
|
||||
|
||||
public ulong Run(
|
||||
public int Run(
|
||||
CpuContext context,
|
||||
GuestThreadState? state,
|
||||
ulong guestThreadHandle,
|
||||
|
|
@ -592,49 +445,6 @@ public sealed partial class DirectExecutionBackend
|
|||
_runForcedExit = false;
|
||||
SignalWorkAvailable();
|
||||
WaitWorkCompleted();
|
||||
|
||||
// Normal path: RunEpilogue/ExitRun clears _entered before SetEvent(done).
|
||||
// TBB abort stub SetEvent's without ExitRun — _entered stays true.
|
||||
if (_entered)
|
||||
{
|
||||
var waitRc = WaitForSingleObject(_threadHandle, 500u);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} aborted during run; " +
|
||||
$"wait_rc=0x{waitRc:X8} respawning");
|
||||
Console.Error.Flush();
|
||||
if (_runState is { } abortedState)
|
||||
{
|
||||
_ = GuestThreadExecution.NotifyGuestThreadAbandoned(
|
||||
abortedState.ThreadHandle,
|
||||
"tbb_worker_abort");
|
||||
Volatile.Write(ref abortedState.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
_entered = false;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
// Abort stub parks (no ExitThread). Force-kill the parked OS
|
||||
// thread so we can recreate the loop without process teardown.
|
||||
if (waitRc != 0u)
|
||||
{
|
||||
_ = TerminateThread(_threadHandle, unchecked((uint)(-1)));
|
||||
_ = WaitForSingleObject(_threadHandle, 1000u);
|
||||
}
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
_nativeThreadId = 0;
|
||||
}
|
||||
if (!RestartWorkerThread())
|
||||
{
|
||||
_runPrologueFailed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_runPrologueFailed = false;
|
||||
_runForcedExit = true;
|
||||
_runNativeResult = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
|
|
@ -642,22 +452,7 @@ public sealed partial class DirectExecutionBackend
|
|||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
// Never throw out of the native-worker rent path: an uncaught
|
||||
// exception mid-TBB storm kills the process with no FailFast
|
||||
// breadcrumb.
|
||||
var n = Interlocked.Increment(ref _tbbWorkerPrologueFaultCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_worker prologue fault #{n}; soft-fail run " +
|
||||
$"(tid={_nativeThreadId})");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
yieldRequested = true;
|
||||
yieldReason = "tbb_worker_prologue_fault";
|
||||
forcedExit = true;
|
||||
return 0x80020012UL;
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
|
|
@ -707,7 +502,7 @@ public sealed partial class DirectExecutionBackend
|
|||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static void RunEpilogue(nint executorHandle, ulong nativeResult)
|
||||
private static void RunEpilogue(nint executorHandle, int nativeResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -750,20 +545,7 @@ public sealed partial class DirectExecutionBackend
|
|||
_activeGuestThreadYieldReason = null;
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
RouteGuestAccessFaultsToSignals();
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint doneHandle = OperatingSystem.IsWindows()
|
||||
? _workCompleted!.SafeWaitHandle.DangerousGetHandle()
|
||||
: _doneSemaphore;
|
||||
TlsSetValue(backend._workerDoneEventTlsIndex, doneHandle);
|
||||
}
|
||||
if (backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint eligible = _runState is { Name: "tbb_thead" } ? 1 : 0;
|
||||
TlsSetValue(backend._tbbAbortEligibleTlsIndex, eligible);
|
||||
}
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
|
|
@ -782,7 +564,7 @@ public sealed partial class DirectExecutionBackend
|
|||
return _runEntryStub;
|
||||
}
|
||||
|
||||
private void ExitRun(ulong nativeResult)
|
||||
private void ExitRun(int nativeResult)
|
||||
{
|
||||
_runNativeResult = nativeResult;
|
||||
_runYieldRequested = _activeGuestThreadYieldRequested;
|
||||
|
|
@ -798,14 +580,6 @@ public sealed partial class DirectExecutionBackend
|
|||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
if (_backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._workerDoneEventTlsIndex, 0);
|
||||
}
|
||||
if (_backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._tbbAbortEligibleTlsIndex, 0);
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
|
|
@ -822,7 +596,7 @@ public sealed partial class DirectExecutionBackend
|
|||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X16} yield={_runYieldRequested}");
|
||||
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.HLE;
|
||||
using static Iced.Intel.AssemblerRegisters;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Guest faults are delivered on the guest stack. The runtime only knows the thread's own stack,
|
||||
// so managed code that runs on the guest stack can make a garbage collection misread the frames
|
||||
// of that thread. Windows swaps the stack bounds in the TEB while guest code runs; POSIX cannot,
|
||||
// so the signal handler moves to the host stack below the frames that entered guest code first.
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
// Host stack below the saved host RSP that is still treated as the host frames in use.
|
||||
private const ulong HostStackSearchBytes = 0x100_0000;
|
||||
private const int SignalStackGap = 0x1000;
|
||||
|
||||
// [0] the host RSP TLS slot (uint.MaxValue while unknown), [8] the Win64 TlsGetValue stub.
|
||||
private static nint _signalStackCells;
|
||||
|
||||
private static unsafe nint CreatePosixSignalTrampoline()
|
||||
{
|
||||
_signalStackCells = (nint)NativeMemory.AllocZeroed(16);
|
||||
*(uint*)_signalStackCells = uint.MaxValue;
|
||||
|
||||
var a = new Assembler(64);
|
||||
var direct = a.CreateLabel();
|
||||
var call = a.CreateLabel();
|
||||
var done = a.CreateLabel();
|
||||
a.push(rbx);
|
||||
a.push(r12);
|
||||
a.push(r13);
|
||||
a.push(r14);
|
||||
a.push(r15);
|
||||
a.mov(r12d, edi);
|
||||
a.mov(r13, rsi);
|
||||
a.mov(r14, rdx);
|
||||
a.mov(rbx, rsp);
|
||||
|
||||
// The saved host RSP of this thread, when it entered guest code.
|
||||
a.mov(rax, (ulong)_signalStackCells);
|
||||
a.mov(ecx, __dword_ptr[rax]);
|
||||
a.cmp(ecx, -1);
|
||||
a.je(direct);
|
||||
a.mov(rax, __qword_ptr[rax + 8]);
|
||||
a.test(rax, rax);
|
||||
a.je(direct);
|
||||
a.sub(rsp, 48);
|
||||
a.call(rax);
|
||||
a.add(rsp, 48);
|
||||
a.test(rax, rax);
|
||||
a.je(direct);
|
||||
a.mov(r15, __qword_ptr[rax]);
|
||||
a.test(r15, r15);
|
||||
a.je(direct);
|
||||
|
||||
// Already on the host stack below the saved RSP: an import or host code faulted.
|
||||
a.cmp(rbx, r15);
|
||||
a.jae(call);
|
||||
a.mov(rax, r15);
|
||||
a.sub(rax, rbx);
|
||||
a.cmp(rax, (int)HostStackSearchBytes);
|
||||
a.jb(direct);
|
||||
|
||||
a.Label(ref call);
|
||||
a.lea(rsp, __[r15 - SignalStackGap]);
|
||||
a.and(rsp, -16);
|
||||
a.Label(ref direct);
|
||||
a.mov(edi, r12d);
|
||||
a.mov(rsi, r13);
|
||||
a.mov(rdx, r14);
|
||||
a.mov(rax, (ulong)(nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal);
|
||||
a.call(rax);
|
||||
a.Label(ref done);
|
||||
a.mov(rsp, rbx);
|
||||
a.pop(r15);
|
||||
a.pop(r14);
|
||||
a.pop(r13);
|
||||
a.pop(r12);
|
||||
a.pop(rbx);
|
||||
a.ret();
|
||||
|
||||
var page = (byte*)HostMemory.Alloc(null, 4096, HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE);
|
||||
if (page == null)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate the POSIX signal trampoline page");
|
||||
}
|
||||
|
||||
var writer = new SpanCodeWriter(new Span<byte>(page, 4096));
|
||||
a.Assemble(writer, (ulong)page);
|
||||
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to protect the POSIX signal trampoline page");
|
||||
}
|
||||
|
||||
return (nint)page;
|
||||
}
|
||||
|
||||
private unsafe void PublishSignalStackState()
|
||||
{
|
||||
if (_signalStackCells == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
*(nint*)(_signalStackCells + 8) = _tlsGetValueAddress;
|
||||
*(uint*)_signalStackCells = _hostRspSlotTlsIndex;
|
||||
}
|
||||
|
||||
private sealed unsafe class SpanCodeWriter : CodeWriter
|
||||
{
|
||||
private readonly byte* _start;
|
||||
private readonly int _length;
|
||||
private int _offset;
|
||||
|
||||
public SpanCodeWriter(Span<byte> destination)
|
||||
{
|
||||
fixed (byte* start = destination)
|
||||
{
|
||||
_start = start;
|
||||
}
|
||||
|
||||
_length = destination.Length;
|
||||
}
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (_offset >= _length)
|
||||
{
|
||||
throw new InvalidOperationException("The POSIX signal trampoline does not fit its page.");
|
||||
}
|
||||
|
||||
_start[_offset++] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ using System;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
|
|
@ -24,7 +23,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
private const int PosixSigIll = 4;
|
||||
private const int PosixSigTrap = 5;
|
||||
private const int PosixSigAbort = 6;
|
||||
private const int PosixSigFpe = 8;
|
||||
private const int PosixSigSegv = 11;
|
||||
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
||||
|
||||
|
|
@ -47,28 +45,11 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
// registers in gregs[23]. Rosetta 2 delivers the regular x86-64 layout
|
||||
// to translated processes.
|
||||
private const int DarwinUcontextMcontextOffset = 48;
|
||||
private const int DarwinUserContextMachineContextSizeOffset = 40;
|
||||
// Darwin stores vector registers at the same offset in both signal frame formats.
|
||||
// The system headers i386/_mcontext.h and mach/i386/_structs.h define this offset.
|
||||
private const int DarwinMachineContextVectorRegistersOffset = 352;
|
||||
private const int DarwinMcontextErrOffset = 4;
|
||||
private const int DarwinMcontextFaultAddressOffset = 8;
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsErrOffset = 19 * 8;
|
||||
|
||||
// The kernel's x86-64 sigcontext places the FXSAVE-image pointer right
|
||||
// after the general registers it hands to the handler: err(152)
|
||||
// trapno(160) oldmask(168) cr2(176) fpstate(184), all relative to
|
||||
// GetPosixRegisterBase. glibc and musl both overlay this kernel layout
|
||||
// verbatim (glibc's mcontext_t.fpregs is the same slot), so the offset
|
||||
// is libc-independent. Inside the FXSAVE image the XMM registers start
|
||||
// at +160 (32-byte header + 8 legacy x87/MMX slots x 16 bytes) - the
|
||||
// same relative position they occupy in the Win64 CONTEXT's FltSave
|
||||
// area (Win64ContextXmm0Offset = 256 + 160).
|
||||
private const int LinuxGregsFpstateOffset = 184;
|
||||
private const int FxsaveXmmOffset = 160;
|
||||
private const int XmmBlockSize = 16 * 16;
|
||||
|
||||
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||
|
|
@ -79,7 +60,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
|
||||
private static DirectExecutionBackend? _posixSignalBackend;
|
||||
private static bool _posixSignalHandlersInstalled;
|
||||
private static nint _posixSignalTrampoline;
|
||||
private static bool _posixRawRecoveryEnabled;
|
||||
private static bool _posixSignalWarmup;
|
||||
private static readonly nint[] _posixPreviousActions = new nint[32];
|
||||
|
|
@ -91,11 +71,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
[ThreadStatic]
|
||||
private static int _posixSignalHandlerDepth;
|
||||
|
||||
// True when the signal context contains the current vector register values.
|
||||
// Copy changes back to these registers before the guest continues.
|
||||
[ThreadStatic]
|
||||
private static bool _posixXmmContextBridged;
|
||||
|
||||
private void SetupPosixExceptionHandler()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
||||
|
|
@ -107,7 +82,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
_posixSignalBackend = this;
|
||||
if (_posixSignalHandlersInstalled)
|
||||
{
|
||||
PublishSignalStackState();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -121,20 +95,17 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
WarmUpPosixSignalPath();
|
||||
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
|
||||
|
||||
_posixSignalTrampoline = CreatePosixSignalTrampoline();
|
||||
PublishSignalStackState();
|
||||
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||
!InstallPosixSignalHandler(PosixSigIll) ||
|
||||
!InstallPosixSignalHandler(PosixSigTrap) ||
|
||||
!InstallPosixSignalHandler(PosixSigAbort) ||
|
||||
!InstallPosixSignalHandler(PosixSigFpe))
|
||||
!InstallPosixSignalHandler(PosixSigAbort))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
||||
}
|
||||
|
||||
_posixSignalHandlersInstalled = true;
|
||||
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL/SIGFPE)");
|
||||
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -148,23 +119,19 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
/// </summary>
|
||||
private void WarmUpPosixSignalPath()
|
||||
{
|
||||
byte* testUserContext = stackalloc byte[512];
|
||||
new Span<byte>(testUserContext, 512).Clear();
|
||||
byte* testMachineContext = stackalloc byte[1024];
|
||||
new Span<byte>(testMachineContext, 1024).Clear();
|
||||
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**)(testUserContext + DarwinUcontextMcontextOffset) = testMachineContext;
|
||||
*(ulong*)(testUserContext + DarwinUserContextMachineContextSizeOffset) = 1024;
|
||||
// Load the Mach memory functions before the first signal uses them.
|
||||
byte readProbe = 0;
|
||||
_ = TryReadMacOsMemory((ulong)testMachineContext, &readProbe, 1);
|
||||
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
|
||||
}
|
||||
|
||||
_posixSignalWarmup = true;
|
||||
try
|
||||
{
|
||||
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)testUserContext);
|
||||
((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
|
||||
|
|
@ -198,7 +165,7 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
{
|
||||
byte* action = stackalloc byte[PosixSigactionSize];
|
||||
new Span<byte>(action, PosixSigactionSize).Clear();
|
||||
*(nint*)action = _posixSignalTrampoline;
|
||||
*(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
|
||||
|
|
@ -245,7 +212,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
// address (safe for host and guest threads alike) and must resume
|
||||
// the faulting write immediately after restoring write access.
|
||||
if (signal != PosixSigIll &&
|
||||
signal != PosixSigFpe &&
|
||||
siginfo != 0 &&
|
||||
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
|
||||
|
|
@ -253,17 +219,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
return;
|
||||
}
|
||||
|
||||
// GPU-tracked pages fault on every first CPU access after the GPU or an upload used them,
|
||||
// thousands of times a second in a frame. Resolving them needs only the address and the
|
||||
// access kind, so they skip the Win64 context conversion and the other handlers.
|
||||
if (PosixGpuFaultFastPath && !_posixSignalWarmup &&
|
||||
(signal == PosixSigSegv || signal == PosixSigBus) &&
|
||||
siginfo != 0 &&
|
||||
TryResolvePosixGpuFault(siginfo, ucontext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
||||
{
|
||||
return;
|
||||
|
|
@ -281,28 +236,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
ChainPreviousPosixAction(signal, siginfo, ucontext);
|
||||
}
|
||||
|
||||
private static readonly bool PosixGpuFaultFastPath =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_POSIX_GPU_FAULT_FAST_PATH") != "0";
|
||||
|
||||
private static bool TryResolvePosixGpuFault(nint siginfo, nint ucontext)
|
||||
{
|
||||
byte* registers = GetPosixRegisterBase(ucontext);
|
||||
if (registers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||
ulong rip = *(ulong*)(registers + PosixRegisterOffsets[16]);
|
||||
var kind = GetPosixAccessType(registers, faultAddress, rip) switch
|
||||
{
|
||||
0 => FaultKind.Read,
|
||||
1 => FaultKind.Write,
|
||||
_ => FaultKind.Unknown,
|
||||
};
|
||||
return kind != FaultKind.Unknown && GuestGpuMemoryHook.TryResolveFault(kind, faultAddress);
|
||||
}
|
||||
|
||||
private static bool TryHandlePosixFault(int signal, nint siginfo, nint ucontext)
|
||||
{
|
||||
byte* registers = GetPosixRegisterBase(ucontext);
|
||||
|
|
@ -319,19 +252,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||
}
|
||||
|
||||
// Copy the vector registers that instruction recovery can change.
|
||||
// Keep all other floating-point state unchanged.
|
||||
byte* vectorRegisters = GetSignalVectorRegisterAddress(ucontext, registers);
|
||||
if (vectorRegisters != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
vectorRegisters,
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
_posixXmmContextBridged = vectorRegisters != null;
|
||||
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||
if (signal == PosixSigIll)
|
||||
|
|
@ -346,11 +266,6 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
{
|
||||
record.ExceptionCode = 1073741845u;
|
||||
}
|
||||
else if (signal == PosixSigFpe)
|
||||
{
|
||||
// STATUS_INTEGER_DIVIDE_BY_ZERO: #DE from div/idiv (zero divisor or quotient overflow).
|
||||
record.ExceptionCode = 3221225620u;
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||
|
|
@ -402,36 +317,9 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
{
|
||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||
}
|
||||
if (vectorRegisters != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
vectorRegisters,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte* GetSignalVectorRegisterAddress(nint userContextAddress, byte* machineContext)
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
var machineContextSize = *(ulong*)((byte*)userContextAddress + DarwinUserContextMachineContextSizeOffset);
|
||||
return machineContextSize >= DarwinMachineContextVectorRegistersOffset + XmmBlockSize
|
||||
? machineContext + DarwinMachineContextVectorRegistersOffset
|
||||
: null;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
byte* floatingPointState = *(byte**)(machineContext + LinuxGregsFpstateOffset);
|
||||
return floatingPointState != null ? floatingPointState + FxsaveXmmOffset : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte* GetPosixRegisterBase(nint ucontext)
|
||||
{
|
||||
if (ucontext == 0)
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// The PS5 CPU has the SHA extensions and titles use them without a CPUID check.
|
||||
// Rosetta 2 does not translate them, so they raise #UD on Apple Silicon hosts.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private static int _shaSoftwareFallbackAnnounced;
|
||||
private static long _shaInstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverShaInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
|
||||
!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Sha1rnds4:
|
||||
case Mnemonic.Sha1nexte:
|
||||
case Mnemonic.Sha1msg1:
|
||||
case Mnemonic.Sha1msg2:
|
||||
case Mnemonic.Sha256rnds2:
|
||||
case Mnemonic.Sha256msg1:
|
||||
case Mnemonic.Sha256msg2:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.GetOpKind(0) != OpKind.Register ||
|
||||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset) ||
|
||||
!TryReadXmmOperand(contextRecord, in instruction, 1, out var source2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var source1 = ReadCtxXmm(contextRecord, destOffset);
|
||||
var result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Sha1rnds4 => ShaInstructionEmulator.Sha1Rnds4(source1, source2, instruction.Immediate8),
|
||||
Mnemonic.Sha1nexte => ShaInstructionEmulator.Sha1Nexte(source1, source2),
|
||||
Mnemonic.Sha1msg1 => ShaInstructionEmulator.Sha1Msg1(source1, source2),
|
||||
Mnemonic.Sha1msg2 => ShaInstructionEmulator.Sha1Msg2(source1, source2),
|
||||
Mnemonic.Sha256rnds2 => ShaInstructionEmulator.Sha256Rnds2(
|
||||
source1, source2, ReadCtxXmm(contextRecord, Win64ContextXmm0Offset)),
|
||||
Mnemonic.Sha256msg1 => ShaInstructionEmulator.Sha256Msg1(source1, source2),
|
||||
_ => ShaInstructionEmulator.Sha256Msg2(source1, source2),
|
||||
};
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, (ulong)result);
|
||||
WriteCtxU64(contextRecord, destOffset + 8, (ulong)(result >> 64));
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _shaInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _shaSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks the SHA extensions used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryReadXmmOperand(void* contextRecord, in Instruction instruction, int operandIndex, out UInt128 value)
|
||||
{
|
||||
value = 0;
|
||||
switch (instruction.GetOpKind(operandIndex))
|
||||
{
|
||||
case OpKind.Register:
|
||||
if (!TryGetXmmOffset(instruction.GetOpRegister(operandIndex), out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = ReadCtxXmm(contextRecord, offset);
|
||||
return true;
|
||||
|
||||
case OpKind.Memory:
|
||||
var buffer = new byte[16];
|
||||
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address) ||
|
||||
!TryReadHostBytes(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt128LittleEndian(buffer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe UInt128 ReadCtxXmm(void* contextRecord, int offset) =>
|
||||
new(ReadCtxU64(contextRecord, offset + 8), ReadCtxU64(contextRecord, offset));
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private const int HostStackStateBytes = 3 * sizeof(ulong);
|
||||
|
||||
internal static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
|
||||
{
|
||||
if (OperatingSystem.IsWindows() &&
|
||||
QueryVirtualMemoryInformation(-1, (void*)stackPointer, 0, out var allocation,
|
||||
(nuint)sizeof(StackMemoryRegionInformation), null) != 0 &&
|
||||
((allocation.Flags & 1) == 0 || allocation.CommitSize == allocation.RegionSize) &&
|
||||
allocation.RegionSize != 0 && allocation.RegionSize <= ulong.MaxValue - allocation.AllocationBase &&
|
||||
stackPointer >= allocation.AllocationBase && stackPointer - allocation.AllocationBase < allocation.RegionSize)
|
||||
{
|
||||
bottom = allocation.AllocationBase;
|
||||
top = bottom + allocation.RegionSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
bottom = top = 0;
|
||||
var queried = VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64));
|
||||
if (queried == 0 ||
|
||||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bottom = region.BaseAddress;
|
||||
top = region.BaseAddress + region.RegionSize;
|
||||
return stackPointer >= bottom && stackPointer < top;
|
||||
}
|
||||
|
||||
private static unsafe void EmitStackBound(byte* code, ref int offset, byte register, uint field, bool store)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x65);
|
||||
EmitByte(code, ref offset, (byte)(0x48 | (register >= 8 ? 4 : 0)));
|
||||
EmitByte(code, ref offset, store ? (byte)0x89 : (byte)0x8B);
|
||||
EmitByte(code, ref offset, (byte)(0x04 | ((register & 7) << 3)));
|
||||
EmitByte(code, ref offset, 0x25);
|
||||
EmitUInt32(code, ref offset, field);
|
||||
}
|
||||
|
||||
// Fiber switches replace the guest stack without passing through a guest-entry stub, so the
|
||||
// context-transfer stub keeps the Windows TEB bounds in sync from the target state in R11
|
||||
// (stack top at +160, bottom at +168). Only Windows keeps stack bounds at gs:[8]/gs:[16]: on
|
||||
// macOS gs addresses the pthread TSD, where those slots hold errno's address and another key.
|
||||
// Writing a fiber's bounds there nulled errno, and the next failed libc call on the thread
|
||||
// crashed reading it (Demon's Souls, about 10 s into boot).
|
||||
private static unsafe void EmitFiberStackBounds(byte* code, ref int offset)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
ReadOnlySpan<byte> loadTop = [0x4D, 0x8B, 0x93, 160, 0, 0, 0]; // mov r10, [r11+160]
|
||||
foreach (var value in loadTop) EmitByte(code, ref offset, value);
|
||||
EmitStackBound(code, ref offset, 10, 8, store: true);
|
||||
ReadOnlySpan<byte> loadBottom = [0x4D, 0x8B, 0x93, 168, 0, 0, 0]; // mov r10, [r11+168]
|
||||
foreach (var value in loadBottom) EmitByte(code, ref offset, value);
|
||||
EmitStackBound(code, ref offset, 10, 16, store: true);
|
||||
}
|
||||
|
||||
// Test hook: the bytes the fiber stack-bounds sync emits on this host.
|
||||
internal static unsafe byte[] EmitFiberStackBoundsForTest()
|
||||
{
|
||||
var buffer = new byte[64];
|
||||
var offset = 0;
|
||||
fixed (byte* code = buffer)
|
||||
{
|
||||
EmitFiberStackBounds(code, ref offset);
|
||||
}
|
||||
|
||||
return buffer[..offset];
|
||||
}
|
||||
|
||||
// R10 points to the entry state. R11 is scratch; RAX remains unchanged.
|
||||
private static unsafe void EmitHostStackBounds(byte* code, ref int offset, bool save)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
for (byte field = 8; field <= 16; field += 8)
|
||||
{
|
||||
if (save) EmitStackBound(code, ref offset, 11, field, store: false);
|
||||
EmitByte(code, ref offset, 0x4D);
|
||||
EmitByte(code, ref offset, save ? (byte)0x89 : (byte)0x8B);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, field);
|
||||
if (!save) EmitStackBound(code, ref offset, 11, field, store: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void EmitGuestStackBounds(byte* code, ref int offset, ulong stackPointer)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top))
|
||||
{
|
||||
throw new InvalidOperationException("Guest stack has no committed host mapping.");
|
||||
}
|
||||
|
||||
EmitHostStackBounds(code, ref offset, save: true);
|
||||
for (uint field = 8; field <= 16; field += 8)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x49);
|
||||
EmitByte(code, ref offset, 0xBB);
|
||||
*(ulong*)(code + offset) = field == 8 ? top : bottom;
|
||||
offset += sizeof(ulong);
|
||||
EmitStackBound(code, ref offset, 11, field, store: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void EmitSavedStackBounds(byte* code, ref int offset, bool restore)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
EmitStackBound(code, ref offset, 14, 8, restore);
|
||||
EmitStackBound(code, ref offset, 15, 16, restore);
|
||||
}
|
||||
|
||||
private unsafe void EmitExceptionHostStackBounds(byte* code, ref int offset)
|
||||
{
|
||||
EmitSavedStackBounds(code, ref offset, restore: false);
|
||||
ReadOnlySpan<byte> reserve = [0x48, 0x83, 0xEC, 0x28, 0xB9];
|
||||
foreach (var value in reserve) EmitByte(code, ref offset, value);
|
||||
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
ReadOnlySpan<byte> lookup = [0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x28, 0x48, 0x85, 0xC0, 0x0F, 0x84];
|
||||
foreach (var value in lookup) EmitByte(code, ref offset, value);
|
||||
var missingState = offset;
|
||||
EmitUInt32(code, ref offset, 0);
|
||||
// The state is zeroed at allocation; bounds the entry stub has not saved yet stay untouched.
|
||||
ReadOnlySpan<byte> unsaved = [0x48, 0x83, 0x78, 0x08, 0x00, 0x0F, 0x84];
|
||||
foreach (var value in unsaved) EmitByte(code, ref offset, value);
|
||||
var missingBounds = offset;
|
||||
EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
// A host fault already has live bounds. Do not replace a grown limit with an old one.
|
||||
ReadOnlySpan<byte> sameStack = [0x4C, 0x3B, 0x70, 0x08, 0x0F, 0x85];
|
||||
foreach (var value in sameStack) EmitByte(code, ref offset, value);
|
||||
var guestStack = offset;
|
||||
EmitUInt32(code, ref offset, 0);
|
||||
EmitHostStackBounds(code, ref offset, save: true);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
var hostStack = offset;
|
||||
EmitUInt32(code, ref offset, 0);
|
||||
*(int*)(code + guestStack) = offset - guestStack - sizeof(int);
|
||||
EmitHostStackBounds(code, ref offset, save: false);
|
||||
*(int*)(code + hostStack) = offset - hostStack - sizeof(int);
|
||||
*(int*)(code + missingState) = offset - missingState - sizeof(int);
|
||||
*(int*)(code + missingBounds) = offset - missingBounds - sizeof(int);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static unsafe int SelectExceptionStackBounds(void* exceptionInfo, int disposition, ulong* bounds)
|
||||
{
|
||||
if (disposition != -1) return disposition;
|
||||
var context = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||
if (context == null) return 0;
|
||||
var stackPointer = ReadCtxU64(context, 152);
|
||||
if (stackPointer >= bounds[1] && stackPointer < bounds[0]) return disposition;
|
||||
|
||||
GetCurrentThreadStackLimits(out var nativeBottom, out var nativeTop);
|
||||
if (stackPointer >= nativeBottom && stackPointer < nativeTop)
|
||||
{
|
||||
bounds[0] = nativeTop;
|
||||
bounds[1] = nativeBottom;
|
||||
return disposition;
|
||||
}
|
||||
|
||||
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top)) return 0;
|
||||
bounds[0] = top;
|
||||
bounds[1] = bottom;
|
||||
return disposition;
|
||||
}
|
||||
|
||||
private static unsafe void EmitExceptionStackSelection(byte* code, ref int offset)
|
||||
{
|
||||
// The caller reserves 72 bytes: ABI home space and two stack bounds.
|
||||
ReadOnlySpan<byte> arguments = [0x4C, 0x89, 0x74, 0x24, 0x28, 0x4C, 0x89, 0x7C, 0x24, 0x30,
|
||||
0x89, 0xC2, 0x4C, 0x89, 0xE9, 0x4C, 0x8D, 0x44, 0x24, 0x28, 0x48, 0xB8];
|
||||
foreach (var value in arguments) EmitByte(code, ref offset, value);
|
||||
*(nint*)(code + offset) = (nint)(delegate* unmanaged<void*, int, ulong*, int>)&SelectExceptionStackBounds;
|
||||
offset += sizeof(nint);
|
||||
ReadOnlySpan<byte> result = [0xFF, 0xD0, 0x4C, 0x8B, 0x74, 0x24, 0x28, 0x4C, 0x8B, 0x7C, 0x24, 0x30];
|
||||
foreach (var value in result) EmitByte(code, ref offset, value);
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||
private static extern void GetCurrentThreadStackLimits(out ulong lowLimit, out ulong highLimit);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct StackMemoryRegionInformation
|
||||
{
|
||||
public ulong AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public uint Flags;
|
||||
public ulong RegionSize;
|
||||
public ulong CommitSize;
|
||||
}
|
||||
|
||||
[DllImport("api-ms-win-core-memory-l1-1-4.dll", ExactSpelling = true)]
|
||||
private static extern unsafe int QueryVirtualMemoryInformation(nint process, void* address, int informationClass,
|
||||
out StackMemoryRegionInformation information, nuint informationSize, nuint* returnSize);
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
private SafeWaitHandle? _mainExecutionThreadHandle;
|
||||
// One immutable code page serves all diagnostic samples for the process lifetime.
|
||||
private static readonly Lazy<ThreadContextCaptureCode> ThreadCaptureCode = new(() => new());
|
||||
|
||||
private sealed class ThreadContextCaptureCode : SafeHandle
|
||||
{
|
||||
public override bool IsInvalid => handle == 0;
|
||||
|
||||
public ThreadContextCaptureCode() : base(0, ownsHandle: true)
|
||||
{
|
||||
var kernelModule = GetModuleHandle("kernel32.dll");
|
||||
var suspendAddress = GetProcAddress(kernelModule, "SuspendThread");
|
||||
var contextAddress = GetProcAddress(kernelModule, "GetThreadContext");
|
||||
var resumeAddress = GetProcAddress(kernelModule, "ResumeThread");
|
||||
if (suspendAddress == 0 || contextAddress == 0 || resumeAddress == 0) return;
|
||||
var instructions = BuildThreadContextCaptureCode(suspendAddress, contextAddress, resumeAddress);
|
||||
SetHandle((nint)VirtualAlloc(null, (nuint)instructions.Length, 0x3000, 0x04));
|
||||
if (IsInvalid) return;
|
||||
instructions.CopyTo(new Span<byte>((void*)handle, instructions.Length));
|
||||
uint previousProtection = 0;
|
||||
if (!VirtualProtect((void*)handle, (nuint)instructions.Length, 0x20, &previousProtection))
|
||||
{
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)handle, (nuint)instructions.Length);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle() => VirtualFree((void*)handle, 0, 0x8000);
|
||||
}
|
||||
|
||||
private static SafeWaitHandle? OpenThreadForContext(uint threadId)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 || threadId == 0)
|
||||
return null;
|
||||
|
||||
const uint queryLimitedInformation = 0x0800;
|
||||
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume | queryLimitedInformation, false, threadId);
|
||||
return threadHandle == 0 ? null : new SafeWaitHandle(threadHandle, ownsHandle: true);
|
||||
}
|
||||
|
||||
private static bool TryCaptureHostThreadContext(int threadId, out HostThreadContextSnapshot snapshot)
|
||||
{
|
||||
using var threadHandle = OpenThreadForContext(unchecked((uint)threadId));
|
||||
return TryCaptureThreadContext(threadHandle, out snapshot);
|
||||
}
|
||||
|
||||
private static bool TryCaptureThreadContext(SafeWaitHandle? threadHandle, out HostThreadContextSnapshot snapshot)
|
||||
{
|
||||
snapshot = default;
|
||||
if (threadHandle is null || !OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
return false;
|
||||
|
||||
var handleReferenced = false;
|
||||
try
|
||||
{
|
||||
threadHandle.DangerousAddRef(ref handleReferenced);
|
||||
var nativeHandle = threadHandle.DangerousGetHandle();
|
||||
var threadId = GetThreadId(nativeHandle);
|
||||
if (threadId == 0 || threadId == GetCurrentThreadId()) return false;
|
||||
|
||||
var captureCode = ThreadCaptureCode.Value;
|
||||
if (captureCode.IsInvalid || captureCode.IsClosed) return false;
|
||||
|
||||
byte* contextStorage = stackalloc byte[Win64ContextSize + 15];
|
||||
var contextRecord = (void*)(((nuint)contextStorage + 15) & ~(nuint)15);
|
||||
new Span<byte>(contextRecord, Win64ContextSize).Clear();
|
||||
WriteCtxU32(contextRecord, Win64ContextFlagsOffset, ContextAmd64ControlInteger);
|
||||
|
||||
// One native transition keeps managed work outside the suspended interval.
|
||||
var captured = ((delegate* unmanaged<nint, void*, int>)captureCode.DangerousGetHandle())(nativeHandle, contextRecord);
|
||||
if (captured == 0) return false;
|
||||
snapshot = DecodeHostThreadContext(threadId, contextRecord);
|
||||
return true;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handleReferenced) threadHandle!.DangerousRelease();
|
||||
}
|
||||
}
|
||||
|
||||
private static HostThreadContextSnapshot DecodeHostThreadContext(uint threadId, void* contextRecord) => new(
|
||||
threadId,
|
||||
ReadCtxU64(contextRecord, 248), ReadCtxU64(contextRecord, 152), ReadCtxU64(contextRecord, 160),
|
||||
ReadCtxU64(contextRecord, 120), ReadCtxU64(contextRecord, 144), ReadCtxU64(contextRecord, 128),
|
||||
ReadCtxU64(contextRecord, 136), ReadCtxU64(contextRecord, 168), ReadCtxU64(contextRecord, 176),
|
||||
ReadCtxU64(contextRecord, 184), ReadCtxU64(contextRecord, 192), ReadCtxU64(contextRecord, 200),
|
||||
ReadCtxU64(contextRecord, 208), ReadCtxU64(contextRecord, 216), ReadCtxU64(contextRecord, 224),
|
||||
ReadCtxU64(contextRecord, 232), ReadCtxU64(contextRecord, 240));
|
||||
|
||||
internal static byte[] BuildThreadContextCaptureCode(nint suspendAddress, nint contextAddress, nint resumeAddress)
|
||||
{
|
||||
var instructions = new List<byte>();
|
||||
void CallAddress(nint address)
|
||||
{
|
||||
instructions.AddRange([0x48, 0xB8]);
|
||||
instructions.AddRange(BitConverter.GetBytes((long)address));
|
||||
instructions.AddRange([0xFF, 0xD0]);
|
||||
}
|
||||
|
||||
// Preserve nonvolatile registers and reserve aligned argument space.
|
||||
instructions.AddRange([0x53, 0x56, 0x57, 0x48, 0x83, 0xEC, 0x20,
|
||||
0x48, 0x89, 0xCB, 0x48, 0x89, 0xD6, 0x31, 0xFF]);
|
||||
CallAddress(suspendAddress);
|
||||
instructions.AddRange([0x83, 0xF8, 0xFF, 0x74, 0]);
|
||||
var failedSuspendJump = instructions.Count - 1;
|
||||
instructions.AddRange([0x48, 0x89, 0xD9, 0x48, 0x89, 0xF2]);
|
||||
CallAddress(contextAddress);
|
||||
instructions.AddRange([0x89, 0xC7, 0x48, 0x89, 0xD9]);
|
||||
CallAddress(resumeAddress);
|
||||
instructions.AddRange([0x83, 0xF8, 0xFF, 0x75, 0x02, 0x31, 0xFF]);
|
||||
var returnOffset = instructions.Count;
|
||||
instructions.AddRange([0x89, 0xF8, 0x48, 0x83, 0xC4, 0x20, 0x5F, 0x5E, 0x5B, 0xC3]);
|
||||
instructions[failedSuspendJump] = checked((byte)(returnOffset - failedSuspendJump - 1));
|
||||
return instructions.ToArray();
|
||||
}
|
||||
|
||||
private void LogMainThreadStallContext()
|
||||
{
|
||||
if (!TryCaptureThreadContext(Volatile.Read(ref _mainExecutionThreadHandle), out var context)) return;
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Live hardware context (main thread {context.ThreadId}):");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] RIP=0x{context.Rip:X16} RSP=0x{context.Rsp:X16} RBP=0x{context.Rbp:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] RAX=0x{context.Rax:X16} RBX=0x{context.Rbx:X16} RCX=0x{context.Rcx:X16} RDX=0x{context.Rdx:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] RSI=0x{context.Rsi:X16} RDI=0x{context.Rdi:X16} R8=0x{context.R8:X16} R9=0x{context.R9:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] R10=0x{context.R10:X16} R11=0x{context.R11:X16} R12=0x{context.R12:X16} R13=0x{context.R13:X16} R14=0x{context.R14:X16} R15=0x{context.R15:X16}");
|
||||
|
||||
var modules = Array.Empty<(ulong Start, ulong Size, string Name)>();
|
||||
try
|
||||
{
|
||||
using var process = Process.GetCurrentProcess();
|
||||
modules = process.Modules.Cast<ProcessModule>().Select(module =>
|
||||
(Start: unchecked((ulong)module.BaseAddress), Size: (ulong)module.ModuleMemorySize, Name: module.ModuleName)).ToArray();
|
||||
}
|
||||
catch (Win32Exception) { }
|
||||
catch (InvalidOperationException) { }
|
||||
string DescribeAddress(ulong address)
|
||||
{
|
||||
foreach (var module in modules)
|
||||
if (address >= module.Start && address - module.Start < module.Size)
|
||||
return $"{module.Name}+0x{address - module.Start:X}";
|
||||
return $"0x{address:X16}";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] RIP location: {DescribeAddress(context.Rip)}");
|
||||
// The thread has resumed. These bytes are best-effort observations, not an atomic snapshot.
|
||||
var instructionBytes = new byte[48];
|
||||
if (context.Rip >= 32 && TryReadHostBytes(context.Rip - 32, instructionBytes))
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @[RIP-32..RIP+16]: {Convert.ToHexString(instructionBytes)}");
|
||||
var currentInstruction = new byte[16];
|
||||
if (TryReadHostBytes(context.Rip, currentInstruction))
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @RIP: {Convert.ToHexString(currentInstruction)}");
|
||||
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Live stack pointer candidates:");
|
||||
for (var offset = 0; offset < 256; offset += sizeof(ulong))
|
||||
{
|
||||
var address = context.Rsp + (ulong)offset;
|
||||
if (address < context.Rsp || !TryReadDiagnosticHostQword(address, out var value)) break;
|
||||
if (value != 0)
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] [rsp+0x{offset:X2}] = 0x{value:X16} ({DescribeAddress(value)})");
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetThreadId(nint threadHandle);
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
internal sealed class GuestThreadFlowProfile(int capacity = 131072)
|
||||
{
|
||||
internal static readonly bool Enabled =
|
||||
(Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1" ||
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER") == "1") &&
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE") == "1";
|
||||
|
||||
internal enum EventKind
|
||||
{
|
||||
Blocked, Ready, Claimed, Scheduled, RunStarted, RunStopped, CallbackResumed,
|
||||
ExecutorReleased, ClaimDeferred
|
||||
}
|
||||
|
||||
internal readonly record struct TraceEvent(long Timestamp, int HostThreadId, ulong GuestThreadHandle,
|
||||
string Name, EventKind Kind, string? Reason, string? WakeKey, string? ImportNid,
|
||||
ulong Argument0, ulong Argument1, long DeadlineTimestamp);
|
||||
|
||||
internal readonly record struct Snapshot(TraceEvent[] Events, long TotalEvents);
|
||||
|
||||
private readonly int _capacity = capacity > 0 ? capacity : throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
private readonly object _gate = new();
|
||||
private readonly object _writeGate = new();
|
||||
private TraceEvent[]? _events;
|
||||
private long _eventCount;
|
||||
private bool _closed;
|
||||
|
||||
internal void StartSession()
|
||||
{
|
||||
lock (_writeGate)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_events = null;
|
||||
_eventCount = 0;
|
||||
_closed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Record(in TraceEvent traceEvent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed) return;
|
||||
_events ??= new TraceEvent[_capacity];
|
||||
_events[(int)(_eventCount % _capacity)] = traceEvent;
|
||||
_eventCount++;
|
||||
}
|
||||
}
|
||||
|
||||
internal Snapshot Close()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_closed = true;
|
||||
var retained = (int)Math.Min(_eventCount, _capacity);
|
||||
var records = new TraceEvent[retained];
|
||||
var first = _eventCount - retained;
|
||||
for (var index = 0; index < retained; index++)
|
||||
records[index] = _events![(int)((first + index) % _capacity)];
|
||||
var snapshot = new Snapshot(records, _eventCount);
|
||||
_events = null;
|
||||
_eventCount = 0;
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
internal void WriteTrace(TextWriter output)
|
||||
{
|
||||
// A concurrent shutdown caller must wait for the first writer to finish.
|
||||
lock (_writeGate)
|
||||
{
|
||||
WriteRetainedEvents(output);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteRetainedEvents(TextWriter output)
|
||||
{
|
||||
var snapshot = Close();
|
||||
if (snapshot.TotalEvents == 0) return;
|
||||
output.WriteLine($"[PERF][GUEST_FLOW_TRACE] frequency={Stopwatch.Frequency} retained={snapshot.Events.Length} overwritten={snapshot.TotalEvents - snapshot.Events.Length}");
|
||||
for (var index = 0; index < snapshot.Events.Length; index++)
|
||||
{
|
||||
var item = snapshot.Events[index];
|
||||
output.WriteLine($"[PERF][GUEST_FLOW] sequence={snapshot.TotalEvents - snapshot.Events.Length + index} timestamp={item.Timestamp} thread={item.HostThreadId} guest=0x{item.GuestThreadHandle:X} name='{Quote(item.Name)}' event={item.Kind} reason='{Quote(item.Reason)}' wake='{Quote(item.WakeKey)}' nid='{Quote(item.ImportNid)}' arg0=0x{item.Argument0:X} arg1=0x{item.Argument1:X} deadline={item.DeadlineTimestamp}");
|
||||
}
|
||||
output.Flush();
|
||||
}
|
||||
|
||||
// Guest names must not split one trace event across multiple output lines.
|
||||
private static string Quote(string? value) => (value ?? "none")
|
||||
.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n");
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ public static unsafe class JitStubs
|
|||
var pattern = TlsAccessPattern;
|
||||
var end = start + length - pattern.Length;
|
||||
|
||||
for (var ptr = start; ptr <= end; ptr++)
|
||||
for (var ptr = start; ptr < end; ptr++)
|
||||
{
|
||||
if (MatchesPattern(ptr, pattern))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
// Linux: call pthread_getspecific, preserving the registers that are
|
||||
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
||||
var pthreadGetSpecific = NativeLibrary.GetExport(NativeLibrary.Load("libpthread.so.0"), "pthread_getspecific");
|
||||
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
|
||||
|
|
@ -366,25 +366,25 @@ internal static unsafe class PosixHostStubs
|
|||
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
||||
|
||||
[DllImport("libpthread.so.0", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||
[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("libpthread.so.0", EntryPoint = "pthread_key_delete")]
|
||||
[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("libpthread.so.0", EntryPoint = "pthread_setspecific")]
|
||||
[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("libpthread.so.0", EntryPoint = "pthread_getspecific")]
|
||||
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||
private static extern nint pthread_getspecific_linux(uint key);
|
||||
|
||||
[DllImport("libc")]
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ internal static class WindowsFaultCodes
|
|||
public const uint FastFail = 0xC0000409u; // 3221226505
|
||||
public const uint StackOverflow = 0xC00000FDu;
|
||||
public const uint ClrManagedException = 0xE0434352u;
|
||||
public const uint DebugOutputAnsi = 0x40010006u;
|
||||
public const uint DebugOutputUnicode = 0x4001000Au;
|
||||
|
||||
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
|
||||
public const ulong AccessRead = 0;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
|||
|
||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||
{
|
||||
const uint stubSize = 1024u;
|
||||
const uint stubSize = 256u;
|
||||
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||
if (ptr == null)
|
||||
{
|
||||
|
|
@ -43,16 +43,11 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
|||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
//
|
||||
// FastFail (0xC0000409) is logged from this native path only: managed VEH never
|
||||
// sees it (tLT18–21 silent exits after TBB AV recovery).
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow,
|
||||
WindowsFaultCodes.DebugOutputAnsi, WindowsFaultCodes.DebugOutputUnicode];
|
||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
int fastFailJumpSlot = -1;
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
|
|
@ -60,162 +55,13 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
|||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
if (nonManagedExceptionCodes[i] == WindowsFaultCodes.FastFail)
|
||||
{
|
||||
fastFailJumpSlot = i;
|
||||
}
|
||||
}
|
||||
EmitByte(code, ref offset, 0xE9); // jmp mainBody rel32 (FastFail breadcrumb sits between)
|
||||
var mainBodyJumpSlot = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
|
||||
// FastFail: native stderr breadcrumb with Context.Rip (no managed entry), then CONTINUE_SEARCH.
|
||||
// Keep in sync with DirectExecutionBackend.CreateExceptionHandlerTrampoline.
|
||||
int fastFailPassOffset = offset;
|
||||
var fastFailLogInstalled = false;
|
||||
if (fastFailJumpSlot >= 0 &&
|
||||
NativeLibrary.TryLoad("kernel32.dll", out var kernel32) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "GetStdHandle", out var getStdHandle) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "WriteFile", out var writeFile))
|
||||
{
|
||||
ReadOnlySpan<byte> msg =
|
||||
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8;
|
||||
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
|
||||
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
|
||||
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8]
|
||||
EmitByte(code, ref offset, 0x50); // push rax
|
||||
EmitByte(code, ref offset, 0x51); // push rcx
|
||||
EmitByte(code, ref offset, 0x52); // push rdx
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); // push r8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); // push r9
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
var msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
|
||||
var hexDigitsAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30]
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0);
|
||||
int hexLoopOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x04);
|
||||
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
|
||||
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9);
|
||||
EmitByte(code, ref offset, 0x75);
|
||||
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz rel8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
|
||||
EmitByte(code, ref offset, 0x0A);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, 17u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
var msgOffset = offset;
|
||||
for (int i = 0; i < msg.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msg[i]);
|
||||
}
|
||||
|
||||
var hexDigitsOffset = offset;
|
||||
for (int i = 0; i < hexDigits.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, hexDigits[i]);
|
||||
}
|
||||
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
|
||||
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
|
||||
code[passJumpOffsets[fastFailJumpSlot]] =
|
||||
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
|
||||
fastFailLogInstalled = true;
|
||||
}
|
||||
|
||||
int mainBodyOffset = offset;
|
||||
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
|
||||
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||
|
||||
public ICpuMemory Inner => _inner;
|
||||
|
||||
public string DescribeReadRange(ulong address, ulong size) => _inner.DescribeReadRange(address, size);
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
var result = _inner.TryRead(virtualAddress, destination);
|
||||
|
|
@ -42,20 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||
return result;
|
||||
}
|
||||
|
||||
public bool TryCompare(
|
||||
ulong virtualAddress,
|
||||
ReadOnlySpan<byte> expected,
|
||||
out bool equal) =>
|
||||
_inner.TryCompare(virtualAddress, expected, out equal);
|
||||
|
||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
|
||||
_inner.TryCopy(destinationAddress, sourceAddress, length);
|
||||
|
||||
public bool CanRead(ulong address, ulong size) => _inner.CanRead(address, size);
|
||||
|
||||
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match) =>
|
||||
_inner.TryScanCString(address, needle, findLast, maxLength, out match);
|
||||
|
||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||
{
|
||||
if (_inner is IGuestMemoryAllocator allocator)
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Core.Diagnostics;
|
||||
|
||||
public static partial class WindowsCrashCapture
|
||||
{
|
||||
internal sealed class DebuggerBreakTracker
|
||||
{
|
||||
private readonly Dictionary<uint, nint> _threadStarts = new();
|
||||
private nint _libraryBase;
|
||||
private nint _breakThreadEntry;
|
||||
private nint _breakpointAddress;
|
||||
|
||||
internal void Observe(DebugEvent debugEvent)
|
||||
{
|
||||
switch (debugEvent.Kind)
|
||||
{
|
||||
case CreateProcessEvent:
|
||||
_threadStarts[debugEvent.ThreadId] = debugEvent.ProcessThreadStartAddress;
|
||||
break;
|
||||
case CreateThreadEvent:
|
||||
_threadStarts[debugEvent.ThreadId] = debugEvent.ThreadStartAddress;
|
||||
break;
|
||||
case ExitThreadEvent:
|
||||
_threadStarts.Remove(debugEvent.ThreadId);
|
||||
break;
|
||||
case LoadLibraryEvent:
|
||||
if (TryReadDebuggerExports(debugEvent.FileHandle, out var threadOffset, out var breakpointOffset))
|
||||
{
|
||||
_libraryBase = debugEvent.LoadedLibraryBase;
|
||||
_breakThreadEntry = checked(_libraryBase + threadOffset);
|
||||
_breakpointAddress = checked(_libraryBase + breakpointOffset);
|
||||
}
|
||||
break;
|
||||
case UnloadLibraryEvent when debugEvent.UnloadedLibraryBase == _libraryBase:
|
||||
_libraryBase = _breakThreadEntry = _breakpointAddress = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsDebuggerBreak(DebugEvent debugEvent) =>
|
||||
debugEvent.Kind == ExceptionEvent && debugEvent.FirstChance != 0 &&
|
||||
debugEvent.Exception.Code == BreakpointException && _libraryBase != 0 &&
|
||||
debugEvent.Exception.Address == _breakpointAddress &&
|
||||
_threadStarts.TryGetValue(debugEvent.ThreadId, out var startAddress) && startAddress == _breakThreadEntry;
|
||||
}
|
||||
|
||||
private static unsafe bool TryReadDebuggerExports(nint fileHandle, out int threadOffset, out int breakpointOffset)
|
||||
{
|
||||
threadOffset = breakpointOffset = 0;
|
||||
if (fileHandle == 0 || fileHandle == -1)
|
||||
return false;
|
||||
const int pathCapacity = 512;
|
||||
var pathBuffer = stackalloc char[pathCapacity];
|
||||
var pathLength = GetFinalPathNameByHandle(fileHandle, pathBuffer, pathCapacity, 0);
|
||||
if (pathLength == 0 || pathLength >= pathCapacity)
|
||||
return false;
|
||||
var path = new string(pathBuffer, 0, (int)pathLength);
|
||||
var systemLibrary = @"\\?\" + Path.Combine(Environment.SystemDirectory, "ntdll.dll");
|
||||
if (!string.Equals(path, systemLibrary, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// Read the file supplied by the debug event, then use the target's load address.
|
||||
using var handle = new SafeFileHandle(fileHandle, ownsHandle: false);
|
||||
using var stream = new FileStream(handle, FileAccess.Read);
|
||||
using var image = new PEReader(stream);
|
||||
if (image.PEHeaders.PEHeader is not { Magic: PEMagic.PE32Plus } header)
|
||||
return false;
|
||||
var directory = header.ExportTableDirectory;
|
||||
if (directory.Size < 40)
|
||||
return false;
|
||||
var exports = image.GetSectionData(directory.RelativeVirtualAddress).GetReader(0, 40);
|
||||
exports.Offset = 20;
|
||||
var functionCount = exports.ReadInt32();
|
||||
var nameCount = exports.ReadInt32();
|
||||
var functionsAddress = exports.ReadInt32();
|
||||
var namesAddress = exports.ReadInt32();
|
||||
var ordinalsAddress = exports.ReadInt32();
|
||||
var functions = image.GetSectionData(functionsAddress).GetReader(0, checked(functionCount * 4));
|
||||
var names = image.GetSectionData(namesAddress).GetReader(0, checked(nameCount * 4));
|
||||
var ordinals = image.GetSectionData(ordinalsAddress).GetReader(0, checked(nameCount * 2));
|
||||
for (var nameIndex = 0; nameIndex < nameCount; nameIndex++)
|
||||
{
|
||||
var nameBlock = image.GetSectionData(names.ReadInt32());
|
||||
var nameBytes = nameBlock.GetContent(0, Math.Min(nameBlock.Length, "DbgUiRemoteBreakin".Length + 1)).AsSpan();
|
||||
var terminator = nameBytes.IndexOf((byte)0);
|
||||
var ordinal = ordinals.ReadUInt16();
|
||||
if (ordinal >= functionCount)
|
||||
return false;
|
||||
if (terminator < 0)
|
||||
continue;
|
||||
var name = Encoding.ASCII.GetString(nameBytes[..terminator]);
|
||||
if (name is not ("DbgUiRemoteBreakin" or "DbgBreakPoint"))
|
||||
continue;
|
||||
functions.Offset = ordinal * 4;
|
||||
var functionOffset = functions.ReadInt32();
|
||||
// A forwarded export is not executable code in this image.
|
||||
if (functionOffset <= 0 || functionOffset >= header.SizeOfImage ||
|
||||
(functionOffset >= directory.RelativeVirtualAddress &&
|
||||
functionOffset - directory.RelativeVirtualAddress < directory.Size))
|
||||
return false;
|
||||
if (name == "DbgUiRemoteBreakin")
|
||||
threadOffset = functionOffset;
|
||||
else
|
||||
breakpointOffset = functionOffset;
|
||||
}
|
||||
return threadOffset != 0 && breakpointOffset != 0;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or BadImageFormatException or ArgumentException or OverflowException)
|
||||
{
|
||||
threadOffset = breakpointOffset = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Core.Diagnostics;
|
||||
|
||||
public static partial class WindowsCrashCapture
|
||||
{
|
||||
private const uint ExceptionEvent = 1;
|
||||
private const uint CreateThreadEvent = 2;
|
||||
private const uint CreateProcessEvent = 3;
|
||||
private const uint ExitThreadEvent = 4;
|
||||
private const uint ExitProcessEvent = 5;
|
||||
private const uint LoadLibraryEvent = 6;
|
||||
private const uint UnloadLibraryEvent = 7;
|
||||
private const uint BreakpointException = 0x80000003;
|
||||
private const uint DebugContinue = 0x00010002;
|
||||
private const uint DebugExceptionNotHandled = 0x80010001;
|
||||
private const uint ProcessReadAccess = 0x0450;
|
||||
private const uint ThreadContextAccess = 0x0008;
|
||||
private const uint DumpFullMemory = 0x00000002;
|
||||
private const uint DumpHandleData = 0x00000004;
|
||||
private const uint DumpUnloadedModules = 0x00000020;
|
||||
private const uint DumpMemoryInformation = 0x00000800;
|
||||
private const uint DumpThreadInformation = 0x00001000;
|
||||
private const uint DumpIgnoreInaccessibleMemory = 0x00020000;
|
||||
private const uint FullDumpFlags = DumpFullMemory | DumpHandleData | DumpUnloadedModules |
|
||||
DumpMemoryInformation | DumpThreadInformation | DumpIgnoreInaccessibleMemory;
|
||||
private const uint AllX64ContextFlags = 0x0010001F;
|
||||
|
||||
private static unsafe void WriteDump(uint processId, DebugEvent debugEvent, string dumpPath)
|
||||
{
|
||||
using var process = OpenProcess(ProcessReadAccess, false, processId);
|
||||
if (process.IsInvalid)
|
||||
throw NativeFailure("Open the crashed process");
|
||||
using var thread = OpenThread(ThreadContextAccess, false, debugEvent.ThreadId);
|
||||
if (thread.IsInvalid)
|
||||
throw NativeFailure("Open the faulting thread");
|
||||
var context = (ThreadContext*)NativeMemory.AlignedAlloc((nuint)sizeof(ThreadContext), 16);
|
||||
if (context == null)
|
||||
throw new OutOfMemoryException();
|
||||
try
|
||||
{
|
||||
NativeMemory.Clear(context, (nuint)sizeof(ThreadContext));
|
||||
context->Flags = AllX64ContextFlags;
|
||||
if (!GetThreadContext(thread, context))
|
||||
throw NativeFailure("Read the faulting thread context");
|
||||
|
||||
// The exception pointers refer to this helper, not to the crashed process.
|
||||
var record = debugEvent.Exception;
|
||||
record.NestedRecord = 0;
|
||||
var pointers = new ExceptionPointers { Record = &record, Context = context };
|
||||
var exception = new DumpExceptionInformation
|
||||
{
|
||||
ThreadId = debugEvent.ThreadId,
|
||||
Pointers = &pointers,
|
||||
ClientPointers = 0,
|
||||
};
|
||||
using var output = new FileStream(dumpPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
|
||||
if (!MiniDumpWriteDump(process, processId, output.SafeFileHandle, FullDumpFlags, &exception, 0, 0))
|
||||
throw NativeFailure("Write the crash dump");
|
||||
output.Flush(flushToDisk: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMemory.AlignedFree(context);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 176)]
|
||||
internal struct DebugEvent
|
||||
{
|
||||
[FieldOffset(0)] public uint Kind;
|
||||
[FieldOffset(4)] public uint ProcessId;
|
||||
[FieldOffset(8)] public uint ThreadId;
|
||||
[FieldOffset(16)] public ExceptionRecord Exception;
|
||||
[FieldOffset(168)] public uint FirstChance;
|
||||
[FieldOffset(16)] public nint FileHandle;
|
||||
[FieldOffset(16)] public uint ExitCode;
|
||||
[FieldOffset(24)] public nint LoadedLibraryBase;
|
||||
[FieldOffset(16)] public nint UnloadedLibraryBase;
|
||||
[FieldOffset(32)] public nint ThreadStartAddress;
|
||||
[FieldOffset(64)] public nint ProcessThreadStartAddress;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe struct ExceptionRecord
|
||||
{
|
||||
public uint Code;
|
||||
public uint Flags;
|
||||
public nint NestedRecord;
|
||||
public nint Address;
|
||||
public uint ParameterCount;
|
||||
public fixed ulong Parameters[15];
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 1232)]
|
||||
internal struct ThreadContext
|
||||
{
|
||||
[FieldOffset(48)] public uint Flags;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private unsafe struct ExceptionPointers
|
||||
{
|
||||
public ExceptionRecord* Record;
|
||||
public ThreadContext* Context;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal unsafe struct DumpExceptionInformation
|
||||
{
|
||||
public uint ThreadId;
|
||||
public void* Pointers;
|
||||
public int ClientPointers;
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool DebugActiveProcess(uint processId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool DebugActiveProcessStop(uint processId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool DebugSetProcessKillOnExit([MarshalAs(UnmanagedType.Bool)] bool killOnExit);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool WaitForDebugEventEx(out DebugEvent debugEvent, uint milliseconds);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ContinueDebugEvent(uint processId, uint threadId, uint status);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial SafeProcessHandle OpenProcess(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint processId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial SafeWaitHandle OpenThread(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint threadId);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static unsafe partial bool GetThreadContext(SafeWaitHandle thread, ThreadContext* context);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool CloseHandle(nint handle);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true)]
|
||||
private static unsafe partial uint GetFinalPathNameByHandle(nint file, char* path, uint length, uint flags);
|
||||
|
||||
[LibraryImport("dbghelp.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static unsafe partial bool MiniDumpWriteDump(SafeProcessHandle process, uint processId,
|
||||
SafeFileHandle file, uint dumpType, DumpExceptionInformation* exception, nint userStream, nint callback);
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Core.Diagnostics;
|
||||
|
||||
public static partial class WindowsCrashCapture
|
||||
{
|
||||
public const string HelperArgument = "--sharpemu-crash-helper";
|
||||
private const string CaptureVariable = "SHARPEMU_CRASH_CAPTURE";
|
||||
|
||||
public static void StartIfEnabled(string? logFilePath)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable(CaptureVariable), "1", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
Console.Error.WriteLine("[CRASH][WARN] Crash capture requires a Windows x64 process.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var logPath = string.IsNullOrWhiteSpace(logFilePath)
|
||||
? Path.Combine(AppContext.BaseDirectory, "user", "logs", $"run-{DateTime.UtcNow:yyyyMMdd-HHmmss}.log")
|
||||
: Path.GetFullPath(logFilePath);
|
||||
var dumpPath = Path.ChangeExtension(logPath, $"crash-{Environment.ProcessId}.dmp");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dumpPath)!);
|
||||
var readyEventName = $"Local\\SharpEmu-Crash-{Guid.NewGuid():N}";
|
||||
using var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, readyEventName);
|
||||
var executable = Environment.ProcessPath ?? throw new InvalidOperationException("The executable path is not available.");
|
||||
var startInfo = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true };
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(executable), "dotnet", StringComparison.OrdinalIgnoreCase))
|
||||
startInfo.ArgumentList.Add(Assembly.GetEntryAssembly()!.Location);
|
||||
startInfo.ArgumentList.Add(HelperArgument);
|
||||
startInfo.ArgumentList.Add(Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
|
||||
startInfo.ArgumentList.Add(readyEventName);
|
||||
startInfo.ArgumentList.Add(dumpPath);
|
||||
startInfo.Environment.Remove(CaptureVariable);
|
||||
|
||||
// The helper stays attached until this process exits, including runtime teardown.
|
||||
using var helper = Process.Start(startInfo) ?? throw new InvalidOperationException("The crash helper did not start.");
|
||||
using var exited = new ProcessExitWaitHandle(helper);
|
||||
WaitForHelper(readyEvent, exited);
|
||||
Console.Error.WriteLine($"[CRASH][INFO] Crash capture ready. Dump: {dumpPath}");
|
||||
Console.Error.WriteLine($"[CRASH][INFO] Capture report: {dumpPath}.capture.log");
|
||||
Console.Error.WriteLine($"[CRASH][INFO] Runtime: {RuntimeInformation.FrameworkDescription}. Debugging can change timing.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[CRASH][ERROR] Crash capture is not active: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WaitForHelper(WaitHandle ready, WaitHandle exited)
|
||||
{
|
||||
if (WaitHandle.WaitAny([ready, exited]) != 0)
|
||||
throw new InvalidOperationException("The crash helper exited before capture was ready. Check the capture report.");
|
||||
}
|
||||
|
||||
private sealed class ProcessExitWaitHandle : WaitHandle
|
||||
{
|
||||
public ProcessExitWaitHandle(Process process) =>
|
||||
SafeWaitHandle = new SafeWaitHandle(process.Handle, ownsHandle: false);
|
||||
}
|
||||
|
||||
public static int RunHelper(string[] arguments)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
|
||||
arguments.Length != 4 || arguments[0] != HelperArgument ||
|
||||
!uint.TryParse(arguments[1], NumberStyles.None, CultureInfo.InvariantCulture, out var processId) || processId == 0)
|
||||
return 1;
|
||||
|
||||
var attached = false;
|
||||
StreamWriter? report = null;
|
||||
try
|
||||
{
|
||||
report = new StreamWriter(arguments[3] + ".capture.log", append: true) { AutoFlush = true };
|
||||
report.WriteLine($"Capture helper started. Process: {processId}. Dump: {arguments[3]}");
|
||||
report.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription}. OS: {Environment.OSVersion}.");
|
||||
using var readyEvent = EventWaitHandle.OpenExisting(arguments[2]);
|
||||
if (!DebugActiveProcess(processId))
|
||||
throw NativeFailure("Attach to the game process");
|
||||
attached = true;
|
||||
if (!DebugSetProcessKillOnExit(false))
|
||||
throw NativeFailure("Keep the game alive if the helper exits");
|
||||
return ProcessDebugEvents(processId, arguments[3], readyEvent, report);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
report?.WriteLine($"Capture failed: {exception.Message}");
|
||||
Console.Error.WriteLine($"[CRASH][ERROR] Capture failed: {exception.Message}");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (attached)
|
||||
_ = DebugActiveProcessStop(processId);
|
||||
report?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static int ProcessDebugEvents(uint processId, string dumpPath, EventWaitHandle readyEvent, TextWriter report)
|
||||
{
|
||||
var initialBreakpoint = true;
|
||||
var captured = false;
|
||||
var debuggerBreaks = new DebuggerBreakTracker();
|
||||
while (true)
|
||||
{
|
||||
if (!WaitForDebugEventEx(out var debugEvent, uint.MaxValue))
|
||||
throw NativeFailure("Wait for a debug event");
|
||||
|
||||
var continuation = DebugContinue;
|
||||
var ready = false;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
debuggerBreaks.Observe(debugEvent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (debugEvent.Kind is CreateProcessEvent or LoadLibraryEvent && debugEvent.FileHandle != 0)
|
||||
_ = CloseHandle(debugEvent.FileHandle);
|
||||
}
|
||||
if (debugEvent.Kind == ExceptionEvent)
|
||||
{
|
||||
continuation = DebugExceptionNotHandled;
|
||||
if (initialBreakpoint && debugEvent.FirstChance != 0 && debugEvent.Exception.Code == BreakpointException)
|
||||
{
|
||||
initialBreakpoint = false;
|
||||
continuation = DebugContinue;
|
||||
ready = true;
|
||||
}
|
||||
else if (debuggerBreaks.IsDebuggerBreak(debugEvent))
|
||||
{
|
||||
continuation = DebugContinue;
|
||||
report.WriteLine($"Debugger break continued. Thread: {debugEvent.ThreadId}.");
|
||||
}
|
||||
else if (debugEvent.FirstChance == 0 && !captured)
|
||||
{
|
||||
captured = true;
|
||||
report.WriteLine($"Unhandled exception: 0x{debugEvent.Exception.Code:X8}. " +
|
||||
$"Address: 0x{debugEvent.Exception.Address:X16}. Thread: {debugEvent.ThreadId}.");
|
||||
WriteDump(processId, debugEvent, dumpPath);
|
||||
report.WriteLine("Dump complete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Even a failed dump must leave the original exception unhandled.
|
||||
if (!ContinueDebugEvent(debugEvent.ProcessId, debugEvent.ThreadId, continuation))
|
||||
throw NativeFailure("Continue a debug event");
|
||||
}
|
||||
|
||||
if (ready)
|
||||
{
|
||||
report.WriteLine("Capture ready. Application exceptions remain unhandled by the helper.");
|
||||
readyEvent.Set();
|
||||
}
|
||||
if (debugEvent.Kind == ExitProcessEvent)
|
||||
{
|
||||
report.WriteLine($"Process exited: 0x{debugEvent.ExitCode:X8}. Dump captured: {captured}.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Win32Exception NativeFailure(string operation)
|
||||
{
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
return new Win32Exception(error, $"{operation} failed (Windows error {error}).");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Iced.Intel;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
// Use two 128-bit stores to let Rosetta report a memory fault.
|
||||
// Keep the source register, general registers, and flags unchanged.
|
||||
internal static class RosettaVectorStorePatch
|
||||
{
|
||||
internal static bool IsRequired { get; } = IsRosettaProcess();
|
||||
|
||||
private static unsafe bool IsRosettaProcess()
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int translationStatus = 0;
|
||||
nuint valueByteCount = sizeof(int);
|
||||
return ReadSystemControlValue("sysctl.proc_translated", &translationStatus, &valueByteCount, null, 0) == 0 && translationStatus == 1;
|
||||
}
|
||||
|
||||
[DllImport("libSystem.B.dylib", EntryPoint = "sysctlbyname")]
|
||||
private static extern unsafe int ReadSystemControlValue(string controlName, void* valueBuffer, nuint* valueByteCount, void* newValueBuffer, nuint newValueByteCount);
|
||||
|
||||
internal static bool RequiresStoreSplit(in Instruction instruction) =>
|
||||
instruction.Code is Code.VEX_Vmovups_ymmm256_ymm or Code.VEX_Vmovupd_ymmm256_ymm or Code.VEX_Vmovdqu_ymmm256_ymm &&
|
||||
instruction.Op0Kind == OpKind.Memory && instruction.Op1Register is >= Register.YMM0 and <= Register.YMM15;
|
||||
|
||||
internal static IList<Instruction> SplitVectorStores(IList<Instruction> instructions)
|
||||
{
|
||||
var splitInstructions = new List<Instruction>(instructions.Count * 2);
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (!RequiresStoreSplit(instruction))
|
||||
{
|
||||
splitInstructions.Add(instruction);
|
||||
continue;
|
||||
}
|
||||
|
||||
var lowerStore = instruction;
|
||||
lowerStore.Code = Code.VEX_Vmovups_xmmm128_xmm;
|
||||
lowerStore.Op1Register = Register.XMM0 + (instruction.Op1Register - Register.YMM0);
|
||||
splitInstructions.Add(lowerStore);
|
||||
|
||||
var upperStore = instruction;
|
||||
upperStore.Code = Code.VEX_Vextractf128_xmmm128_ymm_imm8;
|
||||
upperStore.Op2Kind = OpKind.Immediate8;
|
||||
upperStore.Immediate8 = 1;
|
||||
upperStore.MemoryDisplacement64 = unchecked(instruction.MemoryDisplacement64 + 16);
|
||||
// Use the address size for the displacement size that Iced requires.
|
||||
upperStore.MemoryDisplSize = instruction.MemoryBase is >= Register.EAX and <= Register.R15D ||
|
||||
instruction.MemoryIndex is >= Register.EAX and <= Register.R15D ? 4 : 8;
|
||||
// Give the added instruction a separate address for the block encoder.
|
||||
// Keep branch targets at the first store.
|
||||
upperStore.IP = instruction.IP + 1;
|
||||
splitInstructions.Add(upperStore);
|
||||
}
|
||||
|
||||
return splitInstructions;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,8 +27,7 @@ public sealed class SelfImage
|
|||
string? version = null,
|
||||
uint tlsModuleId = 0,
|
||||
ulong tlsMemorySize = 0,
|
||||
ulong tlsStaticOffset = 0,
|
||||
IReadOnlyList<string>? importedModuleNames = null)
|
||||
ulong tlsStaticOffset = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||
|
|
@ -51,7 +50,6 @@ public sealed class SelfImage
|
|||
TlsModuleId = tlsModuleId;
|
||||
TlsMemorySize = tlsMemorySize;
|
||||
TlsStaticOffset = tlsStaticOffset;
|
||||
ImportedModuleNames = importedModuleNames ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public bool IsSelf { get; }
|
||||
|
|
@ -90,6 +88,4 @@ public sealed class SelfImage
|
|||
|
||||
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
|
||||
public ulong TlsStaticOffset { get; }
|
||||
|
||||
public IReadOnlyList<string> ImportedModuleNames { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ namespace SharpEmu.Core.Loader;
|
|||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private const uint ElfMagic = 0x7F454C46;
|
||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||
|
|
@ -56,8 +54,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
private const long DtSceStrSize = 0x61000037;
|
||||
private const long DtSceSymTab = 0x61000039;
|
||||
private const long DtSceSymTabSize = 0x6100003F;
|
||||
private const long DtSceNeededModule = 0x6100000F;
|
||||
private const long DtSceNeededModuleNext = 0x61000045;
|
||||
|
||||
private const uint RelocationTypeNone = 0;
|
||||
private const uint RelocationTypeAbsolute64 = 1;
|
||||
|
|
@ -171,7 +167,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
|
||||
if (clearVirtualMemory)
|
||||
{
|
||||
SharpEmu.Libs.Kernel.KernelMemoryCompatExports.ResetBackingMappings(virtualMemory as IGuestBackedSpace);
|
||||
virtualMemory.Clear();
|
||||
_nextTlsModuleId = 1;
|
||||
GuestTlsTemplate.Reset();
|
||||
|
|
@ -202,32 +197,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||
{
|
||||
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
||||
{
|
||||
// Exact allocation failed — the host may have already claimed
|
||||
// part of this range (ASLR, Rosetta 2, or another process).
|
||||
// Try backing the fixed range page by page to claim whatever
|
||||
// free gaps exist. If the whole range is occupied the backfill
|
||||
// returns false and we surface the original failure reason.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Exact allocation at main image base 0x{imageBase:X16} " +
|
||||
$"(size=0x{totalImageSize:X}) failed; attempting fixed-range backfill.");
|
||||
if (!physicalVm.TryBackFixedRange(imageBase, totalImageSize, executable: true))
|
||||
{
|
||||
// TryBackFixedRange may have partially backed pages before
|
||||
// failing. The earlier Clear() already reset all regions, so
|
||||
// this second Clear() is idempotent for everything except the
|
||||
// partial backfill — it frees only those orphaned pages.
|
||||
physicalVm.Clear();
|
||||
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||
throw new InvalidOperationException(
|
||||
$"Could not allocate main image at required base 0x{imageBase:X16} " +
|
||||
$"(size=0x{totalImageSize:X}): {reason}. " +
|
||||
"Try closing other applications, rebooting, or " +
|
||||
(OperatingSystem.IsWindows()
|
||||
? "setting SHARPEMU_DISABLE_MITIGATION_RELAUNCH=1."
|
||||
: "ensuring no other process maps into this address range."));
|
||||
}
|
||||
|
||||
allocatedBase = imageBase;
|
||||
throw new InvalidOperationException(
|
||||
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}).");
|
||||
}
|
||||
|
||||
imageBase = allocatedBase;
|
||||
|
|
@ -262,14 +233,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||
imageBase,
|
||||
_moduleManager,
|
||||
tlsModuleId,
|
||||
out var importedRelocations,
|
||||
out var importedModuleNames);
|
||||
if (tlsModuleId != 0)
|
||||
{
|
||||
// Thread copies must include pointers written by relocation.
|
||||
GuestTlsTemplate.UpdateInitializationImage(
|
||||
tlsModuleId, ReadTlsInitializationImage(processTlsHeader, virtualMemory, imageBase));
|
||||
}
|
||||
out var importedRelocations);
|
||||
var effectiveImportStubs = importStubs.Count == 0
|
||||
? new Dictionary<ulong, string>()
|
||||
: new Dictionary<ulong, string>(importStubs);
|
||||
|
|
@ -300,16 +264,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
out var initializerFunctions);
|
||||
var procParamAddress = ResolveProcParamAddress(programHeaders, imageBase);
|
||||
|
||||
if (virtualMemory is PhysicalVirtualMemory patchableMemory)
|
||||
{
|
||||
_ = GuestRedZonePatcher.Patch(
|
||||
virtualMemory,
|
||||
patchableMemory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
totalImageSize);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[LOADER] ELF e_entry: 0x{elfHeader.EntryPoint:X16}");
|
||||
Console.WriteLine($"[LOADER] Generation: {(isNextGen ? "Gen5 (PS5)" : "Gen4 (PS4)")}");
|
||||
Console.WriteLine($"[LOADER] Using image base: 0x{imageBase:X16}");
|
||||
|
|
@ -351,8 +305,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||
applicationInfo.Version,
|
||||
tlsModuleId,
|
||||
tlsInfo.MemorySize,
|
||||
tlsInfo.StaticOffset,
|
||||
importedModuleNames);
|
||||
tlsInfo.StaticOffset);
|
||||
}
|
||||
|
||||
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
||||
|
|
@ -401,11 +354,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||
}
|
||||
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
||||
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||
{
|
||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||
if (!selfHeader.HasKnownLayout)
|
||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||
{
|
||||
throw new InvalidDataException("SELF header signature is not recognized.");
|
||||
}
|
||||
|
|
@ -428,11 +380,13 @@ public sealed class SelfLoader : ISelfLoader
|
|||
// acceptable here; anything else — most commonly a still-encrypted
|
||||
// retail eboot — must be reported clearly rather than failing later
|
||||
// with an opaque "not a valid ELF header" message.
|
||||
if (magic != ElfMagic)
|
||||
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
|
||||
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (leadingWord != ElfMagicBigEndian)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
|
||||
$"(leading bytes 0x{magic:X8}). This is almost certainly a still-encrypted " +
|
||||
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
|
||||
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
|
||||
$"fake-signed (fSELF) image.");
|
||||
}
|
||||
|
|
@ -560,7 +514,18 @@ public sealed class SelfLoader : ISelfLoader
|
|||
return default;
|
||||
}
|
||||
|
||||
var initImage = ReadTlsInitializationImage(tlsHeader, virtualMemory, imageBase);
|
||||
// tdata (initialized) bytes come from the mapped segment; tbss is the
|
||||
// implicitly-zero remainder up to MemorySize.
|
||||
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
|
||||
var initImage = fileSize > 0 ? new byte[fileSize] : [];
|
||||
if (fileSize > 0 &&
|
||||
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
|
||||
initImage = [];
|
||||
}
|
||||
|
||||
var staticOffset = GuestTlsTemplate.RegisterModule(
|
||||
tlsModuleId,
|
||||
initImage,
|
||||
|
|
@ -574,19 +539,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
|
||||
}
|
||||
|
||||
private static byte[] ReadTlsInitializationImage(
|
||||
ProgramHeader header, IVirtualMemory virtualMemory, ulong imageBase)
|
||||
{
|
||||
if (header.FileSize > header.MemorySize || header.FileSize > int.MaxValue)
|
||||
throw new InvalidDataException("PT_TLS initialization size is invalid.");
|
||||
|
||||
var initializationImage = new byte[(int)header.FileSize];
|
||||
var address = checked(imageBase + header.VirtualAddress);
|
||||
if (initializationImage.Length != 0 && !virtualMemory.TryRead(address, initializationImage))
|
||||
throw new InvalidDataException($"Cannot read TLS initialization bytes at 0x{address:X}.");
|
||||
return initializationImage;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
||||
ReadOnlySpan<byte> imageData,
|
||||
LoadContext loadContext,
|
||||
|
|
@ -596,11 +548,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||
ulong imageBase,
|
||||
IModuleManager? moduleManager,
|
||||
uint tlsModuleId,
|
||||
out IReadOnlyList<ImportedSymbolRelocation> importedRelocations,
|
||||
out IReadOnlyList<string> importedModuleNames)
|
||||
out IReadOnlyList<ImportedSymbolRelocation> importedRelocations)
|
||||
{
|
||||
importedRelocations = Array.Empty<ImportedSymbolRelocation>();
|
||||
importedModuleNames = Array.Empty<string>();
|
||||
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex))
|
||||
{
|
||||
return EmptyImportStubs;
|
||||
|
|
@ -691,21 +641,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||
|
||||
byte[] stringTable = Array.Empty<byte>();
|
||||
byte[] symbolTable = Array.Empty<byte>();
|
||||
if (dynamicInfo.StrTabOffset != 0 && dynamicInfo.StrTabSize != 0 &&
|
||||
!TryLoadTableBytes(
|
||||
elfData,
|
||||
virtualMemory,
|
||||
imageBase,
|
||||
dynamicInfo.StrTabOffset,
|
||||
dynamicInfo.StrTabSize,
|
||||
out stringTable))
|
||||
{
|
||||
return EmptyImportStubs;
|
||||
}
|
||||
|
||||
if (maxSymbolIndex != 0)
|
||||
{
|
||||
if (stringTable.Length == 0)
|
||||
if (!TryLoadTableBytes(
|
||||
elfData,
|
||||
virtualMemory,
|
||||
imageBase,
|
||||
dynamicInfo.StrTabOffset,
|
||||
dynamicInfo.StrTabSize,
|
||||
out stringTable))
|
||||
{
|
||||
return EmptyImportStubs;
|
||||
}
|
||||
|
|
@ -725,8 +669,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
}
|
||||
}
|
||||
|
||||
importedModuleNames = ReadImportedModuleNames(dynamicTable, stringTable);
|
||||
|
||||
var descriptors = new List<RelocationDescriptor>(256);
|
||||
var orderedImportNids = new List<string>(128);
|
||||
var seenImportNids = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
|
@ -770,9 +712,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||
|
||||
importedRelocations = BuildImportedRelocations(descriptors);
|
||||
|
||||
var stubEligibleNids = CollectStubEligibleNids(descriptors, moduleManager);
|
||||
var stubImportNids = orderedImportNids
|
||||
.Where(stubEligibleNids.Contains)
|
||||
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||
.ToArray();
|
||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||
|
|
@ -1217,35 +1158,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
isWeak);
|
||||
}
|
||||
|
||||
// Collects every NID that needs a trap import stub in a single pass over the
|
||||
// descriptors. This mirrors ShouldCreateImportStub applied per NID, but avoids
|
||||
// the O(nids * descriptors) rescan that filtering each unique NID against the
|
||||
// full descriptor list would incur on large modules. A NID qualifies as soon as
|
||||
// one of its descriptors is non-weak, or is weak but resolvable via the module
|
||||
// manager.
|
||||
private static HashSet<string> CollectStubEligibleNids(
|
||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||
IModuleManager? moduleManager)
|
||||
{
|
||||
var eligible = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < descriptors.Count; i++)
|
||||
{
|
||||
var descriptor = descriptors[i];
|
||||
var nid = descriptor.ImportNid;
|
||||
if (nid is null || eligible.Contains(nid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
|
||||
{
|
||||
eligible.Add(nid);
|
||||
}
|
||||
}
|
||||
|
||||
return eligible;
|
||||
}
|
||||
|
||||
private static bool ShouldCreateImportStub(
|
||||
string nid,
|
||||
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||
|
|
@ -2018,39 +1930,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
preInitArraySize);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ReadImportedModuleNames(
|
||||
ReadOnlySpan<byte> dynamicTable,
|
||||
ReadOnlySpan<byte> stringTable)
|
||||
{
|
||||
if (stringTable.IsEmpty)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var names = new List<string>();
|
||||
for (var offset = 0; offset + DynamicEntrySize <= dynamicTable.Length; offset += DynamicEntrySize)
|
||||
{
|
||||
var tag = BinaryPrimitives.ReadInt64LittleEndian(dynamicTable.Slice(offset, sizeof(long)));
|
||||
var value = BinaryPrimitives.ReadUInt64LittleEndian(dynamicTable.Slice(offset + sizeof(long), sizeof(ulong)));
|
||||
if (tag == DtNull)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag is not (DtSceNeededModule or DtSceNeededModuleNext) ||
|
||||
!TryReadNullTerminatedAscii(stringTable, (uint)value, out var name) ||
|
||||
string.IsNullOrWhiteSpace(name) ||
|
||||
names.Contains(name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
names.Add(name);
|
||||
}
|
||||
|
||||
return names.Count == 0 ? Array.Empty<string>() : names;
|
||||
}
|
||||
|
||||
private static bool IsSupportedRelocationType(uint relocationType)
|
||||
{
|
||||
return relocationType is
|
||||
|
|
@ -2550,19 +2429,6 @@ public sealed class SelfLoader : ISelfLoader
|
|||
Debug.Assert(
|
||||
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||
"An unresolved weak symbol incorrectly received a trap import stub.");
|
||||
|
||||
var strong = new RelocationDescriptor(
|
||||
TargetAddress: 0x3000,
|
||||
Addend: 0,
|
||||
ImportNid: "strong",
|
||||
SymbolValue: 0,
|
||||
RelocationValueKind.Pointer,
|
||||
IsDataImport: false);
|
||||
var mixed = new List<RelocationDescriptor> { weak, strong };
|
||||
var eligible = CollectStubEligibleNids(mixed, moduleManager: null);
|
||||
Debug.Assert(
|
||||
eligible.Contains("strong") && !eligible.Contains("weak"),
|
||||
"CollectStubEligibleNids disagreed with the per-NID stub eligibility rule.");
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
|
|
@ -2922,27 +2788,28 @@ public sealed class SelfLoader : ISelfLoader
|
|||
private readonly ushort _size2;
|
||||
private readonly ulong _fileSize;
|
||||
private readonly ushort _segmentCount;
|
||||
private readonly ushort _flags;
|
||||
private readonly ushort _unknown;
|
||||
private readonly uint _padding;
|
||||
|
||||
public ushort SegmentCount => _segmentCount;
|
||||
|
||||
public ushort Unknown => _unknown;
|
||||
|
||||
public ulong FileSize => _fileSize;
|
||||
|
||||
// Version, key type, and flags are signing metadata and vary across
|
||||
// valid SELF images. They do not change the fixed header layout.
|
||||
public bool HasKnownLayout =>
|
||||
((_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D) ||
|
||||
(_ident0 == 0x54 &&
|
||||
_ident1 == 0x14 &&
|
||||
_ident2 == 0xF5 &&
|
||||
_ident3 == 0xEE)) &&
|
||||
_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D &&
|
||||
_ident4 == 0x00 &&
|
||||
_ident5 == 0x01 &&
|
||||
_ident6 == 0x01 &&
|
||||
_ident7 == 0x12;
|
||||
_ident7 == 0x12 &&
|
||||
_ident8 == 0x01 &&
|
||||
_ident9 == 0x01 &&
|
||||
_ident10 == 0x00 &&
|
||||
_ident11 == 0x00;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
|
|
|
|||
|
|
@ -1,405 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using Iced.Intel;
|
||||
using static Iced.Intel.AssemblerRegisters;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
// Rewrite SHA extension instructions into general-register code when the host CPU lacks them.
|
||||
// Rosetta 2 and some x86 hosts raise #UD for them, and a signal per instruction is far too slow
|
||||
// for titles that hash whole files. The expansion keeps every register except the destination
|
||||
// XMM register and keeps the flags. It runs inside the trampoline, after the red zone is skipped.
|
||||
internal static class ShaInstructionRewrite
|
||||
{
|
||||
// pushfq plus the saved general registers.
|
||||
private const int SavedBytes = 8 * 13;
|
||||
|
||||
// Scratch layout: source 1 (and result), source 2, XMM0 (SHA256RNDS2 round inputs).
|
||||
private const int Source1 = 0;
|
||||
private const int Source2 = 16;
|
||||
private const int Xmm0Copy = 32;
|
||||
private const int ScratchBytes = 48;
|
||||
|
||||
private static readonly Register[] SavedRegisters =
|
||||
{
|
||||
Register.RAX, Register.RCX, Register.RDX, Register.RBX, Register.RSI, Register.RDI,
|
||||
Register.R8, Register.R9, Register.R10, Register.R11, Register.R12, Register.R13,
|
||||
};
|
||||
|
||||
internal static bool IsRequired { get; } = HostLacksSha() &&
|
||||
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_SHA_REWRITE"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Bytes the expansion of one instruction needs at most in the trampoline.
|
||||
internal const int MaximumExpansionBytes = 1024;
|
||||
|
||||
private static bool HostLacksSha()
|
||||
{
|
||||
if (!X86Base.IsSupported)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (_, ebx, _, _) = X86Base.CpuId(7, 0);
|
||||
return (ebx & (1 << 29)) == 0;
|
||||
}
|
||||
|
||||
internal static bool IsShaInstruction(in Instruction instruction) => instruction.Mnemonic is
|
||||
Mnemonic.Sha1rnds4 or Mnemonic.Sha1nexte or Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
|
||||
Mnemonic.Sha256rnds2 or Mnemonic.Sha256msg1 or Mnemonic.Sha256msg2;
|
||||
|
||||
// A memory source with a segment prefix or an RIP-relative address cannot be rebuilt here.
|
||||
internal static bool CanRewrite(in Instruction instruction)
|
||||
{
|
||||
if (!IsShaInstruction(instruction) || instruction.Op0Kind != OpKind.Register)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return instruction.Op1Kind == OpKind.Register ||
|
||||
(instruction.Op1Kind == OpKind.Memory &&
|
||||
instruction.SegmentPrefix == Register.None &&
|
||||
!instruction.IsIPRelativeMemoryOperand);
|
||||
}
|
||||
|
||||
internal static IList<Instruction> Expand(IList<Instruction> instructions)
|
||||
{
|
||||
var expanded = new List<Instruction>(instructions.Count);
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (!CanRewrite(instruction))
|
||||
{
|
||||
expanded.Add(instruction);
|
||||
continue;
|
||||
}
|
||||
|
||||
var assembler = new Assembler(64);
|
||||
EmitExpansion(assembler, instruction);
|
||||
var first = true;
|
||||
ulong generatedIp = 0xFFFF_0000_0000_0000UL + ((instruction.IP & 0xFFFF_FFFF) << 12);
|
||||
foreach (var generated in assembler.Instructions)
|
||||
{
|
||||
var copy = generated;
|
||||
// Keep branch targets at the first generated instruction.
|
||||
copy.IP = first ? instruction.IP : generatedIp++;
|
||||
first = false;
|
||||
expanded.Add(copy);
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
internal static void EmitExpansion(Assembler a, in Instruction instruction)
|
||||
{
|
||||
var destination = ToXmm(instruction.Op0Register);
|
||||
a.pushfq();
|
||||
foreach (var register in SavedRegisters)
|
||||
{
|
||||
a.push(ToGpr64(register));
|
||||
}
|
||||
|
||||
a.sub(rsp, ScratchBytes);
|
||||
if (instruction.Op1Kind == OpKind.Memory)
|
||||
{
|
||||
// LEA reads the guest registers before any of them change.
|
||||
a.lea(rax, RebaseMemory(instruction));
|
||||
a.mov(rcx, __qword_ptr[rax]);
|
||||
a.mov(__qword_ptr[rsp + Source2], rcx);
|
||||
a.mov(rcx, __qword_ptr[rax + 8]);
|
||||
a.mov(__qword_ptr[rsp + Source2 + 8], rcx);
|
||||
}
|
||||
else
|
||||
{
|
||||
a.movdqu(__xmmword_ptr[rsp + Source2], ToXmm(instruction.Op1Register));
|
||||
}
|
||||
|
||||
a.movdqu(__xmmword_ptr[rsp + Source1], destination);
|
||||
if (instruction.Mnemonic == Mnemonic.Sha256rnds2)
|
||||
{
|
||||
a.movdqu(__xmmword_ptr[rsp + Xmm0Copy], xmm0);
|
||||
}
|
||||
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Sha1rnds4: EmitSha1Rnds4(a, instruction.Immediate8 & 3); break;
|
||||
case Mnemonic.Sha1nexte: EmitSha1Nexte(a); break;
|
||||
case Mnemonic.Sha1msg1: EmitSha1Msg1(a); break;
|
||||
case Mnemonic.Sha1msg2: EmitSha1Msg2(a); break;
|
||||
case Mnemonic.Sha256rnds2: EmitSha256Rnds2(a); break;
|
||||
case Mnemonic.Sha256msg1: EmitSha256Msg1(a); break;
|
||||
default: EmitSha256Msg2(a); break;
|
||||
}
|
||||
|
||||
a.movdqu(destination, __xmmword_ptr[rsp + Source1]);
|
||||
a.add(rsp, ScratchBytes);
|
||||
for (var index = SavedRegisters.Length - 1; index >= 0; index--)
|
||||
{
|
||||
a.pop(ToGpr64(SavedRegisters[index]));
|
||||
}
|
||||
|
||||
a.popfq();
|
||||
}
|
||||
|
||||
// The guest stack pointer is above the scratch area, the saved registers and the red zone.
|
||||
private static AssemblerMemoryOperand RebaseMemory(in Instruction instruction)
|
||||
{
|
||||
var displacement = unchecked((long)instruction.MemoryDisplacement64);
|
||||
if (instruction.MemoryBase == Register.RSP)
|
||||
{
|
||||
displacement += ScratchBytes + SavedBytes + 128;
|
||||
}
|
||||
|
||||
var baseRegister = instruction.MemoryBase == Register.None ? default : ToGpr64(instruction.MemoryBase);
|
||||
var memory = instruction.MemoryBase == Register.None ? __[displacement] : __[baseRegister + displacement];
|
||||
if (instruction.MemoryIndex != Register.None)
|
||||
{
|
||||
memory = instruction.MemoryBase == Register.None
|
||||
? __[ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement]
|
||||
: __[baseRegister + ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement];
|
||||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static AssemblerMemoryOperand Dword(int slot, int index) => __dword_ptr[rsp + slot + 4 * index];
|
||||
|
||||
private static void EmitSha1Rnds4(Assembler a, int function)
|
||||
{
|
||||
var k = function switch
|
||||
{
|
||||
0 => 0x5A827999,
|
||||
1 => 0x6ED9EBA1,
|
||||
2 => unchecked((int)0x8F1BBCDC),
|
||||
_ => unchecked((int)0xCA62C1D6),
|
||||
};
|
||||
|
||||
a.mov(eax, Dword(Source1, 3));
|
||||
a.mov(ebx, Dword(Source1, 2));
|
||||
a.mov(ecx, Dword(Source1, 1));
|
||||
a.mov(edx, Dword(Source1, 0));
|
||||
a.xor(esi, esi);
|
||||
for (var round = 0; round < 4; round++)
|
||||
{
|
||||
switch (function)
|
||||
{
|
||||
case 0:
|
||||
// (b & c) ^ (~b & d) == d ^ (b & (c ^ d))
|
||||
a.mov(edi, ecx);
|
||||
a.xor(edi, edx);
|
||||
a.and(edi, ebx);
|
||||
a.xor(edi, edx);
|
||||
break;
|
||||
case 2:
|
||||
// (b & c) | (d & (b | c)) is the majority of b, c and d.
|
||||
a.mov(edi, ebx);
|
||||
a.or(edi, ecx);
|
||||
a.and(edi, edx);
|
||||
a.mov(r8d, ebx);
|
||||
a.and(r8d, ecx);
|
||||
a.or(edi, r8d);
|
||||
break;
|
||||
default:
|
||||
a.mov(edi, ebx);
|
||||
a.xor(edi, ecx);
|
||||
a.xor(edi, edx);
|
||||
break;
|
||||
}
|
||||
|
||||
a.add(edi, Dword(Source2, 3 - round));
|
||||
a.add(edi, esi);
|
||||
a.add(edi, k);
|
||||
a.mov(r8d, eax);
|
||||
a.rol(r8d, (byte)5);
|
||||
a.add(edi, r8d);
|
||||
a.mov(esi, edx);
|
||||
a.mov(edx, ecx);
|
||||
a.mov(ecx, ebx);
|
||||
a.rol(ecx, (byte)30);
|
||||
a.mov(ebx, eax);
|
||||
a.mov(eax, edi);
|
||||
}
|
||||
|
||||
a.mov(Dword(Source1, 3), eax);
|
||||
a.mov(Dword(Source1, 2), ebx);
|
||||
a.mov(Dword(Source1, 1), ecx);
|
||||
a.mov(Dword(Source1, 0), edx);
|
||||
}
|
||||
|
||||
private static void EmitSha1Nexte(Assembler a)
|
||||
{
|
||||
a.mov(eax, Dword(Source1, 3));
|
||||
a.rol(eax, (byte)30);
|
||||
a.add(eax, Dword(Source2, 3));
|
||||
a.mov(Dword(Source1, 3), eax);
|
||||
for (var index = 0; index < 3; index++)
|
||||
{
|
||||
a.mov(ecx, Dword(Source2, index));
|
||||
a.mov(Dword(Source1, index), ecx);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitSha1Msg1(Assembler a)
|
||||
{
|
||||
// w0..w3 from source 1 (w0 in dword 3), w4 and w5 from source 2.
|
||||
a.mov(eax, Dword(Source1, 3));
|
||||
a.mov(ebx, Dword(Source1, 2));
|
||||
a.mov(ecx, Dword(Source1, 1));
|
||||
a.mov(edx, Dword(Source1, 0));
|
||||
a.mov(esi, Dword(Source2, 3));
|
||||
a.mov(edi, Dword(Source2, 2));
|
||||
a.xor(eax, ecx);
|
||||
a.xor(ebx, edx);
|
||||
a.xor(ecx, esi);
|
||||
a.xor(edx, edi);
|
||||
a.mov(Dword(Source1, 3), eax);
|
||||
a.mov(Dword(Source1, 2), ebx);
|
||||
a.mov(Dword(Source1, 1), ecx);
|
||||
a.mov(Dword(Source1, 0), edx);
|
||||
}
|
||||
|
||||
private static void EmitSha1Msg2(Assembler a)
|
||||
{
|
||||
a.mov(eax, Dword(Source1, 3));
|
||||
a.xor(eax, Dword(Source2, 2));
|
||||
a.rol(eax, (byte)1);
|
||||
a.mov(ebx, Dword(Source1, 2));
|
||||
a.xor(ebx, Dword(Source2, 1));
|
||||
a.rol(ebx, (byte)1);
|
||||
a.mov(ecx, Dword(Source1, 1));
|
||||
a.xor(ecx, Dword(Source2, 0));
|
||||
a.rol(ecx, (byte)1);
|
||||
a.mov(edx, Dword(Source1, 0));
|
||||
a.xor(edx, eax);
|
||||
a.rol(edx, (byte)1);
|
||||
a.mov(Dword(Source1, 3), eax);
|
||||
a.mov(Dword(Source1, 2), ebx);
|
||||
a.mov(Dword(Source1, 1), ecx);
|
||||
a.mov(Dword(Source1, 0), edx);
|
||||
}
|
||||
|
||||
private static void EmitSha256Rnds2(Assembler a)
|
||||
{
|
||||
// a..h in eax, ebx, ecx, edx, esi, edi, r8d, r9d.
|
||||
a.mov(eax, Dword(Source2, 3));
|
||||
a.mov(ebx, Dword(Source2, 2));
|
||||
a.mov(ecx, Dword(Source1, 3));
|
||||
a.mov(edx, Dword(Source1, 2));
|
||||
a.mov(esi, Dword(Source2, 1));
|
||||
a.mov(edi, Dword(Source2, 0));
|
||||
a.mov(r8d, Dword(Source1, 1));
|
||||
a.mov(r9d, Dword(Source1, 0));
|
||||
for (var round = 0; round < 2; round++)
|
||||
{
|
||||
// r10d = h + Sigma1(e) + Ch(e, f, g) + wk
|
||||
EmitRotateXor(a, r10d, esi, r11d, 6, 11, 25);
|
||||
a.add(r10d, r9d);
|
||||
a.add(r10d, Dword(Xmm0Copy, round));
|
||||
a.mov(r11d, edi);
|
||||
a.xor(r11d, r8d);
|
||||
a.and(r11d, esi);
|
||||
a.xor(r11d, r8d);
|
||||
a.add(r10d, r11d);
|
||||
|
||||
// r11d = Sigma0(a) + Maj(a, b, c)
|
||||
EmitRotateXor(a, r11d, eax, r12d, 2, 13, 22);
|
||||
a.mov(r12d, eax);
|
||||
a.or(r12d, ebx);
|
||||
a.and(r12d, ecx);
|
||||
a.mov(r13d, eax);
|
||||
a.and(r13d, ebx);
|
||||
a.or(r12d, r13d);
|
||||
a.add(r11d, r12d);
|
||||
|
||||
a.mov(r9d, r8d);
|
||||
a.mov(r8d, edi);
|
||||
a.mov(edi, esi);
|
||||
a.mov(esi, edx);
|
||||
a.add(esi, r10d);
|
||||
a.mov(edx, ecx);
|
||||
a.mov(ecx, ebx);
|
||||
a.mov(ebx, eax);
|
||||
a.mov(eax, r10d);
|
||||
a.add(eax, r11d);
|
||||
}
|
||||
|
||||
a.mov(Dword(Source1, 3), eax);
|
||||
a.mov(Dword(Source1, 2), ebx);
|
||||
a.mov(Dword(Source1, 1), esi);
|
||||
a.mov(Dword(Source1, 0), edi);
|
||||
}
|
||||
|
||||
private static void EmitSha256Msg1(Assembler a)
|
||||
{
|
||||
// w0..w3 in edx, ecx, ebx, eax; w4 in esi. Results go to r9d..r12d.
|
||||
a.mov(eax, Dword(Source1, 3));
|
||||
a.mov(ebx, Dword(Source1, 2));
|
||||
a.mov(ecx, Dword(Source1, 1));
|
||||
a.mov(edx, Dword(Source1, 0));
|
||||
a.mov(esi, Dword(Source2, 0));
|
||||
EmitSmallSigma(a, r9d, esi, r8d, 7, 18, 3);
|
||||
a.add(r9d, eax);
|
||||
EmitSmallSigma(a, r10d, eax, r8d, 7, 18, 3);
|
||||
a.add(r10d, ebx);
|
||||
EmitSmallSigma(a, r11d, ebx, r8d, 7, 18, 3);
|
||||
a.add(r11d, ecx);
|
||||
EmitSmallSigma(a, r12d, ecx, r8d, 7, 18, 3);
|
||||
a.add(r12d, edx);
|
||||
a.mov(Dword(Source1, 3), r9d);
|
||||
a.mov(Dword(Source1, 2), r10d);
|
||||
a.mov(Dword(Source1, 1), r11d);
|
||||
a.mov(Dword(Source1, 0), r12d);
|
||||
}
|
||||
|
||||
private static void EmitSha256Msg2(Assembler a)
|
||||
{
|
||||
// w16 = s1[0] + sigma1(w14); w17 = s1[1] + sigma1(w15); w18 and w19 use w16 and w17.
|
||||
a.mov(esi, Dword(Source2, 2));
|
||||
EmitSmallSigma(a, eax, esi, r8d, 17, 19, 10);
|
||||
a.add(eax, Dword(Source1, 0));
|
||||
a.mov(esi, Dword(Source2, 3));
|
||||
EmitSmallSigma(a, ebx, esi, r8d, 17, 19, 10);
|
||||
a.add(ebx, Dword(Source1, 1));
|
||||
EmitSmallSigma(a, ecx, eax, r8d, 17, 19, 10);
|
||||
a.add(ecx, Dword(Source1, 2));
|
||||
EmitSmallSigma(a, edx, ebx, r8d, 17, 19, 10);
|
||||
a.add(edx, Dword(Source1, 3));
|
||||
a.mov(Dword(Source1, 0), eax);
|
||||
a.mov(Dword(Source1, 1), ebx);
|
||||
a.mov(Dword(Source1, 2), ecx);
|
||||
a.mov(Dword(Source1, 3), edx);
|
||||
}
|
||||
|
||||
// result = ror(x, r1) ^ ror(x, r2) ^ ror(x, r3)
|
||||
private static void EmitRotateXor(
|
||||
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int r3)
|
||||
{
|
||||
a.mov(result, x);
|
||||
a.ror(result, (byte)r1);
|
||||
a.mov(temp, x);
|
||||
a.ror(temp, (byte)r2);
|
||||
a.xor(result, temp);
|
||||
a.mov(temp, x);
|
||||
a.ror(temp, (byte)r3);
|
||||
a.xor(result, temp);
|
||||
}
|
||||
|
||||
// result = ror(x, r1) ^ ror(x, r2) ^ (x >> shift)
|
||||
private static void EmitSmallSigma(
|
||||
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int shift)
|
||||
{
|
||||
a.mov(result, x);
|
||||
a.ror(result, (byte)r1);
|
||||
a.mov(temp, x);
|
||||
a.ror(temp, (byte)r2);
|
||||
a.xor(result, temp);
|
||||
a.mov(temp, x);
|
||||
a.shr(temp, (byte)shift);
|
||||
a.xor(result, temp);
|
||||
}
|
||||
|
||||
private static AssemblerRegisterXMM ToXmm(Register register) => new(register);
|
||||
|
||||
private static AssemblerRegister64 ToGpr64(Register register) => new(register.GetFullRegister());
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ public interface IVirtualMemory : ICpuMemory
|
|||
{
|
||||
void Clear();
|
||||
|
||||
bool IsBackedView(ulong address);
|
||||
|
||||
void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection);
|
||||
|
||||
IReadOnlyList<VirtualMemoryRegion> SnapshotRegions();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
|
|
@ -11,8 +10,6 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||
private readonly object _gate = new();
|
||||
private readonly List<MappedRegion> _regions = new();
|
||||
|
||||
public bool IsBackedView(ulong address) => false;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_gate)
|
||||
|
|
@ -96,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||
}
|
||||
|
||||
CopyToRegions(virtualAddress, source, regionIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GuestWriteWatch.Armed)
|
||||
{
|
||||
GuestWriteWatch.Check(virtualAddress, source);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryValidateRange(
|
||||
|
|
|
|||
|
|
@ -7,16 +7,11 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using SharpEmu.Libs.SystemService;
|
||||
using SharpEmu.Libs.Network;
|
||||
using SharpEmu.Libs.Np;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
|
@ -41,9 +36,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
private readonly ISymbolCatalog _symbolCatalog;
|
||||
private readonly CpuExecutionOptions _cpuExecutionOptions;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly GuestGpuMemory? _gpuMemory;
|
||||
private readonly int _systemLanguage;
|
||||
private readonly object _dynamicModuleGate = new();
|
||||
private bool _disposed;
|
||||
|
||||
public string? LastExecutionDiagnostics { get; private set; }
|
||||
|
|
@ -63,11 +55,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
IModuleManager moduleManager,
|
||||
ISymbolCatalog? symbolCatalog = null,
|
||||
CpuExecutionOptions cpuExecutionOptions = default,
|
||||
IFileSystem? fileSystem = null,
|
||||
GuestGpuMemory? gpuMemory = null,
|
||||
int systemLanguage = 1)
|
||||
IFileSystem? fileSystem = null)
|
||||
{
|
||||
_gpuMemory = gpuMemory;
|
||||
_selfLoader = selfLoader ?? throw new ArgumentNullException(nameof(selfLoader));
|
||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||
_cpuDispatcher = cpuDispatcher ?? throw new ArgumentNullException(nameof(cpuDispatcher));
|
||||
|
|
@ -78,10 +67,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
CpuEngine = cpuExecutionOptions.CpuEngine,
|
||||
StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution,
|
||||
ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit),
|
||||
DebugHook = cpuExecutionOptions.DebugHook,
|
||||
};
|
||||
_fileSystem = fileSystem ?? new PhysicalFileSystem();
|
||||
_systemLanguage = systemLanguage;
|
||||
}
|
||||
|
||||
public static ISharpEmuRuntime CreateDefault(SharpEmuRuntimeOptions options = default)
|
||||
|
|
@ -91,19 +78,14 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
CpuEngine = options.CpuEngine,
|
||||
StrictDynlibResolution = options.StrictDynlibResolution,
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
DebugHook = options.DebugHook,
|
||||
};
|
||||
var virtualMemory = new PhysicalVirtualMemory(
|
||||
viewHost: HostViewMemory.Create(),
|
||||
preReserveGuestAddressSpace: true);
|
||||
var moduleManager = new ModuleManager();
|
||||
// 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();
|
||||
|
||||
var gpuMemory = new GuestGpuMemory(virtualMemory);
|
||||
GuestGpuMemoryHook.Attach(gpuMemory);
|
||||
var virtualMemory = new PhysicalVirtualMemory();
|
||||
|
||||
var fileSystem = new PhysicalFileSystem();
|
||||
|
||||
|
|
@ -114,9 +96,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
moduleManager,
|
||||
Aerolib.Instance,
|
||||
cpuExecutionOptions,
|
||||
fileSystem,
|
||||
gpuMemory,
|
||||
options.SystemLanguage ?? 1);
|
||||
fileSystem);
|
||||
}
|
||||
|
||||
public SelfImage LoadImage(string ebootPath)
|
||||
|
|
@ -157,15 +137,10 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
LastBasicBlockTrace = null;
|
||||
LastMilestoneLog = null;
|
||||
FiberExports.ResetRuntimeState();
|
||||
Http2Exports.ResetRuntimeState();
|
||||
NpAuthExports.ResetRuntimeState();
|
||||
NpUniversalDataSystemExports.ResetRuntimeState();
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
KernelMemoryCompatExports.ConfigureApplicationInfo(image.TitleId);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
SystemServiceExports.ConfigureApplicationInfo(image.TitleId, _systemLanguage);
|
||||
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
|
|
@ -180,16 +155,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
HleDataSymbols.ConfigureProcessImageName(processImageName);
|
||||
MergeKnownHleDataSymbols(activeRuntimeSymbols);
|
||||
var loadedModuleImages = LoadAdjacentSceModules(ebootPath, image, activeImportStubs, activeRuntimeSymbols);
|
||||
var loadedModuleImages = LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols);
|
||||
RebindImportedDataSymbols(image, loadedModuleImages, activeRuntimeSymbols);
|
||||
var app0Root = Path.GetDirectoryName(normalizedEbootPath) ?? string.Empty;
|
||||
KernelModuleRegistry.ConfigureModuleLoader(modulePath => LoadRequestedAppModule(
|
||||
modulePath,
|
||||
app0Root,
|
||||
image,
|
||||
loadedModuleImages,
|
||||
activeImportStubs,
|
||||
activeRuntimeSymbols));
|
||||
var initializerResult = RunAllInitializers(
|
||||
image,
|
||||
loadedModuleImages,
|
||||
|
|
@ -220,16 +187,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
|
||||
// Stop is a host operation, not an emulation failure. The detailed
|
||||
// trace and session-summary builders can traverse a partially torn
|
||||
// down native backend, delaying the GUI exit callback indefinitely.
|
||||
if (HostSessionControl.IsShutdownRequested)
|
||||
{
|
||||
Console.Error.WriteLine("[RUNTIME] Skipping post-exit diagnostics for host shutdown.");
|
||||
return result;
|
||||
}
|
||||
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
|
|
@ -429,9 +386,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
}
|
||||
|
||||
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
||||
// Overlap the cooked-id APR walk with module load so the first ReadFile
|
||||
// miss mid-boot does not stall on a cold USB index.
|
||||
SharpEmu.Libs.Ampr.AmprFileRegistry.BeginApp0IndexPreload(app0Root);
|
||||
return new App0BindingScope(app0VariableName);
|
||||
}
|
||||
|
||||
|
|
@ -655,7 +609,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
private List<LoadedModuleImage> LoadAdjacentSceModules(
|
||||
string ebootPath,
|
||||
SelfImage mainImage,
|
||||
IDictionary<ulong, string> importStubs,
|
||||
IDictionary<string, ulong> runtimeSymbols)
|
||||
{
|
||||
|
|
@ -668,20 +621,16 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
var moduleDirectories = new[]
|
||||
{
|
||||
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
// A linked PRX can be beside eboot.bin. Load only the app-root modules
|
||||
// named by the main image at boot, after the normal module directories
|
||||
// they may depend on. Map the other app-root modules so
|
||||
// sceKernelLoadStartModule can start them later.
|
||||
(Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
(Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
|
||||
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
|
||||
// them up front so the HLE loader can return a real module handle and dlsym
|
||||
// can resolve their exports, but defer DT_INIT until the guest requests them.
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
|
||||
}
|
||||
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.Where(entry => Directory.Exists(entry.Path))
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -692,9 +641,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory.Path, "*", directory.SearchOption)
|
||||
.EnumerateFiles(directory.Path)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(path => !directory.LinkedOnly || IsMainImageLinkedModule(path, mainImage.ImportedModuleNames))
|
||||
.Select(path => (Path: path, directory.StartAtBoot)))
|
||||
.Where(entry =>
|
||||
{
|
||||
|
|
@ -781,176 +729,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
return loadedImages;
|
||||
}
|
||||
|
||||
private KernelModuleRegistry.ModuleLoadResult LoadRequestedAppModule(
|
||||
string guestModulePath,
|
||||
string app0Root,
|
||||
SelfImage mainImage,
|
||||
List<LoadedModuleImage> loadedModuleImages,
|
||||
Dictionary<ulong, string> activeImportStubs,
|
||||
Dictionary<string, ulong> activeRuntimeSymbols)
|
||||
{
|
||||
lock (_dynamicModuleGate)
|
||||
{
|
||||
if (!TryResolveApp0ModulePath(app0Root, guestModulePath, out var modulePath))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Runtime module path rejected: '{guestModulePath}'");
|
||||
return KernelModuleRegistry.ModuleLoadResult.Failure(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (KernelModuleRegistry.TryFindByExactPath(modulePath, out var existingModule))
|
||||
{
|
||||
return KernelModuleRegistry.ModuleLoadResult.Success(existingModule.Handle);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(modulePath);
|
||||
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue)
|
||||
{
|
||||
return KernelModuleRegistry.ModuleLoadResult.Failure(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
var moduleBytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
|
||||
using (var stream = File.OpenRead(modulePath))
|
||||
{
|
||||
stream.ReadExactly(moduleBytes);
|
||||
}
|
||||
|
||||
var moduleImage = _selfLoader.LoadAdditional(
|
||||
moduleBytes.AsSpan(),
|
||||
_virtualMemory,
|
||||
_moduleManager,
|
||||
_fileSystem,
|
||||
Path.GetDirectoryName(modulePath));
|
||||
var candidateImportStubs = new Dictionary<ulong, string>(activeImportStubs);
|
||||
var candidateRuntimeSymbols = new Dictionary<string, ulong>(activeRuntimeSymbols, StringComparer.Ordinal);
|
||||
_ = MergeImportStubs(candidateImportStubs, moduleImage.ImportStubs, modulePath);
|
||||
_ = MergeRuntimeSymbols(candidateRuntimeSymbols, moduleImage.RuntimeSymbols);
|
||||
InstallNativePluginCompatibilityHooks(candidateImportStubs, moduleImage, modulePath);
|
||||
|
||||
string? installError = null;
|
||||
if (_cpuDispatcher is not CpuDispatcher dispatcher ||
|
||||
!dispatcher.TryInstallAdditionalModule(
|
||||
candidateImportStubs,
|
||||
candidateRuntimeSymbols,
|
||||
out installError))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Runtime module import installation failed: {modulePath} " +
|
||||
$"({installError ?? "unsupported CPU backend"})");
|
||||
return KernelModuleRegistry.ModuleLoadResult.Failure(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
ReplaceDictionary(activeImportStubs, candidateImportStubs);
|
||||
ReplaceDictionary(activeRuntimeSymbols, candidateRuntimeSymbols);
|
||||
var handle = RegisterLoadedModule(
|
||||
modulePath,
|
||||
moduleImage,
|
||||
isMain: false,
|
||||
isSystemModule: false);
|
||||
loadedModuleImages.Add(new LoadedModuleImage(modulePath, moduleImage, handle, StartAtBoot: false));
|
||||
RebindImportedDataSymbols(mainImage, loadedModuleImages, activeRuntimeSymbols);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded requested module '{guestModulePath}' as {Path.GetFileName(modulePath)}: " +
|
||||
$"handle={handle}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
return KernelModuleRegistry.ModuleLoadResult.Success(handle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Runtime module load failed: {modulePath} " +
|
||||
$"({ex.GetType().Name}: {ex.Message})");
|
||||
return KernelModuleRegistry.ModuleLoadResult.Failure(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveApp0ModulePath(
|
||||
string app0Root,
|
||||
string guestModulePath,
|
||||
out string modulePath)
|
||||
{
|
||||
modulePath = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(app0Root) || string.IsNullOrWhiteSpace(guestModulePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedGuestPath = guestModulePath.Replace('\\', '/');
|
||||
string relativePath;
|
||||
if (normalizedGuestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativePath = normalizedGuestPath[6..];
|
||||
}
|
||||
else if (normalizedGuestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativePath = normalizedGuestPath[5..];
|
||||
}
|
||||
else if (!normalizedGuestPath.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
relativePath = normalizedGuestPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var root = Path.GetFullPath(app0Root);
|
||||
var candidate = Path.GetFullPath(
|
||||
Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
||||
var rootPrefix = Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar;
|
||||
if (!candidate.StartsWith(rootPrefix, HostPathComparison))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
modulePath = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static StringComparison HostPathComparison => OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
private static void ReplaceDictionary<TKey, TValue>(
|
||||
IDictionary<TKey, TValue> destination,
|
||||
IReadOnlyDictionary<TKey, TValue> source)
|
||||
where TKey : notnull
|
||||
{
|
||||
destination.Clear();
|
||||
foreach (var entry in source)
|
||||
{
|
||||
destination.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMainImageLinkedModule(string modulePath, IReadOnlyList<string> importedModuleNames)
|
||||
{
|
||||
var fileName = Path.GetFileName(modulePath);
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(modulePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName) || string.IsNullOrWhiteSpace(fileNameWithoutExtension))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < importedModuleNames.Count; i++)
|
||||
{
|
||||
var importedName = importedModuleNames[i];
|
||||
if (string.Equals(importedName, fileName, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(importedName, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void InstallNativePluginCompatibilityHooks(
|
||||
IDictionary<ulong, string> importStubs,
|
||||
SelfImage moduleImage,
|
||||
|
|
@ -1009,7 +787,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DATA_REBIND"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
Span<byte> relocationValueBytes = stackalloc byte[sizeof(ulong)];
|
||||
for (var i = 0; i < image.ImportedRelocations.Count; i++)
|
||||
{
|
||||
var relocation = image.ImportedRelocations[i];
|
||||
|
|
@ -1036,19 +813,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
{
|
||||
if (logRebind)
|
||||
{
|
||||
var readable = _virtualMemory.TryRead(relocation.TargetAddress, relocationValueBytes);
|
||||
var currentValue = readable ? BinaryPrimitives.ReadUInt64LittleEndian(relocationValueBytes) : 0;
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16} readable={readable} current=0x{currentValue:X16}");
|
||||
foreach (var region in _virtualMemory.SnapshotRegions())
|
||||
{
|
||||
if (relocation.TargetAddress >= region.VirtualAddress &&
|
||||
relocation.TargetAddress - region.VirtualAddress < region.MemorySize)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data target-region: start=0x{region.VirtualAddress:X16} size=0x{region.MemorySize:X} protection={region.Protection}");
|
||||
}
|
||||
}
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
|
|
@ -1378,34 +1144,12 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
}
|
||||
|
||||
_disposed = true;
|
||||
KernelModuleRegistry.ConfigureModuleLoader(null);
|
||||
|
||||
if (_cpuDispatcher is IDisposable disposableDispatcher)
|
||||
{
|
||||
disposableDispatcher.Dispose();
|
||||
}
|
||||
|
||||
if (_cpuDispatcher is CpuDispatcher { NativeSessionLeaked: true })
|
||||
{
|
||||
// A guest worker is still inside guest code; unmapping the guest
|
||||
// address space under it would fault the whole process, which
|
||||
// hosts the GUI launcher in embedded sessions.
|
||||
Console.Error.WriteLine(
|
||||
"[RUNTIME] Guest workers were still active at teardown; keeping the guest address space mapped.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gpuMemory != null)
|
||||
{
|
||||
if (GuestGpuMemoryHook.TryTakeShutdownSummary(out var gpuSummary))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] " + gpuSummary);
|
||||
}
|
||||
GuestGpuMemoryHook.Attach(null);
|
||||
_gpuMemory.Dispose();
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.ResetBackingMappings(_virtualMemory as IGuestBackedSpace);
|
||||
if (_virtualMemory is IDisposable disposableMemory)
|
||||
{
|
||||
disposableMemory.Dispose();
|
||||
|
|
|
|||
|
|
@ -4,22 +4,12 @@
|
|||
namespace SharpEmu.Core.Runtime;
|
||||
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
public readonly struct SharpEmuRuntimeOptions
|
||||
{
|
||||
public CpuExecutionEngine CpuEngine { get; init; }
|
||||
|
||||
public int? SystemLanguage { get; init; }
|
||||
|
||||
public bool StrictDynlibResolution { get; init; }
|
||||
|
||||
public int ImportTraceLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional debugger to attach to guest execution. Flows through to
|
||||
/// <see cref="CpuExecutionOptions.DebugHook"/>. Null (the default) runs with
|
||||
/// no debugger attached.
|
||||
/// </summary>
|
||||
public ICpuDebugHook? DebugHook { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace SharpEmu.DebugClient;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the <c>host:port</c> the client connects to. Mirrors the server's
|
||||
/// defaults (loopback, port 5714) so a bare invocation attaches to a local
|
||||
/// emulator with no arguments.
|
||||
/// </summary>
|
||||
internal static class ClientEndpoint
|
||||
{
|
||||
public const int DefaultPort = 5714;
|
||||
|
||||
public static bool TryParse(string? text, out string host, out int port, out string error)
|
||||
{
|
||||
host = "127.0.0.1";
|
||||
port = DefaultPort;
|
||||
error = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var value = text.Trim();
|
||||
var separator = value.LastIndexOf(':');
|
||||
if (separator >= 0)
|
||||
{
|
||||
var portText = value[(separator + 1)..];
|
||||
if (portText.Length > 0 && (!int.TryParse(portText, out port) || port is <= 0 or > 65535))
|
||||
{
|
||||
error = $"Invalid port '{portText}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value[..separator];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
host = string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase) ? "127.0.0.1" : value;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(host, out _) && !Uri.CheckHostName(host).Equals(UriHostNameType.Dns))
|
||||
{
|
||||
error = $"Invalid host '{host}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.DebugClient;
|
||||
|
||||
/// <summary>
|
||||
/// Turns a friendly REPL line (<c>mem 0x1000 64</c>) into the JSON request the
|
||||
/// server understands. Local-only verbs (help, quit) are reported back to the
|
||||
/// caller instead of producing a request.
|
||||
/// </summary>
|
||||
internal static class CommandTranslator
|
||||
{
|
||||
public enum ActionKind
|
||||
{
|
||||
SendRequest,
|
||||
ShowHelp,
|
||||
Quit,
|
||||
Ignore,
|
||||
Error,
|
||||
}
|
||||
|
||||
public readonly record struct Result(ActionKind Kind, string? Payload = null, string? Error = null);
|
||||
|
||||
public static Result Translate(string line)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
return new Result(ActionKind.Ignore);
|
||||
}
|
||||
|
||||
var parts = trimmed.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
var verb = parts[0].ToLowerInvariant();
|
||||
switch (verb)
|
||||
{
|
||||
case "help" or "?":
|
||||
return new Result(ActionKind.ShowHelp);
|
||||
case "quit" or "exit" or "q":
|
||||
return new Result(ActionKind.Quit);
|
||||
case "raw":
|
||||
var json = trimmed[verb.Length..].Trim();
|
||||
return json.Length == 0
|
||||
? Error("raw requires a JSON object argument.")
|
||||
: Send(json);
|
||||
|
||||
case "ping":
|
||||
return Request("ping");
|
||||
case "status" or "info":
|
||||
return Request("status");
|
||||
case "state":
|
||||
return Request("state");
|
||||
case "regs" or "registers":
|
||||
return Request("registers");
|
||||
case "continue" or "cont" or "c":
|
||||
return Request("continue");
|
||||
case "step" or "s":
|
||||
return Request("step");
|
||||
case "pause" or "p":
|
||||
return Request("pause");
|
||||
case "bp" or "breakpoints" or "bl":
|
||||
return Request("list-breakpoints");
|
||||
|
||||
case "setreg":
|
||||
return parts.Length >= 3
|
||||
? Request("set-register", ("register", parts[1]), ("value", parts[2]))
|
||||
: Error("Usage: setreg <register> <value>");
|
||||
case "mem" or "read":
|
||||
return parts.Length >= 3
|
||||
? Request("read-memory", ("address", parts[1]), ("length", parts[2]))
|
||||
: Error("Usage: mem <address> <length>");
|
||||
case "write":
|
||||
return parts.Length >= 3
|
||||
? Request("write-memory", ("address", parts[1]), ("bytes", parts[2]))
|
||||
: Error("Usage: write <address> <hex-bytes>");
|
||||
case "break" or "b":
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return Error("Usage: break <address> [kind] [length]");
|
||||
}
|
||||
|
||||
var breakArgs = new List<(string, string)> { ("address", parts[1]) };
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
breakArgs.Add(("kind", parts[2]));
|
||||
}
|
||||
|
||||
if (parts.Length >= 4)
|
||||
{
|
||||
breakArgs.Add(("length", parts[3]));
|
||||
}
|
||||
|
||||
return Request("add-breakpoint", breakArgs.ToArray());
|
||||
case "del" or "rm" or "delete":
|
||||
return parts.Length >= 2
|
||||
? Request("remove-breakpoint", ("id", parts[1]))
|
||||
: Error("Usage: del <id>");
|
||||
case "enable":
|
||||
return parts.Length >= 2
|
||||
? Request("enable-breakpoint", ("id", parts[1]), ("enabled", "true"))
|
||||
: Error("Usage: enable <id>");
|
||||
case "disable":
|
||||
return parts.Length >= 2
|
||||
? RequestWithBool("enable-breakpoint", ("id", parts[1]), enabledName: "enabled", enabled: false)
|
||||
: Error("Usage: disable <id>");
|
||||
|
||||
default:
|
||||
return Error($"Unknown command '{verb}'. Type 'help' for the command list.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Result Request(string command, params (string Name, string Value)[] args)
|
||||
{
|
||||
var payload = new Dictionary<string, object?> { ["command"] = command };
|
||||
foreach (var (name, value) in args)
|
||||
{
|
||||
payload[name] = value;
|
||||
}
|
||||
|
||||
return Send(JsonSerializer.Serialize(payload));
|
||||
}
|
||||
|
||||
private static Result RequestWithBool(string command, (string Name, string Value) idArg, string enabledName, bool enabled)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["command"] = command,
|
||||
[idArg.Name] = idArg.Value,
|
||||
[enabledName] = enabled,
|
||||
};
|
||||
return Send(JsonSerializer.Serialize(payload));
|
||||
}
|
||||
|
||||
private static Result Send(string json) => new(ActionKind.SendRequest, json);
|
||||
|
||||
private static Result Error(string message) => new(ActionKind.Error, Error: message);
|
||||
|
||||
public const string HelpText = """
|
||||
SharpEmu debug client commands:
|
||||
status | info Show target state and last stop
|
||||
state Show run state only
|
||||
regs | registers Dump integer registers (paused only)
|
||||
setreg <reg> <value> Set a register (rip/rflags/gp, paused only)
|
||||
mem <addr> <len> Read guest memory as hex (paused only)
|
||||
write <addr> <hex> Write guest memory from hex (paused only)
|
||||
break <addr> [kind] [len] Add a breakpoint (kind: execute/readwatch/writewatch/accesswatch)
|
||||
bp | breakpoints List breakpoints
|
||||
del <id> Remove a breakpoint
|
||||
enable <id> / disable <id> Toggle a breakpoint
|
||||
continue | c Resume the target
|
||||
step | s Resume and stop at the next frame
|
||||
pause Ask a running target to stop
|
||||
ping Round-trip check
|
||||
raw <json> Send a literal JSON request
|
||||
help | ? Show this help
|
||||
quit | exit Disconnect and exit
|
||||
Addresses and values accept decimal or 0x-prefixed hex.
|
||||
""";
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# SharpEmu.DebugClient
|
||||
|
||||
A small, standalone command-line client that connects to the SharpEmu
|
||||
emulator's **live debug server** and drives it interactively. It ships as its
|
||||
own executable (`SharpEmu.DebugClient`) and takes no dependency on the emulator
|
||||
assemblies — it speaks the server's line-delimited JSON protocol directly over
|
||||
TCP, so you can also drive the server from `nc`, a script, or your own tool.
|
||||
|
||||
> **Status:** infrastructure. The transport, protocol, session model, and
|
||||
> breakpoint store are in place. Stops are delivered at **frame boundaries**
|
||||
> (process entry and each module initializer); per-instruction stepping and data
|
||||
> watchpoints are part of the surface and become live as the CPU backend grows
|
||||
> the corresponding hooks. See [`docs/debugger-server.md`](../../docs/debugger-server.md)
|
||||
> for the architecture and protocol reference.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
+-------------------------+ TCP (JSON lines) +----------------------+
|
||||
| SharpEmu (emulator) | <------------------------------> | SharpEmu.DebugClient |
|
||||
| --debug-server | | (this executable) |
|
||||
| | | |
|
||||
| DebuggerServerHost | | REPL / --exec |
|
||||
| +- DebuggerServer | frame boundaries via ICpuDebugHook| |
|
||||
| +- DebuggerSession <-+------ CPU dispatcher ------------ | |
|
||||
+-------------------------+ +----------------------+
|
||||
```
|
||||
|
||||
The emulator is the **server**; this client is a separate process that connects
|
||||
to it and issues commands. The two never share memory — everything crosses the
|
||||
socket as JSON.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
dotnet build src/SharpEmu.DebugClient/SharpEmu.DebugClient.csproj
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Launch the emulator with the debug server enabled. It listens on
|
||||
`127.0.0.1:5714` by default and, with stop-at-entry on, parks the guest at
|
||||
its first frame until you continue:
|
||||
|
||||
```bash
|
||||
SharpEmu --debug-server "/path/to/game/eboot.bin"
|
||||
# or choose an endpoint:
|
||||
SharpEmu --debug-server=127.0.0.1:5714 "/path/to/game/eboot.bin"
|
||||
```
|
||||
|
||||
2. In another terminal, attach the client:
|
||||
|
||||
```bash
|
||||
SharpEmu.DebugClient # defaults to 127.0.0.1:5714
|
||||
SharpEmu.DebugClient 127.0.0.1:5714 # explicit endpoint
|
||||
```
|
||||
|
||||
3. Drive the target:
|
||||
|
||||
```
|
||||
status
|
||||
regs
|
||||
break 0x00000008801234a0
|
||||
continue
|
||||
mem 0x00000008802000000 64
|
||||
```
|
||||
|
||||
## Invocation
|
||||
|
||||
```
|
||||
SharpEmu.DebugClient [host:port] [--exec "<command>"]... [--quiet]
|
||||
```
|
||||
|
||||
| Option | Meaning |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| `host:port` | Server endpoint. Default `127.0.0.1:5714`. `localhost` is fine. |
|
||||
| `--exec, -e` | Run one command non-interactively, then exit. Repeatable. |
|
||||
| `--quiet` | Suppress the connection banner. |
|
||||
| `--help, -h` | Show usage and the command list. |
|
||||
|
||||
Non-interactive example (scriptable):
|
||||
|
||||
```bash
|
||||
SharpEmu.DebugClient --exec "break 0x8801234a0" --exec "continue"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
Addresses and values accept decimal or `0x`-prefixed hex. Register and memory
|
||||
commands only succeed while the target is **paused**.
|
||||
|
||||
| Command | Server verb | Description |
|
||||
| ------- | ----------- | ----------- |
|
||||
| `status` \| `info` | `status` | Target state plus the last stop. |
|
||||
| `state` | `state` | Run state only (`Running`/`Paused`/…). |
|
||||
| `regs` \| `registers` | `registers` | Dump the integer registers. |
|
||||
| `setreg <reg> <value>` | `set-register` | Set `rip`, `rflags`, or a GP register. |
|
||||
| `mem <addr> <len>` \| `read <addr> <len>` | `read-memory` | Read guest memory as hex. |
|
||||
| `write <addr> <hex>` | `write-memory` | Write guest memory from a hex string. |
|
||||
| `break <addr> [kind] [len]` \| `b …` | `add-breakpoint` | Add a breakpoint. `kind`: `execute` (default), `readwatch`, `writewatch`, `accesswatch`. |
|
||||
| `bp` \| `breakpoints` | `list-breakpoints` | List breakpoints. |
|
||||
| `del <id>` \| `rm <id>` | `remove-breakpoint` | Remove a breakpoint. |
|
||||
| `enable <id>` / `disable <id>` | `enable-breakpoint` | Toggle a breakpoint. |
|
||||
| `continue` \| `c` | `continue` | Resume a paused target. |
|
||||
| `step` \| `s` | `step` | Resume and stop at the next frame boundary. |
|
||||
| `pause` | `pause` | Ask a running target to stop at the next boundary. |
|
||||
| `ping` | `ping` | Round-trip liveness check. |
|
||||
| `raw <json>` | *(passthrough)* | Send a literal JSON request. |
|
||||
| `help` \| `?` | — | Show the command list (local). |
|
||||
| `quit` \| `exit` | — | Disconnect and exit (local). |
|
||||
|
||||
## Output
|
||||
|
||||
The client prints two kinds of lines as they arrive:
|
||||
|
||||
- `reply>` — the response to a command you sent (`ok`, plus `data` or `error`).
|
||||
- `event>` — an unsolicited notification: `hello` on connect, `stopped` when the
|
||||
target hits a breakpoint / entry / step / pause, `resumed` on continue, and
|
||||
`terminated` when the run ends.
|
||||
|
||||
Because replies and events share one stream, the client prints everything it
|
||||
receives rather than pairing replies to requests — a `stopped` event may arrive
|
||||
between your command and its reply.
|
||||
|
||||
## Protocol (for building your own client)
|
||||
|
||||
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"command":"read-memory","address":"0x8802000000","length":64}
|
||||
```
|
||||
|
||||
Reply:
|
||||
|
||||
```json
|
||||
{"ok":true,"command":"read-memory","data":{"address":"0x0000000880200000","length":64,"bytes":"48894C24.."}}
|
||||
```
|
||||
|
||||
Event:
|
||||
|
||||
```json
|
||||
{"event":"stopped","reason":"Breakpoint","address":"0x00000008801234A0","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{ ... }}
|
||||
```
|
||||
|
||||
The full verb list and payload fields live in
|
||||
[`docs/debugger-server.md`](../../docs/debugger-server.md).
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.DebugClient;
|
||||
|
||||
/// <summary>
|
||||
/// A thin TCP wrapper around the server's line-delimited JSON protocol: it
|
||||
/// writes request lines and runs a background loop that prints incoming
|
||||
/// responses and events as they arrive. Because the stream interleaves replies
|
||||
/// with asynchronous stop/resume events, a single reader printing everything is
|
||||
/// simpler and more robust than correlating request/response pairs.
|
||||
/// </summary>
|
||||
internal sealed class DebugClientConnection : IAsyncDisposable
|
||||
{
|
||||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||
private static readonly JsonSerializerOptions PrettyOptions = new() { WriteIndented = true };
|
||||
|
||||
private readonly TcpClient _client;
|
||||
private readonly StreamReader _reader;
|
||||
private readonly StreamWriter _writer;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
private DebugClientConnection(TcpClient client, NetworkStream stream)
|
||||
{
|
||||
_client = client;
|
||||
_reader = new StreamReader(stream, Utf8NoBom);
|
||||
_writer = new StreamWriter(stream, Utf8NoBom) { AutoFlush = false };
|
||||
}
|
||||
|
||||
public static async Task<DebugClientConnection> ConnectAsync(string host, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = new TcpClient();
|
||||
await client.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false);
|
||||
return new DebugClientConnection(client, client.GetStream());
|
||||
}
|
||||
|
||||
/// <summary>Continuously prints incoming lines until the stream closes.</summary>
|
||||
public async Task ReceiveLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = await _reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("[connection closed by server]");
|
||||
return;
|
||||
}
|
||||
|
||||
Print(line);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("[connection lost]");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(string json, CancellationToken cancellationToken)
|
||||
{
|
||||
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _writer.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
|
||||
await _writer.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Print(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(line);
|
||||
var root = document.RootElement;
|
||||
var isEvent = root.TryGetProperty("event", out _);
|
||||
var prefix = isEvent ? "event>" : "reply>";
|
||||
var pretty = JsonSerializer.Serialize(root, PrettyOptions);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{prefix}\n{pretty}");
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _writer.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
|
||||
_writeLock.Dispose();
|
||||
_reader.Dispose();
|
||||
await _writer.DisposeAsync().ConfigureAwait(false);
|
||||
_client.Dispose();
|
||||
}
|
||||
}
|
||||