mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-20 13:36:47 -04:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfe32f61e1 | |||
| e3f5e3a3ec | |||
| 0321842569 | |||
| 35b552782f | |||
| 2f30417d61 | |||
| 46eb6b46f5 | |||
| 4f166e1403 | |||
| 6b4de9fbdb | |||
| 16bfe7b095 | |||
| efedf42035 | |||
| 3ff14738bd | |||
| 8676d1ab14 | |||
| c255df5136 | |||
| 2ee48fc880 | |||
| d7b64361e7 | |||
| 5ee158918f | |||
| bd12087cc3 | |||
| d5296779ec | |||
| a93eaf582f | |||
| ed6fea0efb | |||
| 95a49f3c89 | |||
| 60197f2e75 | |||
| b87186c96b | |||
| 0b12fc864a | |||
| 90589131b5 | |||
| bd219e38a3 | |||
| 196b48528e | |||
| 54c608ef9c | |||
| f3df63b529 | |||
| dd6f596b45 | |||
| 6fa24737d3 | |||
| 279ce8328f | |||
| 9d182f8316 |
@@ -46,3 +46,69 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
||||
|
||||
platform_tests:
|
||||
name: Host platform (${{ matrix.runner }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-24.04, macos-14, macos-15-intel]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure without graphics dependencies or game assets
|
||||
run: >-
|
||||
cmake -S runtime -B build-platform -G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
|
||||
|
||||
- name: Build and run host-platform tests
|
||||
run: |
|
||||
cmake --build build-platform --parallel 3
|
||||
ctest --test-dir build-platform --output-on-failure
|
||||
|
||||
macos_substrate:
|
||||
name: macOS arm64 (configure + substrate tests)
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Test macOS app dependency packaging
|
||||
shell: bash
|
||||
run: bash Launcher/macos/test-publish-app.command
|
||||
|
||||
- name: Configure native runtime
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = arm64
|
||||
cmake -S runtime -B build-macos -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
|
||||
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
|
||||
build-macos/CMakeCache.txt
|
||||
|
||||
- name: Build macOS portability targets
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build build-macos --target \
|
||||
mkw_platform_paths_tests \
|
||||
mkw_runtime_config_tests \
|
||||
mkw_nand_save_tests \
|
||||
mkw_nand_settings_tests \
|
||||
mkw_sc_serial_tests \
|
||||
mkw_input_expr_tests \
|
||||
mkw_bluetooth_controller_tests \
|
||||
mkw_macos_native_compile \
|
||||
mkw_macos_context_abi_tests \
|
||||
mkw_macos_host_context_tests \
|
||||
mkw_macos_guest_flat_memory_tests \
|
||||
mkw_macos_external_audio_tests
|
||||
|
||||
- name: Test execution substrate
|
||||
shell: bash
|
||||
run: ctest --test-dir build-macos --output-on-failure
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Package installers
|
||||
|
||||
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
|
||||
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
|
||||
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
|
||||
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
|
||||
# the same scripts a maintainer runs by hand today to
|
||||
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
|
||||
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
|
||||
# which is proprietary and not present in this repository or in CI.
|
||||
@@ -11,6 +11,11 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Package version
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -92,14 +97,140 @@ jobs:
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
macos-setup-package:
|
||||
name: macOS (universal Setup.pkg)
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Verify Apple Silicon runner
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = arm64
|
||||
xcode-select -p
|
||||
|
||||
- name: Download pinned Nod tools
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p Launcher/artifacts/macos
|
||||
nodtool_version=v2.0.0-alpha.10
|
||||
nodtool_arm64_asset=nodtool-macos-arm64
|
||||
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
|
||||
nodtool_x86_64_asset=nodtool-macos-x86_64
|
||||
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
|
||||
curl -fsSL --retry 3 \
|
||||
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
|
||||
-o Launcher/artifacts/macos/nodtool-arm64
|
||||
curl -fsSL --retry 3 \
|
||||
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
|
||||
-o Launcher/artifacts/macos/nodtool-x86_64
|
||||
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
|
||||
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
|
||||
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
|
||||
|
||||
- name: Publish self-contained Translator tools
|
||||
shell: bash
|
||||
run: |
|
||||
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
|
||||
-c Release -r osx-arm64 --self-contained true \
|
||||
-p:PublishSingleFile=true \
|
||||
-o Launcher/artifacts/macos/translator-arm64
|
||||
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
|
||||
-c Release -r osx-x64 --self-contained true \
|
||||
-p:PublishSingleFile=true \
|
||||
-o Launcher/artifacts/macos/translator-x86_64
|
||||
|
||||
- name: Download pinned universal Ninja
|
||||
shell: bash
|
||||
run: |
|
||||
ninja_version=1.13.2
|
||||
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
|
||||
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
|
||||
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
|
||||
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
|
||||
chmod +x Launcher/artifacts/macos/ninja/ninja
|
||||
|
||||
- name: Download pinned portable CMake
|
||||
shell: bash
|
||||
run: |
|
||||
cmake_version=4.4.3
|
||||
archive="cmake-${cmake_version}-macos-universal.tar.gz"
|
||||
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
|
||||
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
|
||||
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
|
||||
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
|
||||
tar -xzf "$archive"
|
||||
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
|
||||
|
||||
- name: Build Setup.pkg
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ inputs.version }}
|
||||
TAG_VERSION: ${{ github.ref_name }}
|
||||
shell: bash
|
||||
run: |
|
||||
package_version="$PACKAGE_VERSION"
|
||||
if [[ -z "$package_version" ]]; then
|
||||
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
|
||||
package_version="${TAG_VERSION#v}"
|
||||
elif [[ -f "Launcher/Directory.Build.props" ]]; then
|
||||
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
|
||||
fi
|
||||
fi
|
||||
mkdir -p Launcher/dist
|
||||
Launcher/macos/build-setup-pkg.command \
|
||||
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
|
||||
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
|
||||
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
|
||||
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
|
||||
--cmake-root Launcher/artifacts/macos/cmake \
|
||||
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
|
||||
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
|
||||
--output Launcher/dist/WiiCompiled-Setup.pkg \
|
||||
${package_version:+--version "$package_version"}
|
||||
|
||||
- name: Verify package layout and architecture-specific tools
|
||||
shell: bash
|
||||
run: |
|
||||
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg || true
|
||||
if pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
|
||||
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'; then
|
||||
echo "::error::Setup.pkg contains a forbidden payload"
|
||||
exit 1
|
||||
fi
|
||||
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
|
||||
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
|
||||
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
|
||||
for arch in arm64 x86_64; do
|
||||
for tool in nodtool Translator.Cli ninja; do
|
||||
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
|
||||
done
|
||||
done
|
||||
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
|
||||
bash "$resources/setup.command" --help
|
||||
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-macos-universal
|
||||
path: Launcher/dist/WiiCompiled-Setup.pkg
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
|
||||
# discovers updates from these releases, so the contract it relies on is enforced here: a full
|
||||
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
|
||||
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
|
||||
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
|
||||
# produced by setup hosts that report that same version.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux-appimage, windows-installer, recompilation]
|
||||
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -144,7 +275,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
ls -lR artifacts
|
||||
assets=()
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
|
||||
found="$(find artifacts -type f -name "$name" | head -n 1)"
|
||||
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
|
||||
assets+=("$found")
|
||||
|
||||
@@ -46,6 +46,20 @@ jobs:
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-PortableTools.ps1
|
||||
|
||||
- name: Build and run Windows host-platform tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools = Join-Path $PWD 'Launcher/artifacts/portable-tools'
|
||||
$env:PATH = "$tools/llvm-mingw/bin;$tools/CMake/bin;$tools/Ninja;$env:PATH"
|
||||
cmake -S runtime -B build-platform -G Ninja `
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ `
|
||||
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cmake --build build-platform --parallel 3
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
ctest --test-dir build-platform --output-on-failure
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
- name: Prepare pinned native dependencies
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-Dependencies.ps1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/generated/
|
||||
/runtime/native-index.json
|
||||
/Assets/
|
||||
/assets/
|
||||
/PulsarPacks/
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
[submodule "aurora-main"]
|
||||
path = aurora-main
|
||||
url = https://github.com/patchzyy/aurora.git
|
||||
url = https://github.com/patchzyy/aurora-wiicompiled.git
|
||||
branch = main
|
||||
|
||||
@@ -121,6 +121,20 @@ Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# back to compiling aurora from source, which is what this package exists to
|
||||
# avoid - and a mismatched one would be an ABI hazard.
|
||||
$portableClangHash = Get-MkwFileSha256 (Join-Path $portableTools 'llvm-mingw\bin\clang-22.exe')
|
||||
# The identity the runtime build compares against the package's runtime archive.
|
||||
function Get-RuntimeSourceFingerprint([string]$RuntimeDirectory, [string]$DependenciesDirectory) {
|
||||
$arguments = @("-DMKW_RUNTIME_DIR=$(ConvertTo-MkwCMakePath $RuntimeDirectory)")
|
||||
$cppWinRt = Join-Path $DependenciesDirectory 'cppwinrt'
|
||||
if (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) {
|
||||
$arguments += "-DMKW_CPPWINRT_INCLUDE_DIR=$(ConvertTo-MkwCMakePath $cppWinRt)"
|
||||
}
|
||||
$arguments += @('-P', (ConvertTo-MkwCMakePath (Join-Path $RuntimeDirectory 'cmake\RuntimeFingerprint.cmake')))
|
||||
$output = @(& (Join-Path $portableTools 'CMake\bin\cmake.exe') @arguments)
|
||||
if ($LASTEXITCODE -ne 0) { throw "Runtime fingerprinting failed with exit code $LASTEXITCODE." }
|
||||
$line = $output | Where-Object { "$_" -match '^runtime_fingerprint=[0-9a-f]{64}$' } | Select-Object -First 1
|
||||
if (-not $line) { throw "RuntimeFingerprint.cmake printed no fingerprint: $($output -join ' ')" }
|
||||
return "$line".Substring('runtime_fingerprint='.Length)
|
||||
}
|
||||
function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
$provenancePath = Join-Path $PackageDirectory 'provenance.json'
|
||||
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) { return $false }
|
||||
@@ -138,6 +152,9 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
# release ships runtime/third_party sources that first-party TUs compile
|
||||
# against, so archives harvested before a vendored change are stale.
|
||||
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
|
||||
# The precompiled runtime archive must be from these runtime sources; a package without one
|
||||
# would make every installation compile the runtime itself.
|
||||
$recordedRuntime = $provenance.PSObject.Properties['RuntimeSourceFingerprint']
|
||||
return ($provenance.CompilerSha256 -eq $portableClangHash) -and
|
||||
($provenance.FlagFingerprint -eq (Get-MkwNativeFlagFingerprint)) -and
|
||||
($null -ne $recordedDawn) -and
|
||||
@@ -145,11 +162,13 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
($null -ne $recordedAurora) -and
|
||||
($recordedAurora.Value -eq (Get-MkwAuroraSourceFingerprint (Join-Path $repoRoot 'aurora-main'))) -and
|
||||
($null -ne $recordedThirdParty) -and
|
||||
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party')))
|
||||
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party'))) -and
|
||||
($null -ne $recordedRuntime) -and
|
||||
($recordedRuntime.Value -eq (Get-RuntimeSourceFingerprint (Join-Path $repoRoot 'runtime') $dependencySources))
|
||||
}
|
||||
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
|
||||
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
|
||||
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
|
||||
Write-Host '[0/6] Building the precompiled aurora, third-party and runtime package...'
|
||||
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
|
||||
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
|
||||
@@ -231,6 +250,22 @@ Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -Destina
|
||||
[IO.Directory]::CreateDirectory((Join-Path $workspace 'Dependencies')) | Out-Null
|
||||
foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependencySources $name) (Join-Path $workspace "Dependencies\$name") }
|
||||
|
||||
# The translation identity covers the runtime's native registrations through this index (see
|
||||
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
|
||||
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
|
||||
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
|
||||
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --out $nativeIndex
|
||||
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $nativeIndex 'Runtime native index'
|
||||
# A staged runtime that fingerprints differently from the shipped archive would make every
|
||||
# installation silently compile the runtime from source.
|
||||
$stagedRuntimeFingerprint = Get-RuntimeSourceFingerprint (Join-Path $workspace 'runtime') (Join-Path $workspace 'Dependencies')
|
||||
$packagedRuntimeFingerprint = (Get-Content -LiteralPath (Join-Path $workspace 'Dependencies\native_prebuilt\provenance.json') -Raw |
|
||||
ConvertFrom-Json).RuntimeSourceFingerprint
|
||||
if ($stagedRuntimeFingerprint -ne $packagedRuntimeFingerprint) {
|
||||
throw "The staged runtime ($stagedRuntimeFingerprint) does not match the precompiled runtime archive ($packagedRuntimeFingerprint)."
|
||||
}
|
||||
|
||||
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
|
||||
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- The one product version. Release tags must match it. -->
|
||||
<Version>0.2.33</Version>
|
||||
<Version>0.2.35</Version>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -423,13 +423,14 @@ try {
|
||||
$exe = Join-Path $build "$Target.exe"
|
||||
Assert-File $exe 'Locally compiled game executable'
|
||||
Copy-Item -LiteralPath $exe -Destination (Join-Path $Destination "$Target.exe")
|
||||
foreach ($name in @('dxcompiler.dll','dxil.dll','libc++.dll','libpng16.dll','libunwind.dll','libz.dll','SDL3.dll','sqlite3.dll','webgpu_dawn.dll','z.dll','noshaders.zip','dsp_coef.bin','initial_pipeline_cache.db')) {
|
||||
foreach ($name in @('dxcompiler.dll','dxil.dll','libc++.dll','libpng16.dll','libunwind.dll','libz.dll','libzlib1.dll','SDL3.dll','sqlite3.dll','webgpu_dawn.dll','z.dll','noshaders.zip','dsp_coef.bin','initial_pipeline_cache.db')) {
|
||||
$file = Join-Path $build $name
|
||||
if (Test-Path -LiteralPath $file -PathType Leaf) { Copy-Item -LiteralPath $file -Destination $Destination }
|
||||
}
|
||||
$redist = Join-Path $Toolkit 'Redist'
|
||||
if (Test-Path -LiteralPath $redist -PathType Container) {
|
||||
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
|
||||
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
|
||||
Copy-Item -Destination $Destination -Force
|
||||
}
|
||||
foreach ($name in @('config','wii_bootstrap')) {
|
||||
$directory = Join-Path $build $name
|
||||
|
||||
@@ -145,15 +145,23 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
|
||||
else { Write-Host "${LogPrefix}: $Description" }
|
||||
$quotedArguments = @($Arguments | ForEach-Object {
|
||||
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
||||
'"' + $_ + '"'
|
||||
# Backslashes directly before the closing quote would otherwise escape it.
|
||||
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
|
||||
})
|
||||
if ($WaitForProcessTree) {
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
} else {
|
||||
& $FilePath @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
# Tools report progress on stderr (CMake message()); only the exit code decides failure.
|
||||
$previousPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& $FilePath @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $previousPreference
|
||||
}
|
||||
}
|
||||
$global:LASTEXITCODE = $exitCode
|
||||
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
|
||||
@@ -207,13 +215,14 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
||||
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
||||
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
||||
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
||||
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
||||
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
||||
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
|
||||
Returns '' when absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object { -not ($_.FullName.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') }) } |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
@@ -244,14 +253,13 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
||||
(Join-Path $root 'extern') + '\',
|
||||
(Join-Path $root 'build') + '\'
|
||||
)
|
||||
# Git metadata identifies a checkout location, not a source revision's bytes.
|
||||
# Exclude it explicitly, including a submodule's non-hidden .git file.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
# Dot-paths (the submodule's .git pointer) are skipped by name, not by the hidden attribute:
|
||||
# the payload zip drops that attribute, so the installed side would otherwise hash them.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object {
|
||||
$full = $_.FullName
|
||||
$relative = $full.Substring($root.Length + 1).Replace('\', '/')
|
||||
($relative -notmatch '(^|/)\.git(/|$)') -and
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
|
||||
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
|
||||
} |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
|
||||
@@ -21,6 +21,8 @@ $auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
|
||||
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
|
||||
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
|
||||
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
|
||||
$auroraVersions = Join-Path $repoRoot 'aurora-main\cmake\AuroraDependencyVersions.cmake'
|
||||
$auroraImgui = Join-Path $repoRoot 'aurora-main\cmake\AuroraDearImguiProvider.cmake'
|
||||
if (-not (Test-Path -LiteralPath $auroraCMake -PathType Leaf)) {
|
||||
throw 'Aurora sources are missing. Run git submodule update --init --recursive from the repository root first.'
|
||||
}
|
||||
@@ -30,10 +32,10 @@ if (-not (Test-Path -LiteralPath $auroraCMake -PathType Leaf)) {
|
||||
# FetchContent names, not the upstream project names.
|
||||
$packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
|
||||
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
|
||||
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
|
||||
Name = 'SDL'; File = 'SDL3-3.4.10.tar.gz'
|
||||
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.10/SDL3-3.4.10.tar.gz')
|
||||
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_SDL3_REF "refs/tags/release-3.4.10"' },
|
||||
@{ File = $auroraSdl; Text = 'URL "https://github.com/libsdl-org/SDL/archive/${AURORA_SDL3_REF}.tar.gz"' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
|
||||
@@ -41,15 +43,15 @@ $packages = @(
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/theofficialgman/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
|
||||
@{ File = $auroraDawn; Text = 'SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260807.225922-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/encounter/dawn/releases/download/v20260807.225922/dawn-windows-amd64.tar.gz')
|
||||
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_DAWN_VERSION "v20260807.225922"' },
|
||||
@{ File = $auroraDawn; Text = 'SHA256=42a7a295c87b2e8a36312e2f84a5823aac63325e8e1d271bc7ced6f83bfb075f' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
|
||||
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz' })
|
||||
Name = 'fmt'; File = 'fmt-12.1.0.tar.gz'
|
||||
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
|
||||
@@ -61,12 +63,12 @@ $packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
|
||||
Uris = @('https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
|
||||
Pins = @(@{ File = $auroraImgui; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
|
||||
Uris = @('https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_LIBUSB_VERSION "1.0.30"' },
|
||||
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_LIBUSB_VERSION "1.0.30"' },
|
||||
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
@@ -80,9 +82,9 @@ $packages = @(
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
|
||||
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
|
||||
Name = 'tracy'; File = 'tracy-0.14.1.zip'
|
||||
Uris = @('https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'xxhash'; File = 'xxHash-0.8.4.tar.gz'
|
||||
@@ -90,9 +92,9 @@ $packages = @(
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
|
||||
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
|
||||
Name = 'zlib'; File = 'zlib-ng-2.3.3.tar.gz'
|
||||
Uris = @('https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
|
||||
@@ -171,8 +173,12 @@ function Expand-Package([string]$Archive, [string]$Target) {
|
||||
foreach ($package in $packages) {
|
||||
Assert-Pinned $package
|
||||
$target = Join-Path $Destination $package.Name
|
||||
if (Test-Path -LiteralPath $target -PathType Container) { continue }
|
||||
# Records which archive a tree came from, so a pin change replaces a stale tree.
|
||||
$stamp = $target + '.source'
|
||||
$current = (Test-Path -LiteralPath $stamp -PathType Leaf) -and ([IO.File]::ReadAllText($stamp) -eq $package.File)
|
||||
if ($current -and (Test-Path -LiteralPath $target -PathType Container)) { continue }
|
||||
Expand-Package (Get-Archive $package) $target
|
||||
[IO.File]::WriteAllText($stamp, $package.File)
|
||||
Write-Host "Prepared $($package.Name)"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Builds the redistributable precompiled aurora + third-party package: aurora (~43% of local build
|
||||
# CPU time) and vendored Crypto++ are identical for every user, so this configures runtime/ like
|
||||
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure, and harvests the
|
||||
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
|
||||
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure plus the
|
||||
# game-independent runtime archive, and harvests the archives and a generated CMake description
|
||||
# into launcher/artifacts/dependencies/native_prebuilt.
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
|
||||
@@ -87,24 +88,29 @@ try {
|
||||
-AdditionalArguments @("-DMKW_NATIVE_PREBUILT_EXPORT_DIR=$(ConvertTo-MkwCMakePath $exportDirectory)")
|
||||
Invoke-Checked $cmake $configure 'Configuring the aurora/third-party staging build' -LogPrefix 'MKWNP'
|
||||
|
||||
$metaPath = Join-Path $exportDirectory 'meta.txt'
|
||||
Assert-File $metaPath 'Native prebuilt export metadata'
|
||||
$meta = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $metaPath) {
|
||||
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
# The game-independent runtime ships precompiled too; it is outside the probe closure.
|
||||
if (-not $meta.ContainsKey('runtime_fingerprint')) {
|
||||
throw 'The staging configure exported no runtime_fingerprint; the x86_64 package must carry the runtime archive.'
|
||||
}
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products
|
||||
# link, so building it compiles the whole redistributable closure and nothing
|
||||
# else - and a successful link proves the harvested archives are complete.
|
||||
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
|
||||
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
|
||||
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--target', 'mkw_runtime',
|
||||
'--parallel', "$Parallel") 'Building the aurora/third-party closure and the runtime' -LogPrefix 'MKWNP'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# Read the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
$metaPath = Join-Path $exportDirectory 'meta.txt'
|
||||
Assert-File $metaPath 'Native prebuilt export metadata'
|
||||
$meta = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $metaPath) {
|
||||
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
|
||||
$buildNinja = Join-Path $stage 'build.ninja'
|
||||
Assert-File $buildNinja 'Generated build.ninja'
|
||||
@@ -234,6 +240,12 @@ foreach ($name in ($targets.Keys | Sort-Object)) {
|
||||
}
|
||||
if ($sharedImports.Count -eq 0) { throw 'No shared libraries were harvested; the SDL/zlib/libpng DLLs would be missing.' }
|
||||
if ($dawnLinkerFile) { $linkerFileToReference[$dawnLinkerFile] = 'dawn::webgpu_dawn' }
|
||||
$runtimeTarget = $targets['mkw_runtime']
|
||||
if ($null -eq $runtimeTarget -or $runtimeTarget.Type -ne 'STATIC_LIBRARY' -or
|
||||
-not $linkerFileToReference.ContainsKey($runtimeTarget.LinkerFile)) {
|
||||
throw 'The runtime archive (mkw_runtime) was not built and harvested.'
|
||||
}
|
||||
$runtimeArchiveReference = $linkerFileToReference[$runtimeTarget.LinkerFile]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
@@ -297,6 +309,9 @@ $packageLinkItems = @($linkItems | ForEach-Object {
|
||||
if (StartsWithPath $absolute $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $absolute $dependencyRoot)" }
|
||||
throw "Link item is neither a system library nor a harvested archive: $item"
|
||||
})
|
||||
if ($packageLinkItems -contains $runtimeArchiveReference) {
|
||||
throw 'The runtime archive leaked into the aurora link interface.'
|
||||
}
|
||||
|
||||
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile
|
||||
# line, so re-scope it to C++. The runtime also assembles generated .S blobs
|
||||
@@ -333,6 +348,11 @@ $generated.Add('')
|
||||
$generated.Add("set(MKW_NP_DAWN_CONFIG_DIR `"$(ConvertTo-Token $meta['dawn_config_dir'] 'dawn')`")")
|
||||
$generated.Add("set(MKW_NP_DAWN_VERSION `"$($meta['aurora_dawn_version'])`")")
|
||||
$generated.Add('')
|
||||
$generated.Add('# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.')
|
||||
$generated.Add("set(MKW_NP_RUNTIME_ARCHIVE `"$runtimeArchiveReference`")")
|
||||
$generated.Add("set(MKW_NP_RUNTIME_FINGERPRINT `"$($meta['runtime_fingerprint'])`")")
|
||||
$generated.Add("set(MKW_NP_RUNTIME_CPU_PROFILE `"$($meta['runtime_cpu_profile'])`")")
|
||||
$generated.Add('')
|
||||
$generated.Add('# Shared third-party libraries keep imported SHARED targets so that')
|
||||
$generated.Add('# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.')
|
||||
foreach ($import in $sharedImports) {
|
||||
@@ -393,6 +413,9 @@ $provenance = [ordered]@{
|
||||
# release build and the local build can refuse a package aurora has outgrown.
|
||||
AuroraSourceFingerprint = $auroraSourceFingerprint
|
||||
ThirdPartySourceFingerprint = $thirdPartySourceFingerprint
|
||||
# runtime/cmake/RuntimeFingerprint.cmake over the runtime sources the archive was built from.
|
||||
RuntimeSourceFingerprint = $meta['runtime_fingerprint']
|
||||
RuntimeCpuProfile = $meta['runtime_cpu_profile']
|
||||
Sdl3Target = $meta['aurora_sdl3_target']
|
||||
HarvestedLibraryCount = @($linkerFileToReference.Keys).Count
|
||||
Contents = $contents
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
|
||||
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1), Crypto++ and mbed TLS are identical
|
||||
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
|
||||
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
|
||||
# generated CMake description into an output package - the Linux counterpart to
|
||||
# runtime/ against that toolchain, builds just that closure (plus, on x86_64, the game-independent
|
||||
# runtime archive), and harvests the archives plus a generated CMake description into an output
|
||||
# package - the Linux counterpart to
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
@@ -191,12 +192,23 @@ if [[ "${cxx_sha256}" != "${compiler_sha256}" ]]; then
|
||||
compiler_sha256="${compiler_sha256}:${cxx_sha256}"
|
||||
fi
|
||||
|
||||
# The game-independent runtime ships precompiled on x86_64 only; aarch64 compiles it locally.
|
||||
runtime_fingerprint=""
|
||||
if [[ "$arch" == x86_64 ]]; then
|
||||
runtime_fingerprint=$("$cmake_bin" -DMKW_RUNTIME_DIR="$runtime_source" \
|
||||
-P "$runtime_source/cmake/RuntimeFingerprint.cmake" | sed -n 's/^runtime_fingerprint=\([0-9a-f]\{64\}\)$/\1/p')
|
||||
[[ -n "$runtime_fingerprint" ]] || fail "RuntimeFingerprint.cmake printed no runtime fingerprint"
|
||||
fi
|
||||
|
||||
if [[ "$print_fingerprint_only" -eq 1 ]]; then
|
||||
printf 'compiler_sha256=%s\n' "$compiler_sha256"
|
||||
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
|
||||
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
|
||||
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
|
||||
printf 'mbedtls_fingerprint=%s\n' "$mbedtls_fingerprint"
|
||||
if [[ -n "$runtime_fingerprint" ]]; then
|
||||
printf 'runtime_fingerprint=%s\n' "$runtime_fingerprint"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -220,20 +232,30 @@ echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging buil
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
|
||||
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete.
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
meta_path="$export_dir/meta.txt"
|
||||
assert_file "$meta_path" "Native prebuilt export metadata"
|
||||
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete. The runtime archive is outside that closure and built alongside.
|
||||
build_targets=(--target mkw_np_probe)
|
||||
meta_runtime_fingerprint=$(get_meta runtime_fingerprint)
|
||||
if [[ -n "$meta_runtime_fingerprint" ]]; then
|
||||
if [[ -n "$runtime_fingerprint" && "$meta_runtime_fingerprint" != "$runtime_fingerprint" ]]; then
|
||||
fail "The configure's runtime fingerprint ($meta_runtime_fingerprint) differs from RuntimeFingerprint.cmake's ($runtime_fingerprint)"
|
||||
fi
|
||||
build_targets+=(--target mkw_runtime)
|
||||
elif [[ -n "$runtime_fingerprint" ]]; then
|
||||
fail "The staging configure exported no runtime_fingerprint; the $arch package must carry the runtime archive"
|
||||
fi
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" "${build_targets[@]}" --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
build_ninja="$stage_dir/build.ninja"
|
||||
assert_file "$build_ninja" "Generated build.ninja"
|
||||
|
||||
@@ -397,6 +419,13 @@ while IFS='|' read -r name type file linkerfile; do
|
||||
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
|
||||
fi
|
||||
done < "$targets_txt"
|
||||
runtime_archive_ref=""
|
||||
if [[ -n "$meta_runtime_fingerprint" ]]; then
|
||||
runtime_linker=$(awk -F'|' '$1 == "mkw_runtime" && $2 == "STATIC_LIBRARY" { print $4; exit }' "$targets_txt")
|
||||
[[ -n "$runtime_linker" && -f "$runtime_linker" ]] || fail "The runtime archive (mkw_runtime) was not built"
|
||||
runtime_archive_ref=${linker_file_to_reference["$(normalize "$runtime_linker")"]:-}
|
||||
[[ "$runtime_archive_ref" == "@PKG@/lib/"* ]] || fail "The runtime archive (mkw_runtime) was not harvested"
|
||||
fi
|
||||
mbedtls_refs=()
|
||||
mbedtls_txt="$export_dir/mbedtls.txt"
|
||||
assert_file "$mbedtls_txt" "Mbed TLS export targets list"
|
||||
@@ -498,6 +527,11 @@ for item in "${link_items[@]}"; do
|
||||
esac
|
||||
fail "Link item is neither a system library nor a harvested archive: $item"
|
||||
done
|
||||
if [[ -n "$runtime_archive_ref" ]]; then
|
||||
for item in "${package_link_items[@]}"; do
|
||||
[[ "$item" != "$runtime_archive_ref" ]] || fail "The runtime archive leaked into the aurora link interface"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
@@ -562,6 +596,12 @@ generated_cmake="$output_dir/native_prebuilt.cmake"
|
||||
format_cmake_block MKW_NP_MBEDTLS_LIBRARIES "${mbedtls_refs[@]}"
|
||||
echo 'set(MKW_NP_MBEDTLS_INCLUDE_DIR "@PKG@/include/mbedtls")'
|
||||
printf 'set(MKW_NP_MBEDTLS_FINGERPRINT "%s")\n' "$mbedtls_fingerprint"
|
||||
if [[ -n "$runtime_archive_ref" ]]; then
|
||||
# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.
|
||||
printf 'set(MKW_NP_RUNTIME_ARCHIVE "%s")\n' "$runtime_archive_ref"
|
||||
printf 'set(MKW_NP_RUNTIME_FINGERPRINT "%s")\n' "$meta_runtime_fingerprint"
|
||||
printf 'set(MKW_NP_RUNTIME_CPU_PROFILE "%s")\n' "$(get_meta runtime_cpu_profile)"
|
||||
fi
|
||||
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
|
||||
echo ""
|
||||
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
|
||||
@@ -597,12 +637,13 @@ harvested_count=${#linker_file_to_reference[@]}
|
||||
|
||||
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
|
||||
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
|
||||
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" <<'PY'
|
||||
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" \
|
||||
"${runtime_archive_ref:+$meta_runtime_fingerprint}" "${runtime_archive_ref:+$(get_meta runtime_cpu_profile)}" <<'PY'
|
||||
import hashlib, json, os, sys, datetime
|
||||
|
||||
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
|
||||
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
|
||||
harvested_count, mbedtls_fingerprint) = sys.argv[1:]
|
||||
harvested_count, mbedtls_fingerprint, runtime_fingerprint, runtime_cpu_profile) = sys.argv[1:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
@@ -644,6 +685,9 @@ provenance = {
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
if runtime_fingerprint:
|
||||
provenance["RuntimeSourceFingerprint"] = runtime_fingerprint
|
||||
provenance["RuntimeCpuProfile"] = runtime_cpu_profile
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
@@ -127,7 +127,7 @@ foreach ($mandatory in ([regex]::Matches($parameterBlock,
|
||||
}
|
||||
|
||||
# --- The build steps the installer's progress bar acts on.
|
||||
$installProgress = Read-SourceFile (Join-Path $setup 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$installProgress = Read-SourceFile (Join-Path $common 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$knownSteps = Get-QuotedList $installProgress '(?s)class BuildStepIds\s*\{(.*?)\n\}' `
|
||||
'const string \w+ = "([^"]+)";' 'BuildStepIds'
|
||||
$emittedSteps = @([regex]::Matches($localBuild, "(?:-StepId|Write-MkwBuildStep)\s+'([^']+)'") |
|
||||
|
||||
+39
-39
@@ -1,35 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
|
||||
/// existing identifier never changes meaning.
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractToolkit = "extract-toolkit";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string PrepareRetro = "prepare-retro";
|
||||
public const string BuildBase = "build-base";
|
||||
public const string BuildRetro = "build-retro";
|
||||
public const string Publish = "publish";
|
||||
}
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
public interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>Reports to a caller-supplied sink; used by console and maintenance commands.</summary>
|
||||
internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
public sealed class DelegatingInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string> _sink;
|
||||
|
||||
@@ -43,15 +27,20 @@ internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
public sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly string _version;
|
||||
private readonly Action<string>? _log;
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public NdjsonInstallReporter(Action<string>? log = null) => _log = log;
|
||||
public NdjsonInstallReporter(string version, Action<string>? log = null)
|
||||
{
|
||||
_version = version;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
@@ -78,7 +67,7 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
WriteLine(new { type = "result", success = true, version = _version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +95,8 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
public sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string>? _log;
|
||||
|
||||
@@ -126,10 +115,10 @@ internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// <summary>Build step identifiers from the build scripts' <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// id is the contract; LocalBuild.ps1 emits these exactly and Test-PinnedFacts.ps1 fails the release if
|
||||
/// the two sets disagree, so progress never depends on matching reworded English prose.</summary>
|
||||
internal static class BuildStepIds
|
||||
public static class BuildStepIds
|
||||
{
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
@@ -143,16 +132,16 @@ internal static class BuildStepIds
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The bundled
|
||||
/// build script announces every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can
|
||||
/// advance on real events instead of on a timer.
|
||||
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The build
|
||||
/// scripts announce every step they start with an <c>MKWCBUILD:</c> prefix, so the slice can advance
|
||||
/// on real events instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
public sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
/// <summary>The fraction the compile step reaches; beyond it, Ninja's counter drives progress.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
@@ -164,7 +153,7 @@ internal sealed class BuildProgressWindow
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the bundled compiler"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
@@ -209,19 +198,30 @@ internal sealed class BuildProgressWindow
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - an unknown step identifier from a newer script, a plain MKWCBUILD note, or
|
||||
// raw tool output - stays a diagnostic and only feeds the heartbeat below.
|
||||
// Anything else - an unknown step identifier, a plain MKWCBUILD note, or raw tool output -
|
||||
// stays a diagnostic.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
// Ninja prefixes every edge with [done/total]; how many edges there are depends on how much
|
||||
// of the build is already up to date, so follow the counter rather than counting lines.
|
||||
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNinjaProgress(string line, out int done, out int total)
|
||||
{
|
||||
done = total = 0;
|
||||
if (!line.StartsWith('[')) return false;
|
||||
var close = line.IndexOf(']');
|
||||
var slash = line.IndexOf('/');
|
||||
return close > 0 && slash > 0 && slash < close
|
||||
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
|
||||
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
|
||||
&& total > 0 && done <= total;
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Text;
|
||||
using Tomlyn;
|
||||
using Tomlyn.Parsing;
|
||||
using Tomlyn.Syntax;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
@@ -17,12 +20,6 @@ public static class RuntimeConfiguration
|
||||
{
|
||||
public const string ConfigFileName = "Config.toml";
|
||||
|
||||
/// <summary>
|
||||
/// Ordinal, matching the runtime parser/writer (<c>runtime_config.h</c>) exactly. Case-insensitive
|
||||
/// matching here would let setup rewrite a <c>[Paths]</c>/<c>Dvd_Root</c> spelling the runtime ignores.
|
||||
/// </summary>
|
||||
private const StringComparison KeyComparison = StringComparison.Ordinal;
|
||||
|
||||
/// <summary>The per-user configuration an ordinary (non-portable) installation shares.</summary>
|
||||
public static string ApplicationDataConfigPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
@@ -75,9 +72,11 @@ public static class RuntimeConfiguration
|
||||
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
/// <summary>The stored string of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
public static string? GetPath(string configPath, string key) =>
|
||||
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
|
||||
TryLoad(configPath) is { } document && Find(document, ["paths"], key)?.Item.Value is StringValueSyntax value
|
||||
? value.Value!
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
|
||||
@@ -99,9 +98,19 @@ public static class RuntimeConfiguration
|
||||
|
||||
private static void SetPath(string configPath, string key, string value)
|
||||
{
|
||||
var lines = ReadLinesOrDefault(configPath);
|
||||
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
|
||||
WriteLines(configPath, lines);
|
||||
var document = Parse(File.Exists(configPath) ? File.ReadAllText(configPath) : DefaultConfig);
|
||||
var section = new[] { "paths" };
|
||||
var newValue = new StringValueSyntax(FormatPathValue(configPath, value));
|
||||
if (Find(document, section, key) is { } existing)
|
||||
{
|
||||
LastToken(newValue).TrailingTrivia = LastToken(existing.Item.Value!).TrailingTrivia;
|
||||
existing.Item.Value = newValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Append(document, section, new KeyValueSyntax(key, newValue));
|
||||
}
|
||||
Save(configPath, document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,176 +132,104 @@ public static class RuntimeConfiguration
|
||||
|
||||
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
|
||||
{
|
||||
if (!File.Exists(configPath)) return;
|
||||
var lines = File.ReadAllLines(configPath).ToList();
|
||||
var inPaths = false;
|
||||
var changed = false;
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (IsSection(trimmed))
|
||||
{
|
||||
inPaths = trimmed.Equals("[paths]", KeyComparison);
|
||||
continue;
|
||||
}
|
||||
if (!inPaths || trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison))
|
||||
continue;
|
||||
if (!TryUnquoteToml(trimmed[(equals + 1)..].Trim(), out var configured)) return;
|
||||
// A portable installation stores this relative, so ownership is decided on the path the
|
||||
// runtime would actually resolve, not on the stored text.
|
||||
if (!ResolveAgainstConfig(configPath, configured)
|
||||
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
lines.RemoveAt(index);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
if (changed) WriteLines(configPath, lines);
|
||||
if (TryLoad(configPath) is not { } document ||
|
||||
Find(document, ["paths"], key) is not { } entry ||
|
||||
entry.Item.Value is not StringValueSyntax configured)
|
||||
return;
|
||||
// A portable installation stores this relative, so ownership is decided on the path the
|
||||
// runtime would actually resolve, not on the stored text.
|
||||
if (!ResolveAgainstConfig(configPath, configured.Value!)
|
||||
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
var previous = entry.Previous?.EndOfLineToken ?? entry.Table?.EndOfLineToken;
|
||||
if (previous is not null && entry.Item.EndOfLineToken?.TrailingTrivia is { } trivia)
|
||||
(previous.TrailingTrivia ??= []).AddRange(trivia);
|
||||
entry.Items.RemoveChild(entry.Item);
|
||||
Save(configPath, document);
|
||||
}
|
||||
|
||||
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
|
||||
public static string? GetRawValue(string configPath, string section, string key)
|
||||
private const string DefaultConfig = "# WiiCompiled user configuration\n\n";
|
||||
|
||||
private sealed record Entry(
|
||||
KeyValueSyntax Item, SyntaxList<KeyValueSyntax> Items, TableSyntax? Table, KeyValueSyntax? Previous);
|
||||
|
||||
private static DocumentSyntax Parse(string text) => SyntaxParser.ParseStrict(text);
|
||||
|
||||
private static DocumentSyntax? TryLoad(string configPath)
|
||||
{
|
||||
if (!File.Exists(configPath)) return null;
|
||||
var header = $"[{section}]";
|
||||
var inSection = false;
|
||||
foreach (var line in File.ReadLines(configPath))
|
||||
try { return Parse(File.ReadAllText(configPath)); }
|
||||
catch (TomlException) { return null; }
|
||||
}
|
||||
|
||||
/// <summary>Re-parses the edited tree before replacing the file, so an invalid edit never reaches disk.</summary>
|
||||
private static void Save(string configPath, DocumentSyntax document)
|
||||
{
|
||||
var text = document.ToString();
|
||||
Parse(text);
|
||||
FileSystemUtilities.WriteAtomic(configPath, new UTF8Encoding(false).GetBytes(text));
|
||||
}
|
||||
|
||||
private static IEnumerable<TableSyntax> Tables(DocumentSyntax document) =>
|
||||
document.Tables.OfType<TableSyntax>();
|
||||
|
||||
private static string[] Parts(KeySyntax key) =>
|
||||
[Part(key.Key!), .. key.DotKeys.Select(dotted => Part(dotted.Key!))];
|
||||
|
||||
private static string Part(BareKeyOrStringValueSyntax part) =>
|
||||
part is StringValueSyntax quoted ? quoted.Value! : ((BareKeySyntax)part).Key!.Text!;
|
||||
|
||||
/// <summary>Matches by TOML meaning, so <c>"dvd_root"</c> and <c>dvd_root</c> are the same key.</summary>
|
||||
private static Entry? Find(DocumentSyntax document, string[] section, string key)
|
||||
{
|
||||
string[] wanted = [.. section, key];
|
||||
var scopes = Tables(document)
|
||||
.Select(table => (Table: (TableSyntax?)table, Prefix: Parts(table.Name!), Items: table.Items))
|
||||
.Prepend((Table: null, Prefix: [], Items: document.KeyValues));
|
||||
foreach (var (table, prefix, items) in scopes)
|
||||
{
|
||||
var trimmed = RemoveComment(line).Trim();
|
||||
if (IsSection(trimmed))
|
||||
KeyValueSyntax? previous = null;
|
||||
foreach (var item in items)
|
||||
{
|
||||
inSection = trimmed.Equals(header, KeyComparison);
|
||||
continue;
|
||||
if (prefix.Concat(Parts(item.Key!)).SequenceEqual(wanted))
|
||||
return new Entry(item, items, table, previous);
|
||||
previous = item;
|
||||
}
|
||||
if (!inSection || trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison)) continue;
|
||||
return trimmed[(equals + 1)..].Trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces one key in one section, preserving comments, ordering, and every unrelated setting -
|
||||
/// this file belongs to the user and to the in-game settings bar as much as to setup. A
|
||||
/// commented-out key is deliberately not matched, matching the runtime's own writer.
|
||||
/// </summary>
|
||||
private static void SetSectionValue(List<string> lines, string section, string key, string value)
|
||||
private static void Append(DocumentSyntax document, string[] section, KeyValueSyntax item)
|
||||
{
|
||||
var header = $"[{section}]";
|
||||
var sectionStart = -1;
|
||||
var sectionEnd = lines.Count;
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
var text = document.ToString();
|
||||
var newline = text.Contains("\r\n") ? "\r\n" : "\n";
|
||||
var table = Tables(document).FirstOrDefault(t => Parts(t.Name!).SequenceEqual(section));
|
||||
if (table is null)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (!IsSection(trimmed)) continue;
|
||||
if (sectionStart >= 0)
|
||||
{
|
||||
sectionEnd = index;
|
||||
break;
|
||||
}
|
||||
if (trimmed.Equals(header, KeyComparison)) sectionStart = index;
|
||||
}
|
||||
if (sectionStart < 0)
|
||||
{
|
||||
if (lines.Count > 0 && lines[^1].Length != 0) lines.Add("");
|
||||
lines.Add(header);
|
||||
lines.Add($"{key} = {value}");
|
||||
return;
|
||||
var name = new KeySyntax(section[0]);
|
||||
foreach (var part in section.Skip(1)) name.DotKeys.Add(new DottedKeyItemSyntax(part));
|
||||
table = new TableSyntax(name) { EndOfLineToken = NewLine(newline) };
|
||||
var blankLine = text.EndsWith("\n\n") || text.EndsWith("\n\r\n");
|
||||
var gap = text.Length == 0 ? 0 : !text.EndsWith('\n') ? 2 : blankLine ? 0 : 1;
|
||||
for (var i = 0; i < gap; i++) (table.LeadingTrivia ??= []).Add(new SyntaxTrivia(TokenKind.NewLine, newline));
|
||||
document.Tables.Add(table);
|
||||
}
|
||||
|
||||
for (var index = sectionStart + 1; index < sectionEnd; index++)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals >= 0 && trimmed[..equals].Trim().Equals(key, KeyComparison))
|
||||
{
|
||||
lines[index] = $"{key} = {value}";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Append after the section's last real line, not after the blank line that separates it from
|
||||
// the next header, so a generated file stays readable for the user who also edits it by hand.
|
||||
var insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && lines[insertAt - 1].Trim().Length == 0) insertAt--;
|
||||
lines.Insert(insertAt, $"{key} = {value}");
|
||||
// Blank lines and comments after the last entry stay after the new one.
|
||||
var last = table.Items.LastOrDefault();
|
||||
var before = last is null ? table.EndOfLineToken ??= NewLine(newline) : last.EndOfLineToken ??= NewLine(newline);
|
||||
item.EndOfLineToken = NewLine(newline);
|
||||
item.EndOfLineToken.TrailingTrivia = before.TrailingTrivia;
|
||||
before.TrailingTrivia = null;
|
||||
table.Items.Add(item);
|
||||
}
|
||||
|
||||
private static List<string> ReadLinesOrDefault(string configPath) =>
|
||||
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
|
||||
private static SyntaxToken NewLine(string text) => new(TokenKind.NewLine, text);
|
||||
|
||||
private static void WriteLines(string configPath, List<string> lines) =>
|
||||
FileSystemUtilities.WriteAtomic(configPath,
|
||||
Encoding.UTF8.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine));
|
||||
|
||||
private static List<string> DefaultConfigLines() =>
|
||||
[
|
||||
"# WiiCompiled user configuration",
|
||||
"",
|
||||
"[paths]"
|
||||
];
|
||||
|
||||
private static bool IsSection(string value) =>
|
||||
value.Length >= 2 && value[0] == '[' && value[^1] == ']';
|
||||
|
||||
/// <summary>
|
||||
/// Line-for-line port of <c>RemoveComment</c> in <c>runtime_config.h</c>, including its backslash
|
||||
/// escaping inside basic strings: without it a Windows path ending in <c>\\"</c> reads as
|
||||
/// re-opening the quote, and the two implementations disagree about which <c>#</c> starts a comment.
|
||||
/// </summary>
|
||||
private static string RemoveComment(string line)
|
||||
private static SyntaxToken LastToken(SyntaxNode node)
|
||||
{
|
||||
var inSingle = false;
|
||||
var inDouble = false;
|
||||
var escaped = false;
|
||||
for (var index = 0; index < line.Length; index++)
|
||||
{
|
||||
var character = line[index];
|
||||
if (inDouble && character == '\\' && !escaped)
|
||||
{
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (character == '\'' && !inDouble) inSingle = !inSingle;
|
||||
else if (character == '"' && !inSingle && !escaped) inDouble = !inDouble;
|
||||
else if (character == '#' && !inSingle && !inDouble) return line[..index];
|
||||
escaped = false;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private static string QuoteToml(string value) =>
|
||||
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
|
||||
public static bool TryUnquoteToml(string value, out string result)
|
||||
{
|
||||
result = "";
|
||||
if (value.Length < 2) return false;
|
||||
// Literal strings carry no escapes; the runtime's parser treats them the same way.
|
||||
if (value[0] == '\'' && value[^1] == '\'')
|
||||
{
|
||||
result = value[1..^1];
|
||||
return true;
|
||||
}
|
||||
if (value[0] != '"' || value[^1] != '"') return false;
|
||||
var builder = new StringBuilder(value.Length - 2);
|
||||
for (var index = 1; index < value.Length - 1; index++)
|
||||
{
|
||||
if (value[index] == '\\' && index + 1 < value.Length - 1)
|
||||
{
|
||||
var escaped = value[++index];
|
||||
builder.Append(escaped switch { '\\' => '\\', '"' => '"', _ => escaped });
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(value[index]);
|
||||
}
|
||||
}
|
||||
result = builder.ToString();
|
||||
return true;
|
||||
while (node is not SyntaxToken)
|
||||
node = node.GetChild(node.ChildrenCount - 1)!;
|
||||
return (SyntaxToken)node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,7 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Tomlyn" Version="2.10.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -82,6 +82,7 @@ internal static class BuildRunner
|
||||
{
|
||||
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
|
||||
}
|
||||
startInfo.ArgumentList.Add("--setup-version"); startInfo.ArgumentList.Add(ProductInfo.Version);
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
@@ -15,6 +15,12 @@ internal static class DiscTool
|
||||
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
|
||||
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsAlreadyExtracted(manifest, assetsDirectory))
|
||||
{
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Reusing the verified extracted disc", 6);
|
||||
return;
|
||||
}
|
||||
|
||||
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
|
||||
|
||||
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
|
||||
@@ -35,6 +41,8 @@ internal static class DiscTool
|
||||
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Delete(Path.Combine(assetsDirectory, ExtractionMarker));
|
||||
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
|
||||
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
|
||||
|
||||
@@ -61,6 +69,7 @@ internal static class DiscTool
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
|
||||
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
|
||||
File.WriteAllText(Path.Combine(assetsDirectory, ExtractionMarker), ExtractionRecord(dolSha, dataDir));
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
|
||||
}
|
||||
|
||||
@@ -86,6 +95,36 @@ internal static class DiscTool
|
||||
return stdout;
|
||||
}
|
||||
|
||||
private const string ExtractionMarker = ".disc-extracted";
|
||||
|
||||
// Only one disc revision is accepted, so a completed, verified extraction is identical to a fresh one.
|
||||
private static bool IsAlreadyExtracted(ProjectManifest manifest, string assetsDirectory)
|
||||
{
|
||||
var marker = Path.Combine(assetsDirectory, ExtractionMarker);
|
||||
var dol = Path.Combine(assetsDirectory, "main.dol");
|
||||
var rel = Path.Combine(assetsDirectory, "StaticR.rel");
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
if (!File.Exists(marker) || !File.Exists(dol) || !File.Exists(rel) ||
|
||||
!Directory.Exists(Path.Combine(dataDir, "files")))
|
||||
return false;
|
||||
return string.Equals(File.ReadAllText(marker), ExtractionRecord(manifest.DolSha256, dataDir),
|
||||
StringComparison.Ordinal) &&
|
||||
string.Equals(Sha256Of(dol), manifest.DolSha256, StringComparison.Ordinal) &&
|
||||
string.Equals(Sha256Of(rel), manifest.RelSha256, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// The DOL hash plus every extracted file's path and size, so a later deletion or truncation inside
|
||||
// DATA forces a fresh extraction.
|
||||
private static string ExtractionRecord(string dolSha, string dataDir)
|
||||
{
|
||||
var record = new System.Text.StringBuilder(dolSha).Append('\n');
|
||||
foreach (var path in Directory.EnumerateFiles(dataDir, "*", SearchOption.AllDirectories)
|
||||
.Select(path => Path.GetRelativePath(dataDir, path).Replace('\\', '/'))
|
||||
.Order(StringComparer.Ordinal))
|
||||
record.Append(new FileInfo(Path.Combine(dataDir, path)).Length).Append(' ').Append(path).Append('\n');
|
||||
return record.ToString();
|
||||
}
|
||||
|
||||
private static string Sha256Of(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
|
||||
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
|
||||
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
|
||||
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
|
||||
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
|
||||
/// scoping decision).
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
|
||||
// because a later stage happened to estimate a lower number.
|
||||
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
|
||||
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
|
||||
}
|
||||
}
|
||||
|
||||
public void Diagnostic(string line) => Console.Error.WriteLine(line);
|
||||
|
||||
public void Success(string installDirectory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
public void Failure(string error)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = false, error });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
|
||||
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
|
||||
/// </summary>
|
||||
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
|
||||
|
||||
private static void WriteLine(object value)
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
public void Progress(string stage, string message, int percent) =>
|
||||
Console.Out.WriteLine($"[{percent,3}%] {message}");
|
||||
|
||||
public void Diagnostic(string line) => Console.Out.WriteLine(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:<id></c> lines - the id is
|
||||
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
|
||||
/// </summary>
|
||||
internal static class BuildStepIds
|
||||
{
|
||||
public const string BuildTranslator = "build-translator";
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
public const string TranslateBase = "translate-base";
|
||||
public const string EmitBaseManifest = "emit-base-manifest";
|
||||
public const string TranslateMod = "translate-mod";
|
||||
public const string GenerateDataInit = "generate-data-init";
|
||||
public const string EmitBuildShards = "emit-build-shards";
|
||||
public const string ConfigureNative = "configure-native";
|
||||
public const string Compile = "compile";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
|
||||
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
|
||||
/// instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
[
|
||||
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
|
||||
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
|
||||
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
|
||||
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
|
||||
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
private readonly IInstallReporter _reporter;
|
||||
private readonly string _stage;
|
||||
private readonly int _start;
|
||||
private readonly int _end;
|
||||
private double _fraction;
|
||||
private string _message = "Preparing the local build";
|
||||
private int _reportedPercent = -1;
|
||||
|
||||
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_stage = stage;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public void Observe(string line)
|
||||
{
|
||||
var index = line.IndexOf(Marker, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
var text = line[(index + Marker.Length)..].Trim();
|
||||
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
|
||||
{
|
||||
var identifier = text[StepMarker.Length..];
|
||||
var end = identifier.IndexOf(' ');
|
||||
if (end >= 0) identifier = identifier[..end];
|
||||
foreach (var (id, fraction, message) in Steps)
|
||||
{
|
||||
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
|
||||
if (fraction > _fraction)
|
||||
{
|
||||
_fraction = fraction;
|
||||
_message = message;
|
||||
Emit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
|
||||
// feeds the heartbeat below.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
if (percent == _reportedPercent) return;
|
||||
_reportedPercent = percent;
|
||||
_reporter.Progress(_stage, _message, percent);
|
||||
}
|
||||
|
||||
private int Interpolate(double fraction) =>
|
||||
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>Stable stage identifiers reported by <c>--progress-json</c>.</summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
var (command, flags) = ParseArgs(args);
|
||||
if (command is null) { PrintUsage(); return 1; }
|
||||
var progressJson = flags.ContainsKey("progress-json");
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) : new ConsoleInstallReporter();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
<Using Include="WiiCompiled.Setup.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -57,7 +57,7 @@ internal static class ConsoleCommands
|
||||
|
||||
public static int VerifyInputs(CommandLine command)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
|
||||
var temp = Path.Combine(Path.GetTempPath(), "mkwc-verify-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
try
|
||||
@@ -107,7 +107,7 @@ internal static class ConsoleCommands
|
||||
}
|
||||
File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}");
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
|
||||
IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log);
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
try
|
||||
@@ -145,7 +145,7 @@ internal static class ConsoleCommands
|
||||
public static int CheckProducts(CommandLine command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
|
||||
var installation = new Installation(string.IsNullOrWhiteSpace(command.InstallDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: Path.GetFullPath(command.InstallDirectory));
|
||||
@@ -198,7 +198,7 @@ internal static class ConsoleCommands
|
||||
}
|
||||
}
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
|
||||
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
var installation = new Installation(installDirectory);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
|
||||
/// existing identifier never changes meaning.
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractToolkit = "extract-toolkit";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string PrepareRetro = "prepare-retro";
|
||||
public const string BuildBase = "build-base";
|
||||
public const string BuildRetro = "build-retro";
|
||||
public const string Publish = "publish";
|
||||
}
|
||||
@@ -501,17 +501,31 @@ internal sealed class InstallerEngine
|
||||
var dol = Path.Combine(dataRoot, "sys", "main.dol");
|
||||
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
|
||||
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
|
||||
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
|
||||
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
|
||||
var dolHash = InputValidation.Sha256File(dol);
|
||||
var relHash = InputValidation.Sha256File(rel);
|
||||
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var missing = new[] { dol, rel, fst }.Where(path => !File.Exists(path))
|
||||
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new InvalidDataException(
|
||||
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
"The disc extraction is missing required Mario Kart Wii files: " +
|
||||
string.Join(", ", missing) + ".");
|
||||
|
||||
var mismatches = new List<string>();
|
||||
foreach (var (name, path, expected) in new[]
|
||||
{
|
||||
("main.dol", dol, manifest.ExpectedDolSha256),
|
||||
("StaticR.rel", rel, manifest.ExpectedRelSha256)
|
||||
})
|
||||
{
|
||||
var actual = InputValidation.Sha256File(path);
|
||||
if (!actual.Equals(expected, StringComparison.OrdinalIgnoreCase))
|
||||
mismatches.Add($"{name} is {new FileInfo(path).Length:N0} bytes with SHA-256 {actual} " +
|
||||
$"(expected {expected})");
|
||||
}
|
||||
|
||||
if (mismatches.Count > 0)
|
||||
throw new InvalidDataException(
|
||||
"The disc is RMCP01 but its game code does not match the supported clean PAL revision: " +
|
||||
string.Join("; ", mismatches) + ". " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
}
|
||||
|
||||
private static void ValidateInstallDirectory(string path) =>
|
||||
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
{
|
||||
// A command line was supplied, so a caller is driving this process. Rejecting the
|
||||
// command with a modal dialog would hang that caller forever.
|
||||
if (progressJson) new NdjsonInstallReporter().Failure(ex.Message);
|
||||
if (progressJson) new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
else Console.Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ internal static class Program
|
||||
// errors raised outside it.
|
||||
if (progressJson)
|
||||
{
|
||||
new NdjsonInstallReporter().Failure(ex.Message);
|
||||
new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using Tomlyn;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
@@ -51,6 +52,7 @@ internal static class SelfTests
|
||||
RuntimeConfiguration.SetRetroRewindRoot, RuntimeConfiguration.RemoveRetroRewindRootIfOwned),
|
||||
failures);
|
||||
Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures);
|
||||
Test("Runtime configuration quoted keys", TestRuntimeConfigurationQuotedKeys, failures);
|
||||
Test("Install location validation", TestInstallLocationValidation, failures);
|
||||
Test("Command line contract", TestCommandLineContract, failures);
|
||||
Test("Portable root discovery", TestPortableRootDiscovery, failures);
|
||||
@@ -229,6 +231,70 @@ internal static class SelfTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quoted and bare spellings of a key or table are the same TOML key, so rewriting replaces the
|
||||
/// entry in place, keeps everything else byte-identical, and never touches an unparseable file.
|
||||
/// </summary>
|
||||
private static void TestRuntimeConfigurationQuotedKeys()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "mkwc-tomlquoted-" + Guid.NewGuid().ToString("N"));
|
||||
var config = Path.Combine(root, RuntimeConfiguration.ConfigFileName);
|
||||
var owned = Path.Combine(root, "Owned Location");
|
||||
var fullOwned = Path.GetFullPath(owned);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(owned);
|
||||
foreach (var header in new[] { "[\"paths\"]", "[ 'paths' ]", "[paths]" })
|
||||
foreach (var key in new[] { "\"dvd_root\"", "'dvd_root'", "\"dvd\\u005Froot\"", "dvd_root" })
|
||||
{
|
||||
File.WriteAllText(config,
|
||||
$"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=\"D:\\\\Old\" # note\r\nother = 1\r\n");
|
||||
RuntimeConfiguration.SetDvdRoot(config, owned);
|
||||
var text = File.ReadAllText(config);
|
||||
var expected = $"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=" +
|
||||
$"\"{fullOwned.Replace("\\", "\\\\")}\" # note\r\nother = 1\r\n";
|
||||
if (text != expected)
|
||||
throw new Exception($"{header} {key} was not replaced in place:\r\n{text}");
|
||||
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
|
||||
throw new Exception($"The replaced {key} could not be read back.");
|
||||
}
|
||||
|
||||
File.WriteAllText(config, "[paths]\r\nother = 1\r\n\r\n# next\r\n[video]\r\nx = 2");
|
||||
RuntimeConfiguration.SetDvdRoot(config, owned);
|
||||
var appended = File.ReadAllText(config);
|
||||
if (!appended.StartsWith("[paths]\r\nother = 1\r\ndvd_root = ", StringComparison.Ordinal) ||
|
||||
!appended.EndsWith("\r\n\r\n# next\r\n[video]\r\nx = 2", StringComparison.Ordinal))
|
||||
throw new Exception("A missing key was not appended to its section:\r\n" + appended);
|
||||
|
||||
File.WriteAllText(config, "dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n");
|
||||
RuntimeConfiguration.SetDvdRoot(config, owned);
|
||||
var created = File.ReadAllText(config);
|
||||
if (!created.StartsWith("dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n\r\n[paths]\r\n", StringComparison.Ordinal) ||
|
||||
RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
|
||||
throw new Exception("A missing section was not created without touching a same-named root key:\r\n" + created);
|
||||
|
||||
File.Delete(config);
|
||||
RuntimeConfiguration.SetDvdRoot(config, owned);
|
||||
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
|
||||
throw new Exception("A missing config file was not created.");
|
||||
|
||||
const string broken = "[paths]\r\ndvd_root = 1\r\ndvd_root = 2\r\n";
|
||||
File.WriteAllText(config, broken);
|
||||
try
|
||||
{
|
||||
RuntimeConfiguration.SetDvdRoot(config, owned);
|
||||
throw new Exception("An unparseable config was rewritten.");
|
||||
}
|
||||
catch (TomlException) { }
|
||||
if (File.ReadAllText(config) != broken)
|
||||
throw new Exception("An unparseable config was modified.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One location rule set covers both an installation directory and a portable root, so the
|
||||
/// rejections a portable install performs are the ones an ordinary install performs too.
|
||||
@@ -1073,10 +1139,10 @@ internal static class SelfTests
|
||||
if (topologyChangedRuntimeAssets == changedRuntimeAssets)
|
||||
throw new Exception("An empty copied runtime-asset directory was omitted from its identity.");
|
||||
|
||||
// Cache-reuse subsets: translator or runtime/src changes (the translator regex-scans it
|
||||
// for native overrides and effect contracts) invalidate translation but not toolchain,
|
||||
// since CMake/Ninja rebuild exactly the affected objects; a compiler change invalidates
|
||||
// only toolchain; a native_prebuilt provenance rewrite invalidates neither.
|
||||
// Cache-reuse subsets: translator or native registration changes invalidate translation
|
||||
// but not toolchain, since CMake/Ninja rebuild exactly the affected objects; a compiler
|
||||
// change invalidates only toolchain; a native_prebuilt provenance rewrite invalidates
|
||||
// neither. Without a current native index, runtime/src itself decides translation.
|
||||
var components = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (components.Compile != afterPatch)
|
||||
throw new Exception("ComputeComponents disagrees with the compile identity.");
|
||||
@@ -1087,15 +1153,36 @@ internal static class SelfTests
|
||||
throw new Exception("A new translator did not change the translation identity.");
|
||||
if (afterNewTranslator.NativeToolchain != components.NativeToolchain)
|
||||
throw new Exception("A new translator needlessly changed the native toolchain identity.");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"),
|
||||
"int main3();");
|
||||
var runtimeMain = Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp");
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
var afterUnindexedRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterUnindexedRuntime.Translation == afterNewTranslator.Translation)
|
||||
throw new Exception("A runtime source change without a native index did not change the translation identity.");
|
||||
|
||||
File.WriteAllText(runtimeMain, "int main();");
|
||||
if (WriteFakeNativeIndex(root, "\"a\"") !=
|
||||
"50b9779591f4b205751361c8899accd362f96ea5dfd32febed1bedca534c523a")
|
||||
throw new Exception("The native index source digest no longer matches the translator's.");
|
||||
var indexed = ToolkitFingerprint.ComputeComponents(root);
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
WriteFakeNativeIndex(root, "\"a\"");
|
||||
var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterNewRuntime.Translation == afterNewTranslator.Translation)
|
||||
throw new Exception("A runtime source change did not change the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain)
|
||||
if (afterNewRuntime.Translation != indexed.Translation)
|
||||
throw new Exception("A runtime source change with unchanged registrations changed the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
|
||||
throw new Exception("A runtime source change needlessly changed the native toolchain identity.");
|
||||
if (afterNewRuntime.Compile == afterNewTranslator.Compile)
|
||||
if (afterNewRuntime.Compile == indexed.Compile)
|
||||
throw new Exception("A runtime source change did not change the compile identity.");
|
||||
File.WriteAllText(runtimeMain, "int main4();");
|
||||
if (ToolkitFingerprint.ComputeComponents(root).Translation == afterNewRuntime.Translation)
|
||||
throw new Exception("A stale native index was trusted for the translation identity.");
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
WriteFakeNativeIndex(root, "\"a\", \"b\"");
|
||||
afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterNewRuntime.Translation == indexed.Translation)
|
||||
throw new Exception("A native registration change did not change the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
|
||||
throw new Exception("A native registration change needlessly changed the native toolchain identity.");
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", "clang-22.exe"),
|
||||
"clang-v2");
|
||||
var afterNewCompiler = ToolkitFingerprint.ComputeComponents(root);
|
||||
@@ -1148,6 +1235,26 @@ internal static class SelfTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes a native index current for the fake runtime/src; returns its source digest.</summary>
|
||||
private static string WriteFakeNativeIndex(string root, string registrations)
|
||||
{
|
||||
var source = Path.Combine(root, "BuildWorkspace", "runtime", "src");
|
||||
var manifest = new System.Text.StringBuilder();
|
||||
foreach (var (relative, path) in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)
|
||||
.Where(path => path.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".h", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(path => (Path.GetRelativePath(source, path).Replace('\\', '/'), path))
|
||||
.OrderBy(item => item.Item1, StringComparer.Ordinal))
|
||||
manifest.Append(InputValidation.Sha256File(path)).Append(" ").Append(relative).Append('\n');
|
||||
var digest = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(manifest.ToString())))
|
||||
.ToLowerInvariant();
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "native-index.json"),
|
||||
"{\n \"format\": \"wiicompiled-runtime-native-index\",\n \"sourceDigest\": \"" + digest +
|
||||
"\",\n \"registrations\": [" + registrations + "]\n}\n");
|
||||
return digest;
|
||||
}
|
||||
|
||||
private static void CreateFakeToolkit(string root)
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Translator"));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
@@ -12,9 +14,10 @@ namespace WiiCompiled.Setup.Windows;
|
||||
internal static class ToolkitFingerprint
|
||||
{
|
||||
// v6/native-toolchain v2 forces one clean rebuild past a Ninja stale-object bug.
|
||||
// translation v2 added runtime/src, which the translator regex-scans for native overrides.
|
||||
// translation v2 added runtime/src, which the translator regex-scans for native overrides;
|
||||
// v3 replaced it with the native index extracted from it, whenever that index is current.
|
||||
private const string Version = "mkwc-toolkit-compile-v6";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v3";
|
||||
private const string NativeToolchainVersion = "mkwc-toolkit-native-toolchain-v2";
|
||||
private const string PackageVersion = "mkwc-toolkit-package-v1";
|
||||
private const string RuntimeAssetsVersion = "mkwc-product-runtime-assets-v3";
|
||||
@@ -25,20 +28,25 @@ internal static class ToolkitFingerprint
|
||||
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that decide translator output: translator, translation project, runtime sources it
|
||||
/// scans for native override registrations/effect contracts, and the build script's translator
|
||||
/// command lines. Unchanged means the completed base translation can be reused instead of re-run.
|
||||
/// Inputs that decide translator output: translator, translation project, the native override
|
||||
/// registrations/effect contracts it extracts from runtime/src (see
|
||||
/// <see cref="AddNativeRegistrations"/>), and the build script's translator command lines.
|
||||
/// Unchanged means the completed base translation can be reused instead of re-run.
|
||||
/// </summary>
|
||||
private static readonly string[] TranslationPrefixes =
|
||||
[
|
||||
InstalledLayout.ToolkitEntryPrefix + "Translator/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/"
|
||||
];
|
||||
|
||||
private static readonly string[] TranslationFiles =
|
||||
[InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"];
|
||||
|
||||
private const string NativeSourcePrefix = InstalledLayout.WorkspaceEntryPrefix + "runtime/src/";
|
||||
private const string NativeIndexEntry = InstalledLayout.WorkspaceEntryPrefix + "runtime/native-index.json";
|
||||
// The file set the translator's native index source digest covers.
|
||||
private static readonly string[] NativeIndexSourceExtensions = [".cpp", ".h", ".hpp"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that make compiled objects unsafe to reuse: the compiler/CMake/Ninja toolchain, the
|
||||
/// shipped runtime DLLs, and the scripts owning configure flags. Source/dependency changes are
|
||||
@@ -108,14 +116,16 @@ internal static class ToolkitFingerprint
|
||||
if (entries.Count == 0)
|
||||
throw new InvalidDataException($"No toolkit files were found under {root}; the installation is incomplete.");
|
||||
|
||||
var translation = SelectSubset(entries, TranslationPrefixes, TranslationFiles);
|
||||
AddNativeRegistrations(translation, entries, root);
|
||||
return new ToolkitFingerprintComponents(
|
||||
BuildIdentity(Version, entries),
|
||||
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
|
||||
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
|
||||
NativeToolchainFiles));
|
||||
BuildIdentity(TranslationVersion, translation),
|
||||
BuildIdentity(NativeToolchainVersion,
|
||||
SelectSubset(entries, NativeToolchainPrefixes, NativeToolchainFiles)));
|
||||
}
|
||||
|
||||
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
|
||||
private static SortedDictionary<string, string> SelectSubset(SortedDictionary<string, string> entries,
|
||||
string[] prefixes, string[] files)
|
||||
{
|
||||
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
@@ -125,9 +135,61 @@ internal static class ToolkitFingerprint
|
||||
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
|
||||
subset[relative] = hash;
|
||||
}
|
||||
return BuildIdentity(version, subset);
|
||||
return subset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A native-index.json extracted from exactly the shipped runtime/src stands in for those
|
||||
/// sources, minus its source digest, so a runtime change that leaves the registrations alone
|
||||
/// keeps the base translation. A missing, unreadable or stale index keeps the sources instead.
|
||||
/// </summary>
|
||||
private static void AddNativeRegistrations(SortedDictionary<string, string> subset,
|
||||
SortedDictionary<string, string> entries, string root)
|
||||
{
|
||||
var sources = entries.Where(entry => entry.Key.StartsWith(NativeSourcePrefix, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
var registrations = TryHashCurrentNativeIndex(
|
||||
Path.Combine(InstalledLayout.Workspace(root), "runtime", "native-index.json"), sources);
|
||||
if (registrations is not null)
|
||||
{
|
||||
subset[NativeIndexEntry] = registrations;
|
||||
return;
|
||||
}
|
||||
foreach (var (relative, hash) in sources) subset[relative] = hash;
|
||||
}
|
||||
|
||||
private static string? TryHashCurrentNativeIndex(string path, List<KeyValuePair<string, string>> sources)
|
||||
{
|
||||
if (!File.Exists(path)) return null;
|
||||
JsonObject? document;
|
||||
try { document = JsonNode.Parse(File.ReadAllText(path)) as JsonObject; }
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (document?["sourceDigest"] is not JsonValue recorded || !recorded.TryGetValue<string>(out var digest))
|
||||
return null;
|
||||
|
||||
// Mirrors the translator's RuntimeNativeIndexFile.ComputeSourceDigest.
|
||||
var manifest = new StringBuilder();
|
||||
foreach (var (relative, hash) in sources)
|
||||
{
|
||||
if (!NativeIndexSourceExtensions.Any(extension =>
|
||||
relative.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
continue;
|
||||
manifest.Append(hash).Append(" ").Append(relative, NativeSourcePrefix.Length,
|
||||
relative.Length - NativeSourcePrefix.Length).Append('\n');
|
||||
}
|
||||
if (!digest.Equals(Sha256Hex(manifest.ToString()), StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
document.Remove("sourceDigest");
|
||||
return Sha256Hex(document.ToJsonString());
|
||||
}
|
||||
|
||||
private static string Sha256Hex(string text) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of the source-owned files copied verbatim beside every product. This is
|
||||
/// intentionally independent of <see cref="Compute"/>: an asset-only release is published
|
||||
@@ -273,8 +335,7 @@ internal static class ToolkitFingerprint
|
||||
var builder = new StringBuilder(version).Append('\n');
|
||||
foreach (var (relative, hash) in entries)
|
||||
builder.Append(relative).Append('|').Append(hash).Append('\n');
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
|
||||
.ToLowerInvariant();
|
||||
return Sha256Hex(builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
<Using Include="WiiCompiled.Setup.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+32
-11
@@ -127,10 +127,10 @@ mkdir -p "$appdir/usr/toolchain"
|
||||
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
|
||||
|
||||
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
|
||||
# recompiles the aurora/Crypto++/mbed TLS closure with the toolchain above, so this is skipped unless
|
||||
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
|
||||
# the current compiler, flags, source trees or mbed TLS pin.
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time), nor on x86_64
|
||||
# the game-independent runtime. Re-harvesting recompiles that closure with the toolchain above, so
|
||||
# this is skipped unless --print-fingerprint-only (a fast, build-free check) says the existing
|
||||
# package no longer matches the current compiler, flags, source trees or mbed TLS pin.
|
||||
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
|
||||
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
|
||||
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
|
||||
@@ -149,6 +149,8 @@ fields = {
|
||||
"aurora_fingerprint": "AuroraSourceFingerprint",
|
||||
"third_party_fingerprint": "ThirdPartySourceFingerprint",
|
||||
"mbedtls_fingerprint": "MbedTlsFingerprint",
|
||||
# Only printed on x86_64, so a package without the runtime archive is stale there.
|
||||
"runtime_fingerprint": "RuntimeSourceFingerprint",
|
||||
}
|
||||
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
|
||||
PY
|
||||
@@ -176,7 +178,29 @@ find "$appdir/workspace" -name .git -prune -exec rm -rf {} +
|
||||
# is a plain developer build directory.
|
||||
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$appdir/workspace/Launcher/"
|
||||
|
||||
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
|
||||
# index, so it is extracted from exactly the bundled runtime sources.
|
||||
echo "Extracting the runtime native index..."
|
||||
"$appdir/usr/bin/translator-cli" emit-native-index \
|
||||
--native-source-dir "$appdir/workspace/runtime/src" \
|
||||
--out "$appdir/workspace/runtime/native-index.json"
|
||||
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
|
||||
|
||||
# A bundled runtime that fingerprints differently from the shipped archive would make every
|
||||
# installation silently compile the runtime from source.
|
||||
if [[ "$appimagetool_arch" == x86_64 ]]; then
|
||||
staged_runtime_fingerprint=$("$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch/bin/cmake" \
|
||||
-DMKW_RUNTIME_DIR="$appdir/workspace/runtime" -P "$appdir/workspace/runtime/cmake/RuntimeFingerprint.cmake" |
|
||||
sed -n 's/^runtime_fingerprint=//p')
|
||||
packaged_runtime_fingerprint=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("RuntimeSourceFingerprint", ""))' \
|
||||
"$appdir/native-prebuilt/provenance.json")
|
||||
if [[ -z "$staged_runtime_fingerprint" || "$staged_runtime_fingerprint" != "$packaged_runtime_fingerprint" ]]; then
|
||||
echo "build-appimage.sh: the bundled runtime ($staged_runtime_fingerprint) does not match the precompiled runtime archive ($packaged_runtime_fingerprint)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
|
||||
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
|
||||
@@ -206,12 +230,9 @@ CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
|
||||
mkdir -p "$CACHE"
|
||||
if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
|
||||
mkdir -p "$CACHE/Launcher"
|
||||
for dir in runtime aurora-main projects; do
|
||||
rm -rf "$CACHE/$dir"
|
||||
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
|
||||
done
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
# Unchanged files keep their timestamps so the incremental build survives the update.
|
||||
source "$HERE/workspace/Launcher/build-cache.sh"
|
||||
mkw_sync_tree "$HERE/workspace" "$CACHE" runtime aurora-main projects Launcher >&2
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Shared incremental-build logic for the Unix setups (Linux AppImage and macOS), sourced by
|
||||
# local-build.sh, local-build-macos.command, the AppImage's AppRun and macos/setup.command.
|
||||
# Mirrors the Windows installer's translation/toolchain identities and timestamp preservation.
|
||||
# Must stay compatible with macOS's bash 3.2.
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
mkw_sha256_cmd=(sha256sum)
|
||||
else
|
||||
mkw_sha256_cmd=(shasum -a 256)
|
||||
fi
|
||||
|
||||
mkw_sha256() {
|
||||
"${mkw_sha256_cmd[@]}" "$@" | awk '{print $1}'
|
||||
}
|
||||
|
||||
# Prints "<sha> <relative path>" for every file under each directory, relative to $1.
|
||||
mkw_digest_tree() {
|
||||
local root=$1; shift
|
||||
(
|
||||
cd "$root" || exit 1
|
||||
local dir
|
||||
for dir in "$@"; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
printf 'tree %s\n' "$dir"
|
||||
find "$dir" -type f -print0 | LC_ALL=C sort -z | xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
|
||||
else
|
||||
printf 'absent %s\n' "$dir"
|
||||
fi
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
# Content-only digest of a file or directory, independent of where it lives (the AppImage mount
|
||||
# path changes every launch).
|
||||
mkw_digest_path() {
|
||||
if [[ -d "$1" ]]; then
|
||||
mkw_digest_tree "$1" . | mkw_sha256
|
||||
elif [[ -f "$1" ]]; then
|
||||
mkw_sha256 < "$1"
|
||||
else
|
||||
printf 'absent\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# Same digest as the translator's RuntimeNativeIndexFile.ComputeSourceDigest. $1 = runtime/src.
|
||||
mkw_native_source_digest() {
|
||||
(
|
||||
cd "$1" || exit 1
|
||||
find . -type f \( -iname '*.cpp' -o -iname '*.h' -o -iname '*.hpp' \) -print0 | LC_ALL=C sort -z |
|
||||
xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
|
||||
) | grep -v ' /dev/null$' | sed 's| \./| |' | mkw_sha256
|
||||
}
|
||||
|
||||
# The native registrations the translator reads from runtime/src. A current native-index.json
|
||||
# stands in for the sources (minus its source digest), so runtime edits that leave the
|
||||
# registrations alone keep the translation; a missing or stale index falls back to the sources.
|
||||
# $1 = workspace.
|
||||
mkw_native_index_identity() {
|
||||
local index="$1/runtime/native-index.json" recorded
|
||||
recorded=$(mkw_read_identity "$index" sourceDigest)
|
||||
if [[ -n "$recorded" && -d "$1/runtime/src" && "$recorded" == "$(mkw_native_source_digest "$1/runtime/src")" ]]; then
|
||||
printf 'native-index %s\n' "$(grep -v '^[[:space:]]*"sourceDigest"[[:space:]]*:' "$index" | mkw_sha256)"
|
||||
else
|
||||
mkw_digest_tree "$1" runtime/src
|
||||
fi
|
||||
}
|
||||
|
||||
# Everything the base translation depends on: game inputs, project, native registrations, the
|
||||
# translator itself and the build script that drives it.
|
||||
# $1 = workspace, $2 = translator file or directory, $3 = build script.
|
||||
mkw_translation_identity() {
|
||||
local workspace=$1 translator=$2 script=$3
|
||||
{
|
||||
printf 'mkw-translation-v2\n'
|
||||
printf 'dol %s\n' "$(mkw_digest_path "$workspace/Assets/main.dol")"
|
||||
printf 'rel %s\n' "$(mkw_digest_path "$workspace/Assets/StaticR.rel")"
|
||||
printf 'translator %s\n' "$(mkw_digest_path "$translator")"
|
||||
printf 'script %s\n' "$(mkw_digest_path "$script")"
|
||||
mkw_digest_tree "$workspace" projects
|
||||
mkw_native_index_identity "$workspace"
|
||||
} | mkw_sha256
|
||||
}
|
||||
|
||||
# Inputs that make compiled objects unsafe to reuse: compiler/CMake/Ninja versions and the
|
||||
# from-source dependency pins. Source changes are excluded; Ninja rebuilds exactly what they touch.
|
||||
# $1 = workspace, remaining = tools (C compiler, C++ compiler, cmake, ninja).
|
||||
mkw_toolchain_identity() {
|
||||
local workspace=$1; shift
|
||||
local tool
|
||||
{
|
||||
printf 'mkw-toolchain-v2\n'
|
||||
for tool in "$@"; do
|
||||
# First line only: clang appends its InstalledDir, which moves with the AppImage mount.
|
||||
printf '%s\n' "$("$tool" --version 2>/dev/null | head -n 1)"
|
||||
done
|
||||
mkw_digest_tree "$workspace" aurora-main/extern aurora-main/cmake
|
||||
printf 'aurora %s\n' "$(mkw_digest_path "$workspace/aurora-main/CMakeLists.txt")"
|
||||
printf 'mbedtls %s\n' "$(mkw_digest_path "$workspace/runtime/cmake/MbedTLSPin.cmake")"
|
||||
} | mkw_sha256
|
||||
}
|
||||
|
||||
# $1 = JSON file, $2 = key. Prints the recorded string value, or nothing.
|
||||
mkw_read_identity() {
|
||||
[[ -f "$1" ]] || return 0
|
||||
grep -o "\"$2\" *: *\"[^\"]*\"" "$1" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true
|
||||
}
|
||||
|
||||
mkw_write_identity() {
|
||||
printf '{"SchemaVersion":1,"%s":"%s"}\n' "$2" "$3" > "$1"
|
||||
}
|
||||
|
||||
# Mirrors each named directory from $1 into $2. Unchanged files are left untouched so their
|
||||
# timestamps (which clang's PCH validation and Ninja compare) survive; changed and new files are
|
||||
# copied fresh and files no longer shipped are removed.
|
||||
mkw_sync_tree() {
|
||||
local source=$1 destination=$2; shift 2
|
||||
local dir relative changed=0 unchanged=0 removed=0
|
||||
for dir in "$@"; do
|
||||
if [[ ! -d "$source/$dir" ]]; then
|
||||
rm -rf "$destination/$dir"
|
||||
continue
|
||||
fi
|
||||
if [[ -e "$destination/$dir" && ! -d "$destination/$dir" ]]; then
|
||||
rm -f "$destination/$dir"
|
||||
fi
|
||||
mkdir -p "$destination/$dir"
|
||||
while IFS= read -r -d '' relative; do
|
||||
relative=${relative#./}
|
||||
if [[ -d "$source/$dir/$relative" ]]; then
|
||||
[[ -d "$destination/$dir/$relative" ]] || { rm -f "$destination/$dir/$relative"; mkdir -p "$destination/$dir/$relative"; }
|
||||
elif [[ -f "$destination/$dir/$relative" ]] && cmp -s "$source/$dir/$relative" "$destination/$dir/$relative"; then
|
||||
unchanged=$((unchanged + 1))
|
||||
else
|
||||
rm -rf "$destination/$dir/$relative"
|
||||
cp "$source/$dir/$relative" "$destination/$dir/$relative"
|
||||
changed=$((changed + 1))
|
||||
fi
|
||||
done < <(cd "$source/$dir" && find . -mindepth 1 -print0)
|
||||
while IFS= read -r -d '' relative; do
|
||||
relative=${relative#./}
|
||||
if [[ ! -e "$source/$dir/$relative" && ( -e "$destination/$dir/$relative" || -L "$destination/$dir/$relative" ) ]]; then
|
||||
[[ -d "$destination/$dir/$relative" ]] || removed=$((removed + 1))
|
||||
rm -rf "$destination/$dir/$relative"
|
||||
fi
|
||||
done < <(cd "$destination/$dir" && find . -mindepth 1 -depth -print0)
|
||||
done
|
||||
printf 'Workspace refreshed: %d changed, %d unchanged, %d removed file(s).\n' "$changed" "$unchanged" "$removed"
|
||||
}
|
||||
@@ -4,11 +4,12 @@ set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
default_workspace=$(cd "$script_dir/.." && pwd)
|
||||
# shellcheck source=build-cache.sh
|
||||
source "$script_dir/build-cache.sh"
|
||||
|
||||
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -22,17 +23,20 @@ Usage: local-build-macos.command --output-dir DIR [options]
|
||||
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
|
||||
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
|
||||
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
|
||||
--force-clean-build Delete local generated and native-build-macos caches
|
||||
--force-clean-build Delete local generated and current-architecture native build caches
|
||||
--parallel N Pin translation and build parallelism
|
||||
--cmake PATH --ninja PATH Override build tools
|
||||
--dotnet PATH Override dotnet
|
||||
--translator-bin PATH Use a self-contained Translator.Cli executable
|
||||
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
|
||||
Launcher/Directory.Build.props when present)
|
||||
EOF
|
||||
}
|
||||
|
||||
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
|
||||
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
|
||||
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
|
||||
setup_version=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
@@ -50,14 +54,21 @@ while (($#)); do
|
||||
--ninja) ninja_bin=${2:-}; shift 2 ;;
|
||||
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
|
||||
--translator-bin) translator_bin=${2:-}; shift 2 ;;
|
||||
--setup-version) setup_version=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
|
||||
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
|
||||
macos_arch=$(uname -m)
|
||||
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
|
||||
macos_deployment_target=12.0
|
||||
workspace=$(cd "$workspace" && pwd)
|
||||
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
|
||||
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
|
||||
fi
|
||||
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail '--setup-version contains unsupported characters'
|
||||
[[ -n "$output_dir" ]] || fail '--output-dir is required'
|
||||
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
|
||||
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
|
||||
@@ -73,7 +84,8 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
|
||||
|
||||
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
|
||||
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
|
||||
native_build="$workspace/native-build-macos-$macos_arch"
|
||||
assert_file "$project" 'translation project'
|
||||
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
|
||||
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
|
||||
@@ -115,11 +127,30 @@ if (( builds_retro )); then
|
||||
fi
|
||||
fi
|
||||
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
translation_provenance="$generated/translation-provenance.json"
|
||||
translation_fingerprint=$(mkw_translation_identity "$workspace" "${translator_bin:-$(dirname "${translator_dll:-}")}" "$script_dir/local-build-macos.command")
|
||||
reuse_base=0
|
||||
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" && -f "$metadata" && -f "$manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
if (( reuse_base && builds_retro )); then
|
||||
# A base tree that never saw this Code.pul would bake vanilla code into the modded product.
|
||||
pul_sha=$(mkw_sha256 "$retro_root/Binaries/Code.pul")
|
||||
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$metadata" && \
|
||||
! translator check-base-mod-awareness --project "$project" --profile retro-rewind --translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul"; then
|
||||
reuse_base=0
|
||||
fi
|
||||
fi
|
||||
if (( reuse_base )); then
|
||||
step reuse-base-translation 'Reusing the completed base translation'
|
||||
else
|
||||
rm -f "$translation_provenance"; mkdir -p "$functions" "$manifest_dir"
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
|
||||
fi
|
||||
if (( builds_retro )); then
|
||||
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
|
||||
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
|
||||
@@ -130,10 +161,19 @@ args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-
|
||||
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
|
||||
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
|
||||
|
||||
toolchain_provenance="$native_build/toolchain-provenance.json"
|
||||
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" clang clang++ "$cmake_bin" "$ninja_bin")
|
||||
if [[ -d "$native_build" && "$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
|
||||
printf 'MKWCBUILD: The toolchain or a pinned dependency changed; rebuilding the native build from scratch\n'
|
||||
rm -rf "$native_build"
|
||||
fi
|
||||
step configure-native 'Configuring the native toolchain'
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
|
||||
# Aurora's required API even when find_package() succeeds.
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
|
||||
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
|
||||
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
|
||||
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
|
||||
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
|
||||
|
||||
+59
-15
@@ -39,7 +39,7 @@ require_command() {
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
mkw_sha256 "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -47,6 +47,8 @@ sha256_of() {
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck source=build-cache.sh
|
||||
source "$script_dir/build-cache.sh"
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
profile=base
|
||||
output_dir=""
|
||||
@@ -66,6 +68,8 @@ translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
sysroot=""
|
||||
setup_version=""
|
||||
x86_cpu_profile=auto
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -90,6 +94,10 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
skips compiling aurora-main from source entirely
|
||||
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
|
||||
standard headers/startup files
|
||||
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
|
||||
Launcher/Directory.Build.props when present)
|
||||
--x86-cpu-profile {auto|v2|v3} x86-64 instruction set to build for (default: auto, v3 when
|
||||
this machine supports it)
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -114,16 +122,27 @@ while [[ $# -gt 0 ]]; do
|
||||
--translator-bin) translator_bin_override=$2; shift 2 ;;
|
||||
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
|
||||
--sysroot) sysroot=$2; shift 2 ;;
|
||||
--setup-version) setup_version=$2; shift 2 ;;
|
||||
--x86-cpu-profile) x86_cpu_profile=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
|
||||
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
|
||||
fi
|
||||
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail "--setup-version contains unsupported characters"
|
||||
|
||||
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
|
||||
case "$profile" in
|
||||
base|retro-rewind|both) ;;
|
||||
*) fail "--profile must be base, retro-rewind, or both" ;;
|
||||
esac
|
||||
case "$x86_cpu_profile" in
|
||||
auto|v2|v3) ;;
|
||||
*) fail "--x86-cpu-profile must be auto, v2, or v3" ;;
|
||||
esac
|
||||
|
||||
builds_retro=0
|
||||
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
|
||||
@@ -173,6 +192,22 @@ if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
|
||||
fi
|
||||
|
||||
host_supports_x86_64_v3() {
|
||||
local macros macro
|
||||
macros=$("$cc_bin" -march=native -dM -E -x c /dev/null 2>/dev/null) || return 1
|
||||
for macro in __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__; do
|
||||
grep -q "^#define $macro " <<<"$macros" || return 1
|
||||
done
|
||||
}
|
||||
|
||||
if [[ "$x86_cpu_profile" == "auto" ]]; then
|
||||
x86_cpu_profile=v3
|
||||
if [[ "$(uname -m)" == "x86_64" ]] && ! host_supports_x86_64_v3; then
|
||||
x86_cpu_profile=v2
|
||||
echo "MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2"
|
||||
fi
|
||||
fi
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
@@ -266,10 +301,9 @@ else
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
|
||||
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
|
||||
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
|
||||
# a match plus every expected output file present means the previous translation is still good.
|
||||
# Translation cache: this script is the only owner of the reuse decision. The identity (see
|
||||
# build-cache.sh) covers the game inputs, project, native registrations, translator and this script,
|
||||
# matching the Windows installer's translation identity.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (( force_clean_build )); then
|
||||
@@ -277,13 +311,12 @@ if (( force_clean_build )); then
|
||||
rm -rf "$generated" "$base_manifest_dir" "$build"
|
||||
fi
|
||||
|
||||
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
|
||||
translator_identity_input=${translator_bin:-$(dirname "$translator_dll")}
|
||||
translation_fingerprint=$(mkw_translation_identity "$workspace" "$translator_identity_input" "$script_dir/local-build.sh")
|
||||
reuse_base=0
|
||||
if [[ -f "$translation_provenance" ]]; then
|
||||
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
|
||||
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" \
|
||||
&& -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -331,8 +364,7 @@ else
|
||||
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
|
||||
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
|
||||
|
||||
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
|
||||
> "$translation_provenance"
|
||||
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -407,6 +439,13 @@ if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
|
||||
fi
|
||||
fi
|
||||
# The AppImage keeps tool paths stable across updates, so a changed compiler is only visible here.
|
||||
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin")
|
||||
if [[ "$keep_native_build" -eq 1 && \
|
||||
"$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
|
||||
keep_native_build=0
|
||||
stale_reason=" (the toolchain or a pinned dependency changed)"
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
|
||||
rm -rf "$build"
|
||||
@@ -418,7 +457,9 @@ configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
-DMKW_SETUP_VERSION="$setup_version"
|
||||
-DMKW_X86_CPU_PROFILE="$x86_cpu_profile")
|
||||
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
|
||||
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
|
||||
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
|
||||
@@ -437,7 +478,9 @@ if [[ "$cc_bin" == */* ]]; then
|
||||
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
|
||||
fi
|
||||
if [[ -n "$fuse_ld_override" ]]; then
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
|
||||
# CMAKE_EXE_LINKER_FLAGS is one string that CMake pastes into the shell link command.
|
||||
printf -v fuse_ld_quoted '%q' "$fuse_ld_override"
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_quoted")
|
||||
fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
@@ -453,6 +496,7 @@ fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
|
||||
@@ -9,45 +9,97 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
|
||||
Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
|
||||
|
||||
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
|
||||
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
|
||||
is unsigned unless --installer-identity is supplied; releases should sign and
|
||||
notarize it with a Developer ID Installer certificate.
|
||||
maintainer-verified, redistributable macOS artifacts for both arm64 and
|
||||
x86_64. The setup package selects native tools for its host while the game is
|
||||
compiled locally for that host architecture. CMake must be universal2. The
|
||||
resulting pkg is unsigned unless
|
||||
--installer-identity is supplied; releases should sign and notarize it with a
|
||||
Developer ID Installer certificate.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--version VERSION Bundle/package version (default: project version, or 0.1.0)
|
||||
--installer-identity NAME Developer ID Installer identity for productbuild
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
|
||||
version_supplied=false
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--translator) translator=${2:-}; shift 2 ;;
|
||||
--nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
|
||||
--nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
|
||||
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
|
||||
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
|
||||
--cmake-root) cmake_root=${2:-}; shift 2 ;;
|
||||
--ninja) ninja=${2:-}; shift 2 ;;
|
||||
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
|
||||
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; version_supplied=true; shift 2 ;;
|
||||
--installer-identity) identity=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
version=${version#v}
|
||||
if [[ "$version_supplied" == false ]]; then
|
||||
local_csproj="$workspace/Launcher/Directory.Build.props"
|
||||
if [[ -f "$local_csproj" ]]; then
|
||||
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
|
||||
if [[ -n "$detected" ]]; then
|
||||
version="$detected"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
|
||||
IFS=. read -r version_major version_minor version_patch <<< "$version"
|
||||
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
|
||||
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
|
||||
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
|
||||
[[ -x "$translator" ]] || fail '--translator must name an executable'
|
||||
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
|
||||
for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
|
||||
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
|
||||
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
|
||||
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
|
||||
require_arch() {
|
||||
local artifact=$1 arch=$2 label=$3
|
||||
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
|
||||
}
|
||||
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
|
||||
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
|
||||
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
|
||||
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
|
||||
|
||||
# Slice checks above prevent accidental cross-architecture packaging. Exercise
|
||||
# each supplied executable as well: an incorrectly bundled runtime can have a
|
||||
# valid Mach-O header but still fail before the setup app can use it. Apple
|
||||
# Silicon maintainers validate Intel tools through Rosetta when it is present.
|
||||
host_arch=$(uname -m)
|
||||
run_for_arch() {
|
||||
local arch=$1 label=$2
|
||||
shift 2
|
||||
if [[ "$arch" == "$host_arch" ]]; then
|
||||
"$@" >/dev/null || fail "$label did not run successfully"
|
||||
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
|
||||
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
|
||||
else
|
||||
# Intel hosts cannot execute arm64 binaries. The slice remains checked
|
||||
# above; CI or an Apple Silicon maintainer must execute that tool set.
|
||||
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
|
||||
"$label" "$host_arch" "$arch" >&2
|
||||
fi
|
||||
}
|
||||
for arch in arm64 x86_64; do
|
||||
if [[ "$arch" == arm64 ]]; then
|
||||
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
|
||||
else
|
||||
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
|
||||
fi
|
||||
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
|
||||
run_for_arch "$arch" "--translator-$arch" "$translator" --help
|
||||
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
|
||||
done
|
||||
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
|
||||
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
@@ -64,7 +116,7 @@ cat > "$app/Contents/Info.plist" <<EOF
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>$short_version</string>
|
||||
<key>CFBundleVersion</key><string>$version</string>
|
||||
<key>LSMinimumSystemVersion</key><string>14.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>12.0</string>
|
||||
</dict></plist>
|
||||
EOF
|
||||
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
|
||||
@@ -96,20 +148,36 @@ copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$re
|
||||
mkdir -p "$resources/workspace"
|
||||
for source in aurora-main projects runtime translator; do
|
||||
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
|
||||
[[ "$source" != aurora-main || -f "$workspace/$source/CMakeLists.txt" ]] ||
|
||||
fail "aurora-main is empty; run git submodule update --init --recursive"
|
||||
copy_clean "$workspace/$source" "$resources/workspace/$source"
|
||||
done
|
||||
# Setup bundles contain materialized sources and must work without Git.
|
||||
find "$resources/workspace" -name .git -prune -exec rm -rf {} +
|
||||
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
|
||||
# index, so it is extracted from exactly the packaged runtime sources.
|
||||
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
|
||||
"$host_translator" emit-native-index --native-source-dir "$resources/workspace/runtime/src" \
|
||||
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
|
||||
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
copy_clean "$workspace/Launcher/Directory.Build.props" "$resources/workspace/Launcher/Directory.Build.props"
|
||||
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
|
||||
copy_clean "$workspace/Launcher/build-cache.sh" "$resources/workspace/Launcher/build-cache.sh"
|
||||
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
|
||||
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
|
||||
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
|
||||
mkdir -p "$resources/tools/cmake"
|
||||
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
|
||||
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
|
||||
# setup.command uses this marker to update source inputs in an existing user
|
||||
# workspace without replacing extracted game assets or Retro Rewind files.
|
||||
printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
|
||||
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
|
||||
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
|
||||
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
|
||||
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
|
||||
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
|
||||
copy_clean "$cmake_root" "$resources/tools/cmake"
|
||||
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
|
||||
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
|
||||
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
|
||||
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
|
||||
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
|
||||
codesign --force --deep --sign - "$app"
|
||||
|
||||
@@ -38,6 +38,16 @@ done
|
||||
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
|
||||
|
||||
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
|
||||
|
||||
# Sorted "<size> <path>" of every extracted file; recorded at extraction, compared on reuse.
|
||||
data_manifest() { (cd "$1" && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 stat -f '%z %N'); }
|
||||
|
||||
if [[ -d "$assets_dir/DATA/files" && -f "$assets_dir/.data-manifest" && -f "$assets_dir/main.dol" && -f "$assets_dir/StaticR.rel" ]] && \
|
||||
[[ $(sha256 "$assets_dir/main.dol") == "$EXPECTED_DOL_SHA256" && $(sha256 "$assets_dir/StaticR.rel") == "$EXPECTED_REL_SHA256" ]] && \
|
||||
[[ "$(data_manifest "$assets_dir/DATA")" == "$(cat "$assets_dir/.data-manifest")" ]]; then
|
||||
printf 'MKWCBUILD:STEP:disc-ready Reusing the verified extracted game assets\n'
|
||||
exit 0
|
||||
fi
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
|
||||
cleanup() { rm -rf "$scratch"; }
|
||||
trap cleanup EXIT
|
||||
@@ -51,8 +61,10 @@ dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
|
||||
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
|
||||
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
|
||||
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
|
||||
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
|
||||
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
|
||||
dol_sha=$(sha256 "$dol")
|
||||
rel_sha=$(sha256 "$rel")
|
||||
[[ $dol_sha == "$EXPECTED_DOL_SHA256" ]] || fail "main.dol does not match the clean PAL RMCP01 image (expected $EXPECTED_DOL_SHA256, got $dol_sha)"
|
||||
[[ $rel_sha == "$EXPECTED_REL_SHA256" ]] || fail "StaticR.rel does not match the clean PAL RMCP01 image (expected $EXPECTED_REL_SHA256, got $rel_sha)"
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
@@ -64,15 +76,17 @@ mkdir -p "$stage"
|
||||
ditto "$data_root" "$stage/DATA"
|
||||
ditto "$dol" "$stage/main.dol"
|
||||
ditto "$rel" "$stage/StaticR.rel"
|
||||
data_manifest "$stage/DATA" > "$stage/.data-manifest"
|
||||
|
||||
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/.data-manifest" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
for item in DATA .data-manifest main.dol StaticR.rel; do
|
||||
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
|
||||
done
|
||||
fi
|
||||
mv "$stage/DATA" "$assets_dir/DATA"
|
||||
mv "$stage/.data-manifest" "$assets_dir/.data-manifest"
|
||||
mv "$stage/main.dol" "$assets_dir/main.dol"
|
||||
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
|
||||
rmdir "$stage"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
|
||||
# Pass this file on the first configure with:
|
||||
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
|
||||
#
|
||||
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
|
||||
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
|
||||
# x86_64 artifacts.
|
||||
set(CMAKE_SYSTEM_NAME Darwin)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
|
||||
"Target macOS architectures" FORCE)
|
||||
@@ -5,28 +5,36 @@ set -euo pipefail
|
||||
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
|
||||
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
|
||||
|
||||
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
|
||||
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
|
||||
result. This is suitable for local use; a release must replace ad-hoc signing
|
||||
with the project's Developer ID signing and notarization process.
|
||||
|
||||
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
|
||||
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
|
||||
EOF
|
||||
}
|
||||
|
||||
build_dir=""; product=""; output_dir=""
|
||||
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--build-dir) build_dir=${2:-}; shift 2 ;;
|
||||
--product) product=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
--architecture) architecture=${2:-}; shift 2 ;;
|
||||
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
|
||||
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
|
||||
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
|
||||
app="$output_dir/$product.app"
|
||||
@@ -47,7 +55,7 @@ cat > "$app/Contents/Info.plist" <<EOF
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.1.0</string>
|
||||
<key>CFBundleVersion</key><string>1</string>
|
||||
<key>LSMinimumSystemVersion</key><string>14.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
</dict></plist>
|
||||
EOF
|
||||
@@ -57,25 +65,96 @@ for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
|
||||
ln -s "../Resources/$asset" "$macos/$asset"
|
||||
done
|
||||
|
||||
# Build a closure of Homebrew dylibs. System libraries remain system references.
|
||||
queue=("$macos/$product")
|
||||
# Expand each image's rpaths before inheriting them, so @loader_path stays
|
||||
# relative to the image that declared it rather than a descendant library.
|
||||
expanded_rpaths() {
|
||||
local target=$1 rpath
|
||||
while IFS= read -r rpath; do
|
||||
case "$rpath" in
|
||||
@loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;;
|
||||
@loader_path) rpath="$(dirname "$target")" ;;
|
||||
@executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;;
|
||||
@executable_path) rpath="$build_dir" ;;
|
||||
esac
|
||||
printf '%s\n' "$rpath"
|
||||
done < <(otool -l "$target" | awk '
|
||||
/LC_RPATH/ { rpath = 1; next }
|
||||
rpath && /^[[:space:]]*path / {
|
||||
sub(/^[[:space:]]*path[[:space:]]+/, "");
|
||||
sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, "");
|
||||
print;
|
||||
rpath = 0;
|
||||
}')
|
||||
}
|
||||
|
||||
# Resolve a non-system dependency using the current image's rpaths followed
|
||||
# by the inherited loader stack, matching dyld's dependency-chain search.
|
||||
dependency_path() {
|
||||
local current=$1 dependency=$2 search_rpaths=$3 rpath candidate
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*)
|
||||
return 1
|
||||
;;
|
||||
/*)
|
||||
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
|
||||
;;
|
||||
@loader_path/*)
|
||||
candidate="$(dirname "$current")/${dependency#@loader_path/}"
|
||||
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
|
||||
;;
|
||||
@executable_path/*)
|
||||
candidate="$build_dir/${dependency#@executable_path/}"
|
||||
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
|
||||
;;
|
||||
@rpath/*|*.dylib)
|
||||
local subpath
|
||||
if [[ "$dependency" == @rpath/* ]]; then
|
||||
subpath="${dependency#@rpath/}"
|
||||
else
|
||||
subpath="$dependency"
|
||||
fi
|
||||
while IFS= read -r rpath; do
|
||||
[[ -n "$rpath" ]] || continue
|
||||
candidate="$rpath/$subpath"
|
||||
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
|
||||
done <<< "$search_rpaths"
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# Build a closure of non-system dylibs. System libraries remain system
|
||||
# references, while every resolved dependency is copied beside the executable.
|
||||
queue=("$build_dir/$product")
|
||||
queue_rpaths=("")
|
||||
while ((${#queue[@]})); do
|
||||
current=${queue[0]}
|
||||
inherited_rpaths=${queue_rpaths[0]}
|
||||
queue=("${queue[@]:1}")
|
||||
queue_rpaths=("${queue_rpaths[@]:1}")
|
||||
current_rpaths=$(expanded_rpaths "$current")
|
||||
if [[ -n "$inherited_rpaths" ]]; then
|
||||
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
|
||||
fi
|
||||
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
|
||||
while IFS= read -r dependency; do
|
||||
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
|
||||
[[ -f "$dependency" ]] || continue
|
||||
name=$(basename "$dependency")
|
||||
[[ -z "$dependency" ]] && continue
|
||||
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*) continue ;;
|
||||
esac
|
||||
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
|
||||
name=$(basename "$dep_path")
|
||||
if [[ ! -f "$frameworks/$name" ]]; then
|
||||
ditto "$dependency" "$frameworks/$name"
|
||||
ditto "$dep_path" "$frameworks/$name"
|
||||
install_name_tool -id "@rpath/$name" "$frameworks/$name"
|
||||
queue+=("$frameworks/$name")
|
||||
queue+=("$dep_path")
|
||||
queue_rpaths+=("$current_rpaths")
|
||||
fi
|
||||
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
|
||||
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
|
||||
done
|
||||
while IFS= read -r binary; do
|
||||
while IFS= read -r old; do
|
||||
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
|
||||
name=$(basename "$old")
|
||||
[[ -f "$frameworks/$name" ]] || continue
|
||||
if [[ "$binary" == "$macos/$product" ]]; then
|
||||
@@ -83,7 +162,7 @@ while IFS= read -r binary; do
|
||||
else
|
||||
install_name_tool -change "$old" "@loader_path/$name" "$binary"
|
||||
fi
|
||||
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
|
||||
done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
|
||||
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
|
||||
|
||||
find "$frameworks" -type f -exec codesign --force --sign - {} +
|
||||
|
||||
@@ -5,16 +5,19 @@ set -euo pipefail
|
||||
|
||||
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace_source="$resources/workspace"
|
||||
nodtool="$resources/tools/nodtool"
|
||||
translator="$resources/tools/Translator.Cli"
|
||||
cmake_bin="$resources/tools/cmake/bin/cmake"
|
||||
ninja_bin="$resources/tools/ninja"
|
||||
support_root="$HOME/Library/Application Support/WiiCompiled"
|
||||
workspace="$support_root/BuildWorkspace"
|
||||
products="$support_root/Products"
|
||||
|
||||
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
|
||||
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
|
||||
notice() {
|
||||
/usr/bin/osascript - "$1" >/dev/null <<'APPLESCRIPT'
|
||||
on run argv
|
||||
display dialog (item 1 of argv) buttons {"OK"} default button "OK" with icon caution
|
||||
end run
|
||||
APPLESCRIPT
|
||||
}
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
@@ -36,6 +39,13 @@ while (($#)); do
|
||||
done
|
||||
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
|
||||
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
|
||||
host_tools="$resources/tools/$host_arch"
|
||||
nodtool="$host_tools/nodtool"
|
||||
translator="$host_tools/Translator.Cli"
|
||||
ninja_bin="$host_tools/ninja"
|
||||
|
||||
if [[ -z "$game" ]]; then
|
||||
game=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
|
||||
@@ -60,12 +70,34 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
|
||||
/usr/bin/xcode-select --install || true
|
||||
exit 1
|
||||
fi
|
||||
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
|
||||
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
|
||||
fail "the packaged $(basename "$tool") does not support $host_arch"
|
||||
done
|
||||
|
||||
mkdir -p "$support_root" "$products"
|
||||
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
source_bundle_version="$workspace_source/.bundle-version"
|
||||
workspace_bundle_version="$workspace/.bundle-version"
|
||||
needs_workspace_refresh=0
|
||||
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
needs_workspace_refresh=1
|
||||
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
|
||||
needs_workspace_refresh=1
|
||||
fi
|
||||
|
||||
if (( needs_workspace_refresh )); then
|
||||
printf 'Preparing the local build workspace...\n'
|
||||
rm -rf "$workspace"
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
else
|
||||
# Refresh only packaged source inputs. Assets and the staged Retro
|
||||
# Rewind package belong to the user and stay in place; unchanged files
|
||||
# keep their timestamps so the incremental native build survives.
|
||||
# shellcheck source=../build-cache.sh
|
||||
source "$workspace_source/Launcher/build-cache.sh"
|
||||
mkw_sync_tree "$workspace_source" "$workspace" aurora-main projects runtime translator Launcher
|
||||
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
|
||||
fi
|
||||
fi
|
||||
|
||||
profile=base
|
||||
@@ -117,7 +149,7 @@ fi
|
||||
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
|
||||
|
||||
config="$support_root/Config.toml"
|
||||
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
|
||||
toml_string() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
|
||||
set_path() {
|
||||
local key=$1 value=$2 encoded line temporary
|
||||
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
|
||||
@@ -143,8 +175,13 @@ install_app() {
|
||||
local app=$1
|
||||
[[ -d "$products/$app" ]] || return 0
|
||||
if [[ "$destination" == /Applications ]]; then
|
||||
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
|
||||
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
|
||||
/usr/bin/osascript - "$products/$app" "/Applications/$app" <<'APPLESCRIPT'
|
||||
on run argv
|
||||
set sourcePath to quoted form of (item 1 of argv)
|
||||
set installedPath to quoted form of (item 2 of argv)
|
||||
do shell script "mkdir -p /Applications && rm -rf " & installedPath & " && /usr/bin/ditto " & sourcePath & " " & installedPath with administrator privileges
|
||||
end run
|
||||
APPLESCRIPT
|
||||
else
|
||||
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exercise a real dyld dependency chain before and after app packaging.
|
||||
set -euo pipefail
|
||||
|
||||
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
|
||||
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
|
||||
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
|
||||
trap 'rm -rf "$test_root"' EXIT
|
||||
|
||||
build_dir="$test_root/build with spaces"
|
||||
output_dir="$test_root/output with spaces"
|
||||
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
|
||||
cat > "$test_root/c.c" <<'EOF'
|
||||
int value_c(void) { return 7; }
|
||||
EOF
|
||||
cat > "$test_root/b.c" <<'EOF'
|
||||
extern int value_c(void);
|
||||
int value_b(void) { return 2 * value_c(); }
|
||||
EOF
|
||||
cat > "$test_root/a.c" <<'EOF'
|
||||
extern int value_b(void);
|
||||
int value_a(void) { return 1 + value_b(); }
|
||||
EOF
|
||||
cat > "$test_root/main.c" <<'EOF'
|
||||
#include <stdio.h>
|
||||
extern int value_a(void);
|
||||
int main(void) { printf("%d\n", value_a()); return 0; }
|
||||
EOF
|
||||
|
||||
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
|
||||
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
|
||||
-L "$build_dir/A/c" -lC \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
|
||||
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
|
||||
-L "$build_dir/A/b" -lB \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
|
||||
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
|
||||
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
|
||||
-L "$build_dir/A" -lA \
|
||||
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
|
||||
|
||||
# B has no rpaths: its C dependency must inherit A's loader-relative path.
|
||||
[[ $("$build_dir/WiiCompiled") == 15 ]]
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
|
||||
: > "$build_dir/$asset"
|
||||
done
|
||||
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
|
||||
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
|
||||
for name in libA.dylib libB.dylib libC.dylib; do
|
||||
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
|
||||
done
|
||||
|
||||
# Remove access to the original paths and relocate the app before executing.
|
||||
mv "$build_dir" "$test_root/hidden build"
|
||||
mkdir "$test_root/relocated app"
|
||||
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
|
||||
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
|
||||
printf 'publish-app dependency-chain test passed\n'
|
||||
@@ -11,6 +11,7 @@
|
||||
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
|
||||
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
|
||||
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
|
||||
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Total downloads" src="https://img.shields.io/github/downloads/patchzyy/Wiicompiled/total?color=00B8D9&logo=github"></a>
|
||||
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
@@ -52,12 +53,16 @@ Play at several times the console's resolution.
|
||||
**Music ducking.**
|
||||
Start playing something else, Spotify, a YouTube video, and
|
||||
the game automatically mutes its own music until the other audio stops. Optional, if you'd
|
||||
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
|
||||
rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
|
||||
On macOS 14.2 or later, this detects other apps with active audio output and excludes
|
||||
the game's own audio. Apps that keep an output stream running silently can keep
|
||||
game music muted even when nothing is audible.
|
||||
|
||||
**An in-game settings bar.**
|
||||
Press **F10** while the game window has focus:
|
||||
- Internal resolution
|
||||
- FPS counter
|
||||
- MetalFX spatial upscaling on supported macOS GPUs
|
||||
- Controller assignment for all four ports
|
||||
- Full per-controller button mapping, including the bumpers
|
||||
- Dolphin-syntax input expressions and GCPadNew.ini import
|
||||
@@ -96,8 +101,8 @@ Known limitations of the Wii Remote path:
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||
- macOS 14 (Sonoma) or later on Apple Silicon
|
||||
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
|
||||
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -118,6 +123,21 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||
|
||||
### macOS
|
||||
|
||||
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
|
||||
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
|
||||
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
|
||||
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
|
||||
or its parent folder.
|
||||
|
||||
Setup verifies and extracts the image locally, then translates and compiles the native app on your
|
||||
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
|
||||
then open Setup again. When the build completes, Setup asks for administrator approval once to install
|
||||
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
|
||||
|
||||
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
|
||||
reporting—remain visible.
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
|
||||
+1
-1
Submodule aurora-main updated: 5506c5dc53...648e77d7e8
+122
-124
@@ -1,27 +1,79 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
|
||||
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
|
||||
# initializes the Apple toolchain so direct developer CMake invocations cannot
|
||||
# accidentally inherit the running SDK's deployment version. This cache entry
|
||||
# is harmless on non-Apple platforms and remains overridable by a caller.
|
||||
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
|
||||
"Minimum macOS version supported by WiiCompiled" FORCE)
|
||||
endif()
|
||||
project(mkw_recompiled)
|
||||
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
|
||||
endif()
|
||||
|
||||
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
|
||||
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports one macOS architecture per build directory; "
|
||||
"configure separate arm64 and x86_64 build directories")
|
||||
endif()
|
||||
list(GET CMAKE_OSX_ARCHITECTURES 0 _mkw_osx_arch)
|
||||
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}" CACHE STRING "Target processor architecture" FORCE)
|
||||
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}")
|
||||
endif()
|
||||
|
||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
# The first native macOS target is Apple Silicon. Intel and universal
|
||||
# binaries remain future compatibility work; do not silently claim them.
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
|
||||
else()
|
||||
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
|
||||
endif()
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
# CMake and Ninja write response files with Windows quoting, but clang tokenizes them with
|
||||
# POSIX rules by default, where a bare apostrophe in a path opens a quoted span.
|
||||
add_link_options(--rsp-quoting=windows)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
|
||||
set(MKW_X86_CPU_PROFILE "v3" CACHE STRING
|
||||
"Minimum x86-64 CPU profile for translated products (v2 or v3)")
|
||||
set_property(CACHE MKW_X86_CPU_PROFILE PROPERTY STRINGS v2 v3)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
|
||||
NOT MKW_X86_CPU_PROFILE MATCHES "^(v2|v3)$")
|
||||
message(FATAL_ERROR "MKW_X86_CPU_PROFILE must be v2 or v3 (got '${MKW_X86_CPU_PROFILE}')")
|
||||
endif()
|
||||
|
||||
set(MKW_SETUP_VERSION "" CACHE STRING "WiiCompiled version recorded in the runtime log")
|
||||
|
||||
option(MKW_PLATFORM_TESTS_ONLY "Build host-platform tests without graphics dependencies or game assets" OFF)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/HostLibraries.cmake")
|
||||
if(MKW_PLATFORM_TESTS_ONLY)
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
message(FATAL_ERROR "MKW_PLATFORM_TESTS_ONLY requires MKW_BUILD_PRODUCTS=OFF")
|
||||
endif()
|
||||
add_compile_definitions(NOMINMAX)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
@@ -42,6 +94,8 @@ set(MKW_NATIVE_PREBUILT_DIR "" CACHE PATH
|
||||
# package can be harvested from it.
|
||||
set(MKW_NATIVE_PREBUILT_EXPORT_DIR "" CACHE PATH
|
||||
"Maintainer-only: write a description of the configured aurora build here")
|
||||
option(MKW_USE_PREBUILT_RUNTIME
|
||||
"Link the native prebuilt package's runtime archive when it matches this workspace" ON)
|
||||
|
||||
# The two Crypto++ configuration macros are load-bearing for ABI: they shape the
|
||||
# structs and inline code the headers generate, so a first-party TU including
|
||||
@@ -59,20 +113,6 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# Runtime configuration is real TOML, parsed by toml11 rather than a project-
|
||||
# specific line parser. Keep it header-only and vendored so disconnected release
|
||||
# builds have exactly the same parser as developer builds.
|
||||
@@ -169,6 +209,18 @@ endif()
|
||||
# Fast-math may erase them and change guest-visible integer conversions.
|
||||
set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off)
|
||||
|
||||
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_helpers.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/fpu_helpers.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
|
||||
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
|
||||
SKIP_UNITY_BUILD_INCLUSION ON
|
||||
SKIP_PRECOMPILE_HEADERS ON
|
||||
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
|
||||
# Match the optimization policy of the translated callers as well as their FP policy.
|
||||
set_property(SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp" APPEND PROPERTY
|
||||
COMPILE_OPTIONS -O2 -fno-slp-vectorize)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Third-party: aurora-main (provides SDL3 + GPU backends)
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -223,6 +275,19 @@ else()
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(SDL_TEST OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Aurora components the runtime does not use. They would otherwise fetch
|
||||
# extra dependencies at configure time, which offline builds cannot do.
|
||||
set(AURORA_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(AURORA_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(AURORA_ENABLE_DVD OFF CACHE BOOL "" FORCE)
|
||||
set(AURORA_ENABLE_THP OFF CACHE BOOL "" FORCE)
|
||||
set(AURORA_ENABLE_RMLUI OFF CACHE BOOL "" FORCE)
|
||||
set(AURORA_ENABLE_CARD OFF CACHE BOOL "" FORCE)
|
||||
# The runtime implements the Wii SDK itself; Aurora stays on its GameCube SDK.
|
||||
set(AURORA_TARGET_RVL OFF CACHE BOOL "" FORCE)
|
||||
# Every dependency comes from the pinned sources, never the build host.
|
||||
set(AURORA_USE_SYSTEM_DEPENDENCIES OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# MinGW/WSL can choke on SDL precompiled headers; disable PCH for aurora/SDL.
|
||||
set(CMAKE_DISABLE_PRECOMPILE_HEADERS ON)
|
||||
# Isolate aurora build products to a subdirectory under the main build dir.
|
||||
@@ -259,20 +324,16 @@ else()
|
||||
TARGET_DIRECTORY aurora_gx
|
||||
PROPERTIES COMPILE_OPTIONS "-fno-finite-math-only")
|
||||
endif()
|
||||
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Everything from here down is this project's own code, so this is where the
|
||||
# project definitions enter the directory scope. A directory-scoped definition
|
||||
# seeds the COMPILE_DEFINITIONS of targets as they are created, which means:
|
||||
# * aurora-main and its third-party tree were added above -> unaffected;
|
||||
# * mkw_runtime_common, the translated shard libraries and the products in
|
||||
# cmake/PublicProducts.cmake are all created below (that file is include()d,
|
||||
# not add_subdirectory()d, so it shares this directory scope) -> they pick
|
||||
# the definitions up exactly as before.
|
||||
# * the runtime library, the translated shard libraries and the products in
|
||||
# cmake/RuntimeLibrary.cmake and cmake/PublicProducts.cmake are all created
|
||||
# below (those files are include()d, not add_subdirectory()d, so they share
|
||||
# this directory scope) -> they pick the definitions up exactly as before.
|
||||
if(MKW_PROJECT_COMPILE_DEFINITIONS)
|
||||
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
|
||||
endif()
|
||||
@@ -285,12 +346,14 @@ endif()
|
||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
if(MKW_PLATFORM_MACOS_ARM64)
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
endif()
|
||||
else()
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
endif()
|
||||
@@ -299,91 +362,25 @@ set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
# x86-64-v3 flags, so it gets its own object library instead of riding along in
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
# the runtime library. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/RuntimeLibrary.cmake")
|
||||
|
||||
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
|
||||
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the Apple Silicon context ABI and the public host-memory
|
||||
# contracts separately from translated products.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
# The prebuilt export also carries the game-independent runtime as a static
|
||||
# archive (x86_64 only; AArch64 compiles it locally with -mcpu=native). It is
|
||||
# defined here, after the project definitions, so its flags match the products'.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
mkw_add_runtime_library(mkw_runtime STATIC ${SOURCES})
|
||||
set_target_properties(mkw_runtime PROPERTIES EXCLUDE_FROM_ALL ON)
|
||||
endif()
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
@@ -393,8 +390,8 @@ if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
# The prebuilt export only needs the aurora/third-party closure and the runtime
|
||||
# configured above, so a packaging machine without a translation stops here.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
@@ -411,20 +408,13 @@ if(MKW_BUILD_PRODUCTS)
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
# runtime/src is game-independent; per-game glue lives in runtime/game.
|
||||
add_library(mkw_macos_native_compile OBJECT ${SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${MKW_AURORA_DIR}/include")
|
||||
@@ -432,7 +422,15 @@ else()
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_MACOS_X86_64)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
|
||||
target_compile_options(mkw_macos_native_compile PRIVATE
|
||||
-march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
# Shared by the product build and the dependency-free platform test configuration.
|
||||
get_filename_component(MKW_HOST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
# POSIX guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
|
||||
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
|
||||
# libco's existing System V AMD64 backend.
|
||||
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
add_library(mkw_libco STATIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco/libco.c")
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco")
|
||||
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_HOST_RUNTIME_DIR}/src/platform/host_platform.cpp")
|
||||
target_include_directories(mkw_platform PUBLIC "${MKW_HOST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_compile_definitions(mkw_platform PRIVATE NOMINMAX)
|
||||
target_link_libraries(mkw_platform PUBLIC shell32 ole32 uuid)
|
||||
endif()
|
||||
@@ -3,10 +3,10 @@
|
||||
#
|
||||
# Produced by launcher/Prepare-NativePrebuilt.ps1 with the same toolchain and the
|
||||
# same canonical flag set the consuming build uses, which is what makes a single
|
||||
# package valid for every user. Nothing about this project's own code changes:
|
||||
# the runtime, the translated shards and the products still compile locally and
|
||||
# still link targets named aurora::gx / aurora::pad / aurora::si / aurora::vi /
|
||||
# aurora::mtx, so cmake/PublicProducts.cmake needs no prebuilt-specific branch.
|
||||
# package valid for every user. The translated shards and the products still
|
||||
# compile locally and still link targets named aurora::gx / aurora::pad /
|
||||
# aurora::si / aurora::vi / aurora::mtx. The package may also carry the native
|
||||
# runtime archive; cmake/RuntimeLibrary.cmake decides whether it can be used.
|
||||
#
|
||||
# The aurora and dependency source trees are still present in the workspace - the
|
||||
# runtime includes aurora headers directly - so the package records include
|
||||
@@ -42,6 +42,10 @@ endfunction()
|
||||
mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES})
|
||||
mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES})
|
||||
mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR})
|
||||
set(MKW_NP_RUNTIME_ARCHIVE_PATH "")
|
||||
if(MKW_NP_RUNTIME_ARCHIVE)
|
||||
mkw_np_resolve(MKW_NP_RUNTIME_ARCHIVE_PATH "${MKW_NP_RUNTIME_ARCHIVE}")
|
||||
endif()
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
if(NOT MKW_NP_MBEDTLS_LIBRARIES OR NOT MKW_NP_MBEDTLS_INCLUDE_DIR)
|
||||
message(FATAL_ERROR
|
||||
|
||||
@@ -87,6 +87,11 @@ mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets)
|
||||
# the scan above cannot see it; it is appended explicitly. The type and closure
|
||||
# checks below still apply to it.
|
||||
list(APPEND _mkw_np_all_targets "mkw_cryptopp")
|
||||
# The game-independent runtime archive (RuntimeLibrary.cmake). It is outside the
|
||||
# probe closure, so it ships only when the harvester builds it explicitly.
|
||||
if(TARGET mkw_runtime)
|
||||
list(APPEND _mkw_np_all_targets "mkw_runtime")
|
||||
endif()
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
set(_mkw_np_mbedtls_targets mbedtls mbedx509 mbedcrypto)
|
||||
# mbedcrypto links these third-party archives when upstream defines them.
|
||||
@@ -168,5 +173,12 @@ file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
|
||||
"cxx_compiler=${CMAKE_CXX_COMPILER}\n"
|
||||
"cxx_compiler_version=${CMAKE_CXX_COMPILER_VERSION}\n"
|
||||
"library_targets=${_mkw_np_lib_targets_csv}\n")
|
||||
if(TARGET mkw_runtime)
|
||||
mkw_runtime_fingerprint(_mkw_np_runtime_fingerprint "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
file(APPEND "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
|
||||
"runtime_fingerprint=${_mkw_np_runtime_fingerprint}\n"
|
||||
"runtime_cpu_profile=${MKW_X86_CPU_PROFILE}\n")
|
||||
endif()
|
||||
|
||||
message(STATUS "Native prebuilt export: ${MKW_NATIVE_PREBUILT_EXPORT_DIR}")
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
get_filename_component(MKW_TEST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
if(MKW_BUILD_PSQ_TESTS)
|
||||
add_executable(mkw_psq_helpers_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/psq_helpers_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/ppc_quantized.cpp")
|
||||
target_include_directories(mkw_psq_helpers_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/psq_memory"
|
||||
"${MKW_TEST_RUNTIME_DIR}/include/isa"
|
||||
"${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE
|
||||
-O2 -fno-fast-math -ffp-contract=off -fno-slp-vectorize)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE
|
||||
-march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
endif()
|
||||
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
|
||||
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
|
||||
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
|
||||
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
|
||||
-P "${MKW_TEST_RUNTIME_DIR}/tests/psq_reserved_tests.cmake")
|
||||
endif()
|
||||
|
||||
add_executable(mkw_platform_paths_tests "${MKW_TEST_RUNTIME_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_runtime_config_tests "${MKW_TEST_RUNTIME_DIR}/tests/runtime_config_tests.cpp")
|
||||
target_include_directories(mkw_runtime_config_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/toml11")
|
||||
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
|
||||
|
||||
add_executable(mkw_nand_save_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_sc_serial_tests "${MKW_TEST_RUNTIME_DIR}/tests/sc_serial_tests.cpp")
|
||||
target_include_directories(mkw_sc_serial_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/test_expr.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# The emulated Bluetooth controller is pure host code.
|
||||
add_executable(mkw_bluetooth_controller_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_controller.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_remote_link.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_sdp.cpp")
|
||||
target_include_directories(mkw_bluetooth_controller_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_bluetooth_controller_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_bluetooth_controller_tests COMMAND mkw_bluetooth_controller_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
add_executable(mkw_windows_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_windows_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_windows_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_windows_host_context_tests COMMAND mkw_windows_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
# Probe at the base ISA, including OS support for AVX register state. Cross
|
||||
# builds execute target code only through a configured emulator.
|
||||
if(NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
|
||||
include(CheckCXXSourceRuns)
|
||||
include(CMakePushCheckState)
|
||||
cmake_push_check_state(RESET)
|
||||
set(CMAKE_REQUIRED_FLAGS "-march=x86-64")
|
||||
check_cxx_source_runs([=[
|
||||
#include <cpuid.h>
|
||||
int main() {
|
||||
unsigned a, b, c, d;
|
||||
if (__get_cpuid_max(0, nullptr) < 7 ||
|
||||
__get_cpuid_max(0x80000000u, nullptr) < 0x80000001u)
|
||||
return 1;
|
||||
__cpuid_count(1, 0, a, b, c, d);
|
||||
// v2 plus FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C.
|
||||
const unsigned leaf1 = (1u << 0) | (1u << 9) | (1u << 12) |
|
||||
(1u << 13) | (1u << 19) | (1u << 20) | (1u << 22) |
|
||||
(1u << 23) | (1u << 26) | (1u << 27) | (1u << 28) | (1u << 29);
|
||||
if ((c & leaf1) != leaf1) return 1;
|
||||
__cpuid_count(7, 0, a, b, c, d);
|
||||
const unsigned leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
|
||||
if ((b & leaf7) != leaf7) return 1;
|
||||
__cpuid_count(0x80000001u, 0, a, b, c, d);
|
||||
if ((c & 0x21u) != 0x21u) return 1; // LAHF-SAHF and LZCNT.
|
||||
__asm__ __volatile__("xgetbv" : "=a"(a), "=d"(d) : "c"(0));
|
||||
return (a & 0x6u) == 0x6u ? 0 : 1;
|
||||
}
|
||||
]=] MKW_HOST_SUPPORTS_X86_V3)
|
||||
cmake_pop_check_state()
|
||||
endif()
|
||||
# Compile the same oracle cases for both paths. The v2 binary proves the
|
||||
# scalar fallback stays free of FMA; the v3 binary checks the existing
|
||||
# vector intrinsic path against the same strict result.
|
||||
foreach(profile IN ITEMS v2 v3)
|
||||
add_executable(mkw_ppc_pair_fma_${profile}_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/ppc_pair_fma_tests.cpp")
|
||||
target_include_directories(mkw_ppc_pair_fma_${profile}_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_ppc_pair_fma_${profile}_tests PRIVATE cxx_std_17)
|
||||
target_compile_options(mkw_ppc_pair_fma_${profile}_tests PRIVATE
|
||||
-march=x86-64-${profile} -fno-fast-math -ffp-contract=off)
|
||||
if((NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR) AND
|
||||
(profile STREQUAL "v2" OR MKW_HOST_SUPPORTS_X86_V3))
|
||||
add_test(NAME mkw_ppc_pair_fma_${profile}_tests COMMAND mkw_ppc_pair_fma_${profile}_tests)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(mkw_cpu_baseline_v2_compile OBJECT
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_cpu_baseline.cpp")
|
||||
target_compile_features(mkw_cpu_baseline_v2_compile PRIVATE cxx_std_17)
|
||||
target_compile_definitions(mkw_cpu_baseline_v2_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
target_compile_options(mkw_cpu_baseline_v2_compile PRIVATE -march=x86-64-v2 -w)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the public host-memory contracts separately from translated products.
|
||||
if(MKW_PLATFORM_MACOS_ARM64)
|
||||
# Apple Silicon's context ABI is implemented by the local assembly backend.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
else()
|
||||
# Intel macOS follows the same System V AMD64 libco path as Linux.
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
|
||||
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
|
||||
endif()
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
|
||||
add_executable(mkw_macos_external_audio_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_external_audio_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/external_audio_macos.cpp")
|
||||
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
|
||||
endif()
|
||||
@@ -1,57 +1,45 @@
|
||||
# Public WiiCompiled product graph.
|
||||
# Public WiiCompiled product graph.
|
||||
#
|
||||
# The translator owns the translated build graph. Mario Kart's profile-neutral
|
||||
# functions are compiled once into mkw_base_shared; only callers whose direct
|
||||
# ABI differs between profiles receive small base/RR variants.
|
||||
|
||||
# Per-game data the runtime reads but does not own. Kept out of the runtime
|
||||
# library so that library stays game-independent (and can be precompiled).
|
||||
set(MKW_GAME_DATA_SOURCES "")
|
||||
set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp")
|
||||
set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S")
|
||||
if(EXISTS "${DATA_INIT_FILE}")
|
||||
list(APPEND SOURCES "${DATA_INIT_FILE}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_FILE}")
|
||||
endif()
|
||||
# Crash-report symbolization table emitted by generate-data-init. The stub
|
||||
# (deliberately outside the globbed src/ tree so it is never picked up twice)
|
||||
# keeps link succeeding when the generated table has not been produced yet.
|
||||
set(GUEST_SYMBOL_TABLE_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/guest_symbol_table.cpp")
|
||||
if(EXISTS "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
else()
|
||||
list(APPEND SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
|
||||
endif()
|
||||
if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
enable_language(ASM)
|
||||
set_source_files_properties("${DATA_INIT_BLOB_ASM}" PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
list(APPEND SOURCES "${DATA_INIT_BLOB_ASM}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
# Runtime glue that depends on generated/ headers (e.g. RuntimeConfig.h).
|
||||
file(GLOB MKW_GAME_GLUE_SOURCES CONFIGURE_DEPENDS "${MKW_RUNTIME_SOURCE_DIR}/game/*.cpp")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES ${MKW_GAME_GLUE_SOURCES})
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_apply_translated_compile_options target)
|
||||
target_compile_options(${target} PRIVATE
|
||||
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_configure_object_target target)
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_AURORA_DIR}/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
TARGET_PC)
|
||||
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
|
||||
endfunction()
|
||||
|
||||
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
|
||||
# comparatively small. A dedicated Ninja job pool caps how many of them run at once so the
|
||||
# global parallelism can use every core for the cheap TUs without the memory ceiling being a
|
||||
@@ -73,83 +61,64 @@ function(mkw_configure_translated_target target)
|
||||
mkw_bound_translated_compiles(${target})
|
||||
endfunction()
|
||||
|
||||
add_library(mkw_runtime_common OBJECT ${SOURCES})
|
||||
mkw_configure_object_target(mkw_runtime_common)
|
||||
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
mkw_select_runtime_provider()
|
||||
if(MKW_RUNTIME_USE_PREBUILT)
|
||||
mkw_import_prebuilt_runtime(mkw_runtime_prebuilt "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
|
||||
# A replacement archive can carry an older mtime than the last link, so relink on fingerprint change.
|
||||
set(MKW_RUNTIME_PREBUILT_STAMP "${CMAKE_BINARY_DIR}/mkw_runtime_prebuilt.stamp")
|
||||
file(CONFIGURE OUTPUT "${MKW_RUNTIME_PREBUILT_STAMP}" CONTENT "${MKW_NP_RUNTIME_FINGERPRINT}\n")
|
||||
else()
|
||||
mkw_add_runtime_library(mkw_runtime_common OBJECT ${SOURCES})
|
||||
endif()
|
||||
|
||||
add_library(mkw_game_data OBJECT ${MKW_GAME_DATA_SOURCES})
|
||||
mkw_configure_object_target(mkw_game_data)
|
||||
target_compile_features(mkw_game_data PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_game_data PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
target_include_directories(mkw_runtime_common PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
if(MKW_SETUP_VERSION)
|
||||
set_property(SOURCE "${MKW_RUNTIME_SOURCE_DIR}/game/setup_version.cpp" APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "MKW_SETUP_VERSION=\"${MKW_SETUP_VERSION}\"")
|
||||
endif()
|
||||
mkw_runtime_link_dependencies(mkw_game_data PRIVATE)
|
||||
set_target_properties(mkw_game_data PROPERTIES UNITY_BUILD OFF)
|
||||
mkw_apply_common_compile_options(mkw_game_data)
|
||||
|
||||
# Keep runtime unity units small and semantically related. The old generated-TU
|
||||
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
|
||||
foreach(source IN LISTS SOURCES)
|
||||
get_filename_component(source_name "${source}" NAME_WE)
|
||||
string(REPLACE "\\" "/" source_normalized "${source}")
|
||||
if(source_normalized MATCHES "/hle/gx/")
|
||||
set(runtime_group "gx_bridge")
|
||||
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
|
||||
set(runtime_group "network_ios")
|
||||
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
|
||||
set(runtime_group "guest_system")
|
||||
elseif(source_name MATCHES "debug|trace|prof")
|
||||
set(runtime_group "diagnostics")
|
||||
else()
|
||||
string(SHA256 source_hash "${source_name}")
|
||||
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
|
||||
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
|
||||
set(runtime_group "runtime_${runtime_bucket}")
|
||||
endif()
|
||||
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
|
||||
endforeach()
|
||||
# These translation units implement guest-visible floating-point bit
|
||||
# semantics. Keep them out of the fast-math runtime unity groups and apply
|
||||
# the same contraction/rounding policy as translated PPC shards.
|
||||
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp")
|
||||
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
|
||||
SKIP_UNITY_BUILD_INCLUSION ON
|
||||
SKIP_PRECOMPILE_HEADERS ON
|
||||
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
|
||||
set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
# Host ISA guard deliberately keeps the plain baseline ISA and checks the
|
||||
# selected x86 profile before optional instructions can execute. AArch64 has
|
||||
# no equivalent optional ISA floor to probe: NEON/FMA are architectural
|
||||
# requirements.
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(mkw_cpu_baseline PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
endif()
|
||||
|
||||
# Translated code compiles against the guest ABI surface only, so runtime
|
||||
# implementation edits do not invalidate its PCH.
|
||||
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/guest_abi/guest_abi_pch.h")
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_PCH}")
|
||||
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
endif()
|
||||
|
||||
add_library(mkw_base_shared STATIC ${MKW_BASE_COMMON_SHARDS})
|
||||
mkw_configure_translated_target(mkw_base_shared)
|
||||
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_TRANSLATED_PCH}")
|
||||
|
||||
# Registration and indirect-dispatch tables are translator output too; they
|
||||
# compile with the translated configuration rather than inside the products.
|
||||
add_library(mkw_base_registration OBJECT ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
mkw_configure_translated_target(mkw_base_registration)
|
||||
target_precompile_headers(mkw_base_registration REUSE_FROM mkw_base_shared)
|
||||
|
||||
if(MKW_BASE_PORTABLE_SENSITIVE_SHARDS)
|
||||
add_library(mkw_base_sensitive OBJECT ${MKW_BASE_PORTABLE_SENSITIVE_SHARDS})
|
||||
@@ -172,16 +141,31 @@ if(MKW_HAVE_RETRO_REWIND)
|
||||
set_source_files_properties("${source}" PROPERTIES LANGUAGE ASM SKIP_PRECOMPILE_HEADERS ON)
|
||||
endif()
|
||||
endforeach()
|
||||
# mod_data_patches (MKW_RETRO_EXTRA_SOURCES) calls runtime APIs directly; it
|
||||
# keeps the full runtime include path this target already has.
|
||||
add_library(mkw_retro_rewind_functions OBJECT ${MKW_RETRO_TRANSLATED_SOURCES})
|
||||
mkw_configure_translated_target(mkw_retro_rewind_functions)
|
||||
target_precompile_headers(mkw_retro_rewind_functions REUSE_FROM mkw_base_shared)
|
||||
|
||||
add_library(mkw_retro_registration OBJECT ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
mkw_configure_translated_target(mkw_retro_registration)
|
||||
target_precompile_headers(mkw_retro_registration REUSE_FROM mkw_base_shared)
|
||||
endif()
|
||||
|
||||
function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
if(TARGET mkw_runtime_common)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
else()
|
||||
# Natives self-register from static constructors, so every archive
|
||||
# member must be kept. Listed before mkw_base_shared so single-pass
|
||||
# linkers resolve the runtime's references to translated functions.
|
||||
target_link_libraries(${target} PRIVATE "$<LINK_LIBRARY:WHOLE_ARCHIVE,mkw_runtime_prebuilt>")
|
||||
set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${MKW_RUNTIME_PREBUILT_STAMP}")
|
||||
endif()
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_game_data>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
@@ -195,9 +179,6 @@ function(mkw_configure_product target)
|
||||
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||
mkw_apply_common_compile_options(${target})
|
||||
# The dispatch-table and registration shards compile inside the product target itself and
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
|
||||
@@ -205,7 +186,9 @@ function(mkw_configure_product target)
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}" "${MKW_COREAUDIO_FRAMEWORK}")
|
||||
target_link_options(${target} PRIVATE
|
||||
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
|
||||
endif()
|
||||
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
@@ -218,15 +201,15 @@ function(mkw_configure_product target)
|
||||
(MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR
|
||||
MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY"))
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}> VERBATIM)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp bthprops)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
@@ -252,7 +235,7 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}> VERBATIM)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
@@ -261,7 +244,7 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Missing Wii first-run bootstrap payload: ${MKW_WII_BOOTSTRAP_SOURCE_DIR}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
|
||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap" VERBATIM)
|
||||
|
||||
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
|
||||
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
||||
@@ -273,7 +256,7 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Wii DSP coefficient ROM hash mismatch: ${MKW_DSP_COEFFICIENT_ROM_SHA256}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin")
|
||||
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin" VERBATIM)
|
||||
|
||||
# Aurora imports this portable recipe database into each user's writable
|
||||
# pipeline cache. Keep the upstream filename so its default resourcesPath
|
||||
@@ -285,7 +268,7 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db" VERBATIM)
|
||||
|
||||
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
|
||||
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
|
||||
@@ -299,12 +282,13 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem" VERBATIM)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}")
|
||||
mkw_configure_product(WiiCompiled)
|
||||
target_sources(WiiCompiled PRIVATE $<TARGET_OBJECTS:mkw_base_registration>)
|
||||
target_precompile_headers(WiiCompiled PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
if(TARGET mkw_base_sensitive)
|
||||
@@ -312,9 +296,10 @@ if(TARGET mkw_base_sensitive)
|
||||
endif()
|
||||
|
||||
if(MKW_HAVE_RETRO_REWIND)
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
|
||||
mkw_configure_product(RetroRewind)
|
||||
target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled)
|
||||
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_registration>)
|
||||
if(TARGET mkw_retro_sensitive)
|
||||
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_sensitive>)
|
||||
endif()
|
||||
@@ -328,23 +313,12 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
# mkw_runtime_common gets the same flags from mkw_add_runtime_library.
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind)
|
||||
mkw_game_data mkw_base_shared mkw_base_registration mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions mkw_retro_registration WiiCompiled RetroRewind)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
if(TARGET ${target})
|
||||
mkw_apply_baseline_arch(${target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Content identity of the game-independent runtime and the rule deciding whether
|
||||
# a precompiled runtime archive may replace compiling it. Script mode prints
|
||||
# exactly one stdout line, runtime_fingerprint=<sha256 hex>:
|
||||
# cmake -DMKW_RUNTIME_DIR=<abs runtime dir> [-DMKW_CPPWINRT_INCLUDE_DIR=<dir>]
|
||||
# [-DMKW_X86_CPU_PROFILE=v2|v3] [-DCMAKE_OSX_DEPLOYMENT_TARGET=<ver>]
|
||||
# -P runtime/cmake/RuntimeFingerprint.cmake
|
||||
#
|
||||
# Inputs: src/, include/, cmake/ and CMakeLists.txt (sorted relative paths and
|
||||
# content hashes), winrt/base.h, the CPU profile and the macOS deployment target.
|
||||
# third_party/ has its own package fingerprint; game/ is per-user. Dot-files
|
||||
# differ between machines.
|
||||
|
||||
function(mkw_runtime_fingerprint_inputs out_var runtime_dir)
|
||||
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
|
||||
file(GLOB_RECURSE _files LIST_DIRECTORIES false RELATIVE "${_root}"
|
||||
"${_root}/src/*" "${_root}/include/*" "${_root}/cmake/*")
|
||||
list(FILTER _files EXCLUDE REGEX "(^|/)\\.")
|
||||
list(APPEND _files "CMakeLists.txt")
|
||||
list(SORT _files COMPARE STRING CASE SENSITIVE)
|
||||
set(${out_var} "${_files}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(mkw_runtime_fingerprint out_var runtime_dir cppwinrt_dir cpu_profile)
|
||||
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
|
||||
mkw_runtime_fingerprint_inputs(_files "${_root}")
|
||||
set(_manifest "")
|
||||
foreach(_file IN LISTS _files)
|
||||
if(NOT EXISTS "${_root}/${_file}")
|
||||
message(FATAL_ERROR "Runtime fingerprint input is missing: ${_root}/${_file}")
|
||||
endif()
|
||||
file(SHA256 "${_root}/${_file}" _hash)
|
||||
string(APPEND _manifest "${_file} ${_hash}\n")
|
||||
endforeach()
|
||||
if(cppwinrt_dir)
|
||||
if(NOT EXISTS "${cppwinrt_dir}/winrt/base.h")
|
||||
message(FATAL_ERROR "MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${cppwinrt_dir}")
|
||||
endif()
|
||||
file(SHA256 "${cppwinrt_dir}/winrt/base.h" _hash)
|
||||
string(APPEND _manifest "cppwinrt:winrt/base.h ${_hash}\n")
|
||||
endif()
|
||||
string(APPEND _manifest "x86_cpu_profile ${cpu_profile}\n")
|
||||
string(APPEND _manifest "osx_deployment_target ${CMAKE_OSX_DEPLOYMENT_TARGET}\n")
|
||||
string(SHA256 _fingerprint "${_manifest}")
|
||||
set(${out_var} "${_fingerprint}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Sets out_use to TRUE/FALSE and out_reason to a one-line explanation. Pure, so
|
||||
# it can be exercised in script mode without a configured project.
|
||||
cmake_policy(PUSH)
|
||||
if(POLICY CMP0174)
|
||||
cmake_policy(SET CMP0174 NEW)
|
||||
endif()
|
||||
function(mkw_runtime_prebuilt_decision out_use out_reason)
|
||||
cmake_parse_arguments(PARSE_ARGV 2 _d ""
|
||||
"ARCHIVE;ENABLED;X86_64;ARCHIVE_EXISTS;PACKAGE_PROFILE;PROFILE;PACKAGE_FINGERPRINT;FINGERPRINT" "")
|
||||
set(_use FALSE)
|
||||
if(NOT _d_ARCHIVE)
|
||||
set(_reason "the native prebuilt package has no runtime archive")
|
||||
elseif(NOT _d_ENABLED)
|
||||
set(_reason "MKW_USE_PREBUILT_RUNTIME is OFF")
|
||||
elseif(NOT _d_X86_64)
|
||||
set(_reason "the precompiled runtime is x86_64-only")
|
||||
elseif(NOT _d_ARCHIVE_EXISTS)
|
||||
set(_reason "the runtime archive is missing: ${_d_ARCHIVE}")
|
||||
elseif(NOT _d_PACKAGE_PROFILE STREQUAL _d_PROFILE)
|
||||
set(_reason "the package was built for x86-64-${_d_PACKAGE_PROFILE}, this build targets x86-64-${_d_PROFILE}")
|
||||
elseif(NOT _d_PACKAGE_FINGERPRINT)
|
||||
set(_reason "the package records no runtime fingerprint")
|
||||
elseif(NOT _d_FINGERPRINT OR NOT _d_PACKAGE_FINGERPRINT STREQUAL _d_FINGERPRINT)
|
||||
set(_reason "runtime sources differ from the package (package ${_d_PACKAGE_FINGERPRINT}, workspace ${_d_FINGERPRINT})")
|
||||
else()
|
||||
set(_use TRUE)
|
||||
set(_reason "fingerprint ${_d_FINGERPRINT} matches")
|
||||
endif()
|
||||
set(${out_use} ${_use} PARENT_SCOPE)
|
||||
set(${out_reason} "${_reason}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
cmake_policy(POP)
|
||||
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
get_filename_component(_mkw_script "${CMAKE_SCRIPT_MODE_FILE}" REALPATH)
|
||||
get_filename_component(_mkw_self "${CMAKE_CURRENT_LIST_FILE}" REALPATH)
|
||||
endif()
|
||||
if(CMAKE_SCRIPT_MODE_FILE AND _mkw_script STREQUAL _mkw_self)
|
||||
if(NOT MKW_RUNTIME_DIR OR NOT IS_DIRECTORY "${MKW_RUNTIME_DIR}")
|
||||
message(FATAL_ERROR "Pass -DMKW_RUNTIME_DIR=<runtime directory>")
|
||||
endif()
|
||||
if(NOT MKW_X86_CPU_PROFILE)
|
||||
set(MKW_X86_CPU_PROFILE "v3")
|
||||
endif()
|
||||
# Same default runtime/CMakeLists.txt forces on every platform before project().
|
||||
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
|
||||
endif()
|
||||
mkw_runtime_fingerprint(_mkw_fingerprint "${MKW_RUNTIME_DIR}" "${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
# message() writes to stderr; the echo child inherits stdout.
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "runtime_fingerprint=${_mkw_fingerprint}")
|
||||
endif()
|
||||
@@ -0,0 +1,180 @@
|
||||
# The game-independent native runtime (runtime/src). One definition serves
|
||||
# three uses: the OBJECT library the products link from source, the STATIC
|
||||
# archive the native prebuilt export ships (mkw_runtime), and that archive
|
||||
# imported in place of compiling it (mkw_runtime_prebuilt).
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/RuntimeFingerprint.cmake")
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_HOST_IS_X86_64 TRUE)
|
||||
else()
|
||||
set(MKW_HOST_IS_X86_64 FALSE)
|
||||
endif()
|
||||
|
||||
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
|
||||
# builds are compiled locally for the host that will run them, so both Linux
|
||||
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
|
||||
# target-specific performance on the table.
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
|
||||
endif()
|
||||
|
||||
# CMake decides WHOLE_ARCHIVE's form by probing ld.lld in ELF mode, which
|
||||
# accepts --push-state; the MinGW lld driver rejects it at link time.
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(CMAKE_CXX_LINK_LIBRARY_USING_WHOLE_ARCHIVE
|
||||
"LINKER:--whole-archive" "<LINK_ITEM>" "LINKER:--no-whole-archive")
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_apply_baseline_arch target)
|
||||
if(MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(mkw_configure_object_target target)
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_AURORA_DIR}/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
TARGET_PC)
|
||||
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
|
||||
endfunction()
|
||||
|
||||
# scope is PRIVATE for a compiled target and INTERFACE for the imported archive.
|
||||
function(mkw_runtime_link_dependencies target scope)
|
||||
target_link_libraries(${target} ${scope}
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp bthprops)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
target_link_libraries(${target} ${scope} mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# CoreAudio framework is required for automatic music muting on macOS.
|
||||
target_link_libraries(${target} ${scope} "${MKW_COREAUDIO_FRAMEWORK}")
|
||||
if(MKW_PLATFORM_MACOS_X86_64)
|
||||
# libco is used by Intel macOS. Apple Silicon uses the local
|
||||
# x18-safe assembly backend and therefore does not define mkw::libco.
|
||||
target_link_libraries(${target} ${scope} mkw::libco)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Compiles the runtime sources as an OBJECT or STATIC library.
|
||||
function(mkw_add_runtime_library target type)
|
||||
set(_sources ${ARGN})
|
||||
add_library(${target} ${type} ${_sources})
|
||||
mkw_configure_object_target(${target})
|
||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
mkw_runtime_link_dependencies(${target} PRIVATE)
|
||||
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
# Keep runtime unity units small and semantically related. The old generated-TU
|
||||
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
|
||||
foreach(source IN LISTS _sources)
|
||||
get_filename_component(source_name "${source}" NAME_WE)
|
||||
string(REPLACE "\\" "/" source_normalized "${source}")
|
||||
if(source_normalized MATCHES "/hle/gx/")
|
||||
set(runtime_group "gx_bridge")
|
||||
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
|
||||
set(runtime_group "network_ios")
|
||||
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
|
||||
set(runtime_group "guest_system")
|
||||
elseif(source_name MATCHES "debug|trace|prof")
|
||||
set(runtime_group "diagnostics")
|
||||
else()
|
||||
string(SHA256 source_hash "${source_name}")
|
||||
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
|
||||
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
|
||||
set(runtime_group "runtime_${runtime_bucket}")
|
||||
endif()
|
||||
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
|
||||
endforeach()
|
||||
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
|
||||
set_target_properties(${target} PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
|
||||
target_precompile_headers(${target} PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(${target})
|
||||
mkw_apply_baseline_arch(${target})
|
||||
endfunction()
|
||||
|
||||
function(mkw_import_prebuilt_runtime target archive)
|
||||
add_library(${target} STATIC IMPORTED GLOBAL)
|
||||
set_target_properties(${target} PROPERTIES IMPORTED_LOCATION "${archive}")
|
||||
mkw_runtime_link_dependencies(${target} INTERFACE)
|
||||
endfunction()
|
||||
|
||||
# Sets MKW_RUNTIME_USE_PREBUILT in the caller. The archive replaces the
|
||||
# runtime's objects only when it was built from exactly this workspace's
|
||||
# runtime inputs for the same x86-64 profile.
|
||||
function(mkw_select_runtime_provider)
|
||||
set(_archive "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
|
||||
set(_exists FALSE)
|
||||
if(_archive AND EXISTS "${_archive}")
|
||||
set(_exists TRUE)
|
||||
endif()
|
||||
set(_fingerprint "")
|
||||
if(_exists AND MKW_USE_PREBUILT_RUNTIME AND MKW_HOST_IS_X86_64 AND
|
||||
MKW_NP_RUNTIME_CPU_PROFILE STREQUAL MKW_X86_CPU_PROFILE)
|
||||
mkw_runtime_fingerprint(_fingerprint "${MKW_RUNTIME_SOURCE_DIR}"
|
||||
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
endif()
|
||||
mkw_runtime_prebuilt_decision(_use _reason
|
||||
ARCHIVE "${_archive}"
|
||||
ENABLED "${MKW_USE_PREBUILT_RUNTIME}"
|
||||
X86_64 "${MKW_HOST_IS_X86_64}"
|
||||
ARCHIVE_EXISTS "${_exists}"
|
||||
PACKAGE_PROFILE "${MKW_NP_RUNTIME_CPU_PROFILE}"
|
||||
PROFILE "${MKW_X86_CPU_PROFILE}"
|
||||
PACKAGE_FINGERPRINT "${MKW_NP_RUNTIME_FINGERPRINT}"
|
||||
FINGERPRINT "${_fingerprint}")
|
||||
if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
set(_reason "no native prebuilt package is configured")
|
||||
endif()
|
||||
|
||||
if(_use)
|
||||
message(STATUS "Native runtime: precompiled archive ${_archive} (${_reason})")
|
||||
# Any runtime edit must re-run configure so the mismatch is noticed.
|
||||
mkw_runtime_fingerprint_inputs(_inputs "${MKW_RUNTIME_SOURCE_DIR}")
|
||||
list(TRANSFORM _inputs PREPEND "${MKW_RUNTIME_SOURCE_DIR}/")
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
list(APPEND _inputs "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
endif()
|
||||
set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_inputs})
|
||||
else()
|
||||
message(STATUS "Native runtime: compiled from source (${_reason})")
|
||||
endif()
|
||||
set(MKW_RUNTIME_USE_PREBUILT ${_use} PARENT_SCOPE)
|
||||
endfunction()
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "guest_small_data.h"
|
||||
|
||||
#include "generated/RuntimeConfig.h"
|
||||
|
||||
namespace GuestSmallData {
|
||||
extern const uint32_t kSda1Base = RuntimeConfig::SDA1_BASE;
|
||||
extern const uint32_t kSda2Base = RuntimeConfig::SDA2_BASE;
|
||||
} // namespace GuestSmallData
|
||||
@@ -0,0 +1,7 @@
|
||||
// Compiled per user rather than into the shared runtime archive.
|
||||
extern const char* const kMkwSetupVersion =
|
||||
#if defined(MKW_SETUP_VERSION)
|
||||
MKW_SETUP_VERSION;
|
||||
#else
|
||||
"";
|
||||
#endif
|
||||
@@ -18,638 +18,14 @@
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
|
||||
#include "guest_abi/dispatch.h"
|
||||
|
||||
// Some game-facing runtime options alter arguments at well-defined ABI
|
||||
// boundaries. Keep this independent of the dispatch mechanism: generated
|
||||
// static calls deliberately bypass InvokeDirectCpu for performance.
|
||||
// (used for the path mask filtering in ScnRenderer::createPath: depth of
|
||||
// field is always removed, bloom when the user disabled it)
|
||||
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
|
||||
if (target == 0x8023BD38u) {
|
||||
// ScnRenderer::createPath receives the post-processing path mask in r4.
|
||||
ctx->gpr[4] = RuntimeGameGraphicsOptions::FilterScnRendererPathMask(ctx->gpr[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent per-thread CPU context used across translated function calls.
|
||||
CpuContext& GetPersistentCpuContext();
|
||||
void InitializePersistentCpuContext();
|
||||
|
||||
enum class FunctionKind : uint8_t {
|
||||
BaseTranslated = 0,
|
||||
ModTranslated = 1,
|
||||
Native = 2,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
|
||||
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
|
||||
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
|
||||
inline constexpr uint32_t kNativeFunctionPriority = 10000;
|
||||
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
|
||||
struct TranslatedFunctionInfo {
|
||||
uint32_t address = 0;
|
||||
const char* name = "";
|
||||
uint64_t moduleId = 0;
|
||||
uint32_t priority = 0;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
|
||||
void (*rawCpuInvoker)(CpuContext*) = nullptr;
|
||||
bool mustRemainDynamicallyDispatchable = true;
|
||||
FunctionKind kind = FunctionKind::BaseTranslated;
|
||||
};
|
||||
|
||||
struct RawDispatchRecord {
|
||||
uint32_t address = 0;
|
||||
void (*entry)(CpuContext*) = nullptr;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
bool preserveNonvolatileGprs = false;
|
||||
};
|
||||
|
||||
// Translator-owned indirect dispatch is split by the high address byte and
|
||||
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
|
||||
// page range that contains verified function entry points. The final search is
|
||||
// bounded to at most the 1024 aligned instruction addresses in one guest page.
|
||||
struct StaticIndirectDispatchPage {
|
||||
uint32_t firstEntry = 0;
|
||||
uint16_t entryCount = 0;
|
||||
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
|
||||
// literal third initializer, so it is part of the generated-data contract.
|
||||
uint16_t reserved = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchSegment {
|
||||
const StaticIndirectDispatchPage* pages = nullptr;
|
||||
uint16_t firstPage = 0;
|
||||
uint16_t pageCount = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchTable {
|
||||
const char* profileName = nullptr;
|
||||
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
|
||||
const RawDispatchRecord* entries = nullptr;
|
||||
size_t entryCount = 0;
|
||||
};
|
||||
|
||||
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
|
||||
const StaticIndirectDispatchTable* table,
|
||||
uint32_t address) noexcept {
|
||||
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& segment = table->segments[address >> 24];
|
||||
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
|
||||
if (!segment.pages || pageNumber < segment.firstPage) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t relativePage = pageNumber - segment.firstPage;
|
||||
if (relativePage >= segment.pageCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& page = segment.pages[relativePage];
|
||||
const size_t first = page.firstEntry;
|
||||
const size_t count = page.entryCount;
|
||||
if (first > table->entryCount || count > table->entryCount - first) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t lower = first;
|
||||
size_t upper = first + count;
|
||||
while (lower < upper) {
|
||||
const size_t middle = lower + (upper - lower) / 2;
|
||||
const uint32_t candidate = table->entries[middle].address;
|
||||
if (candidate < address) {
|
||||
lower = middle + 1;
|
||||
} else {
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
return lower < first + count && table->entries[lower].address == address
|
||||
? &table->entries[lower]
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
|
||||
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
|
||||
|
||||
class StaticIndirectDispatchTableRegistrar {
|
||||
public:
|
||||
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
|
||||
RegisterStaticIndirectDispatchTable(table);
|
||||
}
|
||||
};
|
||||
|
||||
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
|
||||
return static_cast<unsigned>(__builtin_ctz(value));
|
||||
}
|
||||
|
||||
class PpcNonvolatileFprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
|
||||
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
// Real masks are a single contiguous register run - f14..f31 for the
|
||||
// conservative default and f23..f31 or similar for a measured writer -
|
||||
// so resolve the run once and copy it as one straight-line block instead
|
||||
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
|
||||
// saves and restores exactly the same registers.
|
||||
const uint32_t lowest = mask_ & (0u - mask_);
|
||||
const uint32_t aboveRun = mask_ + lowest;
|
||||
if ((aboveRun & (aboveRun - 1u)) == 0u) {
|
||||
const unsigned first = PpcLowestSetBitIndex(mask_);
|
||||
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
|
||||
firstSlot_ = static_cast<uint8_t>(first - 14u);
|
||||
slotCount_ = static_cast<uint8_t>(end - first);
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
saved_[i] = cpu_->fpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileFprGuard() noexcept {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (slotCount_ != 0) {
|
||||
const unsigned first = 14u + firstSlot_;
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
cpu_->fpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
|
||||
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
uint32_t mask_ = 0;
|
||||
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
|
||||
// a single run, so the destructor mirrors the constructor's per-bit loop.
|
||||
uint8_t firstSlot_ = 0;
|
||||
uint8_t slotCount_ = 0;
|
||||
// Every element selected by mask_ is written before it is read. Leaving the
|
||||
// remaining slots uninitialized avoids zero-filling all 18 values around a
|
||||
// call that may preserve only one or two architectural FPRs.
|
||||
std::array<PPC_FPR, 18> saved_;
|
||||
};
|
||||
|
||||
class PpcNonvolatileGprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
|
||||
: cpu_(enabled ? cpu : nullptr) {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
saved_[i] = cpu_->gpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileGprGuard() noexcept {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
cpu_->gpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
|
||||
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
|
||||
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
|
||||
std::array<uint32_t, 18> saved_;
|
||||
};
|
||||
|
||||
// Guest address of OSLoadContext, which models rfi-style context restoration
|
||||
// and intentionally replaces the full guest register file instead of returning
|
||||
// like a normal ABI call. It can never be guarded.
|
||||
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
|
||||
|
||||
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
|
||||
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
|
||||
}
|
||||
|
||||
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
|
||||
if (!info || info->nonvolatileFprWriteMask == 0) {
|
||||
return 0;
|
||||
}
|
||||
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTranslatedCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = true;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
// Translator-emitted trait specializations, one macro shared by every generated shard.
|
||||
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
|
||||
// publish their own name). A specialization only exists for a single statically bound winner,
|
||||
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
|
||||
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
|
||||
extern "C" void winner(CpuContext* ctx); \
|
||||
template <> \
|
||||
struct KnownTranslatedCpuCall<0x##addr##u> { \
|
||||
static constexpr bool kAvailable = true; \
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
|
||||
static constexpr void (*Entry)(CpuContext*) = &winner; \
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTypedNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
};
|
||||
|
||||
#include "native_cpu_calls.inc"
|
||||
|
||||
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
|
||||
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
|
||||
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
|
||||
// power of two, the index masks with (size - 1).
|
||||
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
|
||||
|
||||
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
|
||||
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in ppc_runtime.h).
|
||||
struct IndirectResolvedDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const TranslatedFunctionInfo* info;
|
||||
};
|
||||
inline thread_local IndirectResolvedDispatchMemoEntry
|
||||
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
struct IndirectRawDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const RawDispatchRecord* record;
|
||||
};
|
||||
inline thread_local IndirectRawDispatchMemoEntry
|
||||
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
class TranslatedFunctionRegistry {
|
||||
public:
|
||||
static void Register(TranslatedFunctionInfo info);
|
||||
static void Finalize();
|
||||
static inline bool IsLookupPublished() noexcept {
|
||||
return lookupPublished_.load(std::memory_order_acquire);
|
||||
}
|
||||
static std::optional<TranslatedFunctionInfo> FindNearestByAddress(uint32_t address);
|
||||
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return FindByAddressPtrSlow(address);
|
||||
}
|
||||
|
||||
auto& cached = g_indirectResolvedDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.info;
|
||||
}
|
||||
|
||||
const auto* info = FindByAddressPtrSlow(address);
|
||||
cached = {true, address, info};
|
||||
return info;
|
||||
}
|
||||
|
||||
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
|
||||
// generated-table binary search alongside it pushed the whole function past
|
||||
// the inliner's budget and out-of-lined the probe itself, which is the part
|
||||
// every bctrl executes.
|
||||
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& cached = g_indirectRawDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.record;
|
||||
}
|
||||
|
||||
return FindRawByAddressPtrMiss(address, cached);
|
||||
}
|
||||
// Look up a registered function by host (native) address.
|
||||
// This enables stack trace symbolication for translated functions.
|
||||
static std::optional<TranslatedFunctionInfo> FindByHostAddress(uintptr_t hostAddr);
|
||||
|
||||
private:
|
||||
// Memo miss: resolve against the header-inline generated table first; only dynamically
|
||||
// registered records or a not-yet-published table fall through to the slow path.
|
||||
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
|
||||
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
|
||||
const RawDispatchRecord* record = nullptr;
|
||||
if (const auto* table =
|
||||
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
|
||||
record = FindStaticIndirectDispatchEntry(table, address);
|
||||
}
|
||||
if (record == nullptr) {
|
||||
record = FindRawByAddressPtrSlow(address);
|
||||
}
|
||||
cached = {true, address, record};
|
||||
return record;
|
||||
}
|
||||
|
||||
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
|
||||
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
|
||||
|
||||
// Finalize publishes immutable registry, generated-table, and dynamic raw
|
||||
// dispatch storage. Header-inlined cache hits are enabled only after that
|
||||
// release publication, so a pre-finalization miss can never poison them.
|
||||
inline static std::atomic_bool lookupPublished_{false};
|
||||
};
|
||||
|
||||
// Cold, profile-owned registration data is emitted in compact table shards.
|
||||
// Keeping it out of generated function bodies avoids one static constructor
|
||||
// per translated function.
|
||||
struct BulkTranslatedFunctionRecord {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
void (*entry)(CpuContext*);
|
||||
FunctionKind kind;
|
||||
bool preservesNonvolatileFprs;
|
||||
uint32_t nonvolatileFprWriteMask;
|
||||
uint32_t priority;
|
||||
uint64_t moduleId;
|
||||
bool mustRemainDynamicallyDispatchable;
|
||||
};
|
||||
|
||||
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
|
||||
|
||||
class BulkTranslatedFunctionRegistrar {
|
||||
public:
|
||||
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
|
||||
RegisterBulkTranslatedFunctions(records, count);
|
||||
}
|
||||
};
|
||||
|
||||
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
|
||||
if (!info || !info->rawCpuInvoker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
|
||||
if (!record || !record->entry) {
|
||||
return false;
|
||||
}
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
record->entry(cpu);
|
||||
} else {
|
||||
record->entry(cpu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
|
||||
const auto invokeKnownTranslated = [&]() {
|
||||
KnownTranslatedCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
|
||||
// A statically resolved, non-overridable translated call already running
|
||||
// in this CpuContext needs no registry, context, or diagnostic target
|
||||
// transition. This is the normal generated-to-generated hot path.
|
||||
if (TryGetCpuContext() == cpu) {
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(
|
||||
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CpuContextScope contextScope(cpu);
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
|
||||
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
|
||||
// in the run's log folder, popup, exit.
|
||||
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
|
||||
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
|
||||
// Only latch a result once the registry is published (immutable for the process lifetime);
|
||||
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
|
||||
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
|
||||
static std::atomic<uintptr_t> cachedValue{0};
|
||||
constexpr uintptr_t kNegativeCache = 1;
|
||||
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kNegativeCache
|
||||
? nullptr
|
||||
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
|
||||
return ResolveDirectCpuTargetInfo<Target>();
|
||||
}
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
cachedValue.store(info != nullptr
|
||||
? reinterpret_cast<uintptr_t>(info)
|
||||
: kNegativeCache,
|
||||
std::memory_order_release);
|
||||
return info;
|
||||
}
|
||||
|
||||
// Caches the "is the published winner still base translation?" verdict for state-free fast
|
||||
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
|
||||
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
|
||||
template <uint32_t Target>
|
||||
inline bool IsBaseTranslatedCpuTargetActive() {
|
||||
static std::atomic<uint8_t> cachedVerdict{0};
|
||||
constexpr uint8_t kNegativeVerdict = 1;
|
||||
constexpr uint8_t kPositiveVerdict = 2;
|
||||
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kPositiveVerdict;
|
||||
}
|
||||
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
|
||||
if (publishable) {
|
||||
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
|
||||
std::memory_order_release);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void InvokeDirectCpu(CpuContext* ctx) {
|
||||
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
ApplyRuntimeCallOptions(Target, cpu);
|
||||
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
|
||||
const auto invokeKnownNative = [&]() {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
return;
|
||||
}
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownNative();
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownNative();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
} else {
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
|
||||
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
|
||||
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportMissingCpuTarget(Target, cpu);
|
||||
}
|
||||
|
||||
// Indirect jump (bctr without link) - used for switch tables.
|
||||
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
|
||||
// Use the caller's context, not the persistent global context!
|
||||
// This is essential for tail calls via bctr where the 'this' pointer (r3)
|
||||
// must be passed correctly to the target function.
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cold fallback for dynamically registered or signature-only targets.
|
||||
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
|
||||
if (info) {
|
||||
if (TryDispatchResolvedCpuTarget(info, cpu)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If not registered, this is likely a switch table jump to an intra-function label.
|
||||
// The translator should have recognized this pattern and generated proper switch code.
|
||||
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
|
||||
<< std::hex << target << std::dec
|
||||
<< " is not a registered function.\n"
|
||||
<< "This is likely a switch statement jump table that the translator\n"
|
||||
<< "failed to recognize. The function containing this bctr needs\n"
|
||||
<< "proper switch pattern recognition." << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
|
||||
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
|
||||
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
|
||||
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
|
||||
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
|
||||
DumpHostStackTraceForRuntimeHelper();
|
||||
std::fflush(stderr);
|
||||
std::ostringstream message;
|
||||
message << "The game stopped because an indirect jump targeted guest address 0x"
|
||||
<< std::hex << target
|
||||
<< ", but that address is not a registered translated function.\n\n"
|
||||
<< "The translator may have missed a switch/jump-table target.";
|
||||
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
|
||||
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
|
||||
// atexit reporter, so the crash log has to be written here.
|
||||
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
|
||||
SetRuntimeExitCode(EXIT_FAILURE);
|
||||
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
|
||||
MarkFatalErrorReported();
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (target == 0) {
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
ApplyRuntimeCallOptions(target, cpu);
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address);
|
||||
// Look up a registered function by host (native) address.
|
||||
// This enables stack trace symbolication for translated functions.
|
||||
std::optional<TranslatedFunctionInfo> FindTranslatedFunctionByHostAddress(uintptr_t hostAddr);
|
||||
|
||||
template <typename Signature>
|
||||
class AbiTrampoline;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// HCI and L2CAP fields are little-endian.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
inline uint16_t Le16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>(p[0] | (p[1] << 8));
|
||||
}
|
||||
|
||||
inline void PutLe16(std::vector<uint8_t>& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
// Emulated Bluetooth controller behind the Wii's internal USB module
|
||||
// (/dev/usb/oh1/57e/305). The game's own Broadcom stack (BTE), WUD, WPAD and KPAD
|
||||
// talk HCI to it; each of the four virtual Wii Remotes it hosts is backed by a
|
||||
// physical remote on the PC (see RemoteHost), whose HID reports are passed
|
||||
// through unchanged. Pure host code: no guest memory, so it is unit-testable.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
// Bluetooth device address in HCI (least significant byte first) order.
|
||||
using BdAddr = std::array<uint8_t, 6>;
|
||||
|
||||
inline constexpr size_t kRemoteSlots = 4;
|
||||
|
||||
enum class LinkState : uint8_t {
|
||||
Down, // no baseband connection to the game
|
||||
Connecting, // baseband up, HID channels being opened
|
||||
Ready, // HID control and interrupt channels open
|
||||
};
|
||||
|
||||
// The physical side of the virtual remotes. Called from the thread that drives
|
||||
// the Controller; implementations synchronize with their own I/O threads.
|
||||
class RemoteHost {
|
||||
public:
|
||||
virtual ~RemoteHost() = default;
|
||||
// A physical remote is attached to `slot` and can carry a connection.
|
||||
virtual bool IsAttached(size_t slot) const = 0;
|
||||
// True once each time the remote on `slot` asks to connect (it was just
|
||||
// attached, or a button was pressed while it had no connection).
|
||||
virtual bool ConsumeWake(size_t slot) = 0;
|
||||
// Next input report (report id first, no HID header), false when none.
|
||||
virtual bool PopInputReport(size_t slot, std::vector<uint8_t>& report) = 0;
|
||||
// Output report from the game (report id first, no HID header).
|
||||
virtual void SendOutputReport(size_t slot, const uint8_t* report, size_t size) = 0;
|
||||
virtual void OnLinkStateChanged(size_t slot, LinkState state) = 0;
|
||||
// The game ended a working connection; a real remote powers off here.
|
||||
virtual void OnGuestDisconnect(size_t slot) = 0;
|
||||
// The game is searching for remotes (its 1+2 / reconnect screens).
|
||||
virtual void OnGuestInquiry(bool active) = 0;
|
||||
// "Nintendo RVL-CNT-01" or "Nintendo RVL-CNT-01-TR".
|
||||
virtual const char* DeviceName(size_t slot) const = 0;
|
||||
};
|
||||
|
||||
class RemoteLink;
|
||||
|
||||
class Controller {
|
||||
public:
|
||||
explicit Controller(RemoteHost& host);
|
||||
~Controller();
|
||||
Controller(const Controller&) = delete;
|
||||
Controller& operator=(const Controller&) = delete;
|
||||
|
||||
// The guest opened / closed the USB device.
|
||||
void Open(uint64_t nowUs);
|
||||
void Close();
|
||||
bool IsOpen() const { return m_open; }
|
||||
|
||||
// Raw HCI command packet (opcode first, no H4 byte).
|
||||
void ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Raw ACL packet from the guest (handle first).
|
||||
void ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Advances timers, connection attempts, input forwarding and flow control.
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
bool HasEvent() const { return !m_events.empty(); }
|
||||
bool PopEvent(std::vector<uint8_t>& event);
|
||||
// ACL data is only handed out once every queued event has been delivered,
|
||||
// so the guest always sees a connection before data on it.
|
||||
bool PopAcl(std::vector<uint8_t>& packet);
|
||||
size_t AclQueued() const { return m_acl.size(); }
|
||||
|
||||
LinkState SlotLinkState(size_t slot) const;
|
||||
bool PageScanEnabled() const { return (m_scanEnable & 0x02) != 0; }
|
||||
bool InquiryActive() const { return m_inquiry.active; }
|
||||
|
||||
static BdAddr LocalAddress();
|
||||
static BdAddr RemoteAddress(size_t slot);
|
||||
static uint16_t ConnectionHandle(size_t slot);
|
||||
|
||||
// Used by RemoteLink.
|
||||
void QueueEvent(std::vector<uint8_t> event);
|
||||
void QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame);
|
||||
RemoteHost& Host() { return m_host; }
|
||||
uint32_t PageTimeoutUs() const { return static_cast<uint32_t>(m_pageTimeoutSlots) * 625u; }
|
||||
|
||||
private:
|
||||
struct StoredKey {
|
||||
BdAddr addr{};
|
||||
std::array<uint8_t, 16> key{};
|
||||
};
|
||||
struct Inquiry {
|
||||
bool active = false;
|
||||
uint64_t endUs = 0;
|
||||
uint8_t maxResponses = 0;
|
||||
uint8_t reported = 0;
|
||||
std::array<bool, kRemoteSlots> slotReported{};
|
||||
};
|
||||
struct Reassembly {
|
||||
std::vector<uint8_t> frame;
|
||||
size_t expected = 0;
|
||||
};
|
||||
// A reply the controller sends after a delay (page / name request timeouts).
|
||||
struct PendingEvent {
|
||||
uint64_t dueUs = 0;
|
||||
std::vector<uint8_t> event;
|
||||
};
|
||||
|
||||
void ResetState(bool keepStoredKeys);
|
||||
void CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params);
|
||||
void CommandStatus(uint16_t opcode, uint8_t status);
|
||||
void TickInquiry(uint64_t nowUs);
|
||||
void FinishInquiry();
|
||||
void SendNumberOfCompletedPackets();
|
||||
int SlotForAddress(const BdAddr& addr) const;
|
||||
int SlotForHandle(uint16_t handle) const;
|
||||
const StoredKey* FindStoredKey(const BdAddr& addr) const;
|
||||
|
||||
void HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleInformational(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleVendor(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void UnknownCommand(uint16_t opcode);
|
||||
|
||||
RemoteHost& m_host;
|
||||
bool m_open = false;
|
||||
uint8_t m_scanEnable = 0;
|
||||
uint16_t m_pageTimeoutSlots = 0x2000;
|
||||
uint8_t m_inquiryMode = 0;
|
||||
std::array<uint8_t, 3> m_classOfDevice{};
|
||||
std::array<char, 248> m_localName{};
|
||||
Inquiry m_inquiry;
|
||||
bool m_hostInquiryNotified = false;
|
||||
std::vector<StoredKey> m_storedKeys;
|
||||
std::array<std::unique_ptr<RemoteLink>, kRemoteSlots> m_links;
|
||||
std::array<uint16_t, kRemoteSlots> m_completedPackets{};
|
||||
std::array<Reassembly, kRemoteSlots> m_reassembly;
|
||||
std::array<bool, kRemoteSlots> m_authPending{};
|
||||
std::vector<PendingEvent> m_pending;
|
||||
std::deque<std::vector<uint8_t>> m_events;
|
||||
std::deque<std::vector<uint8_t>> m_acl;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
// One emulated Wii Remote as seen from the controller: its baseband connection,
|
||||
// L2CAP channels (SDP, HID control, HID interrupt) and the HID transaction layer
|
||||
// that carries the physical remote's reports.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
class RemoteLink {
|
||||
public:
|
||||
enum class Baseband : uint8_t {
|
||||
Idle,
|
||||
Requesting, // Connection Request sent, waiting for the guest to accept
|
||||
Paging, // the guest paged this remote (Create Connection)
|
||||
Connected,
|
||||
};
|
||||
|
||||
RemoteLink(Controller& controller, size_t slot);
|
||||
|
||||
// Drops every connection without telling the guest (controller reset/close).
|
||||
void Reset();
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
// Remote side wants a connection and should be offered to the guest's inquiry.
|
||||
bool Discoverable() const;
|
||||
bool ConnectWanted() const { return m_connectWanted; }
|
||||
|
||||
// Guest accepted / rejected this remote's Connection Request.
|
||||
bool IsRequesting() const { return m_baseband == Baseband::Requesting; }
|
||||
void OnAccepted(uint8_t role);
|
||||
void OnRejected(uint8_t reason, uint64_t nowUs);
|
||||
// Guest paged this remote. Returns false when the remote cannot answer.
|
||||
bool OnPaged(uint64_t nowUs);
|
||||
// Baseband went down. `byGuest` = the guest's HCI Disconnect.
|
||||
void OnDisconnected(bool byGuest, uint64_t nowUs);
|
||||
// The physical remote vanished while connected: tells the guest.
|
||||
void DropFromRemote(uint8_t reason);
|
||||
|
||||
void ReceiveL2cap(const uint8_t* frame, size_t size);
|
||||
|
||||
Baseband BasebandState() const { return m_baseband; }
|
||||
LinkState State() const { return m_state; }
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
uint16_t psm = 0;
|
||||
uint16_t localCid = 0;
|
||||
uint16_t remoteCid = 0;
|
||||
uint16_t remoteMtu = 672;
|
||||
bool weInitiated = false;
|
||||
bool connected = false; // connection response success seen/sent
|
||||
bool configSent = false; // our Configuration Request is out
|
||||
bool localConfigured = false; // the guest accepted our configuration
|
||||
bool remoteConfigured = false; // we accepted the guest's configuration
|
||||
uint8_t connectIdent = 0;
|
||||
uint8_t configIdent = 0;
|
||||
uint64_t requestUs = 0;
|
||||
std::vector<uint8_t> pendingOptions; // continued Configuration Request
|
||||
bool Open() const { return connected && localConfigured && remoteConfigured; }
|
||||
};
|
||||
|
||||
Channel* FindByLocal(uint16_t cid);
|
||||
Channel* FindByPsm(uint16_t psm);
|
||||
uint16_t AllocateCid() const;
|
||||
uint8_t NextIdent();
|
||||
|
||||
void SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data);
|
||||
void SendData(const Channel& channel, const std::vector<uint8_t>& payload);
|
||||
void RequestChannel(uint16_t psm, uint64_t nowUs);
|
||||
void SendConfigRequest(Channel& channel);
|
||||
void HandleSignalling(const uint8_t* data, size_t size);
|
||||
void HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionResponse(const uint8_t* p, size_t n);
|
||||
void HandleChannelData(Channel& channel, const uint8_t* data, size_t size);
|
||||
void HandleHidControl(Channel& channel, const uint8_t* data, size_t size);
|
||||
void UpdateState();
|
||||
void SetState(LinkState state);
|
||||
void ForwardInput();
|
||||
void SendInput(const Channel& interrupt, const std::vector<uint8_t>& report);
|
||||
void ClearConnection();
|
||||
|
||||
Controller& m_controller;
|
||||
size_t m_slot;
|
||||
Baseband m_baseband = Baseband::Idle;
|
||||
LinkState m_state = LinkState::Down;
|
||||
bool m_connectWanted = false;
|
||||
bool m_guestInitiated = false;
|
||||
bool m_everReady = false;
|
||||
uint8_t m_ident = 0;
|
||||
uint64_t m_stateSinceUs = 0;
|
||||
uint64_t m_retryAtUs = 0;
|
||||
uint64_t m_nowUs = 0;
|
||||
std::vector<Channel> m_channels;
|
||||
std::vector<uint8_t> m_report;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// SDP server of an emulated Wii Remote: the HID service record (0x00010000)
|
||||
// and the Device ID record (0x00010001) a real RVL-CNT-01 publishes.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
|
||||
// The Wii Remote's HID report descriptor, as the game also embeds it.
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor();
|
||||
|
||||
// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the
|
||||
// guest's L2CAP MTU on the SDP channel).
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse);
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
namespace MusicAttenuation {
|
||||
|
||||
struct MacOSAudioStatus {
|
||||
bool available = false;
|
||||
bool playing = false;
|
||||
};
|
||||
|
||||
// Queries output activity without capturing audio. Requires Core Audio process
|
||||
// objects (macOS 14.2+); older systems return an unavailable sample.
|
||||
MacOSAudioStatus QueryMacOSExternalAudio() noexcept;
|
||||
|
||||
} // namespace MusicAttenuation
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
// Bump whenever guest_abi/, isa/ or ppc_isa_memory.h changes what translated code compiles against.
|
||||
#define MKW_GUEST_ABI_VERSION 1
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "isa/ppc_isa_fpenv.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
union PPC_FPR {
|
||||
uint64_t raw;
|
||||
double d;
|
||||
struct {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
float ps1; // Low word (Least Significant)
|
||||
float ps0; // High word (Most Significant)
|
||||
#else
|
||||
float ps0; // High word
|
||||
float ps1; // Low word
|
||||
#endif
|
||||
} paired;
|
||||
};
|
||||
|
||||
// PowerPC CPU Context
|
||||
struct CpuContext {
|
||||
// Standard GPRs
|
||||
uint32_t gpr[32];
|
||||
|
||||
// Special Purpose Registers defined by standard PPC
|
||||
uint32_t cr; // Condition Register
|
||||
uint32_t lr; // Link Register
|
||||
uint32_t ctr; // Count Register
|
||||
uint32_t xer; // Integer Exception Register
|
||||
uint32_t fpscr; // Floating-Point Status and Control Register
|
||||
|
||||
// Program State
|
||||
uint32_t pc; // Program Counter (not used alot tho)
|
||||
|
||||
// Floating Point Registers (Modified for Paired Single support)
|
||||
PPC_FPR fpr[32];
|
||||
|
||||
// Broadway Specific Extensions
|
||||
uint32_t gqr[8]; // Graphics Quantization Registers
|
||||
uint32_t hid0; // HID0
|
||||
uint32_t hid1; // HID1
|
||||
uint32_t hid2; // HID2
|
||||
|
||||
uint32_t srr0; // Save/Restore Register 0
|
||||
uint32_t srr1; // Save/Restore Register 1
|
||||
uint32_t msr; // Machine State Register
|
||||
};
|
||||
|
||||
inline thread_local CpuContext* g_currentCpuContext = nullptr;
|
||||
|
||||
class CpuContextScope {
|
||||
public:
|
||||
explicit CpuContextScope(CpuContext* ctx)
|
||||
: previous_(g_currentCpuContext)
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
~CpuContextScope()
|
||||
{
|
||||
g_currentCpuContext = previous_;
|
||||
if (previous_ != nullptr)
|
||||
MkwApplyHostNiMode(previous_->fpscr);
|
||||
else
|
||||
MkwRestoreHostMxcsr(savedMxcsr_);
|
||||
}
|
||||
|
||||
CpuContextScope(const CpuContextScope&) = delete;
|
||||
CpuContextScope& operator=(const CpuContextScope&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* previous_ = nullptr;
|
||||
uint32_t savedMxcsr_ = 0;
|
||||
};
|
||||
|
||||
inline CpuContext* TryGetCpuContext() noexcept
|
||||
{
|
||||
return g_currentCpuContext;
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
#pragma once
|
||||
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "isa/ppc_isa_config.h"
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RuntimeCrash {
|
||||
[[noreturn]] void FatalMissingGuestTarget(uint32_t target, struct CpuContext* cpu) noexcept;
|
||||
} // namespace RuntimeCrash
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
|
||||
|
||||
uint32_t FilterScnRendererPathMaskOption(uint32_t pathMask) noexcept;
|
||||
|
||||
// Some game-facing runtime options alter arguments at well-defined ABI
|
||||
// boundaries. Keep this independent of the dispatch mechanism: generated
|
||||
// static calls deliberately bypass InvokeDirectCpu for performance.
|
||||
// (used for the path mask filtering in ScnRenderer::createPath: depth of
|
||||
// field is always removed, bloom when the user disabled it)
|
||||
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
|
||||
if (target == 0x8023BD38u) {
|
||||
// ScnRenderer::createPath receives the post-processing path mask in r4.
|
||||
ctx->gpr[4] = FilterScnRendererPathMaskOption(ctx->gpr[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent per-thread CPU context used across translated function calls.
|
||||
CpuContext& GetPersistentCpuContext();
|
||||
|
||||
enum class FunctionKind : uint8_t {
|
||||
BaseTranslated = 0,
|
||||
ModTranslated = 1,
|
||||
Native = 2,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
|
||||
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
|
||||
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
|
||||
inline constexpr uint32_t kNativeFunctionPriority = 10000;
|
||||
|
||||
struct TranslatedFunctionInfo {
|
||||
uint32_t address = 0;
|
||||
const char* name = "";
|
||||
uint64_t moduleId = 0;
|
||||
uint32_t priority = 0;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
|
||||
void (*rawCpuInvoker)(CpuContext*) = nullptr;
|
||||
bool mustRemainDynamicallyDispatchable = true;
|
||||
FunctionKind kind = FunctionKind::BaseTranslated;
|
||||
};
|
||||
|
||||
struct RawDispatchRecord {
|
||||
uint32_t address = 0;
|
||||
void (*entry)(CpuContext*) = nullptr;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
bool preserveNonvolatileGprs = false;
|
||||
};
|
||||
|
||||
// Translator-owned indirect dispatch is split by the high address byte and
|
||||
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
|
||||
// page range that contains verified function entry points. The final search is
|
||||
// bounded to at most the 1024 aligned instruction addresses in one guest page.
|
||||
struct StaticIndirectDispatchPage {
|
||||
uint32_t firstEntry = 0;
|
||||
uint16_t entryCount = 0;
|
||||
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
|
||||
// literal third initializer, so it is part of the generated-data contract.
|
||||
uint16_t reserved = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchSegment {
|
||||
const StaticIndirectDispatchPage* pages = nullptr;
|
||||
uint16_t firstPage = 0;
|
||||
uint16_t pageCount = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchTable {
|
||||
const char* profileName = nullptr;
|
||||
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
|
||||
const RawDispatchRecord* entries = nullptr;
|
||||
size_t entryCount = 0;
|
||||
};
|
||||
|
||||
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
|
||||
const StaticIndirectDispatchTable* table,
|
||||
uint32_t address) noexcept {
|
||||
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& segment = table->segments[address >> 24];
|
||||
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
|
||||
if (!segment.pages || pageNumber < segment.firstPage) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t relativePage = pageNumber - segment.firstPage;
|
||||
if (relativePage >= segment.pageCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& page = segment.pages[relativePage];
|
||||
const size_t first = page.firstEntry;
|
||||
const size_t count = page.entryCount;
|
||||
if (first > table->entryCount || count > table->entryCount - first) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t lower = first;
|
||||
size_t upper = first + count;
|
||||
while (lower < upper) {
|
||||
const size_t middle = lower + (upper - lower) / 2;
|
||||
const uint32_t candidate = table->entries[middle].address;
|
||||
if (candidate < address) {
|
||||
lower = middle + 1;
|
||||
} else {
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
return lower < first + count && table->entries[lower].address == address
|
||||
? &table->entries[lower]
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
|
||||
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
|
||||
|
||||
class StaticIndirectDispatchTableRegistrar {
|
||||
public:
|
||||
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
|
||||
RegisterStaticIndirectDispatchTable(table);
|
||||
}
|
||||
};
|
||||
|
||||
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
|
||||
return static_cast<unsigned>(__builtin_ctz(value));
|
||||
}
|
||||
|
||||
class PpcNonvolatileFprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
|
||||
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
// Real masks are a single contiguous register run - f14..f31 for the
|
||||
// conservative default and f23..f31 or similar for a measured writer -
|
||||
// so resolve the run once and copy it as one straight-line block instead
|
||||
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
|
||||
// saves and restores exactly the same registers.
|
||||
const uint32_t lowest = mask_ & (0u - mask_);
|
||||
const uint32_t aboveRun = mask_ + lowest;
|
||||
if ((aboveRun & (aboveRun - 1u)) == 0u) {
|
||||
const unsigned first = PpcLowestSetBitIndex(mask_);
|
||||
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
|
||||
firstSlot_ = static_cast<uint8_t>(first - 14u);
|
||||
slotCount_ = static_cast<uint8_t>(end - first);
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
saved_[i] = cpu_->fpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileFprGuard() noexcept {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (slotCount_ != 0) {
|
||||
const unsigned first = 14u + firstSlot_;
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
cpu_->fpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
|
||||
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
uint32_t mask_ = 0;
|
||||
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
|
||||
// a single run, so the destructor mirrors the constructor's per-bit loop.
|
||||
uint8_t firstSlot_ = 0;
|
||||
uint8_t slotCount_ = 0;
|
||||
// Every element selected by mask_ is written before it is read. Leaving the
|
||||
// remaining slots uninitialized avoids zero-filling all 18 values around a
|
||||
// call that may preserve only one or two architectural FPRs.
|
||||
std::array<PPC_FPR, 18> saved_;
|
||||
};
|
||||
|
||||
class PpcNonvolatileGprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
|
||||
: cpu_(enabled ? cpu : nullptr) {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
saved_[i] = cpu_->gpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileGprGuard() noexcept {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
cpu_->gpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
|
||||
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
|
||||
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
|
||||
std::array<uint32_t, 18> saved_;
|
||||
};
|
||||
|
||||
// Guest address of OSLoadContext, which models rfi-style context restoration
|
||||
// and intentionally replaces the full guest register file instead of returning
|
||||
// like a normal ABI call. It can never be guarded.
|
||||
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
|
||||
|
||||
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
|
||||
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
|
||||
}
|
||||
|
||||
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
|
||||
if (!info || info->nonvolatileFprWriteMask == 0) {
|
||||
return 0;
|
||||
}
|
||||
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTranslatedCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = true;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
// Translator-emitted trait specializations, one macro shared by every generated shard.
|
||||
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
|
||||
// publish their own name). A specialization only exists for a single statically bound winner,
|
||||
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
|
||||
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
|
||||
extern "C" void winner(CpuContext* ctx); \
|
||||
template <> \
|
||||
struct KnownTranslatedCpuCall<0x##addr##u> { \
|
||||
static constexpr bool kAvailable = true; \
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
|
||||
static constexpr void (*Entry)(CpuContext*) = &winner; \
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTypedNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
};
|
||||
|
||||
#include "guest_abi/native_cpu_calls.inc"
|
||||
|
||||
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
|
||||
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
|
||||
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
|
||||
// power of two, the index masks with (size - 1).
|
||||
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
|
||||
|
||||
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
|
||||
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in guest_abi/cpu_context.h).
|
||||
struct IndirectResolvedDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const TranslatedFunctionInfo* info;
|
||||
};
|
||||
inline thread_local IndirectResolvedDispatchMemoEntry
|
||||
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
struct IndirectRawDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const RawDispatchRecord* record;
|
||||
};
|
||||
inline thread_local IndirectRawDispatchMemoEntry
|
||||
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
class TranslatedFunctionRegistry {
|
||||
public:
|
||||
static void Register(TranslatedFunctionInfo info);
|
||||
static void Finalize();
|
||||
static inline bool IsLookupPublished() noexcept {
|
||||
return lookupPublished_.load(std::memory_order_acquire);
|
||||
}
|
||||
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return FindByAddressPtrSlow(address);
|
||||
}
|
||||
|
||||
auto& cached = g_indirectResolvedDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.info;
|
||||
}
|
||||
|
||||
const auto* info = FindByAddressPtrSlow(address);
|
||||
cached = {true, address, info};
|
||||
return info;
|
||||
}
|
||||
|
||||
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
|
||||
// generated-table binary search alongside it pushed the whole function past
|
||||
// the inliner's budget and out-of-lined the probe itself, which is the part
|
||||
// every bctrl executes.
|
||||
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& cached = g_indirectRawDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.record;
|
||||
}
|
||||
|
||||
return FindRawByAddressPtrMiss(address, cached);
|
||||
}
|
||||
|
||||
private:
|
||||
// Memo miss: resolve against the header-inline generated table first; only dynamically
|
||||
// registered records or a not-yet-published table fall through to the slow path.
|
||||
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
|
||||
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
|
||||
const RawDispatchRecord* record = nullptr;
|
||||
if (const auto* table =
|
||||
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
|
||||
record = FindStaticIndirectDispatchEntry(table, address);
|
||||
}
|
||||
if (record == nullptr) {
|
||||
record = FindRawByAddressPtrSlow(address);
|
||||
}
|
||||
cached = {true, address, record};
|
||||
return record;
|
||||
}
|
||||
|
||||
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
|
||||
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
|
||||
|
||||
// Finalize publishes immutable registry, generated-table, and dynamic raw
|
||||
// dispatch storage. Header-inlined cache hits are enabled only after that
|
||||
// release publication, so a pre-finalization miss can never poison them.
|
||||
inline static std::atomic_bool lookupPublished_{false};
|
||||
};
|
||||
|
||||
// Cold, profile-owned registration data is emitted in compact table shards.
|
||||
// Keeping it out of generated function bodies avoids one static constructor
|
||||
// per translated function.
|
||||
struct BulkTranslatedFunctionRecord {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
void (*entry)(CpuContext*);
|
||||
FunctionKind kind;
|
||||
bool preservesNonvolatileFprs;
|
||||
uint32_t nonvolatileFprWriteMask;
|
||||
uint32_t priority;
|
||||
uint64_t moduleId;
|
||||
bool mustRemainDynamicallyDispatchable;
|
||||
};
|
||||
|
||||
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
|
||||
|
||||
class BulkTranslatedFunctionRegistrar {
|
||||
public:
|
||||
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
|
||||
RegisterBulkTranslatedFunctions(records, count);
|
||||
}
|
||||
};
|
||||
|
||||
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
|
||||
if (!info || !info->rawCpuInvoker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
|
||||
if (!record || !record->entry) {
|
||||
return false;
|
||||
}
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
record->entry(cpu);
|
||||
} else {
|
||||
record->entry(cpu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
|
||||
const auto invokeKnownTranslated = [&]() {
|
||||
KnownTranslatedCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
|
||||
// A statically resolved, non-overridable translated call already running
|
||||
// in this CpuContext needs no registry, context, or diagnostic target
|
||||
// transition. This is the normal generated-to-generated hot path.
|
||||
if (TryGetCpuContext() == cpu) {
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(
|
||||
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CpuContextScope contextScope(cpu);
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
|
||||
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
|
||||
// in the run's log folder, popup, exit.
|
||||
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
|
||||
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
|
||||
// Only latch a result once the registry is published (immutable for the process lifetime);
|
||||
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
|
||||
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
|
||||
static std::atomic<uintptr_t> cachedValue{0};
|
||||
constexpr uintptr_t kNegativeCache = 1;
|
||||
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kNegativeCache
|
||||
? nullptr
|
||||
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
|
||||
return ResolveDirectCpuTargetInfo<Target>();
|
||||
}
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
cachedValue.store(info != nullptr
|
||||
? reinterpret_cast<uintptr_t>(info)
|
||||
: kNegativeCache,
|
||||
std::memory_order_release);
|
||||
return info;
|
||||
}
|
||||
|
||||
// Caches the "is the published winner still base translation?" verdict for state-free fast
|
||||
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
|
||||
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
|
||||
template <uint32_t Target>
|
||||
inline bool IsBaseTranslatedCpuTargetActive() {
|
||||
static std::atomic<uint8_t> cachedVerdict{0};
|
||||
constexpr uint8_t kNegativeVerdict = 1;
|
||||
constexpr uint8_t kPositiveVerdict = 2;
|
||||
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kPositiveVerdict;
|
||||
}
|
||||
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
|
||||
if (publishable) {
|
||||
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
|
||||
std::memory_order_release);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void InvokeDirectCpu(CpuContext* ctx) {
|
||||
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
ApplyRuntimeCallOptions(Target, cpu);
|
||||
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
|
||||
const auto invokeKnownNative = [&]() {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
return;
|
||||
}
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownNative();
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownNative();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
} else {
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
|
||||
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
|
||||
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportMissingCpuTarget(Target, cpu);
|
||||
}
|
||||
|
||||
// Cold tail of InvokeIndirectJump: a bctr target that is not a registered function.
|
||||
[[noreturn]] MKW_PPC_NO_INLINE MKW_PPC_COLD void ReportMissingIndirectJumpTarget(uint32_t target, CpuContext* cpu);
|
||||
|
||||
// Indirect jump (bctr without link) - used for switch tables.
|
||||
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
|
||||
// Use the caller's context, not the persistent global context!
|
||||
// This is essential for tail calls via bctr where the 'this' pointer (r3)
|
||||
// must be passed correctly to the target function.
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cold fallback for dynamically registered or signature-only targets.
|
||||
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
|
||||
if (info) {
|
||||
if (TryDispatchResolvedCpuTarget(info, cpu)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ReportMissingIndirectJumpTarget(target, cpu);
|
||||
}
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (target == 0) {
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
ApplyRuntimeCallOptions(target, cpu);
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RecompMod {
|
||||
|
||||
// Diagnostic-only, read by fatal reporters (unmapped access, executable-write, CPU dumps).
|
||||
// Defined here as `inline thread_local` with a constant initializer, not `extern thread_local`
|
||||
// in the .cpp: every indirect dispatch scopes this, so an out-of-line ctor/dtor would cost two
|
||||
// un-inlinable calls plus a register spill each for three instructions of work.
|
||||
inline thread_local uint32_t g_currentTranslatedExecutionAddress = 0;
|
||||
|
||||
class ScopedTranslatedExecutionAddress {
|
||||
public:
|
||||
explicit ScopedTranslatedExecutionAddress(uint32_t address) noexcept
|
||||
: previous_(g_currentTranslatedExecutionAddress) {
|
||||
// Address 0 means "nothing new to report" - the enclosing scope's value
|
||||
// stays visible, and the unconditional restore below keeps nesting exact.
|
||||
if (address != 0) {
|
||||
g_currentTranslatedExecutionAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedTranslatedExecutionAddress() noexcept {
|
||||
g_currentTranslatedExecutionAddress = previous_;
|
||||
}
|
||||
|
||||
ScopedTranslatedExecutionAddress(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
ScopedTranslatedExecutionAddress& operator=(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
|
||||
private:
|
||||
uint32_t previous_ = 0;
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageShift = 12;
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageCount = 1u << (32 - kExecutableWriteGuardPageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageShift = 20;
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageCount = 1u << (32 - kExecutableWriteGuardCoarsePageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageShift = 16;
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageCount = 1u << (32 - kExecutableWriteGuardMidPageShift);
|
||||
|
||||
extern std::atomic<bool> g_executableWriteGuardEnabled;
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount];
|
||||
|
||||
} // namespace RecompMod
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// Flat 4 GiB guest address space constants used by translated accesses; see guest_flat_memory.h.
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_abi/flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
} // namespace GuestFlat
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// The only header translated code includes. Everything reachable from here is the
|
||||
// guest ABI; runtime-only headers must never be pulled in.
|
||||
#include "guest_abi/abi_version.h"
|
||||
|
||||
#include "isa/ppc_isa_config.h"
|
||||
#include "isa/big_endian.h"
|
||||
#include "isa/ppc_isa_fpenv.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "isa/ppc_isa_int.h"
|
||||
#include "isa/ppc_isa_float.h"
|
||||
#include "guest_abi/flat_memory.h"
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "guest_abi/memory.h"
|
||||
#include "isa/ppc_isa_quantized.h"
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
#include "guest_abi/guest_cr.h"
|
||||
#include "guest_abi/dispatch.h"
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#include "guest_abi/guest_abi.h"
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// Condition Register Fields
|
||||
#define CR_LT 0
|
||||
#define CR_GT 1
|
||||
#define CR_EQ 2
|
||||
#define CR_SO 3
|
||||
|
||||
[[noreturn]] void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details);
|
||||
|
||||
// Used by generated code when the translator encounters privileged/unmodeled PPC instructions
|
||||
// (e.g. rfi). This is intentionally loud so missing HLE hooks are easy to find.
|
||||
#define UNDEFINED(pc, rawInstruction, details) PPC_Undefined((pc), (rawInstruction), (details))
|
||||
|
||||
// Helper to set CR bits (Signed)
|
||||
inline void SetCR(CpuContext* cpu, int field, int32_t a, int32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Unsigned)
|
||||
inline void SetCR(CpuContext* cpu, int field, uint32_t a, uint32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Floating-point)
|
||||
inline void SetCRFloat(CpuContext* cpu, int field, double a, double b) {
|
||||
uint32_t crField = 0;
|
||||
if (std::isnan(a) || std::isnan(b)) {
|
||||
// Unordered: LT/GT/EQ clear, SO set.
|
||||
crField = 0x1;
|
||||
} else {
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
}
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to get CR bit
|
||||
inline bool GetCRBit(CpuContext* cpu, int field, int bit) {
|
||||
int shift = (7 - field) * 4 + (3 - bit);
|
||||
return (cpu->cr >> shift) & 1;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Memory {
|
||||
public:
|
||||
using DeferredReadCallback = bool (*)(void* user);
|
||||
|
||||
static constexpr size_t kMem1Size = 24u * 1024u * 1024u;
|
||||
static constexpr size_t kMem2Size = 128u * 1024u * 1024u;
|
||||
static constexpr uint32_t kMem1PhysicalBase = 0x00000000u;
|
||||
static constexpr uint32_t kMem1CachedBase = 0x80000000u;
|
||||
static constexpr uint32_t kMem1UncachedBase = 0xC0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalBase = 0x10000000u;
|
||||
static constexpr uint32_t kMem2CachedBase = 0x90000000u;
|
||||
static constexpr uint32_t kMem2UncachedBase = 0xD0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalEnd =
|
||||
kMem2PhysicalBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2CachedEnd =
|
||||
kMem2CachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2UncachedEnd =
|
||||
kMem2UncachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
|
||||
struct RegionConfig {
|
||||
std::string name;
|
||||
uint32_t baseAddress = 0;
|
||||
size_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<RegionConfig> regions;
|
||||
static Config WiiDefaults();
|
||||
};
|
||||
|
||||
class AccessViolation : public std::runtime_error {
|
||||
public:
|
||||
AccessViolation(uint32_t address, size_t length, std::string_view reason);
|
||||
|
||||
uint32_t address() const noexcept { return address_; }
|
||||
size_t length() const noexcept { return length_; }
|
||||
std::string_view reason() const noexcept { return reason_; }
|
||||
|
||||
private:
|
||||
uint32_t address_ = 0;
|
||||
size_t length_ = 0;
|
||||
std::string reason_;
|
||||
};
|
||||
|
||||
static void Init(const Config& config);
|
||||
// Single flat MEM1 region. Not used by the shipped runtime, but the
|
||||
// translator integration-test harnesses emit calls to it.
|
||||
static void Init(size_t mem1Size);
|
||||
static void Reset();
|
||||
// Executable ranges are normally registered during startup. Rebuild the
|
||||
// writable fast-path classification after each registration so a page
|
||||
// previously classified as ordinary data cannot retain a stale direct
|
||||
// write bias.
|
||||
static void RefreshWritableFastPathsForExecutableRanges();
|
||||
|
||||
static uint8_t Read8(uint32_t addr);
|
||||
static uint16_t Read16(uint32_t addr);
|
||||
static uint32_t Read32(uint32_t addr);
|
||||
static uint64_t Read64(uint32_t addr);
|
||||
static float ReadFloat32(uint32_t addr);
|
||||
static double ReadFloat64(uint32_t addr);
|
||||
static void Write8(uint32_t addr, uint8_t val);
|
||||
static void Write16(uint32_t addr, uint16_t val);
|
||||
static void Write32(uint32_t addr, uint32_t val);
|
||||
static void Write64(uint32_t addr, uint64_t val);
|
||||
static void WriteFloat32(uint32_t addr, double val);
|
||||
static void WriteFloat64(uint32_t addr, double val);
|
||||
|
||||
// Exception-safe scalar access for HLE code. These keep Read32/Write32's
|
||||
// full mapping behavior and only convert an unmapped address into a failure
|
||||
// result; they are deliberately not MemoryInline::Try*GuestScalar, which is
|
||||
// the translated-code fast path over the page table.
|
||||
static bool TryRead32(uint32_t addr, uint32_t& value) noexcept {
|
||||
try {
|
||||
value = Read32(addr);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryWrite32(uint32_t addr, uint32_t value) noexcept {
|
||||
try {
|
||||
Write32(addr, value);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t* GetPointer(uint32_t addr);
|
||||
static uint8_t* GetPointer(uint32_t addr, size_t length);
|
||||
static bool Contains(uint32_t addr, size_t length = 1);
|
||||
|
||||
static uint64_t RegisterDeferredRead(uint32_t addr, size_t length,
|
||||
DeferredReadCallback callback, void* user);
|
||||
static void ClearDeferredReads();
|
||||
|
||||
// sizeBytes reports the storage actually allocated, which is not always the
|
||||
// configured size (aliased MEM1/MEM2 windows are clamped to what is behind
|
||||
// them). Used by the crash dump.
|
||||
static std::vector<RegionConfig> DescribeRegions();
|
||||
};
|
||||
|
||||
#include "guest_abi/memory_access.h"
|
||||
@@ -0,0 +1,732 @@
|
||||
#pragma once
|
||||
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "guest_abi/flat_memory.h"
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
// Hooks for GX HLE FIFO handling
|
||||
extern "C" {
|
||||
void GX_HLE_FIFO_WriteFloat(float val);
|
||||
void GX_HLE_FIFO_Write32(uint32_t val);
|
||||
void GX_HLE_FIFO_Write16(uint16_t val);
|
||||
void GX_HLE_FIFO_Write8(uint8_t val);
|
||||
void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
}
|
||||
|
||||
namespace MemoryInline {
|
||||
#if defined(_WIN32)
|
||||
#define MKW_MEMORY_FORCE_INLINE __forceinline
|
||||
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
// See runtime/include/isa/ppc_isa_config.h for why non-Windows Clang needs the GNU-attribute
|
||||
// spellings instead of the MS-extension keywords.
|
||||
#define MKW_MEMORY_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_MEMORY_NO_INLINE __attribute__((noinline))
|
||||
#endif
|
||||
#define MKW_MEMORY_COLD __attribute__((cold))
|
||||
inline constexpr uint32_t kPageShift = 20;
|
||||
inline constexpr uint32_t kPageSize = 1u << kPageShift;
|
||||
inline constexpr uint32_t kPageMask = kPageSize - 1u;
|
||||
inline constexpr uint32_t kPageCount = 1u << (32 - kPageShift);
|
||||
inline constexpr uint32_t kMaxFastScalarSize = 8;
|
||||
inline constexpr uint32_t kWritableSubPageShift = RecompMod::kExecutableWriteGuardPageShift;
|
||||
inline constexpr uint32_t kWritableSubPageSize = 1u << kWritableSubPageShift;
|
||||
inline constexpr uint32_t kWritableSubPagesPerPage = kPageSize / kWritableSubPageSize;
|
||||
|
||||
struct PageEntry {
|
||||
uint8_t* base = nullptr;
|
||||
uint32_t limit = 0;
|
||||
};
|
||||
|
||||
extern PageEntry g_pageTable[kPageCount];
|
||||
// Encoded (host page base - guest page base) + 1 for full pages whose next
|
||||
// page is contiguous. This permits any native access up to 8 bytes without a
|
||||
// per-access mask/limit check. Zero retains the general PageEntry fallback.
|
||||
extern uintptr_t g_fullPageBias[kPageCount];
|
||||
// Runtime-active readable biases. Deferred-read pages clear these entries once
|
||||
// instead of paying a mode branch on every translated read.
|
||||
extern uintptr_t g_fullReadablePageBias[kPageCount];
|
||||
// Same encoding, but only for pages proven not to contain executable bytes.
|
||||
// Executable-range registration invalidates entries before guest execution.
|
||||
extern uintptr_t g_fullWritablePageBias[kPageCount];
|
||||
|
||||
// Allocated only for mapped 1 MiB pages that contain both executable and data
|
||||
// 4 KiB pages. Entries use the same encoded host bias as the coarse table.
|
||||
// Exact executable bits remain authoritative and are checked at lookup time,
|
||||
// including both sides of a cross-4-KiB access.
|
||||
struct SparseWritablePageTable {
|
||||
uintptr_t encodedBias[kWritableSubPagesPerPage]{};
|
||||
};
|
||||
extern const SparseWritablePageTable* g_sparseWritablePageTables[kPageCount];
|
||||
|
||||
// Nonzero while any registered deferred read overlaps the page. Small
|
||||
// mappings (e.g. the 16 KiB locked cache) have no coarse bias entry, so a
|
||||
// zero readable bias alone cannot distinguish "deferred content pending"
|
||||
// from "small but plain memory"; range resolution needs the explicit flag.
|
||||
extern uint8_t g_deferredReadCoveredPages[kPageCount];
|
||||
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint8_t Read8Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint16_t Read16Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint32_t Read32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint64_t Read64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadFloat32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadFloat64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write8Slow(uint32_t addr, uint8_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write16Slow(uint32_t addr, uint16_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write32Slow(uint32_t addr, uint32_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write64Slow(uint32_t addr, uint64_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat32Slow(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat64Slow(uint32_t addr, double val);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD T ReadResolvedFallback(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadResolvedFallbackFloat32(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadResolvedFallbackFloat64(uint32_t addr);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallback(uint32_t addr, T value);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat32(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat64(uint32_t addr, double val);
|
||||
bool ResolveDeferredReads(uint32_t addr, size_t length);
|
||||
|
||||
constexpr bool IsMmioAddress(uint32_t addr) {
|
||||
return addr >= 0xCC000000u && addr < 0xCE000000u;
|
||||
}
|
||||
|
||||
constexpr bool IsGpuFifoAddress(uint32_t addr) {
|
||||
return addr >= 0xCC008000u && addr < 0xCC008100u;
|
||||
}
|
||||
|
||||
// Page protections can't cover this: an MMIO write must reach GX HLE with its value or be
|
||||
// reported, and a fault record can't carry the value, so this mask/compare sits in front of
|
||||
// every flat store instead.
|
||||
MKW_MEMORY_FORCE_INLINE bool FlatWriteNeedsPolicy(uint32_t address) {
|
||||
return (address & 0xFE000000u) == 0xCC000000u; // 0xCC000000..0xCDFFFFFF
|
||||
}
|
||||
|
||||
// Gekko stfs conversion is a bit-level narrowing operation. In particular it
|
||||
// does not behave like a host double-to-float cast for values which were left
|
||||
// in double precision, and it has hardware-tested handling for tiny values.
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ConvertPpcDoubleToSingleBits(double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const uint32_t exponent = static_cast<uint32_t>((bits >> 52) & 0x7FFu);
|
||||
// The subnormal-single arm applies to exactly the exponents 874..896; every
|
||||
// other exponent takes the plain sign/exponent/fraction narrowing below.
|
||||
// Inside that window the exponent field is nonzero, so the magnitude cannot
|
||||
// be zero and needs no separate test - the zero case (exponent 0) reaches
|
||||
// the narrowing exactly as it did when the two arms shared that test.
|
||||
if (exponent - 874u <= 22u) [[unlikely]]
|
||||
{
|
||||
uint32_t narrowed = static_cast<uint32_t>(
|
||||
0x80000000ULL | ((bits & 0x000FFFFFFFFFFFFFULL) >> 21));
|
||||
narrowed >>= (905u - exponent);
|
||||
narrowed |= static_cast<uint32_t>((bits >> 32) & 0x80000000ULL);
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
// Results below the documented conversion range are architecturally
|
||||
// undefined; this is the behavior measured on Gekko/Broadway hardware.
|
||||
return static_cast<uint32_t>(
|
||||
((bits >> 32) & 0xC0000000ULL) | ((bits >> 29) & 0x3FFFFFFFULL));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float PpcSingleBitsToFloat(uint32_t bits) {
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetPointerFast(uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
const uint32_t page = address >> kPageShift;
|
||||
if (length <= 8) {
|
||||
const uintptr_t encodedBias = g_fullPageBias[page];
|
||||
if (encodedBias != 0) {
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[page];
|
||||
if (!entry.base || offset + length > entry.limit) {
|
||||
pointer = nullptr;
|
||||
return false;
|
||||
}
|
||||
pointer = entry.base + offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint8_t* GetPointerFast(uint32_t address, size_t length) {
|
||||
uint8_t* pointer = nullptr;
|
||||
return TryGetPointerFast(address, length, pointer) ? pointer : nullptr;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetWritablePointerFast(
|
||||
uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
// The sparse-table hit below distinguishes itself from the small-mapping
|
||||
// hit through `pointer`, so the out-parameter must start null regardless
|
||||
// of what the caller passed in. (A caller handing in an uninitialized
|
||||
// pointer used to turn every sparse hit into a write through stack
|
||||
// garbage - random host memory corruption.)
|
||||
pointer = nullptr;
|
||||
if (length == 0 || length > 8 || address > UINT32_MAX - (length - 1))
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(length - 1);
|
||||
const uint32_t firstExactPage = address >> kWritableSubPageShift;
|
||||
const uint32_t lastExactPage = endAddress >> kWritableSubPageShift;
|
||||
if (encodedBias != 0 && (endAddress >> kPageShift) != coarsePage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (encodedBias == 0) {
|
||||
const auto* subTable = g_sparseWritablePageTables[coarsePage];
|
||||
if (subTable != nullptr) {
|
||||
encodedBias = subTable->encodedBias[
|
||||
(address & kPageMask) >> kWritableSubPageShift];
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
} else {
|
||||
// Small mappings such as Broadway's 16 KiB locked cache cannot
|
||||
// populate the full-1-MiB bias table. Keep them native by proving
|
||||
// the exact access against the ordinary page entry, then applying
|
||||
// the same executable-write policy as a sparse-table hit.
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[coarsePage];
|
||||
if (!entry.base || offset + length > entry.limit)
|
||||
return false;
|
||||
pointer = entry.base + offset;
|
||||
}
|
||||
|
||||
// The exact 4 KiB guard is the final authority. Checking it on every
|
||||
// checked/sparse hit also makes later executable-range registration
|
||||
// safe when a prebuilt table still contains the old mapped bias.
|
||||
if (RecompMod::g_executableWriteGuardPages[firstExactPage].load(
|
||||
std::memory_order_relaxed) != 0 ||
|
||||
(lastExactPage != firstExactPage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointer != nullptr)
|
||||
return true;
|
||||
}
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flat form: guest_flat_memory.h's page protections already answer mapped/non-deferred/non-executable, so resolving is pure address arithmetic.
|
||||
// Two checks stay inline: a wrapped guest address can't survive 64-bit `host + rangeOffset`, and an MMIO write's value isn't recoverable from a
|
||||
// fault record, so a write touching that window must resolve null and fall back to Memory::Write*. Checking both range endpoints is a complete
|
||||
// proof since length <= kPageSize (1 MiB) can't straddle the 32 MiB MMIO window.
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffset, uint32_t length,
|
||||
bool needsRead, bool needsWrite) {
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + guestStart;
|
||||
}
|
||||
|
||||
// Guest-address byte order. Distinct from isa/big_endian.h, which is the
|
||||
// host-pointer codec; do not "unify" them.
|
||||
inline uint16_t ByteSwap16(uint16_t value) {
|
||||
return __builtin_bswap16(value);
|
||||
}
|
||||
|
||||
inline uint32_t ByteSwap32(uint32_t value) {
|
||||
return __builtin_bswap32(value);
|
||||
}
|
||||
|
||||
inline uint64_t ByteSwap64(uint64_t value) {
|
||||
return __builtin_bswap64(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T MaybeByteSwap(T value) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
return value;
|
||||
} else if constexpr (sizeof(T) == 2) {
|
||||
return static_cast<T>(ByteSwap16(static_cast<uint16_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
return static_cast<T>(ByteSwap32(static_cast<uint32_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 8) {
|
||||
return static_cast<T>(ByteSwap64(static_cast<uint64_t>(value)));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool ReadResolvedScalar(uint8_t* host, uint32_t rangeOffset, T& outValue) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = host[rangeOffset];
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, host + rangeOffset, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ResolvedLoadPair {
|
||||
uint32_t first = 0;
|
||||
uint32_t second = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset) {
|
||||
uint32_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {packed >> 16, packed & 0xFFFFu, true};
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair32(uint8_t* host, uint32_t rangeOffset) {
|
||||
uint64_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {static_cast<uint32_t>(packed >> 32), static_cast<uint32_t>(packed), true};
|
||||
}
|
||||
|
||||
template <typename Packed>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPairFast(
|
||||
uint8_t* host, uint32_t rangeOffset, Packed packed) {
|
||||
if (!host) return false;
|
||||
const Packed swapped = MaybeByteSwap(packed);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(swapped));
|
||||
return true;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset, uint32_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair32(
|
||||
uint8_t* host, uint32_t rangeOffset, uint64_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedScalar(uint8_t* host, uint32_t rangeOffset, T value) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
host[rangeOffset] = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool TryReadMappedScalar(uint32_t address, T& outValue) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryReadGuestScalar(uint32_t address, T& outValue) {
|
||||
const uintptr_t encodedBias = g_fullReadablePageBias[address >> kPageShift];
|
||||
if (encodedBias == 0) [[unlikely]]
|
||||
return false;
|
||||
auto* ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryWriteGuestScalar(uint32_t address, T value) {
|
||||
static_assert(sizeof(T) >= 1 && sizeof(T) <= kMaxFastScalarSize);
|
||||
// A mixed executable/data 1 MiB page zeroes the coarse writable bias even though most of
|
||||
// its 4 KiB sub-pages are plain data; MKW's THP buffers share such a page with .text, which
|
||||
// used to force ~15% of total CPU through the cold path. The sparse sub-page tier below
|
||||
// keeps those stores native while the exact 4 KiB executable guards stay authoritative.
|
||||
if (address > UINT32_MAX - static_cast<uint32_t>(sizeof(T) - 1u)) [[unlikely]]
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(sizeof(T) - 1u);
|
||||
if ((endAddress >> kPageShift) != coarsePage) [[unlikely]]
|
||||
return false;
|
||||
const uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
uint8_t* ptr = nullptr;
|
||||
if (encodedBias != 0) {
|
||||
ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
} else if (!TryGetWritablePointerFast(address, sizeof(T), ptr)) [[unlikely]] {
|
||||
return false;
|
||||
}
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool WriteStackScalarFast(uint32_t address, T value) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline uint8_t ReadStack8(uint32_t address) {
|
||||
uint8_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read8(address);
|
||||
}
|
||||
|
||||
inline uint16_t ReadStack16(uint32_t address) {
|
||||
uint16_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read16(address);
|
||||
}
|
||||
|
||||
inline uint32_t ReadStack32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read32(address);
|
||||
}
|
||||
|
||||
inline uint64_t ReadStack64(uint32_t address) {
|
||||
uint64_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read64(address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline void WriteStack8(uint32_t address, uint8_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write8(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack16(uint32_t address, uint16_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write16(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack32(uint32_t address, uint32_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack64(uint32_t address, uint64_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat32(uint32_t address, double value) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename SlowRead>
|
||||
MKW_MEMORY_FORCE_INLINE T ReadResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address,
|
||||
SlowRead slow) {
|
||||
T value = 0;
|
||||
if (ReadResolvedScalar(host, rangeOffset, value)) {
|
||||
return value;
|
||||
}
|
||||
[[unlikely]] return slow(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t ReadResolved8(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint8_t>(r, o, a, ReadResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t ReadResolved16(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint16_t>(r, o, a, ReadResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ReadResolved32(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint32_t>(r, o, a, ReadResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h (the psq resolved tier packs two lanes into
|
||||
// one 64-bit access); generated code never names it directly.
|
||||
MKW_MEMORY_FORCE_INLINE uint64_t ReadResolved64(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint64_t>(r, o, a, ReadResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE float ReadResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint32_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat32(a);
|
||||
float value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE double ReadResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint64_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat64(a);
|
||||
double value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
|
||||
template <typename T, typename SlowWrite>
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address, T value,
|
||||
SlowWrite slow) {
|
||||
if (WriteResolvedScalar(host, rangeOffset, value)) {
|
||||
return;
|
||||
}
|
||||
[[unlikely]] slow(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved8(uint8_t* r, uint32_t o, uint32_t a, uint8_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved16(uint8_t* r, uint32_t o, uint32_t a, uint16_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved32(uint8_t* r, uint32_t o, uint32_t a, uint32_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h, as ReadResolved64 above.
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved64(uint8_t* r, uint32_t o, uint32_t a, uint64_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(v);
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat32(a, v);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
uint64_t bits; std::memcpy(&bits, &v, sizeof(bits));
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat64(a, v);
|
||||
}
|
||||
|
||||
// Flat guest memory (audit item T-MEM): the 4 GiB reservation makes a guest access a byte swap
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
T value{};
|
||||
std::memcpy(&value, MKW_FLAT_GUEST_BASE + address, sizeof(T));
|
||||
return MaybeByteSwap(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
// Check-free stores: emitted ONLY for addresses the translator proved at translate time are ordinary guest RAM (r1-relative stack slots, ~45%
|
||||
// of flat stores), skipping the MMIO mask/compare that's pure overhead there. Still safe if that proof were ever wrong: the flat view maps
|
||||
// 0xCC000000..0xCDFFFFFF PAGE_NOACCESS, so a stray MMIO store faults into the same handler and diagnostic as the checked path, just reported
|
||||
// instead of dispatched inline. Never use these for an address the translator hasn't proven.
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam8(uint32_t address, uint8_t value) {
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam16(uint32_t address, uint16_t value) {
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam32(uint32_t address, uint32_t value) {
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat32(uint32_t address, double value) {
|
||||
FlatStore<uint32_t>(address, ConvertPpcDoubleToSingleBits(value));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
} // namespace MemoryInline
|
||||
|
||||
#undef MKW_MEMORY_FORCE_INLINE
|
||||
#undef MKW_MEMORY_NO_INLINE
|
||||
#undef MKW_MEMORY_COLD
|
||||
|
||||
inline uint8_t Memory::Read8(uint32_t addr) {
|
||||
uint8_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read8Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint16_t Memory::Read16(uint32_t addr) {
|
||||
uint16_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read16Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint32_t Memory::Read32(uint32_t addr) {
|
||||
uint32_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read32Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint64_t Memory::Read64(uint32_t addr) {
|
||||
uint64_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read64Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float Memory::ReadFloat32(uint32_t addr) {
|
||||
uint32_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat32Slow(addr);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline double Memory::ReadFloat64(uint32_t addr) {
|
||||
uint64_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat64Slow(addr);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void Memory::Write8(uint32_t addr, uint8_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write8Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write16(uint32_t addr, uint16_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write16Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write32(uint32_t addr, uint32_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write64(uint32_t addr, uint64_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write64Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat32(uint32_t addr, double val) {
|
||||
{
|
||||
const uint32_t bits = MemoryInline::ConvertPpcDoubleToSingleBits(val);
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat64(uint32_t addr, double val) {
|
||||
{
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &val, sizeof(bits));
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat64Slow(addr, val);
|
||||
}
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
// Native call catalog used by InvokeDirectCpu. Keep project-specific HLE
|
||||
// address mappings here; abi_bridge.h contains only the generic ABI machinery.
|
||||
// address mappings here; dispatch.h contains only the generic ABI machinery.
|
||||
// Part of the guest ABI: translated shards bind these calls at compile time.
|
||||
//
|
||||
// This is a .inc, not a header: it has no includes of its own and references
|
||||
// CpuContext, KnownTypedNativeCpuCall<> and KnownNativeCpuCall<>, all of which
|
||||
// are declared above its single mid-file include site in abi_bridge.h. It
|
||||
// are declared above its single mid-file include site in dispatch.h. It
|
||||
// cannot compile standalone and must never be included anywhere else.
|
||||
|
||||
extern "C" void nw4r__lyt__detail__DrawQuad_800847c0(CpuContext* ctx);
|
||||
@@ -9,39 +9,10 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "guest_abi/flat_memory.h"
|
||||
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
enum class Backing {
|
||||
Owned,
|
||||
Mem1,
|
||||
@@ -65,25 +36,6 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// PowerPC EABI small-data base pointers of the translated game. Defined per game
|
||||
// in runtime/game/runtime_config_values.cpp so the runtime itself stays game-independent.
|
||||
namespace GuestSmallData {
|
||||
extern const uint32_t kSda1Base; // _SDA_BASE_, loaded into r13
|
||||
extern const uint32_t kSda2Base; // _SDA2_BASE_, loaded into r2
|
||||
} // namespace GuestSmallData
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
// IOS side of the emulated Bluetooth module (/dev/usb/oh1/57e/305): routes the
|
||||
// guest's USB control / bulk / interrupt transfers to Bluetooth::Controller and
|
||||
// completes the held event and ACL reads from the alarm pump.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct CpuContext;
|
||||
|
||||
// Returns the fd for the Bluetooth device path, 0 when `path` is not ours.
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path);
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd);
|
||||
// Cancels the held reads (their callbacks are queued) and closes the device.
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd);
|
||||
// True when the request completed now with `result`; false when it is held
|
||||
// until data arrives (its callback is queued later by the pump).
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result);
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr);
|
||||
// Runs the controller and completes held reads. Emulation thread only.
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu);
|
||||
@@ -41,42 +41,3 @@ inline GuestStatus Encode(const Fields& fields)
|
||||
}
|
||||
|
||||
} // namespace PadStatusContract
|
||||
|
||||
namespace WpadContract {
|
||||
|
||||
inline constexpr std::size_t kChannelCount = 4;
|
||||
inline constexpr int32_t kStatusDisabled = 0;
|
||||
inline constexpr int32_t kStatusReady = 3;
|
||||
inline constexpr int32_t kErrorNoController = -1;
|
||||
inline constexpr int32_t kErrorNotReady = -2;
|
||||
inline constexpr int32_t kErrorBadChannel = -6;
|
||||
inline constexpr int32_t kExtensionCore = 0;
|
||||
|
||||
class State {
|
||||
public:
|
||||
void Initialize() { m_initialized = true; }
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
|
||||
|
||||
int32_t GetDataFormat(uint32_t chan) const
|
||||
{
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? 0 : kErrorNotReady;
|
||||
}
|
||||
|
||||
int32_t SetDataFormat(uint32_t chan, int32_t format) const
|
||||
{
|
||||
(void)format;
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? kErrorNoController : kErrorNotReady;
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
} // namespace WpadContract
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// HW_GPIOB_OUT, the GPIO outputs the PPC owns. WPAD sets bit 8 to power the
|
||||
// sensor bar; there is nothing behind it on a PC, so the register only latches.
|
||||
inline constexpr uint32_t kHollywoodGpioBOut = 0xCD0000C0u;
|
||||
|
||||
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value);
|
||||
bool HollywoodGpio_Write32(uint32_t address, uint32_t value);
|
||||
@@ -3,11 +3,11 @@
|
||||
#include <cstddef>
|
||||
|
||||
// HostContext is the deliberately small boundary between the guest scheduler
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers and
|
||||
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
|
||||
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
|
||||
// backend does not save. Its handles are only valid on the thread that
|
||||
// initialized the scheduler.
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers;
|
||||
// Linux uses libco's host backend; Intel macOS uses its System V x86-64 backend. macOS AArch64 uses
|
||||
// the local assembly backend because it must preserve Darwin's platform-reserved
|
||||
// x18 register, which libco's AArch64 backend does not save. Its handles are
|
||||
// only valid on the thread that initialized the scheduler.
|
||||
namespace HostContext {
|
||||
|
||||
using Handle = void*;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "ppc_isa_fpenv.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -10,85 +11,6 @@
|
||||
|
||||
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept;
|
||||
|
||||
union PPC_FPR {
|
||||
uint64_t raw;
|
||||
double d;
|
||||
struct {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
float ps1; // Low word (Least Significant)
|
||||
float ps0; // High word (Most Significant)
|
||||
#else
|
||||
float ps0; // High word
|
||||
float ps1; // Low word
|
||||
#endif
|
||||
} paired;
|
||||
};
|
||||
|
||||
// PowerPC CPU Context
|
||||
struct CpuContext {
|
||||
// Standard GPRs
|
||||
uint32_t gpr[32];
|
||||
|
||||
// Special Purpose Registers defined by standard PPC
|
||||
uint32_t cr; // Condition Register
|
||||
uint32_t lr; // Link Register
|
||||
uint32_t ctr; // Count Register
|
||||
uint32_t xer; // Integer Exception Register
|
||||
uint32_t fpscr; // Floating-Point Status and Control Register
|
||||
|
||||
// Program State
|
||||
uint32_t pc; // Program Counter (not used alot tho)
|
||||
|
||||
// Floating Point Registers (Modified for Paired Single support)
|
||||
PPC_FPR fpr[32];
|
||||
|
||||
// Broadway Specific Extensions
|
||||
uint32_t gqr[8]; // Graphics Quantization Registers
|
||||
uint32_t hid0; // HID0
|
||||
uint32_t hid1; // HID1
|
||||
uint32_t hid2; // HID2
|
||||
|
||||
uint32_t srr0; // Save/Restore Register 0
|
||||
uint32_t srr1; // Save/Restore Register 1
|
||||
uint32_t msr; // Machine State Register
|
||||
};
|
||||
|
||||
inline thread_local CpuContext* g_currentCpuContext = nullptr;
|
||||
|
||||
class CpuContextScope {
|
||||
public:
|
||||
explicit CpuContextScope(CpuContext* ctx)
|
||||
: previous_(g_currentCpuContext)
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
~CpuContextScope()
|
||||
{
|
||||
g_currentCpuContext = previous_;
|
||||
if (previous_ != nullptr)
|
||||
MkwApplyHostNiMode(previous_->fpscr);
|
||||
else
|
||||
MkwRestoreHostMxcsr(savedMxcsr_);
|
||||
}
|
||||
|
||||
CpuContextScope(const CpuContextScope&) = delete;
|
||||
CpuContextScope& operator=(const CpuContextScope&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* previous_ = nullptr;
|
||||
uint32_t savedMxcsr_ = 0;
|
||||
};
|
||||
|
||||
inline CpuContext* TryGetCpuContext() noexcept
|
||||
{
|
||||
return g_currentCpuContext;
|
||||
}
|
||||
|
||||
inline CpuContext* CurrentCpuContext()
|
||||
{
|
||||
CpuContext* cpu = TryGetCpuContext();
|
||||
@@ -110,79 +32,7 @@ inline CpuContext* CurrentCpuContext()
|
||||
return cpu;
|
||||
}
|
||||
|
||||
// Condition Register Fields
|
||||
#define CR_LT 0
|
||||
#define CR_GT 1
|
||||
#define CR_EQ 2
|
||||
#define CR_SO 3
|
||||
#include "guest_abi/guest_cr.h"
|
||||
|
||||
extern "C" void DumpHostStackTraceForRuntimeHelper();
|
||||
void MarkFatalErrorReported();
|
||||
|
||||
[[noreturn]] inline void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"[runtime] UNDEFINED guest instruction: pc=0x%08X raw=0x%08X %s\n",
|
||||
pc,
|
||||
rawInstruction,
|
||||
details ? details : "");
|
||||
char message[256]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"The game stopped because it reached an unsupported guest instruction at PC 0x%08X (instruction 0x%08X).\n\n%s",
|
||||
pc, rawInstruction, details ? details : "No additional details were provided.");
|
||||
ShowRuntimeFatalPopup("Unsupported guest instruction", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Used by generated code when the translator encounters privileged/unmodeled PPC instructions
|
||||
// (e.g. rfi). This is intentionally loud so missing HLE hooks are easy to find.
|
||||
#define UNDEFINED(pc, rawInstruction, details) PPC_Undefined((pc), (rawInstruction), (details))
|
||||
|
||||
// Helper to set CR bits (Signed)
|
||||
inline void SetCR(CpuContext* cpu, int field, int32_t a, int32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Unsigned)
|
||||
inline void SetCR(CpuContext* cpu, int field, uint32_t a, uint32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Floating-point)
|
||||
inline void SetCRFloat(CpuContext* cpu, int field, double a, double b) {
|
||||
uint32_t crField = 0;
|
||||
if (std::isnan(a) || std::isnan(b)) {
|
||||
// Unordered: LT/GT/EQ clear, SO set.
|
||||
crField = 0x1;
|
||||
} else {
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
}
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to get CR bit
|
||||
inline bool GetCRBit(CpuContext* cpu, int field, int bit) {
|
||||
int shift = (7 - field) * 4 + (3 - bit);
|
||||
return (cpu->cr >> shift) & 1;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
// the psq_l/psq_st tier that does lives in ppc_isa_quantized.h.
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
#include "ppc_isa_context.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -607,7 +607,18 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__FMA__)
|
||||
return _mm_fmadd_ps(multiplicand, multiplier, addend);
|
||||
#else
|
||||
// x86-64-v2 has SSE4.2 but no FMA. Preserve the one rounding point per
|
||||
// lane through the scalar C++ FMA rather than decomposing into mul+add.
|
||||
const double a = PpcM128ToPsInline(multiplicand);
|
||||
const double c = PpcM128ToPsInline(multiplier);
|
||||
const double b = PpcM128ToPsInline(addend);
|
||||
return PpcPsToM128Inline(PpcPackPairedInline(
|
||||
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
|
||||
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
|
||||
#endif
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
@@ -619,7 +630,16 @@ inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multipl
|
||||
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__FMA__)
|
||||
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
|
||||
#else
|
||||
const double a = PpcM128ToPsInline(multiplicand);
|
||||
const double c = PpcM128ToPsInline(multiplier);
|
||||
const double b = PpcM128ToPsInline(subtractor);
|
||||
return PpcPsToM128Inline(PpcPackPairedInline(
|
||||
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
|
||||
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
|
||||
#endif
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
|
||||
@@ -34,7 +34,7 @@ inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
|
||||
inline thread_local double g_mkwNiFlushThreshold = 0.0;
|
||||
|
||||
// Host FP control register access (MXCSR on x86_64, FPCR on AArch64), abstracted so
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (ppc_isa_context.h) don't each need
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (guest_abi/cpu_context.h) don't each need
|
||||
// their own per-arch branch.
|
||||
inline uint32_t MkwGetHostFpControl() noexcept
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
#include "ppc_isa_context.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "ppc_isa_float.h"
|
||||
#include "big_endian.h"
|
||||
|
||||
@@ -1385,37 +1385,13 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStackInline(uint32_t addr, double value)
|
||||
// Context-free PSQ entries for translated regions which own GQR state as an
|
||||
// ordinary native value. All architecturally valid quantization encodings are
|
||||
// handled directly; reserved encodings retain the generic helper's abort.
|
||||
template <uint32_t W, uint32_t I, bool Stack>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD inline double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr)
|
||||
{
|
||||
static_assert(W <= 1u && I < 8u);
|
||||
const uint32_t type = (gqr >> 16) & 0x7u;
|
||||
const uint32_t scale = (gqr >> 24) & 0x3Fu;
|
||||
if constexpr (W == 0u)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 0u: return Stack ? PpcLoadPairPsqFloatStackInline(addr) : PpcLoadPairPsqFloatFastInline(addr);
|
||||
case 4u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
|
||||
case 5u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
|
||||
case 6u: return Stack ? PpcLoadPairPsqIntegerStackInline<int8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
|
||||
case 7u: return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
|
||||
default: std::abort();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 0u: return Stack ? PpcLoadSinglePsqFloatStackInline(addr) : PpcLoadSinglePsqFloatFastInline(addr);
|
||||
case 4u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
|
||||
case 5u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
|
||||
case 6u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
|
||||
case 7u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
|
||||
default: std::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
template <uint32_t W, bool Stack>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr);
|
||||
|
||||
extern template double PPC_PsqLStateFallback<0u, false>(uint32_t, uint32_t);
|
||||
extern template double PPC_PsqLStateFallback<0u, true>(uint32_t, uint32_t);
|
||||
extern template double PPC_PsqLStateFallback<1u, false>(uint32_t, uint32_t);
|
||||
extern template double PPC_PsqLStateFallback<1u, true>(uint32_t, uint32_t);
|
||||
|
||||
// Keep the normal explicit-state path small and directly optimizable. Exact
|
||||
// unscaled encodings cover the SDK's common GQR setup; scaled and reserved
|
||||
@@ -1442,41 +1418,17 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLStateInline(uint32_t gqr, uint32_t addr)
|
||||
if constexpr (W == 0u) return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, 0u) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, 0u);
|
||||
else return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, 0u) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, 0u);
|
||||
default:
|
||||
return PPC_PsqLStateFallback<W, I, Stack>(gqr, addr);
|
||||
return PPC_PsqLStateFallback<W, Stack>(gqr, addr);
|
||||
}
|
||||
}
|
||||
|
||||
template <uint32_t W, uint32_t I, bool Stack>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD inline void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value)
|
||||
{
|
||||
static_assert(W <= 1u && I < 8u);
|
||||
const uint32_t type = gqr & 0x7u;
|
||||
const uint32_t scale = (gqr >> 8) & 0x3Fu;
|
||||
if constexpr (W == 0u)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 0u: Stack ? PpcStorePairPsqFloatStackInline(addr, value) : PpcStorePairPsqFloatFastInline(addr, value); return;
|
||||
case 4u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
|
||||
case 5u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
|
||||
case 6u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
|
||||
case 7u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
|
||||
default: std::abort();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 0u: if constexpr (Stack) PpcStoreSinglePsqFloatStackInline(addr, value); else PpcStoreSinglePsqFloatFastInline(addr, value); return;
|
||||
case 4u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
|
||||
case 5u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
|
||||
case 6u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
|
||||
case 7u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
|
||||
default: std::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
template <uint32_t W, bool Stack>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value);
|
||||
|
||||
extern template void PPC_PsqStStateFallback<0u, false>(uint32_t, uint32_t, double);
|
||||
extern template void PPC_PsqStStateFallback<0u, true>(uint32_t, uint32_t, double);
|
||||
extern template void PPC_PsqStStateFallback<1u, false>(uint32_t, uint32_t, double);
|
||||
extern template void PPC_PsqStStateFallback<1u, true>(uint32_t, uint32_t, double);
|
||||
|
||||
template <uint32_t W, uint32_t I, bool Stack>
|
||||
MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, double value)
|
||||
@@ -1505,11 +1457,18 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, doub
|
||||
else { if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, 0u); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, 0u); }
|
||||
return;
|
||||
default:
|
||||
PPC_PsqStStateFallback<W, I, Stack>(gqr, addr, value);
|
||||
PPC_PsqStStateFallback<W, Stack>(gqr, addr, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
template <uint32_t W>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLResolvedStateFallback(
|
||||
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr);
|
||||
|
||||
extern template double PPC_PsqLResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t);
|
||||
extern template double PPC_PsqLResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t);
|
||||
|
||||
template <uint32_t W, uint32_t I>
|
||||
MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
|
||||
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr)
|
||||
@@ -1517,33 +1476,41 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
|
||||
static_assert(W <= 1u && I < 8u);
|
||||
if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, I, false>(gqr, addr);
|
||||
const uint32_t type = (gqr >> 16) & 0x7u;
|
||||
const uint32_t scale = (gqr >> 24) & 0x3Fu;
|
||||
if (type == 0u) {
|
||||
if constexpr (W == 0u) return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
|
||||
else return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
|
||||
}
|
||||
if constexpr (W == 0u)
|
||||
{
|
||||
switch (type)
|
||||
switch (gqr & 0x3F070000u)
|
||||
{
|
||||
case 0u: return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
|
||||
case 4u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
|
||||
case 5u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
|
||||
case 6u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
|
||||
case 7u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
|
||||
default: std::abort();
|
||||
case 0x00040000u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00050000u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00060000u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00070000u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
|
||||
default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
switch (gqr & 0x3F070000u)
|
||||
{
|
||||
case 0u: return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
|
||||
case 4u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
|
||||
case 5u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
|
||||
case 6u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
|
||||
case 7u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
|
||||
default: std::abort();
|
||||
case 0x00040000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00050000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00060000u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
|
||||
case 0x00070000u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
|
||||
default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <uint32_t W>
|
||||
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStResolvedStateFallback(
|
||||
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value);
|
||||
|
||||
extern template void PPC_PsqStResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
|
||||
extern template void PPC_PsqStResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
|
||||
|
||||
template <uint32_t W, uint32_t I>
|
||||
MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
|
||||
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value)
|
||||
@@ -1555,29 +1522,32 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
|
||||
return;
|
||||
}
|
||||
const uint32_t type = gqr & 0x7u;
|
||||
const uint32_t scale = (gqr >> 8) & 0x3Fu;
|
||||
if (type == 0u) {
|
||||
if constexpr (W == 0u) PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value);
|
||||
else PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value);
|
||||
return;
|
||||
}
|
||||
if constexpr (W == 0u)
|
||||
{
|
||||
switch (type)
|
||||
switch (gqr & 0x3F07u)
|
||||
{
|
||||
case 0u: PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
|
||||
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
default: std::abort();
|
||||
case 0x3D04u: PpcStorePairPsqU8Scale61ResolvedInline(resolvedHost, offset, addr, value); return;
|
||||
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
switch (gqr & 0x3F07u)
|
||||
{
|
||||
case 0u: PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
|
||||
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
|
||||
default: std::abort();
|
||||
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
|
||||
default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-103
@@ -8,109 +8,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Memory {
|
||||
public:
|
||||
using DeferredReadCallback = bool (*)(void* user);
|
||||
|
||||
static constexpr size_t kMem1Size = 24u * 1024u * 1024u;
|
||||
static constexpr size_t kMem2Size = 128u * 1024u * 1024u;
|
||||
static constexpr uint32_t kMem1PhysicalBase = 0x00000000u;
|
||||
static constexpr uint32_t kMem1CachedBase = 0x80000000u;
|
||||
static constexpr uint32_t kMem1UncachedBase = 0xC0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalBase = 0x10000000u;
|
||||
static constexpr uint32_t kMem2CachedBase = 0x90000000u;
|
||||
static constexpr uint32_t kMem2UncachedBase = 0xD0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalEnd =
|
||||
kMem2PhysicalBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2CachedEnd =
|
||||
kMem2CachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2UncachedEnd =
|
||||
kMem2UncachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
|
||||
struct RegionConfig {
|
||||
std::string name;
|
||||
uint32_t baseAddress = 0;
|
||||
size_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<RegionConfig> regions;
|
||||
static Config WiiDefaults();
|
||||
};
|
||||
|
||||
class AccessViolation : public std::runtime_error {
|
||||
public:
|
||||
AccessViolation(uint32_t address, size_t length, std::string_view reason);
|
||||
|
||||
uint32_t address() const noexcept { return address_; }
|
||||
size_t length() const noexcept { return length_; }
|
||||
std::string_view reason() const noexcept { return reason_; }
|
||||
|
||||
private:
|
||||
uint32_t address_ = 0;
|
||||
size_t length_ = 0;
|
||||
std::string reason_;
|
||||
};
|
||||
|
||||
static void Init(const Config& config);
|
||||
// Single flat MEM1 region. Not used by the shipped runtime, but the
|
||||
// translator integration-test harnesses emit calls to it.
|
||||
static void Init(size_t mem1Size);
|
||||
static void Reset();
|
||||
// Executable ranges are normally registered during startup. Rebuild the
|
||||
// writable fast-path classification after each registration so a page
|
||||
// previously classified as ordinary data cannot retain a stale direct
|
||||
// write bias.
|
||||
static void RefreshWritableFastPathsForExecutableRanges();
|
||||
|
||||
static uint8_t Read8(uint32_t addr);
|
||||
static uint16_t Read16(uint32_t addr);
|
||||
static uint32_t Read32(uint32_t addr);
|
||||
static uint64_t Read64(uint32_t addr);
|
||||
static float ReadFloat32(uint32_t addr);
|
||||
static double ReadFloat64(uint32_t addr);
|
||||
static void Write8(uint32_t addr, uint8_t val);
|
||||
static void Write16(uint32_t addr, uint16_t val);
|
||||
static void Write32(uint32_t addr, uint32_t val);
|
||||
static void Write64(uint32_t addr, uint64_t val);
|
||||
static void WriteFloat32(uint32_t addr, double val);
|
||||
static void WriteFloat64(uint32_t addr, double val);
|
||||
|
||||
// Exception-safe scalar access for HLE code. These keep Read32/Write32's
|
||||
// full mapping behavior and only convert an unmapped address into a failure
|
||||
// result; they are deliberately not MemoryInline::Try*GuestScalar, which is
|
||||
// the translated-code fast path over the page table.
|
||||
static bool TryRead32(uint32_t addr, uint32_t& value) noexcept {
|
||||
try {
|
||||
value = Read32(addr);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryWrite32(uint32_t addr, uint32_t value) noexcept {
|
||||
try {
|
||||
Write32(addr, value);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t* GetPointer(uint32_t addr);
|
||||
static uint8_t* GetPointer(uint32_t addr, size_t length);
|
||||
static bool Contains(uint32_t addr, size_t length = 1);
|
||||
|
||||
static uint64_t RegisterDeferredRead(uint32_t addr, size_t length,
|
||||
DeferredReadCallback callback, void* user);
|
||||
static void ClearDeferredReads();
|
||||
|
||||
// sizeBytes reports the storage actually allocated, which is not always the
|
||||
// configured size (aliased MEM1/MEM2 windows are clamped to what is behind
|
||||
// them). Used by the crash dump.
|
||||
static std::vector<RegionConfig> DescribeRegions();
|
||||
};
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
// The translated-code access layer is kept separately from the public memory API.
|
||||
#include "memory_access.h"
|
||||
|
||||
@@ -4,729 +4,4 @@
|
||||
#include "memory.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
// Hooks for GX HLE FIFO handling
|
||||
extern "C" {
|
||||
void GX_HLE_FIFO_WriteFloat(float val);
|
||||
void GX_HLE_FIFO_Write32(uint32_t val);
|
||||
void GX_HLE_FIFO_Write16(uint16_t val);
|
||||
void GX_HLE_FIFO_Write8(uint8_t val);
|
||||
void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
}
|
||||
|
||||
namespace MemoryInline {
|
||||
#if defined(_WIN32)
|
||||
#define MKW_MEMORY_FORCE_INLINE __forceinline
|
||||
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
// See runtime/include/isa/ppc_isa_config.h for why non-Windows Clang needs the GNU-attribute
|
||||
// spellings instead of the MS-extension keywords.
|
||||
#define MKW_MEMORY_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_MEMORY_NO_INLINE __attribute__((noinline))
|
||||
#endif
|
||||
#define MKW_MEMORY_COLD __attribute__((cold))
|
||||
inline constexpr uint32_t kPageShift = 20;
|
||||
inline constexpr uint32_t kPageSize = 1u << kPageShift;
|
||||
inline constexpr uint32_t kPageMask = kPageSize - 1u;
|
||||
inline constexpr uint32_t kPageCount = 1u << (32 - kPageShift);
|
||||
inline constexpr uint32_t kMaxFastScalarSize = 8;
|
||||
inline constexpr uint32_t kWritableSubPageShift = RecompMod::kExecutableWriteGuardPageShift;
|
||||
inline constexpr uint32_t kWritableSubPageSize = 1u << kWritableSubPageShift;
|
||||
inline constexpr uint32_t kWritableSubPagesPerPage = kPageSize / kWritableSubPageSize;
|
||||
|
||||
struct PageEntry {
|
||||
uint8_t* base = nullptr;
|
||||
uint32_t limit = 0;
|
||||
};
|
||||
|
||||
extern PageEntry g_pageTable[kPageCount];
|
||||
// Encoded (host page base - guest page base) + 1 for full pages whose next
|
||||
// page is contiguous. This permits any native access up to 8 bytes without a
|
||||
// per-access mask/limit check. Zero retains the general PageEntry fallback.
|
||||
extern uintptr_t g_fullPageBias[kPageCount];
|
||||
// Runtime-active readable biases. Deferred-read pages clear these entries once
|
||||
// instead of paying a mode branch on every translated read.
|
||||
extern uintptr_t g_fullReadablePageBias[kPageCount];
|
||||
// Same encoding, but only for pages proven not to contain executable bytes.
|
||||
// Executable-range registration invalidates entries before guest execution.
|
||||
extern uintptr_t g_fullWritablePageBias[kPageCount];
|
||||
|
||||
// Allocated only for mapped 1 MiB pages that contain both executable and data
|
||||
// 4 KiB pages. Entries use the same encoded host bias as the coarse table.
|
||||
// Exact executable bits remain authoritative and are checked at lookup time,
|
||||
// including both sides of a cross-4-KiB access.
|
||||
struct SparseWritablePageTable {
|
||||
uintptr_t encodedBias[kWritableSubPagesPerPage]{};
|
||||
};
|
||||
extern const SparseWritablePageTable* g_sparseWritablePageTables[kPageCount];
|
||||
|
||||
// Nonzero while any registered deferred read overlaps the page. Small
|
||||
// mappings (e.g. the 16 KiB locked cache) have no coarse bias entry, so a
|
||||
// zero readable bias alone cannot distinguish "deferred content pending"
|
||||
// from "small but plain memory"; range resolution needs the explicit flag.
|
||||
extern uint8_t g_deferredReadCoveredPages[kPageCount];
|
||||
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint8_t Read8Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint16_t Read16Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint32_t Read32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint64_t Read64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadFloat32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadFloat64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write8Slow(uint32_t addr, uint8_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write16Slow(uint32_t addr, uint16_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write32Slow(uint32_t addr, uint32_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write64Slow(uint32_t addr, uint64_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat32Slow(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat64Slow(uint32_t addr, double val);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD T ReadResolvedFallback(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadResolvedFallbackFloat32(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadResolvedFallbackFloat64(uint32_t addr);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallback(uint32_t addr, T value);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat32(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat64(uint32_t addr, double val);
|
||||
bool ResolveDeferredReads(uint32_t addr, size_t length);
|
||||
|
||||
constexpr bool IsMmioAddress(uint32_t addr) {
|
||||
return addr >= 0xCC000000u && addr < 0xCE000000u;
|
||||
}
|
||||
|
||||
constexpr bool IsGpuFifoAddress(uint32_t addr) {
|
||||
return addr >= 0xCC008000u && addr < 0xCC008100u;
|
||||
}
|
||||
|
||||
// Page protections can't cover this: an MMIO write must reach GX HLE with its value or be
|
||||
// reported, and a fault record can't carry the value, so this mask/compare sits in front of
|
||||
// every flat store instead.
|
||||
MKW_MEMORY_FORCE_INLINE bool FlatWriteNeedsPolicy(uint32_t address) {
|
||||
return (address & 0xFE000000u) == 0xCC000000u; // 0xCC000000..0xCDFFFFFF
|
||||
}
|
||||
|
||||
// Gekko stfs conversion is a bit-level narrowing operation. In particular it
|
||||
// does not behave like a host double-to-float cast for values which were left
|
||||
// in double precision, and it has hardware-tested handling for tiny values.
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ConvertPpcDoubleToSingleBits(double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const uint32_t exponent = static_cast<uint32_t>((bits >> 52) & 0x7FFu);
|
||||
// The subnormal-single arm applies to exactly the exponents 874..896; every
|
||||
// other exponent takes the plain sign/exponent/fraction narrowing below.
|
||||
// Inside that window the exponent field is nonzero, so the magnitude cannot
|
||||
// be zero and needs no separate test - the zero case (exponent 0) reaches
|
||||
// the narrowing exactly as it did when the two arms shared that test.
|
||||
if (exponent - 874u <= 22u) [[unlikely]]
|
||||
{
|
||||
uint32_t narrowed = static_cast<uint32_t>(
|
||||
0x80000000ULL | ((bits & 0x000FFFFFFFFFFFFFULL) >> 21));
|
||||
narrowed >>= (905u - exponent);
|
||||
narrowed |= static_cast<uint32_t>((bits >> 32) & 0x80000000ULL);
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
// Results below the documented conversion range are architecturally
|
||||
// undefined; this is the behavior measured on Gekko/Broadway hardware.
|
||||
return static_cast<uint32_t>(
|
||||
((bits >> 32) & 0xC0000000ULL) | ((bits >> 29) & 0x3FFFFFFFULL));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float PpcSingleBitsToFloat(uint32_t bits) {
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetPointerFast(uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
const uint32_t page = address >> kPageShift;
|
||||
if (length <= 8) {
|
||||
const uintptr_t encodedBias = g_fullPageBias[page];
|
||||
if (encodedBias != 0) {
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[page];
|
||||
if (!entry.base || offset + length > entry.limit) {
|
||||
pointer = nullptr;
|
||||
return false;
|
||||
}
|
||||
pointer = entry.base + offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint8_t* GetPointerFast(uint32_t address, size_t length) {
|
||||
uint8_t* pointer = nullptr;
|
||||
return TryGetPointerFast(address, length, pointer) ? pointer : nullptr;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetWritablePointerFast(
|
||||
uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
// The sparse-table hit below distinguishes itself from the small-mapping
|
||||
// hit through `pointer`, so the out-parameter must start null regardless
|
||||
// of what the caller passed in. (A caller handing in an uninitialized
|
||||
// pointer used to turn every sparse hit into a write through stack
|
||||
// garbage - random host memory corruption.)
|
||||
pointer = nullptr;
|
||||
if (length == 0 || length > 8 || address > UINT32_MAX - (length - 1))
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(length - 1);
|
||||
const uint32_t firstExactPage = address >> kWritableSubPageShift;
|
||||
const uint32_t lastExactPage = endAddress >> kWritableSubPageShift;
|
||||
if (encodedBias != 0 && (endAddress >> kPageShift) != coarsePage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (encodedBias == 0) {
|
||||
const auto* subTable = g_sparseWritablePageTables[coarsePage];
|
||||
if (subTable != nullptr) {
|
||||
encodedBias = subTable->encodedBias[
|
||||
(address & kPageMask) >> kWritableSubPageShift];
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
} else {
|
||||
// Small mappings such as Broadway's 16 KiB locked cache cannot
|
||||
// populate the full-1-MiB bias table. Keep them native by proving
|
||||
// the exact access against the ordinary page entry, then applying
|
||||
// the same executable-write policy as a sparse-table hit.
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[coarsePage];
|
||||
if (!entry.base || offset + length > entry.limit)
|
||||
return false;
|
||||
pointer = entry.base + offset;
|
||||
}
|
||||
|
||||
// The exact 4 KiB guard is the final authority. Checking it on every
|
||||
// checked/sparse hit also makes later executable-range registration
|
||||
// safe when a prebuilt table still contains the old mapped bias.
|
||||
if (RecompMod::g_executableWriteGuardPages[firstExactPage].load(
|
||||
std::memory_order_relaxed) != 0 ||
|
||||
(lastExactPage != firstExactPage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointer != nullptr)
|
||||
return true;
|
||||
}
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flat form: guest_flat_memory.h's page protections already answer mapped/non-deferred/non-executable, so resolving is pure address arithmetic.
|
||||
// Two checks stay inline: a wrapped guest address can't survive 64-bit `host + rangeOffset`, and an MMIO write's value isn't recoverable from a
|
||||
// fault record, so a write touching that window must resolve null and fall back to Memory::Write*. Checking both range endpoints is a complete
|
||||
// proof since length <= kPageSize (1 MiB) can't straddle the 32 MiB MMIO window.
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffset, uint32_t length,
|
||||
bool needsRead, bool needsWrite) {
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + guestStart;
|
||||
}
|
||||
|
||||
// Guest-address byte order. Distinct from isa/big_endian.h, which is the
|
||||
// host-pointer codec; do not "unify" them.
|
||||
inline uint16_t ByteSwap16(uint16_t value) {
|
||||
return __builtin_bswap16(value);
|
||||
}
|
||||
|
||||
inline uint32_t ByteSwap32(uint32_t value) {
|
||||
return __builtin_bswap32(value);
|
||||
}
|
||||
|
||||
inline uint64_t ByteSwap64(uint64_t value) {
|
||||
return __builtin_bswap64(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T MaybeByteSwap(T value) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
return value;
|
||||
} else if constexpr (sizeof(T) == 2) {
|
||||
return static_cast<T>(ByteSwap16(static_cast<uint16_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
return static_cast<T>(ByteSwap32(static_cast<uint32_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 8) {
|
||||
return static_cast<T>(ByteSwap64(static_cast<uint64_t>(value)));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool ReadResolvedScalar(uint8_t* host, uint32_t rangeOffset, T& outValue) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = host[rangeOffset];
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, host + rangeOffset, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ResolvedLoadPair {
|
||||
uint32_t first = 0;
|
||||
uint32_t second = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset) {
|
||||
uint32_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {packed >> 16, packed & 0xFFFFu, true};
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair32(uint8_t* host, uint32_t rangeOffset) {
|
||||
uint64_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {static_cast<uint32_t>(packed >> 32), static_cast<uint32_t>(packed), true};
|
||||
}
|
||||
|
||||
template <typename Packed>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPairFast(
|
||||
uint8_t* host, uint32_t rangeOffset, Packed packed) {
|
||||
if (!host) return false;
|
||||
const Packed swapped = MaybeByteSwap(packed);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(swapped));
|
||||
return true;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset, uint32_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair32(
|
||||
uint8_t* host, uint32_t rangeOffset, uint64_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedScalar(uint8_t* host, uint32_t rangeOffset, T value) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
host[rangeOffset] = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool TryReadMappedScalar(uint32_t address, T& outValue) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryReadGuestScalar(uint32_t address, T& outValue) {
|
||||
const uintptr_t encodedBias = g_fullReadablePageBias[address >> kPageShift];
|
||||
if (encodedBias == 0) [[unlikely]]
|
||||
return false;
|
||||
auto* ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryWriteGuestScalar(uint32_t address, T value) {
|
||||
static_assert(sizeof(T) >= 1 && sizeof(T) <= kMaxFastScalarSize);
|
||||
// A mixed executable/data 1 MiB page zeroes the coarse writable bias even though most of
|
||||
// its 4 KiB sub-pages are plain data; MKW's THP buffers share such a page with .text, which
|
||||
// used to force ~15% of total CPU through the cold path. The sparse sub-page tier below
|
||||
// keeps those stores native while the exact 4 KiB executable guards stay authoritative.
|
||||
if (address > UINT32_MAX - static_cast<uint32_t>(sizeof(T) - 1u)) [[unlikely]]
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(sizeof(T) - 1u);
|
||||
if ((endAddress >> kPageShift) != coarsePage) [[unlikely]]
|
||||
return false;
|
||||
const uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
uint8_t* ptr = nullptr;
|
||||
if (encodedBias != 0) {
|
||||
ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
} else if (!TryGetWritablePointerFast(address, sizeof(T), ptr)) [[unlikely]] {
|
||||
return false;
|
||||
}
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool WriteStackScalarFast(uint32_t address, T value) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline uint8_t ReadStack8(uint32_t address) {
|
||||
uint8_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read8(address);
|
||||
}
|
||||
|
||||
inline uint16_t ReadStack16(uint32_t address) {
|
||||
uint16_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read16(address);
|
||||
}
|
||||
|
||||
inline uint32_t ReadStack32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read32(address);
|
||||
}
|
||||
|
||||
inline uint64_t ReadStack64(uint32_t address) {
|
||||
uint64_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read64(address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline void WriteStack8(uint32_t address, uint8_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write8(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack16(uint32_t address, uint16_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write16(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack32(uint32_t address, uint32_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack64(uint32_t address, uint64_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat32(uint32_t address, double value) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename SlowRead>
|
||||
MKW_MEMORY_FORCE_INLINE T ReadResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address,
|
||||
SlowRead slow) {
|
||||
T value = 0;
|
||||
if (ReadResolvedScalar(host, rangeOffset, value)) {
|
||||
return value;
|
||||
}
|
||||
[[unlikely]] return slow(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t ReadResolved8(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint8_t>(r, o, a, ReadResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t ReadResolved16(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint16_t>(r, o, a, ReadResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ReadResolved32(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint32_t>(r, o, a, ReadResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h (the psq resolved tier packs two lanes into
|
||||
// one 64-bit access); generated code never names it directly.
|
||||
MKW_MEMORY_FORCE_INLINE uint64_t ReadResolved64(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint64_t>(r, o, a, ReadResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE float ReadResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint32_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat32(a);
|
||||
float value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE double ReadResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint64_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat64(a);
|
||||
double value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
|
||||
template <typename T, typename SlowWrite>
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address, T value,
|
||||
SlowWrite slow) {
|
||||
if (WriteResolvedScalar(host, rangeOffset, value)) {
|
||||
return;
|
||||
}
|
||||
[[unlikely]] slow(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved8(uint8_t* r, uint32_t o, uint32_t a, uint8_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved16(uint8_t* r, uint32_t o, uint32_t a, uint16_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved32(uint8_t* r, uint32_t o, uint32_t a, uint32_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h, as ReadResolved64 above.
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved64(uint8_t* r, uint32_t o, uint32_t a, uint64_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(v);
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat32(a, v);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
uint64_t bits; std::memcpy(&bits, &v, sizeof(bits));
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat64(a, v);
|
||||
}
|
||||
|
||||
// Flat guest memory (audit item T-MEM): the 4 GiB reservation makes a guest access a byte swap
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
T value{};
|
||||
std::memcpy(&value, MKW_FLAT_GUEST_BASE + address, sizeof(T));
|
||||
return MaybeByteSwap(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
// Check-free stores: emitted ONLY for addresses the translator proved at translate time are ordinary guest RAM (r1-relative stack slots, ~45%
|
||||
// of flat stores), skipping the MMIO mask/compare that's pure overhead there. Still safe if that proof were ever wrong: the flat view maps
|
||||
// 0xCC000000..0xCDFFFFFF PAGE_NOACCESS, so a stray MMIO store faults into the same handler and diagnostic as the checked path, just reported
|
||||
// instead of dispatched inline. Never use these for an address the translator hasn't proven.
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam8(uint32_t address, uint8_t value) {
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam16(uint32_t address, uint16_t value) {
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam32(uint32_t address, uint32_t value) {
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat32(uint32_t address, double value) {
|
||||
FlatStore<uint32_t>(address, ConvertPpcDoubleToSingleBits(value));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
} // namespace MemoryInline
|
||||
|
||||
#undef MKW_MEMORY_FORCE_INLINE
|
||||
#undef MKW_MEMORY_NO_INLINE
|
||||
#undef MKW_MEMORY_COLD
|
||||
|
||||
inline uint8_t Memory::Read8(uint32_t addr) {
|
||||
uint8_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read8Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint16_t Memory::Read16(uint32_t addr) {
|
||||
uint16_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read16Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint32_t Memory::Read32(uint32_t addr) {
|
||||
uint32_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read32Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint64_t Memory::Read64(uint32_t addr) {
|
||||
uint64_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read64Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float Memory::ReadFloat32(uint32_t addr) {
|
||||
uint32_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat32Slow(addr);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline double Memory::ReadFloat64(uint32_t addr) {
|
||||
uint64_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat64Slow(addr);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void Memory::Write8(uint32_t addr, uint8_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write8Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write16(uint32_t addr, uint16_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write16Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write32(uint32_t addr, uint32_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write64(uint32_t addr, uint64_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write64Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat32(uint32_t addr, double val) {
|
||||
{
|
||||
const uint32_t bits = MemoryInline::ConvertPpcDoubleToSingleBits(val);
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat64(uint32_t addr, double val) {
|
||||
{
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &val, sizeof(bits));
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat64Slow(addr, val);
|
||||
}
|
||||
#include "guest_abi/memory_access.h"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
namespace MusicAttenuation {
|
||||
|
||||
// Enables the optional external-media integration (Windows media sessions,
|
||||
// or MPRIS over D-Bus on Linux). The monitor is started lazily the first time
|
||||
// Core Audio output activity on macOS, or MPRIS over D-Bus on Linux).
|
||||
// The monitor is started lazily the first time
|
||||
// this is enabled.
|
||||
void SetEnabled(bool enabled) noexcept;
|
||||
void SetMusicVolume(float volume) noexcept;
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
// Host memory seam for isa/ppc_isa_quantized.h. That header includes
|
||||
// "ppc_isa_memory.h" by plain quoted name and expects whatever it resolves to
|
||||
// to supply the Memory / MemoryInline APIs and the flat-guest-base macros its
|
||||
// psq fast paths use. This file is this project's implementation of that
|
||||
// contract; memory.h chains memory_access.h, which defines MemoryInline.
|
||||
// psq fast paths use. Part of the guest ABI, so it resolves to guest_abi/ only.
|
||||
|
||||
#include "memory.h" // chains memory_access.h, which supplies MemoryInline and
|
||||
// includes guest_flat_memory.h for the flat-base macros
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
#include "isa/ppc_isa_int.h"
|
||||
#include "isa/ppc_isa_float.h"
|
||||
#include "isa/ppc_isa_quantized.h"
|
||||
#include "memory.h"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "guest_abi/exec_guard.h"
|
||||
|
||||
struct CpuContext;
|
||||
|
||||
namespace RecompMod {
|
||||
@@ -20,46 +22,6 @@ struct MemoryReservation {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Diagnostic-only, read by fatal reporters (unmapped access, executable-write, CPU dumps).
|
||||
// Defined here as `inline thread_local` with a constant initializer, not `extern thread_local`
|
||||
// in the .cpp: every indirect dispatch scopes this, so an out-of-line ctor/dtor would cost two
|
||||
// un-inlinable calls plus a register spill each for three instructions of work.
|
||||
inline thread_local uint32_t g_currentTranslatedExecutionAddress = 0;
|
||||
|
||||
class ScopedTranslatedExecutionAddress {
|
||||
public:
|
||||
explicit ScopedTranslatedExecutionAddress(uint32_t address) noexcept
|
||||
: previous_(g_currentTranslatedExecutionAddress) {
|
||||
// Address 0 means "nothing new to report" - the enclosing scope's value
|
||||
// stays visible, and the unconditional restore below keeps nesting exact.
|
||||
if (address != 0) {
|
||||
g_currentTranslatedExecutionAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedTranslatedExecutionAddress() noexcept {
|
||||
g_currentTranslatedExecutionAddress = previous_;
|
||||
}
|
||||
|
||||
ScopedTranslatedExecutionAddress(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
ScopedTranslatedExecutionAddress& operator=(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
|
||||
private:
|
||||
uint32_t previous_ = 0;
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageShift = 12;
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageCount = 1u << (32 - kExecutableWriteGuardPageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageShift = 20;
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageCount = 1u << (32 - kExecutableWriteGuardCoarsePageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageShift = 16;
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageCount = 1u << (32 - kExecutableWriteGuardMidPageShift);
|
||||
|
||||
extern std::atomic<bool> g_executableWriteGuardEnabled;
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount];
|
||||
|
||||
// Two initializer phases, both emitted by the translator's mod data-patch
|
||||
// writer. (A third, plain RegisterInitializer/RunInitializers pair existed with
|
||||
// no registrant on either side and was removed.)
|
||||
|
||||
+111
-145
@@ -42,6 +42,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<std::string> graphicsApi;
|
||||
std::optional<std::string> displayMode;
|
||||
std::optional<uint32_t> frameInterpolationFps;
|
||||
std::optional<bool> metalFxSpatialUpscaling;
|
||||
std::optional<bool> skipUnreadyPipelines;
|
||||
std::optional<bool> disableCopyFilter;
|
||||
std::optional<bool> textureReplacements;
|
||||
@@ -56,25 +57,16 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> audioMuted;
|
||||
std::optional<bool> audioMixWorker;
|
||||
std::optional<bool> attenuateMusicWhenMediaPlays;
|
||||
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
|
||||
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
|
||||
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
|
||||
// Real Wii Remotes passed through to the game's own Bluetooth stack. Off hands
|
||||
// Nintendo controllers (e.g. the Wii U Pro Controller) to SDL as plain gamepads.
|
||||
std::optional<bool> wiiRemotes;
|
||||
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
|
||||
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
|
||||
// after launch shows up without restarting.
|
||||
std::optional<bool> wiiContinuousScan;
|
||||
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest.
|
||||
std::optional<double> wiiAccelOffsetX;
|
||||
std::optional<double> wiiAccelOffsetY;
|
||||
std::optional<double> wiiAccelOffsetZ;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
// Look for remotes in pairing mode (1+2 / SYNC) while none is connected.
|
||||
std::optional<bool> wiiAutoPair;
|
||||
// The console's Wii Remote settings (SYSCONF BT.BAR, BT.SENS, BT.SPKV, BT.MOT).
|
||||
std::optional<bool> wiiSensorBarAbove;
|
||||
std::optional<int32_t> wiiIrSensitivity;
|
||||
std::optional<int32_t> wiiSpeakerVolume;
|
||||
std::optional<bool> wiiRumble;
|
||||
std::optional<bool> networkEnabled;
|
||||
std::optional<bool> discordPresenceEnabled;
|
||||
// The application ID of the WiiCompiled Discord application. This is only
|
||||
@@ -137,28 +129,6 @@ inline std::string Trim(std::string_view text) {
|
||||
return std::string(text.substr(begin, end - begin));
|
||||
}
|
||||
|
||||
inline std::string RemoveComment(std::string_view line) {
|
||||
bool inSingle = false;
|
||||
bool inDouble = false;
|
||||
bool escaped = false;
|
||||
for (size_t i = 0; i < line.size(); ++i) {
|
||||
const char ch = line[i];
|
||||
if (inDouble && ch == '\\' && !escaped) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
} else if (ch == '"' && !inSingle && !escaped) {
|
||||
inDouble = !inDouble;
|
||||
} else if (ch == '#' && !inSingle && !inDouble) {
|
||||
return std::string(line.substr(0, i));
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
return std::string(line);
|
||||
}
|
||||
|
||||
inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
static constexpr std::array values{0.0f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, 8.0f};
|
||||
return std::find(values.begin(), values.end(), value) != values.end();
|
||||
@@ -459,6 +429,8 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.frameInterpolationFps = migrated;
|
||||
}
|
||||
}
|
||||
config.metalFxSpatialUpscaling =
|
||||
FindConfigValue<bool>(document, "video", "metalfx_spatial_upscaling");
|
||||
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
|
||||
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
|
||||
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
|
||||
@@ -483,11 +455,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.attenuateMusicWhenMediaPlays =
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
|
||||
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
|
||||
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
config.wiiAutoPair = FindConfigValue<bool>(document, "controller", "wii_auto_pair");
|
||||
if (const auto bar = FindConfigValue<std::string>(document, "controller", "wii_sensor_bar")) {
|
||||
if (*bar == "above" || *bar == "below") config.wiiSensorBarAbove = *bar == "above";
|
||||
}
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_ir_sensitivity"); value && *value >= 1 && *value <= 5) {
|
||||
config.wiiIrSensitivity = *value;
|
||||
}
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_speaker_volume"); value && *value >= 0 && *value <= 127) {
|
||||
config.wiiSpeakerVolume = *value;
|
||||
}
|
||||
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
|
||||
@@ -543,68 +521,50 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
|
||||
return index < Get().controllerButtons.size() ? Get().controllerButtons[index] : empty;
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding comments, unrelated settings, or
|
||||
// user-specific paths. This is used by the in-game F10 settings bar.
|
||||
// Sets section.key (section may be a dotted table path) to a TOML literal, keeping the rest of the
|
||||
// document's keys, order and comments. Returns nullopt when the document or value does not parse.
|
||||
inline std::optional<std::string> UpdateSettingText(const std::string& text, std::string_view section,
|
||||
std::string_view key, std::string_view value) {
|
||||
try {
|
||||
auto root = toml::parse_str<toml::ordered_type_config>(text);
|
||||
auto literal = toml::parse_str<toml::ordered_type_config>("v = " + std::string(value));
|
||||
auto* table = &root;
|
||||
std::istringstream path{std::string(section)};
|
||||
for (std::string part; std::getline(path, part, '.');) {
|
||||
auto& parent = table->as_table();
|
||||
if (!parent.contains(part)) {
|
||||
parent[part] = toml::ordered_value(toml::ordered_table{});
|
||||
}
|
||||
table = &parent.at(part);
|
||||
if (!table->is_table()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
if (table->as_table_fmt().fmt == toml::table_format::implicit) {
|
||||
table->as_table_fmt().fmt = toml::table_format::multiline;
|
||||
}
|
||||
auto& slot = table->as_table()[std::string(key)];
|
||||
auto& updated = literal.as_table().at("v");
|
||||
updated.comments() = slot.comments();
|
||||
slot = std::move(updated);
|
||||
return toml::format(root);
|
||||
} catch (const std::exception&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding unrelated settings or user-specific paths. This is
|
||||
// used by the in-game F10 settings bar. An unparseable file is left untouched.
|
||||
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
|
||||
const auto path = ResolveConfigPath();
|
||||
std::ifstream input(path);
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
std::ostringstream existing;
|
||||
if (std::ifstream input(path, std::ios::binary); input) {
|
||||
existing << input.rdbuf();
|
||||
}
|
||||
|
||||
const std::string normalizedSection = Trim(section);
|
||||
const std::string normalizedKey = Trim(key);
|
||||
size_t sectionStart = lines.size();
|
||||
size_t sectionEnd = lines.size();
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
const std::string trimmed = Trim(RemoveComment(lines[i]));
|
||||
if (trimmed.size() >= 2 && trimmed.front() == '[' && trimmed.back() == ']') {
|
||||
const std::string found = Trim(std::string_view(trimmed).substr(1, trimmed.size() - 2));
|
||||
if (sectionStart != lines.size()) {
|
||||
sectionEnd = i;
|
||||
break;
|
||||
}
|
||||
if (found == normalizedSection) {
|
||||
sectionStart = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string replacement = normalizedKey + " = " + std::string(value);
|
||||
if (sectionStart == lines.size()) {
|
||||
if (!lines.empty() && !lines.back().empty()) {
|
||||
lines.emplace_back();
|
||||
}
|
||||
lines.emplace_back("[" + normalizedSection + "]");
|
||||
lines.push_back(replacement);
|
||||
} else {
|
||||
bool replaced = false;
|
||||
for (size_t i = sectionStart + 1; i < sectionEnd; ++i) {
|
||||
const std::string uncommented = Trim(RemoveComment(lines[i]));
|
||||
const size_t equals = uncommented.find('=');
|
||||
if (equals != std::string::npos && Trim(std::string_view(uncommented).substr(0, equals)) == normalizedKey) {
|
||||
lines[i] = replacement;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
// Append after the section's last real line rather than after the blank line that
|
||||
// separates it from the next header: this file is edited by hand and by the host
|
||||
// installer as well, and a key parked below the separator reads as if it belonged to
|
||||
// the next section. The host writer (Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs)
|
||||
// applies exactly this rule.
|
||||
size_t insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && Trim(lines[insertAt - 1]).empty()) {
|
||||
--insertAt;
|
||||
}
|
||||
lines.insert(lines.begin() + static_cast<std::ptrdiff_t>(insertAt), replacement);
|
||||
}
|
||||
const auto updated = UpdateSettingText(existing.str(), section, key, value);
|
||||
if (!updated) {
|
||||
std::cerr << "[runtime-config] Not updating invalid " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
@@ -616,9 +576,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
output << *updated;
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
@@ -660,6 +618,11 @@ inline bool SetFrameInterpolationFps(uint32_t value) {
|
||||
return WriteSetting("video", "frame_interpolation_fps", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool SetMetalFxSpatialUpscaling(bool value) {
|
||||
Mutable().metalFxSpatialUpscaling = value;
|
||||
return WriteSetting("video", "metalfx_spatial_upscaling", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetDisplayMode(std::string value) {
|
||||
if (!IsSupportedDisplayMode(value)) {
|
||||
return false;
|
||||
@@ -815,6 +778,10 @@ inline float ResolutionMultiplier(float fallback = 1.0f) {
|
||||
return std::max(0.0f, Get().resolutionMultiplier.value_or(fallback));
|
||||
}
|
||||
|
||||
inline bool MetalFxSpatialUpscaling(bool fallback = false) {
|
||||
return Get().metalFxSpatialUpscaling.value_or(fallback);
|
||||
}
|
||||
|
||||
inline float AudioVolume(float fallback = 1.0f) {
|
||||
return std::clamp(Get().audioVolume.value_or(fallback), 0.0f, 1.0f);
|
||||
}
|
||||
@@ -849,62 +816,61 @@ inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
|
||||
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
|
||||
}
|
||||
|
||||
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
|
||||
// subsystem comes up, so a change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = true) {
|
||||
// Bluetooth Wii Remotes. Read once before SDL's joystick subsystem comes up, so a
|
||||
// change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = false) {
|
||||
return Get().wiiRemotes.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the Bluetooth Wii Remote driver switch.
|
||||
inline bool SetWiiRemotesEnabled(bool value) {
|
||||
Mutable().wiiRemotes = value;
|
||||
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
|
||||
inline bool WiiContinuousScanEnabled(bool fallback = false) {
|
||||
return Get().wiiContinuousScan.value_or(fallback);
|
||||
inline bool WiiAutoPairEnabled(bool fallback = true) {
|
||||
return Get().wiiAutoPair.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the continuous scanning switch.
|
||||
inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
Mutable().wiiContinuousScan = value;
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
inline bool SetWiiAutoPairEnabled(bool value) {
|
||||
Mutable().wiiAutoPair = value;
|
||||
return WriteSetting("controller", "wii_auto_pair", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
|
||||
// when the remote has not been calibrated.
|
||||
inline std::array<double, 3> WiiAccelOffset() {
|
||||
const RuntimeUserConfig& config = Get();
|
||||
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
|
||||
config.wiiAccelOffsetZ.value_or(0.0)};
|
||||
// Defaults are the console's.
|
||||
inline bool WiiSensorBarAbove(bool fallback = false) {
|
||||
return Get().wiiSensorBarAbove.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
inline bool SetWiiSensorBarAbove(bool value) {
|
||||
Mutable().wiiSensorBarAbove = value;
|
||||
return WriteSetting("controller", "wii_sensor_bar", value ? "\"above\"" : "\"below\"");
|
||||
}
|
||||
|
||||
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
|
||||
inline bool HasWiiAccelOffset() {
|
||||
const std::array<double, 3> offset = WiiAccelOffset();
|
||||
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
inline int32_t WiiIrSensitivity(int32_t fallback = 3) {
|
||||
return Get().wiiIrSensitivity.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction measured by the overlay's calibration.
|
||||
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsetX = offset[0];
|
||||
Mutable().wiiAccelOffsetY = offset[1];
|
||||
Mutable().wiiAccelOffsetZ = offset[2];
|
||||
bool ok = true;
|
||||
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << offset[i];
|
||||
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
|
||||
}
|
||||
return ok;
|
||||
inline bool SetWiiIrSensitivity(int32_t value) {
|
||||
Mutable().wiiIrSensitivity = value;
|
||||
return WriteSetting("controller", "wii_ir_sensitivity", std::to_string(value));
|
||||
}
|
||||
|
||||
inline int32_t WiiSpeakerVolume(int32_t fallback = 0x59) {
|
||||
return Get().wiiSpeakerVolume.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiSpeakerVolume(int32_t value) {
|
||||
Mutable().wiiSpeakerVolume = value;
|
||||
return WriteSetting("controller", "wii_speaker_volume", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool WiiRumbleEnabled(bool fallback = true) {
|
||||
return Get().wiiRumble.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiRumbleEnabled(bool value) {
|
||||
Mutable().wiiRumble = value;
|
||||
return WriteSetting("controller", "wii_rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct PADStatus;
|
||||
|
||||
// Real Wii Remotes paired over Bluetooth.
|
||||
//
|
||||
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
|
||||
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
|
||||
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
|
||||
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
|
||||
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
|
||||
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
|
||||
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
|
||||
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
|
||||
// extension in or out mid-game switches control scheme like on the console. Only
|
||||
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
|
||||
// aurora's PAD layer as a GameCube pad.
|
||||
namespace WiiRemoteInput {
|
||||
|
||||
enum class Kind : uint8_t {
|
||||
NotWii,
|
||||
Remote, // Wii Remote with no extension
|
||||
RemoteWithNunchuk,
|
||||
RemoteWithClassic,
|
||||
WiiUPro,
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem is initialized (aurora does that
|
||||
// inside aurora_initialize); SDL only consults the hint on its first device scan.
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
|
||||
// Classifies a gamepad by the name SDL's Wii driver reports for it.
|
||||
Kind KindForName(const char* gamepadName);
|
||||
// Classification of the SDL gamepad currently assigned to a game port.
|
||||
Kind KindForPort(uint32_t port);
|
||||
const char* KindLabel(Kind kind);
|
||||
|
||||
// One frame of a Wii Remote in the units KPAD uses.
|
||||
struct KpadSample {
|
||||
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
|
||||
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
|
||||
bool hasNunchuk = false;
|
||||
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
|
||||
float nunchukAcc[3] = {};
|
||||
bool hasClassic = false;
|
||||
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
|
||||
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
|
||||
float clRStick[2] = {};
|
||||
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
|
||||
int16_t clRStickRaw[2] = {};
|
||||
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
|
||||
uint8_t clTriggerR = 0;
|
||||
};
|
||||
|
||||
// What the game should see on `chan`: the controller SDL has there right now,
|
||||
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
|
||||
// destroys and re-creates the joystick when an extension is plugged in or out,
|
||||
// and the console never disconnects for that, so the gap is papered over with
|
||||
// neutral input instead of a "communications interrupted" prompt.
|
||||
Kind EffectiveKind(uint32_t chan);
|
||||
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
|
||||
// Nunchuk or with a Classic Controller (live or within the swap grace period).
|
||||
bool IsRemoteChannel(uint32_t chan);
|
||||
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
|
||||
// is false. During the swap grace period the sample is neutral.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
|
||||
// Remote accelerometer for the overlay readout: the SDL sample in g after the
|
||||
// zero-point correction (x right across the face, y out of the button face, z
|
||||
// towards the user) and the KPAD vector built from it. False when the port has
|
||||
// no remote or SDL has not delivered a sample yet.
|
||||
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
|
||||
|
||||
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
|
||||
// remote's factory calibration block, but over Bluetooth that read often times
|
||||
// out ("Using fallback accelerometer calibration" in console.log) and it falls
|
||||
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
|
||||
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
|
||||
// offset.
|
||||
// StartAccelCalibration expects the remote lying still with the buttons up;
|
||||
// Poll() then collects samples for about a second and stores the difference to
|
||||
// the ideal (0, 1, 0) g in Config.toml.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction.
|
||||
void ClearAccelCalibration();
|
||||
bool IsAccelCalibrating();
|
||||
// 0..1 while a calibration is collecting samples.
|
||||
float AccelCalibrationProgress();
|
||||
// Outcome of the last calibration attempt for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage();
|
||||
|
||||
// Reports "no controller" on the GameCube side for every port served through
|
||||
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
|
||||
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
|
||||
|
||||
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
|
||||
// a failed Bluetooth read or an extension change and never re-adds it on its
|
||||
// own. Poll() runs once per PADRead and, while no Wii controller is present,
|
||||
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
|
||||
void Poll();
|
||||
// Forces one re-enumeration right now (settings overlay "Rescan now").
|
||||
void RescanNow();
|
||||
// True while Poll() is looking for a remote (no Wii controller connected).
|
||||
bool IsScanning();
|
||||
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
|
||||
bool PeriodicRescanEnabled();
|
||||
// Rescans issued since a Wii controller was last seen.
|
||||
uint32_t ScanCount();
|
||||
|
||||
} // namespace WiiRemoteInput
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// The Wii's extension controller encryption: the game writes a random key to
|
||||
// the extension, which then XORs and offsets every byte it reports.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace Wiimote::ExtCrypto {
|
||||
|
||||
struct Tables {
|
||||
std::array<uint8_t, 8> ft{};
|
||||
std::array<uint8_t, 8> sb{};
|
||||
void Encrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
void Decrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
};
|
||||
|
||||
// Tables for the key the game wrote to registers 0x40..0x4F, or nullopt when it
|
||||
// does not come from the first-party key schedule.
|
||||
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData);
|
||||
|
||||
} // namespace Wiimote::ExtCrypto
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// Follows the game's conversation with a remote's extension (type, calibration,
|
||||
// encryption key) so its data can be put at rest while the overlay owns input.
|
||||
|
||||
#include "wiimote_ext_crypto.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
class ExtensionState {
|
||||
public:
|
||||
void ObserveOutput(const uint8_t* report, size_t size);
|
||||
void ObserveInput(const uint8_t* report, size_t size);
|
||||
// Centres the sticks and releases the buttons of the extension data in a
|
||||
// data report, leaving its motion data alone.
|
||||
void Neutralize(std::vector<uint8_t>& report) const;
|
||||
|
||||
private:
|
||||
enum class Type : uint8_t { Unknown, Nunchuk, Classic };
|
||||
|
||||
void Reset();
|
||||
uint8_t Calibration(size_t index, uint8_t fallback) const;
|
||||
|
||||
Type m_type = Type::Unknown;
|
||||
uint8_t m_format = 0;
|
||||
bool m_encrypted = false;
|
||||
std::array<uint8_t, 16> m_key{};
|
||||
std::optional<ExtCrypto::Tables> m_tables;
|
||||
std::array<uint8_t, 16> m_calibration{};
|
||||
bool m_haveCalibration = false;
|
||||
uint32_t m_readAddress = 0;
|
||||
};
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
// Physical Wii Remotes on the PC. Each one is attached to one of the four
|
||||
// virtual remotes of the emulated Bluetooth controller and its HID reports are
|
||||
// passed through unchanged, so the game's own WPAD/KPAD drive it (IR, speaker,
|
||||
// rumble, extensions, battery).
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
struct SlotInfo {
|
||||
bool attached = false;
|
||||
// The game disconnected it and the platform could not power it off; any
|
||||
// button wakes it up again.
|
||||
bool asleep = false;
|
||||
Bluetooth::LinkState link = Bluetooth::LinkState::Down;
|
||||
std::string model; // "Wii Remote" / "Wii Remote Plus"
|
||||
std::string extension; // empty when none
|
||||
int battery = -1; // 0..4 like the game's battery bars, -1 unknown
|
||||
};
|
||||
|
||||
struct PairingInfo {
|
||||
bool supported = false; // this platform can discover and pair remotes itself
|
||||
bool searching = false;
|
||||
std::string status; // last pairing outcome, for the overlay
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem starts (aurora_initialize).
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
// Starts device scanning when Wii Remotes are enabled.
|
||||
void Initialize();
|
||||
// Powers the remotes off, like a console shutting down, and stops the threads.
|
||||
void Shutdown();
|
||||
|
||||
Bluetooth::RemoteHost& Host();
|
||||
|
||||
SlotInfo Slot(size_t slot);
|
||||
PairingInfo Pairing();
|
||||
// The console's SYNC button: look for remotes in pairing mode for a while.
|
||||
void StartPairing();
|
||||
// Ends the game's connection to the remote on `slot` and powers it off.
|
||||
void Disconnect(size_t slot);
|
||||
// Setup problems worth showing (kernel driver, missing permissions), else empty.
|
||||
std::string PlatformNotice();
|
||||
// Connected remotes waiting for a free player slot.
|
||||
size_t WaitingRemotes();
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
// Host Bluetooth services the Wii Remote manager needs from the OS: finding and
|
||||
// pairing remotes in pairing mode, naming a HID device, and dropping a remote's
|
||||
// connection (which powers it off, as a console does).
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
struct SDL_hid_device_info;
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
|
||||
struct Capabilities {
|
||||
bool pairing = false;
|
||||
bool powerOff = false;
|
||||
};
|
||||
|
||||
Capabilities Query();
|
||||
|
||||
// One search for remotes in pairing mode (red SYNC or 1+2); pairs and connects
|
||||
// each one found. Blocks for a few seconds. Returns the number connected.
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status);
|
||||
|
||||
// Disconnects the remote; it switches itself off. False when not possible.
|
||||
bool PowerOff(const std::string& address);
|
||||
|
||||
// Bluetooth address of a HID device as 12 lowercase hex digits, or empty.
|
||||
std::string AddressFor(const SDL_hid_device_info& info);
|
||||
|
||||
// Bluetooth name ("Nintendo RVL-CNT-01-TR", ...) or the HID product string.
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info);
|
||||
|
||||
// False only when the OS knows the remote is not connected right now.
|
||||
bool MaybeConnected(const std::string& address);
|
||||
|
||||
// False when the OS driver would fight over the device (Linux hid-wiimote).
|
||||
bool Usable(const SDL_hid_device_info& info);
|
||||
|
||||
// Setup problems worth showing in the overlay, else empty.
|
||||
std::string Notice();
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
@@ -4,13 +4,15 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "generated/RuntimeConfig.h"
|
||||
#include "guest_small_data.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
// Nothing reads CpuContext by offset; these asserts just canary accidental field reordering
|
||||
@@ -253,6 +255,41 @@ std::string ValidateGeneratedIndirectDispatchLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t FilterScnRendererPathMaskOption(uint32_t pathMask) noexcept {
|
||||
return RuntimeGameGraphicsOptions::FilterScnRendererPathMask(pathMask);
|
||||
}
|
||||
|
||||
void ReportMissingIndirectJumpTarget(uint32_t target, CpuContext* cpu) {
|
||||
// If not registered, this is likely a switch table jump to an intra-function label.
|
||||
// The translator should have recognized this pattern and generated proper switch code.
|
||||
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
|
||||
<< std::hex << target << std::dec
|
||||
<< " is not a registered function.\n"
|
||||
<< "This is likely a switch statement jump table that the translator\n"
|
||||
<< "failed to recognize. The function containing this bctr needs\n"
|
||||
<< "proper switch pattern recognition." << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
|
||||
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
|
||||
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
|
||||
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
|
||||
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
|
||||
DumpHostStackTraceForRuntimeHelper();
|
||||
std::fflush(stderr);
|
||||
std::ostringstream message;
|
||||
message << "The game stopped because an indirect jump targeted guest address 0x"
|
||||
<< std::hex << target
|
||||
<< ", but that address is not a registered translated function.\n\n"
|
||||
<< "The translator may have missed a switch/jump-table target.";
|
||||
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
|
||||
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
|
||||
// atexit reporter, so the crash log has to be written here.
|
||||
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
|
||||
SetRuntimeExitCode(EXIT_FAILURE);
|
||||
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
|
||||
MarkFatalErrorReported();
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table) {
|
||||
std::lock_guard<std::mutex> lock(RegistryMutex());
|
||||
if (RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
@@ -403,7 +440,7 @@ const RawDispatchRecord* TranslatedFunctionRegistry::FindRawByAddressPtrSlow(uin
|
||||
return it != entries.end() && it->address == address ? &*it : nullptr;
|
||||
}
|
||||
|
||||
std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindByHostAddress(uintptr_t hostAddr) {
|
||||
std::optional<TranslatedFunctionInfo> FindTranslatedFunctionByHostAddress(uintptr_t hostAddr) {
|
||||
const bool frozen = RegistryFrozen().load(std::memory_order_acquire);
|
||||
if (!frozen) {
|
||||
std::lock_guard<std::mutex> lock(RegistryMutex());
|
||||
@@ -439,7 +476,7 @@ std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindByHostAddr
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindNearestByAddress(uint32_t address) {
|
||||
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address) {
|
||||
std::optional<TranslatedFunctionInfo> best;
|
||||
uint32_t bestDelta = UINT32_MAX;
|
||||
const auto scan = [&] {
|
||||
@@ -478,9 +515,24 @@ void InitializePersistentCpuContext() {
|
||||
// seed the PowerPC ABI environmental registers if no one has yet:
|
||||
// r2 = _SDA2_BASE_ (read-only small data), r13 = _SDA_BASE_.
|
||||
if (g_persistentCpu.gpr[2] == 0) {
|
||||
g_persistentCpu.gpr[2] = RuntimeConfig::SDA2_BASE;
|
||||
g_persistentCpu.gpr[2] = GuestSmallData::kSda2Base;
|
||||
}
|
||||
if (g_persistentCpu.gpr[13] == 0) {
|
||||
g_persistentCpu.gpr[13] = RuntimeConfig::SDA1_BASE;
|
||||
g_persistentCpu.gpr[13] = GuestSmallData::kSda1Base;
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"[runtime] UNDEFINED guest instruction: pc=0x%08X raw=0x%08X %s\n",
|
||||
pc,
|
||||
rawInstruction,
|
||||
details ? details : "");
|
||||
char message[256]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"The game stopped because it reached an unsupported guest instruction at PC 0x%08X (instruction 0x%08X).\n\n%s",
|
||||
pc, rawInstruction, details ? details : "No additional details were provided.");
|
||||
ShowRuntimeFatalPopup("Unsupported guest instruction", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include "external_audio_macos.h"
|
||||
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace MusicAttenuation {
|
||||
namespace {
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140200
|
||||
constexpr auto kProcessObjectList = kAudioHardwarePropertyProcessObjectList;
|
||||
constexpr auto kProcessPid = kAudioProcessPropertyPID;
|
||||
constexpr auto kProcessRunningOutput = kAudioProcessPropertyIsRunningOutput;
|
||||
#else
|
||||
// Public Core Audio selector ABI values, for SDKs predating process objects.
|
||||
// The runtime HasProperty check still determines whether the OS supports them.
|
||||
constexpr AudioObjectPropertySelector kProcessObjectList = 'prs#';
|
||||
constexpr AudioObjectPropertySelector kProcessPid = 'ppid';
|
||||
constexpr AudioObjectPropertySelector kProcessRunningOutput = 'piro';
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
bool ReadAudioProcessProperty(AudioObjectID object, AudioObjectPropertySelector selector,
|
||||
T& value) noexcept {
|
||||
const AudioObjectPropertyAddress address{
|
||||
selector, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
|
||||
UInt32 size = sizeof(value);
|
||||
return AudioObjectGetPropertyData(object, &address, 0, nullptr, &size, &value) == noErr &&
|
||||
size == sizeof(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MacOSAudioStatus QueryMacOSExternalAudio() noexcept {
|
||||
const AudioObjectPropertyAddress address{
|
||||
kProcessObjectList,
|
||||
kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
|
||||
// Probe the property instead of raising the game's minimum macOS version.
|
||||
if (!AudioObjectHasProperty(kAudioObjectSystemObject, &address)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Processes may start between the size and data queries. Retry a changed
|
||||
// list a bounded number of times; subsequent monitor polls also retry.
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
UInt32 size = 0;
|
||||
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address,
|
||||
0, nullptr, &size) != noErr ||
|
||||
size % sizeof(AudioObjectID) != 0) {
|
||||
return {};
|
||||
}
|
||||
if (size == 0) {
|
||||
return {true, false};
|
||||
}
|
||||
std::vector<AudioObjectID> processes(size / sizeof(AudioObjectID));
|
||||
const auto result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address,
|
||||
0, nullptr, &size, processes.data());
|
||||
if (result == kAudioHardwareBadPropertySizeError) {
|
||||
continue;
|
||||
}
|
||||
if (result != noErr || size % sizeof(AudioObjectID) != 0 ||
|
||||
size / sizeof(AudioObjectID) > processes.size()) {
|
||||
return {};
|
||||
}
|
||||
const pid_t ownPid = getpid();
|
||||
for (size_t index = 0; index < size / sizeof(AudioObjectID); ++index) {
|
||||
pid_t pid = 0;
|
||||
UInt32 runningOutput = 0;
|
||||
// An exiting process may disappear mid-query. Never count an
|
||||
// unknown PID or our own SDL output as external playback.
|
||||
if (ReadAudioProcessProperty(processes[index], kProcessPid, pid) &&
|
||||
pid > 0 && pid != ownPid &&
|
||||
ReadAudioProcessProperty(processes[index], kProcessRunningOutput,
|
||||
runningOutput) && runningOutput != 0) {
|
||||
return {true, true};
|
||||
}
|
||||
}
|
||||
return {true, false};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace MusicAttenuation
|
||||
|
||||
#endif
|
||||
@@ -2,79 +2,176 @@
|
||||
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace GuestFlat {
|
||||
bool g_requiresCheckedAccess = false;
|
||||
namespace {
|
||||
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
|
||||
|
||||
struct Store {
|
||||
Backing kind = Backing::Owned;
|
||||
uint32_t owned = 0;
|
||||
uint64_t size = 0;
|
||||
uint8_t* host = nullptr;
|
||||
};
|
||||
|
||||
struct Mapping {
|
||||
uint32_t base = 0;
|
||||
uint64_t size = 0;
|
||||
uint8_t* host = nullptr;
|
||||
};
|
||||
|
||||
std::mutex g_mutex;
|
||||
std::vector<Store> g_stores;
|
||||
std::vector<Mapping> g_mappings;
|
||||
std::vector<RegionRequest> g_layout;
|
||||
uint8_t* g_base = nullptr;
|
||||
bool g_active = false;
|
||||
|
||||
inline uint64_t RoundUp(uint64_t value, uint64_t align) {
|
||||
return (value + align - 1) & ~(align - 1);
|
||||
}
|
||||
|
||||
uint64_t Offset(const RegionRequest& r) {
|
||||
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
|
||||
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
|
||||
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
|
||||
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
|
||||
}
|
||||
int BackingFile(size_t size) {
|
||||
char name[] = "/tmp/wiicompiled-guest-XXXXXX";
|
||||
const int fd = mkstemp(name);
|
||||
if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast<off_t>(size)) != 0) { close(fd); return -1; } }
|
||||
return fd;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsActive() { return g_active; }
|
||||
|
||||
void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
|
||||
if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; }
|
||||
const uint64_t pageSize = static_cast<uint64_t>(getpagesize());
|
||||
g_requiresCheckedAccess = pageSize > kGuestPageSize;
|
||||
if (g_active) {
|
||||
if (!Same(g_layout, regions))
|
||||
throw std::runtime_error("flat guest layout cannot be remapped");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < regions.size(); ++i) {
|
||||
const auto& r = regions[i];
|
||||
if (r.size == 0) continue;
|
||||
if (r.size > kGuestSpaceSize - r.base ||
|
||||
r.base % pageSize != 0 || Offset(r) % pageSize != 0) {
|
||||
throw std::runtime_error("invalid flat guest memory region");
|
||||
}
|
||||
const uint64_t end = static_cast<uint64_t>(r.base) + RoundUp(r.size, pageSize);
|
||||
for (size_t j = 0; j < i; ++j) {
|
||||
const auto& prior = regions[j];
|
||||
if (prior.size == 0) continue;
|
||||
const uint64_t priorEnd = static_cast<uint64_t>(prior.base) + RoundUp(prior.size, pageSize);
|
||||
if (r.base < priorEnd && prior.base < end) {
|
||||
throw std::runtime_error("overlapping flat guest memory regions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mach_vm_address_t address = kFixedFlatGuestBase;
|
||||
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase)
|
||||
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase) {
|
||||
throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
|
||||
}
|
||||
g_base = reinterpret_cast<uint8_t*>(address);
|
||||
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
|
||||
|
||||
std::vector<Store> stores;
|
||||
for (const auto& r : regions) {
|
||||
if (!r.size) continue;
|
||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
||||
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
|
||||
const uint64_t need = Offset(r) + r.size;
|
||||
if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need);
|
||||
try {
|
||||
for (const auto& r : regions) {
|
||||
if (!r.size) continue;
|
||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
||||
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) {
|
||||
return s.kind == r.backing && s.owned == owned;
|
||||
});
|
||||
const uint64_t need = RoundUp(Offset(r) + r.size, pageSize);
|
||||
if (it == stores.end()) {
|
||||
stores.push_back({r.backing, owned, need, nullptr});
|
||||
} else {
|
||||
it->size = std::max(it->size, need);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& s : stores) {
|
||||
void* host = mmap(nullptr, static_cast<size_t>(s.size), PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (host == MAP_FAILED) {
|
||||
throw std::runtime_error("unable to allocate anonymous macOS guest backing store");
|
||||
}
|
||||
s.host = static_cast<uint8_t*>(host);
|
||||
}
|
||||
|
||||
for (const auto& r : regions) {
|
||||
if (!r.size) continue;
|
||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
||||
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) {
|
||||
return x.kind == r.backing && x.owned == owned;
|
||||
});
|
||||
mach_vm_address_t target = reinterpret_cast<mach_vm_address_t>(g_base + r.base);
|
||||
vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE;
|
||||
const kern_return_t kr = mach_vm_remap(
|
||||
mach_task_self(),
|
||||
&target,
|
||||
static_cast<mach_vm_size_t>(RoundUp(r.size, pageSize)),
|
||||
0,
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||
mach_task_self(),
|
||||
reinterpret_cast<mach_vm_address_t>(s.host + Offset(r)),
|
||||
FALSE,
|
||||
&cur,
|
||||
&max,
|
||||
VM_INHERIT_NONE
|
||||
);
|
||||
if (kr != KERN_SUCCESS) {
|
||||
throw std::runtime_error(std::string("mach_vm_remap guest alias failed: ") + mach_error_string(kr));
|
||||
}
|
||||
g_mappings.push_back({r.base, r.size, s.host + Offset(r)});
|
||||
}
|
||||
|
||||
g_layout = regions;
|
||||
g_stores = std::move(stores);
|
||||
g_active = true;
|
||||
} catch (...) {
|
||||
for (auto& s : stores) {
|
||||
if (s.host != nullptr) {
|
||||
munmap(s.host, static_cast<size_t>(s.size));
|
||||
}
|
||||
}
|
||||
mach_vm_deallocate(mach_task_self(), address, kGuestSpaceSize);
|
||||
g_mappings.clear();
|
||||
g_stores.clear();
|
||||
g_layout.clear();
|
||||
g_base = nullptr;
|
||||
throw;
|
||||
}
|
||||
for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); }
|
||||
for (const auto& r : regions) {
|
||||
if (!r.size) continue;
|
||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
||||
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
|
||||
auto* host = static_cast<uint8_t*>(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r)));
|
||||
auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r));
|
||||
if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias");
|
||||
g_mappings.push_back({r.base, r.size, host});
|
||||
}
|
||||
for (auto& s : stores) close(s.fd);
|
||||
g_layout = regions; g_active = true;
|
||||
}
|
||||
uint8_t* HostPointer(uint32_t a) { for (const auto& m : g_mappings) if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); return nullptr; }
|
||||
|
||||
uint8_t* HostPointer(uint32_t a) {
|
||||
for (const auto& m : g_mappings) {
|
||||
if (a >= m.base && uint64_t(a - m.base) < m.size) {
|
||||
return m.host + (a - m.base);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ProtectDeferredRange(uint32_t, size_t) {}
|
||||
void UnprotectDeferredRange(uint32_t, size_t) {}
|
||||
void RegisterExecutableRange(uint32_t, uint32_t) {}
|
||||
FaultCounters Counters() { return {}; }
|
||||
void LogFaultSummary() noexcept {}
|
||||
bool HandleAccessViolation(void*, bool) noexcept { return false; }
|
||||
|
||||
} // namespace GuestFlat
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace HciDetail {
|
||||
|
||||
|
||||
constexpr uint16_t kOgfLinkControl = 0x01;
|
||||
constexpr uint16_t kOgfLinkPolicy = 0x02;
|
||||
constexpr uint16_t kOgfControllerBaseband = 0x03;
|
||||
constexpr uint16_t kOgfInformational = 0x04;
|
||||
constexpr uint16_t kOgfStatus = 0x05;
|
||||
constexpr uint16_t kOgfVendor = 0x3F;
|
||||
|
||||
constexpr uint8_t kEventInquiryComplete = 0x01;
|
||||
constexpr uint8_t kEventInquiryResult = 0x02;
|
||||
constexpr uint8_t kEventConnectionComplete = 0x03;
|
||||
constexpr uint8_t kEventDisconnectionComplete = 0x05;
|
||||
constexpr uint8_t kEventAuthenticationComplete = 0x06;
|
||||
constexpr uint8_t kEventRemoteNameComplete = 0x07;
|
||||
constexpr uint8_t kEventEncryptionChange = 0x08;
|
||||
constexpr uint8_t kEventRemoteFeatures = 0x0B;
|
||||
constexpr uint8_t kEventRemoteVersion = 0x0C;
|
||||
constexpr uint8_t kEventCommandComplete = 0x0E;
|
||||
constexpr uint8_t kEventCommandStatus = 0x0F;
|
||||
constexpr uint8_t kEventRoleChange = 0x12;
|
||||
constexpr uint8_t kEventNumberOfCompletedPackets = 0x13;
|
||||
constexpr uint8_t kEventModeChange = 0x14;
|
||||
constexpr uint8_t kEventReturnLinkKeys = 0x15;
|
||||
constexpr uint8_t kEventPinCodeRequest = 0x16;
|
||||
constexpr uint8_t kEventLinkKeyRequest = 0x17;
|
||||
constexpr uint8_t kEventLinkKeyNotification = 0x18;
|
||||
constexpr uint8_t kEventClockOffset = 0x1C;
|
||||
constexpr uint8_t kEventPacketTypeChanged = 0x1D;
|
||||
constexpr uint8_t kEventInquiryResultWithRssi = 0x22;
|
||||
constexpr uint8_t kEventExtendedInquiryResult = 0x2F;
|
||||
|
||||
constexpr uint8_t kStatusSuccess = 0x00;
|
||||
constexpr uint8_t kStatusUnknownCommand = 0x01;
|
||||
constexpr uint8_t kStatusUnknownConnection = 0x02;
|
||||
constexpr uint8_t kStatusPageTimeout = 0x04;
|
||||
constexpr uint8_t kStatusAuthenticationFailure = 0x05;
|
||||
constexpr uint8_t kStatusKeyMissing = 0x06;
|
||||
constexpr uint8_t kStatusUnsupportedFeature = 0x11;
|
||||
constexpr uint8_t kReasonLocalHostTerminated = 0x16;
|
||||
|
||||
// What a Wii's BCM2045 reports (the version makes WUD apply its usual patch).
|
||||
constexpr BdAddr kLocalAddress = {0x11, 0x02, 0x19, 0x79, 0x00, 0xFF};
|
||||
constexpr std::array<uint8_t, 8> kLocalFeatures = {0xFF, 0xFF, 0x8D, 0xFE, 0x9B, 0xF9, 0x00, 0x80};
|
||||
// Never advertise more than 10 ACL buffers: the stack keeps the count in a u8.
|
||||
constexpr uint16_t kAclBufferSize = 339;
|
||||
constexpr uint16_t kAclBufferCount = 10;
|
||||
constexpr uint8_t kScoBufferSize = 64;
|
||||
|
||||
// A real RVL-CNT-01.
|
||||
constexpr std::array<uint8_t, 8> kRemoteFeatures = {0xBC, 0x02, 0x04, 0x38, 0x08, 0x00, 0x00, 0x00};
|
||||
constexpr uint8_t kRemoteLmpVersion = 0x02;
|
||||
constexpr uint16_t kRemoteManufacturer = 0x000F;
|
||||
constexpr uint16_t kRemoteLmpSubversion = 0x0229;
|
||||
constexpr uint16_t kRemoteClockOffset = 0x3818;
|
||||
constexpr uint8_t kInquiryRssi = 0xD8; // -40 dBm
|
||||
constexpr uint64_t kInquiryUnitUs = 1'280'000;
|
||||
constexpr uint16_t kMaxStoredKeys = 0x00FF;
|
||||
constexpr uint8_t kKeysPerEvent = 11;
|
||||
|
||||
void PutAddr(std::vector<uint8_t>& out, const BdAddr& addr) {
|
||||
out.insert(out.end(), addr.begin(), addr.end());
|
||||
}
|
||||
|
||||
BdAddr ReadAddr(const uint8_t* p) {
|
||||
BdAddr addr{};
|
||||
std::memcpy(addr.data(), p, addr.size());
|
||||
return addr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Event(uint8_t code, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> event;
|
||||
event.reserve(2 + params.size());
|
||||
event.push_back(code);
|
||||
event.push_back(static_cast<uint8_t>(params.size()));
|
||||
event.insert(event.end(), params.begin(), params.end());
|
||||
return event;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 16> SlotLinkKey(size_t slot) {
|
||||
std::array<uint8_t, 16> key{};
|
||||
for (size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<uint8_t>(0xA0 + slot + i * 0x11);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace HciDetail
|
||||
|
||||
using namespace HciDetail;
|
||||
|
||||
Controller::Controller(RemoteHost& host) : m_host(host) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
m_links[slot] = std::make_unique<RemoteLink>(*this, slot);
|
||||
m_storedKeys.push_back({RemoteAddress(slot), SlotLinkKey(slot)});
|
||||
}
|
||||
ResetState(true);
|
||||
}
|
||||
|
||||
Controller::~Controller() = default;
|
||||
|
||||
BdAddr Controller::LocalAddress() {
|
||||
return kLocalAddress;
|
||||
}
|
||||
|
||||
BdAddr Controller::RemoteAddress(size_t slot) {
|
||||
return {0x11, 0x02, 0x19, 0x79, 0x00, static_cast<uint8_t>(slot)};
|
||||
}
|
||||
|
||||
uint16_t Controller::ConnectionHandle(size_t slot) {
|
||||
return static_cast<uint16_t>(0x0100 + slot);
|
||||
}
|
||||
|
||||
void Controller::Open(uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
ResetState(true);
|
||||
m_open = true;
|
||||
}
|
||||
|
||||
void Controller::Close() {
|
||||
ResetState(true);
|
||||
m_open = false;
|
||||
}
|
||||
|
||||
void Controller::ResetState(bool keepStoredKeys) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
RemoteLink& link = *m_links[slot];
|
||||
// A reset drops the radio links: the remotes lose their connection.
|
||||
if (link.State() == LinkState::Ready) {
|
||||
m_host.OnGuestDisconnect(slot);
|
||||
}
|
||||
link.Reset();
|
||||
}
|
||||
if (!keepStoredKeys) {
|
||||
m_storedKeys.clear();
|
||||
}
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
m_inquiry = {};
|
||||
m_scanEnable = 0;
|
||||
m_pageTimeoutSlots = 0x2000;
|
||||
m_inquiryMode = 0;
|
||||
m_classOfDevice = {};
|
||||
m_localName = {};
|
||||
m_completedPackets = {};
|
||||
for (Reassembly& r : m_reassembly) r = {};
|
||||
m_events.clear();
|
||||
m_acl.clear();
|
||||
m_pending.clear();
|
||||
m_authPending = {};
|
||||
}
|
||||
|
||||
LinkState Controller::SlotLinkState(size_t slot) const {
|
||||
return slot < kRemoteSlots ? m_links[slot]->State() : LinkState::Down;
|
||||
}
|
||||
|
||||
int Controller::SlotForAddress(const BdAddr& addr) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (RemoteAddress(slot) == addr) return static_cast<int>(slot);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Controller::SlotForHandle(uint16_t handle) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (ConnectionHandle(slot) == (handle & 0x0FFF) &&
|
||||
m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected) {
|
||||
return static_cast<int>(slot);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const Controller::StoredKey* Controller::FindStoredKey(const BdAddr& addr) const {
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (key.addr == addr) return &key;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Controller::QueueEvent(std::vector<uint8_t> event) {
|
||||
m_events.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void Controller::QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame) {
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(4 + l2capFrame.size());
|
||||
// Packet boundary 0x2: first (and only) fragment, automatically flushable.
|
||||
PutLe16(packet, static_cast<uint16_t>((handle & 0x0FFF) | 0x2000));
|
||||
PutLe16(packet, static_cast<uint16_t>(l2capFrame.size()));
|
||||
packet.insert(packet.end(), l2capFrame.begin(), l2capFrame.end());
|
||||
m_acl.push_back(std::move(packet));
|
||||
}
|
||||
|
||||
bool Controller::PopEvent(std::vector<uint8_t>& event) {
|
||||
if (m_events.empty()) return false;
|
||||
event = std::move(m_events.front());
|
||||
m_events.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Controller::PopAcl(std::vector<uint8_t>& packet) {
|
||||
if (!m_events.empty() || m_acl.empty()) return false;
|
||||
packet = std::move(m_acl.front());
|
||||
m_acl.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutLe16(body, opcode);
|
||||
body.insert(body.end(), params.begin(), params.end());
|
||||
QueueEvent(Event(kEventCommandComplete, body));
|
||||
}
|
||||
|
||||
void Controller::CommandStatus(uint16_t opcode, uint8_t status) {
|
||||
std::vector<uint8_t> body = {status, 0x01};
|
||||
PutLe16(body, opcode);
|
||||
QueueEvent(Event(kEventCommandStatus, body));
|
||||
}
|
||||
|
||||
void Controller::UnknownCommand(uint16_t opcode) {
|
||||
std::cerr << "[bluetooth] unsupported HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
if (size < 3) return;
|
||||
const uint16_t opcode = Le16(packet);
|
||||
const size_t length = packet[2];
|
||||
if (3 + length > size) {
|
||||
std::cerr << "[bluetooth] truncated HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = packet + 3;
|
||||
switch (opcode >> 10) {
|
||||
case kOgfLinkControl: HandleLinkControl(opcode, p, length, nowUs); break;
|
||||
case kOgfLinkPolicy: HandleLinkPolicy(opcode, p, length); break;
|
||||
case kOgfControllerBaseband: HandleControllerBaseband(opcode, p, length, nowUs); break;
|
||||
case kOgfInformational: HandleInformational(opcode, p, length); break;
|
||||
case kOgfStatus: HandleStatusParameters(opcode, p, length); break;
|
||||
case kOgfVendor: HandleVendor(opcode, p, length); break;
|
||||
default: UnknownCommand(opcode); break;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
const auto handleSlot = [&](size_t offset) { return n >= offset + 2 ? SlotForHandle(Le16(p + offset)) : -1; };
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Inquiry
|
||||
if (n < 5) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_inquiry = {};
|
||||
m_inquiry.active = true;
|
||||
m_inquiry.endUs = nowUs + std::max<uint8_t>(p[3], 1) * kInquiryUnitUs;
|
||||
m_inquiry.maxResponses = p[4];
|
||||
if (!m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(true);
|
||||
m_hostInquiryNotified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0002: // Inquiry Cancel
|
||||
m_inquiry = {};
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0003: // Periodic Inquiry Mode
|
||||
case 0x0004: // Exit Periodic Inquiry Mode
|
||||
CommandComplete(opcode, {kStatusUnsupportedFeature});
|
||||
return;
|
||||
case 0x0005: { // Create Connection
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_links[slot]->OnPaged(nowUs)) {
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
} else {
|
||||
std::vector<uint8_t> body = {kStatusPageTimeout, 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventConnectionComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0006: { // Disconnect
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kReasonLocalHostTerminated);
|
||||
QueueEvent(Event(kEventDisconnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_authPending[slot] = false;
|
||||
m_links[slot]->OnDisconnected(true, nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x0008: { // Create Connection Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventConnectionComplete && ReadAddr(&e.event[5]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Accept Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const uint8_t role = p[6];
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
if (role == 0x00) {
|
||||
std::vector<uint8_t> change = {kStatusSuccess};
|
||||
PutAddr(change, addr);
|
||||
change.push_back(0x00);
|
||||
QueueEvent(Event(kEventRoleChange, change));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_links[slot]->OnAccepted(role);
|
||||
return;
|
||||
}
|
||||
case 0x000A: { // Reject Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {p[6], 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_links[slot]->OnRejected(p[6], nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x000B: { // Link Key Request Reply
|
||||
if (n < 22) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
const bool match = std::equal(p + 6, p + 22, SlotLinkKey(slot).begin());
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(match ? kStatusSuccess : kStatusKeyMissing)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000C: { // Link Key Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, addr);
|
||||
QueueEvent(Event(kEventPinCodeRequest, request));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000D: { // PIN Code Request Reply
|
||||
if (n < 23) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_authPending[slot]) return;
|
||||
m_authPending[slot] = false;
|
||||
// A remote takes the host's address (SYNC button) or its own (1+2) as PIN.
|
||||
const uint8_t pinLength = p[6];
|
||||
const uint8_t* pin = p + 7;
|
||||
const BdAddr local = LocalAddress();
|
||||
const bool valid = pinLength == 6 && (std::equal(pin, pin + 6, local.begin()) ||
|
||||
std::equal(pin, pin + 6, addr.begin()));
|
||||
if (valid) {
|
||||
std::vector<uint8_t> notification;
|
||||
PutAddr(notification, addr);
|
||||
const auto key = SlotLinkKey(slot);
|
||||
notification.insert(notification.end(), key.begin(), key.end());
|
||||
notification.push_back(0x00); // combination key
|
||||
QueueEvent(Event(kEventLinkKeyNotification, notification));
|
||||
}
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(valid ? kStatusSuccess : kStatusAuthenticationFailure)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
return;
|
||||
}
|
||||
case 0x000E: { // PIN Code Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
std::vector<uint8_t> auth = {kStatusKeyMissing};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000F: { // Change Connection Packet Type
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 4) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, Le16(p + 2));
|
||||
QueueEvent(Event(kEventPacketTypeChanged, body));
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Authentication Requested
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_authPending[slot] = true;
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, RemoteAddress(slot));
|
||||
QueueEvent(Event(kEventLinkKeyRequest, request));
|
||||
return;
|
||||
}
|
||||
case 0x0013: { // Set Connection Encryption
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(p[2]);
|
||||
QueueEvent(Event(kEventEncryptionChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x0019: { // Remote Name Request
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
const bool reachable = slot >= 0 && (m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected ||
|
||||
m_links[slot]->Discoverable());
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(reachable ? kStatusSuccess : kStatusPageTimeout)};
|
||||
PutAddr(body, addr);
|
||||
std::array<char, 248> name{};
|
||||
if (reachable) {
|
||||
std::strncpy(name.data(), m_host.DeviceName(slot), name.size() - 1);
|
||||
}
|
||||
body.insert(body.end(), name.begin(), name.end());
|
||||
if (reachable) {
|
||||
QueueEvent(Event(kEventRemoteNameComplete, body));
|
||||
} else {
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventRemoteNameComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001A: { // Remote Name Request Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventRemoteNameComplete && ReadAddr(&e.event[3]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001B: { // Read Remote Supported Features
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.insert(body.end(), kRemoteFeatures.begin(), kRemoteFeatures.end());
|
||||
QueueEvent(Event(kEventRemoteFeatures, body));
|
||||
return;
|
||||
}
|
||||
case 0x001D: { // Read Remote Version Information
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kRemoteLmpVersion);
|
||||
PutLe16(body, kRemoteManufacturer);
|
||||
PutLe16(body, kRemoteLmpSubversion);
|
||||
QueueEvent(Event(kEventRemoteVersion, body));
|
||||
return;
|
||||
}
|
||||
case 0x001F: { // Read Clock Offset
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventClockOffset, body));
|
||||
return;
|
||||
}
|
||||
case 0x0007: // Add SCO Connection
|
||||
case 0x0028: // Setup Synchronous Connection
|
||||
case 0x0029: // Accept Synchronous Connection Request
|
||||
case 0x002A: // Reject Synchronous Connection Request
|
||||
CommandStatus(opcode, kStatusUnsupportedFeature);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
const auto modeChange = [&](uint8_t mode, uint16_t interval) {
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(static_cast<size_t>(slot)));
|
||||
body.push_back(mode);
|
||||
PutLe16(body, interval);
|
||||
QueueEvent(Event(kEventModeChange, body));
|
||||
};
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: modeChange(0x01, n >= 4 ? Le16(p + 2) : 0); return; // Hold Mode
|
||||
case 0x0003: modeChange(0x02, n >= 4 ? Le16(p + 2) : 0); return; // Sniff Mode
|
||||
case 0x0004: modeChange(0x00, 0); return; // Exit Sniff Mode
|
||||
case 0x0005: modeChange(0x03, n >= 4 ? Le16(p + 2) : 0); return; // Park State
|
||||
case 0x0006: modeChange(0x00, 0); return; // Exit Park State
|
||||
case 0x0009: handleReply({0x00}); return; // Role Discovery
|
||||
case 0x000B: { // Switch Role
|
||||
if (n < 7) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), p, p + 7);
|
||||
QueueEvent(Event(kEventRoleChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x000C: handleReply({0x05, 0x00}); return; // Read Link Policy Settings
|
||||
case 0x000D: handleReply({}); return; // Write Link Policy Settings
|
||||
case 0x000E: CommandComplete(opcode, {kStatusSuccess, 0x05, 0x00}); return; // Read Default Link Policy
|
||||
case 0x000F: CommandComplete(opcode, {kStatusSuccess}); return; // Write Default Link Policy
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Reset
|
||||
ResetState(true);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x000D: { // Read Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool readAll = p[6] == 0x01;
|
||||
std::vector<StoredKey> keys;
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (readAll || key.addr == addr) keys.push_back(key);
|
||||
}
|
||||
for (size_t start = 0; start < keys.size(); start += kKeysPerEvent) {
|
||||
const size_t count = std::min<size_t>(kKeysPerEvent, keys.size() - start);
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(count)};
|
||||
for (size_t i = start; i < start + count; ++i) {
|
||||
PutAddr(body, keys[i].addr);
|
||||
body.insert(body.end(), keys[i].key.begin(), keys[i].key.end());
|
||||
}
|
||||
QueueEvent(Event(kEventReturnLinkKeys, body));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kMaxStoredKeys);
|
||||
PutLe16(body, static_cast<uint16_t>(keys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Write Stored Link Key
|
||||
if (n < 1) break;
|
||||
const uint8_t count = p[0];
|
||||
if (n < 1 + count * size_t{22}) break;
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
const uint8_t* entry = p + 1 + i * 22;
|
||||
StoredKey key{ReadAddr(entry), {}};
|
||||
std::copy(entry + 6, entry + 22, key.key.begin());
|
||||
auto existing = std::find_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return k.addr == key.addr; });
|
||||
if (existing != m_storedKeys.end()) {
|
||||
*existing = key;
|
||||
} else if (m_storedKeys.size() < kMaxStoredKeys) {
|
||||
m_storedKeys.push_back(key);
|
||||
}
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess, count});
|
||||
return;
|
||||
}
|
||||
case 0x0012: { // Delete Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool deleteAll = p[6] == 0x01;
|
||||
const size_t before = m_storedKeys.size();
|
||||
m_storedKeys.erase(std::remove_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return deleteAll || k.addr == addr; }),
|
||||
m_storedKeys.end());
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, static_cast<uint16_t>(before - m_storedKeys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0013: // Change Local Name
|
||||
if (n < m_localName.size()) break;
|
||||
std::memcpy(m_localName.data(), p, m_localName.size());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0014: { // Read Local Name
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), m_localName.begin(), m_localName.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0018: // Write Page Timeout
|
||||
if (n < 2) break;
|
||||
m_pageTimeoutSlots = Le16(p);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0019: // Read Scan Enable
|
||||
CommandComplete(opcode, {kStatusSuccess, m_scanEnable});
|
||||
return;
|
||||
case 0x001A: // Write Scan Enable
|
||||
if (n < 1) break;
|
||||
m_scanEnable = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0023: // Read Class of Device
|
||||
CommandComplete(opcode, {kStatusSuccess, m_classOfDevice[0], m_classOfDevice[1], m_classOfDevice[2]});
|
||||
return;
|
||||
case 0x0024: // Write Class of Device
|
||||
if (n < 3) break;
|
||||
std::copy(p, p + 3, m_classOfDevice.begin());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0045: // Write Inquiry Mode
|
||||
if (n < 1) break;
|
||||
m_inquiryMode = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0028: // Write Automatic Flush Timeout
|
||||
case 0x0037: // Write Link Supervision Timeout
|
||||
handleReply({});
|
||||
return;
|
||||
case 0x0036: // Read Link Supervision Timeout
|
||||
handleReply({0x80, 0x0C});
|
||||
return;
|
||||
case 0x0001: // Set Event Mask
|
||||
case 0x0005: // Set Event Filter
|
||||
case 0x000A: // Write PIN Type
|
||||
case 0x0016: // Write Connection Accept Timeout
|
||||
case 0x001C: // Write Page Scan Activity
|
||||
case 0x001E: // Write Inquiry Scan Activity
|
||||
case 0x0020: // Write Authentication Enable
|
||||
case 0x0022: // Write Encryption Mode
|
||||
case 0x0031: // Set Controller To Host Flow Control
|
||||
case 0x0033: // Host Buffer Size
|
||||
case 0x003A: // Write Current IAC LAP
|
||||
case 0x003F: // Set AFH Host Channel Classification
|
||||
case 0x0043: // Write Inquiry Scan Type
|
||||
case 0x0047: // Write Page Scan Type
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0035: // Host Number Of Completed Packets: no event
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandComplete(opcode, {kStatusUnknownCommand});
|
||||
}
|
||||
|
||||
void Controller::HandleInformational(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Read Local Version Information
|
||||
std::vector<uint8_t> body = {kStatusSuccess, 0x03};
|
||||
PutLe16(body, 0x40A7);
|
||||
body.push_back(0x03);
|
||||
PutLe16(body, 0x000F);
|
||||
PutLe16(body, 0x430E);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0003: { // Read Local Supported Features
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), kLocalFeatures.begin(), kLocalFeatures.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0005: { // Read Buffer Size
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kAclBufferSize);
|
||||
body.push_back(kScoBufferSize);
|
||||
PutLe16(body, kAclBufferCount);
|
||||
PutLe16(body, 0);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Read BD_ADDR
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, LocalAddress());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Get Link Quality
|
||||
body.push_back(0xFF);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
case 0x0005: // Read RSSI: 0 = inside the golden receive power range
|
||||
body.push_back(0x00);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleVendor(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
// WUD's firmware patch upload (0xFC4C writes, 0xFC4F install) and friends.
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
}
|
||||
|
||||
void Controller::ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
if (size < 4) return;
|
||||
const uint16_t header = Le16(packet);
|
||||
const uint16_t length = Le16(packet + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] dropped malformed ACL packet" << std::endl;
|
||||
return;
|
||||
}
|
||||
const int slot = SlotForHandle(header & 0x0FFF);
|
||||
if (slot < 0) return;
|
||||
++m_completedPackets[slot];
|
||||
const uint8_t boundary = static_cast<uint8_t>((header >> 12) & 0x3);
|
||||
Reassembly& r = m_reassembly[slot];
|
||||
const uint8_t* data = packet + 4;
|
||||
if (boundary == 0x1) {
|
||||
if (r.expected == 0) return;
|
||||
r.frame.insert(r.frame.end(), data, data + length);
|
||||
} else {
|
||||
r.frame.assign(data, data + length);
|
||||
r.expected = length >= 2 ? static_cast<size_t>(Le16(data)) + 4 : 0;
|
||||
}
|
||||
if (r.expected != 0 && r.frame.size() >= r.expected) {
|
||||
const std::vector<uint8_t> frame(r.frame.begin(), r.frame.begin() + static_cast<std::ptrdiff_t>(r.expected));
|
||||
r = {};
|
||||
m_links[slot]->ReceiveL2cap(frame.data(), frame.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::Tick(uint64_t nowUs) {
|
||||
if (!m_open) return;
|
||||
for (size_t i = 0; i < m_pending.size();) {
|
||||
if (nowUs >= m_pending[i].dueUs) {
|
||||
QueueEvent(std::move(m_pending[i].event));
|
||||
m_pending.erase(m_pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
TickInquiry(nowUs);
|
||||
for (auto& link : m_links) {
|
||||
link->Tick(nowUs);
|
||||
}
|
||||
SendNumberOfCompletedPackets();
|
||||
}
|
||||
|
||||
void Controller::TickInquiry(uint64_t nowUs) {
|
||||
if (!m_inquiry.active) return;
|
||||
if (nowUs >= m_inquiry.endUs ||
|
||||
(m_inquiry.maxResponses != 0 && m_inquiry.reported >= m_inquiry.maxResponses)) {
|
||||
FinishInquiry();
|
||||
return;
|
||||
}
|
||||
// One result per tick, lowest slot first, so remotes keep their order.
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_inquiry.slotReported[slot] || !m_links[slot]->Discoverable()) continue;
|
||||
m_inquiry.slotReported[slot] = true;
|
||||
++m_inquiry.reported;
|
||||
const BdAddr addr = RemoteAddress(slot);
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutAddr(body, addr);
|
||||
body.push_back(0x01); // page scan repetition mode R1
|
||||
if (m_inquiryMode == 0) {
|
||||
body.insert(body.end(), {0x00, 0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventInquiryResult, body));
|
||||
} else {
|
||||
body.insert(body.end(), {0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
body.push_back(kInquiryRssi);
|
||||
if (m_inquiryMode == 2) {
|
||||
std::array<uint8_t, 240> eir{};
|
||||
const char* name = m_host.DeviceName(slot);
|
||||
const size_t nameLength = std::min<size_t>(std::strlen(name), eir.size() - 2);
|
||||
eir[0] = static_cast<uint8_t>(nameLength + 1);
|
||||
eir[1] = 0x09; // complete local name
|
||||
std::memcpy(&eir[2], name, nameLength);
|
||||
body.insert(body.end(), eir.begin(), eir.end());
|
||||
QueueEvent(Event(kEventExtendedInquiryResult, body));
|
||||
} else {
|
||||
QueueEvent(Event(kEventInquiryResultWithRssi, body));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::FinishInquiry() {
|
||||
m_inquiry = {};
|
||||
QueueEvent(Event(kEventInquiryComplete, {kStatusSuccess}));
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::SendNumberOfCompletedPackets() {
|
||||
std::vector<uint8_t> body = {0};
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_completedPackets[slot] == 0) continue;
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, m_completedPackets[slot]);
|
||||
m_completedPackets[slot] = 0;
|
||||
++body[0];
|
||||
}
|
||||
if (body[0] != 0) {
|
||||
QueueEvent(Event(kEventNumberOfCompletedPackets, body));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,567 @@
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace L2capDetail {
|
||||
|
||||
constexpr uint16_t kSignallingCid = 0x0001;
|
||||
constexpr uint16_t kFirstDynamicCid = 0x0040;
|
||||
|
||||
constexpr uint16_t kPsmSdp = 0x0001;
|
||||
constexpr uint16_t kPsmHidControl = 0x0011;
|
||||
constexpr uint16_t kPsmHidInterrupt = 0x0013;
|
||||
|
||||
constexpr uint8_t kCommandReject = 0x01;
|
||||
constexpr uint8_t kConnectionRequest = 0x02;
|
||||
constexpr uint8_t kConnectionResponse = 0x03;
|
||||
constexpr uint8_t kConfigurationRequest = 0x04;
|
||||
constexpr uint8_t kConfigurationResponse = 0x05;
|
||||
constexpr uint8_t kDisconnectionRequest = 0x06;
|
||||
constexpr uint8_t kDisconnectionResponse = 0x07;
|
||||
constexpr uint8_t kEchoRequest = 0x08;
|
||||
constexpr uint8_t kEchoResponse = 0x09;
|
||||
constexpr uint8_t kInformationRequest = 0x0A;
|
||||
constexpr uint8_t kInformationResponse = 0x0B;
|
||||
|
||||
constexpr uint16_t kResultSuccess = 0x0000;
|
||||
constexpr uint16_t kResultPending = 0x0001;
|
||||
constexpr uint16_t kResultPsmNotSupported = 0x0002;
|
||||
constexpr uint16_t kResultNoResources = 0x0004;
|
||||
|
||||
constexpr uint8_t kOptionMtu = 0x01;
|
||||
constexpr uint8_t kOptionFlushTimeout = 0x02;
|
||||
// A real remote asks for this MTU on its own channels.
|
||||
constexpr uint16_t kRemoteMtu = 185;
|
||||
|
||||
constexpr uint8_t kHidHandshake = 0x0;
|
||||
constexpr uint8_t kHidControl = 0x1;
|
||||
constexpr uint8_t kHidSetReport = 0x5;
|
||||
constexpr uint8_t kHidData = 0xA;
|
||||
constexpr uint8_t kHidParamOutput = 0x2;
|
||||
constexpr uint8_t kHidHandshakeSuccess = 0x00;
|
||||
constexpr uint8_t kHidHandshakeUnsupported = 0x03;
|
||||
constexpr uint8_t kHidVirtualCableUnplug = 0x5;
|
||||
constexpr uint8_t kHidDataInput = 0xA1;
|
||||
|
||||
constexpr uint64_t kRequestTimeoutUs = 10'000'000;
|
||||
constexpr uint64_t kRetryDelayUs = 3'000'000;
|
||||
constexpr uint8_t kReasonRemoteUserTerminated = 0x13;
|
||||
// Output reports carry at most 21 payload bytes; anything longer is not a Wii Remote report.
|
||||
constexpr size_t kMaxReportSize = 22;
|
||||
constexpr size_t kInputBacklog = 8;
|
||||
|
||||
} // namespace L2capDetail
|
||||
|
||||
using namespace L2capDetail;
|
||||
|
||||
RemoteLink::RemoteLink(Controller& controller, size_t slot) : m_controller(controller), m_slot(slot) {}
|
||||
|
||||
void RemoteLink::Reset() {
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
|
||||
void RemoteLink::ClearConnection() {
|
||||
m_channels.clear();
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
}
|
||||
|
||||
bool RemoteLink::Discoverable() const {
|
||||
return m_baseband == Baseband::Idle && m_connectWanted && m_controller.Host().IsAttached(m_slot);
|
||||
}
|
||||
|
||||
void RemoteLink::Tick(uint64_t nowUs) {
|
||||
m_nowUs = nowUs;
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_baseband == Baseband::Idle) {
|
||||
if (host.ConsumeWake(m_slot)) {
|
||||
m_connectWanted = true;
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
m_connectWanted = false;
|
||||
}
|
||||
if (m_connectWanted && m_controller.PageScanEnabled() && nowUs >= m_retryAtUs) {
|
||||
std::vector<uint8_t> event = {0x04, 10};
|
||||
const BdAddr addr = Controller::RemoteAddress(m_slot);
|
||||
event.insert(event.end(), addr.begin(), addr.end());
|
||||
// Class of device 0x002504: peripheral, joystick.
|
||||
event.insert(event.end(), {0x04, 0x25, 0x00, 0x01});
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
m_baseband = Baseband::Requesting;
|
||||
m_stateSinceUs = nowUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Requesting) {
|
||||
if (nowUs - m_stateSinceUs > kRequestTimeoutUs) {
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Connected) {
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_connectWanted = false;
|
||||
return;
|
||||
}
|
||||
if (!m_guestInitiated) {
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
if (control == nullptr) {
|
||||
RequestChannel(kPsmHidControl, nowUs);
|
||||
} else if (control->Open() && FindByPsm(kPsmHidInterrupt) == nullptr) {
|
||||
RequestChannel(kPsmHidInterrupt, nowUs);
|
||||
}
|
||||
}
|
||||
for (const Channel& channel : m_channels) {
|
||||
const bool waiting = (channel.weInitiated && !channel.connected) ||
|
||||
(channel.configSent && !channel.localConfigured);
|
||||
if (waiting && nowUs - channel.requestUs > kRequestTimeoutUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": L2CAP channel 0x" << std::hex
|
||||
<< channel.psm << std::dec << " did not open, dropping the connection" << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ForwardInput();
|
||||
}
|
||||
|
||||
void RemoteLink::ForwardInput() {
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_state != LinkState::Ready) {
|
||||
while (host.PopInputReport(m_slot, m_report)) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
while (m_controller.AclQueued() < kInputBacklog && host.PopInputReport(m_slot, m_report)) {
|
||||
if (m_report.empty() || m_report.size() > kMaxReportSize) continue;
|
||||
SendInput(*interrupt, m_report);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::SendInput(const Channel& interrupt, const std::vector<uint8_t>& report) {
|
||||
std::vector<uint8_t> payload;
|
||||
payload.reserve(report.size() + 1);
|
||||
payload.push_back(kHidDataInput);
|
||||
payload.insert(payload.end(), report.begin(), report.end());
|
||||
if (payload.size() <= interrupt.remoteMtu) {
|
||||
SendData(interrupt, payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RemoteLink::OnAccepted(uint8_t role) {
|
||||
(void)role;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = m_nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
}
|
||||
|
||||
void RemoteLink::OnRejected(uint8_t reason, uint64_t nowUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": connection rejected (0x" << std::hex
|
||||
<< static_cast<int>(reason) << std::dec << ")" << std::endl;
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
|
||||
bool RemoteLink::OnPaged(uint64_t nowUs) {
|
||||
if (!Discoverable()) {
|
||||
return false;
|
||||
}
|
||||
m_nowUs = nowUs;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = true;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RemoteLink::OnDisconnected(bool byGuest, uint64_t nowUs) {
|
||||
const bool wasReady = m_everReady;
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
if (byGuest && wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
} else {
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::DropFromRemote(uint8_t reason) {
|
||||
std::vector<uint8_t> event = {0x05, 4, 0x00};
|
||||
PutLe16(event, Controller::ConnectionHandle(m_slot));
|
||||
event.push_back(reason);
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
}
|
||||
|
||||
void RemoteLink::SetState(LinkState state) {
|
||||
if (m_state == state) return;
|
||||
m_state = state;
|
||||
if (state == LinkState::Ready) {
|
||||
m_everReady = true;
|
||||
m_connectWanted = false;
|
||||
}
|
||||
m_controller.Host().OnLinkStateChanged(m_slot, state);
|
||||
}
|
||||
|
||||
void RemoteLink::UpdateState() {
|
||||
if (m_baseband != Baseband::Connected) {
|
||||
SetState(LinkState::Down);
|
||||
return;
|
||||
}
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
const bool ready = control != nullptr && interrupt != nullptr && control->Open() && interrupt->Open();
|
||||
SetState(ready ? LinkState::Ready : LinkState::Connecting);
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByLocal(uint16_t cid) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.localCid == cid) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByPsm(uint16_t psm) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.psm == psm) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint16_t RemoteLink::AllocateCid() const {
|
||||
uint16_t cid = kFirstDynamicCid;
|
||||
while (std::any_of(m_channels.begin(), m_channels.end(), [&](const Channel& c) { return c.localCid == cid; })) {
|
||||
++cid;
|
||||
}
|
||||
return cid;
|
||||
}
|
||||
|
||||
uint8_t RemoteLink::NextIdent() {
|
||||
m_ident = static_cast<uint8_t>(m_ident + 1);
|
||||
if (m_ident == 0) m_ident = 1;
|
||||
return m_ident;
|
||||
}
|
||||
|
||||
void RemoteLink::SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(8 + data.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(4 + data.size()));
|
||||
PutLe16(frame, kSignallingCid);
|
||||
frame.push_back(code);
|
||||
frame.push_back(ident);
|
||||
PutLe16(frame, static_cast<uint16_t>(data.size()));
|
||||
frame.insert(frame.end(), data.begin(), data.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::SendData(const Channel& channel, const std::vector<uint8_t>& payload) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(4 + payload.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(payload.size()));
|
||||
PutLe16(frame, channel.remoteCid);
|
||||
frame.insert(frame.end(), payload.begin(), payload.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::RequestChannel(uint16_t psm, uint64_t nowUs) {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.weInitiated = true;
|
||||
channel.connectIdent = NextIdent();
|
||||
channel.requestUs = nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, psm);
|
||||
PutLe16(data, channel.localCid);
|
||||
SendSignal(kConnectionRequest, channel.connectIdent, data);
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
|
||||
void RemoteLink::SendConfigRequest(Channel& channel) {
|
||||
channel.configSent = true;
|
||||
channel.configIdent = NextIdent();
|
||||
channel.requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel.remoteCid);
|
||||
PutLe16(data, 0);
|
||||
data.push_back(kOptionMtu);
|
||||
data.push_back(2);
|
||||
PutLe16(data, kRemoteMtu);
|
||||
SendSignal(kConfigurationRequest, channel.configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::ReceiveL2cap(const uint8_t* frame, size_t size) {
|
||||
if (m_baseband != Baseband::Connected || size < 4) return;
|
||||
const uint16_t length = Le16(frame);
|
||||
const uint16_t cid = Le16(frame + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": dropped malformed L2CAP frame" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (cid == kSignallingCid) {
|
||||
HandleSignalling(frame + 4, length);
|
||||
} else if (Channel* channel = FindByLocal(cid)) {
|
||||
HandleChannelData(*channel, frame + 4, length);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleSignalling(const uint8_t* data, size_t size) {
|
||||
while (size >= 4) {
|
||||
const uint8_t code = data[0];
|
||||
const uint8_t ident = data[1];
|
||||
const uint16_t length = Le16(data + 2);
|
||||
if (static_cast<size_t>(length) + 4 > size) {
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = data + 4;
|
||||
switch (code) {
|
||||
case kConnectionRequest: HandleConnectionRequest(ident, p, length); break;
|
||||
case kConnectionResponse: HandleConnectionResponse(ident, p, length); break;
|
||||
case kConfigurationRequest: HandleConfigurationRequest(ident, p, length); break;
|
||||
case kConfigurationResponse: HandleConfigurationResponse(ident, p, length); break;
|
||||
case kDisconnectionRequest: HandleDisconnectionRequest(ident, p, length); break;
|
||||
case kDisconnectionResponse: HandleDisconnectionResponse(p, length); break;
|
||||
case kEchoRequest: SendSignal(kEchoResponse, ident, std::vector<uint8_t>(p, p + length)); break;
|
||||
case kInformationRequest: {
|
||||
if (length >= 2) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, Le16(p));
|
||||
PutLe16(reply, 0x0001); // not supported
|
||||
SendSignal(kInformationResponse, ident, reply);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kCommandReject:
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game rejected L2CAP command "
|
||||
<< static_cast<int>(ident) << std::endl;
|
||||
break;
|
||||
case kEchoResponse:
|
||||
case kInformationResponse:
|
||||
break;
|
||||
default: {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0000); // command not understood
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
break;
|
||||
}
|
||||
}
|
||||
data += 4 + length;
|
||||
size -= 4 + length;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t psm = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
uint16_t result = kResultSuccess;
|
||||
uint16_t dcid = 0;
|
||||
if (psm != kPsmSdp && psm != kPsmHidControl && psm != kPsmHidInterrupt) {
|
||||
result = kResultPsmNotSupported;
|
||||
} else if (FindByPsm(psm) != nullptr) {
|
||||
// A real remote refuses a second channel on the same PSM.
|
||||
result = kResultNoResources;
|
||||
} else {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.remoteCid = scid;
|
||||
channel.connected = true;
|
||||
dcid = channel.localCid;
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
PutLe16(reply, result);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kConnectionResponse, ident, reply);
|
||||
if (result == kResultSuccess) {
|
||||
SendConfigRequest(*FindByLocal(dcid));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 8) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->weInitiated || channel->connected || channel->connectIdent != ident) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->remoteCid = dcid;
|
||||
channel->connected = true;
|
||||
SendConfigRequest(*channel);
|
||||
} else if (result == kResultPending) {
|
||||
channel->requestUs = m_nowUs;
|
||||
} else {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game refused L2CAP PSM 0x" << std::hex
|
||||
<< channel->psm << " (result 0x" << result << ")" << std::dec << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = m_nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
Channel* channel = FindByLocal(dcid);
|
||||
if (channel == nullptr) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0002); // invalid CID
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
return;
|
||||
}
|
||||
channel->pendingOptions.insert(channel->pendingOptions.end(), p + 4, p + n);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, channel->remoteCid);
|
||||
if (flags & 1) {
|
||||
PutLe16(reply, 1);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
return;
|
||||
}
|
||||
PutLe16(reply, 0);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
const std::vector<uint8_t>& options = channel->pendingOptions;
|
||||
for (size_t i = 0; i + 2 <= options.size();) {
|
||||
const uint8_t type = options[i] & 0x7F;
|
||||
const uint8_t length = options[i + 1];
|
||||
if (i + 2 + length > options.size()) break;
|
||||
if (type == kOptionMtu && length == 2) {
|
||||
channel->remoteMtu = Le16(&options[i + 2]);
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
} else if (type == kOptionFlushTimeout && length == 2) {
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
}
|
||||
i += 2 + length;
|
||||
}
|
||||
channel->pendingOptions.clear();
|
||||
channel->remoteConfigured = true;
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 6) return;
|
||||
const uint16_t scid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->configSent || channel->configIdent != ident) return;
|
||||
if (flags & 1) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->localConfigured = true;
|
||||
UpdateState();
|
||||
return;
|
||||
}
|
||||
// Our only option is the MTU; offer the default instead.
|
||||
channel->configIdent = NextIdent();
|
||||
channel->requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel->remoteCid);
|
||||
PutLe16(data, 0);
|
||||
SendSignal(kConfigurationRequest, channel->configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
SendSignal(kDisconnectionResponse, ident, reply);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == dcid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionResponse(const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == scid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleChannelData(Channel& channel, const uint8_t* data, size_t size) {
|
||||
if (size == 0) return;
|
||||
if (channel.psm == kPsmSdp) {
|
||||
const std::vector<uint8_t> response = Sdp::HandleRequest(data, size, channel.remoteMtu);
|
||||
SendData(channel, response);
|
||||
return;
|
||||
}
|
||||
if (channel.psm == kPsmHidControl) {
|
||||
HandleHidControl(channel, data, size);
|
||||
return;
|
||||
}
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
if (type == kHidData && param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleHidControl(Channel& channel, const uint8_t* data, size_t size) {
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
switch (type) {
|
||||
case kHidHandshake:
|
||||
return;
|
||||
case kHidControl:
|
||||
if (param == kHidVirtualCableUnplug) {
|
||||
const bool wasReady = m_everReady;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
if (wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case kHidSetReport:
|
||||
SendData(channel, {kHidHandshakeSuccess});
|
||||
if ((param & 0x3) == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
case kHidData:
|
||||
if (param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
SendData(channel, {kHidHandshakeUnsupported});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
constexpr uint8_t kErrorResponse = 0x01;
|
||||
constexpr uint8_t kServiceSearchRequest = 0x02;
|
||||
constexpr uint8_t kServiceSearchResponse = 0x03;
|
||||
constexpr uint8_t kServiceAttributeRequest = 0x04;
|
||||
constexpr uint8_t kServiceAttributeResponse = 0x05;
|
||||
constexpr uint8_t kServiceSearchAttributeRequest = 0x06;
|
||||
constexpr uint8_t kServiceSearchAttributeResponse = 0x07;
|
||||
|
||||
constexpr uint16_t kErrInvalidHandle = 0x0002;
|
||||
constexpr uint16_t kErrInvalidSyntax = 0x0003;
|
||||
constexpr uint16_t kErrInvalidPduSize = 0x0004;
|
||||
constexpr uint16_t kErrInvalidContinuation = 0x0005;
|
||||
|
||||
void PutBe16(Bytes& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
}
|
||||
|
||||
void PutBe32(Bytes& out, uint32_t v) {
|
||||
PutBe16(out, static_cast<uint16_t>(v >> 16));
|
||||
PutBe16(out, static_cast<uint16_t>(v));
|
||||
}
|
||||
|
||||
uint16_t Be16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>((p[0] << 8) | p[1]);
|
||||
}
|
||||
|
||||
uint32_t Be32(const uint8_t* p) {
|
||||
return (static_cast<uint32_t>(Be16(p)) << 16) | Be16(p + 2);
|
||||
}
|
||||
|
||||
// Data element constructors (Bluetooth Core, Vol 3 Part B, 3.2).
|
||||
Bytes U8(uint8_t v) { return {0x08, v}; }
|
||||
Bytes U16(uint16_t v) { Bytes b{0x09}; PutBe16(b, v); return b; }
|
||||
Bytes U32(uint32_t v) { Bytes b{0x0A}; PutBe32(b, v); return b; }
|
||||
Bytes Uuid16(uint16_t v) { Bytes b{0x19}; PutBe16(b, v); return b; }
|
||||
Bytes Bool(bool v) { return {0x28, static_cast<uint8_t>(v ? 1 : 0)}; }
|
||||
|
||||
Bytes WithLength(uint8_t type, size_t length, bool wide = false) {
|
||||
if (length <= 0xFF && !wide) {
|
||||
return {static_cast<uint8_t>((type << 3) | 5), static_cast<uint8_t>(length)};
|
||||
}
|
||||
Bytes b{static_cast<uint8_t>((type << 3) | 6)};
|
||||
PutBe16(b, static_cast<uint16_t>(length));
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(const Bytes& raw) {
|
||||
Bytes b = WithLength(4, raw.size());
|
||||
b.insert(b.end(), raw.begin(), raw.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(std::string_view s) {
|
||||
return Text(Bytes(s.begin(), s.end()));
|
||||
}
|
||||
|
||||
Bytes Seq(std::initializer_list<Bytes> items) {
|
||||
size_t length = 0;
|
||||
for (const Bytes& item : items) length += item.size();
|
||||
Bytes b = WithLength(6, length);
|
||||
for (const Bytes& item : items) b.insert(b.end(), item.begin(), item.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
using Record = std::vector<std::pair<uint16_t, Bytes>>;
|
||||
|
||||
const Bytes kHidDescriptor = {
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
|
||||
0x01, 0x06, 0x00, 0xFF, 0x09, 0x01, 0x91, 0x00, 0x85, 0x11, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x12, 0x95, 0x02, 0x09, 0x01, 0x91, 0x00, 0x85, 0x13, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x14, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x15, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x16, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x17, 0x95, 0x06, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x18, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x19, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x1A, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x20, 0x95, 0x06, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x21, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x22, 0x95, 0x04, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x30, 0x95, 0x02, 0x09, 0x01, 0x81, 0x00, 0x85, 0x31, 0x95, 0x05, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x32, 0x95, 0x0A, 0x09, 0x01, 0x81, 0x00, 0x85, 0x33, 0x95, 0x11, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x34, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x35, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x36, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x37, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3D, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3E, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3F, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0xC0,
|
||||
};
|
||||
|
||||
constexpr uint32_t kHidRecordHandle = 0x00010000;
|
||||
constexpr uint32_t kDeviceIdRecordHandle = 0x00010001;
|
||||
|
||||
const Record& HidRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kHidRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1124)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0011)}), Seq({Uuid16(0x0011)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0006, Seq({U16(0x656E), U16(0x006A), U16(0x0100)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1124), U16(0x0100)})})},
|
||||
{0x000D, Seq({Seq({Seq({Uuid16(0x0100), U16(0x0013)}), Seq({Uuid16(0x0011)})})})},
|
||||
{0x0100, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0101, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0102, Text("Nintendo")},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x0111)},
|
||||
{0x0202, U8(0x04)},
|
||||
{0x0203, U8(0x33)},
|
||||
{0x0204, Bool(false)},
|
||||
{0x0205, Bool(true)},
|
||||
{0x0206, Seq({Seq({U8(0x22), Text(kHidDescriptor)})})},
|
||||
{0x0207, Seq({Seq({U16(0x0409), U16(0x0100)})})},
|
||||
{0x0208, Bool(false)},
|
||||
{0x0209, Bool(true)},
|
||||
{0x020A, Bool(true)},
|
||||
{0x020B, U16(0x0100)},
|
||||
{0x020C, U16(0x0C80)},
|
||||
{0x020D, Bool(false)},
|
||||
{0x020E, Bool(false)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
const Record& DeviceIdRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kDeviceIdRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1200)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0001)}), Seq({Uuid16(0x0001)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1200), U16(0x0100)})})},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x057E)},
|
||||
{0x0202, U16(0x0306)},
|
||||
{0x0203, U16(0x0600)},
|
||||
{0x0204, Bool(true)},
|
||||
{0x0205, U16(0x0002)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
struct NamedRecord {
|
||||
uint32_t handle;
|
||||
const Record& (*get)();
|
||||
};
|
||||
constexpr std::array<NamedRecord, 2> kRecords = {{
|
||||
{kHidRecordHandle, HidRecord},
|
||||
{kDeviceIdRecordHandle, DeviceIdRecord},
|
||||
}};
|
||||
|
||||
using Uuid = std::array<uint8_t, 16>;
|
||||
|
||||
Uuid FromShort(uint32_t value) {
|
||||
// Bluetooth base UUID 00000000-0000-1000-8000-00805F9B34FB.
|
||||
Uuid u = {0, 0, 0, 0, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
|
||||
u[0] = static_cast<uint8_t>(value >> 24);
|
||||
u[1] = static_cast<uint8_t>(value >> 16);
|
||||
u[2] = static_cast<uint8_t>(value >> 8);
|
||||
u[3] = static_cast<uint8_t>(value);
|
||||
return u;
|
||||
}
|
||||
|
||||
// One parsed data element header: type, header size and payload size.
|
||||
struct ElementHeader {
|
||||
uint8_t type = 0;
|
||||
size_t headerSize = 0;
|
||||
size_t dataSize = 0;
|
||||
};
|
||||
|
||||
std::optional<ElementHeader> ParseHeader(const uint8_t* p, size_t n) {
|
||||
if (n == 0) return std::nullopt;
|
||||
ElementHeader h;
|
||||
h.type = static_cast<uint8_t>(p[0] >> 3);
|
||||
const uint8_t sizeIndex = p[0] & 7;
|
||||
if (h.type == 0) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = 0;
|
||||
} else if (sizeIndex <= 4) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = size_t{1} << sizeIndex;
|
||||
} else if (sizeIndex == 5) {
|
||||
if (n < 2) return std::nullopt;
|
||||
h.headerSize = 2;
|
||||
h.dataSize = p[1];
|
||||
} else if (sizeIndex == 6) {
|
||||
if (n < 3) return std::nullopt;
|
||||
h.headerSize = 3;
|
||||
h.dataSize = Be16(p + 1);
|
||||
} else {
|
||||
if (n < 5) return std::nullopt;
|
||||
h.headerSize = 5;
|
||||
h.dataSize = Be32(p + 1);
|
||||
}
|
||||
if (h.headerSize + h.dataSize > n) return std::nullopt;
|
||||
return h;
|
||||
}
|
||||
|
||||
std::optional<Uuid> ReadUuid(const uint8_t* p, const ElementHeader& h) {
|
||||
if (h.type != 3) return std::nullopt;
|
||||
const uint8_t* d = p + h.headerSize;
|
||||
if (h.dataSize == 2) return FromShort(Be16(d));
|
||||
if (h.dataSize == 4) return FromShort(Be32(d));
|
||||
if (h.dataSize == 16) {
|
||||
Uuid u{};
|
||||
std::copy(d, d + 16, u.begin());
|
||||
return u;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CollectUuids(const uint8_t* p, size_t n, std::vector<Uuid>& out) {
|
||||
while (n > 0) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h) return;
|
||||
if (h->type == 6 || h->type == 7) {
|
||||
CollectUuids(p + h->headerSize, h->dataSize, out);
|
||||
} else if (const auto uuid = ReadUuid(p, *h)) {
|
||||
out.push_back(*uuid);
|
||||
}
|
||||
const size_t total = h->headerSize + h->dataSize;
|
||||
p += total;
|
||||
n -= total;
|
||||
}
|
||||
}
|
||||
|
||||
bool RecordMatches(const Record& record, const std::vector<Uuid>& pattern) {
|
||||
std::vector<Uuid> present;
|
||||
for (const auto& [id, value] : record) CollectUuids(value.data(), value.size(), present);
|
||||
return std::all_of(pattern.begin(), pattern.end(), [&](const Uuid& wanted) {
|
||||
return std::find(present.begin(), present.end(), wanted) != present.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Parses a data element sequence at `p`; advances `p`/`n` past it.
|
||||
std::optional<std::pair<const uint8_t*, size_t>> TakeSequence(const uint8_t*& p, size_t& n) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h || (h->type != 6)) return std::nullopt;
|
||||
const std::pair<const uint8_t*, size_t> body{p + h->headerSize, h->dataSize};
|
||||
p += h->headerSize + h->dataSize;
|
||||
n -= h->headerSize + h->dataSize;
|
||||
return body;
|
||||
}
|
||||
|
||||
std::optional<std::vector<Uuid>> ParsePattern(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<Uuid> pattern;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h) return std::nullopt;
|
||||
const auto uuid = ReadUuid(q, *h);
|
||||
if (!uuid) return std::nullopt;
|
||||
pattern.push_back(*uuid);
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (pattern.empty() || pattern.size() > 12) return std::nullopt;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
using IdRange = std::pair<uint16_t, uint16_t>;
|
||||
|
||||
std::optional<std::vector<IdRange>> ParseAttributeIds(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<IdRange> ranges;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h || h->type != 1) return std::nullopt;
|
||||
const uint8_t* d = q + h->headerSize;
|
||||
if (h->dataSize == 2) {
|
||||
ranges.emplace_back(Be16(d), Be16(d));
|
||||
} else if (h->dataSize == 4) {
|
||||
ranges.emplace_back(Be16(d), Be16(d + 2));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (ranges.empty()) return std::nullopt;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Continuation state is the byte offset into the complete response (2 bytes BE).
|
||||
std::optional<size_t> ParseContinuation(const uint8_t* p, size_t n) {
|
||||
if (n < 1 || p[0] + size_t{1} != n) return std::nullopt;
|
||||
if (p[0] == 0) return 0;
|
||||
if (p[0] != 2) return std::nullopt;
|
||||
return Be16(p + 1);
|
||||
}
|
||||
|
||||
Bytes AttributeList(const Record& record, const std::vector<IdRange>& ranges) {
|
||||
Bytes inner;
|
||||
for (const auto& [id, value] : record) {
|
||||
const bool wanted = std::any_of(ranges.begin(), ranges.end(),
|
||||
[id = id](const IdRange& r) { return id >= r.first && id <= r.second; });
|
||||
if (!wanted) continue;
|
||||
const Bytes idElement = U16(id);
|
||||
inner.insert(inner.end(), idElement.begin(), idElement.end());
|
||||
inner.insert(inner.end(), value.begin(), value.end());
|
||||
}
|
||||
// A real remote always sends attribute lists with a 16-bit length.
|
||||
Bytes list = WithLength(6, inner.size(), true);
|
||||
list.insert(list.end(), inner.begin(), inner.end());
|
||||
return list;
|
||||
}
|
||||
|
||||
Bytes Pdu(uint8_t id, uint16_t tid, const Bytes& params) {
|
||||
Bytes out{id};
|
||||
PutBe16(out, tid);
|
||||
PutBe16(out, static_cast<uint16_t>(params.size()));
|
||||
out.insert(out.end(), params.begin(), params.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
Bytes Error(uint16_t tid, uint16_t code) {
|
||||
Bytes params;
|
||||
PutBe16(params, code);
|
||||
return Pdu(kErrorResponse, tid, params);
|
||||
}
|
||||
|
||||
// Emits the slice of `full` starting at `offset` that fits, plus continuation.
|
||||
Bytes Chunked(uint8_t responseId, uint16_t tid, const Bytes& full, size_t offset, uint16_t maxBytes,
|
||||
size_t maxResponse) {
|
||||
// PDU header 5, byte count 2, continuation up to 3.
|
||||
const size_t room = maxResponse > 10 ? maxResponse - 10 : 1;
|
||||
const size_t limit = std::max<size_t>(1, std::min<size_t>(maxBytes, room));
|
||||
const size_t count = std::min(limit, full.size() - offset);
|
||||
Bytes params;
|
||||
PutBe16(params, static_cast<uint16_t>(count));
|
||||
params.insert(params.end(), full.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
full.begin() + static_cast<std::ptrdiff_t>(offset + count));
|
||||
const size_t next = offset + count;
|
||||
if (next < full.size()) {
|
||||
params.push_back(2);
|
||||
PutBe16(params, static_cast<uint16_t>(next));
|
||||
} else {
|
||||
params.push_back(0);
|
||||
}
|
||||
return Pdu(responseId, tid, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor() {
|
||||
return kHidDescriptor;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse) {
|
||||
if (size < 5) {
|
||||
return Error(0, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t id = pdu[0];
|
||||
const uint16_t tid = Be16(pdu + 1);
|
||||
const uint16_t paramLength = Be16(pdu + 3);
|
||||
if (paramLength != size - 5) {
|
||||
return Error(tid, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t* p = pdu + 5;
|
||||
size_t n = paramLength;
|
||||
|
||||
switch (id) {
|
||||
case kServiceSearchRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxCount = Be16(p);
|
||||
if (!ParseContinuation(p + 2, n - 2)) return Error(tid, kErrInvalidContinuation);
|
||||
Bytes handles;
|
||||
uint16_t found = 0;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (found < maxCount && RecordMatches(record.get(), *pattern)) {
|
||||
PutBe32(handles, record.handle);
|
||||
++found;
|
||||
}
|
||||
}
|
||||
Bytes params;
|
||||
PutBe16(params, found);
|
||||
PutBe16(params, found);
|
||||
params.insert(params.end(), handles.begin(), handles.end());
|
||||
params.push_back(0);
|
||||
return Pdu(kServiceSearchResponse, tid, params);
|
||||
}
|
||||
case kServiceAttributeRequest: {
|
||||
if (n < 6) return Error(tid, kErrInvalidSyntax);
|
||||
const uint32_t handle = Be32(p);
|
||||
const uint16_t maxBytes = Be16(p + 4);
|
||||
p += 6;
|
||||
n -= 6;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
const auto record = std::find_if(kRecords.begin(), kRecords.end(),
|
||||
[&](const NamedRecord& r) { return r.handle == handle; });
|
||||
if (record == kRecords.end()) return Error(tid, kErrInvalidHandle);
|
||||
const Bytes full = AttributeList(record->get(), *ranges);
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
case kServiceSearchAttributeRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxBytes = Be16(p);
|
||||
p += 2;
|
||||
n -= 2;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
Bytes lists;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (RecordMatches(record.get(), *pattern)) {
|
||||
const Bytes list = AttributeList(record.get(), *ranges);
|
||||
lists.insert(lists.end(), list.begin(), list.end());
|
||||
}
|
||||
}
|
||||
Bytes full = WithLength(6, lists.size());
|
||||
full.insert(full.end(), lists.begin(), lists.end());
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceSearchAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
default:
|
||||
return Error(tid, kErrInvalidSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,126 @@
|
||||
// SYSCONF items behind the Wii Remote stack. The runtime never parses a
|
||||
// SYSCONF (SCCheckStatus is HLE'd), so these accessors serve the Bluetooth items
|
||||
// directly: BT.DINF registers the four virtual remotes of the emulated
|
||||
// controller, and the console settings come from Config.toml.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace BluetoothSysconf {
|
||||
|
||||
constexpr uint32_t kDeviceInfoSize = 0x461;
|
||||
constexpr uint32_t kCmpDeviceInfoSize = 0x205;
|
||||
constexpr uint32_t kDeviceEntrySize = 0x46;
|
||||
// [0..9] registered remotes, [10..13] the remote last connected on each channel.
|
||||
constexpr uint32_t kActiveEntriesStart = 10;
|
||||
constexpr const char kRemoteName[] = "Nintendo RVL-CNT-01";
|
||||
|
||||
void Fill(uint32_t buffer, uint32_t size, uint8_t value) {
|
||||
std::memset(Memory::GetPointer(buffer, size), value, size);
|
||||
}
|
||||
|
||||
void WriteEntry(uint32_t buffer, uint32_t index, size_t slot) {
|
||||
const uint32_t entry = buffer + 1 + index * kDeviceEntrySize;
|
||||
// SYSCONF keeps addresses most significant byte first.
|
||||
const Bluetooth::BdAddr addr = Bluetooth::Controller::RemoteAddress(slot);
|
||||
for (size_t i = 0; i < addr.size(); ++i) {
|
||||
Memory::Write8(entry + static_cast<uint32_t>(i), addr[addr.size() - 1 - i]);
|
||||
}
|
||||
std::memcpy(Memory::GetPointer(entry + 6, sizeof(kRemoteName)), kRemoteName, sizeof(kRemoteName));
|
||||
}
|
||||
|
||||
void SetReturn(CpuContext* ctx, uint32_t value) {
|
||||
ctx->gpr[3] = value;
|
||||
}
|
||||
|
||||
} // namespace BluetoothSysconf
|
||||
|
||||
extern "C" void SCGetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
const uint32_t buffer = ctx->gpr[3];
|
||||
try {
|
||||
BluetoothSysconf::Fill(buffer, BluetoothSysconf::kDeviceInfoSize, 0);
|
||||
Memory::Write8(buffer, static_cast<uint8_t>(Bluetooth::kRemoteSlots));
|
||||
for (size_t slot = 0; slot < Bluetooth::kRemoteSlots; ++slot) {
|
||||
BluetoothSysconf::WriteEntry(buffer, static_cast<uint32_t>(slot), slot);
|
||||
BluetoothSysconf::WriteEntry(buffer, BluetoothSysconf::kActiveEntriesStart + static_cast<uint32_t>(slot), slot);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtDeviceInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1ED0, SCGetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// WUD writes the list back after reconciling it with the controller's link
|
||||
// keys, and WPAD whenever a channel's remote changes. The virtual remotes are
|
||||
// fixed, so the list is rebuilt on every boot.
|
||||
extern "C" void SCSetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EDC, SCSetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// Remotes paired with 1+2 on the game's own sync screens. The virtual remotes
|
||||
// are all registered, so the list is always empty.
|
||||
extern "C" void SCGetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
try {
|
||||
BluetoothSysconf::Fill(ctx->gpr[3], BluetoothSysconf::kCmpDeviceInfoSize, 0);
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtCmpDevInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EE8, SCGetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCSetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EF4, SCSetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetBtDpdSensibility_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiIrSensitivity(), 1, 5)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F00, SCGetBtDpdSensibility_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiRumbleEnabled() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F68, SCGetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's vibration switch, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
const bool enabled = (ctx->gpr[3] & 0xFF) == 1;
|
||||
if (enabled != RuntimeConfigFile::WiiRumbleEnabled()) {
|
||||
RuntimeConfigFile::SetWiiRumbleEnabled(enabled);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FBC, SCSetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSensorBarPosition_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiSensorBarAbove() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FC4, SCGetWpadSensorBarPosition_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiSpeakerVolume(), 0, 0x7F)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B2018, SCGetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's speaker volume, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
const int32_t volume = std::min<int32_t>(static_cast<int32_t>(ctx->gpr[3] & 0xFF), 0x7F);
|
||||
if (volume != RuntimeConfigFile::WiiSpeakerVolume()) {
|
||||
RuntimeConfigFile::SetWiiSpeakerVolume(volume);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B206C, SCSetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "hle/bluetooth_hle.h"
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle/runtime_parse_helpers.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace BluetoothUsb {
|
||||
|
||||
constexpr const char* kDevicePath = "/dev/usb/oh1/57e/305";
|
||||
constexpr int32_t kDeviceFd = 5;
|
||||
constexpr int32_t kIpcSuccess = 0;
|
||||
constexpr int32_t kIpcEinval = -4;
|
||||
|
||||
constexpr uint32_t kCtrlMsg = 0;
|
||||
constexpr uint32_t kBulkMsg = 1;
|
||||
constexpr uint32_t kIntrMsg = 2;
|
||||
|
||||
constexpr uint8_t kEndpointEvents = 0x81;
|
||||
constexpr uint8_t kEndpointAclIn = 0x82;
|
||||
constexpr uint8_t kEndpointAclOut = 0x02;
|
||||
|
||||
// The guest copies each delivered packet into a GKI buffer that its 2 ms
|
||||
// Bluetooth tick frees; a pool it cannot refill drops the packet, so ACL data is
|
||||
// paced. Plenty for four remotes reporting at 200 Hz.
|
||||
constexpr double kAclPacketsPerMs = 3.0;
|
||||
constexpr double kAclBurst = 6.0;
|
||||
|
||||
struct HeldRead {
|
||||
bool active = false;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t length = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackArg = 0;
|
||||
};
|
||||
|
||||
struct Device {
|
||||
bool open = false;
|
||||
std::unique_ptr<Bluetooth::Controller> controller;
|
||||
HeldRead eventRead;
|
||||
HeldRead aclRead;
|
||||
double aclTokens = kAclBurst;
|
||||
uint64_t lastRefillUs = 0;
|
||||
};
|
||||
|
||||
Device& Dev() {
|
||||
static Device device;
|
||||
return device;
|
||||
}
|
||||
|
||||
uint64_t NowUs() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
Bluetooth::Controller& ControllerFor(Device& device) {
|
||||
if (!device.controller) {
|
||||
device.controller = std::make_unique<Bluetooth::Controller>(Wiimote::Host());
|
||||
}
|
||||
return *device.controller;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ReadGuest(uint32_t address, uint32_t length) {
|
||||
std::vector<uint8_t> data(length);
|
||||
if (length != 0) {
|
||||
std::memcpy(data.data(), Memory::GetPointer(address, length), length);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void Complete(HeldRead& read, const std::vector<uint8_t>& data) {
|
||||
const uint32_t size = static_cast<uint32_t>(std::min<size_t>(data.size(), read.length));
|
||||
if (size < data.size()) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: packet of " << data.size() << " bytes truncated to " << size << std::endl;
|
||||
}
|
||||
if (size != 0) {
|
||||
std::memcpy(Memory::GetPointer(read.buffer, size), data.data(), size);
|
||||
}
|
||||
read.active = false;
|
||||
NandQueueIosCallback(read.callback, static_cast<int32_t>(size), read.callbackArg);
|
||||
}
|
||||
|
||||
// Hands queued events and ACL data to whichever reads the guest has armed.
|
||||
bool Deliver(Device& device, uint64_t nowUs) {
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
bool delivered = false;
|
||||
std::vector<uint8_t> packet;
|
||||
if (device.eventRead.active && controller.PopEvent(packet)) {
|
||||
Complete(device.eventRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
if (nowUs > device.lastRefillUs) {
|
||||
device.aclTokens = std::min(kAclBurst, device.aclTokens + (nowUs - device.lastRefillUs) / 1000.0 * kAclPacketsPerMs);
|
||||
device.lastRefillUs = nowUs;
|
||||
}
|
||||
if (device.aclRead.active && device.aclTokens >= 1.0 && controller.PopAcl(packet)) {
|
||||
device.aclTokens -= 1.0;
|
||||
Complete(device.aclRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
bool Hold(HeldRead& read, uint32_t buffer, uint32_t length, uint32_t callback, uint32_t callbackArg) {
|
||||
if (length != 0) {
|
||||
Memory::GetPointer(buffer, length);
|
||||
}
|
||||
if (read.active) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: a second read on the same endpoint replaced the first" << std::endl;
|
||||
NandQueueIosCallback(read.callback, kIpcEinval, read.callbackArg);
|
||||
}
|
||||
read = {true, buffer, length, callback, callbackArg};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Executes a transfer. Returns true with `result` when it completed now.
|
||||
bool Transfer(uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t callback,
|
||||
uint32_t callbackArg, bool async, int32_t& result) {
|
||||
Device& device = Dev();
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
const uint64_t nowUs = NowUs();
|
||||
try {
|
||||
if (cmd == kCtrlMsg && numIn == 6 && numOut == 1) {
|
||||
const auto requestType = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 4);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 6);
|
||||
const uint8_t bmRequestType = Memory::Read8(requestType.address);
|
||||
// wLength is little-endian in the request block.
|
||||
const uint16_t wLength = static_cast<uint16_t>(Memory::Read8(lengthVector.address) |
|
||||
(Memory::Read8(lengthVector.address + 1) << 8));
|
||||
if (bmRequestType != 0x20 || wLength > data.size) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unexpected control transfer type 0x" << std::hex
|
||||
<< static_cast<int>(bmRequestType) << std::dec << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
const std::vector<uint8_t> command = ReadGuest(data.address, wLength);
|
||||
controller.ExecuteCommand(command.data(), command.size(), nowUs);
|
||||
result = wLength;
|
||||
return true;
|
||||
}
|
||||
if ((cmd == kBulkMsg || cmd == kIntrMsg) && numIn == 2 && numOut == 1) {
|
||||
const auto endpointVector = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 1);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 2);
|
||||
const uint8_t endpoint = Memory::Read8(endpointVector.address);
|
||||
// Bulk / interrupt lengths are big-endian.
|
||||
const uint16_t length = Memory::Read16(lengthVector.address);
|
||||
if (length > data.size) {
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
if (cmd == kBulkMsg && endpoint == kEndpointAclOut) {
|
||||
const std::vector<uint8_t> packet = ReadGuest(data.address, length);
|
||||
controller.ReceiveAcl(packet.data(), packet.size(), nowUs);
|
||||
result = length;
|
||||
return true;
|
||||
}
|
||||
if (async && cmd == kBulkMsg && endpoint == kEndpointAclIn) {
|
||||
Hold(device.aclRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
if (async && cmd == kIntrMsg && endpoint == kEndpointEvents) {
|
||||
Hold(device.eventRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported transfer cmd " << cmd << " endpoint 0x" << std::hex
|
||||
<< static_cast<int>(endpoint) << std::dec << (async ? "" : " (sync)") << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth transfer", e);
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported ioctlv " << cmd << " (" << numIn << " in, " << numOut << " out)"
|
||||
<< std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace BluetoothUsb
|
||||
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path) {
|
||||
if (path == nullptr || std::strcmp(path, BluetoothUsb::kDevicePath) != 0) {
|
||||
return 0;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (device.open) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: device opened twice" << std::endl;
|
||||
}
|
||||
device.open = true;
|
||||
device.eventRead = {};
|
||||
device.aclRead = {};
|
||||
device.aclTokens = BluetoothUsb::kAclBurst;
|
||||
device.lastRefillUs = BluetoothUsb::NowUs();
|
||||
BluetoothUsb::ControllerFor(device).Open(device.lastRefillUs);
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " opened" << std::endl;
|
||||
return BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd) {
|
||||
return static_cast<int32_t>(fd) == BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd) {
|
||||
if (!Bluetooth_HLE_IsFd(fd)) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
// IOS cancels the transfers still queued on a device it closes.
|
||||
for (BluetoothUsb::HeldRead* read : {&device.eventRead, &device.aclRead}) {
|
||||
if (read->active) {
|
||||
read->active = false;
|
||||
NandQueueIosCallback(read->callback, BluetoothUsb::kIpcEinval, read->callbackArg);
|
||||
}
|
||||
}
|
||||
BluetoothUsb::ControllerFor(device).Close();
|
||||
device.open = false;
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " closed" << std::endl;
|
||||
return BluetoothUsb::kIpcSuccess;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
result = BluetoothUsb::kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
return BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, callback, callbackArg, true, result);
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
int32_t result = BluetoothUsb::kIpcEinval;
|
||||
BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, 0, 0, false, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu) {
|
||||
(void)cpu;
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (!device.open) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t nowUs = BluetoothUsb::NowUs();
|
||||
try {
|
||||
BluetoothUsb::ControllerFor(device).Tick(nowUs);
|
||||
return BluetoothUsb::Deliver(device, nowUs);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth delivery", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1040,7 +1040,6 @@ static std::vector<uint8_t>& FlattenDisplayListScratch() {
|
||||
|
||||
static bool s_appliedVtxDescValid = false;
|
||||
static std::array<GXAttrType, 26> s_appliedVtxDesc{};
|
||||
static std::array<GXAttrType, 26> s_appliedSourceVtxDesc{};
|
||||
static bool s_appliedVtxAttrFmtValid = false;
|
||||
static VtxAttrFmt s_appliedVtxAttrFmt[8][26]{};
|
||||
|
||||
@@ -1075,14 +1074,12 @@ static void SyncAppliedVtxStateFromHleReal() {
|
||||
s_appliedVtxStateGeneration == g_hleGxState.vtxStateGeneration) {
|
||||
return;
|
||||
}
|
||||
// Both mirrors are dense arrays of the same trivially copyable element types
|
||||
// as their HLE counterparts, so publish them with three memcpys instead of
|
||||
// The mirrors are dense arrays of the same trivially copyable element types
|
||||
// as their HLE counterparts, so publish them with memcpys instead of
|
||||
// 234 individual element assignments.
|
||||
static_assert(sizeof(s_appliedVtxDesc) == sizeof(g_hleGxState.vtxDesc));
|
||||
static_assert(sizeof(s_appliedSourceVtxDesc) == sizeof(g_hleGxState.vtxDesc));
|
||||
static_assert(sizeof(s_appliedVtxAttrFmt) == sizeof(g_hleGxState.vtxAttrFmt));
|
||||
std::memcpy(s_appliedVtxDesc.data(), g_hleGxState.vtxDesc, sizeof(g_hleGxState.vtxDesc));
|
||||
std::memcpy(s_appliedSourceVtxDesc.data(), g_hleGxState.vtxDesc, sizeof(g_hleGxState.vtxDesc));
|
||||
s_appliedVtxDescValid = true;
|
||||
|
||||
std::memcpy(s_appliedVtxAttrFmt, g_hleGxState.vtxAttrFmt, sizeof(g_hleGxState.vtxAttrFmt));
|
||||
@@ -1137,10 +1134,6 @@ static void ApplyAuroraVtxDesc() {
|
||||
GXSetVtxDesc(gxAttr, type);
|
||||
s_appliedVtxDesc[attr] = type;
|
||||
}
|
||||
if (!hadValidState || s_appliedSourceVtxDesc[attr] != type) {
|
||||
GXSetSourceVtxDesc(gxAttr, type);
|
||||
s_appliedSourceVtxDesc[attr] = type;
|
||||
}
|
||||
}
|
||||
s_appliedVtxDescValid = true;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user