Compare commits

..

24 Commits

Author SHA1 Message Date
patchzyy b23d38e241 fail on unusable native index without sources, create assets dir before marker delete 2026-10-18 18:02:44 +02:00
patchzyy 954c43127c match script-mode fingerprint deployment target default 2026-10-18 17:27:48 +02:00
patchzyy 7d048d604b address review: validate reused disc data, fingerprint osx deployment target 2026-10-18 17:12:31 +02:00
patchzyy 4ae883ec84 merge main 2026-10-18 17:02:27 +02:00
patchzyy 252d00f697 Faster updates mac and linux 2026-10-18 16:47:28 +02:00
patchzyy 3ff14738bd Support Linux Wii controller mappings (#309)
* Support Linux Wii controller mappings

* read kernel remote buttons and pair classic node
2026-10-18 15:15:14 +02:00
patchzyy 8676d1ab14 Improve controller port identity matching (#305)
* Improve controller port identity matching

* fill in old saved port identities
2026-10-18 15:08:03 +02:00
patchzyy c255df5136 Synchronize surface release with GPU idle (#308) 2026-10-18 15:05:53 +02:00
patchzyy 2ee48fc880 Implement controller sumble on wiimote (#304) 2026-10-18 15:05:43 +02:00
patchzyy d7b64361e7 Fix soft-drift (#302) 2026-10-18 15:05:31 +02:00
patchzyy 5ee158918f Show sha (#301) 2026-10-18 15:05:19 +02:00
patchzyy bd12087cc3 Fix native build path quoting (#300) 2026-10-18 15:05:08 +02:00
patchzyy d5296779ec Store Wii calibration per remote (#299)
* Store Wii calibration per remote

* show calibration state per port

* tie calibration message to remote
2026-10-18 14:16:38 +02:00
patchzyy a93eaf582f Sky Station crash (#303) 2026-10-18 14:14:50 +02:00
patchzyy ed6fea0efb fix 279 (#297)
* fix 279

* treat conditional calls as calls

* keep local linked branches as jumps
2026-10-18 14:03:38 +02:00
patchzyy 95a49f3c89 fix 280 (#298)
* fix 280

* keep scalars and implicit tables in config
2026-10-18 13:47:47 +02:00
patchzyy 60197f2e75 Update AuroraSDL3Patches.cmake (#307)
* Update AuroraSDL3Patches.cmake

* wait for rumble before closing device
2026-10-18 13:47:25 +02:00
patchzyy b87186c96b Calibrate Wii accelerometers from EEPROM (#306) 2026-10-18 13:47:01 +02:00
patchzyy 0b12fc864a mac version fix (#296) 2026-10-18 13:08:15 +02:00
patchzyy 90589131b5 Fix the date 2026-10-18 00:09:54 +02:00
martipops bd219e38a3 fix: drive compile progress from ninja's [done/total] counter (#285)
The compile slice advanced a fixed step per line of build output, so how
far it moved depended on how many lines the build printed. When aurora and
its dependencies are compiled locally (as on macOS) the bar reached its cap
long before the game itself was compiled. Read ninja's edge counter instead.
2026-10-09 22:52:17 +02:00
therenow9 196b48528e Fix display lists dropping draws when the staging batch fills (#288)
* test: cover display lists that overflow the staging batch

GXCallDisplayList and GXCallDisplayListLE decode a list with a single
fifo::process() call. When a draw does not fit the staging batch,
process() stops at that draw, and the rest of the list is never drawn.

The test stubs can now admit N draws, refuse the next one as a full
batch would, and count the split that follows. The new test refuses
the middle or the last of three draws, in big- and little-endian
lists, and expects one split and the last draw's vertices.

Also stub aurora::window::set_force_aspect_16_9, called since #256,
so gx_fifo_tests links again.

Fails without the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: submit staging and continue when a display list overflows it

drain() already loops process() -> split_staging_batch() -> retry.
Move that loop into fifo::process_all() and use it for drain() and
both GXCallDisplayList variants, so a display list that fills the
staging batch submits it and draws the rest instead of dropping it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 22:48:24 +02:00
Michael G 54c608ef9c Add experimental x86-64-v2 CPU support for older PCs and Intel Macs (#258)
* runtime: add experimental x86-64-v2 profile

* fix review findings in network and translator

* build: include mbedtls in macos compile audit

* review fixes

* cmake: restore macOS deployment target floor before project()

* fix: Preserve explicit package versions and gate v3 tests on host support

* fix: Prevent TLS session reuse after failed writes

* fix: Gate cross-compiled profile tests on emulator availability

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-09 22:46:11 +02:00
patchzyy f3df63b529 Update README.md 2026-10-09 10:51:22 +02:00
117 changed files with 5611 additions and 2961 deletions
+23
View File
@@ -47,6 +47,29 @@ 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
+13 -5
View File
@@ -11,6 +11,11 @@ on:
tags:
- '*'
workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions:
contents: read
@@ -163,14 +168,17 @@ jobs:
- name: Build Setup.pkg
env:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version=""
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/')
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 \
+14
View File
@@ -45,6 +45,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
View File
@@ -1,4 +1,5 @@
/generated/
/runtime/native-index.json
/Assets/
/assets/
/PulsarPacks/
+36 -2
View File
@@ -112,6 +112,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 }
@@ -129,6 +143,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
@@ -136,11 +153,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.' }
@@ -222,6 +241,21 @@ 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'
& $translator emit-native-index --project (Join-Path $workspace 'projects\mkwii\recomp.yml') --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')
+2 -1
View File
@@ -429,7 +429,8 @@ try {
}
$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
+2 -1
View File
@@ -145,7 +145,8 @@ 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 `
+34 -11
View File
@@ -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
+58 -14
View File
@@ -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")
+1 -1
View File
@@ -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+'([^']+)'") |
@@ -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:&lt;id&gt;</c> lines. The
/// <summary>Build step identifiers from the build scripts' <c>MKWCBUILD:STEP:&lt;id&gt;</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:&lt;id&gt;</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";
}
+1 -1
View File
@@ -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
+117 -10
View File
@@ -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
View File
@@ -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
@@ -174,7 +176,29 @@ done
# 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 \
--project "$appdir/workspace/projects/mkwii/recomp.yml" \
--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
@@ -204,12 +228,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 /
+148
View File
@@ -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"
}
+42 -7
View File
@@ -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'
@@ -27,12 +28,15 @@ Usage: local-build-macos.command --output-dir DIR [options]
--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,6 +54,7 @@ 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
@@ -60,6 +65,10 @@ 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
@@ -118,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
@@ -133,10 +161,17 @@ 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'
# 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
"$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}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
+34 -15
View File
@@ -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,7 @@ translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
setup_version=""
usage() {
cat <<'EOF'
@@ -90,6 +93,8 @@ 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)
EOF
}
@@ -114,11 +119,17 @@ 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 ;;
-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) ;;
@@ -266,10 +277,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 +287,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 +340,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 +415,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 +433,8 @@ 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")
# 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 +453,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 +471,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) ;;
+12 -3
View File
@@ -20,13 +20,14 @@ resulting pkg is unsigned unless
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_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 ;;
@@ -38,14 +39,14 @@ while (($#)); do
--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 [[ -z "$version" || "$version" == "0.1.0" ]]; then
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)
@@ -149,8 +150,16 @@ for source in aurora-main projects runtime translator; do
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
copy_clean "$workspace/$source" "$resources/workspace/$source"
done
# 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 --project "$resources/workspace/projects/mkwii/recomp.yml" \
--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
+18 -4
View File
@@ -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"
+20 -12
View File
@@ -11,7 +11,13 @@ 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}]
@@ -85,15 +91,12 @@ if (( needs_workspace_refresh )); 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.
for source in aurora-main projects runtime translator Launcher; do
rm -rf "$workspace/$source"
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
# 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"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi
@@ -146,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"
@@ -172,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
+1
View File
@@ -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&amp;logo=github"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
@@ -15,6 +15,7 @@ function(aurora_copy_runtime_dlls target)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${_t}> $<TARGET_FILE_DIR:${target}>
COMMAND_EXPAND_LISTS
VERBATIM
)
endforeach ()
@@ -30,6 +31,7 @@ function(aurora_copy_runtime_dlls target)
if (EXISTS "${_dawn_bin_dir}/${_dll}")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dawn_bin_dir}/${_dll}" $<TARGET_FILE_DIR:${target}>
VERBATIM
)
endif ()
endforeach ()
+259
View File
@@ -32,6 +32,52 @@ function(aurora_sdl3_apply_patches sdl_source_dir)
if (NOT EXISTS "${_wii}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
endif ()
set(_rumble "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_rumble.c")
if (NOT EXISTS "${_rumble}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_rumble.c")
endif ()
set(_hidapi "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapijoystick.c")
if (NOT EXISTS "${_hidapi}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapijoystick.c")
endif ()
# Backport of SDL 71f4af732 (drop once SDL includes it): rumble writes must not hold dev_lock.
_aurora_sdl3_replace("${_rumble}" "HIDAPI rumble writes no longer hold the device lock"
[==[ if (request) {
SDL_LockMutex(request->device->dev_lock);
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
SDL_UnlockMutex(request->device->dev_lock);
]==]
[==[ if (request) {
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
]==])
# Without the lock, closing the handle must wait for in-flight rumble writes.
_aurora_sdl3_replace("${_hidapi}" "HIDAPI device close waits for pending rumble"
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
SDL_LockMutex(device->dev_lock);
]==]
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
while (SDL_GetAtomicInt(&device->rumble_pending) > 0) {
SDL_Delay(10);
}
SDL_LockMutex(device->dev_lock);
]==])
# Wii Remote: rebuild the joystick in place after an extension change.
#
@@ -118,6 +164,219 @@ static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
now = SDL_GetTicks();
]==])
# Classic Controller sticks: the stock path stretches each half-range by learned min/max;
# this posts WPAD's linear value (raw - first-report centre) * unit, times 64.
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (helper)"
[==[static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==]
[==[/* WiiCompiled: axis = (raw - centre) * unit * 64, y inverted; see AuroraSDL3Patches.cmake */
static void PostClassicStickLinear(Uint64 timestamp, SDL_Joystick *joystick, StickCalibrationData *calibration, Uint8 axis, Uint16 data, int unit)
{
int value;
if (!calibration->center) {
// Center on first read
calibration->center = data;
return;
}
value = ((int)data - (int)calibration->center) * unit * 64;
if (axis == SDL_GAMEPAD_AXIS_LEFTY || axis == SDL_GAMEPAD_AXIS_RIGHTY) {
value = -value;
}
SDL_SendJoystickAxis(timestamp, joystick, axis, (Sint16)SDL_clamp(value, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX));
}
static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (axes)"
[==[ PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry);
]==]
[==[ PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx, 32);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry, 32);
]==])
# Wii Remote: decode the accelerometer with the factory calibration from the EEPROM.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (context fields)"
[==[ StickCalibrationData m_StickCalibrationData[6];
} SDL_DriverWii_Context;
]==]
[==[ StickCalibrationData m_StickCalibrationData[6];
float m_fAccelZeroG[3];
float m_fAccelPerG[3];
} SDL_DriverWii_Context;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (memory space parameter)"
[==[static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(0x04 | (Uint8)ctx->m_bRumbleActive);
]==]
[==[static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync);
static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
return ReadMemory(ctx, 0x04, address, size, sync);
}
static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(addressSpace | (Uint8)ctx->m_bRumbleActive);
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (EEPROM read)"
[==[static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==]
[==[// Nominal decode: zero-g at 0x200, 100 units per g
#define WII_ACCEL_NOMINAL_ZERO_G 512.0f
#define WII_ACCEL_NOMINAL_PER_G 100.0f
// EEPROM calibration at 0x16, copy at 0x20: zero-g XYZ, 1 g XYZ, LSBs of both, checksum
#define WII_ACCEL_CALIBRATION_SIZE 10
#define WII_ADDRESS_SPACE_EEPROM 0x00
static bool ReadAccelCalibrationBlock(SDL_DriverWii_Context *ctx, Uint8 address, Uint8 *block)
{
if (!ReadMemory(ctx, WII_ADDRESS_SPACE_EEPROM, address, WII_ACCEL_CALIBRATION_SIZE, true)) {
return false;
}
if (ctx->m_rgucReadBuffer[5] != address ||
ctx->m_rgucReadBuffer[3] != ((WII_ACCEL_CALIBRATION_SIZE - 1) << 4)) {
SDL_SetError("Unexpected EEPROM read response");
return false;
}
SDL_memcpy(block, ctx->m_rgucReadBuffer + 6, WII_ACCEL_CALIBRATION_SIZE);
return true;
}
static bool ParseAccelCalibration(SDL_DriverWii_Context *ctx, const Uint8 *block)
{
int zero[3];
int perG[3];
Uint8 checksum = 0x55;
int i;
for (i = 0; i < WII_ACCEL_CALIBRATION_SIZE - 1; ++i) {
checksum = (Uint8)(checksum + block[i]);
}
if (checksum != block[WII_ACCEL_CALIBRATION_SIZE - 1]) {
SDL_SetError("Bad checksum");
return false;
}
for (i = 0; i < 3; ++i) {
int shift = 4 - 2 * i;
int oneG = (block[4 + i] << 2) | ((block[7] >> shift) & 0x03);
zero[i] = (block[i] << 2) | ((block[3] >> shift) & 0x03);
perG[i] = oneG - zero[i];
// Sanity limits around the nominal values
if (zero[i] < 384 || zero[i] > 640 || perG[i] < 60 || perG[i] > 160) {
SDL_SetError("Implausible values");
return false;
}
}
for (i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = (float)zero[i];
ctx->m_fAccelPerG[i] = (float)perG[i];
}
return true;
}
static void ReadAccelCalibration(SDL_DriverWii_Context *ctx)
{
static const Uint8 addresses[] = { 0x16, 0x20 };
Uint8 block[WII_ACCEL_CALIBRATION_SIZE];
int i;
for (i = 0; i < SDL_arraysize(addresses); ++i) {
if (ReadAccelCalibrationBlock(ctx, addresses[i], block) &&
ParseAccelCalibration(ctx, block)) {
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration from EEPROM 0x%02X: zero %d/%d/%d, %d/%d/%d units per g",
addresses[i],
(int)ctx->m_fAccelZeroG[0], (int)ctx->m_fAccelZeroG[1], (int)ctx->m_fAccelZeroG[2],
(int)ctx->m_fAccelPerG[0], (int)ctx->m_fAccelPerG[1], (int)ctx->m_fAccelPerG[2]);
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration at EEPROM 0x%02X unusable: %s", addresses[i], SDL_GetError());
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Using nominal accelerometer calibration");
}
static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal defaults)"
[==[ ctx->device = device;
device->context = ctx;
]==]
[==[ ctx->device = device;
device->context = ctx;
for (int i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = WII_ACCEL_NOMINAL_ZERO_G;
ctx->m_fAccelPerG[i] = WII_ACCEL_NOMINAL_PER_G;
}
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (read at init)"
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
]==]
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
ReadAccelCalibration(ctx);
]==])
# Reports without accelerometer bytes (status, memory) decoded as garbage.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer guard"
[==[ }
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==]
[==[ }
if (!data->hasAccelerometer) {
return;
}
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal constant)"
[==[const WiiButtonData *data)
{
const float ACCEL_RES_PER_G = 100.0f;
Sint16 x, y, z;
]==]
[==[const WiiButtonData *data)
{
Sint16 x, y, z;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (decode)"
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03)) - 0x200;
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02)) - 0x200;
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02)) - 0x200;
values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
]==]
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
values[2] = (((float)y - ctx->m_fAccelZeroG[1]) / ctx->m_fAccelPerG[1]) * SDL_STANDARD_GRAVITY;
]==])
endfunction()
# Script mode (FetchContent PATCH_COMMAND).
+2 -2
View File
@@ -66,7 +66,7 @@ void GXCallDisplayList(const void* data, u32 nbytes) {
// Decode the display list immediately while its borrowed resources are valid.
aurora::gx::fifo::drain();
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, true);
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, true);
}
void GXCallDisplayListLE(const void* data, u32 nbytes) {
@@ -84,6 +84,6 @@ void GXCallDisplayListLE(const void* data, u32 nbytes) {
aurora::gx::fifo::drain();
// Process the display list through the command processor (little-endian)
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, false);
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, false);
}
}
+6 -3
View File
@@ -195,7 +195,7 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
}};
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
// singles them out, so they are picked by the name the driver gives them (see
// singles them out, so they are picked by name or USB id (see
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
// Controller, are read by the game through KPAD instead and never get a
// GameCube mapping (the runtime hides those ports from PADRead).
@@ -451,11 +451,14 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
// other names it produces are Wii Remotes, which the game reads through KPAD
// and which therefore never take a GameCube mapping.
// and which therefore never take a GameCube mapping. The Linux kernel driver's
// "Nintendo Wii Remote Pro Controller" is only recognised by its USB id.
static bool wii_default_mapping(const aurora::input::GameController* controller,
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
const char* name = SDL_GetGamepadName(controller->m_controller);
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
const bool wiiUProId = SDL_GetGamepadVendor(controller->m_controller) == 0x057e &&
SDL_GetGamepadProduct(controller->m_controller) == 0x0330;
if (!wiiUProId && (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr)) {
return false;
}
out = g_defaultButtonsWiiUPro;
+1 -1
View File
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue;
}
s_replacementIndex.try_emplace(*parsed, path);
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
}
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
+8 -4
View File
@@ -82,19 +82,23 @@ void drain() {
if (detail::sBufferSize == 0) {
return;
}
process_all(detail::sBufferData, detail::sBufferSize, true);
detail::sBufferSize = 0;
}
void process_all(const uint8_t* data, uint32_t size, bool bigEndian) {
uint32_t consumed = 0;
bool retried = false;
while (consumed < detail::sBufferSize) {
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
while (consumed < size) {
const auto count = process(data + consumed, size - consumed, bigEndian);
if (count == 0 && retried)
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
consumed += count;
if (consumed == detail::sBufferSize) break;
if (consumed == size) break;
// process returned with its renderer lock released. No recursive drain.
gfx::split_staging_batch();
retried = true;
}
detail::sBufferSize = 0;
}
const uint8_t* get_buffer_data() { return detail::sBufferData; }
+4
View File
@@ -81,6 +81,10 @@ bool in_display_list();
// Drain the internal FIFO buffer through the command processor
void drain();
// Decode a whole command stream, submitting the staging batch whenever a draw does not fit.
// process() alone stops at that draw, so a caller that ignores its count drops the rest.
void process_all(const uint8_t* data, uint32_t size, bool bigEndian);
// Internal buffer inspection
const uint8_t* get_buffer_data();
uint32_t get_buffer_size();
+16
View File
@@ -174,6 +174,12 @@ static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const Shade
case GX_CC_KONST: {
switch (stage.kcSel) {
DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKColorSel>(0x08):
case static_cast<GXTevKColorSel>(0x09):
case static_cast<GXTevKColorSel>(0x0A):
case static_cast<GXTevKColorSel>(0x0B):
return "vec3f(0.0)";
case GX_TEV_KCSEL_8_8:
return "vec3f(1.0)";
case GX_TEV_KCSEL_7_8:
@@ -275,6 +281,16 @@ static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const Shade
case GX_CA_KONST: {
switch (stage.kaSel) {
DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKAlphaSel>(0x08):
case static_cast<GXTevKAlphaSel>(0x09):
case static_cast<GXTevKAlphaSel>(0x0A):
case static_cast<GXTevKAlphaSel>(0x0B):
case static_cast<GXTevKAlphaSel>(0x0C):
case static_cast<GXTevKAlphaSel>(0x0D):
case static_cast<GXTevKAlphaSel>(0x0E):
case static_cast<GXTevKAlphaSel>(0x0F):
return "0.0";
case GX_TEV_KASEL_8_8:
return "1.0";
case GX_TEV_KASEL_7_8:
+108 -58
View File
@@ -14,6 +14,7 @@
#include <array>
#include <string>
#include <utility>
#include <vector>
using namespace std::string_view_literals;
@@ -23,7 +24,7 @@ absl::flat_hash_map<Uint32, GameController> g_GameControllers;
namespace {
constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT');
constexpr uint32_t kPortPreferencesVersion = 2;
constexpr uint32_t kPortPreferencesVersion = 3;
constexpr uint32_t kMaxPersistedStringLength = 256;
enum class PortPreferenceState : uint8_t {
@@ -35,6 +36,8 @@ enum class PortPreferenceState : uint8_t {
struct ControllerIdentity {
std::string guid;
std::string serial;
std::string path;
int32_t slot = -1;
};
struct PortPreference {
@@ -44,6 +47,9 @@ struct PortPreference {
std::array<PortPreference, PAD_MAX_CONTROLLERS> g_portPreferences;
bool g_portPreferencesLoaded = false;
// Startup controllers are added one by one via events; capture their SDL player index at init,
// before earlier port reassignments can move it.
absl::flat_hash_map<SDL_JoystickID, int32_t> g_connectSlots;
std::string port_preferences_path() {
if (g_config.userPath == nullptr) {
@@ -83,6 +89,8 @@ ControllerIdentity controller_identity(const GameController& controller) {
SDL_GUIDToString(SDL_GetGamepadGUIDForID(SDL_GetGamepadID(controller.m_controller)), guid, sizeof(guid));
identity.guid = guid;
identity.serial = normalize_serial(SDL_GetGamepadSerial(controller.m_controller));
identity.path = controller.m_path;
identity.slot = controller.m_slot;
return identity;
}
@@ -137,12 +145,16 @@ bool write_string(SDL_IOStream* file, const std::string& value) {
return write_value(file, size) && (size == 0 || write_exact(file, value.data(), size));
}
bool read_identity(SDL_IOStream* file, ControllerIdentity& identity) {
return read_string(file, identity.guid) && read_string(file, identity.serial);
bool read_identity(SDL_IOStream* file, uint32_t version, ControllerIdentity& identity) {
if (!read_string(file, identity.guid) || !read_string(file, identity.serial)) {
return false;
}
return version < 3 || (read_string(file, identity.path) && read_value(file, identity.slot));
}
bool write_identity(SDL_IOStream* file, const ControllerIdentity& identity) {
return write_string(file, identity.guid) && write_string(file, identity.serial);
return write_string(file, identity.guid) && write_string(file, identity.serial) &&
write_string(file, identity.path) && write_value(file, identity.slot);
}
bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>& preferences) {
@@ -159,13 +171,13 @@ bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>&
uint32_t magic = 0;
uint32_t version = 0;
bool ok = read_value(file, magic) && read_value(file, version) && magic == kPortPreferencesMagic &&
version == kPortPreferencesVersion;
version >= 2 && version <= kPortPreferencesVersion;
for (auto& preference : preferences) {
uint8_t state = 0;
ControllerIdentity identity;
ok = ok && read_value(file, state) && state <= static_cast<uint8_t>(PortPreferenceState::Controller) &&
read_identity(file, identity);
read_identity(file, version, identity);
if (ok) {
preference.state = static_cast<PortPreferenceState>(state);
preference.identity = std::move(identity);
@@ -225,22 +237,31 @@ void save_port_preferences() {
}
}
enum class IdentityMatch {
None,
Fallback,
Exact,
};
// Zero means no match. Higher scores claim ports first: path+slot > GUID > VID/PID.
constexpr int kScoreVidPid = 1;
constexpr int kScoreGuid = 2;
constexpr int kScoreSlot = 1;
constexpr int kScorePath = 2;
IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerIdentity& current) {
int identity_score(const ControllerIdentity& saved, const ControllerIdentity& current) {
if (saved.guid.empty()) {
return IdentityMatch::None;
return 0;
}
if (saved.guid == current.guid) {
return saved.serial.empty() || serial_matches(saved.serial, current.serial) ? IdentityMatch::Exact
: IdentityMatch::None;
if (!saved.serial.empty() && !serial_matches(saved.serial, current.serial)) {
return 0;
}
int score = kScoreGuid;
if (!saved.path.empty() && saved.path == current.path) {
score += kScorePath;
}
if (saved.slot >= 0 && saved.slot == current.slot) {
score += kScoreSlot;
}
return score;
}
if (!serial_matches(saved.serial, current.serial)) {
return IdentityMatch::None;
return 0;
}
// Attempt to match against VID/PID as a fallback if the GUID changes
@@ -250,9 +271,9 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
uint16_t currentProduct = 0;
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(saved.guid.c_str()), &savedVendor, &savedProduct, nullptr, nullptr);
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(current.guid.c_str()), &currentVendor, &currentProduct, nullptr, nullptr);
return savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct
? IdentityMatch::Fallback
: IdentityMatch::None;
const bool sameDevice =
savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct;
return sameDevice ? kScoreVidPid : 0;
}
void assign_player_index(GameController& controller, int32_t port) {
@@ -267,12 +288,6 @@ int32_t effective_player_index(const GameController& controller) {
return player >= 0 ? player : controller.m_playerIndex;
}
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
Uint32 instance) {
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
claimedControllers.begin() + claimedCount;
}
void apply_port_preferences() noexcept {
ensure_port_preferences_loaded();
if (!std::any_of(g_portPreferences.begin(), g_portPreferences.end(),
@@ -288,45 +303,63 @@ void apply_port_preferences() noexcept {
}
}
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedControllers{};
struct Candidate {
int score;
uint32_t port;
Uint32 instance;
};
std::vector<Candidate> candidates;
for (const auto& [instance, controller] : g_GameControllers) {
const auto identity = controller_identity(controller);
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state != PortPreferenceState::Controller) {
continue;
}
if (const int score = identity_score(g_portPreferences[port].identity, identity); score > 0) {
candidates.push_back({score, port, instance});
}
}
}
// Best matches claim first so identical controllers each end up on their own port
std::sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) {
if (a.score != b.score) {
return a.score > b.score;
}
return a.port != b.port ? a.port < b.port : a.instance < b.instance;
});
std::array<bool, PAD_MAX_CONTROLLERS> portClaimed{};
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedInstances{};
size_t claimedCount = 0;
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
const auto& preference = g_portPreferences[port];
if (preference.state != PortPreferenceState::Controller) {
bool upgraded = false;
for (const auto& candidate : candidates) {
if (portClaimed[candidate.port] ||
std::find(claimedInstances.begin(), claimedInstances.begin() + claimedCount, candidate.instance) !=
claimedInstances.begin() + claimedCount) {
continue;
}
portClaimed[candidate.port] = true;
claimedInstances[claimedCount++] = candidate.instance;
auto& controller = g_GameControllers[candidate.instance];
assign_player_index(controller, static_cast<int32_t>(candidate.port));
Uint32 fallbackInstance = 0;
GameController* fallbackController = nullptr;
for (auto& [instance, controller] : g_GameControllers) {
if (is_instance_claimed(claimedControllers, claimedCount, instance)) {
continue;
// Preferences saved before path and slot existed gain them on their first GUID match
auto& saved = g_portPreferences[candidate.port].identity;
if (candidate.score >= kScoreGuid && (saved.path.empty() || saved.slot < 0)) {
const auto current = controller_identity(controller);
if (saved.path.empty() && !current.path.empty()) {
saved.path = current.path;
upgraded = true;
}
switch (identity_match(preference.identity, controller_identity(controller))) {
case IdentityMatch::Exact:
assign_player_index(controller, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = instance;
fallbackController = nullptr;
break;
case IdentityMatch::Fallback:
// Prefer any later exact match before claiming a fallback candidate
if (fallbackController == nullptr) {
fallbackInstance = instance;
fallbackController = &controller;
}
continue;
case IdentityMatch::None:
continue;
if (saved.slot < 0 && current.slot >= 0) {
saved.slot = current.slot;
upgraded = true;
}
break;
}
if (fallbackController != nullptr) {
assign_player_index(*fallbackController, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = fallbackInstance;
}
}
if (upgraded) {
save_port_preferences();
}
}
// SDL only hands out a player index when the device already had a gamepad mapping
@@ -401,6 +434,15 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
if (const char* path = SDL_GetGamepadPath(ctrl); path != nullptr) {
controller.m_path = path;
}
if (const auto slot = g_connectSlots.find(which); slot != g_connectSlots.end()) {
controller.m_slot = slot->second;
g_connectSlots.erase(slot);
} else {
controller.m_slot = SDL_GetGamepadPlayerIndex(ctrl);
}
const char* serial = SDL_GetGamepadSerial(ctrl);
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
if (controller.m_isGameCube ||
@@ -420,8 +462,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
bool hasOtherPortPreference = false;
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
IdentityMatch::None) {
identity_score(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) > 0) {
hasOtherPortPreference = true;
break;
}
@@ -457,6 +498,7 @@ bool refresh_controller(SDL_JoystickID instance) noexcept {
}
void remove_controller(Uint32 instance) noexcept {
g_connectSlots.erase(instance);
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
SDL_CloseGamepad(it->second.m_controller);
g_GameControllers.erase(it);
@@ -540,6 +582,14 @@ void initialize() noexcept {
* as expected */
ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD), "Failed to initialize SDL subsystems: {}",
SDL_GetError());
int count = 0;
if (SDL_JoystickID* ids = SDL_GetGamepads(&count)) {
for (int i = 0; i < count; ++i) {
g_connectSlots[ids[i]] = SDL_GetGamepadPlayerIndexForID(ids[i]);
}
SDL_free(ids);
}
}
struct MouseScrollStatus {
+3
View File
@@ -20,6 +20,9 @@ struct GameController {
bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1;
Sint32 m_playerIndex = -1;
// Device path and the SDL player index at connect (the adapter port for GameCube adapters).
std::string m_path;
Sint32 m_slot = -1;
bool m_hasRumble = false;
PADDeadZones m_deadZones{
.emulateTriggers = true,
+6 -1
View File
@@ -893,9 +893,14 @@ void release_surface() noexcept {
}
g_surface = {};
if (hadSurface && g_instance && g_device && g_queue) {
// Retired swapchain is freed after a later submission completes and the device ticks.
submit_staging_commands(g_device.CreateCommandEncoder().Finish());
const auto future = g_queue.OnSubmittedWorkDone(
wgpu::CallbackMode::WaitAnyOnly, [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {});
g_instance.WaitAny(future, 1000000000);
if (const auto status = g_instance.WaitAny(future, 1000000000); status != wgpu::WaitStatus::Success) {
Log.warn("Timed out waiting for queue idle on surface release: {}", magic_enum::enum_name(status));
}
g_device.Tick();
}
}
+4
View File
@@ -50,6 +50,9 @@ void reset_uniform_allocations() noexcept;
const std::vector<uint8_t>& uniform_allocation(size_t index) noexcept;
void use_draw_command_tracking(bool enabled) noexcept;
void use_real_vertex_format_helpers(bool enabled) noexcept;
// Admit this many draws, then refuse one as if the staging batch were full.
void refuse_staging_admission_after(uint32_t admissions) noexcept;
void reset_staging_capacity() noexcept;
} // namespace aurora::gfx::testing
class GXFifoTest : public ::testing::Test {
@@ -65,6 +68,7 @@ protected:
aurora::gfx::testing::reset_resolve_pass_records();
aurora::gfx::testing::reset_vertex_push_record();
aurora::gfx::testing::use_real_vertex_format_helpers(false);
aurora::gfx::testing::reset_staging_capacity();
}
// Copy the internal FIFO buffer contents and clear it
+30 -2
View File
@@ -34,6 +34,9 @@ std::optional<aurora::gx::DrawData> s_lastGxDraw;
bool s_trackDrawCommands = false;
bool s_useRealVertexFormatHelpers = false;
std::deque<std::vector<uint8_t>> s_uniformAllocations;
std::optional<uint32_t> s_stagingAdmissionsBeforeRefusal;
bool s_stagingRefused = false;
uint64_t s_stagingSplitCount = 0;
} // namespace
// --- aurora::g_config ---
@@ -301,8 +304,23 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
bool staging_has_space(const StagingSizes&) { return true; }
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
bool staging_has_space(const StagingSizes&) {
if (!s_stagingAdmissionsBeforeRefusal) return true;
if (*s_stagingAdmissionsBeforeRefusal > 0) {
--*s_stagingAdmissionsBeforeRefusal;
return true;
}
// Refuse once, as a full batch would; the split that follows makes room again.
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = true;
return false;
}
void split_staging_batch() {
if (!s_stagingRefused) throw StagingCapacityError("Unexpected split in FIFO unit test");
s_stagingRefused = false;
++s_stagingSplitCount;
}
uint64_t staging_split_count() noexcept { return s_stagingSplitCount; }
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
@@ -345,6 +363,15 @@ void use_draw_command_tracking(bool enabled) noexcept {
void use_real_vertex_format_helpers(bool enabled) noexcept {
s_useRealVertexFormatHelpers = enabled;
}
void refuse_staging_admission_after(uint32_t admissions) noexcept {
s_stagingAdmissionsBeforeRefusal = admissions;
s_stagingRefused = false;
}
void reset_staging_capacity() noexcept {
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = false;
s_stagingSplitCount = 0;
}
} // namespace aurora::gfx::testing
// --- Pipeline/draw command stubs ---
@@ -525,6 +552,7 @@ std::optional<TextureHandle> find_replacement(const GXTexObj_&) noexcept { retur
namespace aurora::window {
AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; }
void set_frame_buffer_aspect_fit(bool) {}
void set_force_aspect_16_9(bool) {}
} // namespace aurora::window
// --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) ---
@@ -88,6 +88,31 @@ TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
}
TEST_F(GXFifoTest, DisplayListSubmitsStagingWhenADrawDoesNotFit) {
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
// admitted=1 refuses the middle draw; admitted=2 refuses the last, so only a replay of the refused draw pushes 0x33.
for (const u32 admitted : {1u, 2u}) for (const bool bigEndian : {true, false}) {
std::vector<u8> list;
for (const u8 fill : {0x11, 0x22, 0x33}) {
auto bytes = draw(GX_TRIANGLES, 3);
if (!bigEndian) std::swap(bytes[1], bytes[2]);
std::fill(bytes.begin() + 3, bytes.end(), fill);
list.insert(list.end(), bytes.begin(), bytes.end());
}
const auto splitsBefore = aurora::gfx::staging_split_count();
aurora::gfx::testing::refuse_staging_admission_after(admitted);
if (bigEndian) GXCallDisplayList(list.data(), static_cast<u32>(list.size()));
else GXCallDisplayListLE(list.data(), static_cast<u32>(list.size()));
EXPECT_EQ(aurora::gfx::staging_split_count() - splitsBefore, 1u)
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), (std::vector<u8>{0x33, 0x33, 0x33}))
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
}
}
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
using namespace aurora::gfx;
StagingMapState state;
+54 -163
View File
@@ -42,12 +42,38 @@ else()
"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
@@ -68,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
@@ -85,21 +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)
# POSIX x86-64 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 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.
@@ -294,20 +307,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()
@@ -336,137 +345,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()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_helpers_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_memory"
"${CMAKE_CURRENT_LIST_DIR}/include/isa"
"${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -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-v3)
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/RuntimeLibrary.cmake")
# 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()
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 "${CMAKE_CURRENT_LIST_DIR}/tests/psq_reserved_tests.cmake")
endif()
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)
add_executable(mkw_runtime_config_tests "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_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 "${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 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
"${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")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_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
"${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)
add_executable(mkw_macos_external_audio_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_external_audio_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${CMAKE_CURRENT_LIST_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)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
endif()
# The translator emits the complete, content-addressed source graph. Consuming
@@ -476,8 +373,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()
@@ -494,20 +391,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}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
@@ -518,10 +408,11 @@ else()
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
# Keep this compile-only audit on the same Haswell-era x86-64-v3
# baseline as the translated product. PPC paired FMA helpers use
# FMA intrinsics and intentionally cannot compile for plain x86-64.
target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
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()
+29
View File
@@ -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()
+8 -4
View File
@@ -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
+12
View File
@@ -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}")
+188
View File
@@ -0,0 +1,188 @@
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)
# 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()
+84 -115
View File
@@ -4,55 +4,42 @@
# 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)
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio 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_RUNTIME_SOURCE_DIR}/../aurora-main/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
@@ -74,85 +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).
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
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)
if(MKW_PLATFORM_MACOS)
# CoreAudio framework is required for automatic music muting on macOS.
target_link_libraries(mkw_runtime_common PRIVATE "${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(mkw_runtime_common PRIVATE mkw::libco)
endif()
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}")
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(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})
@@ -175,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
@@ -198,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)
@@ -223,7 +201,7 @@ 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)
@@ -257,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()
@@ -266,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}")
@@ -278,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
@@ -290,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
@@ -304,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)
@@ -317,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()
@@ -333,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()
+98
View File
@@ -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()
+180
View File
@@ -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_RUNTIME_SOURCE_DIR}/../aurora-main/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)
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()
+8
View File
@@ -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
+7
View File
@@ -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
+5 -629
View File
@@ -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;
+4
View File
@@ -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
+84
View File
@@ -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;
}
+618
View File
@@ -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);
}
+48
View File
@@ -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
+59
View File
@@ -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
+19
View File
@@ -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"
+66
View File
@@ -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;
}
+114
View File
@@ -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"
+732
View File
@@ -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);
}
@@ -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);
+2 -50
View File
@@ -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
+10
View File
@@ -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
+1 -1
View File
@@ -4,7 +4,7 @@
// HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers;
// Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses
// 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.
+2 -152
View File
@@ -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;
}
+21 -1
View File
@@ -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]]
+1 -1
View File
@@ -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
{
+1 -1
View File
@@ -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"
+1 -103
View File
@@ -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"
+1 -726
View File
@@ -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"
+2 -4
View File
@@ -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"
+1
View File
@@ -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"
+2 -40
View File
@@ -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.)
+94 -117
View File
@@ -69,10 +69,11 @@ struct RuntimeUserConfig {
// 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;
// measured in the overlay with the remote at rest. Stored per remote under
// [controller.wii_accel_offsets]; the old shared
// wii_accel_offset_* keys are only detected, to report that they are ignored.
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
bool hasLegacyWiiAccelOffset = false;
// 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;
@@ -138,28 +139,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();
@@ -403,6 +382,28 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
}
// Reads every supported setting out of a parsed Config.toml document.
// [x, y, z] of finite numbers (integers allowed), else nullopt.
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
if (!value.is_array() || value.as_array().size() != 3) {
return std::nullopt;
}
std::array<double, 3> offset{};
for (size_t i = 0; i < 3; ++i) {
const toml::value& element = value.as_array()[i];
if (element.is_floating()) {
offset[i] = element.as_floating();
} else if (element.is_integer()) {
offset[i] = static_cast<double>(element.as_integer());
} else {
return std::nullopt;
}
if (!std::isfinite(offset[i])) {
return std::nullopt;
}
}
return offset;
}
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config;
@@ -426,6 +427,13 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerExpressions[key] = value.as_string();
}
}
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
if (const auto offset = ParseAccelOffset(value)) {
config.wiiAccelOffsets[key] = *offset;
}
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
@@ -487,9 +495,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
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");
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
}
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
@@ -546,68 +554,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;
@@ -619,9 +609,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);
}
@@ -884,12 +872,13 @@ inline bool SetWiiContinuousScanEnabled(bool value) {
return WriteSetting("controller", "wii_continuous_scan", 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)};
// Stored correction of the remote identified by `remoteKey`, if it has one.
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
const auto it = Get().wiiAccelOffsets.find(remoteKey);
if (it == Get().wiiAccelOffsets.end()) {
return std::nullopt;
}
return it->second;
}
// Whether to write the per-frame accelerometer trace (off unless asked for).
@@ -897,26 +886,14 @@ inline bool WiiAccelTraceEnabled(bool fallback = false) {
return Get().wiiAccelTrace.value_or(fallback);
}
// 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;
}
// 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;
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsets[remoteKey] = offset;
// Always float literals, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
<< ']';
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
}
// Target frame rate for frame interpolation, or 0 to disable it.
+13 -14
View File
@@ -1,6 +1,8 @@
#pragma once
#include <array>
#include <cstdint>
#include <optional>
struct PADStatus;
@@ -73,23 +75,20 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// 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.
// Residual zero-point trim on top of SDL's factory-calibrated decode, stored per
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
// still, buttons up; Poll() averages about a second and stores it for that remote.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
bool IsAccelCalibrating();
// Forgets the stored correction of the remote on `chan`.
void ClearAccelCalibration(uint32_t chan);
bool IsAccelCalibrating(uint32_t chan);
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Outcome of the last calibration attempt for the overlay, or nullptr.
const char* AccelCalibrationMessage();
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
// nullopt when the port has no remote.
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
const char* AccelCalibrationMessage(uint32_t chan);
// 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.
+57 -5
View File
@@ -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();
}
+5
View File
@@ -89,6 +89,11 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
} // namespace
extern "C" bool PAD_HLE_RumbleEnabled()
{
return g_rumbleEnabled.load(std::memory_order_relaxed);
}
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
{
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
+5 -2
View File
@@ -5,6 +5,9 @@
#include <cstdint>
#include <dolphin/pad.h>
extern "C" bool PAD_HLE_RumbleEnabled();
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
namespace {
@@ -143,8 +146,8 @@ PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t t
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
{
(void)chan;
(void)command;
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
}
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
-1
View File
@@ -242,7 +242,6 @@ void WritePollResults(uint32_t outAddress,
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
// network_socket.cpp
int32_t DeleteWiiSocket(uint32_t fd);
void CleanupAllWiiSockets();
sockaddr_in ReadWiiSockAddr(uint32_t addr);
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
+21 -3
View File
@@ -54,6 +54,9 @@ constexpr int kMaxSslSessions = 4;
struct SslSession {
bool active = false;
bool handshaked = false;
// A failed POSIX TLS write cannot be resumed with a new guest buffer.
// Keep the slot and socket ownership intact until explicit teardown.
bool failed = false;
bool plaintextWfc = false;
uint32_t socketFd = UINT32_MAX;
NativeSocket native = kInvalidSocket;
@@ -761,6 +764,14 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
return SSL_OK;
}
static int32_t FailSslWrite(SslSession& ssl) {
ssl.failed = true;
// Stop transport I/O without deleting the guest descriptor or freeing a
// session still referenced by the IOCTLV_NET_SSL_WRITE caller.
::shutdown(ssl.native, SHUT_RDWR);
return SSL_ERR_FAILED;
}
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
if (!data || size == 0) {
return SSL_ERR_ZERO;
@@ -795,12 +806,11 @@ static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
}
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
if (std::chrono::steady_clock::now() >= writeDeadline) {
DeleteWiiSocket(ssl.socketFd);
return SSL_ERR_FAILED;
return FailSslWrite(ssl);
}
continue;
}
return SSL_ERR_FAILED;
return FailSslWrite(ssl);
}
return static_cast<int32_t>(totalWritten);
}
@@ -842,6 +852,9 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
// The handshake runs on every SSL read/write, so a failure repeats for as long
// as the session lives; report only the first one.
static int32_t SslHandshake(SslSession& ssl) {
if (ssl.failed) {
return SSL_ERR_FAILED;
}
const int32_t result = SslHandshakeImpl(ssl);
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
ssl.loggedHandshakeFail = true;
@@ -947,6 +960,11 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
WriteSslReturn(in, SSL_ERR_ID);
return 0;
}
// Reject before NAS buffering can acknowledge data on a failed session.
if (g_sslSessions[sslId].failed) {
WriteSslReturn(in, SSL_ERR_FAILED);
return 0;
}
if (out.size() < 2 || !out[1].address) {
WriteSslReturn(in, SSL_ERR_FAILED);
return 0;
+3 -3
View File
@@ -11,7 +11,7 @@
#include "ppc_runtime.h"
#include "runtime_log.h"
#include "hle/net/network.h"
#include "generated/RuntimeConfig.h"
#include "guest_small_data.h"
#include "os_internal.h"
extern "C" void func_801AADE0(CpuContext* ctx);
@@ -41,7 +41,7 @@ struct AlarmProcessScope {
// the other call sites have never logged.
void EnsureSda1Base(CpuContext* cpu, bool logOnce = false)
{
if (cpu->gpr[13] == RuntimeConfig::SDA1_BASE) {
if (cpu->gpr[13] == GuestSmallData::kSda1Base) {
return;
}
@@ -51,7 +51,7 @@ void EnsureSda1Base(CpuContext* cpu, bool logOnce = false)
RT_LOG(RT_TAG_OS) << "OS_HLE_ProcessAlarms: fixing r13 to SDA1 base" << std::endl;
});
}
cpu->gpr[13] = RuntimeConfig::SDA1_BASE;
cpu->gpr[13] = GuestSmallData::kSda1Base;
}
void IncrementSchedulerDisableCount()
+46 -23
View File
@@ -1,14 +1,12 @@
// Host ISA guard. Every other product target builds with -march=x86-64-v3, so a pre-Haswell
// Intel or pre-Excavator AMD machine would otherwise die on an illegal-instruction fault with no
// Host ISA guard. Product targets build with an explicit x86-64-v2 or x86-64-v3 profile, so an
// unsupported machine would otherwise die on an illegal-instruction fault with no
// explanation. This TU alone skips that flag (own CMake object library, excluded from unity
// build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer
// and thus the first AVX2 code that could execute. Keep it free of anything that could pull in
// vectorized code: no iostreams, no std::string, no runtime-wide headers.
//
// x86-64-v3 is an x86-specific optional-feature baseline (AVX2/BMI2/FMA and friends are not
// guaranteed present on every x86_64 chip); nothing here applies on AArch64, where ASIMD/NEON is
// mandatory in the base architecture and PublicProducts.cmake never applies an -march=x86-64-v3
// equivalent flag to begin with. That branch below is a no-op stub, not a port of this check.
// x86-64-v2/v3 are x86-specific optional-feature baselines; nothing here applies on AArch64,
// where ASIMD/NEON is mandatory in the base architecture.
#if defined(__x86_64__)
@@ -58,27 +56,27 @@ struct CpuFeature {
bool isOsXsave;
};
// Everything x86-64-v3 implies, which includes all of x86-64-v2. Spelled out so
// the error message can name the exact instruction sets the machine lacks
// rather than only "AVX2", which is merely the best known member of the set.
// x86-64-v2 requirements. The v3-only extension below retains a useful
// feature-by-feature diagnostic rather than reducing a failed v3 check to AVX2.
constexpr CpuFeature kRequiredFeatures[] = {
{"SSE3", 1, 0, 2, 0, false},
{"SSSE3", 1, 0, 2, 9, false},
{"FMA", 1, 0, 2, 12, false},
{"CMPXCHG16B", 1, 0, 2, 13, false},
{"SSE4.1", 1, 0, 2, 19, false},
{"SSE4.2", 1, 0, 2, 20, false},
{"MOVBE", 1, 0, 2, 22, false},
{"POPCNT", 1, 0, 2, 23, false},
{"OSXSAVE", 1, 0, 2, 27, true},
{"AVX", 1, 0, 2, 28, false},
{"F16C", 1, 0, 2, 29, false},
{"BMI1", 7, 0, 1, 3, false},
{"AVX2", 7, 0, 1, 5, false},
{"BMI2", 7, 0, 1, 8, false},
{"LAHF-SAHF", 0x80000001u, 0, 2, 0, false},
};
#if !defined(MKW_X86_CPU_PROFILE_V2)
constexpr CpuFeature kV3RequiredFeatures[] = {
{"FMA", 1, 0, 2, 12, false}, {"MOVBE", 1, 0, 2, 22, false},
{"OSXSAVE", 1, 0, 2, 27, true}, {"AVX", 1, 0, 2, 28, false},
{"F16C", 1, 0, 2, 29, false}, {"BMI1", 7, 0, 1, 3, false},
{"AVX2", 7, 0, 1, 5, false}, {"BMI2", 7, 0, 1, 8, false},
{"LZCNT", 0x80000001u, 0, 2, 5, false},
};
#endif
// Fixed-capacity text accumulation: no allocation, no exceptions, nothing that
// could route through code this file is trying to stay ahead of.
@@ -130,6 +128,27 @@ bool CollectMissingBaselineFeatures(TextBuffer& missing) {
ok = false;
}
#if !defined(MKW_X86_CPU_PROFILE_V2)
for (const CpuFeature& feature : kV3RequiredFeatures) {
const bool leafAvailable = (feature.leaf & 0x80000000u) != 0
? feature.leaf <= maxExtended
: feature.leaf <= maxBasic;
bool present = false;
if (leafAvailable) {
unsigned regs[4] = {0, 0, 0, 0};
HostCpuId(feature.leaf, feature.subleaf, regs);
present = (regs[feature.reg] & (1u << feature.bit)) != 0;
}
if (present) {
haveOsXsave = haveOsXsave || feature.isOsXsave;
continue;
}
if (!ok) missing.Append(", ");
missing.Append(feature.name);
ok = false;
}
#endif
// CPUID reporting AVX is not sufficient: the OS also has to have enabled
// XMM and YMM state saving or every VEX-encoded instruction faults. This is
// the same guard a compiler's own runtime feature dispatch applies.
@@ -175,13 +194,17 @@ void WriteStdErrEarly(const char* text) {
[[noreturn]] void ReportUnsupportedCpu(const char* missing) {
TextBuffer message;
message.Append(
"This build needs a processor that supports AVX2 and the rest of the "
"x86-64-v3 instruction set.\n\nMissing on this machine: ");
#if defined(MKW_X86_CPU_PROFILE_V2)
message.Append("This build needs a processor that supports the x86-64-v2 instruction set.\n\nMissing on this machine: ");
#else
message.Append("This build needs a processor that supports the x86-64-v3 instruction set.\n\nMissing on this machine: ");
#endif
message.Append(missing);
message.Append(
"\n\nx86-64-v3 covers Intel Core processors from Haswell (4th "
"generation, 2013) onward and AMD processors from Excavator (2015) onward.");
#if defined(MKW_X86_CPU_PROFILE_V2)
message.Append("\n\nx86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors from Jaguar (2013) onward.");
#else
message.Append("\n\nx86-64-v3 covers Intel Core processors from Haswell (4th generation, 2013) onward and AMD processors from Excavator (2015) onward.");
#endif
// The tag matches RT_TAG_RUNTIME in runtime_log.h. It is spelled out here
// because this translation unit must not include runtime-wide headers (see
+9 -4
View File
@@ -152,6 +152,8 @@ private:
#endif
} // namespace
extern const char* const kMkwSetupVersion;
namespace {
std::string g_lastEntryLabel;
@@ -378,10 +380,13 @@ void AttachParentConsoleForDiagnostics() {
void AttachParentConsoleForDiagnostics() {}
#endif
// The setup writes build-fingerprint.json beside every product executable; its SetupVersion is
// the only version identity the runtime has (products are compiled locally, so nothing is baked
// into the binary). Surface it at the top of the transcript so every attached log self-identifies.
// The macOS and Linux local builds bake the setup version in; the Windows setup instead writes it
// to build-fingerprint.json beside the executable. Surfaced at the top of the transcript so every
// attached log self-identifies.
std::string ReadInstalledSetupVersion() {
if (const std::string_view builtIn = kMkwSetupVersion; !builtIn.empty()) {
return std::string(builtIn);
}
const auto directory = RuntimeConfigFile::ExecutableDirectory();
if (!directory) {
return {};
@@ -625,7 +630,7 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
symbol->MaxNameLen = MAX_SYM_NAME;
if (symbolsReady && SymFromAddr(process, addr, &symbolDisp, symbol)) {
symbolName = symbol->Name;
} else if (auto info = TranslatedFunctionRegistry::FindByHostAddress(static_cast<uintptr_t>(addr))) {
} else if (auto info = FindTranslatedFunctionByHostAddress(static_cast<uintptr_t>(addr))) {
// DbgHelp could not name it; the translated-function registry can.
translatedFuncName = info->name;
ppcAddress = info->address;
+13
View File
@@ -1,6 +1,7 @@
#include "platform/host_platform.h"
#include <cstdlib>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
@@ -46,6 +47,18 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#elif defined(__linux__)
std::vector<char> buffer(256);
for (;;) {
const auto length = ::readlink("/proc/self/exe", buffer.data(), buffer.size());
if (length < 0) {
return std::nullopt;
}
if (static_cast<std::size_t>(length) < buffer.size()) {
return std::filesystem::path(std::string(buffer.data(), length)).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#else
return std::nullopt;
#endif
+39 -1
View File
@@ -12,6 +12,7 @@
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <stdexcept>
#include <limits>
@@ -44,11 +45,48 @@ uint32_t g_reservationAddr = 0;
bool g_hasReservation = false;
const auto g_timeBaseStart = std::chrono::steady_clock::now();
// Days from 1970-01-01 to the given civil date (Howard Hinnant's algorithm).
constexpr int64_t DaysFromCivil(int64_t y, unsigned m, unsigned d)
{
y -= m <= 2;
const int64_t era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = static_cast<unsigned>(y - era * 400);
const unsigned doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + static_cast<int64_t>(doe) - 719468;
}
// The Wii RTC holds local time; the OS counts ticks from 2000-01-01.
uint64_t InitialTimeBase()
{
const std::time_t now = std::time(nullptr);
std::tm local{};
#if defined(_WIN32)
if (localtime_s(&local, &now) != 0) {
return 0;
}
#else
if (localtime_r(&now, &local) == nullptr) {
return 0;
}
#endif
const int64_t days = DaysFromCivil(local.tm_year + 1900, static_cast<unsigned>(local.tm_mon + 1),
static_cast<unsigned>(local.tm_mday)) -
DaysFromCivil(2000, 1, 1);
const int64_t seconds = days * 86400 + local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec;
if (seconds <= 0) {
return 0;
}
return static_cast<uint64_t>(seconds) * TimeBaseContract::kTicksPerSecond;
}
const uint64_t g_timeBaseOrigin = InitialTimeBase();
uint64_t GetTimeBase() {
const auto elapsed = std::chrono::steady_clock::now() - g_timeBaseStart;
const auto nanoseconds =
std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
return TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
return g_timeBaseOrigin + TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
}
constexpr uint32_t kFpscrFx = 1u << 31;
+25 -11
View File
@@ -309,10 +309,9 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
} else {
ImGui::TextDisabled("No accelerometer data yet.");
}
// SDL's read of the remote's calibration block often times out over Bluetooth
// and it falls back to a nominal zero point, leaving a small per-axis bias;
// measured here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating()) {
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
// here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating(port)) {
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
WiiRemoteInput::StartAccelCalibration(port);
@@ -322,18 +321,20 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
"for about two seconds. Corrects the steering offset of a remote held sideways.");
}
ImGui::SameLine();
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
if (ImGui::Button("Clear calibration")) {
WiiRemoteInput::ClearAccelCalibration();
WiiRemoteInput::ClearAccelCalibration(port);
}
ImGui::EndDisabled();
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
ImGui::TextWrapped("%s", message);
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
} else if (calibrated) {
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
ImGui::TextDisabled("No trim stored.");
}
}
@@ -779,8 +780,15 @@ void DrawControllerSettings() {
if (ImGui::BeginMenu("Assign connected controller")) {
for (uint32_t index = 0; index < controllerCount; ++index) {
const char* name = PADGetNameForControllerIndex(index);
std::string label = name != nullptr ? name : "Unknown controller";
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
if (PADGetIndexForPort(port) == static_cast<s32>(index)) {
label += " (Port " + std::to_string(port + 1) + ")";
break;
}
}
ImGui::PushID(static_cast<int>(index));
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
if (ImGui::MenuItem(label.c_str())) {
PADSetPortForIndex(index, selectedGamePort);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
ApplyConfiguredMappings();
@@ -790,6 +798,12 @@ void DrawControllerSettings() {
ImGui::EndMenu();
}
if (WiiRemoteInput::IsRemoteChannel(selectedGamePort)) {
ImGui::TextDisabled("The game reads this controller as a Wii Remote, so the button mapping does not apply.");
DrawRumbleSettings();
return;
}
uint32_t mappingCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
+4 -4
View File
@@ -57,8 +57,8 @@ std::optional<std::string> PrintableU32(uint32_t value) {
return out;
}
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address) {
auto best = TranslatedFunctionRegistry::FindNearestByAddress(address);
std::optional<TranslatedFunctionInfo> FindNearbyTranslatedFunction(uint32_t address) {
auto best = FindNearestTranslatedFunction(address);
// Crash-only heuristic: large gaps are usually data/FourCC values, not code offsets.
if (best && address - best->address < 0x10000u) {
@@ -161,7 +161,7 @@ void PrintGuestValue(std::ostream& os, std::string_view label, uint32_t value) {
} else {
os << "<" << exact->name << ">";
}
} else if (auto nearest = FindNearestTranslatedFunction(value)) {
} else if (auto nearest = FindNearbyTranslatedFunction(value)) {
if (mapName && mapStart >= nearest->address) {
os << "<" << mapName;
if (value != mapStart) {
@@ -255,7 +255,7 @@ void DumpGuestStackHints(std::ostream& os, const CpuContext* cpu) {
if (!value) {
continue;
}
if (!TranslatedFunctionRegistry::FindByAddressPtr(*value) && !FindNearestTranslatedFunction(*value)) {
if (!TranslatedFunctionRegistry::FindByAddressPtr(*value) && !FindNearbyTranslatedFunction(*value)) {
continue;
}
if (printed == 0) {
+385 -96
View File
@@ -5,6 +5,7 @@
#include <dolphin/pad.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_hidapi.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_sensor.h>
@@ -12,11 +13,16 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
namespace WiiRemoteInput {
@@ -63,6 +69,8 @@ constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x00
kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800,
kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000;
constexpr Uint16 kNintendoVendor = 0x057e;
// How long a vanished remote keeps its channel alive with neutral input. SDL's
// in-place reconnect after an extension change takes well under a second; a
// remote that is really gone shows up as disconnected after this.
@@ -86,36 +94,46 @@ bool g_scanning = false;
uint64_t g_driverOffSinceMs = 0;
// When the current scan started (last Wii controller seen).
uint64_t g_lostAtMs = 0;
bool g_hidrawWarned = false;
// Instance ids whose accelerometers have been switched on. SDL keeps sensors
// off until asked and forgets that when the gamepad is closed, so a re-paired
// remote gets a fresh id and is enabled again.
std::array<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
// Zero-point correction subtracted from the remote's accelerometer, in g and in
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
// calibration run. The Nunchuk accelerometer is left uncorrected.
std::array<float, 3> g_accelOffset{};
bool g_accelOffsetLoaded = false;
// Zero-point correction subtracted from a remote's accelerometer, in g and in
// SDL's sensor frame, with the config key it is stored under. Resolved once per
// gamepad instance (a re-created joystick of the same remote resolves to the same
// key) and replaced by a calibration run. The Nunchuk accelerometer is left
// uncorrected.
struct RemoteCalibration {
SDL_JoystickID instance = 0;
std::string key;
std::array<float, 3> offset{};
};
std::array<RemoteCalibration, PAD_MAX_CONTROLLERS> g_remoteCalibration{};
// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a
// sample may stray from the first one before the run is declared "moved".
constexpr int kCalibrationSamples = 90;
constexpr float kCalibrationMaxDeviationG = 0.15f;
// |mean| outside this range means the remote was not at rest or the
// accelerometer is far off its nominal scale; either way the offset is useless.
constexpr float kCalibrationMinGravityG = 0.8f;
constexpr float kCalibrationMaxGravityG = 1.2f;
// A resting remote reads about 1 g and lies within 20 degrees of face up.
constexpr double kCalibrationMinGravityG = 0.8;
constexpr double kCalibrationMaxGravityG = 1.2;
constexpr double kCalibrationMinTiltCosine = 0.9396926;
struct AccelCalibration {
bool active = false;
uint32_t chan = 0;
SDL_JoystickID instance = 0;
int count = 0;
double sum[3] = {};
float first[3] = {};
};
AccelCalibration g_calibration;
char g_calibrationMessage[160] = {};
uint32_t g_calibrationMessageChan = 0;
SDL_JoystickID g_calibrationMessageRemote = 0;
// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on
// frames that bring no sample or a glitched one, so the game never sees a jump.
@@ -155,14 +173,73 @@ void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) {
}
}
// Loads the stored zero-point correction once.
const std::array<float, 3>& AccelOffset() {
if (!g_accelOffsetLoaded) {
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
g_accelOffsetLoaded = true;
// USB interface number of the HID device at `path`, or -1 when it is not a USB
// interface or cannot be found. A DolphinBar shows each remote slot as one.
int UsbInterfaceForPath(const char* path, Uint16 vendor, Uint16 product) {
if (path == nullptr) {
return -1;
}
return g_accelOffset;
int interfaceNumber = -1;
SDL_hid_device_info* list = SDL_hid_enumerate(vendor, product);
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
if (info->path != nullptr && std::strcmp(info->path, path) == 0) {
interfaceNumber = info->interface_number;
break;
}
}
SDL_hid_free_enumeration(list);
return interfaceNumber;
}
// Config key identifying the physical remote behind a gamepad: its HID serial
// (the Bluetooth address when paired directly), else its USB interface number
// (a DolphinBar has no serial and one interface per remote slot) and the game
// port, else the game port. Only characters valid in a bare TOML key.
std::string RemoteKey(SDL_Gamepad* gamepad, uint32_t port) {
std::string serial;
if (const char* raw = SDL_GetGamepadSerial(gamepad)) {
for (const char* c = raw; *c != '\0'; ++c) {
if (std::isalnum(static_cast<unsigned char>(*c))) {
serial += static_cast<char>(std::tolower(static_cast<unsigned char>(*c)));
}
}
}
if (serial.find_first_not_of('0') != std::string::npos) {
return "serial_" + serial;
}
const Uint16 vendor = SDL_GetGamepadVendor(gamepad);
const Uint16 product = SDL_GetGamepadProduct(gamepad);
const int interfaceNumber = UsbInterfaceForPath(SDL_GetGamepadPath(gamepad), vendor, product);
char key[48];
if (interfaceNumber >= 0) {
std::snprintf(key, sizeof(key), "usb_%04x_%04x_if%d_port%u", vendor, product, interfaceNumber,
port + 1);
} else {
std::snprintf(key, sizeof(key), "port_%u", port + 1);
}
return key;
}
// Calibration of the remote currently on `port`, resolved on first use per gamepad
// instance. A remote without a stored entry has a zero offset.
RemoteCalibration& CalibrationFor(SDL_Gamepad* gamepad, uint32_t port) {
RemoteCalibration& calibration = g_remoteCalibration[port];
const SDL_JoystickID id = SDL_GetGamepadID(gamepad);
if (calibration.instance != id) {
static bool s_legacyReported = false;
if (!s_legacyReported && RuntimeConfigFile::Get().hasLegacyWiiAccelOffset) {
s_legacyReported = true;
RT_LOG(RT_TAG_CONFIG) << "Old shared Wii Remote calibration ignored; recalibrate each remote in F10"
<< std::endl;
}
calibration.instance = id;
calibration.key = RemoteKey(gamepad, port);
const std::optional<std::array<double, 3>> stored = RuntimeConfigFile::WiiAccelOffsetFor(calibration.key);
for (size_t i = 0; i < 3; ++i) calibration.offset[i] = stored ? static_cast<float>((*stored)[i]) : 0.0f;
RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": calibration key " << calibration.key
<< (stored ? "" : " (no stored offset)") << std::endl;
}
return calibration;
}
// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet.
@@ -200,9 +277,10 @@ bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin);
// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of
// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip.
// The remote's sample gets the zero-point correction; the Nunchuk's does not.
// False when there is no sample yet or the sample is a glitch (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
// The remote's sample gets the zero-point correction of the remote on `port`; the
// Nunchuk's does not. False when there is no sample yet or the sample is a glitch
// (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* g) {
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, sensor, sdl)) {
return false;
@@ -212,7 +290,7 @@ bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
return false;
}
if (sensor == SDL_SENSOR_ACCEL) {
const std::array<float, 3>& offset = AccelOffset();
const std::array<float, 3>& offset = CalibrationFor(gamepad, port).offset;
for (int i = 0; i < 3; ++i) g[i] -= offset[i];
}
return true;
@@ -231,9 +309,9 @@ void AccelGToKpad(const float* g, float* kpad) {
}
// Sensor -> KPAD acc for ReadKpadSample.
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* kpad) {
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* kpad) {
float g[3] = {};
if (!ReadAccelG(gamepad, sensor, g)) {
if (!ReadAccelG(gamepad, port, sensor, g)) {
return false;
}
AccelGToKpad(g, kpad);
@@ -275,10 +353,25 @@ void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, boo
trace.flush();
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(const char* message) {
g_calibration.active = false;
// Gamepad of the Wii Remote on `chan`, or nullptr.
SDL_Gamepad* RemoteGamepad(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS || !IsRemoteChannel(chan)) {
return nullptr;
}
return SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
}
void SetCalibrationMessage(uint32_t chan, const char* message) {
SDL_Gamepad* gamepad = RemoteGamepad(chan);
g_calibrationMessageChan = chan;
g_calibrationMessageRemote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message);
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(uint32_t chan, const char* message) {
g_calibration.active = false;
SetCalibrationMessage(chan, message);
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl;
}
@@ -288,9 +381,10 @@ void StepAccelCalibration() {
if (!g_calibration.active) {
return;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
FinishAccelCalibration("Cancelled: the Wii Remote went away.");
SDL_Gamepad* gamepad = RemoteGamepad(g_calibration.chan);
if (gamepad == nullptr ||
SDL_GetGamepadID(gamepad) != g_calibration.instance) {
FinishAccelCalibration(g_calibration.chan, "Cancelled: the Wii Remote went away.");
return;
}
EnsureSensors(gamepad, g_calibration.chan);
@@ -308,7 +402,7 @@ void StepAccelCalibration() {
} else {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) {
FinishAccelCalibration("Failed: the remote moved. Put it down, buttons up, and try again.");
FinishAccelCalibration(g_calibration.chan, "Failed: the remote moved. Put it down, buttons up, and try again.");
return;
}
}
@@ -320,19 +414,20 @@ void StepAccelCalibration() {
std::array<double, 3> mean{};
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
mean[1] / length < kCalibrationMinTiltCosine) {
FinishAccelCalibration(g_calibration.chan, "Failed: the remote was not resting flat with the buttons up.");
return;
}
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
g_accelOffsetLoaded = true;
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(offset);
RemoteCalibration& calibration = CalibrationFor(gamepad, g_calibration.chan);
for (int i = 0; i < 3; ++i) calibration.offset[i] = static_cast<float>(offset[i]);
const bool saved = RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, offset);
char message[160];
std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g",
saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1],
offset[2]);
FinishAccelCalibration(message);
FinishAccelCalibration(g_calibration.chan, message);
}
// True when any controller aurora knows about is a Wii device.
@@ -346,6 +441,110 @@ bool AnyWiiControllerConnected() {
return false;
}
// True for an evdev node served by the kernel hid-wiimote driver.
bool IsKernelWiiNode(SDL_JoystickID id) {
const char* path = SDL_GetGamepadPathForID(id);
return SDL_GetGamepadVendorForID(id) == kNintendoVendor && path != nullptr &&
std::strncmp(path, "/dev/input/", 11) == 0;
}
bool IsClassicNodeName(const char* name) {
return NameContains(name, "Classic Controller");
}
// HID device behind a kernel evdev node; the remote and its extension nodes share it.
std::string KernelParent(SDL_JoystickID id) {
static std::unordered_map<SDL_JoystickID, std::string> s_parents;
if (const auto it = s_parents.find(id); it != s_parents.end()) {
return it->second;
}
const char* path = SDL_GetGamepadPathForID(id);
if (path == nullptr) {
return {};
}
std::error_code ec;
const auto parent = std::filesystem::canonical(
std::filesystem::path("/sys/class/input") / std::filesystem::path(path).filename() / "device" / "device", ec);
if (ec) {
return {};
}
return s_parents.emplace(id, parent.string()).first->second;
}
// The kernel node on the same remote as `gamepad`: its Classic Controller node
// when `wantClassic`, else its remote node.
SDL_Gamepad* KernelSibling(SDL_Gamepad* gamepad, bool wantClassic) {
const SDL_JoystickID self = SDL_GetGamepadID(gamepad);
const std::string parent = KernelParent(self);
if (parent.empty()) {
return nullptr;
}
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (ids == nullptr) {
return nullptr;
}
SDL_Gamepad* sibling = nullptr;
for (int i = 0; i < count && sibling == nullptr; ++i) {
if (ids[i] != self && IsKernelWiiNode(ids[i]) &&
IsClassicNodeName(SDL_GetGamepadNameForID(ids[i])) == wantClassic && KernelParent(ids[i]) == parent) {
sibling = SDL_GetGamepadFromID(ids[i]);
}
}
SDL_free(ids);
return sibling;
}
// The kernel exposes a remote and its Classic Controller as separate gamepads.
// The remote's port reads both, as on a Wii; the Classic node's own port stays empty.
Kind KindForGamepad(SDL_Gamepad* gamepad) {
if (gamepad == nullptr) {
return Kind::NotWii;
}
const char* name = SDL_GetGamepadName(gamepad);
const Kind kind = KindForName(name);
if ((kind != Kind::Remote && kind != Kind::RemoteWithClassic) || !IsKernelWiiNode(SDL_GetGamepadID(gamepad))) {
return kind;
}
if (IsClassicNodeName(name)) {
return KernelSibling(gamepad, false) != nullptr ? Kind::NotWii : kind;
}
return KernelSibling(gamepad, true) != nullptr ? Kind::RemoteWithClassic : kind;
}
// A kernel Classic Controller node whose remote node carries it.
bool IsCarriedKernelNode(SDL_Gamepad* gamepad) {
return gamepad != nullptr && IsKernelWiiNode(SDL_GetGamepadID(gamepad)) &&
IsClassicNodeName(SDL_GetGamepadName(gamepad)) && KernelSibling(gamepad, false) != nullptr;
}
// Logs once when the kernel driver serves a Wii controller SDL's driver could have had.
void WarnIfKernelDriverServing() {
if (g_hidrawWarned || !g_wiiDriverEnabled) {
return;
}
const uint64_t now = SDL_GetTicks();
static uint64_t s_nextCheckMs = 0;
if (now < s_nextCheckMs) {
return;
}
s_nextCheckMs = now + 2000;
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (ids == nullptr) {
return;
}
for (int i = 0; i < count && !g_hidrawWarned; ++i) {
const Uint16 product = SDL_GetGamepadProductForID(ids[i]);
if ((product == 0x0306 || product == 0x0330) && IsKernelWiiNode(ids[i])) {
g_hidrawWarned = true;
RT_LOG(RT_TAG_CONFIG) << "A Wii controller is served by the kernel hid-wiimote driver. For full Wii "
<< "Remote support see the Linux udev rule in the README." << std::endl;
}
}
SDL_free(ids);
}
// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's
// extension/status messages) into console.log, minus the periodic chatter.
// A sub-warning message is written once: SDL repeats the same line on every
@@ -464,6 +663,7 @@ void Poll() {
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
(void)EffectiveKind(port);
}
WarnIfKernelDriverServing();
if (!g_wiiDriverEnabled) {
return;
}
@@ -518,11 +718,13 @@ uint32_t ScanCount() {
return g_scanCount;
}
// Maps the gamepad name SDL's Wii driver reports to a Kind.
// Maps the gamepad name SDL reports to a Kind: "Nintendo Wii Remote",
// "... with Nunchuk", "... with Classic Controller" and "Nintendo Wii U Pro
// Controller" from SDL's Wii driver; the Linux kernel's "Nintendo Wii Remote
// Classic Controller" is read like the Classic Controller.
Kind KindForName(const char* name) {
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
// Controller" and "Nintendo Wii U Pro Controller".
// The kernel's Pro Controller is a plain gamepad with a built-in SDL mapping.
if (NameContains(name, "Wii Remote Pro Controller")) return Kind::NotWii;
if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro;
if (!NameContains(name, "Wii Remote")) return Kind::NotWii;
if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk;
@@ -534,8 +736,7 @@ Kind KindForName(const char* name) {
Kind KindForPort(uint32_t port) {
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
if (gamepad == nullptr) return Kind::NotWii;
return KindForName(SDL_GetGamepadName(gamepad));
return KindForGamepad(gamepad);
}
// Human-readable name of a Kind for the settings overlay.
@@ -561,7 +762,7 @@ Kind EffectiveKind(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
PortMemory& memory = g_ports[chan];
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const Kind live = KindForGamepad(gamepad);
const uint64_t now = SDL_GetTicks();
if (live != Kind::NotWii) {
memory.lastKind = live;
@@ -587,7 +788,8 @@ bool IsRemoteChannel(uint32_t chan) {
// Marks KPAD-served ports as "no controller" in the GameCube pad statuses.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count) {
for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) {
if (IsRemoteChannel(port)) {
if (IsRemoteChannel(port) ||
IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast<int>(port)))) {
statuses[port] = {};
statuses[port].err = PAD_ERR_NO_CONTROLLER;
}
@@ -602,24 +804,47 @@ void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) {
sample.hasClassic = kind == Kind::RemoteWithClassic;
}
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
// normalises every Classic Controller stick, whatever the report's resolution,
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
// WPAD.h; wut's WPADStatusClassic documents the same range).
// The patched SDL Wii driver posts Classic Controller sticks as WPAD's own
// value times 64 (aurora-main/cmake/AuroraSDL3Patches.cmake), so WPADCLStatus
// is the axis divided back: signed, 0 at the centre, -512..511, +y up.
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
const long value = invert ? -static_cast<long>(axis) : axis;
return static_cast<int16_t>(std::clamp(std::lround(value / 64.0), -512L, 511L));
}
// The kernel driver's evdev axes span SDL's full range; scaled to WPAD's.
int16_t KernelClassicStickRaw(Sint16 axis, bool invert) {
float value = static_cast<float>(axis) / 32767.0f;
if (invert) value = -value;
value = std::clamp(value, -1.0f, 1.0f);
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
}
// KPAD's default Classic Controller stick limits; the game uses the cross clamp.
constexpr float kKpadStickMin = 60.0f;
constexpr float kKpadStickMax = 308.0f;
// KPAD cross clamp: per-axis dead zone and scale, then the unit circle.
void ClassicStickFloat(const int16_t raw[2], float out[2]) {
for (int i = 0; i < 2; ++i) {
const float magnitude = std::fabs(static_cast<float>(raw[i]));
const float scaled = std::clamp((magnitude - kKpadStickMin) / (kKpadStickMax - kKpadStickMin), 0.0f, 1.0f);
out[i] = raw[i] < 0 ? -scaled : scaled;
}
const float length = std::sqrt(out[0] * out[0] + out[1] * out[1]);
if (length > 1.0f) {
out[0] /= length;
out[1] /= length;
}
}
// Samples buttons, accelerometers and the extension of the remote on a port.
bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
if (chan >= PAD_MAX_CONTROLLERS) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const Kind kind = KindForGamepad(gamepad);
if (!IsKpadKind(kind)) {
const Kind remembered = EffectiveKind(chan);
if (!IsKpadKind(remembered)) {
@@ -635,26 +860,48 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
EnsureSensors(gamepad, chan);
sample = {};
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
// The kernel's evdev nodes have regular SDL mappings, so they are read as gamepads.
const bool kernel = IsKernelWiiNode(SDL_GetGamepadID(gamepad));
const bool classicNode = kernel && IsClassicNodeName(SDL_GetGamepadName(gamepad));
if (kernel) {
const auto mapped = [&](SDL_GamepadButton button, uint32_t bit) {
if (SDL_GetGamepadButton(gamepad, button)) sample.hold |= bit;
};
if (!classicNode) {
mapped(SDL_GAMEPAD_BUTTON_EAST, kWpadA);
mapped(SDL_GAMEPAD_BUTTON_SOUTH, kWpadB);
mapped(SDL_GAMEPAD_BUTTON_WEST, kWpadOne);
mapped(SDL_GAMEPAD_BUTTON_NORTH, kWpadTwo);
mapped(SDL_GAMEPAD_BUTTON_START, kWpadPlus);
mapped(SDL_GAMEPAD_BUTTON_BACK, kWpadMinus);
mapped(SDL_GAMEPAD_BUTTON_GUIDE, kWpadHome);
mapped(SDL_GAMEPAD_BUTTON_DPAD_UP, kWpadUp);
mapped(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kWpadDown);
mapped(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kWpadLeft);
mapped(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kWpadRight);
}
} else {
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
}
float rawG[3] = {};
const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG);
for (float& v : rawG) v /= kStandardGravity;
LastAcc& last = g_lastAcc[chan];
const bool accepted = ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL, sample.acc);
const bool accepted = ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL, sample.acc);
if (accepted) {
last.valid = true;
for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i];
@@ -674,8 +921,17 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
// Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never
// through SDL's gamepad mapping, which expects a hat), ZL/ZR as the
// trigger axes.
const auto cl = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.clHold |= bit;
SDL_Gamepad* classic = gamepad;
if (kernel && !classicNode) {
classic = KernelSibling(gamepad, true);
if (classic == nullptr) {
classic = gamepad;
}
}
const auto cl = [&](SDL_GamepadButton button, uint32_t bit) {
if (kernel ? SDL_GetGamepadButton(classic, button) : SDL_GetJoystickButton(joystick, button)) {
sample.clHold |= bit;
}
};
cl(SDL_GAMEPAD_BUTTON_EAST, kClA);
cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB);
@@ -690,20 +946,29 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown);
cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft);
cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight);
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTY);
if (kernel) {
sample.clLStickRaw[0] = KernelClassicStickRaw(lx, false);
sample.clLStickRaw[1] = KernelClassicStickRaw(ly, true);
sample.clRStickRaw[0] = KernelClassicStickRaw(rx, false);
sample.clRStickRaw[1] = KernelClassicStickRaw(ry, true);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
} else {
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
ClassicStickFloat(sample.clLStickRaw, sample.clLStick);
ClassicStickFloat(sample.clRStickRaw, sample.clRStick);
}
// Only the full-press click of L/R reaches SDL; report it as a full pull.
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
@@ -718,7 +983,7 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
sample.stick[1] = -static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f;
for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f);
LastAcc& lastNunchuk = g_lastNunchukAcc[chan];
if (ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
if (ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
lastNunchuk.valid = true;
for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i];
} else {
@@ -738,7 +1003,7 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
return false;
}
EnsureSensors(gamepad, chan);
if (!ReadAccelG(gamepad, SDL_SENSOR_ACCEL, sdlG)) {
if (!ReadAccelG(gamepad, chan, SDL_SENSOR_ACCEL, sdlG)) {
return false;
}
AccelGToKpad(sdlG, kpadAcc);
@@ -747,28 +1012,47 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
// Begins collecting rest samples from the remote on `chan`.
void StartAccelCalibration(uint32_t chan) {
if (!IsRemoteChannel(chan)) {
FinishAccelCalibration("No Wii Remote on this port.");
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
FinishAccelCalibration(chan, "No Wii Remote on this port.");
return;
}
g_calibration = {};
g_calibration.active = true;
g_calibration.chan = chan;
g_calibration.instance = SDL_GetGamepadID(gamepad);
g_calibrationMessage[0] = '\0';
}
// Drops the stored correction and goes back to SDL's raw reading.
void ClearAccelCalibration() {
g_calibration.active = false;
g_accelOffset = {};
g_accelOffsetLoaded = true;
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
// Zeroes the stored correction of the remote on `chan`.
void ClearAccelCalibration(uint32_t chan) {
if (g_calibration.chan == chan) {
g_calibration.active = false;
}
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
SetCalibrationMessage(chan, "No Wii Remote on this port.");
return;
}
RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
calibration.offset = {};
RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, {0.0, 0.0, 0.0});
SetCalibrationMessage(chan, "Calibration cleared.");
}
// True while a calibration run is collecting samples.
bool IsAccelCalibrating() {
return g_calibration.active;
// Correction in effect for the remote on `chan`, for the overlay.
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan) {
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
return std::nullopt;
}
const RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
return std::array<double, 3>{calibration.offset[0], calibration.offset[1], calibration.offset[2]};
}
// True while a calibration run is collecting samples from `chan`.
bool IsAccelCalibrating(uint32_t chan) {
return g_calibration.active && g_calibration.chan == chan;
}
// Share of the calibration samples collected so far, 0..1; 0 when idle.
@@ -776,9 +1060,14 @@ float AccelCalibrationProgress() {
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
}
// Outcome of the last calibration run for the overlay, or nullptr before any.
const char* AccelCalibrationMessage() {
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
// Outcome of the last calibration run on `chan` for the overlay, or nullptr.
const char* AccelCalibrationMessage(uint32_t chan) {
if (g_calibrationMessage[0] == '\0' || g_calibrationMessageChan != chan) {
return nullptr;
}
SDL_Gamepad* gamepad = RemoteGamepad(chan);
const SDL_JoystickID remote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
return remote == g_calibrationMessageRemote ? g_calibrationMessage : nullptr;
}
} // namespace WiiRemoteInput
+58
View File
@@ -0,0 +1,58 @@
#include "isa/ppc_isa_float.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
namespace {
bool SameBits(double lhs, double rhs)
{
return PpcBitCastToU64Inline(lhs) == PpcBitCastToU64Inline(rhs);
}
bool CheckPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
bool subtract, const char* operation)
{
const float expected0 = subtract ? std::fma(a0, c0, -b0) : std::fma(a0, c0, b0);
const float expected1 = subtract ? std::fma(a1, c1, -b1) : std::fma(a1, c1, b1);
const double expected = PpcPackPairedInline(expected0, expected1);
if (SameBits(actual, expected)) {
return true;
}
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
return false;
}
bool CheckNegatedPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
bool subtract, const char* operation)
{
const float expected0 = -std::fma(a0, c0, subtract ? -b0 : b0);
const float expected1 = -std::fma(a1, c1, subtract ? -b1 : b1);
const double expected = PpcPackPairedInline(expected0, expected1);
if (SameBits(actual, expected)) {
return true;
}
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
return false;
}
} // namespace
int main()
{
const double a = PpcPackPairedInline(1.000000119f, -123.25f);
const double c = PpcPackPairedInline(33554431.0f, 0.0625f);
const double b = PpcPackPairedInline(-33554430.0f, 7.75f);
bool ok = true;
ok &= CheckPair(PPC_PsMaddNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_madd");
ok &= CheckPair(PPC_PsMsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_msub");
ok &= CheckNegatedPair(PPC_PsNmaddInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_nmadd");
ok &= CheckNegatedPair(PPC_PsNmsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_nmsub");
return ok ? 0 : 1;
}
+64
View File
@@ -1,7 +1,9 @@
#include "runtime_config.h"
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
namespace {
bool Require(bool value, const char* message) {
@@ -11,6 +13,24 @@ bool Require(bool value, const char* message) {
}
return true;
}
std::optional<toml::ordered_value> Update(const std::string& original, const char* section, const char* key,
const char* value, std::string* text = nullptr) {
const auto updated = RuntimeConfigFile::UpdateSettingText(original, section, key, value);
if (!updated) return std::nullopt;
if (text) *text = *updated;
return toml::parse_str<toml::ordered_type_config>(*updated);
}
bool SetsFps(const std::string& original, const char* message) {
const auto document = Update(original, "video", "show_fps", "true");
return Require(document && document->at("video").at("show_fps").as_boolean() &&
document->at("video").as_table().size() == 1,
message);
}
bool SetsOffset(const std::string& original, const char* message) {
const auto document = Update(original, "controller.wii_accel_offsets", "x", "0.5");
return Require(document && document->at("controller").at("wii_accel_offsets").at("x").as_floating() == 0.5,
message);
}
} // namespace
int main() {
@@ -31,5 +51,49 @@ int main() {
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
}
if (!SetsFps("[video]\nshow_fps = false\n", "bare key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show_fps\" = false\n", "basic-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n'show_fps'=false\n", "literal-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show\\u005Ffps\" = false\n", "escaped key was not replaced")) return 1;
if (!SetsFps("[\"video\"]\n show_fps = false\n", "quoted table header was not matched")) return 1;
if (!SetsFps("[video]\nshow_fps = [\n 1,\n]\n", "multi-line value was not replaced")) return 1;
if (!SetsFps("[video]\n", "missing key was not created")) return 1;
if (!SetsFps("", "missing table was not created")) return 1;
{
std::string text;
const auto document = Update("# top\nfoo = 1\n\n[video]\nzzz = 2 # keep\nshow_fps = false # note\naaa = 3\n\n"
"[audio]\nshow_fps = 1\n",
"video", "show_fps", "true", &text);
if (!Require(document && document->at("foo").as_integer() == 1 &&
document->at("audio").at("show_fps").as_integer() == 1 &&
document->at("video").at("zzz").as_integer() == 2 &&
document->at("video").at("aaa").as_integer() == 3 &&
document->at("video").at("show_fps").as_boolean(),
"unrelated settings were not preserved")) return 1;
const size_t zzz = text.find("zzz"), fps = text.find("show_fps"), aaa = text.find("aaa");
if (!Require(text.find("# top") != std::string::npos && text.find("# keep") != std::string::npos &&
text.find("# note") != std::string::npos && zzz < fps && fps < aaa,
"comments or key order were not preserved")) return 1;
}
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video]\nshow_fps = 1\nshow_fps = 2\n", "video", "show_fps", "true"),
"a document with a duplicate key was rewritten")) return 1;
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video\nshow_fps = 1\n", "video", "show_fps", "true"),
"an unparseable document was rewritten")) return 1;
if (!SetsOffset("[controller]\nrumble = true\n\n[controller.wii_accel_offsets]\nx = 0.1\ny = 2\n",
"key in an existing nested table was not replaced")) return 1;
if (!SetsOffset("[controller.wii_accel_offsets]\ny = 2\n", "key was not added to a nested table")) return 1;
if (!SetsOffset("[controller]\nrumble = true\n", "missing nested table was not created")) return 1;
if (!SetsOffset("[controller.\"wii_accel_offsets\"]\nx = 0.1\n", "quoted nested header was not matched")) return 1;
if (!SetsOffset("[controller]\nwii_accel_offsets.y = 2\n", "dotted-key table was not extended")) return 1;
if (!SetsOffset("controller.wii_accel_offsets.y = 2\n", "root-level dotted table was not extended")) return 1;
if (!Require(!RuntimeConfigFile::UpdateSettingText("controller = \"custom\"\n", "controller.wii_accel_offsets",
"x", "0.5"),
"a scalar was replaced by a table")) return 1;
{
const auto document = Update("[video.advanced]\nfoo = 1\n", "video", "show_fps", "true");
if (!Require(document && document->at("video").at("show_fps").as_boolean() &&
document->at("video").at("advanced").at("foo").as_integer() == 1,
"key was not added to an implicit table")) return 1;
}
return 0;
}
+1
View File
@@ -66,6 +66,7 @@ Relative paths resolve from `workspace_root`, which itself resolves from the man
- `generate-data-init --project path`
- `emit-base-manifest --project path`
- `emit-build-shards --project path`
- `emit-native-index --project path --out path [--native-source-dir path]` - writes the serialized runtime native index
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
Any command prints its own option list with `--help`.

Some files were not shown because too many files have changed in this diff Show More