Compare commits

..

2 Commits

Author SHA1 Message Date
patchzyy 42d43ed407 aurora submodule 2026-10-02 11:47:41 +02:00
patchzyy 4b115d73af aurora as submodule 2026-10-02 11:39:35 +02:00
240 changed files with 5597 additions and 77730 deletions
-6
View File
@@ -4,9 +4,3 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
# Region tables
projects/mkwii-ntsc-*/MAP.txt linguist-generated=true
projects/mkwii-ntsc-*/region_port.json linguist-generated=true
projects/mkwii-ntsc-j/data_addresses.txt linguist-generated=true
runtime/include/guest_abi/guest_versions.inc linguist-generated=true
-66
View File
@@ -46,69 +46,3 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
platform_tests:
name: Host platform (${{ matrix.runner }})
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, macos-14, macos-15-intel]
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure without graphics dependencies or game assets
run: >-
cmake -S runtime -B build-platform -G Ninja
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
- name: Build and run host-platform tests
run: |
cmake --build build-platform --parallel 3
ctest --test-dir build-platform --output-on-failure
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
submodules: recursive
- name: Test macOS app dependency packaging
shell: bash
run: bash Launcher/macos/test-publish-app.command
- name: Configure native runtime
shell: bash
run: |
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
mkw_runtime_config_tests \
mkw_nand_save_tests \
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_bluetooth_controller_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
mkw_macos_guest_flat_memory_tests \
mkw_macos_external_audio_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+6 -137
View File
@@ -1,8 +1,8 @@
name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# the same scripts a maintainer runs by hand today to
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI.
@@ -11,11 +11,6 @@ on:
tags:
- '*'
workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions:
contents: read
@@ -97,140 +92,14 @@ jobs:
if-no-files-found: error
archive: false
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
submodules: recursive
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
nodtool_arm64_asset=nodtool-macos-arm64
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
nodtool_x86_64_asset=nodtool-macos-x86_64
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
-o Launcher/artifacts/macos/nodtool-arm64
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
-o Launcher/artifacts/macos/nodtool-x86_64
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-arm64
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-x86_64
- name: Download pinned universal Ninja
shell: bash
run: |
ninja_version=1.13.2
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version="$PACKAGE_VERSION"
if [[ -z "$package_version" ]]; then
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
package_version="${TAG_VERSION#v}"
elif [[ -f "Launcher/Directory.Build.props" ]]; then
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
fi
fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
${package_version:+--version "$package_version"}
- name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg || true
if pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'; then
echo "::error::Setup.pkg contains a forbidden payload"
exit 1
fi
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
for arch in arm64 x86_64; do
for tool in nodtool Translator.Cli ninja; do
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
done
done
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
bash "$resources/setup.command" --help
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
# produced by setup hosts that report that same version.
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -275,7 +144,7 @@ jobs:
set -euo pipefail
ls -lR artifacts
assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
-14
View File
@@ -46,20 +46,6 @@ 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 -3
View File
@@ -1,5 +1,4 @@
/generated/
/runtime/native-index.json
/Assets/
/assets/
/PulsarPacks/
@@ -66,8 +65,7 @@ project.lock.json
/.vs/
/.vscode/
/.idea/
/tools/*
__pycache__/
/tools/
/TEMP/
/tmp/
/test_output/
+1 -2
View File
@@ -1,4 +1,3 @@
[submodule "aurora-main"]
path = aurora-main
url = https://github.com/patchzyy/aurora-wiicompiled.git
branch = main
url = https://github.com/patchzyy/aurora.git
+14 -60
View File
@@ -23,23 +23,12 @@ $publish = Join-Path $workRoot 'publish'
$payloadRoot = Join-Path $workRoot 'payload'
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Windows\WiiCompiled.Setup.Windows.csproj'
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
# Every supported release is one projects/<name>/recomp.yml. Everything Mario-Kart-specific in the
# manifest below is read from those files rather than restated here, and Test-PinnedFacts.ps1
# checks the copies that cannot read them (the C++ entry address, the C# Retro-WFC table, the
# lists this script and the installed host both carry).
$releaseProjects = @(Get-ChildItem -LiteralPath (Join-Path $repoRoot 'projects') -Directory |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'recomp.yml') -PathType Leaf } |
Sort-Object Name)
if ($releaseProjects.Count -eq 0) { throw 'No translation project defines a supported release.' }
$supportedReleases = @(foreach ($releaseProject in $releaseProjects) {
$pins = Get-MkwProjectPins (Join-Path $releaseProject.FullName 'recomp.yml')
[ordered]@{ GameId = $pins.GameId; DolSha256 = $pins.DolSha256; RelSha256 = $pins.RelSha256 }
})
foreach ($key in @('GameId', 'DolSha256')) {
$duplicates = @($supportedReleases | Group-Object { $_[$key] } | Where-Object Count -gt 1)
if ($duplicates.Count -gt 0) { throw "Two translation projects pin the same $($key): $($duplicates[0].Name)." }
}
# Everything Mario-Kart-specific in the manifest below is read from the project file rather than
# restated here, and Test-PinnedFacts.ps1 checks the copies that cannot read it (the C++ entry
# address, the C# endpoint constant, the lists this script and the installed host both carry).
$pins = Get-MkwProjectPins $projectFile
# The audit reports by throwing, which aborts this run; nothing native has been invoked yet, so
# there is no exit code to inspect.
& (Join-Path $PSScriptRoot 'Test-PinnedFacts.ps1') -RepositoryRoot $repoRoot
@@ -132,20 +121,6 @@ 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 }
@@ -163,9 +138,6 @@ 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
@@ -173,13 +145,11 @@ 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'))) -and
($null -ne $recordedRuntime) -and
($recordedRuntime.Value -eq (Get-RuntimeSourceFingerprint (Join-Path $repoRoot 'runtime') $dependencySources))
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party')))
}
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
Write-Host '[0/6] Building the precompiled aurora, third-party and runtime package...'
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
@@ -254,31 +224,13 @@ if (Test-Path -LiteralPath $runtimeDeveloperBuild) {
}
$auroraExtern = Join-Path $workspace 'aurora-main\extern'
Get-ChildItem -LiteralPath $auroraExtern -Directory -Force | Remove-Item -Recurse -Force
foreach ($releaseProject in $releaseProjects) {
Copy-Directory $releaseProject.FullName (Join-Path $workspace "projects\$($releaseProject.Name)")
}
Copy-Directory (Join-Path $repoRoot 'projects\mkwii') (Join-Path $workspace 'projects\mkwii')
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'LocalBuild.ps1') -Destination (Join-Path $workspace 'LocalBuild.ps1')
# LocalBuild.ps1 dot-sources the canonical configure flags from this sibling.
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -Destination (Join-Path $workspace 'NativeBuildFlags.ps1')
[IO.Directory]::CreateDirectory((Join-Path $workspace 'Dependencies')) | Out-Null
foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependencySources $name) (Join-Path $workspace "Dependencies\$name") }
# The translation identity covers the runtime's native registrations through this index (see
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --out $nativeIndex
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
Assert-File $nativeIndex 'Runtime native index'
# A staged runtime that fingerprints differently from the shipped archive would make every
# installation silently compile the runtime from source.
$stagedRuntimeFingerprint = Get-RuntimeSourceFingerprint (Join-Path $workspace 'runtime') (Join-Path $workspace 'Dependencies')
$packagedRuntimeFingerprint = (Get-Content -LiteralPath (Join-Path $workspace 'Dependencies\native_prebuilt\provenance.json') -Raw |
ConvertFrom-Json).RuntimeSourceFingerprint
if ($stagedRuntimeFingerprint -ne $packagedRuntimeFingerprint) {
throw "The staged runtime ($stagedRuntimeFingerprint) does not match the precompiled runtime archive ($packagedRuntimeFingerprint)."
}
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
@@ -339,20 +291,22 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$productVersion = ((& $setupHost --version) -join '').Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($productVersion)) { throw 'The setup host did not report its version.' }
# Schema 3 replaced schema 2's single Expected* release fields with SupportedReleases.
$manifest = [ordered]@{
SchemaVersion = 3
SchemaVersion = 2
ProductVersion = $productVersion
SupportedReleases = $supportedReleases
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
ToolkitReleaseTag = if ($ToolkitReleaseTag) { $ToolkitReleaseTag } else { '' }
BuildModel = 'local-translation-and-compilation'
RetroWfcPayloadUri = $pins.RetroWfcPayloadUri
ToolkitFingerprint = $identities.ToolkitFingerprint
ToolkitPackageFingerprint = $identities.ToolkitPackageFingerprint
RuntimeAssetsFingerprint = $identities.RuntimeAssetsFingerprint
TranslationFingerprint = $identities.TranslationFingerprint
NativeToolchainFingerprint = $identities.NativeToolchainFingerprint
}
$manifest | ConvertTo-Json -Depth 4 | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.35</Version>
<Version>0.2.33</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+13 -18
View File
@@ -166,6 +166,7 @@ $toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
$project = Join-Path $Workspace 'projects\mkwii\recomp.yml'
$assets = Join-Path $Workspace 'Assets'
$generated = Join-Path $Workspace 'generated'
$functions = Join-Path $generated 'functions'
@@ -182,18 +183,14 @@ $retroRoot = if ([string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory)) {
foreach ($required in @(
@($translator, 'Bundled translator'), @($cmake, 'Bundled CMake'), @($ninja, 'Bundled Ninja'),
@($cc, 'Bundled C compiler'), @($cxx, 'Bundled C++ compiler'), @($windres, 'Bundled resource compiler'),
@($cc, 'Bundled C compiler'), @($cxx, 'Bundled C++ compiler'), @($windres, 'Bundled resource compiler'), @($project, 'Translation project'),
@((Join-Path $assets 'main.dol'), 'Extracted main.dol'), @((Join-Path $assets 'StaticR.rel'), 'Extracted StaticR.rel')
)) { Assert-File $required[0] $required[1] }
# Everything Mario-Kart-specific this script needs is read from the project file rather than
# restated here: the translation entry point below, and (through the retro-rewind profile the
# translator loads itself) the module guest/link bases the mod translation uses. The project is
# the one pinned to the extracted main.dol, which decides the region.
$selectedProject = Resolve-MkwProjectForAssets $Workspace $assets
$project = $selectedProject.Path
$pins = $selectedProject.Pins
$retroWfcPayloadFile = "binary\payload.$(($pins.RetroWfcPayloadUri -split 'g=')[-1]).bin"
# translator loads itself) the module guest/link bases the mod translation uses.
$pins = Get-MkwProjectPins $project
# Three independent knobs: $translatorThreads (the translator's own worker threads; peak memory is
# one heap that barely moves with thread count, so clamping it only slows translation), $translatedJobs
@@ -246,8 +243,7 @@ try {
# outputs plus --prune-stale mean Ninja only recompiles shards whose bytes actually moved.
$reuseBase = $false
if (-not $ForceCleanBuild -and -not [string]::IsNullOrWhiteSpace($TranslationFingerprint) -and
(Get-RecordedFingerprint $translationProvenance 'TranslationFingerprint') -eq $TranslationFingerprint -and
(Get-RecordedFingerprint $translationProvenance 'DolSha256') -eq $pins.DolSha256) {
(Get-RecordedFingerprint $translationProvenance 'TranslationFingerprint') -eq $TranslationFingerprint) {
$artifacts = @($baseMetadata, $baseManifest, (Join-Path $generated 'base_translation_sources.bin'),
(Join-Path $generated 'base_translation_mod_awareness.json'))
$reuseBase = -not ($artifacts | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) })
@@ -316,11 +312,11 @@ try {
Invoke-Checked $translator @(
'emit-base-manifest', '--project', $project, '--out', $baseManifestDir,
'--functions-dir', $functions, '--translation-output-metadata', $baseMetadata, '--region', $pins.Region
'--functions-dir', $functions, '--translation-output-metadata', $baseMetadata, '--region', 'P'
) 'Creating the local base translation manifest' -StepId 'emit-base-manifest'
if (-not [string]::IsNullOrWhiteSpace($TranslationFingerprint)) {
[ordered]@{ SchemaVersion = 1; TranslationFingerprint = $TranslationFingerprint; DolSha256 = $pins.DolSha256 } |
[ordered]@{ SchemaVersion = 1; TranslationFingerprint = $TranslationFingerprint } |
ConvertTo-Json | Set-Content -LiteralPath $translationProvenance -Encoding UTF8
}
}
@@ -333,14 +329,14 @@ try {
'translate-mod', '--project', $project, '--profile', 'retro-rewind',
'--base-manifest', $baseManifest, '--base-translation-output-metadata', $baseMetadata,
'--code-pul', $codePul, '--mod-root', $retroRoot, '--mod-name', 'Retro Rewind',
'--region', $pins.Region, '--out', $retroOut, '--prefer-cached-inputs', '--emit-cpp',
'--region', 'P', '--out', $retroOut, '--prefer-cached-inputs', '--emit-cpp',
'--threads', $translatorThreads
)
if ($SkipRetroWfcPayload) {
$translateModArguments += '--skip-retro-wfc'
} elseif (-not [string]::IsNullOrWhiteSpace($RetroWfcOfflineDirectory)) {
$offlineRoot = [IO.Path]::GetFullPath($RetroWfcOfflineDirectory)
$offlinePayload = Join-Path $offlineRoot $retroWfcPayloadFile
$offlinePayload = Join-Path $offlineRoot 'binary\payload.RMCPD00.bin'
Assert-File $offlinePayload 'Offline Retro-WFC shared payload'
$translateModArguments += @('--retro-wfc-payload', $offlinePayload)
} else {
@@ -427,14 +423,13 @@ try {
$exe = Join-Path $build "$Target.exe"
Assert-File $exe 'Locally compiled game executable'
Copy-Item -LiteralPath $exe -Destination (Join-Path $Destination "$Target.exe")
foreach ($name in @('dxcompiler.dll','dxil.dll','libc++.dll','libpng16.dll','libunwind.dll','libz.dll','libzlib1.dll','SDL3.dll','sqlite3.dll','webgpu_dawn.dll','z.dll','noshaders.zip','dsp_coef.bin','initial_pipeline_cache.db')) {
foreach ($name in @('dxcompiler.dll','dxil.dll','libc++.dll','libpng16.dll','libunwind.dll','libz.dll','SDL3.dll','sqlite3.dll','webgpu_dawn.dll','z.dll','noshaders.zip','dsp_coef.bin','initial_pipeline_cache.db')) {
$file = Join-Path $build $name
if (Test-Path -LiteralPath $file -PathType Leaf) { Copy-Item -LiteralPath $file -Destination $Destination }
}
$redist = Join-Path $Toolkit 'Redist'
if (Test-Path -LiteralPath $redist -PathType Container) {
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
Copy-Item -Destination $Destination -Force
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
}
foreach ($name in @('config','wii_bootstrap')) {
$directory = Join-Path $build $name
@@ -451,8 +446,8 @@ try {
RelSha256 = $relSha
CodePulSha256 = if ($isRetro) { Get-MkwFileSha256 (Join-Path $retroRoot 'Binaries\Code.pul') } else { $null }
RetroWfcPayloadMode = if (-not $isRetro) { $null } elseif ($SkipRetroWfcPayload) { 'skipped' } else { $RetroWfcPayloadOrigin }
RetroWfcPayloadSha256 = if ($isRetro -and -not $SkipRetroWfcPayload) { Get-MkwFileSha256 (Join-Path $RetroWfcOfflineDirectory $retroWfcPayloadFile) } else { $null }
RetroWfcPayloadLength = if ($isRetro -and -not $SkipRetroWfcPayload) { (Get-Item -LiteralPath (Join-Path $RetroWfcOfflineDirectory $retroWfcPayloadFile)).Length } else { $null }
RetroWfcPayloadSha256 = if ($isRetro -and -not $SkipRetroWfcPayload) { Get-MkwFileSha256 (Join-Path $RetroWfcOfflineDirectory 'binary\payload.RMCPD00.bin') } else { $null }
RetroWfcPayloadLength = if ($isRetro -and -not $SkipRetroWfcPayload) { (Get-Item -LiteralPath (Join-Path $RetroWfcOfflineDirectory 'binary\payload.RMCPD00.bin')).Length } else { $null }
Compiler = "llvm-mingw clang-22 sha256:$compilerSha"
}
$provenance | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Destination 'local-build.json') -Encoding UTF8
+14 -48
View File
@@ -93,7 +93,7 @@ function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile)
function Get-MkwProjectPins([string]$ProjectFile) {
<#
The Mario Kart Wii facts pinned by a projects/*/recomp.yml (game identity, clean input
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
digests, translation entry point, Retro-WFC endpoint), read from one place instead of being
restated; Test-PinnedFacts.ps1 checks the copies that can't read YAML (C++ header, C#
constants) against these values. Parsing is literal, not a YAML dependency, since the file is
@@ -101,7 +101,7 @@ function Get-MkwProjectPins([string]$ProjectFile) {
#>
Assert-File $ProjectFile 'Translation project file'
$pins = [ordered]@{
GameId = ''; Region = ''; DolSha256 = ''; RelSha256 = ''; EntryPoint = ''; RetroWfcPayloadUri = ''
GameId = ''; DolSha256 = ''; RelSha256 = ''; EntryPoint = ''; RetroWfcPayloadUri = ''
}
$section = ''
$inputKey = ''
@@ -112,8 +112,6 @@ function Get-MkwProjectPins([string]$ProjectFile) {
if ($section -eq 'inputs' -and $line -match '^\s{2}([A-Za-z0-9_]+):\s*$') { $inputKey = $Matches[1]; continue }
if ($section -eq 'project' -and $line -match '^\s*game_id:\s*(\S+)\s*$') {
$pins.GameId = $Matches[1]
} elseif ($section -eq 'project' -and $line -match '^\s*region:\s*(\S+)\s*$') {
$pins.Region = $Matches[1]
} elseif ($section -eq 'inputs' -and $line -match '^\s*sha256:\s*([0-9a-fA-F]{64})\s*$') {
if ($inputKey -eq 'dol') { $pins.DolSha256 = $Matches[1].ToLowerInvariant() }
elseif ($inputKey -eq 'rel') { $pins.RelSha256 = $Matches[1].ToLowerInvariant() }
@@ -132,30 +130,6 @@ function Get-MkwProjectPins([string]$ProjectFile) {
return $pins
}
function Resolve-MkwProjectForAssets([string]$Workspace, [string]$AssetsDirectory) {
<#
The translation project pinned to the main.dol in AssetsDirectory, after checking that
StaticR.rel and any extracted disc data belong to the same release.
#>
$dolSha = Get-MkwFileSha256 (Join-Path $AssetsDirectory 'main.dol')
foreach ($file in Get-ChildItem -LiteralPath (Join-Path $Workspace 'projects') -Filter 'recomp.yml' -Recurse -File) {
$pins = Get-MkwProjectPins $file.FullName
if ($pins.DolSha256 -ne $dolSha) { continue }
if ((Get-MkwFileSha256 (Join-Path $AssetsDirectory 'StaticR.rel')) -ne $pins.RelSha256) {
throw "StaticR.rel does not match the clean $($pins.GameId) release of main.dol."
}
$bootBin = Join-Path $AssetsDirectory 'DATA\sys\boot.bin'
if (Test-Path -LiteralPath $bootBin -PathType Leaf) {
$discId = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($bootBin), 0, 6)
if ($discId -ne $pins.GameId) {
throw "The extracted game data is $discId but main.dol is $($pins.GameId); extract the game again."
}
}
return [pscustomobject]@{ Path = $file.FullName; Pins = $pins }
}
throw "main.dol (sha256 $dolSha) is not a supported clean Mario Kart Wii release."
}
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
<#
@@ -171,23 +145,15 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
else { Write-Host "${LogPrefix}: $Description" }
$quotedArguments = @($Arguments | ForEach-Object {
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
# Backslashes directly before the closing quote would otherwise escape it.
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
'"' + $_ + '"'
})
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
} else {
# Tools report progress on stderr (CMake message()); only the exit code decides failure.
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousPreference
}
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
@@ -241,14 +207,13 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
so a workspace whose vendored tree moved on must not consume archives harvested from an older
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
Returns '' when absent.
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
#>
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
Where-Object { -not ($_.FullName.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') }) } |
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
try {
@@ -279,13 +244,14 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
(Join-Path $root 'extern') + '\',
(Join-Path $root 'build') + '\'
)
# Dot-paths (the submodule's .git pointer) are skipped by name, not by the hidden attribute:
# the payload zip drops that attribute, so the installed side would otherwise hash them.
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
# Git metadata identifies a checkout location, not a source revision's bytes.
# Exclude it explicitly, including a submodule's non-hidden .git file.
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
Where-Object {
$full = $_.FullName
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
$relative = $full.Substring($root.Length + 1).Replace('\', '/')
($relative -notmatch '(^|/)\.git(/|$)') -and
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
} |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
+20 -26
View File
@@ -21,8 +21,6 @@ $auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
$auroraVersions = Join-Path $repoRoot 'aurora-main\cmake\AuroraDependencyVersions.cmake'
$auroraImgui = Join-Path $repoRoot 'aurora-main\cmake\AuroraDearImguiProvider.cmake'
if (-not (Test-Path -LiteralPath $auroraCMake -PathType Leaf)) {
throw 'Aurora sources are missing. Run git submodule update --init --recursive from the repository root first.'
}
@@ -32,10 +30,10 @@ if (-not (Test-Path -LiteralPath $auroraCMake -PathType Leaf)) {
# FetchContent names, not the upstream project names.
$packages = @(
[pscustomobject]@{
Name = 'SDL'; File = 'SDL3-3.4.10.tar.gz'
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.10/SDL3-3.4.10.tar.gz')
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_SDL3_REF "refs/tags/release-3.4.10"' },
@{ File = $auroraSdl; Text = 'URL "https://github.com/libsdl-org/SDL/archive/${AURORA_SDL3_REF}.tar.gz"' })
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
},
[pscustomobject]@{
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
@@ -43,15 +41,15 @@ $packages = @(
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
},
[pscustomobject]@{
Name = 'dawn_prebuilt'; File = 'dawn-v20260807.225922-windows-amd64.tar.gz'
Uris = @('https://github.com/encounter/dawn/releases/download/v20260807.225922/dawn-windows-amd64.tar.gz')
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_DAWN_VERSION "v20260807.225922"' },
@{ File = $auroraDawn; Text = 'SHA256=42a7a295c87b2e8a36312e2f84a5823aac63325e8e1d271bc7ced6f83bfb075f' })
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
Uris = @('https://github.com/theofficialgman/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
@{ File = $auroraDawn; Text = 'SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
},
[pscustomobject]@{
Name = 'fmt'; File = 'fmt-12.1.0.tar.gz'
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz' })
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz' })
},
[pscustomobject]@{
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
@@ -63,12 +61,12 @@ $packages = @(
[pscustomobject]@{
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
Uris = @('https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz')
Pins = @(@{ File = $auroraImgui; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
},
[pscustomobject]@{
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
Uris = @('https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2')
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_LIBUSB_VERSION "1.0.30"' },
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_LIBUSB_VERSION "1.0.30"' },
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
},
[pscustomobject]@{
@@ -82,9 +80,9 @@ $packages = @(
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
},
[pscustomobject]@{
Name = 'tracy'; File = 'tracy-0.14.1.zip'
Uris = @('https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip' })
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
},
[pscustomobject]@{
Name = 'xxhash'; File = 'xxHash-0.8.4.tar.gz'
@@ -92,9 +90,9 @@ $packages = @(
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz' })
},
[pscustomobject]@{
Name = 'zlib'; File = 'zlib-ng-2.3.3.tar.gz'
Uris = @('https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz' })
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
},
[pscustomobject]@{
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
@@ -173,12 +171,8 @@ function Expand-Package([string]$Archive, [string]$Target) {
foreach ($package in $packages) {
Assert-Pinned $package
$target = Join-Path $Destination $package.Name
# Records which archive a tree came from, so a pin change replaces a stale tree.
$stamp = $target + '.source'
$current = (Test-Path -LiteralPath $stamp -PathType Leaf) -and ([IO.File]::ReadAllText($stamp) -eq $package.File)
if ($current -and (Test-Path -LiteralPath $target -PathType Container)) { continue }
if (Test-Path -LiteralPath $target -PathType Container) { continue }
Expand-Package (Get-Archive $package) $target
[IO.File]::WriteAllText($stamp, $package.File)
Write-Host "Prepared $($package.Name)"
}
+11 -34
View File
@@ -1,8 +1,7 @@
# 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 plus the
# game-independent runtime archive, and harvests the archives and a generated CMake description
# into launcher/artifacts/dependencies/native_prebuilt.
# 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.
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
@@ -88,29 +87,24 @@ 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', '--target', 'mkw_runtime',
'--parallel', "$Parallel") 'Building the aurora/third-party closure and the runtime' -LogPrefix 'MKWNP'
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
} finally {
$env:PATH = $oldPath
}
# ---------------------------------------------------------------------------
# Read the resolved command lines Ninja holds.
# Read the description CMake wrote, plus 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'
@@ -240,12 +234,6 @@ 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.
@@ -309,9 +297,6 @@ $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
@@ -348,11 +333,6 @@ $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) {
@@ -413,9 +393,6 @@ $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
+14 -58
View File
@@ -2,9 +2,8 @@
# 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 (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
# 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
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
#
@@ -192,23 +191,12 @@ 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
@@ -232,30 +220,20 @@ 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"
@@ -419,13 +397,6 @@ 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"
@@ -527,11 +498,6 @@ 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.
@@ -596,12 +562,6 @@ 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"
@@ -637,13 +597,12 @@ 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" \
"${runtime_archive_ref:+$meta_runtime_fingerprint}" "${runtime_archive_ref:+$(get_meta runtime_cpu_profile)}" <<'PY'
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" <<'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, runtime_fingerprint, runtime_cpu_profile) = sys.argv[1:]
harvested_count, mbedtls_fingerprint) = sys.argv[1:]
contents = []
for root, dirs, files in os.walk(output_dir):
@@ -685,9 +644,6 @@ 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")
+30 -37
View File
@@ -1,8 +1,7 @@
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy
# that owns it (each projects/*/recomp.yml). Scripts read pinned facts through Get-MkwProjectPins
# or build-cache.sh's mkw_project_pin, but some consumers can't read YAML (the C++ runtime header,
# the C# Retro-WFC table, and hand-written lists on both sides of the C#/PowerShell boundary), so
# those are checked here instead.
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
# consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on
# both sides of the C#/PowerShell boundary), so those are checked here instead.
[CmdletBinding()]
param([string]$RepositoryRoot)
@@ -47,36 +46,32 @@ function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedNa
if ($extra.Count -gt 0) { Add-Failure "$ActualName has entries $ExpectedName does not: $($extra -join ', ')." }
}
# Every supported release is one projects/<name>/recomp.yml, the same set Build-Installer.ps1 ships.
$releases = @(Get-ChildItem -LiteralPath (Join-Path $repoRoot 'projects') -Directory | Sort-Object Name |
ForEach-Object { Join-Path $_.FullName 'recomp.yml' } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
ForEach-Object { $pins = Get-MkwProjectPins $_; $pins.Project = $_; [pscustomobject]$pins })
if ($releases.Count -eq 0) { throw 'No projects/*/recomp.yml was found; update Test-PinnedFacts.ps1.' }
$pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml')
# --- The Retro-WFC endpoints: each recomp.yml owns its release's; the host derives the same URL
# --- from its game table so a redirected or rewritten endpoint cannot be fetched from.
# --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a
# --- redirected or rewritten endpoint cannot be fetched from. The literal lives in
# --- WiiCompiled.Setup.Common (shared with WiiCompiled.Setup.Linux) - InputValidation.cs only
# --- re-exports it as `= RetroWfcPayload.CurrentRetroWfcPayloadUri;`, no literal to capture there.
$retroWfcPayload = Read-SourceFile (Join-Path $common 'RetroWfcPayload.cs') 'RetroWfcPayload.cs'
$payloadGames = @{}
foreach ($entry in [regex]::Matches((Get-CapturedValue $retroWfcPayload `
'(?s)PayloadGames\s*=\s*new\([^)]*\)\s*\{(.*?)\};' 'The host Retro-WFC game table'),
'\["([^"]+)"\]\s*=\s*"([^"]+)"')) {
$payloadGames[$entry.Groups[1].Value] = $entry.Groups[2].Value
$hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
'The host Retro-WFC endpoint constant'
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
$uriTemplate = Get-CapturedValue $retroWfcPayload 'PayloadUri\(string gameId\)\s*=>\s*\$"([^"]+)"' `
'The host Retro-WFC endpoint template'
Compare-Set ([string[]]@($releases.GameId)) ([string[]]@($payloadGames.Keys)) `
'projects/*/recomp.yml' 'RetroWfcPayload.PayloadGames'
foreach ($release in $releases) {
if (-not $payloadGames.ContainsKey($release.GameId)) { continue }
$hostUri = $uriTemplate.Replace('{PayloadGame(gameId)}', $payloadGames[$release.GameId])
if ($hostUri -cne $release.RetroWfcPayloadUri) {
Add-Failure "RetroWfcPayload.PayloadUri($($release.GameId)) is '$hostUri' but $($release.Project) pins '$($release.RetroWfcPayloadUri)'."
}
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
'The macOS Retro-WFC endpoint'
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
foreach ($key in @('GameId', 'DolSha256')) {
foreach ($duplicate in @($releases | Group-Object $key | Where-Object Count -gt 1)) {
Add-Failure "Several projects pin the same $($key) $($duplicate.Name)."
}
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a
# --- manifest that predates the field, and that fallback decides which disc is accepted.
$models = Read-SourceFile (Join-Path $setup 'Models.cs') 'Models.cs'
$hostGameId = Get-CapturedValue $models 'ExpectedGameId\s*\{\s*get;\s*set;\s*\}\s*=\s*"([^"]+)"' `
'The host expected game id'
if ($hostGameId -cne $pins.GameId) {
Add-Failure "PayloadManifest.ExpectedGameId defaults to '$hostGameId' but recomp.yml pins '$($pins.GameId)'."
}
# --- The translation entry point: the translator is told it by LocalBuild.ps1 (which reads
@@ -84,10 +79,8 @@ foreach ($key in @('GameId', 'DolSha256')) {
$systemBridge = Read-SourceFile (Join-Path $repoRoot 'runtime\include\system_bridge.h') 'system_bridge.h'
$runtimeEntry = (Get-CapturedValue $systemBridge 'kDefaultEntryAddress\s*=\s*(0[xX][0-9a-fA-F]+)' `
'The runtime default entry address').ToLowerInvariant()
foreach ($release in $releases) {
if ($runtimeEntry -ne $release.EntryPoint) {
Add-Failure "system_bridge.h enters at $runtimeEntry but $($release.Project) translates from $($release.EntryPoint)."
}
if ($runtimeEntry -ne $pins.EntryPoint) {
Add-Failure "system_bridge.h enters at $runtimeEntry but recomp.yml translates from $($pins.EntryPoint)."
}
# --- The pinned dependency set: Build-Installer.ps1 stages it, the installed host requires it.
@@ -134,7 +127,7 @@ foreach ($mandatory in ([regex]::Matches($parameterBlock,
}
# --- The build steps the installer's progress bar acts on.
$installProgress = Read-SourceFile (Join-Path $common 'InstallProgress.cs') 'InstallProgress.cs'
$installProgress = Read-SourceFile (Join-Path $setup '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+'([^']+)'") |
@@ -144,6 +137,6 @@ Compare-Set $knownSteps $emittedSteps 'BuildStepIds' 'the steps LocalBuild.ps1 e
if ($failures.Count -gt 0) {
throw "Pinned facts disagree between their copies:`n $($failures -join "`n ")"
}
Write-Host ("Pinned-fact audit passed: games {0}, entry {1}; Retro-WFC endpoint, dependency, runtime-asset, " -f
($releases.GameId -join ', '), $runtimeEntry) -NoNewline
Write-Host ("Pinned-fact audit passed: game {0}, entry {1}, endpoint {2}; dependency, runtime-asset, " -f
$pins.GameId, $pins.EntryPoint, $pins.RetroWfcPayloadUri) -NoNewline
Write-Host 'build-step, and build-script parameter contracts all agree.'
+15 -7
View File
@@ -54,10 +54,15 @@ foreach ($relative in $sourceFiles | Sort-Object -Unique) {
}
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
# Guest functions the runtime calls directly resolve through the registry at run time, so the
# product links without any game code.
# Native HLE wrappers also call these eight guest symbols directly. Give each
# its own generated blr function so the real product can link without game code.
# Keep this list explicit: a new unresolved guest dependency must fail the test.
[uint32]$entry = 0x80001000L
$textSize = 0x10
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
)
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
$dataOffset = 0x100 + $textSize
$dol = [byte[]]::new($dataOffset + 4)
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
@@ -68,7 +73,7 @@ function Write-BigEndian32([int]$Offset, [uint32]$Value) {
}
Write-BigEndian32 0x00 0x100 # text[0] file offset
Write-BigEndian32 0x48 $entry # text[0] guest address
Write-BigEndian32 0x90 $textSize # text[0] length
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
Write-BigEndian32 0xAC 4 # data[0] length
@@ -79,10 +84,13 @@ Write-BigEndian32 0x100 0x38600028 # li r3,40
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
Write-BigEndian32 0x108 0x60000000 # nop
Write-BigEndian32 0x10C 0x4E800020 # blr
foreach ($address in $guestCallbacks) {
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
}
Write-BigEndian32 $dataOffset 0x12345678
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
$entryPoints = '0x{0:X8}' -f $entry
$functionMap = '{0:X8} func_{0:X8}' -f $entry
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
$manifest = Join-Path $stage 'recomp.yml'
[IO.File]::WriteAllText($manifest, @"
@@ -120,7 +128,7 @@ Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--pr
'Translating the synthetic DOL'
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
foreach ($address in @($entry)) {
foreach ($address in @($entry) + $guestCallbacks) {
if ($address -notin $translated.functions.entryPoint) {
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
}
@@ -11,39 +11,22 @@ namespace WiiCompiled.Setup.Common;
public sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
/// <summary>
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from one fixed
/// endpoint per game release). Shared by both installers and the translator.
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from a single
/// fixed endpoint). Shared by both installers - moved here from WiiCompiled.Setup.Windows's
/// InputValidation.cs, which keeps every one of these method names as thin forwarding wrappers so
/// its many existing call sites (ProductRepairService.cs, LocalBuildService.cs, Installation.cs,
/// SelfTests.cs) needed no changes.
/// </summary>
public static class RetroWfcPayload
{
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
// The payload is tens of kilobytes from a fixed regional endpoint; allow 30 seconds.
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
/// <summary>
/// Base game id to Retro-WFC payload game. Test-PinnedFacts.ps1 checks this table against the
/// retro_wfc_payload every projects/*/recomp.yml pins.
/// </summary>
private static readonly Dictionary<string, string> PayloadGames = new(StringComparer.Ordinal)
{
["RMCP01"] = "RMCPD00",
["RMCE01"] = "RMCED00",
["RMCJ01"] = "RMCJD00",
};
public static IReadOnlyCollection<string> SupportedGameIds => PayloadGames.Keys;
public static string PayloadGame(string gameId) =>
PayloadGames.TryGetValue(gameId, out var payloadGame)
? payloadGame
: throw new InvalidDataException($"Unsupported Retro-WFC base game '{gameId}'.");
public static string PayloadRelativePath(string gameId) =>
Path.Combine("binary", $"payload.{PayloadGame(gameId)}.bin");
public static string PayloadUri(string gameId) =>
$"https://rwfc.net/api/wfc/payload?g={PayloadGame(gameId)}";
public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile =
Path.Combine("binary", "payload.RMCPD00.bin");
/// <summary>Retro-WFC production payload signing key (PROD PayloadPublicKey). Verifies
/// the payload past the 0x110 header; on-console stage1 pins the same key, so
@@ -62,48 +45,47 @@ public static class RetroWfcPayload
private const int RetroWfcPayloadSignatureOffset = 0x10;
private const int RetroWfcPayloadMinimumBytes = 0x130;
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory, string gameId,
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null)
{
if (string.IsNullOrWhiteSpace(stagedDirectory))
throw new InvalidDataException("The staged Retro-WFC payload directory is missing.");
var root = Path.GetFullPath(stagedDirectory);
var relativePath = PayloadRelativePath(gameId);
var payload = Path.Combine(root, relativePath);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
try
{
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
throw new InvalidDataException(
$"The staged Retro-WFC payload directory does not contain {relativePath}.");
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (FileNotFoundException)
{
throw new InvalidDataException(
$"The staged Retro-WFC payload directory does not contain {relativePath}.");
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (DirectoryNotFoundException)
{
throw new InvalidDataException(
$"The staged Retro-WFC payload directory does not contain {relativePath}.");
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
ValidateRetroWfcPayloadFile(payload, PayloadGame(gameId), signingKey);
ValidateRetroWfcPayloadFile(payload, signingKey);
return root;
}
public static string ResolveRetroWfcPayloadFile(string stagedDirectory, string gameId,
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
RSAParameters? signingKey = null) =>
Path.Combine(ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, gameId, signingKey),
PayloadRelativePath(gameId));
Path.Combine(ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey),
RetroWfcOfflinePayloadFile);
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory, string gameId,
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
RSAParameters? signingKey = null) =>
Sha256File(ResolveRetroWfcPayloadFile(stagedDirectory, gameId, signingKey));
Sha256File(ResolveRetroWfcPayloadFile(stagedDirectory, signingKey));
public static void ValidateRetroWfcPayloadUri(string uriText)
{
if (!PayloadGames.Keys.Any(gameId => string.Equals(uriText, PayloadUri(gameId), StringComparison.Ordinal)))
throw new InvalidDataException("The installer does not define this fixed Retro-WFC payload endpoint.");
if (!string.Equals(uriText, CurrentRetroWfcPayloadUri, StringComparison.Ordinal))
throw new InvalidDataException("The installer does not define the fixed Retro-WFC payload endpoint.");
}
public static async Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
@@ -114,8 +96,7 @@ public static class RetroWfcPayload
var uri = new Uri(uriText, UriKind.Absolute);
var root = Path.GetFullPath(destinationDirectory);
var payloadGame = uri.Query[3..]; // ValidateRetroWfcPayloadUri accepts only the exact ?g= form.
var destination = Path.Combine(root, "binary", $"payload.{payloadGame}.bin");
var destination = Path.Combine(root, RetroWfcOfflinePayloadFile);
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
using var handler = new HttpClientHandler { AllowAutoRedirect = false };
using var client = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
@@ -173,7 +154,7 @@ public static class RetroWfcPayload
}
actualSha256 = Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
}
ValidateRetroWfcPayloadFile(temporary, uri.Query[3..]);
ValidateRetroWfcPayloadFile(temporary);
File.Move(temporary, destination, overwrite: true);
return new RetroWfcPayloadSnapshot(root, actualSha256, total);
}
@@ -200,8 +181,7 @@ public static class RetroWfcPayload
request.StatusCode is { } status && (int)status >= 500;
}
private static void ValidateRetroWfcPayloadFile(string payload, string expectedGame,
RSAParameters? signingKey = null)
private static void ValidateRetroWfcPayloadFile(string payload, RSAParameters? signingKey = null)
{
byte[] image;
using (var stream = File.OpenRead(payload))
@@ -221,10 +201,6 @@ public static class RetroWfcPayload
throw new InvalidDataException(
$"The Retro-WFC payload declares {declaredSize} bytes but contains {image.Length}.");
if (image.Length < 0x144 ||
System.Text.Encoding.ASCII.GetString(image, 0x138, 12).TrimEnd('\0') != expectedGame)
throw new InvalidDataException($"The Retro-WFC payload is not for {expectedGame}.");
using var rsa = RSA.Create();
rsa.ImportParameters(signingKey ?? new RSAParameters
{
@@ -1,7 +1,4 @@
using System.Text;
using Tomlyn;
using Tomlyn.Parsing;
using Tomlyn.Syntax;
namespace WiiCompiled.Setup.Common;
@@ -20,6 +17,12 @@ 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),
@@ -72,11 +75,9 @@ public static class RuntimeConfiguration
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
/// <summary>The stored string of a <c>[paths]</c> key, exactly as the file holds it.</summary>
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
public static string? GetPath(string configPath, string key) =>
TryLoad(configPath) is { } document && Find(document, ["paths"], key)?.Item.Value is StringValueSyntax value
? value.Value!
: null;
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
/// <summary>
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
@@ -98,19 +99,9 @@ public static class RuntimeConfiguration
private static void SetPath(string configPath, string key, string value)
{
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);
var lines = ReadLinesOrDefault(configPath);
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
WriteLines(configPath, lines);
}
/// <summary>
@@ -132,104 +123,176 @@ public static class RuntimeConfiguration
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
{
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);
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);
}
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)
/// <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)
{
if (!File.Exists(configPath)) return null;
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 header = $"[{section}]";
var inSection = false;
foreach (var line in File.ReadLines(configPath))
{
KeyValueSyntax? previous = null;
foreach (var item in items)
var trimmed = RemoveComment(line).Trim();
if (IsSection(trimmed))
{
if (prefix.Concat(Parts(item.Key!)).SequenceEqual(wanted))
return new Entry(item, items, table, previous);
previous = item;
inSection = trimmed.Equals(header, KeyComparison);
continue;
}
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;
}
private static void Append(DocumentSyntax document, string[] section, KeyValueSyntax item)
/// <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)
{
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 header = $"[{section}]";
var sectionStart = -1;
var sectionEnd = lines.Count;
for (var index = 0; index < lines.Count; index++)
{
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);
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;
}
// 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);
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}");
}
private static SyntaxToken NewLine(string text) => new(TokenKind.NewLine, text);
private static List<string> ReadLinesOrDefault(string configPath) =>
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
private static SyntaxToken LastToken(SyntaxNode node)
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)
{
while (node is not SyntaxToken)
node = node.GetChild(node.ChildrenCount - 1)!;
return (SyntaxToken)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;
}
}
@@ -11,7 +11,4 @@
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tomlyn" Version="2.10.1" />
</ItemGroup>
</Project>
@@ -82,7 +82,6 @@ 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);
+8 -64
View File
@@ -11,30 +11,23 @@ namespace WiiCompiled.Setup.Linux;
/// </summary>
internal static class DiscTool
{
/// <summary>Validates and extracts the disc, returning the supported release it is.</summary>
public static async Task<ProjectManifest> ValidateAndExtractAsync(
string isoPath, IReadOnlyList<ProjectManifest> releases, string assetsDirectory, string workspace,
public static async Task ValidateAndExtractAsync(
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
{
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
// copies the whole data partition to disk (tens of seconds for a custom-track-heavy MKWii
// ISO). The game ID picks the release, so a wrong disc fails fast and an existing
// extraction is only reused for the same release.
// ISO). Checking the game ID first, before extracting, means a wrong disc fails fast -
// matching the original dolphin-tool `header` step this replaces.
reporter.Progress(InstallStages.ExtractDisc, "Reading the disc header", 2);
var info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken));
var manifest = releases.FirstOrDefault(release =>
string.Equals(info.GameId, release.GameId, StringComparison.Ordinal))
?? throw new InvalidOperationException(
$"This disc is '{info.GameId}', which is not a supported Mario Kart Wii release " +
$"({ProjectManifest.Describe(releases)}). " +
"Only your own legally-owned copy of a supported game/region can be used.");
if (IsAlreadyExtracted(manifest, assetsDirectory))
if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal))
{
reporter.Progress(InstallStages.ExtractDisc, "Reusing the verified extracted disc", 6);
return manifest;
throw new InvalidOperationException(
$"This disc is '{info.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " +
"Only your own legally-owned copy of that exact game/region can be used.");
}
// Extracted straight into Assets/DATA (kept, not a scratch dir) - the runtime reads course/
@@ -42,8 +35,6 @@ 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);
@@ -70,24 +61,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);
return manifest;
}
/// <summary>The release already extracted into Assets, chosen by its main.dol like local-build.sh.</summary>
public static ProjectManifest SelectExtracted(IReadOnlyList<ProjectManifest> releases, string assetsDirectory)
{
var dolSha = Sha256Of(Path.Combine(assetsDirectory, "main.dol"));
var manifest = releases.FirstOrDefault(release => string.Equals(dolSha, release.DolSha256, StringComparison.Ordinal))
?? throw new InvalidOperationException(
$"Assets/main.dol (sha256 {dolSha}) is not a supported clean Mario Kart Wii release " +
$"({ProjectManifest.Describe(releases)}).");
var relSha = Sha256Of(Path.Combine(assetsDirectory, "StaticR.rel"));
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
throw new InvalidOperationException(
$"Assets/StaticR.rel does not match the clean {manifest.GameId} release of Assets/main.dol.");
return manifest;
}
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
@@ -112,36 +86,6 @@ internal static class DiscTool
return stdout;
}
private const string ExtractionMarker = ".disc-extracted";
// Each release has one accepted revision, 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);
@@ -0,0 +1,209 @@
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));
}
@@ -1,10 +0,0 @@
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";
}
+7 -9
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(ProductInfo.Version) : new ConsoleInstallReporter();
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
try
{
@@ -95,14 +95,13 @@ internal static class Program
if (installsRetro) retroDir = RetroRewindSource.ResolveRetroRewind6(retroDir!);
var workspace = flags.GetValueOrDefault("workspace") ?? WorkspaceLocator.FindFrom(AppContext.BaseDirectory);
var releases = ProjectManifest.LoadAll(workspace);
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
var assetsDir = Path.Combine(workspace, "Assets");
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
ProjectManifest release;
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
{
release = await DiscTool.ValidateAndExtractAsync(isoPath, releases, assetsDir, workspace,
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
}
else
@@ -115,7 +114,6 @@ internal static class Program
"No --game ISO was given and Assets/main.dol + Assets/StaticR.rel are not already present. " +
"Either pass --game <path-to-iso>, or extract them yourself first (see translator/README.md).");
}
release = DiscTool.SelectExtracted(releases, assetsDir);
}
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
@@ -135,10 +133,10 @@ internal static class Program
try
{
// A valid signature authenticates a payload, but does not prove it is the latest
// signed revision. Always ask the release's fixed endpoint for the current snapshot;
// the downloader verifies it before atomically replacing the cache.
// signed revision. Always ask the fixed endpoint for the current snapshot; the
// downloader verifies it before atomically replacing the cache.
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.PayloadUri(release.GameId), cacheDir, token);
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
downloadFailure is HttpRequestException or TimeoutException
@@ -149,7 +147,7 @@ internal static class Program
// that must remain a hard failure instead of hiding possible endpoint tampering.
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir, gameId: release.GameId);
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// The handful of facts this tool needs out of a projects/*/recomp.yml. Parsed literally line by
/// The handful of facts this tool needs out of projects/mkwii/recomp.yml. Parsed literally line by
/// line - the same approach Launcher/NativeBuildFlags.ps1's Get-MkwProjectPins and
/// Launcher/local-build.sh already use - rather than pulling in a YAML library, since the manifest
/// is machine-written with a fixed shape.
@@ -67,21 +67,4 @@ internal sealed class ProjectManifest
return new ProjectManifest { GameId = gameId, Region = region, DolSha256 = dolSha, RelSha256 = relSha };
}
/// <summary>Every supported release: one per projects/*/recomp.yml, as local-build.sh selects them.</summary>
public static IReadOnlyList<ProjectManifest> LoadAll(string workspace)
{
var projects = Directory.EnumerateDirectories(Path.Combine(workspace, "projects"))
.Select(directory => Path.Combine(directory, "recomp.yml"))
.Where(File.Exists)
.Order(StringComparer.Ordinal)
.Select(Load)
.ToList();
if (projects.Count == 0)
throw new InvalidOperationException($"No projects/*/recomp.yml was found under {workspace}.");
return projects;
}
public static string Describe(IEnumerable<ProjectManifest> projects) =>
string.Join(", ", projects.Select(project => $"{project.GameId} (region {project.Region})"));
}
@@ -15,6 +15,5 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
<Using Include="WiiCompiled.Setup.Common" />
</ItemGroup>
</Project>
@@ -57,14 +57,16 @@ internal static class ConsoleCommands
public static int VerifyInputs(CommandLine command)
{
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
var temp = Path.Combine(Path.GetTempPath(), "mkwc-verify-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(temp);
try
{
using var payload = PayloadArchive.OpenCurrent();
var manifest = payload.ReadManifest();
var tool = payload.ExtractDiscTool(temp);
var tool = Path.Combine(temp, "nodtool.exe");
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
InputValidation.EnsureCompatibleDisc(header, manifest);
@@ -105,7 +107,7 @@ internal static class ConsoleCommands
}
File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}");
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log);
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
try
@@ -143,7 +145,7 @@ internal static class ConsoleCommands
public static int CheckProducts(CommandLine command,
CancellationToken cancellationToken = default)
{
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
var installation = new Installation(string.IsNullOrWhiteSpace(command.InstallDirectory)
? AppContext.BaseDirectory
: Path.GetFullPath(command.InstallDirectory));
@@ -196,7 +198,7 @@ internal static class ConsoleCommands
}
}
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
var installation = new Installation(installDirectory);
@@ -54,29 +54,33 @@ internal static class InputValidation
}
: "Unknown";
/// <summary>The supported release this disc is, or a clear rejection naming every supported one.</summary>
public static GameRelease EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest) =>
manifest.SupportedReleases.FirstOrDefault(release =>
release.GameId.Equals(header.GameId, StringComparison.OrdinalIgnoreCase))
?? throw new InvalidDataException(
"This build supports Mario Kart Wii " +
string.Join(", ", manifest.SupportedReleases.Select(release =>
$"{RegionFromGameId(release.GameId)} ({release.GameId})")) +
$". The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
}
}
// Forwarding wrappers over WiiCompiled.Setup.Common.RetroWfcPayload that require the game id,
// so no caller here can silently fall back to one region's payload.
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory, string gameId,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, gameId, signingKey);
// Thin forwarding wrappers: the actual download/RSA-verification logic lives in
// WiiCompiled.Setup.Common.RetroWfcPayload (shared with WiiCompiled.Setup.Linux) so there's one
// copy of it, not two. Kept under these names so every existing call site here
// (ProductRepairService.cs, LocalBuildService.cs, Installation.cs, SelfTests.cs) is unchanged.
public const string CurrentRetroWfcPayloadUri = RetroWfcPayload.CurrentRetroWfcPayloadUri;
public static string ResolveRetroWfcPayloadFile(string stagedDirectory, string gameId,
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, gameId, signingKey);
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey);
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory, string gameId,
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, gameId, signingKey);
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, signingKey);
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, signingKey);
public static void ValidateRetroWfcPayloadUri(string uriText) =>
RetroWfcPayload.ValidateRetroWfcPayloadUri(uriText);
@@ -1,19 +1,35 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Common;
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";
}
/// <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>
public interface IInstallReporter
internal 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>
public sealed class DelegatingInstallReporter : IInstallReporter
internal sealed class DelegatingInstallReporter : IInstallReporter
{
private readonly Action<string> _sink;
@@ -27,20 +43,15 @@ public 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>
public sealed class NdjsonInstallReporter : IInstallReporter
internal 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(string version, Action<string>? log = null)
{
_version = version;
_log = log;
}
public NdjsonInstallReporter(Action<string>? log = null) => _log = log;
public void Progress(string stage, string message, int percent)
{
@@ -67,7 +78,7 @@ public sealed class NdjsonInstallReporter : IInstallReporter
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = true, version = _version, installDir = installDirectory });
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
}
}
@@ -95,8 +106,8 @@ public sealed class NdjsonInstallReporter : IInstallReporter
}
}
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
public sealed class ConsoleInstallReporter : IInstallReporter
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
internal sealed class ConsoleInstallReporter : IInstallReporter
{
private readonly Action<string>? _log;
@@ -115,10 +126,10 @@ public sealed class ConsoleInstallReporter : IInstallReporter
}
}
/// <summary>Build step identifiers from the build scripts' <c>MKWCBUILD:STEP:&lt;id&gt;</c> lines. The
/// <summary>Build step identifiers from the bundled script's <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>
public static class BuildStepIds
internal static class BuildStepIds
{
public const string ReuseBaseTranslation = "reuse-base-translation";
public const string RetranslateBase = "retranslate-base";
@@ -132,16 +143,16 @@ public static class BuildStepIds
}
/// <summary>
/// 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.
/// 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.
/// </summary>
public sealed class BuildProgressWindow
internal sealed class BuildProgressWindow
{
private const string Marker = "MKWCBUILD:";
private const string StepMarker = "STEP:";
/// <summary>The fraction the compile step reaches; beyond it, Ninja's counter drives progress.</summary>
/// <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 =
@@ -153,7 +164,7 @@ public 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 compiler"),
(BuildStepIds.ConfigureNative, 0.52, "Configuring the bundled compiler"),
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
];
@@ -198,30 +209,19 @@ public sealed class BuildProgressWindow
}
}
// Anything else - an unknown step identifier, a plain MKWCBUILD note, or raw tool output -
// stays a diagnostic.
// 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.
_reporter.Diagnostic(line);
// 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))
// 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.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
_fraction = Math.Min(0.97, _fraction + 0.0015);
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,17 +0,0 @@
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";
}
@@ -131,8 +131,6 @@ internal sealed class Installation
Workspace + "LocalBuild.ps1",
Workspace + "NativeBuildFlags.ps1",
Workspace + @"projects\mkwii\recomp.yml",
Workspace + @"projects\mkwii-ntsc-u\recomp.yml",
Workspace + @"projects\mkwii-ntsc-j\recomp.yml",
Workspace + @"runtime\CMakeLists.txt",
Workspace + @"runtime\assets\dsp\dsp_coef.bin"
];
@@ -170,26 +168,6 @@ internal sealed class Installation
public InstallState? ReadInstallState() => JsonState.TryRead<InstallState>(InstallStatePath);
/// <summary>The game id of the installed disc, which selects its region's Retro-WFC payload.</summary>
public string ReadGameId()
{
try { return ReadDiscGameId(GameDataDirectory); }
catch (IOException ex)
{
throw new InvalidDataException(
"The installed game data has no readable disc header. Apply setup again with the disc image.", ex);
}
}
/// <summary>The six-character game id in an extracted disc's <c>sys/boot.bin</c>.</summary>
internal static string ReadDiscGameId(string dataRoot)
{
using var stream = File.OpenRead(Path.Combine(dataRoot, "sys", "boot.bin"));
var id = new byte[6];
stream.ReadExactly(id);
return System.Text.Encoding.ASCII.GetString(id);
}
internal ToolkitState? ReadToolkitState() => JsonState.TryRead<ToolkitState>(ToolkitStatePath);
/// <summary>
@@ -418,7 +396,7 @@ internal sealed class Installation
try
{
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload, ReadGameId());
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload);
if (!InputValidation.Sha256File(payload).Equals(fingerprint.RetroWfcPayloadSha256,
StringComparison.OrdinalIgnoreCase) ||
new FileInfo(payload).Length != fingerprint.RetroWfcPayloadLength)
@@ -603,7 +581,7 @@ internal sealed class Installation
{
try
{
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload, ReadGameId());
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload);
return new FileInfo(payload).Length == snapshot.ByteLength &&
InputValidation.Sha256File(payload).Equals(snapshot.Sha256, StringComparison.Ordinal);
}
@@ -40,28 +40,6 @@ internal sealed class InstallerEngine
var toolkit = InstalledLayout.Toolkit(staging);
var workspace = InstalledLayout.Workspace(staging);
// The disc decides which release's inputs everything below expects, so it is identified
// before any reuse decision or toolkit extraction.
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 1);
var nodTool = payload.ExtractDiscTool(Path.Combine(staging, "disc-tool"));
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
var release = InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
? null
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
ValidateRetroOptions(canonicalRetroRoot, options.RetroWfcPayloadMode);
var retroCompileInputs = canonicalRetroRoot is null
? null
: SnapshotRetroRewindCompileInputs(canonicalRetroRoot,
Path.Combine(staging, "compile-inputs"), cancellationToken);
if (options.Portable)
{
var portableRoot = PortableRoot.Create(parent);
_reporter.Diagnostic($"Installing as a portable installation under {portableRoot}.");
}
var candidateToolkitFingerprint = manifest.ToolkitFingerprint;
var candidateToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint;
var candidateRuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint;
@@ -71,7 +49,7 @@ internal sealed class InstallerEngine
var installedToolkitPackageFingerprint = existing.IsPresent
? existing.ResolveToolkitPackageFingerprint()
: "";
var reusableGameAssets = FindReusableGameAssets(existing, release);
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
@@ -90,7 +68,7 @@ internal sealed class InstallerEngine
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
_reporter.Progress(InstallStages.ExtractToolkit,
"Inspecting the release's local recompilation toolkit...", 4);
"Inspecting the release's local recompilation toolkit...", 1);
if (extractToolkit) payload.ExtractDirectory(InstalledLayout.ToolkitDirectoryName, toolkit);
if (extractWorkspace)
{
@@ -104,10 +82,30 @@ internal sealed class InstallerEngine
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
? null
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
ValidateRetroOptions(canonicalRetroRoot, options.RetroWfcPayloadMode, manifest);
var retroCompileInputs = canonicalRetroRoot is null
? null
: SnapshotRetroRewindCompileInputs(canonicalRetroRoot,
Path.Combine(staging, "compile-inputs"), cancellationToken);
if (options.Portable)
{
var portableRoot = PortableRoot.Create(parent);
_reporter.Diagnostic($"Installing as a portable installation under {portableRoot}.");
}
if (sameToolkit)
{
var desiredInputDrift = ProductRepairService.FindDesiredInputDrift(existing,
candidateToolkitFingerprint, release.DolSha256, release.RelSha256);
candidateToolkitFingerprint, manifest.ExpectedDolSha256, manifest.ExpectedRelSha256);
if (desiredInputDrift.Retro && retroCompileInputs is null)
throw new InvalidOperationException(
"This release changes the clean game inputs used by the installed Retro Rewind product. " +
@@ -115,7 +113,7 @@ internal sealed class InstallerEngine
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, Path.Combine(staging, "repair"),
release.DolSha256, release.RelSha256, cancellationToken);
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
var remainingCancellation = reconciliation.PublicationCommitted
? CancellationToken.None
: cancellationToken;
@@ -127,10 +125,11 @@ internal sealed class InstallerEngine
remainingCancellation.ThrowIfCancellationRequested();
EnsureReconciliationCurrent(existing, candidateToolkitFingerprint, retroCompileInputs,
release.DolSha256, release.RelSha256, reconciliation.CachedRetroWfcPayloadMatches);
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
release, retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, updatedState);
remainingCancellation.ThrowIfCancellationRequested();
@@ -160,11 +159,11 @@ internal sealed class InstallerEngine
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(nodTool, options.GamePath,
Path.Combine(staging, "GameAssets"), release, cancellationToken);
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
}
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
release, previousState, options, canonicalRetroRoot, retroCompileInputs,
previousState, options, canonicalRetroRoot, retroCompileInputs,
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
cancellationToken: cancellationToken);
}
@@ -188,7 +187,7 @@ internal sealed class InstallerEngine
["generated", "build", "native-build", "Assets", "PulsarPacks"];
private async Task PublishToolkitAndReconcileProductsAsync(Installation existing, string staging,
string stagedWorkspace, PayloadManifest manifest, GameRelease release, InstallState? previousState,
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
@@ -207,7 +206,7 @@ internal sealed class InstallerEngine
}
var state = BuildInstallState(previousState, installDirectory, manifest, release,
var state = BuildInstallState(previousState, installDirectory, manifest,
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, state);
@@ -227,9 +226,10 @@ internal sealed class InstallerEngine
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, repairScratch: null,
release.DolSha256, release.RelSha256, cancellationToken);
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
EnsureReconciliationCurrent(existing, manifest.ToolkitFingerprint, retroCompileInputs,
release.DolSha256, release.RelSha256, reconciliation.CachedRetroWfcPayloadMatches);
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
}
private Task<ProductRepairService.ReconciliationResult> ReconcileSameToolkitAsync(Installation existing,
@@ -354,8 +354,7 @@ internal sealed class InstallerEngine
}
private static InstallState BuildInstallState(InstallState? previousState, string installDirectory,
PayloadManifest manifest, GameRelease release, RetroRewindCompileInputs? compileInputs,
string? canonicalRetroRoot,
PayloadManifest manifest, RetroRewindCompileInputs? compileInputs, string? canonicalRetroRoot,
RetroWfcPayloadMode payloadMode,
RetroWfcPayloadSnapshot? retroWfcPayloadSnapshot = null)
{
@@ -369,8 +368,8 @@ internal sealed class InstallerEngine
InstalledUtc = previousState?.InstalledUtc ?? DateTime.UtcNow,
RetroRewindInstalled = previousState?.RetroRewindInstalled ?? false,
ToolkitReleaseTag = manifest.ToolkitReleaseTag,
DolSha256 = release.DolSha256,
RelSha256 = release.RelSha256,
DolSha256 = manifest.ExpectedDolSha256,
RelSha256 = manifest.ExpectedRelSha256,
RetroRewindCodePulSha256 = previousState?.RetroRewindCodePulSha256 ?? "",
RetroRewindCompileInputsSha256 = previousState?.RetroRewindCompileInputsSha256 ?? "",
RetroWfcPayloadMode = previousState?.RetroWfcPayloadMode ?? "",
@@ -445,7 +444,8 @@ internal sealed class InstallerEngine
Path.Combine(InstalledLayout.Workspace(installDirectory), "runtime", "assets")));
}
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode)
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode,
PayloadManifest manifest)
{
if (canonicalRetroRoot is null && mode != RetroWfcPayloadMode.NotApplicable)
throw new InvalidOperationException(
@@ -453,9 +453,11 @@ internal sealed class InstallerEngine
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.NotApplicable)
throw new InvalidOperationException(
"Download the Retro-WFC payload, or explicitly skip the optional payload.");
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.Online)
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
}
private string? FindReusableGameAssets(Installation existing, GameRelease release)
private string? FindReusableGameAssets(Installation existing, PayloadManifest manifest)
{
var dataRoot = existing.GameDataDirectory;
// No fst.bin simply means nothing was extracted here before; anything past that gate is an
@@ -464,7 +466,7 @@ internal sealed class InstallerEngine
if (!File.Exists(Path.Combine(dataRoot, "sys", "fst.bin"))) return null;
try
{
ValidateExtractedGame(dataRoot, release);
ValidateExtractedGame(dataRoot, manifest);
return dataRoot;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
@@ -476,7 +478,7 @@ internal sealed class InstallerEngine
}
private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination,
GameRelease release, CancellationToken cancellationToken)
PayloadManifest manifest, CancellationToken cancellationToken)
{
_reporter.Progress(InstallStages.ExtractDisc,
"Extracting the game disc. This is the longest preparation step...", 6);
@@ -491,43 +493,25 @@ internal sealed class InstallerEngine
cancellationToken);
if (extraction.ExitCode != 0)
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
ValidateExtractedGame(dataRoot, release);
ValidateExtractedGame(dataRoot, manifest);
}
internal static void ValidateExtractedGame(string dataRoot, GameRelease release)
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
{
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");
var boot = Path.Combine(dataRoot, "sys", "boot.bin");
var missing = new[] { dol, rel, fst, boot }.Where(path => !File.Exists(path))
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
if (missing.Count > 0)
throw new InvalidDataException(
"The disc extraction is missing required Mario Kart Wii files: " +
string.Join(", ", missing) + ".");
var mismatches = new List<string>();
var discGameId = Installation.ReadDiscGameId(dataRoot);
if (!discGameId.Equals(release.GameId, StringComparison.Ordinal))
mismatches.Add($"sys/boot.bin names {discGameId}");
foreach (var (name, path, expected) in new[]
{
("main.dol", dol, release.DolSha256),
("StaticR.rel", rel, release.RelSha256)
})
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 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 {release.GameId} but its game data does not match the supported clean revision: " +
string.Join("; ", mismatches) + ". " +
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
"Patched or otherwise modified game code cannot be installed safely.");
}
}
private static void ValidateInstallDirectory(string path) =>
@@ -23,8 +23,7 @@ internal sealed class LocalBuildService
/// </summary>
public async Task BuildAsync(string installStaging, BuildProfile profile, string outputDirectory,
RetroWfcPayloadMode retroWfcPayloadMode, string? retroWfcOfflinePayloadDirectory,
string? retroWfcGameId, CancellationToken cancellationToken,
ToolkitFingerprintComponents? toolkitComponents = null,
CancellationToken cancellationToken, ToolkitFingerprintComponents? toolkitComponents = null,
bool forceCleanBuild = false, BuildProgressWindow? progress = null,
string? retroRewindPackageDirectory = null, string? baseOutputDirectory = null)
{
@@ -46,9 +45,8 @@ internal sealed class LocalBuildService
retroRewindPackageDirectory = RetroRewindSource.ResolveRetroRewind6(
retroRewindPackageDirectory);
if (retroWfcPayloadMode == RetroWfcPayloadMode.Online)
retroWfcOfflinePayloadDirectory = InputValidation.ValidateStagedRetroWfcPayloadDirectory(
retroWfcOfflinePayloadDirectory!,
retroWfcGameId ?? throw new ArgumentException("The Retro-WFC payload needs its game id."));
retroWfcOfflinePayloadDirectory =
InputValidation.ValidateStagedRetroWfcPayloadDirectory(retroWfcOfflinePayloadDirectory!);
var arguments = new List<string>
{
+4 -11
View File
@@ -33,23 +33,16 @@ internal sealed class InstallOptions
public bool HasRetroRewind => RetroDirectoryPath is not null;
}
/// <summary>A clean retail release the payload installs, as pinned by its projects/*/recomp.yml.</summary>
internal sealed class GameRelease
{
public string GameId { get; set; } = "";
public string DolSha256 { get; set; } = "";
public string RelSha256 { get; set; } = "";
}
internal sealed class PayloadManifest
{
public const int CurrentSchemaVersion = 3;
public int SchemaVersion { get; set; }
public string ProductVersion { get; set; } = "";
public List<GameRelease> SupportedReleases { get; set; } = [];
public string ExpectedGameId { get; set; } = "RMCP01";
public string ExpectedDolSha256 { get; set; } = "";
public string ExpectedRelSha256 { get; set; } = "";
public string ToolkitReleaseTag { get; set; } = "";
public string BuildModel { get; set; } = "";
public string RetroWfcPayloadUri { get; set; } = "";
/// <summary>
/// Content identities computed once at release-build time (<c>Build-Installer.ps1 --emit-payload-identities</c>)
@@ -54,21 +54,10 @@ internal sealed class PayloadArchive : IDisposable
var entry = FindEntry(InstalledLayout.PayloadManifestFileName)
?? throw new InvalidDataException("The payload manifest is missing.");
using var stream = entry.Open();
return ParseManifest(stream);
}
internal static PayloadManifest ParseManifest(Stream stream)
{
var manifest = JsonSerializer.Deserialize<PayloadManifest>(stream,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidDataException("The payload manifest is invalid.");
ValidateManifest(manifest);
return manifest;
}
internal static void ValidateManifest(PayloadManifest manifest)
{
if (manifest.SchemaVersion != PayloadManifest.CurrentSchemaVersion)
if (manifest.SchemaVersion != 2)
throw new InvalidDataException($"Unsupported payload schema {manifest.SchemaVersion}.");
// The payload identities are the release-computed replacement for hashing the whole
// toolkit on the user's machine; a payload without them was packaged incorrectly.
@@ -76,30 +65,8 @@ internal sealed class PayloadArchive : IDisposable
string.IsNullOrWhiteSpace(manifest.ToolkitPackageFingerprint) ||
string.IsNullOrWhiteSpace(manifest.RuntimeAssetsFingerprint))
throw new InvalidDataException("The payload manifest is missing its content identities.");
if (manifest.SupportedReleases is not { Count: > 0 })
throw new InvalidDataException("The payload manifest names no supported game release.");
foreach (var release in manifest.SupportedReleases)
{
if (!RetroWfcPayload.SupportedGameIds.Contains(release.GameId) ||
!IsSha256(release.DolSha256) || !IsSha256(release.RelSha256))
throw new InvalidDataException(
$"The payload manifest describes an unsupported or incomplete release '{release.GameId}'.");
}
if (manifest.SupportedReleases.DistinctBy(release => release.GameId).Count() !=
manifest.SupportedReleases.Count)
throw new InvalidDataException("The payload manifest lists a game release twice.");
}
private static bool IsSha256(string value) =>
value.Length == 64 && value.All(char.IsAsciiHexDigit);
/// <summary>Extracts nodtool and the runtime DLLs it loads; returns the nodtool path.</summary>
public string ExtractDiscTool(string directory)
{
var tool = Path.Combine(directory, "nodtool.exe");
ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", directory);
return tool;
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
return manifest;
}
public void ExtractEntry(string entryName, string destination)
@@ -99,11 +99,7 @@ internal sealed class ProductRepairService
Directory.CreateDirectory(scratchRoot);
RetroWfcPayloadSnapshot? payloadSnapshot = null;
// The installed disc's region decides which Retro-WFC payload the product embeds.
var payloadGameId = requestedPayloadMode == RetroWfcPayloadMode.Online
? _installation.ReadGameId()
: null;
if (payloadGameId is not null)
if (requestedPayloadMode == RetroWfcPayloadMode.Online)
{
_reporter.Progress(InstallStages.PrepareRetro,
"Downloading the current shared Retro-WFC payload snapshot...", 3);
@@ -111,12 +107,12 @@ internal sealed class ProductRepairService
try
{
payloadSnapshot = await InputValidation.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.PayloadUri(payloadGameId), payloadScratch, cancellationToken);
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or TimeoutException or IOException)
{
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint, payloadGameId,
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
}
}
@@ -240,7 +236,7 @@ internal sealed class ProductRepairService
"Recompiling Mario Kart Wii and Retro Rewind with the installed toolkit...", 5);
await BuildWithCleanRetryAsync(forceCleanBuild, clean =>
builder.BuildAsync(_installation.Root, BuildProfile.Both, retroOutput,
requestedPayloadMode, payloadSnapshot?.Directory, payloadGameId, cancellationToken,
requestedPayloadMode, payloadSnapshot?.Directory, cancellationToken,
toolkitComponents, clean,
progress: new BuildProgressWindow(_reporter, InstallStages.BuildBase, 5, 92),
retroRewindPackageDirectory: compileInputs.RetroRewindRoot,
@@ -259,7 +255,7 @@ internal sealed class ProductRepairService
"Recompiling Mario Kart Wii with the installed toolkit...", 5);
await BuildWithCleanRetryAsync(forceCleanBuild, clean =>
builder.BuildAsync(_installation.Root, BuildProfile.Base, baseOutput,
RetroWfcPayloadMode.NotApplicable, null, null, cancellationToken,
RetroWfcPayloadMode.NotApplicable, null, cancellationToken,
toolkitComponents, clean,
progress: new BuildProgressWindow(_reporter, InstallStages.BuildBase, 5, 92)));
LocalBuildService.WriteFingerprint(baseOutput, BuildProfile.Base, toolkitFingerprint,
@@ -274,7 +270,7 @@ internal sealed class ProductRepairService
"Recompiling Retro Rewind for the canonical Code.pul...", 5);
await BuildWithCleanRetryAsync(forceCleanBuild, clean =>
builder.BuildAsync(_installation.Root, BuildProfile.RetroRewind, retroOutput,
requestedPayloadMode, payloadSnapshot?.Directory, payloadGameId, cancellationToken,
requestedPayloadMode, payloadSnapshot?.Directory, cancellationToken,
toolkitComponents, clean,
progress: new BuildProgressWindow(_reporter, InstallStages.BuildRetro, 5, 92),
retroRewindPackageDirectory: compileInputs.RetroRewindRoot));
@@ -314,8 +310,7 @@ internal sealed class ProductRepairService
private RetroWfcPayloadSnapshot RecoverInstalledRetroWfcPayload(string toolkitFingerprint,
string gameId, string destinationDirectory, Exception downloadFailure,
CancellationToken cancellationToken)
string destinationDirectory, Exception downloadFailure, CancellationToken cancellationToken)
{
InvalidOperationException Unavailable(string reason) => new(
"The Retro-WFC payload service is unavailable" +
@@ -339,7 +334,7 @@ internal sealed class ProductRepairService
try
{
cachedFile = InputValidation.ResolveRetroWfcPayloadFile(
_installation.WorkspaceRetroWfcPayload, gameId);
_installation.WorkspaceRetroWfcPayload);
cancellationToken.ThrowIfCancellationRequested();
if (new FileInfo(cachedFile).Length != fingerprint.RetroWfcPayloadLength ||
!InputValidation.Sha256File(cachedFile).Equals(fingerprint.RetroWfcPayloadSha256,
@@ -348,11 +343,11 @@ internal sealed class ProductRepairService
throw Unavailable("the installed payload snapshot no longer matches its recorded identity.");
}
var destination = Path.Combine(Path.GetFullPath(destinationDirectory),
RetroWfcPayload.PayloadRelativePath(gameId));
var destination = Path.Combine(Path.GetFullPath(destinationDirectory), "binary",
"payload.RMCPD00.bin");
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
File.Copy(cachedFile, destination, overwrite: true);
var root = InputValidation.ValidateStagedRetroWfcPayloadDirectory(destinationDirectory, gameId);
var root = InputValidation.ValidateStagedRetroWfcPayloadDirectory(destinationDirectory);
_reporter.Diagnostic(
"The Retro-WFC payload service is unavailable; reusing the installation's verified " +
$"payload snapshot (sha256 {fingerprint.RetroWfcPayloadSha256[..12]}..., " +
@@ -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(ProductInfo.Version).Failure(ex.Message);
if (progressJson) new NdjsonInstallReporter().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(ProductInfo.Version).Failure(ex.Message);
new NdjsonInstallReporter().Failure(ex.Message);
Console.Error.WriteLine(ex);
}
else
+45 -339
View File
@@ -1,6 +1,5 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using Tomlyn;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
@@ -28,9 +27,19 @@ internal static class SelfTests
}
finally { File.Delete(path); }
}, failures);
Test("Disc compatibility", TestDiscCompatibility, failures);
Test("Payload manifest releases", TestPayloadManifestReleases, failures);
Test("Extracted game release validation", TestExtractedGameRelease, failures);
Test("Disc compatibility", () =>
InputValidation.EnsureCompatibleDisc(new DiscHeader { GameId = "RMCP01" },
new PayloadManifest { ExpectedGameId = "RMCP01" }), failures);
Test("Disc rejection", () =>
{
try
{
InputValidation.EnsureCompatibleDisc(new DiscHeader { GameId = "RMCJ01" },
new PayloadManifest { ExpectedGameId = "RMCP01" });
throw new Exception("Wrong region was accepted.");
}
catch (InvalidDataException) { }
}, failures);
Test("Payload timestamp normalization", TestPayloadTimestampNormalization, failures);
Test("Workspace timestamp reconciliation", TestWorkspaceTimestampReconciliation, failures);
Test("Toolkit accompanies the workspace", TestToolkitRefreshDecision, failures);
@@ -42,7 +51,6 @@ 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);
@@ -67,152 +75,6 @@ internal static class SelfTests
return failures.Count == 0 ? 0 : 1;
}
private static PayloadManifest MultiReleaseManifest() => new()
{
SchemaVersion = PayloadManifest.CurrentSchemaVersion,
ToolkitFingerprint = "toolkit", ToolkitPackageFingerprint = "package", RuntimeAssetsFingerprint = "assets",
SupportedReleases =
[
new GameRelease { GameId = "RMCP01", DolSha256 = new string('1', 64), RelSha256 = new string('2', 64) },
new GameRelease { GameId = "RMCE01", DolSha256 = new string('3', 64), RelSha256 = new string('4', 64) },
new GameRelease { GameId = "RMCJ01", DolSha256 = new string('5', 64), RelSha256 = new string('6', 64) }
]
};
private static void TestDiscCompatibility()
{
var manifest = MultiReleaseManifest();
foreach (var release in manifest.SupportedReleases)
{
var selected = InputValidation.EnsureCompatibleDisc(
new DiscHeader { GameId = release.GameId.ToLowerInvariant() }, manifest);
if (!ReferenceEquals(selected, release))
throw new Exception($"{release.GameId} did not select its own release.");
}
foreach (var unsupported in new[] { "RMCK01", "RMCP02", "RSBE01" })
{
try
{
InputValidation.EnsureCompatibleDisc(new DiscHeader { GameId = unsupported }, manifest);
throw new Exception($"{unsupported} was accepted.");
}
catch (InvalidDataException ex) when (ex.Message.Contains("NTSC-U (RMCE01)", StringComparison.Ordinal) &&
ex.Message.Contains(unsupported, StringComparison.Ordinal)) { }
}
}
private static void TestPayloadManifestReleases()
{
PayloadArchive.ValidateManifest(MultiReleaseManifest());
// The shape Build-Installer.ps1 writes (PowerShell's ConvertTo-Json of the ordered manifest).
var dol = new string('a', 64);
var rel = new string('b', 64);
var json = $$"""
{
"SchemaVersion": 3,
"ProductVersion": "1.0.0",
"SupportedReleases": [
{ "GameId": "RMCP01", "DolSha256": "{{dol}}", "RelSha256": "{{rel}}" },
{ "GameId": "RMCE01", "DolSha256": "{{rel}}", "RelSha256": "{{dol}}" }
],
"ToolkitReleaseTag": "",
"BuildModel": "local-translation-and-compilation",
"ToolkitFingerprint": "toolkit",
"ToolkitPackageFingerprint": "package",
"RuntimeAssetsFingerprint": "assets",
"TranslationFingerprint": "translation",
"NativeToolchainFingerprint": "native"
}
""";
using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json)))
{
var parsed = PayloadArchive.ParseManifest(stream);
if (parsed.SupportedReleases.Count != 2 || parsed.SupportedReleases[1].GameId != "RMCE01" ||
parsed.SupportedReleases[1].DolSha256 != rel)
throw new Exception("The packaged release list was not read back exactly.");
}
void Reject(string description, Action<PayloadManifest> corrupt)
{
var manifest = MultiReleaseManifest();
corrupt(manifest);
try
{
PayloadArchive.ValidateManifest(manifest);
throw new Exception($"A manifest with {description} was accepted.");
}
catch (InvalidDataException) { }
}
Reject("the previous single-release schema", manifest => manifest.SchemaVersion = 2);
Reject("no releases", manifest => manifest.SupportedReleases.Clear());
Reject("an unsupported release", manifest => manifest.SupportedReleases[0].GameId = "RMCK01");
Reject("a missing input digest", manifest => manifest.SupportedReleases[1].RelSha256 = "");
Reject("a duplicated release", manifest =>
manifest.SupportedReleases.Add(new GameRelease
{
GameId = "RMCE01", DolSha256 = new string('7', 64), RelSha256 = new string('8', 64)
}));
}
private static void TestExtractedGameRelease()
{
var root = Path.Combine(Path.GetTempPath(), "mkwc-release-" + Guid.NewGuid().ToString("N"));
try
{
var data = Path.Combine(root, "GameAssets", "DATA");
Directory.CreateDirectory(Path.Combine(data, "sys"));
Directory.CreateDirectory(Path.Combine(data, "files", "rel"));
File.WriteAllText(Path.Combine(data, "sys", "main.dol"), "ntsc-u dol");
File.WriteAllText(Path.Combine(data, "sys", "fst.bin"), "fst");
File.WriteAllText(Path.Combine(data, "files", "rel", "StaticR.rel"), "ntsc-u rel");
File.WriteAllText(Path.Combine(data, "sys", "boot.bin"), "RMCE01" + new string('\0', 26));
var release = new GameRelease
{
GameId = "RMCE01",
DolSha256 = InputValidation.Sha256File(Path.Combine(data, "sys", "main.dol")),
RelSha256 = InputValidation.Sha256File(Path.Combine(data, "files", "rel", "StaticR.rel"))
};
InstallerEngine.ValidateExtractedGame(data, release);
if (new Installation(root).ReadGameId() != "RMCE01")
throw new Exception("The installed game id was not read from the extracted disc header.");
void Reject(string description, GameRelease expected)
{
try
{
InstallerEngine.ValidateExtractedGame(data, expected);
throw new Exception($"{description} was accepted.");
}
catch (InvalidDataException) { }
}
Reject("Another release's digests", new GameRelease
{
GameId = "RMCE01", DolSha256 = new string('1', 64), RelSha256 = release.RelSha256
});
Reject("A disc header of another region", new GameRelease
{
GameId = "RMCJ01", DolSha256 = release.DolSha256, RelSha256 = release.RelSha256
});
File.Delete(Path.Combine(data, "sys", "boot.bin"));
Reject("An extraction without a disc header", release);
try
{
_ = new Installation(root).ReadGameId();
throw new Exception("A missing disc header produced a game id.");
}
catch (InvalidDataException) { }
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static void TestAcceptedExtension(string extension)
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + extension);
@@ -367,70 +229,6 @@ 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.
@@ -830,91 +628,42 @@ internal static class SelfTests
private static void TestRetroWfcPayloadSnapshotReconciliation()
{
// The endpoint is pinned in exactly one place; this test must not restate it.
const string endpoint = InputValidation.CurrentRetroWfcPayloadUri;
const string shaB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
var root = Path.Combine(Path.GetTempPath(), "mkwc-wfc-snapshot-" + Guid.NewGuid().ToString("N"));
try
{
using var signingRsa = RSA.Create(2048);
var signingKey = signingRsa.ExportParameters(includePrivateParameters: false);
byte[] SignedPayload(string payloadGame)
{
var image = new byte[0x150];
"WWFC/Payload"u8.CopyTo(image);
BinaryPrimitives.WriteUInt32BigEndian(image.AsSpan(0x0C), (uint)image.Length);
System.Text.Encoding.ASCII.GetBytes(payloadGame).CopyTo(image, 0x138);
image[^1] = 0x5A;
signingRsa.SignData(image.AsSpan(0x110), HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1).CopyTo(image.AsSpan(0x10));
return image;
}
// Every supported release has its own endpoint and staged file, and a payload staged for
// one region never validates as another region's.
var endpoints = new HashSet<string>(StringComparer.Ordinal);
foreach (var gameId in RetroWfcPayload.SupportedGameIds)
{
var endpoint = RetroWfcPayload.PayloadUri(gameId);
InputValidation.ValidateRetroWfcPayloadUri(endpoint);
if (!endpoints.Add(endpoint))
throw new Exception($"{gameId} shares another release's Retro-WFC endpoint.");
try
{
InputValidation.ValidateRetroWfcPayloadUri(endpoint + "&extra=1");
throw new Exception("A non-canonical Retro-WFC endpoint was accepted.");
}
catch (InvalidDataException) { }
var regionalRoot = Path.Combine(root, "regional-" + gameId);
var regionalPath = Path.Combine(regionalRoot, RetroWfcPayload.PayloadRelativePath(gameId));
Directory.CreateDirectory(Path.GetDirectoryName(regionalPath)!);
File.WriteAllBytes(regionalPath, SignedPayload(RetroWfcPayload.PayloadGame(gameId)));
if (InputValidation.ResolveRetroWfcPayloadFile(regionalRoot, gameId, signingKey) != regionalPath)
throw new Exception($"{gameId} did not resolve its own staged payload file.");
foreach (var otherGameId in RetroWfcPayload.SupportedGameIds.Where(other => other != gameId))
{
var foreignRoot = Path.Combine(root, $"foreign-{gameId}-{otherGameId}");
var foreignPath = Path.Combine(foreignRoot, RetroWfcPayload.PayloadRelativePath(otherGameId));
Directory.CreateDirectory(Path.GetDirectoryName(foreignPath)!);
File.WriteAllBytes(foreignPath, SignedPayload(RetroWfcPayload.PayloadGame(gameId)));
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(foreignRoot, otherGameId,
signingKey);
throw new Exception($"A {gameId} payload was accepted for {otherGameId}.");
}
catch (InvalidDataException) { }
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(regionalRoot, otherGameId,
signingKey);
throw new Exception($"{otherGameId} accepted a directory staged only for {gameId}.");
}
catch (InvalidDataException) { }
}
}
InputValidation.ValidateRetroWfcPayloadUri(endpoint);
try
{
_ = RetroWfcPayload.PayloadUri("RMCK01");
throw new Exception("An unsupported release has a Retro-WFC endpoint.");
InputValidation.ValidateRetroWfcPayloadUri(endpoint + "&extra=1");
throw new Exception("A non-canonical Retro-WFC endpoint was accepted.");
}
catch (InvalidDataException) { }
const string gameUnderTest = "RMCE01";
var installation = new Installation(root);
Directory.CreateDirectory(installation.RetroDirectory);
var payloadRoot = Path.Combine(root, "download");
var payloadPath = Path.Combine(payloadRoot, RetroWfcPayload.PayloadRelativePath(gameUnderTest));
var payloadPath = Path.Combine(payloadRoot, "binary", "payload.RMCPD00.bin");
Directory.CreateDirectory(Path.GetDirectoryName(payloadPath)!);
var bytes = SignedPayload(RetroWfcPayload.PayloadGame(gameUnderTest));
using var signingRsa = RSA.Create(2048);
var signingKey = signingRsa.ExportParameters(includePrivateParameters: false);
var bytes = new byte[0x140];
"WWFC/Payload"u8.CopyTo(bytes);
BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(0x0C), (uint)bytes.Length);
bytes[^1] = 0x5A;
signingRsa.SignData(bytes.AsSpan(0x110), HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1).CopyTo(bytes.AsSpan(0x10));
File.WriteAllBytes(payloadPath, bytes);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, gameUnderTest, signingKey);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey);
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, gameUnderTest);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot);
throw new Exception("A payload signed by a foreign key passed the pinned production key.");
}
catch (InvalidDataException) { }
var shaA = InputValidation.ComputeRetroWfcPayloadSha256(payloadRoot, gameUnderTest, signingKey);
var shaA = InputValidation.ComputeRetroWfcPayloadSha256(payloadRoot, signingKey);
var snapshot = new RetroWfcPayloadSnapshot(payloadRoot, shaA, bytes.Length);
JsonState.Write(Path.Combine(installation.RetroDirectory, ProductFingerprint.FileName),
new ProductFingerprint
@@ -949,7 +698,7 @@ internal static class SelfTests
File.WriteAllBytes(payloadPath, bytes);
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, gameUnderTest, signingKey);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey);
throw new Exception("A payload with a malformed declared length was accepted.");
}
catch (InvalidDataException) { }
@@ -959,7 +708,7 @@ internal static class SelfTests
File.WriteAllBytes(payloadPath, bytes);
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, gameUnderTest, signingKey);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey);
throw new Exception("A payload with tampered signed content was accepted.");
}
catch (InvalidDataException) { }
@@ -969,7 +718,7 @@ internal static class SelfTests
File.WriteAllBytes(payloadPath, bytes);
try
{
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, gameUnderTest, signingKey);
_ = InputValidation.ValidateStagedRetroWfcPayloadDirectory(payloadRoot, signingKey);
throw new Exception("A payload with a malformed header was accepted.");
}
catch (InvalidDataException) { }
@@ -1324,10 +1073,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 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.
// 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.
var components = ToolkitFingerprint.ComputeComponents(root);
if (components.Compile != afterPatch)
throw new Exception("ComputeComponents disagrees with the compile identity.");
@@ -1338,36 +1087,15 @@ 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.");
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\"");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"),
"int main3();");
var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
if (afterNewRuntime.Translation != indexed.Translation)
throw new Exception("A runtime source change with unchanged registrations changed the translation identity.");
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
if (afterNewRuntime.Translation == afterNewTranslator.Translation)
throw new Exception("A runtime source change did not change the translation identity.");
if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain)
throw new Exception("A runtime source change needlessly changed the native toolchain identity.");
if (afterNewRuntime.Compile == indexed.Compile)
if (afterNewRuntime.Compile == afterNewTranslator.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);
@@ -1420,34 +1148,13 @@ 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"));
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "CMake", "bin"));
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Ninja"));
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "llvm-mingw", "bin"));
foreach (var project in new[] { "mkwii", "mkwii-ntsc-u", "mkwii-ntsc-j" })
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "projects", project));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "projects", "mkwii"));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "src"));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2",
"wc24"));
@@ -1474,8 +1181,7 @@ internal static class SelfTests
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags");
foreach (var project in new[] { "mkwii", "mkwii-ntsc-u", "mkwii-ntsc-j" })
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", project, "recomp.yml"), "profiles: {}");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"), "int main();");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2", "wc24",
@@ -1,7 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
@@ -14,42 +12,32 @@ 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;
// v3 replaced it with the native index extracted from it, whenever that index is current.
private const string Version = "mkwc-toolkit-compile-v7";
private const string TranslationVersion = "mkwc-toolkit-translation-v4";
// translation v2 added runtime/src, which the translator regex-scans for native overrides.
private const string Version = "mkwc-toolkit-compile-v6";
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
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";
private const string RuntimeAssetsDescription = "The product runtime assets";
private static readonly string[] SourceExtensions =
[".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".inl", ".inc", ".s", ".asm",
[".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".inl", ".s", ".asm",
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
/// <summary>
/// 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.
/// 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.
/// </summary>
private static readonly string[] TranslationPrefixes =
[
InstalledLayout.ToolkitEntryPrefix + "Translator/",
InstalledLayout.WorkspaceEntryPrefix + "projects/"
InstalledLayout.WorkspaceEntryPrefix + "projects/",
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
];
private static readonly string[] TranslationFiles =
[
InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1",
// Maps the PAL-keyed native index to the installed release's addresses.
InstalledLayout.WorkspaceEntryPrefix + "runtime/include/guest_abi/guest_versions.inc"
];
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"];
[InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"];
/// <summary>
/// Inputs that make compiled objects unsafe to reuse: the compiler/CMake/Ninja toolchain, the
@@ -120,16 +108,14 @@ 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),
BuildIdentity(TranslationVersion, translation),
BuildIdentity(NativeToolchainVersion,
SelectSubset(entries, NativeToolchainPrefixes, NativeToolchainFiles)));
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
NativeToolchainFiles));
}
private static SortedDictionary<string, string> SelectSubset(SortedDictionary<string, string> entries,
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
string[] prefixes, string[] files)
{
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
@@ -139,61 +125,9 @@ internal static class ToolkitFingerprint
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
subset[relative] = hash;
}
return subset;
return BuildIdentity(version, 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
@@ -339,7 +273,8 @@ 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 Sha256Hex(builder.ToString());
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
.ToLowerInvariant();
}
}
@@ -15,6 +15,5 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
<Using Include="WiiCompiled.Setup.Common" />
</ItemGroup>
</Project>
+11 -32
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), 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.
# 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.
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,8 +149,6 @@ 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
@@ -178,29 +176,7 @@ find "$appdir/workspace" -name .git -prune -exec rm -rf {} +
# is a plain developer build directory.
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$appdir/workspace/Launcher/"
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
# index, so it is extracted from exactly the bundled runtime sources.
echo "Extracting the runtime native index..."
"$appdir/usr/bin/translator-cli" emit-native-index \
--native-source-dir "$appdir/workspace/runtime/src" \
--out "$appdir/workspace/runtime/native-index.json"
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
# A bundled runtime that fingerprints differently from the shipped archive would make every
# installation silently compile the runtime from source.
if [[ "$appimagetool_arch" == x86_64 ]]; then
staged_runtime_fingerprint=$("$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch/bin/cmake" \
-DMKW_RUNTIME_DIR="$appdir/workspace/runtime" -P "$appdir/workspace/runtime/cmake/RuntimeFingerprint.cmake" |
sed -n 's/^runtime_fingerprint=//p')
packaged_runtime_fingerprint=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("RuntimeSourceFingerprint", ""))' \
"$appdir/native-prebuilt/provenance.json")
if [[ -z "$staged_runtime_fingerprint" || "$staged_runtime_fingerprint" != "$packaged_runtime_fingerprint" ]]; then
echo "build-appimage.sh: the bundled runtime ($staged_runtime_fingerprint) does not match the precompiled runtime archive ($packaged_runtime_fingerprint)" >&2
exit 1
fi
fi
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
# 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
@@ -230,9 +206,12 @@ 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
# 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
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"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
-211
View File
@@ -1,211 +0,0 @@
# Shared incremental-build and release-selection logic for the Unix setups (Linux AppImage and
# macOS), sourced by local-build.sh, local-build-macos.command, macos/extract-disc.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")"
printf 'regions %s\n' "$(mkw_digest_path "$workspace/runtime/include/guest_abi/guest_versions.inc")"
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
}
# Prints one fact a projects/*/recomp.yml pins: game_id, region, dol, rel or retro_wfc_payload.
# Literal line matching against the fixed shape, like NativeBuildFlags.ps1's Get-MkwProjectPins.
# $1 = recomp.yml, $2 = fact.
mkw_project_pin() {
awk -v want="$2" '
{ sub(/\r$/, ""); sub(/#.*/, "") }
/^[A-Za-z0-9_]+:/ { section = $1; sub(/:.*/, "", section); input = ""; next }
section == "inputs" && /^ [A-Za-z0-9_]+:[[:space:]]*$/ { input = $1; sub(/:$/, "", input); next }
section == "project" && $1 == (want ":") { print $2; exit }
section == "inputs" && input == want && $1 == "sha256:" { print tolower($2); exit }
want == "retro_wfc_payload" && $1 == "retro_wfc_payload:" { print $2; exit }
' "$1"
}
# Prints the recomp.yml of the supported release whose game id is $2. $1 = workspace.
mkw_project_for_game() {
local project
for project in "$1"/projects/*/recomp.yml; do
if [[ -f "$project" && "$(mkw_project_pin "$project" game_id)" == "$2" ]]; then
printf '%s\n' "$project"
return 0
fi
done
return 1
}
# Prints the recomp.yml pinned to $2/main.dol after checking StaticR.rel and any extracted disc
# header against it, like NativeBuildFlags.ps1's Resolve-MkwProjectForAssets.
# $1 = workspace, $2 = assets directory.
mkw_resolve_project() {
local project dol_sha game_id disc_id
dol_sha=$(mkw_sha256 "$2/main.dol")
for project in "$1"/projects/*/recomp.yml; do
[[ -f "$project" && "$(mkw_project_pin "$project" dol)" == "$dol_sha" ]] || continue
game_id=$(mkw_project_pin "$project" game_id)
if [[ "$(mkw_sha256 "$2/StaticR.rel")" != "$(mkw_project_pin "$project" rel)" ]]; then
printf 'StaticR.rel does not match the clean %s release of main.dol.\n' "$game_id" >&2
return 1
fi
if [[ -f "$2/DATA/sys/boot.bin" ]]; then
disc_id=$(head -c 6 "$2/DATA/sys/boot.bin")
if [[ "$disc_id" != "$game_id" ]]; then
printf 'The extracted game data is %s but main.dol is %s; extract the game again.\n' "$disc_id" "$game_id" >&2
return 1
fi
fi
printf '%s\n' "$project"
return 0
done
printf 'main.dol (sha256 %s) is not a supported clean Mario Kart Wii release.\n' "$dol_sha" >&2
return 1
}
# The offline file name the translator expects for a release's Retro-WFC payload. $1 = recomp.yml.
mkw_retro_wfc_payload_file() {
local uri
uri=$(mkw_project_pin "$1" retro_wfc_payload)
[[ -n "$uri" ]] || return 1
printf 'binary/payload.%s.bin\n' "${uri##*g=}"
}
# $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"
}
+19 -61
View File
@@ -4,12 +4,11 @@ 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'
@@ -19,24 +18,21 @@ Usage: local-build-macos.command --output-dir DIR [options]
--profile {base|retro-rewind|both} Product to build (default: base)
--output-dir DIR Output .app directory (required; Retro Rewind for both)
--base-output-dir DIR Base .app directory (required with --profile both)
--game IMAGE --nodtool PATH Extract and verify a clean supported disc image first
--game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing the release's binary/payload.<game>.bin
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
--force-clean-build Delete local generated and current-architecture native build caches
--force-clean-build Delete local generated and native-build-macos caches
--parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
--translator-bin PATH Use a self-contained Translator.Cli executable
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
EOF
}
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
setup_version=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
@@ -54,21 +50,14 @@ while (($#)); do
--ninja) ninja_bin=${2:-}; shift 2 ;;
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
--translator-bin) translator_bin=${2:-}; shift 2 ;;
--setup-version) setup_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
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
@@ -82,15 +71,12 @@ if (( builds_retro )); then
fi
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
assets="$workspace/Assets"; generated="$workspace/generated"
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
native_build="$workspace/native-build-macos-$macos_arch"
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --workspace "$workspace" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
# The translation project is the one pinned to the extracted main.dol, which decides the region.
project=$(mkw_resolve_project "$workspace" "$assets") || fail 'the extracted game cannot be translated'
region=$(mkw_project_pin "$project" region); [[ -n "$region" ]] || fail "could not find the region of $project"
if (( force_clean )); then
step force-clean 'Discarding translation and native build caches'
@@ -129,33 +115,14 @@ if (( builds_retro )); then
fi
fi
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 "$region"
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
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
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 "$region" --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else payload_file=$(mkw_retro_wfc_payload_file "$project") || fail "$project pins no Retro-WFC payload"; offline_payload="$retro_wfc/$payload_file"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
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
step translate-mod 'Translating Retro Rewind'; translator "${args[@]}"
fi
step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project"
@@ -163,19 +130,10 @@ 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 -DMKW_SETUP_VERSION="$setup_version"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
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
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+20 -67
View File
@@ -39,7 +39,7 @@ require_command() {
}
sha256_of() {
mkw_sha256 "$1"
sha256sum "$1" | awk '{print $1}'
}
# ---------------------------------------------------------------------------
@@ -47,8 +47,6 @@ 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=""
@@ -68,8 +66,6 @@ translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
setup_version=""
x86_cpu_profile=auto
usage() {
cat <<'EOF'
@@ -94,10 +90,6 @@ Usage: local-build.sh --output-dir DIR [options]
skips compiling aurora-main from source entirely
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
standard headers/startup files
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
--x86-cpu-profile {auto|v2|v3} x86-64 instruction set to build for (default: auto, v3 when
this machine supports it)
EOF
}
@@ -122,27 +114,16 @@ while [[ $# -gt 0 ]]; do
--translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
--setup-version) setup_version=$2; shift 2 ;;
--x86-cpu-profile) x86_cpu_profile=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
done
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
fi
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail "--setup-version contains unsupported characters"
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
case "$profile" in
base|retro-rewind|both) ;;
*) fail "--profile must be base, retro-rewind, or both" ;;
esac
case "$x86_cpu_profile" in
auto|v2|v3) ;;
*) fail "--x86-cpu-profile must be auto, v2, or v3" ;;
esac
builds_retro=0
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
@@ -192,22 +173,7 @@ if [[ -n "$native_prebuilt_dir" ]]; then
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
fi
host_supports_x86_64_v3() {
local macros macro
macros=$("$cc_bin" -march=native -dM -E -x c /dev/null 2>/dev/null) || return 1
for macro in __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__; do
grep -q "^#define $macro " <<<"$macros" || return 1
done
}
if [[ "$x86_cpu_profile" == "auto" ]]; then
x86_cpu_profile=v3
if [[ "$(uname -m)" == "x86_64" ]] && ! host_supports_x86_64_v3; then
x86_cpu_profile=v2
echo "MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2"
fi
fi
project=$workspace/projects/mkwii/recomp.yml
assets=$workspace/Assets
generated=$workspace/generated
functions=$generated/functions
@@ -220,12 +186,9 @@ translation_provenance=$generated/translation-provenance.json
toolchain_provenance=$build/toolchain-provenance.json
retro_root=${retro_rewind_package_dir:-$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6}
assert_file "$project" "Translation project"
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
# The translation project is the one pinned to the extracted main.dol, which decides the region.
project=$(mkw_resolve_project "$workspace" "$assets") || fail "the extracted game cannot be translated"
region=$(mkw_project_pin "$project" region)
[[ -n "$region" ]] || fail "Could not find the region of $project"
# The AppImage bundles Clang, but Linux startup objects and the C/C++ link runtimes
# still come from the host. Check them before the expensive translation so a missing
@@ -303,9 +266,10 @@ else
fi
# ---------------------------------------------------------------------------
# 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.
# 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.
# ---------------------------------------------------------------------------
if (( force_clean_build )); then
@@ -313,12 +277,13 @@ if (( force_clean_build )); then
rm -rf "$generated" "$base_manifest_dir" "$build"
fi
translator_identity_input=${translator_bin:-$(dirname "$translator_dll")}
translation_fingerprint=$(mkw_translation_identity "$workspace" "$translator_identity_input" "$script_dir/local-build.sh")
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
reuse_base=0
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" \
&& -f "$base_metadata" && -f "$base_manifest" ]]; then
reuse_base=1
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
fi
if (( builds_retro )); then
@@ -364,9 +329,10 @@ else
log_step emit-base-manifest "Creating the local base translation manifest"
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region "$region"
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
> "$translation_provenance"
fi
if (( builds_retro )); then
@@ -376,13 +342,12 @@ if (( builds_retro )); then
translate_mod_args=(translate-mod --project "$project" --profile retro-rewind
--base-manifest "$base_manifest" --base-translation-output-metadata "$base_metadata"
--code-pul "$code_pul" --mod-root "$retro_root" --mod-name "Retro Rewind"
--region "$region" --out "$retro_out" --prefer-cached-inputs --emit-cpp
--region P --out "$retro_out" --prefer-cached-inputs --emit-cpp
--threads "$translator_threads")
if (( skip_retro_wfc_payload )); then
translate_mod_args+=(--skip-retro-wfc)
else
payload_file=$(mkw_retro_wfc_payload_file "$project") || fail "$project pins no Retro-WFC payload"
offline_payload=$retro_wfc_offline_dir/$payload_file
offline_payload=$retro_wfc_offline_dir/binary/payload.RMCPD00.bin
assert_file "$offline_payload" "Offline Retro-WFC shared payload"
translate_mod_args+=(--retro-wfc-payload "$offline_payload")
fi
@@ -442,13 +407,6 @@ 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"
@@ -460,9 +418,7 @@ 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_SETUP_VERSION="$setup_version"
-DMKW_X86_CPU_PROFILE="$x86_cpu_profile")
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
# 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
@@ -481,9 +437,7 @@ if [[ "$cc_bin" == */* ]]; then
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
fi
if [[ -n "$fuse_ld_override" ]]; then
# 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")
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
fi
if [[ -n "$native_prebuilt_dir" ]]; then
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
@@ -499,7 +453,6 @@ 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) ;;
+20 -88
View File
@@ -9,97 +9,45 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() {
cat <<'EOF'
Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS artifacts for both arm64 and
x86_64. The setup package selects native tools for its host while the game is
compiled locally for that host architecture. CMake must be universal2. The
resulting pkg is unsigned unless
--installer-identity is supplied; releases should sign and notarize it with a
Developer ID Installer certificate.
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
is unsigned unless --installer-identity is supplied; releases should sign and
notarize it with a Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: project version, or 0.1.0)
--version VERSION Bundle/package version (default: 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
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
--nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
--translator) translator=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--ninja) ninja=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; version_supplied=true; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
version=${version#v}
if [[ "$version_supplied" == false ]]; then
local_csproj="$workspace/Launcher/Directory.Build.props"
if [[ -f "$local_csproj" ]]; then
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
if [[ -n "$detected" ]]; then
version="$detected"
fi
fi
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
[[ -x "$translator" ]] || fail '--translator must name an executable'
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
require_arch() {
local artifact=$1 arch=$2 label=$3
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
}
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
# Slice checks above prevent accidental cross-architecture packaging. Exercise
# each supplied executable as well: an incorrectly bundled runtime can have a
# valid Mach-O header but still fail before the setup app can use it. Apple
# Silicon maintainers validate Intel tools through Rosetta when it is present.
host_arch=$(uname -m)
run_for_arch() {
local arch=$1 label=$2
shift 2
if [[ "$arch" == "$host_arch" ]]; then
"$@" >/dev/null || fail "$label did not run successfully"
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
else
# Intel hosts cannot execute arm64 binaries. The slice remains checked
# above; CI or an Apple Silicon maintainer must execute that tool set.
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
"$label" "$host_arch" "$arch" >&2
fi
}
for arch in arm64 x86_64; do
if [[ "$arch" == arm64 ]]; then
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
else
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
fi
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
run_for_arch "$arch" "--translator-$arch" "$translator" --help
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
done
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
@@ -116,7 +64,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
</dict></plist>
EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -148,36 +96,20 @@ copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$re
mkdir -p "$resources/workspace"
for source in aurora-main projects runtime translator; do
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
[[ "$source" != aurora-main || -f "$workspace/$source/CMakeLists.txt" ]] ||
fail "aurora-main is empty; run git submodule update --init --recursive"
copy_clean "$workspace/$source" "$resources/workspace/$source"
done
# Setup bundles contain materialized sources and must work without Git.
find "$resources/workspace" -name .git -prune -exec rm -rf {} +
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
# index, so it is extracted from exactly the packaged runtime sources.
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
"$host_translator" emit-native-index --native-source-dir "$resources/workspace/runtime/src" \
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
mkdir -p "$resources/workspace/Launcher/macos"
copy_clean "$workspace/Launcher/Directory.Build.props" "$resources/workspace/Launcher/Directory.Build.props"
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
copy_clean "$workspace/Launcher/build-cache.sh" "$resources/workspace/Launcher/build-cache.sh"
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
# setup.command uses this marker to update source inputs in an existing user
# workspace without replacing extracted game assets or Retro Rewind files.
printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
mkdir -p "$resources/tools/cmake"
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
+14 -41
View File
@@ -1,35 +1,30 @@
#!/usr/bin/env bash
# Extract a user-owned Mario Kart Wii disc image of a supported release for a local build.
# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build.
# This script deliberately contains no game data and is intended for the macOS
# setup application and for maintainers testing that setup path.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../build-cache.sh
source "$script_dir/../build-cache.sh"
readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05
readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d
fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; }
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
usage() {
cat <<'EOF'
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH [--workspace DIR]
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH
Extracts a user-owned Mario Kart Wii image into DIR. The image must be a release
a projects/*/recomp.yml in the workspace (default: this script's repository)
pins. The final layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing
data is left untouched unless the complete new extraction passes both content
hash checks.
Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final
layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left
untouched unless the complete new extraction passes both content hash checks.
EOF
}
game=""
assets_dir=""
nodtool=""
workspace=$(cd "$script_dir/../.." && pwd)
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--game) game=${2:-}; shift 2 ;;
--assets-dir) assets_dir=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
@@ -43,30 +38,12 @@ done
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
# The disc's game ID selects the release, so an existing extraction is only reused for the same one.
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
disc_info=$("$nodtool" info "$game") || fail 'nodtool could not read the disc image'
game_id=$(awk '/^Game ID: / { print $3; exit }' <<<"$disc_info")
[[ -n "$game_id" ]] || fail 'nodtool did not report the disc game ID'
project=$(mkw_project_for_game "$workspace" "$game_id") || fail "disc $game_id is not a supported Mario Kart Wii release"
expected_dol_sha256=$(mkw_project_pin "$project" dol)
expected_rel_sha256=$(mkw_project_pin "$project" rel)
[[ -n "$expected_dol_sha256" && -n "$expected_rel_sha256" ]] || fail "$project does not pin the clean $game_id digests"
# 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
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
"$nodtool" info "$game" >/dev/null
printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n'
"$nodtool" extract "$game" "$scratch/extracted"
@@ -74,10 +51,8 @@ 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'
dol_sha=$(sha256 "$dol")
rel_sha=$(sha256 "$rel")
[[ $dol_sha == "$expected_dol_sha256" ]] || fail "main.dol does not match the clean $game_id image (expected $expected_dol_sha256, got $dol_sha)"
[[ $rel_sha == "$expected_rel_sha256" ]] || fail "StaticR.rel does not match the clean $game_id image (expected $expected_rel_sha256, got $rel_sha)"
[[ $(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'
data_root=$(dirname "$(dirname "$dol")")
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
@@ -89,20 +64,18 @@ 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/.data-manifest" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
mkdir -p "$backup"
for item in DATA .data-manifest main.dol StaticR.rel; do
for item in DATA 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"
rm -rf "$backup"
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean %s game assets\n' "$game_id"
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n'
@@ -1,11 +0,0 @@
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
# Pass this file on the first configure with:
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
#
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
# x86_64 artifacts.
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
"Target macOS architectures" FORCE)
+14 -93
View File
@@ -5,36 +5,28 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process.
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF
}
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
build_dir=""; product=""; output_dir=""
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
@@ -55,7 +47,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
@@ -65,96 +57,25 @@ for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset"
done
# Expand each image's rpaths before inheriting them, so @loader_path stays
# relative to the image that declared it rather than a descendant library.
expanded_rpaths() {
local target=$1 rpath
while IFS= read -r rpath; do
case "$rpath" in
@loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;;
@loader_path) rpath="$(dirname "$target")" ;;
@executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;;
@executable_path) rpath="$build_dir" ;;
esac
printf '%s\n' "$rpath"
done < <(otool -l "$target" | awk '
/LC_RPATH/ { rpath = 1; next }
rpath && /^[[:space:]]*path / {
sub(/^[[:space:]]*path[[:space:]]+/, "");
sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, "");
print;
rpath = 0;
}')
}
# Resolve a non-system dependency using the current image's rpaths followed
# by the inherited loader stack, matching dyld's dependency-chain search.
dependency_path() {
local current=$1 dependency=$2 search_rpaths=$3 rpath candidate
case "$dependency" in
/System/Library/*|/usr/lib/*)
return 1
;;
/*)
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
;;
@loader_path/*)
candidate="$(dirname "$current")/${dependency#@loader_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@executable_path/*)
candidate="$build_dir/${dependency#@executable_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@rpath/*|*.dylib)
local subpath
if [[ "$dependency" == @rpath/* ]]; then
subpath="${dependency#@rpath/}"
else
subpath="$dependency"
fi
while IFS= read -r rpath; do
[[ -n "$rpath" ]] || continue
candidate="$rpath/$subpath"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
done <<< "$search_rpaths"
;;
esac
return 1
}
# Build a closure of non-system dylibs. System libraries remain system
# references, while every resolved dependency is copied beside the executable.
queue=("$build_dir/$product")
queue_rpaths=("")
# Build a closure of Homebrew dylibs. System libraries remain system references.
queue=("$macos/$product")
while ((${#queue[@]})); do
current=${queue[0]}
inherited_rpaths=${queue_rpaths[0]}
queue=("${queue[@]:1}")
queue_rpaths=("${queue_rpaths[@]:1}")
current_rpaths=$(expanded_rpaths "$current")
if [[ -n "$inherited_rpaths" ]]; then
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
fi
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
while IFS= read -r dependency; do
[[ -z "$dependency" ]] && continue
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
case "$dependency" in
/System/Library/*|/usr/lib/*) continue ;;
esac
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
name=$(basename "$dep_path")
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dep_path" "$frameworks/$name"
ditto "$dependency" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
queue+=("$frameworks/$name")
fi
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done
while IFS= read -r binary; do
while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then
@@ -162,7 +83,7 @@ while IFS= read -r binary; do
else
install_name_tool -change "$old" "@loader_path/$name" "$binary"
fi
done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} +
+19 -65
View File
@@ -5,19 +5,16 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace"
products="$support_root/Products"
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
notice() {
/usr/bin/osascript - "$1" >/dev/null <<'APPLESCRIPT'
on run argv
display dialog (item 1 of argv) buttons {"OK"} default button "OK" with icon caution
end run
APPLESCRIPT
}
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
usage() {
cat <<'EOF'
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
@@ -39,16 +36,9 @@ while (($#)); do
done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
ninja_bin="$host_tools/ninja"
if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii disc image"
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
POSIX path of selectedFile
APPLESCRIPT
) || exit 0
@@ -70,58 +60,26 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true
exit 1
fi
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
fail "the packaged $(basename "$tool") does not support $host_arch"
done
mkdir -p "$support_root" "$products"
source_bundle_version="$workspace_source/.bundle-version"
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
printf 'Preparing the local build workspace...\n'
if [[ ! -d "$workspace" ]]; then
/usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place; unchanged files
# keep their timestamps so the incremental native build survives.
# shellcheck source=../build-cache.sh
source "$workspace_source/Launcher/build-cache.sh"
mkw_sync_tree "$workspace_source" "$workspace" aurora-main projects runtime translator Launcher
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
fi
rm -rf "$workspace"
/usr/bin/ditto "$workspace_source" "$workspace"
fi
# The extracted disc decides the release, and with it the region's Retro-WFC payload.
# shellcheck source=../build-cache.sh
source "$workspace/Launcher/build-cache.sh"
"$workspace/Launcher/macos/extract-disc.command" --workspace "$workspace" --game "$game" \
--assets-dir "$workspace/Assets" --nodtool "$nodtool"
project=$(mkw_resolve_project "$workspace" "$workspace/Assets") || fail 'the extracted game cannot be built'
profile=base
build_args=(--workspace "$workspace" --output-dir "$products")
build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products")
if [[ -n "$retro_dir" ]]; then
profile=both
# Online play needs the shared Retro-WFC payload. Keep it in the per-user
# support directory rather than the packaged app or build workspace, then
# verify its pinned signature before publishing it into the local cache.
game_id=$(mkw_project_pin "$project" game_id)
payload_uri=$(mkw_project_pin "$project" retro_wfc_payload)
payload_file=$(mkw_retro_wfc_payload_file "$project") || fail "$project pins no Retro-WFC payload"
retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/$payload_file"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
cached_payload_valid=0
if [[ -f "$retro_wfc_payload" ]]; then
if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir" --game-id "$game_id"; then
if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
cached_payload_valid=1
else
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
@@ -135,14 +93,15 @@ if [[ -n "$retro_dir" ]]; then
printf 'Downloading the current Retro-WFC payload needed for online play...\n'
mkdir -p "$retro_wfc_dir"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/$payload_file"
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT
if /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" "$payload_uri"; then
"$translator" validate-retro-wfc-payload --directory "$payload_stage" --game-id "$game_id" || \
--retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$(dirname "$retro_wfc_payload")"
mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT
@@ -158,7 +117,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"
@@ -184,13 +143,8 @@ install_app() {
local app=$1
[[ -d "$products/$app" ]] || return 0
if [[ "$destination" == /Applications ]]; then
/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
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
else
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
fi
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env bash
# Exercise a real dyld dependency chain before and after app packaging.
set -euo pipefail
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
trap 'rm -rf "$test_root"' EXIT
build_dir="$test_root/build with spaces"
output_dir="$test_root/output with spaces"
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
cat > "$test_root/c.c" <<'EOF'
int value_c(void) { return 7; }
EOF
cat > "$test_root/b.c" <<'EOF'
extern int value_c(void);
int value_b(void) { return 2 * value_c(); }
EOF
cat > "$test_root/a.c" <<'EOF'
extern int value_b(void);
int value_a(void) { return 1 + value_b(); }
EOF
cat > "$test_root/main.c" <<'EOF'
#include <stdio.h>
extern int value_a(void);
int main(void) { printf("%d\n", value_a()); return 0; }
EOF
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
-L "$build_dir/A/c" -lC \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
-L "$build_dir/A/b" -lB \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
-L "$build_dir/A" -lA \
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
# B has no rpaths: its C dependency must inherit A's loader-relative path.
[[ $("$build_dir/WiiCompiled") == 15 ]]
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
: > "$build_dir/$asset"
done
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
for name in libA.dylib libB.dylib libC.dylib; do
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
done
# Remove access to the original paths and relocate the app before executing.
mv "$build_dir" "$test_root/hidden build"
mkdir "$test_root/relocated app"
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
printf 'publish-app dependency-chain test passed\n'
+3 -23
View File
@@ -11,7 +11,6 @@
<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>
@@ -53,16 +52,12 @@ Play at several times the console's resolution.
**Music ducking.**
Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd
rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
On macOS 14.2 or later, this detects other apps with active audio output and excludes
the game's own audio. Apps that keep an output stream running silently can keep
game music muted even when nothing is audible.
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
**An in-game settings bar.**
Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- MetalFX spatial upscaling on supported macOS GPUs
- Controller assignment for all four ports
- Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
@@ -101,8 +96,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- macOS 14 (Sonoma) or later on Apple Silicon
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -123,21 +118,6 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION]
> Only take builds from this repository's
File diff suppressed because it is too large Load Diff
-150
View File
@@ -1,150 +0,0 @@
# RMCJ01 addresses for the PAL (RMCP01) data globals named by the WiiCompiled runtime.
# Derived from projects/mkwii-ntsc-u/data_addresses.txt and confirmed against the RMCJ01 binaries.
# Format: <PAL hex> <target hex> <verdict and evidence>
# PROVEN - the instruction at the ported reference site forms exactly this address,
# and the independent chunk-table port of the PAL address agrees.
# CALIBRATED - the reference site reproduces the known reference-disc address for this
# global, so its decode in this region is trusted even where the chunk
# table has no mapping.
# MATCHED - the reference site's instruction window, with the fields that legitimately
# differ between regions masked out, occurs exactly once in this region's
# code; the displacement was read from that instruction. Used where the
# chunk table is not instruction-exact inside a function.
# FIELD - uniquely matched PAL base and field instructions, with the base register
# preserved and the target's own displacement decoded (including REL relocations).
# RECORD - a verified table base, target indexing stride and matching immutable records.
# SECTION - a section boundary, taken from the region section table.
# CHUNK-ONLY - chunk-table port only; no reference site formed exactly this address.
# Small-data bases: RMCJ01 r13=0x8038C580 r2=0x8038E920.
# Totals: 115 confirmed, 16 chunk-only, 0 unresolved.
80344090 80343A10 PROVEN @80172D38 (calibrated on P@80172E18) forms 0x80343A10 (chunk port agrees)
80344094 80343A14 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
80344098 80343A18 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
803440A4 80343A24 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
803440A8 80343A28 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
803440AC 80343A2C CHUNK-ONLY (no reference site confirmed it: no-reference-site)
80344110 80343A90 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
80344710 80344090 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
803867D8 80386158 PROVEN @8016EA44 (calibrated on P@8016EB24) forms 0x80386158 (chunk port agrees)
803886C8 80388048 PROVEN @8016B794 (calibrated on P@8016B874) forms 0x80388048 (chunk port agrees)
80343230 80342BB0 PROVEN @801634E0 (calibrated on P@801635C0) forms 0x80342BB0 (chunk port agrees)
803434E0 80342E60 PROVEN @801648A0 (calibrated on P@80164980) forms 0x80342E60 (chunk port agrees)
80386664 80385FE4 PROVEN @80162B34 (calibrated on P@80162C14) forms 0x80385FE4 (chunk port agrees)
80386668 80385FE8 PROVEN @80162B78 (calibrated on P@80162C58) forms 0x80385FE8 (chunk port agrees)
80386670 80385FF0 PROVEN @801633B0 (calibrated on P@80163490) forms 0x80385FF0 (chunk port agrees)
8038667C 80385FFC PROVEN @80163018 (calibrated on P@801630F8) forms 0x80385FFC (chunk port agrees)
803866A8 80386028 PROVEN @801633A0 (calibrated on P@80163480) forms 0x80386028 (chunk port agrees)
803866F0 80386070 PROVEN @80162B2C (calibrated on P@80162C0C) forms 0x80386070 (chunk port agrees)
80386720 803860A0 PROVEN @80164908 (calibrated on P@801649E8) forms 0x803860A0 (chunk port agrees)
80386724 803860A4 PROVEN @80164890 (calibrated on P@80164970) forms 0x803860A4 (chunk port agrees)
80386725 803860A5 PROVEN @80164784 (calibrated on P@80164864) forms 0x803860A5 (chunk port agrees)
803866A0 80386020 PROVEN @8015E950 (calibrated on P@8015EA30) forms 0x80386020 (chunk port agrees)
80346D20 803466A0 PROVEN @8019E794 (calibrated on P@8019E874) forms 0x803466A0 (chunk port agrees)
80386848 803861C8 PROVEN @8019E0CC (calibrated on P@8019E1AC) forms 0x803861C8 (chunk port agrees)
80350864 803501E4 CHUNK-ONLY (no reference site confirmed it: 801B95EC forms 0x803501E0)
80350866 803501E6 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
80350872 803501F2 CHUNK-ONLY (no reference site confirmed it: 801B96CC forms 0x803501E0)
8035087C 803501FC CHUNK-ONLY (no reference site confirmed it: no-reference-site)
80350890 80350210 CHUNK-ONLY (no reference site confirmed it: 801BA9F4 forms 0x803501E0)
80386B38 803864B8 PROVEN @801B93D8 (calibrated on P@801B94B8) forms 0x803864B8 (chunk port agrees)
80386BA8 80386528 PROVEN @801BAC0C (calibrated on P@801BACEC) forms 0x80386528 (chunk port agrees)
80386BE4 80386564 PROVEN @801B9928 (calibrated on P@801B9A08) forms 0x80386564 (chunk port agrees)
80386B44 803864C4 PROVEN @801B98C4 (calibrated on P@801B99A4) forms 0x803864C4 (chunk port agrees)
80386BB8 80386538 PROVEN @801B902C (calibrated on P@801B910C) forms 0x80386538 (chunk port agrees)
80386BB4 80386534 PROVEN @801B9070 (calibrated on P@801B9150) forms 0x80386534 (chunk port agrees)
80386BA0 80386520 PROVEN @801BAA44 (calibrated on P@801BAB24) forms 0x80386520 (chunk port agrees)
80386BC0 80386540 PROVEN @801B992C (calibrated on P@801B9A0C) forms 0x80386540 (chunk port agrees)
80386F60 803868E0 PROVEN @80008E74 (calibrated on P@80008F18) forms 0x803868E0 (chunk port agrees)
80244DE0 80244D00 SECTION .ctors start (from the region section table)
80244EA0 80244DC0 SECTION .dtors start (from the region section table)
8088F400 8088EA6C SECTION .ctors start (from the region section table)
8088F704 8088ED70 SECTION .dtors start (from the region section table)
80343740 803430C0 CHUNK-ONLY (no reference site confirmed it: no-reference-site)
803437C0 80343140 CHUNK-ONLY (no reference site confirmed it: 8016B794 forms 0x80388048)
80343DC0 80343740 PROVEN @8016D0EC (calibrated on P@8016D1CC) forms 0x80343740 (chunk port agrees)
80343DE4 80343764 PROVEN @8016D0D4 (calibrated on P@8016D1B4) forms 0x80343764 (chunk port agrees)
803867B0 80386130 PROVEN @8016D100 (calibrated on P@8016D1E0) forms 0x80386130 (chunk port agrees)
803867B1 80386131 PROVEN @8016D104 (calibrated on P@8016D1E4) forms 0x80386131 (chunk port agrees)
803867C0 80386140 PROVEN @8016D0DC (calibrated on P@8016D1BC) forms 0x80386140 (chunk port agrees)
803867C4 80386144 PROVEN @8016D0D0 (calibrated on P@8016D1B0) forms 0x80386144 (chunk port agrees)
803867D0 80386150 PROVEN @8016ED60 (calibrated on P@8016EE40) forms 0x80386150 (chunk port agrees)
803477B0 80347130 PROVEN @801A9970 (calibrated on P@801A9A50) forms 0x80347130 (chunk port agrees)
80386918 80386298 PROVEN @801A97A0 (calibrated on P@801A9880) forms 0x80386298 (chunk port agrees)
8038691C 8038629C PROVEN @801A96D8 (calibrated on P@801A97B8) forms 0x8038629C (chunk port agrees)
80386920 803862A0 PROVEN @801A96D0 (calibrated on P@801A97B0) forms 0x803862A0 (chunk port agrees)
803869E0 80386360 PROVEN @801B2350 (calibrated on P@801B2430) forms 0x80386360 (chunk port agrees)
80386000 80385980 PROVEN @8000A540 (calibrated on P@8000A5E4) forms 0x80385980 (chunk port agrees)
80386F14 80386894 PROVEN @8023E470 (calibrated on P@8023E550) forms 0x80386894 (chunk port agrees)
80386F20 803868A0 PROVEN @8023E624 (calibrated on P@8023E704) forms 0x803868A0 (chunk port agrees)
80386F24 803868A4 CHUNK-ONLY (no reference site confirmed it: 8023E624 forms 0x803868A0)
80388B80 80388500 PROVEN @8021B448 (calibrated on P@8021B528) forms 0x80388500 (chunk port agrees)
80388BA0 80388520 PROVEN @8021B4D8 (calibrated on P@8021B5B8) forms 0x80388520 (chunk port agrees)
80388BC0 80388540 PROVEN @8021B548 (calibrated on P@8021B628) forms 0x80388540 (chunk port agrees)
80388BE0 80388560 PROVEN @8021B540 (calibrated on P@8021B620) forms 0x80388560 (chunk port agrees)
802574A0 80256E40 FIELD base @8021B230 -> 0x80256E20; field @8021B298 word 389F0020 (unique PAL instruction matches; register r31 preserved)
802574E0 80256E80 FIELD base @8021B230 -> 0x80256E20; field @8021B2A8 word 389F0060 (unique PAL instruction matches; register r31 preserved)
80257520 80256EC0 FIELD base @8021B230 -> 0x80256E20; field @8021B434 word 389F00A0 (unique PAL instruction matches; register r31 preserved)
80257540 80256EE0 FIELD base @8021B230 -> 0x80256E20; field @8021B4C4 word 389F00C0 (unique PAL instruction matches; register r31 preserved)
802A3EE8 802A3888 CALIBRATED @8023E494 (calibrated on P@8023E574) forms 0x802A3888 (chunk port has no mapping)
802A3EF4 802A3894 RECORD table @8023E494 -> 0x802A3888; stride @8023E48C mulli r4,r0,12; both 12-byte records equal PAL
802A3F0C 802A38AC CALIBRATED @8023D250 (calibrated on P@8023D330) forms 0x802A38AC (chunk port has no mapping)
808B4C20 808B3D80 CALIBRATED @805651D0 (calibrated on P@80565850) forms 0x808B3D80 (chunk port has no mapping)
809C1830 809C0890 PROVEN @80564A9C (calibrated on P@8056511C) forms 0x809C0890 (chunk port agrees)
809C183C 809C089C FIELD base @80564BF0 -> 0x809C0890; field @80564C08 word 3865000C (unique PAL instruction matches; register r5 preserved)
809C2898 809C18F8 CALIBRATED @807164A0 (calibrated on P@80716E34) forms 0x809C18F8 (chunk port has no mapping)
8027F820 8027F1C0 CALIBRATED @80126920 (calibrated on P@80126A00) forms 0x8027F1C0 (chunk port has no mapping)
802F81A0 802F7B20 PROVEN @80126910 (calibrated on P@801269F0) forms 0x802F7B20 (chunk port agrees)
802F8200 802F7B80 PROVEN @801268E4 (calibrated on P@801269C4) forms 0x802F7B80 (chunk port agrees)
80386608 80385F88 PROVEN @8015D390 (calibrated on P@8015D470) forms 0x80385F88 (chunk port agrees)
80386610 80385F90 PROVEN @8015D4D0 (calibrated on P@8015D5B0) forms 0x80385F90 (chunk port agrees)
80386614 80385F94 PROVEN @8015D4CC (calibrated on P@8015D5AC) forms 0x80385F94 (chunk port agrees)
80386618 80385F98 PROVEN @8015D3F8 (calibrated on P@8015D4D8) forms 0x80385F98 (chunk port agrees)
8038661C 80385F9C PROVEN @8015D400 (calibrated on P@8015D4E0) forms 0x80385F9C (chunk port agrees)
80386620 80385FA0 PROVEN @8015D404 (calibrated on P@8015D4E4) forms 0x80385FA0 (chunk port agrees)
80386624 80385FA4 PROVEN @8015D3FC (calibrated on P@8015D4DC) forms 0x80385FA4 (chunk port agrees)
80388588 80387F08 PROVEN @8012B7DC (calibrated on P@8012B8BC) forms 0x80387F08 (chunk port agrees)
8038858C 80387F0C PROVEN @8012B7EC (calibrated on P@8012B8CC) forms 0x80387F0C (chunk port agrees)
80386448 80385DC8 PROVEN @80123FEC (calibrated on P@801240CC) forms 0x80385DC8 (chunk port agrees)
8038644C 80385DCC PROVEN @80124198 (calibrated on P@80124278) forms 0x80385DCC (chunk port agrees)
8038647C 80385DFC PROVEN @8012411C (calibrated on P@801241FC) forms 0x80385DFC (chunk port agrees)
80386480 80385E00 PROVEN @80123EC0 (calibrated on P@80123FA0) forms 0x80385E00 (chunk port agrees)
80347130 80346AB0 CHUNK-ONLY (no reference site confirmed it: 801A9E68 forms 0x80346A70)
80347498 80346E18 PROVEN @801A9560 (calibrated on P@801A9640) forms 0x80346E18 (chunk port agrees)
803478B0 80347230 PROVEN @801A9C60 (calibrated on P@801A9D40) forms 0x80347230 (chunk port agrees)
80385AA8 80385428 PROVEN @801A9EA0 (calibrated on P@801A9F80) forms 0x80385428 (chunk port agrees)
80385AE0 80385460 PROVEN @801A94EC (calibrated on P@801A95CC) forms 0x80385460 (chunk port agrees)
803868F8 80386278 PROVEN @801A6518 (calibrated on P@801A65F8) forms 0x80386278 (chunk port agrees)
80386298 80385C18 PROVEN @800BC390 (calibrated on P@800BC470) forms 0x80385C18 (chunk port agrees)
80336278 80335BF8 PROVEN @8013A220 (calibrated on P@8013A300) forms 0x80335BF8 (chunk port agrees)
809BEBA0 809BDC00 FIELD base @80550654 -> 0x809BDB60; field @8055067C word 801E00A0 (unique PAL instruction matches; register r30 preserved)
809BEBA4 809BDC04 FIELD base @80550654 -> 0x809BDB60; field @80550894 word 989E00A4 (unique PAL instruction matches; register r30 preserved)
809BEBA5 809BDC05 FIELD base @80550654 -> 0x809BDB60; field @8055088C word 989E00A5 (unique PAL instruction matches; register r30 preserved)
809BEBA6 809BDC06 FIELD base @80550654 -> 0x809BDB60; field @80550898 word 989E00A6 (unique PAL instruction matches; register r30 preserved)
809BEBA7 809BDC07 FIELD base @80550654 -> 0x809BDB60; field @8055079C word 981E00A7 (unique PAL instruction matches; register r30 preserved)
809BECF0 809BDD50 CALIBRATED @80550620 (calibrated on P@80550CA0) forms 0x809BDD50 (chunk port has no mapping)
8088FDB8 8088F418 CALIBRATED @805296A8 (calibrated on P@80529D28) forms 0x8088F418 (chunk port has no mapping)
80386934 803862B4 PROVEN @801AB7D0 (calibrated on P@801AB8B0) forms 0x803862B4 (chunk port agrees)
80386938 803862B8 PROVEN @801AB7C8 (calibrated on P@801AB8A8) forms 0x803862B8 (chunk port agrees)
8038693C 803862BC PROVEN @801AB7F0 (calibrated on P@801AB8D0) forms 0x803862BC (chunk port agrees)
80386940 803862C0 PROVEN @801AB6C0 (calibrated on P@801AB7A0) forms 0x803862C0 (chunk port agrees)
80386948 803862C8 PROVEN @801AB6A0 (calibrated on P@801AB780) forms 0x803862C8 (chunk port agrees)
80386770 803860F0 PROVEN @80169B04 (calibrated on P@80169BE4) forms 0x803860F0 (chunk port agrees)
80386774 803860F4 PROVEN @80169B34 (calibrated on P@80169C14) forms 0x803860F4 (chunk port agrees)
8038677C 803860FC PROVEN @80169B14 (calibrated on P@80169BF4) forms 0x803860FC (chunk port agrees)
80386780 80386100 PROVEN @80169B1C (calibrated on P@80169BFC) forms 0x80386100 (chunk port agrees)
80386784 80386104 PROVEN @80169BEC (calibrated on P@80169CCC) forms 0x80386104 (chunk port agrees)
803867EC 8038616C PROVEN @80192ED0 (calibrated on P@80192FB0) forms 0x8038616C (chunk port agrees)
803867F0 80386170 PROVEN @80192ECC (calibrated on P@80192FAC) forms 0x80386170 (chunk port agrees)
80385920 803852A0 PROVEN @80169B7C (calibrated on P@80169C5C) forms 0x803852A0 (chunk port agrees)
80385928 803852A8 PROVEN @80169B64 (calibrated on P@80169C44) forms 0x803852A8 (chunk port agrees)
803868A0 80386220 PROVEN @801A0500 (calibrated on P@801A05E0) forms 0x80386220 (chunk port agrees)
80386528 80385EA8 PROVEN @8012686C (calibrated on P@8012694C) forms 0x80385EA8 (chunk port agrees)
80386524 80385EA4 PROVEN @801267C4 (calibrated on P@801268A4) forms 0x80385EA4 (chunk port agrees)
80385800 80385180 PROVEN @80126934 (calibrated on P@80126A14) forms 0x80385180 (chunk port agrees)
80385802 80385182 PROVEN @8012693C (calibrated on P@80126A1C) forms 0x80385182 (chunk port agrees)
80385804 80385184 PROVEN @8012692C (calibrated on P@80126A0C) forms 0x80385184 (chunk port agrees)
80388DD0 80388750 PROVEN @8022D694 (calibrated on P@8022D774) forms 0x80388750 (chunk port agrees)
80388DE0 80388760 PROVEN @8022DDC8 (calibrated on P@8022DEA8) forms 0x80388760 (chunk port agrees)
80388DF4 80388774 PROVEN @8022E120 (calibrated on P@8022E200) forms 0x80388774 (chunk port agrees)
8029CEB0 8029C850 CALIBRATED @801B22E0 (calibrated on P@801B23C0) forms 0x8029C850 (chunk port has no mapping)
8029CEF8 8029C898 CALIBRATED @801B2408 (calibrated on P@801B24E8) forms 0x8029C898 (chunk port has no mapping)
-72
View File
@@ -1,72 +0,0 @@
schema_version: 1
workspace_root: ../..
# Mario Kart Wii NTSC-J (RMCJ01). The same translation surface as projects/mkwii/recomp.yml, with
# the facts that differ per region: the small-data bases the DOL's __init_registers installs, the
# StaticR.rel load address and the clean-input digests. The function map is the PAL map ported
# through the community region table and validated against these binaries.
project:
id: mkwii-rmcj01
display_name: Mario Kart Wii NTSC-J
game_id: RMCJ01
region: J
base_manifest_format: mkwii-recomp-base-manifest
base_manifest_stem: mkwii_base
memory:
base: 0x80000000
size: 0x01A00000
sda_base: 0x8038C580
sda2_base: 0x8038E920
inputs:
dol:
path: Assets/main.dol
sha256: 1b9621ef7c5d97dada103e50e5389730e67f3c2545dda592edd4b5843655af91
rel:
path: Assets/StaticR.rel
load_address: 0x8050FC60
sha256: 88539012d357a1420724e51dc7e351192ce696da4b0045994895518a3fad6fae
translation:
entry_points:
- 0x800060A4
function_map:
path: projects/mkwii-ntsc-j/MAP.txt
allow_unsupported_instructions: false
runtime:
native_registration_root: runtime/src
native_abi_directories:
- runtime/src/hle/gx
output:
root: generated
functions: functions
runtime_config: RuntimeConfig.h
data_initializer: data_sections_init.cpp
base_manifest: ../build/base/mkwii_base_manifest.json
# Code.pul carries a separate chunk for this region. The regional link base determines
# the branch bytes checked at startup; execution is remapped to module_guest_base.
profiles:
retro-rewind:
enabled: true
requires_game_id: RMCJ01
requires_dol_sha256: 1b9621ef7c5d97dada103e50e5389730e67f3c2545dda592edd4b5843655af91
code_pul: PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul
mod_root: PulsarPacks/completed/RetroRewind/RetroRewind6
region: J
module_guest_base: 0x81800000
module_link_base: 0x80398C60
output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true
retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCJD00
retro_wfc_legacy_bootstrap_hook: 0x800ED608
riivolution:
xml: xml/RetroRewind6.xml
options:
- section: Retro Rewind
option: Pack
choice: 1
-591
View File
@@ -1,591 +0,0 @@
{
"source_region": "P",
"target_region": "J",
"attribution": "chunk table and section layouts vendored from the mkw-sp project (https://github.com/mkw-sp/mkw-sp, port.py)",
"license": "MIT. Copyright (c) 2021-2023 Pablo Stebler. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"port_rule": "if src_start <= a < src_end then a - src_start + dst_start",
"address_format": "hex strings",
"chunks": [
[
"0x80004000",
"0x80008024",
"0x80004000"
],
[
"0x800080E8",
"0x8000ADC0",
"0x80008044"
],
[
"0x8000AF08",
"0x80021BAC",
"0x8000AE2C"
],
[
"0x80021BAC",
"0x80244DE0",
"0x80021ACC"
],
[
"0x802A4080",
"0x8038917C",
"0x802A3A00"
],
[
"0x805103B4",
"0x805CC0A8",
"0x8050FD34"
],
[
"0x805CC1B4",
"0x805FA33C",
"0x805CBA90"
],
[
"0x805FA344",
"0x805FF6E8",
"0x805F9C20"
],
[
"0x805FFD70",
"0x806003E8",
"0x805FF528"
],
[
"0x80600C78",
"0x80620CB4",
"0x806003EC"
],
[
"0x80620D7C",
"0x80637A24",
"0x806204C8"
],
[
"0x80637A80",
"0x8063BCF8",
"0x8063716C"
],
[
"0x8063BE40",
"0x8088F400",
"0x8063B4AC"
],
[
"0x808913C0",
"0x808913C4",
"0x80890A10"
],
[
"0x80895238",
"0x808952B8",
"0x80894888"
],
[
"0x808AD3C4",
"0x808AD3C8",
"0x808AC524"
],
[
"0x808B3984",
"0x808B3988",
"0x808B2AE4"
],
[
"0x808B5B1C",
"0x808B5B20",
"0x808B4C7C"
],
[
"0x808B5C78",
"0x808B5C7C",
"0x808B4DD8"
],
[
"0x808BB034",
"0x808BB098",
"0x808BA184"
],
[
"0x808CB550",
"0x808CB554",
"0x808CA6A0"
],
[
"0x808D3698",
"0x808D369C",
"0x808D27E8"
],
[
"0x808D36CC",
"0x808D36D0",
"0x808D281C"
],
[
"0x808D36D4",
"0x808D36D8",
"0x808D2824"
],
[
"0x808D3744",
"0x808D3748",
"0x808D2894"
],
[
"0x808D374C",
"0x808D3750",
"0x808D289C"
],
[
"0x808D3E14",
"0x808D3E18",
"0x808D2F64"
],
[
"0x808DA070",
"0x808DA080",
"0x808D91C0"
],
[
"0x808DA318",
"0x808DA368",
"0x808D9468"
],
[
"0x809BD6E8",
"0x809BD74C",
"0x809BC748"
],
[
"0x809C1830",
"0x809C1834",
"0x809C0890"
],
[
"0x809C1874",
"0x809C1878",
"0x809C08D4"
],
[
"0x809C18F8",
"0x809C18FC",
"0x809C0958"
],
[
"0x809C1988",
"0x809C198C",
"0x809C09E8"
],
[
"0x809C19A0",
"0x809C19BC",
"0x809C0A00"
],
[
"0x809C1E38",
"0x809C1E3C",
"0x809C0E98"
],
[
"0x809C21D0",
"0x809C21D4",
"0x809C1230"
],
[
"0x809C21D8",
"0x809C21DC",
"0x809C1238"
],
[
"0x809C2328",
"0x809C232C",
"0x809C1388"
],
[
"0x809C27F0",
"0x809C27FC",
"0x809C1850"
],
[
"0x809C282C",
"0x809C2854",
"0x809C188C"
],
[
"0x809C28B8",
"0x809C28BC",
"0x809C1918"
],
[
"0x809C2BE8",
"0x809C2BEC",
"0x809C1C48"
],
[
"0x809C2EF0",
"0x809C2EF4",
"0x809C1F50"
],
[
"0x809C2F38",
"0x809C2F40",
"0x809C1F98"
],
[
"0x809C2F44",
"0x809C2F48",
"0x809C1FA4"
],
[
"0x809C3618",
"0x809C361C",
"0x809C2678"
],
[
"0x809C38B8",
"0x809C38BC",
"0x809C2918"
],
[
"0x809C4330",
"0x809C4334",
"0x809C3390"
],
[
"0x809C4680",
"0x809C4684",
"0x809C36E0"
],
[
"0x809C4740",
"0x809C474C",
"0x809C37A0"
],
[
"0x809C496C",
"0x809C4970",
"0x809C39CC"
]
],
"rel_load_address": {
"P": "0x805102E0",
"E": "0x8050BF60",
"J": "0x8050FC60"
},
"dol_sections": {
"P": [
{
"name": "init",
"start": "0x80004000",
"end": "0x80006460"
},
{
"name": "extab",
"start": "0x80006460",
"end": "0x80006A20"
},
{
"name": "extabindex",
"start": "0x80006A20",
"end": "0x800072C0"
},
{
"name": "text",
"start": "0x800072C0",
"end": "0x80244DE0"
},
{
"name": "ctors",
"start": "0x80244DE0",
"end": "0x80244EA0"
},
{
"name": "dtors",
"start": "0x80244EA0",
"end": "0x80244EC0"
},
{
"name": "rodata",
"start": "0x80244EC0",
"end": "0x80258580"
},
{
"name": "data",
"start": "0x80258580",
"end": "0x802A4040"
},
{
"name": "bss",
"start": "0x802A4080",
"end": "0x80384C00"
},
{
"name": "sdata",
"start": "0x80384C00",
"end": "0x80385FC0"
},
{
"name": "sbss",
"start": "0x80385FC0",
"end": "0x80386FA0"
},
{
"name": "sdata2",
"start": "0x80386FA0",
"end": "0x80389140"
},
{
"name": "sbss2",
"start": "0x80389140",
"end": "0x8038917C"
}
],
"E": [
{
"name": "init",
"start": "0x80004000",
"end": "0x80006460"
},
{
"name": "extab",
"start": "0x80006460",
"end": "0x80006A20"
},
{
"name": "extabindex",
"start": "0x80006A20",
"end": "0x800072C0"
},
{
"name": "text",
"start": "0x800072C0",
"end": "0x80244D40"
},
{
"name": "ctors",
"start": "0x80244D40",
"end": "0x80244E00"
},
{
"name": "dtors",
"start": "0x80244E00",
"end": "0x80244E20"
},
{
"name": "rodata",
"start": "0x80244E40",
"end": "0x80258260"
},
{
"name": "data",
"start": "0x80258260",
"end": "0x8029FD00"
},
{
"name": "bss",
"start": "0x8029FD00",
"end": "0x80380880"
},
{
"name": "sdata",
"start": "0x80380880",
"end": "0x80381C40"
},
{
"name": "sbss",
"start": "0x80381C40",
"end": "0x80382C20"
},
{
"name": "sdata2",
"start": "0x80382C20",
"end": "0x80384DC0"
},
{
"name": "sbss2",
"start": "0x80384DC0",
"end": "0x80384DFC"
}
],
"J": [
{
"name": "init",
"start": "0x80004000",
"end": "0x80006460"
},
{
"name": "extab",
"start": "0x80006460",
"end": "0x80006A20"
},
{
"name": "extabindex",
"start": "0x80006A20",
"end": "0x800072C0"
},
{
"name": "text",
"start": "0x800072C0",
"end": "0x80244D00"
},
{
"name": "ctors",
"start": "0x80244D00",
"end": "0x80244DC0"
},
{
"name": "dtors",
"start": "0x80244DC0",
"end": "0x80244DE0"
},
{
"name": "rodata",
"start": "0x80244E00",
"end": "0x80257F20"
},
{
"name": "data",
"start": "0x80257F20",
"end": "0x802A39E0"
},
{
"name": "bss",
"start": "0x802A3A00",
"end": "0x80384580"
},
{
"name": "sdata",
"start": "0x80384580",
"end": "0x80385940"
},
{
"name": "sbss",
"start": "0x80385940",
"end": "0x80386920"
},
{
"name": "sdata2",
"start": "0x80386920",
"end": "0x80388AC0"
},
{
"name": "sbss2",
"start": "0x80388AC0",
"end": "0x80388AFC"
}
]
},
"rel_sections": {
"P": [
{
"name": "text",
"start": "0x805103B4",
"end": "0x8088F400"
},
{
"name": "ctors",
"start": "0x8088F400",
"end": "0x8088F704"
},
{
"name": "dtors",
"start": "0x8088F704",
"end": "0x8088F710"
},
{
"name": "rodata",
"start": "0x8088F720",
"end": "0x808B2BD0"
},
{
"name": "data",
"start": "0x808B2BD0",
"end": "0x808DD3D4"
},
{
"name": "bss",
"start": "0x809BD6E0",
"end": "0x809C4F90"
}
],
"E": [
{
"name": "text",
"start": "0x8050C034",
"end": "0x8088AFD0"
},
{
"name": "ctors",
"start": "0x8088AFD0",
"end": "0x8088B2D4"
},
{
"name": "dtors",
"start": "0x8088B2D4",
"end": "0x8088B2E0"
},
{
"name": "rodata",
"start": "0x8088B2E0",
"end": "0x808AE520"
},
{
"name": "data",
"start": "0x808AE520",
"end": "0x808D8C7C"
},
{
"name": "bss",
"start": "0x809B8F20",
"end": "0x809C07D0"
}
],
"J": [
{
"name": "text",
"start": "0x8050FD34",
"end": "0x8088EA6C"
},
{
"name": "ctors",
"start": "0x8088EA6C",
"end": "0x8088ED70"
},
{
"name": "dtors",
"start": "0x8088ED70",
"end": "0x8088ED7C"
},
{
"name": "rodata",
"start": "0x8088ED80",
"end": "0x808B1D30"
},
{
"name": "data",
"start": "0x808B1D30",
"end": "0x808DC524"
},
{
"name": "bss",
"start": "0x809BC740",
"end": "0x809C3FF0"
}
]
},
"validated_against": {
"dol": {
"path": "main.dol",
"sha256": "1b9621ef7c5d97dada103e50e5389730e67f3c2545dda592edd4b5843655af91",
"entry": "0x800060A4"
},
"rel": {
"path": "StaticR.rel",
"sha256": "88539012d357a1420724e51dc7e351192ce696da4b0045994895518a3fad6fae",
"module_id": 1,
"prolog": "0x80554C9C",
"epilog": "0x80554CE8",
"unresolved": "0x80554D30"
}
}
}
File diff suppressed because it is too large Load Diff
-243
View File
@@ -1,243 +0,0 @@
# NTSC-U (RMCE01) addresses for the PAL (RMCP01) data globals named by the WiiCompiled runtime.
# Format: <PAL hex> <NTSC hex> <evidence: PALfuncname@ntscaddr instr ...>
# Every cited instruction was disassembled from the RMCE01 main.dol (or StaticR.rel) at the
# stated NTSC-U address; the function names are the PAL MAP.txt names of the function that the
# validated PAL->NTSC-U port table (CHUNKS_E) maps onto that NTSC-U code.
# Small-data bases: NTSC-U r13=0x80388880 r2=0x8038AC20; PAL r13=0x8038CC00 r2=0x8038EFA0.
# Deltas observed: all .data/.bss objects, .sdata2 and every .sbss global up to (at least) PAL 0x80386F27
# are PAL-0x4380; .sbss from PAL 0x80386F60 onward is PAL-0x4388 (r13 offset 8 less). See notes at end.
# ---------------------------------------------------------------- GX
# Display-list fifo object (GXBeginDisplayList/GXEndDisplayList): NTSC base 0x8033FD10 = PAL 0x80344090 - 0x4380
80344090 8033FD10 GX::BeginDisplayList@80172d78 lis r31,0x8034; @80172d7c addi r31,r31,-0x2f0 (=0x8033FD10 dlFifo); @80172d8c addi r29,r31,0; @80172dc4 stw r27,0x0(r29) (dlFifo.base=list); @80172df0 addi r3,r31,0 -> GXSetCPUFifo(&dlFifo); GX::EndDisplayList@80172e2c addi r30,r30,-0x2f0; @80172e34 addi r3,r30,0 -> GXGetCPUFifo(&dlFifo)
80344094 8033FD14 GX::BeginDisplayList@80172dcc stw r4,0x4(r29) (dlFifo.end = list+size-4, r29=0x8033FD10; r4 from @80172db8 add r4,r27,r28 / @80172dc0 addi r4,r4,-4)
80344098 8033FD18 GX::BeginDisplayList@80172dd0 stw r28,0x8(r29) (dlFifo.size = size)
803440A4 8033FD24 GX::BeginDisplayList@80172dd8 stw r27,0x14(r29) (dlFifo.rdPtr = list)
803440A8 8033FD28 GX::BeginDisplayList@80172ddc stw r27,0x18(r29) (dlFifo.wrPtr = list)
803440AC 8033FD2C GX::BeginDisplayList@80172dd4 stw r3,0x1c(r29) (dlFifo.count = 0, li r3,0 @80172dbc)
80344110 8033FD90 GX::BeginDisplayList@80172dac addi r3,r31,0x80 -> memcpy(0x8033FD90, gx, 0x600) (li r5,0x600 @80172db0, saved GXData copy); GX::EndDisplayList@80172e6c addi r4,r30,0x80 (memcpy src)
80344710 80340390 GX::BeginDisplayList@80172de8 addi r3,r31,0x680 -> GXGetCPUFifo(0x80340390) (saved CPU fifo); GX::EndDisplayList@80172e48 addi r3,r30,0x680 -> GXSetCPUFifo(0x80340390)
803867D8 80382458 GX::DrawDone@8016ea84 stb r31,-0x6428(r13); GX::DrawDone@8016eaa4 lbz r0,-0x6428(r13); GX::FinishInterruptHandler@8016ed20 stb r3,-0x6428(r13) # r13-0x6428 in both regions (PAL 0x8038CC00-0x6428=0x803867D8)
803886C8 80384348 GX::Init@8016b7d4 lwz r31,-0x68d8(r2); GX::SetCPUFifo@8016c978 lwz r5,-0x68d8(r2); GX::DrawDone@8016ea3c lwz r4,-0x68d8(r2); GX::BeginDisplayList@80172d74 lwz r30,-0x68d8(r2) # .sdata2 `gx` (__GXData ptr), r2-0x68d8 in both regions (PAL 0x8038EFA0-0x68D8=0x803886C8)
# ---------------------------------------------------------------- DVD
80343230 8033EEB0 __DVDClearWaitingQueue@80163520 lis r6,0x8034; @80163524 addi r6,r6,-0x1150 (=0x8033EEB0); @8016352c stw r6,0x0(r6); @80163538 stw r6,0x4(r6) (4 self-linked queues of 8 bytes: +0..+0x1c); __DVDPushWaitingQueue@80163580 addi r4,r4,-0x1150; __DVDPopWaitingQueue@801635e0 addi r4,r4,-0x1150
803434E0 8033F160 DVD::LowInit@801648e0 lis r4,0x8034; @801648e4 stwu r5,-0xea0(r4) (r4=0x8033F160=ctx[0], ctx[0]+0=0); @801648e8 lis r3,0xfeec; @801648f8 addi r3,r3,-0x2513 (=0xFEEBDAED); @80164900 stw r3,0xc(r4) (ctx[0]+0xC=magic); @80164904 stw r5,0x10(r4) (ctx[0]+0x10=0); @80164918 stw r0,0x30(r4) (ctx[1]+0x10=1); @8016492c stw r6,0x50(r4) (ctx[2]+0x10=2); @80164944 stw r6,0x70(r4) (ctx[3]+0x10=3)
80386664 803822E4 DVD::Pause@80162b74 stw r4,-0x659c(r13); DVD::Resume@80162bb4 stw r4,-0x659c(r13); DVD::CancelAll@80163070 stw r4,-0x659c(r13); DVD::InquiryAsync@801629b4 lwz r0,-0x659c(r13) # r13-0x659c both regions
80386668 803822E8 DVD::Resume@80162bb8 lwz r0,-0x6598(r13); DVD::Resume@80162bc4 stw r4,-0x6598(r13); DVD::Pause@80162b80 stw r4,-0x6598(r13); DVD::GetDriveStatus@80162ae0 lwz r0,-0x6598(r13)
80386670 803822F0 __DVDPrepareReset@801633f0 lwz r0,-0x6590(r13); __DVDPrepareResetAsync@801632c0 lwz r0,-0x6590(r13)
8038667C 803822FC DVD::CancelAll@80163058 stw r0,-0x6584(r13); DVD::CancelAll@801630d4 stw r0,-0x6584(r13); DVD::CancelAll@80163128 lwz r0,-0x6584(r13)
803866A8 80382328 __DVDPrepareReset@801633e0 stw r31,-0x6558(r13); __DVDPrepareReset@80163488 stw r0,-0x6558(r13); __DVDPrepareReset@801634cc lwz r0,-0x6558(r13)
803866F0 80382370 DVD::Pause@80162b6c lwz r0,-0x6510(r13); DVD::CancelAll@80163068 lwz r0,-0x6510(r13); __DVDPrepareReset@8016340c lwz r3,-0x6510(r13); DVD::ReadAbsAsyncPrio@801628c8 lwz r0,-0x6510(r13) # `executing` command block
80386720 803823A0 DVD::LowInit@80164948 stw r5,-0x64e0(r13) (r5=0, current context index, right after the 4-context init); DVD::LowReadDiskID@80164a3c lwz r9,-0x64e0(r13)
80386724 803823A4 DVD::LowInit@801648d0 lbz r0,-0x64dc(r13) (skip context init if already set); DVD::LowInit@8016494c stb r0,-0x64dc(r13) (=1)
80386725 803823A5 DVD::LowInit@801647c4 lbz r0,-0x64db(r13); DVD::LowInit@801647d4 stb r0,-0x64db(r13) (=1); DVD::LowFinalize@80164794 stb r0,-0x64db(r13) (=0)
803866A0 80382320 DVD::Init@8015e990 lwz r0,-0x6560(r13) (return if already initialized); DVD::Init@8015e9a8 stw r0,-0x6560(r13) (=1)
# ---------------------------------------------------------------- NAND
80346D20 803429A0 nandGetHomeDir@8019e7d4 lis r3,0x8034; @8019e7d8 addi r3,r3,0x29a0 (returns &NANDHomeDir=0x803429A0); NAND::Init@8019e178 lis r5,0x8034; @8019e184 addi r5,r5,0x29a0 (ESP_GetDataDir out buffer); NAND::Init@8019e198 lis r4,0x8034; @8019e1a0 addi r4,r4,0x29a0
80386848 803824C8 NAND::Init@8019e10c lwz r0,-0x63b8(r13) (cmpwi 1 / cmpwi 2 = init state); NAND::Init@8019e13c stw r0,-0x63b8(r13) (=1); NAND::Init@8019e1d0 stw r0,-0x63b8(r13) (=2 on success); NAND::GetCurrentDir@8019e308 lwz r0,-0x63b8(r13)
# ---------------------------------------------------------------- VI
# VI HorVer struct (.bss): NTSC 0x8034C4E0 = PAL 0x80350860 - 0x4380; fields +4 DispSizeX, +6 DispSizeY, +0x12 FBSizeX, +0x1C PanSizeY, +0x30 bufAddr
80350864 8034C4E4 VIInit@801b962c lis r30,0x8035; @801b9630 addi r31,r30,-0x3b20 (=0x8034C4E0 HorVer); @801b965c li r7,640; @801b969c sth r7,0x4(r31) (HorVer.DispSizeX=640)
80350866 8034C4E6 VIInit@801b96b0 sth r0,0x6(r31) (HorVer.DispSizeY = timing->acv*2; r31=0x8034C4E0)
80350872 8034C4F2 VIInit@801b970c lis r4,0x8035; @801b9714 addi r4,r4,-0x3b20 (=0x8034C4E0); @801b9718 li r9,640; @801b97b4 sth r9,0x12(r4) (HorVer.FBSizeX=640)
8035087C 8034C4FC VIInit@801b9804 sth r5,0x1c(r4) (HorVer.PanSizeY; r4=0x8034C4E0)
80350890 8034C510 VISetNextFrameBuffer@801baa34 lis r7,0x8035; @801baa3c addi r7,r7,-0x3b20 (=0x8034C4E0); @801baa44 stw r30,0x30(r7) (HorVer.bufAddr=fb); @801baa4c addi r4,r7,0x34; @801baa50 addi r5,r7,0x38 (setFbbRegs(&HorVer,&tfbb,&bfbb,...))
80386B38 803827B8 VIInit@801b9418 lwz r0,-0x60c8(r13) (already-initialised check); VIInit@801b9434 stw r0,-0x60c8(r13) # r13-0x60c8 both regions
80386BA8 80382828 VIGetTvFormat@801bac4c lwz r31,-0x6058(r13); VIInit@801b9690 stw r8,-0x6058(r13) (CurrTvMode=HorVer.tv); __VIRetraceHandler@801b8b10 stw r3,-0x6058(r13)
80386BE4 80382864 VIWaitForRetrace@801b9968 lwz r30,-0x601c(r13); VIWaitForRetrace@801b9974 lwz r0,-0x601c(r13); VIGetRetraceCount@801bab04 lwz r3,-0x601c(r13); __VIRetraceHandler@801b8974 stw r0,-0x601c(r13) (retraceCount++)
80386B44 803827C4 VIInit@801b9904 stw r0,-0x60bc(r13) (=1, VIInit tail); __VIRetraceHandler@801b8da4 lwz r0,-0x60bc(r13); __VIRetraceHandler@801b8dcc stw r0,-0x60bc(r13)
80386BB8 80382838 VISetPreRetraceCallback@801b906c lwz r31,-0x6048(r13); VISetPreRetraceCallback@801b9074 stw r30,-0x6048(r13); __VIRetraceHandler@801b8984 lwz r12,-0x6048(r13); VIInit@801b984c stw r29,-0x6048(r13)
80386BB4 80382834 VISetPostRetraceCallback@801b90b0 lwz r31,-0x604c(r13); VISetPostRetraceCallback@801b90b8 stw r30,-0x604c(r13); __VIRetraceHandler@801b8d7c lwz r12,-0x604c(r13); VIInit@801b9850 stw r29,-0x604c(r13)
80386BA0 80382820 VIGetNextFrameBuffer@801baa84 lwz r3,-0x6060(r13); __VIRetraceHandler@801b8b14 stw r0,-0x6060(r13)
80386BC0 80382840 VIWaitForRetrace@801b996c addi r3,r13,-0x6040 -> OSSleepThread(&retraceQueue); VIInit@801b97f8 addi r3,r13,-0x6040 -> OSInitThreadQueue; __VIRetraceHandler@801b8d8c addi r3,r13,-0x6040 -> OSWakeupThread
# ---------------------------------------------------------------- EGG
# NOTE: past the .sbss gap: PAL r13-0x5ca0, NTSC r13-0x5ca8 (delta 0x4388)
80386F60 80382BD8 RKSystem::Main@80008ec0 lis r31,0x802a; @80008ec4 addi r31,r31,-0x300 (=0x8029FD00 system instance, .bss start); @80008ed8 stw r31,-0x5ca8(r13) (BaseSystem::sSystem = instance, then virtual call via vtable+0x38); EGG::AsyncDisplay::beginFrame@8020fe74 lwz r3,-0x5ca8(r13) (sSystem->vtable[2] call); EGG::Display::beginFrame@80219e50 lwz r3,-0x5ca8(r13); EGG::SceneManager::createScene@8023ad94 lwz r4,-0x5ca8(r13); EGG::DrawDoneCallback@8020fc48 lwz r3,-0x5ca8(r13); EGG::StateGX::InitFrame@80240054 lwz r3,-0x5ca8(r13)
# ---------------------------------------------------------------- Range constants (section tables)
# NTSC-U DOL header: section 9 = .ctors file off 0x240E40 addr 0x80244D40 size 0xC0 (end 0x80244E00)
# section 10 = .dtors file off 0x240F00 addr 0x80244E00 size 0x20 (end 0x80244E20)
# .ctors contents: 80244D40 __init_cpp_exceptions(0x80020684), 80244D44 __sinit_\RKSystem_cpp(0x8000998C), ... 80244DE8 __sinit__eggVector_cpp(0x80242D2C), 80244DEC..80244DFC zero
# .dtors contents: 80244E00 __destroy__global_chain(0x800207F0), 80244E04 __fini_cpp_exceptions(0x800206C0), rest zero
80244DE0 80244D40 DOL section table: .ctors start (sec9 addr), delta 0xA0 like the surrounding text
80244EA0 80244E00 DOL section table: .ctors end = .dtors start (sec9 addr+size = sec10 addr)
# NTSC-U StaticR.rel section table (load base 0x8050BF60): section 2 = .ctors file off 0x37F070 size 0x304 -> 0x8088AFD0..0x8088B2D4 (193 words: 192 R_PPC_ADDR32 self-relocations + null terminator);
# section 3 = .dtors file off 0x37F374 size 0xC -> 0x8088B2D4..0x8088B2E0 (2 ADDR32 relocations); section 4 (.rodata) starts 0x8088B2E0
8088F400 8088AFD0 REL section table: .ctors start (sec2 off 0x37F070 + base 0x8050BF60)
8088F704 8088B2D4 REL section table: .ctors end (0x8088AFD0 + 0x304) = .dtors (sec3) start
# ================================================================ Batch 2
# ---------------------------------------------------------------- GX (DOL .bss/.sbss) - GXInit / __GXFifoInit / __GXPEInit objects
80343740 8033F3C0 GX::Init@8016b824 lis r28,0x8034; @8016b830 addi r3,r28,-0xc40 (=0x8033F3C0 FifoObj -> GXInitFifoBase); @8016b838 addi r3,r28,-0xc40 (GXSetCPUFifo); @8016b840 addi r3,r28,-0xc40 (GXSetGPFifo); @8016bc9c addi r3,r3,-0xc40 (return &FifoObj)
803437C0 8033F440 .sdata2 cell: the `gx` pointer at NTSC 0x80384348 (r2-0x68d8, see 803886C8 above) contains 0x8033F440 in the DOL image (sec14 file data) = &gxData; PAL cell 0x803886C8 holds 0x803437C0. GX::Init@8016b7d4 (ctx) lwz r31,-0x68d8(r2) loads that cell.
80343DC0 8033FA40 __GX::FifoInit@8016d12c lis r3,0x8034; @8016d134 addi r3,r3,-0x5c0 (=0x8033FA40); @8016d138 li r5,0x24 -> memset(&GPFifo,0,0x24)
80343DE4 8033FA64 __GX::FifoInit@8016d114 lis r3,0x8034; @8016d120 addi r3,r3,-0x59c (=0x8033FA64); @8016d124 li r5,0x24 -> memset(&CPUFifo,0,0x24); GX::SetCPUFifo@8016c8d4 lis r4,0x8034; @8016c8dc addi r4,r4,-0x59c; @8016c8e8 stb r0,0x22(r4)
803867B0 80382430 __GX::FifoInit@8016d140 stb r31,-0x6450(r13) (=0); GX::SetCPUFifo@8016c8e0 stb r0,-0x6450(r13); GX::GetCPUFifo@8016ce84 lbz r0,-0x6450(r13)
803867B1 80382431 __GX::FifoInit@8016d144 stb r31,-0x644f(r13) (=0)
803867C0 80382440 __GX::FifoInit@8016d11c stw r31,-0x6440(r13) (r31=0: thread queue head cleared)
803867C4 80382444 __GX::FifoInit@8016d110 stw r3,-0x643c(r13) (r3 = OSGetCurrentThread()); GX::GetCurrentGXThread@8016d2fc lwz r3,-0x643c(r13)
803867D0 80382450 __GX::PEInit@8016eda0 addi r3,r13,-0x6430 -> OSInitThreadQueue; GX::DrawDone@8016ea9c addi r3,r13,-0x6430 -> OSSleepThread; GX::FinishInterruptHandler@8016ed58 addi r3,r13,-0x6430 -> OSWakeupThread
# ---------------------------------------------------------------- OS scheduler (DOL .bss/.sbss)
803477B0 80343430 SetEffectivePriority@801a99b0 lis r3,0x8034; @801a99b8 addi r3,r3,0x3430 (=0x80343430 RunQueue[32], indexed by prio*8: @801a99b4 slwi r0,r31,3); SelectThread@801a9bec lis r3,0x8034; @801a9bf4 addi r3,r3,0x3430; OS::ResumeThread@801aa580 lis r3,0x8034; @801aa588 addi r3,r3,0x3430
80386918 80382598 OS::__ThreadInit@801a97e0 stw r29,-0x62e8(r13) (=0); OS::DisableScheduler@801a985c lwz r31,-0x62e8(r13); @801a9864 stw r0,-0x62e8(r13) (count+1); SelectThread@801a9b80 lwz r0,-0x62e8(r13) (early exit while >0)
8038691C 8038259C OS::__ThreadInit@801a9718 stw r3,-0x62e4(r13)
80386920 803825A0 OS::__ThreadInit@801a9710 stw r3,-0x62e0(r13); UnsetRun@801a9908 lwz r5,-0x62e0(r13); @801a9918 stw r0,-0x62e0(r13)
# ---------------------------------------------------------------- SC / game system (DOL .sbss)
803869E0 80382660 SCGetProductCode@801b2390 addi r4,r13,-0x6220 (6-byte CODE output buffer: @801b2398 li r5,6); @801b23ac addi r3,r13,-0x6220 (returned pointer)
80386000 80381C80 SystemManager::create@8000a5a4 lwz r0,-0x6c00(r13) (s_instance null check); @8000a5c8 stw r3,-0x6c00(r13) (s_instance = new SystemManager); @8000a5d4 lwz r3,-0x6c00(r13); also REL GameScreen::__ct@80561520 lis r3 + @80561524 lwz r3,0(r3) carry module-0 relocations to 0x80381C80
# ---------------------------------------------------------------- EGG::Screen statics (DOL .sbss) - delta 0x4380 still holds here
80386F14 80382B94 EGG::Screen::SetTVModeDefault@8023e1cc lwz r6,-0x5cec(r13) (active screen, cmpwi 0 -> skip); EGG::Screen::SetTVMode@8023e114 lwz r6,-0x5cec(r13); EGG::Screen::CreateDummy@8023cf08 stw r3,-0x5cec(r13)
80386F20 80382BA0 EGG_Screen_cpp_sinit_@8023e37c (ctx) lfs f1,-0x5f84(r2) (=1.0f constant at 0x80384C9C); @8023e380 addi r4,r13,-0x5ce0; @8023e38c stfs f1,-0x5ce0(r13) (gScreenScale.x = 1.0); EGG::Screen::GetPosSizeInEFB@8023d62c lfs f0,-0x5ce0(r13); EGG::Screen::LoadDirectly@8023d1c4 lfs f0,-0x5ce0(r13)
80386F24 80382BA4 EGG_Screen_cpp_sinit_@8023e390 stfs f1,0x4(r4) with r4 = r13-0x5ce0 (=0x80382BA0) from @8023e380 (ctx) addi r4,r13,-0x5ce0 (gScreenScale.y = 1.0; second float of the Vector2f at 0x80382BA0)
# ---------------------------------------------------------------- EGG::DrawGX::SetVtxState vertex arrays (DOL .sdata2 / .rodata)
# .sdata2 arrays keep their r2 offsets (delta 0x4380); the .rodata arrays sit at PAL-0x320 (PAL .rodata carries 0x280 more bytes before them than NTSC; .rodata starts at PAL-0xA0).
80388B80 80384800 EGG::DrawGX::SetVtxState@8021b484 li r3,10(GX_VA_NRM); @8021b488 addi r4,r2,-0x6420; @8021b48c li r5,6 -> GXSetArray (states 7/8)
80388BA0 80384820 EGG::DrawGX::SetVtxState@8021b514 li r3,10; @8021b518 addi r4,r2,-0x6400; @8021b51c li r5,6 -> GXSetArray (state 9)
80388BC0 80384840 EGG::DrawGX::SetVtxState@8021b588 addi r4,r2,-0x63e0 (POS array, states 10/12); @8021b58c li r3,9; @8021b590 li r5,2
80388BE0 80384860 EGG::DrawGX::SetVtxState@8021b580 addi r4,r2,-0x63c0 (POS array, states 11/13); @8021b5a0 li r3,13(GX_VA_TEX0); @8021b5a4 addi r4,r2,-0x63c0; @8021b5a8 li r5,2
802574A0 80257180 EGG::DrawGX::SetVtxState@8021b270 lis r31,0x8025; @8021b274 addi r31,r31,0x7160 (=0x80257160); @8021b2d8 addi r4,r31,0x20 (=0x80257180); @8021b2dc li r3,9(GX_VA_POS); @8021b2e0 li r5,6 (state 0)
802574E0 802571C0 EGG::DrawGX::SetVtxState@8021b2e8 addi r4,r31,0x60 (=0x802571C0); @8021b2ec li r3,10(GX_VA_NRM); @8021b2f0 li r5,6 (state 0)
80257520 80257200 EGG::DrawGX::SetVtxState@8021b474 addi r4,r31,0xa0 (=0x80257200); @8021b478 li r3,9; @8021b47c li r5,4 (states 7/8)
80257540 80257220 EGG::DrawGX::SetVtxState@8021b504 addi r4,r31,0xc0 (=0x80257220); @8021b508 li r3,9; @8021b50c li r5,6 (state 9)
# ---------------------------------------------------------------- EGG::Screen canvas records + vtable (DOL .data) - these sit at PAL-0x4368 (not 0x4380)
# NTSC record table 0x8029FB80: {608,480,1.0526,1.0} then {832,480,0.7692,1.0} (12-byte records, matching the runtime's 608/832 widths); vtable 0x8029FBA4 = {EGG::Screen::__dt, EGG::Screen::LoadDirectly, EGG::Screen::CopyToG3D, ...}
802A3EE8 8029FB80 EGG::Screen::SetTVModeDefault@8023e1c4 bl SCGetAspectRatio; @8023e1e8 mulli r4,r0,12; @8023e1f0 lis r5,0x802a; @8023e1fc addi r5,r5,-0x480 (=0x8029FB80 record[0]); @8023e200 lhzx r4,r5,r4 (record[aspect].width); EGG::Screen::__ct@8023cf38 lis r5,0x802a; @8023cf48 addi r5,r5,-0x480; REL GameScreen::__ct@80561558 lis r4 / @80561568 addi r4,r4,0 carry module-0 relocations to 0x8029FB80
802A3EF4 8029FB8C record[1] = 0x8029FB80 + 12 (SetTVModeDefault index*12 above; 16:9 record {832,480,...} at 0x8029FB8C in the DOL image)
802A3F0C 8029FBA4 EGG::Screen::__ct@8023cfac lis r3,0x802a; @8023cfb0 addi r3,r3,-0x45c (=0x8029FBA4); @8023cfb8 stw r3,0x38(r31) (vtable at this+0x38); EGG::Screen::__ct(args)@8023d058 addi r3,r3,-0x45c
# ---------------------------------------------------------------- StaticR.rel (.data / .bss) - resolved through the NTSC-U REL relocation table
# Method: the REL text carries pre-relocation immediates (lis/addi = 0), so each cited instruction's R_PPC_ADDR16_HA/LO entry (module 1) was
# looked up at instruction+2 in the import table; target = section address + addend. NTSC-U section addresses: .data (sec5) = 0x808AE520,
# .bss (sec6) = 0x809B8F20 = 0x8050BF60 + alignUp(filesize 0x4ACF94 + 0x20 heap header, 0x20), which is exactly where the translator
# (RelFile.CalculateBssOffset) and the NTSC base manifest (build/base/mkwii_base_manifest.json: StaticR.rel:6 = 0x809b8f20..0x809c07d0) place it.
# PAL cross-check: the runtime's PAL bss identities imply PAL bss base 0x809BD6E0 (0x809C1830 - 0x4150; THP flag 0x809BECF0 = +0x1610 is
# referenced by NTSC THP::PlayerQuit/PlayerDrawDone at sec6+0x1610; Race::JugemManager 0x809C28B8 = +0x51D8 referenced by NTSC RaceScene::OnEnterEnd).
# The PAL bss layout is NOT identical everywhere: the audio singleton cluster sits 0x480 higher in PAL (mkw-sp PAL RaceSoundManager 0x809C27F8 =
# +0x5118 vs NTSC Audio::RaceMgr::CreateInstance -> +0x4C98; ItemMusicManager 0x809C27F0 = +0x5110 vs Audio::ItemAlterationMgr -> +0x4C90), so
# REL bss identities were matched by function, never by delta.
808B4C20 808B0570 GameScreen::__ct@805614cc bl -> EGG::Screen::__ct (module-0 REL24 reloc to 0x8023cf30; GameScreen is the only REL caller of an EGG::Screen ctor); @805614d0 lis r3 (ADDR16_HA -> sec5+0x2050); @805614d8 addi r3,r3 (ADDR16_LO -> sec5+0x2050 = 0x808B0570); @805614dc stw r3,0x38(r30) (vtable at this+0x38, same slot as the EGG::Screen vtable). Vtable contents via ADDR32 relocs: +8 GameScreen::__dt, +0xC EGG::Screen::LoadDirectly, +0x10 EGG::Screen::CopyToG3D (same layout as the EGG::Screen vtable at 0x8029FBA4).
809C1830 809BD070 Renderer::Register(listIdx)@80560d9c lis r5 (ADDR16_HA -> sec6+0x4150); @80560da8 addi r5,r5 (ADDR16_LO -> sec6+0x4150 = 0x809BD070 = Render::DrawList instance); @80560da4 mulli r0,r4,12; @80560dc0 add r3,r5,r0 (&lists[listIdx], 12-byte nw4r::ut::List); 38 sites reference sec6+0x4150 (Renderer::Register2ndList, RemoveFrom1stList/2ndList, ...)
809C183C 809BD07C Renderer::Register2ndList(NoAppend)@80560ef0 lis r5 (-> sec6+0x4150); @80560ef8 addi r5,r5 (-> 0x809BD070); @80560f08 addi r3,r5,0xc (second list = DrawList+0xC = 0x809BD07C); Renderer::RemoveFrom2ndList@80560f54 lis r5; @80560f5c addi r5,r5 (-> 0x809BD070); @80560f60 addi r3,r5,0xc
809C2898 809BDC58 Audio::Manager::CreateInstance@8070f390 lis r3 (ADDR16_HA -> sec6+0x4d38); @8070f3ac lwz r0,0(r3) (ADDR16_LO -> sec6+0x4d38 = 0x809BDC58, null check); @8070f440 lis r3; @8070f444 stw r30,0(r3) (-> sec6+0x4d38: spInstance = new Audio::Manager); the object is EGG::ExpAudioMgr-derived: Audio::Manager::StartSound@8070fcc0 lwz r3,0x5bc(r3) (archive player pointer) and DOL EGG::ExpAudioMgr::Initialize@802114e4 lwz r3,0x5bc(r27), matching the runtime's kArchivePlayerOffset 0x5BC. PAL offset 0x51B8 = NTSC 0x4D38 + 0x480, the same shift as the mkw-sp audio singletons.
# ---------------------------------------------------------------- .ctors/.dtors confirmation (batch 2 request) - unchanged from above
# DOL: PAL .ctors 80244DE0..80244EA0 -> NTSC 80244D40..80244E00 (sec9); .dtors NTSC 80244E00..80244E20 (sec10). REL: PAL .ctors 8088F400..8088F704 -> NTSC 8088AFD0..8088B2D4 (sec2); .dtors 8088B2D4..8088B2E0 (sec3).
# The NTSC base manifest agrees: .ctors 0x80244d40-0x80244e00, .dtors 0x80244e00-0x80244e20, StaticR.rel:2 0x8088afd0-0x8088b2d4, StaticR.rel:3 0x8088b2d4-0x8088b2e0.
# ================================================================ Batch 3
# ---------------------------------------------------------------- AX / DSP (DOL .data/.bss/.sbss/.sdata2)
# __AXOutInitDSP fills the DSPTaskInfo __AXDSPTask (NTSC 0x802F3E20): +0xC iram_mmem_addr, +0x10 iram_length, +0x18 dram_mmem_addr, +0x1C dram_length(64), +0x20 dram_addr(3282), +0x28.. callbacks 0x801268a8/0x801268b4/0x80126908/0x80126918 (= PAL 80126948/54/9A8/9B8 - 0xA0)
8027F820 8027B4E0 __AXOutInitDSP@80126960 lis r29,0x8028; @80126964 addi r29,r29,-0x4b20 (=0x8027B4E0, DSP ucode image: vector table 029f 0f3c 029f 0f48 ... at that address); @80126990 stw r29,0xc(r30) (task.iram_mmem_addr; r30 = __AXDSPTask) # .data delta 0x4340 for this object (not 0x4380)
802F81A0 802F3E20 __AXOutInitDSP@80126950 lis r30,0x802f; @80126954 addi r30,r30,0x3e20 (=0x802F3E20 __AXDSPTask); @801269d8 lis r3,0x802f; @801269dc addi r3,r3,0x3e20 -> DSPAddTask(&__AXDSPTask); __AXOutAiCallback@80126830 lis r3,0x802f; @80126838 addi r3,r3,0x3e20
802F8200 802F3E80 __AXOutInitDSP@80126924 lis r11,0x802f; @80126944 addi r11,r11,0x3e80 (=0x802F3E80 DRAM image); @8012699c stw r11,0x18(r30) (task.dram_mmem_addr); @80126978 li r10,64; @801269a0 stw r10,0x1c(r30) (dram_length = 64); @80126980 li r9,3282; @801269a4 stw r9,0x20(r30) (dram_addr)
80386608 80382288 DSPInit@8015d3d0 lwz r0,-0x65f8(r13) (already-initialised check); DSPInit@8015d448 stw r0,-0x65f8(r13) (=1); DSPCheckInit@8015d464 lwz r3,-0x65f8(r13)
80386610 80382290 DSPAssertTask@8015d510 stw r0,-0x65f0(r13) (=1); DSPAssertTask@8015d538 stw r0,-0x65f0(r13); __DSPHandler@8015d6d8 lwz r0,-0x65f0(r13)
80386614 80382294 DSPAssertTask@8015d50c stw r30,-0x65ec(r13) (= task being asserted); DSPAssertTask@8015d534 stw r30,-0x65ec(r13)
80386618 80382298 DSPInit@8015d438 stw r4,-0x65e8(r13) (=0, cleared with the task pointers below)
8038661C 8038229C DSPInit@8015d440 stw r4,-0x65e4(r13) (=0); __DSPHandler@8015d96c lwz r3,-0x65e4(r13); __DSP_insert_task@8015dd60 stw r3,-0x65e4(r13)
80386620 803822A0 DSPInit@8015d444 stw r4,-0x65e0(r13) (=0); DSPAddTask@8015d4ac lwz r0,-0x65e0(r13); __DSPHandler@8015d968 lwz r0,-0x65e0(r13)
80386624 803822A4 DSPInit@8015d43c stw r4,-0x65dc(r13) (=0); DSPAssertTask@8015d4f8 lwz r5,-0x65dc(r13); __DSPHandler@8015d634 lwz r5,-0x65dc(r13)
80388588 80384208 AXFXReverbStdExpCallback@8012b81c lfs f2,-0x6a18(r2) (cell 0x80384208 = 1.0f in the DOL image); AXFXReverbStdExp__InitParams@8012becc lfs f1,-0x6a18(r2) # r2-0x6a18 in both regions (PAL 0x8038EFA0-0x6A18=0x80388588)
8038858C 8038420C AXFXReverbStdExpCallback@8012b82c lfs f3,-0x6a14(r2) (cell 0x8038420C = 0.6f in the DOL image)
# ---------------------------------------------------------------- AI (DOL .sbss)
80386448 803820C8 AIInit@8012402c lwz r0,-0x67b8(r13) (already-initialised check); AIInit@80124170 stw r0,-0x67b8(r13) (=1); AICheckInit@80123ff4 lwz r3,-0x67b8(r13)
8038644C 803820CC __AIDHandler@801241d8 lwz r0,-0x67b4(r13) (callback-busy check); __AIDHandler@801241ec stw r4,-0x67b4(r13) (=1)
8038647C 803820FC AIInit@8012415c stw r31,-0x6784(r13); __AIDHandler@801241e4 lwz r0,-0x6784(r13); __AICallbackStackSwitch@8012425c lis r5,0x8038; @80124260 addi r5,r5,0x20fc (=0x803820FC)
80386480 80382100 AIRegisterDMACallback@80123f00 lwz r31,-0x6780(r13) (old callback); AIRegisterDMACallback@80123f08 stw r30,-0x6780(r13) (new callback); AIInit@80124150 stw r0,-0x6780(r13) (=0); __AIDHandler@801241cc lwz r3,-0x6780(r13)
# ---------------------------------------------------------------- OS thread/interrupt globals (DOL .bss/.sdata/.sbss)
80347130 80342DB0 OS::CreateThread@801a9ea8 lis r4,0x8034; @801a9eac addi r4,r4,0x2d70; @801a9eb0 lwz r0,0x40(r4) (=0x80342DB0); @801a9eb4 cmpwi r0,0 -> skip the FP/PS context init; when set: @801a9ecc ori r5,r5,0x900 (srr1), @801a9ed4 ori r4,r4,1 (mode), fpscr from the .sdata word below (exactly the sequence os_thread.cpp mirrors)
80347498 80343118 OS::__ThreadInit@801a95a0 lis r31,0x8034; @801a95a4 addi r31,r31,0x3118 (=0x80343118 DefaultThread; RunQueue[32] at 0x80343430 = +0x318 follows it)
803478B0 80343530 SelectThread@801a9ca0 lis r3,0x8034; @801a9ca8 addi r3,r3,0x3530 (=0x80343530 IdleContext -> OSLoadContext); SelectThread@801a9cd0 lis r3,0x8034; @801a9cd4 addi r3,r3,0x3530
80385AA8 80381728 OS::CreateThread@801a9ee0 lwz r4,-0x7158(r13) (__OSFpscrEnableBits); @801a9ee4 rlwinm r4,r4,0,24,28 (&0xF8); @801a9ee8 ori r4,r4,4; @801a9eec stw r4,0x194(r31) (thread->context.fpscr); OS::SetErrorHandler@801a28ac lwz r4,-0x7158(r13) # .sdata, r13-0x7158 both regions
80385AE0 80381760 OS::SetSwitchThreadCallback@801a952c lwz r31,-0x7120(r13) (old callback); @801a9540 stw r30,-0x7120(r13) (new); SelectThread@801a9c84 lwz r12,-0x7120(r13) (called on switch); OS::__ThreadInit@801a9620 lwz r12,-0x7120(r13) # .sdata, r13-0x7120 both regions
803868F8 80382578 OS::__SetInterruptHandler@801a6558 lwz r5,-0x6308(r13) (InterruptHandlerTable ptr, then stwx handler); OS::__GetInterruptHandler@801a656c lwz r4,-0x6308(r13); OS::__InterruptInit@801a65a4 stw r3,-0x6308(r13); OS::__DispatchInterrupt@801a6bc0 lwz r3,-0x6308(r13)
80386298 80381F18 RFL::iGetManager@800bc3d0 lwz r3,-0x6968(r13) (returns the RFL manager pointer); RFL::Init@800bbb34 lwz r0,-0x6968(r13)
80336278 80331EF8 BTM_IsDeviceUp@8013a260 lis r3,0x8033; @8013a264 addi r3,r3,0x1ef8 (=0x80331EF8 btm_cb); @8013a268 lbz r3,0x64e(r3) (device state; compared with 5, matching os_init.cpp's +0x64E/5); btm_acl_init@80137bc4 addi r3,r3,0x1ef8
# ---------------------------------------------------------------- StaticR.rel THP player state and movie mode table (relocation method, bss base 0x809B8F20)
# THP ActivePlayer struct = REL bss+0x1420 in both regions (PAL 0x809BEB00 = 0x809BD6E0+0x1420; NTSC 0x809BA340); the runtime's 809BEBA0..A7 are its +0xA0 (open) and +0xA4..+0xA7 byte fields.
809BEBA0 809BA3E0 THP::PlayerOpen@8054ad7c lis r29 (ADDR16_HA -> sec6+0x1420); @8054ad84 addi r29,r29 (ADDR16_LO -> 0x809BA340 ActivePlayer); @8054ada0 addi r30,r29,0; @8054ada4 lwz r0,0xa0(r30) (already-open check); @8054afc8 stw r0,0xa0(r30) (open = 1); THP::PlayerClose@8054aff8 lis r3; @8054b000 addi r3,r3 (-> 0x809BA340); @8054b020 stw r0,0xa0(r3) (open = 0) [reloc-base 0x809BA340 + 0xA0]
809BEBA4 809BA3E4 THP::PlayerOpen@8054afbc stb r4,0xa4(r30) (r30 = ActivePlayer 0x809BA340 from @8054ad7c / @8054ad84 / @8054ada0 [3bdd0000]) [reloc-base 0x809BA340 + 0xA4]
809BEBA5 809BA3E5 THP::PlayerOpen@8054afb4 stb r4,0xa5(r30) (same base: @8054ad7c / @8054ad84 / @8054ada0 [3bdd0000]) [reloc-base 0x809BA340 + 0xA5]
809BEBA6 809BA3E6 THP::PlayerOpen@8054afc0 stb r4,0xa6(r30) (same base: @8054ad7c / @8054ad84 / @8054ada0 [3bdd0000]) [reloc-base 0x809BA340 + 0xA6]
809BEBA7 809BA3E7 THP::PlayerOpen@8054aec4 stb r0,0xa7(r30) (same base: @8054ad7c / @8054ad84 / @8054ada0 [3bdd0000]) [reloc-base 0x809BA340 + 0xA7]
809BECF0 809BA530 THP::PlayerQuit@8054ad48 lis r3 (ADDR16_HA -> sec6+0x1610); @8054ad50 stw r0,0(r3) (ADDR16_LO -> sec6+0x1610 = 0x809BA530, Initialized = 0); THP::PlayerDrawDone@8054bee0 lis r3; @8054bee4 lwz r0,0(r3) (-> 0x809BA530) # PAL +0x1610 under base 0x809BD6E0, same offset
8088FDB8 8088B960 MovieManager::Prepare@805258b0 lwz r0,0xa8(r31) (mode); @805258b4 lis r4 (ADDR16_HA -> sec4+0x680); @805258b8 addi r4,r4 (ADDR16_LO -> sec4+0x680 = 0x8088B960); @805258c0 slwi r0,r0,2; @805258c8 lwzx r4,r4,r0 (audioSystem = table[mode]) -> THP::PlayerPrepare; MovieManager::PrepareAsync@8052594c lis r4; @80525950 addi r4,r4 (-> 0x8088B960); @80525960 lwzx r4,r4,r0 # REL .rodata: PAL 0x8088FDB8 is .rodata+0x698 from 0x8088F720 but the NTSC table is .rodata+0x680; matched by function, not by delta
# ---------------------------------------------------------------- Notes
# * No PAL binary is available on this machine (both MKW images found are RMCE01), so PAL-side offsets are
# PAL_addr - PAL_r13/r2 by definition; each NTSC-U instruction above was matched by function identity (port table +
# PAL MAP name), by the instruction's role in the SDK/EGG function, and by consistency with sibling globals.
# * .sbss gap: delta 0x4380 is directly witnessed up to PAL 0x80386F27 (EGG::Screen gScreenScale Vector2f at
# PAL 0x80386F20/24 -> NTSC 0x80382BA0/A4: EGG_Screen_cpp_sinit_@8023e38c stfs f1,-0x5ce0(r13),
# @8023e390 stfs f1,4(r4) with @8023e380 addi r4,r13,-0x5ce0; EGG::Screen::SetTVModeDefault@8023e1cc lwz r6,-0x5cec(r13)
# = PAL 0x80386F14 active-screen ptr), and delta 0x4388 at PAL 0x80386F60 (sSystem). The extra 8 PAL bytes therefore
# lie in PAL 0x80386F28..0x80386F5F (NTSC 0x80382BA8..0x80382BD7: EGG::Screen/StateGX statics); which object was
# added/removed could not be identified without the PAL binary. None of the identities above fall inside that window.
# * The runtime's comment labels for some DVD .sbss globals do not match the SDK roles visible in the code
# (0x80386664 is the flag DVDPause sets / DVDResume clears, 0x80386668 the one DVDResume tests-and-clears,
# 0x80386670 the one __DVDPrepareReset tests); the address identities are unaffected.
# STM (State Transition Manager) globals. __OSInitSTM stores the two /dev/stm handles and its
# initialized flag into the small-data block; the runtime's stub writes the same three words so
# OSResetSystem's zero-checks pass. Their SDA offsets are NOT the same in every region (RMCK01
# puts them 0x20 lower, where PAL/NTSC-U/NTSC-J keep the OSDisableScheduler nesting count), so
# they are named as identities here rather than as a hardcoded r13 offset.
80386934 803825B4 __OSInitSTM@801AB810 stw r31,-0x62cc(r13) (STM initialized flag)
80386938 803825B8 __OSInitSTM@801AB808 stw r3,-0x62c8(r13) (/dev/stm/immediate handle)
8038693C 803825BC __OSInitSTM@801AB830 stw r3,-0x62c4(r13) (/dev/stm/eventhook handle)
80386940 803825C0 OS::SetPowerCallback@801AB700 lwz r0,-0x62c0(r13); @801AB750 stw r0,-0x62c0(r13); @801AB75C stw r0,-0x62c0(r13) (STM event handler registered flag)
80386948 803825C8 OS::SetPowerCallback@801AB6E0 lwz r29,-0x62b8(r13); @801AB6EC stw r31,-0x62b8(r13); @801AB6FC stw r3,-0x62b8(r13) (registered power callback pointer)
# ISFS / IPC globals. The runtime's ISFS_OpenLib stub brings the filesystem client up itself:
# it writes the "/dev/fs" path string and the device fd, then creates the ISFS heap out of the
# IPC buffer window. Like the STM globals above, these do NOT sit at the same r13 offsets in
# every region - RMCK01 keeps the .sbss cluster 0x20 lower and __ISFS_fd/__ISFS_path 0x8 lower -
# so they are named as identities rather than as hardcoded r13 offsets. Addresses below are the
# NTSC-U ones, each confirmed by the RMCE01 instructions cited; PAL is uniformly +0x4380 here,
# matching the .sbss delta documented at the top of this file.
80386770 803823F0 ISFS@80169B44 lwz r0,-0x6490(r13); ISFS@80169B64 lwz r0,-0x6490(r13); ISFS@80169BCC lwz r4,-0x6490(r13); ISFS@80169C18 stw r0,-0x6490(r13) # isfsHeapInitialized, 4 sites in RMCE01
80386774 803823F4 ISFS@80169B74 stw r3,-0x648C(r13); ISFS@80169BAC lwz r3,-0x648C(r13); ISFS@80169BD0 lwz r30,-0x648C(r13) # isfsHeapBase, 3 sites in RMCE01
8038677C 803823FC ISFS@80169B54 stw r3,-0x6484(r13); ISFS@80169B60 lwz r3,-0x6484(r13) # __IPCArenaLo, 2 sites in RMCE01
80386780 80382400 ISFS@80169B5C stw r3,-0x6480(r13); ISFS@80169B7C lwz r0,-0x6480(r13); ISFS@80169BDC lwz r0,-0x6480(r13) # __IPCArenaHi, 3 sites in RMCE01
80386784 80382404 ISFS@80169C2C stw r3,-0x647C(r13); ISFS@80169DAC lwz r3,-0x647C(r13); ISFS@80169E30 lwz r3,-0x647C(r13); ISFS@80169EA0 lwz r3,-0x647C(r13) # isfsHeapHandle, 36 sites in RMCE01
803867EC 8038246C ISFS@80192F10 stw r3,-0x6414(r13); ISFS@80192F58 stw r3,-0x6414(r13); ISFS@80192F98 lwz r3,-0x6414(r13); ISFS@80192FA0 stw r3,-0x6414(r13) # ipcBufferLo, 4 sites in RMCE01
803867F0 80382470 ISFS@80192F0C stw r4,-0x6410(r13); ISFS@80192F54 stw r4,-0x6410(r13); ISFS@80192F90 lwz r3,-0x6410(r13) # ipcBufferHi, 3 sites in RMCE01
80385920 803815A0 ISFS@80169BBC stw r3,-0x72E0(r13); ISFS@80169E08 lwz r0,-0x72E0(r13); ISFS@80169E88 lwz r3,-0x72E0(r13); ISFS@80169F00 lwz r0,-0x72E0(r13) # __ISFS_fd, 33 sites in RMCE01
80385928 803815A8 ISFS@80169BA4 addi r4,r13,-0x72D8 # __ISFS_path, 1 sites in RMCE01
# OS alarm queue and the two AX state flags. Same story as the STM and ISFS globals above: the
# runtime's alarm stubs walk OSAlarmQueue and the AX stub sets two flags, and RMCK01 keeps this
# part of .sbss 0x20 lower than PAL/NTSC-U/NTSC-J (OSAlarmQueue is r13-0x6360 in PAL but
# r13-0x6340 in RMCK01), so a hardcoded offset walks a string table there instead of the queue.
803868A0 80382520 OSAlarmQueue@801A0540 addi r3,r13,-0x6360; OSAlarmQueue@801A054C stw r0,-0x6360(r13); OSAlarmQueue@801A064C lwz r7,-0x6360(r13); OSAlarmQueue@801A0698 stw r30,-0x6360(r13) # OSAlarmQueue, 13 sites in RMCE01
80386528 803821A8 AX@801268AC stw r0,-0x66D8(r13); AX@8012698C stw r12,-0x66D8(r13); AX@801269E4 lwz r0,-0x66D8(r13) # AX flag 0x66d8, 3 sites in RMCE01
80386524 803821A4 AX@80126804 lwz r0,-0x66DC(r13); AX@80126910 stw r0,-0x66DC(r13); AX@801269C0 stw r12,-0x66DC(r13) # AX flag 0x66dc, 3 sites in RMCE01
# The three .sdata halfwords __AXOutInitDSP copies into the AX DSPTaskInfo: the ucode length
# (task+0x10) and the DSP init/resume vectors (task+0x24/+0x26). The runtime's AX stub reads
# the same three when it builds the task.
80385800 80381480 __AXOutInitDSP@80126974 lhz r8,-0x7400(r13) -> sth r8,0x24(task) # dsp_init_vector
80385802 80381482 __AXOutInitDSP@8012697C lhz r0,-0x73fe(r13) -> sth r0,0x26(task) # dsp_resume_vector
80385804 80381484 __AXOutInitDSP@8012696C lhz r31,-0x73fc(r13) -> stw r31,0x10(task) # iram_length (ucode length)
# EGG fog constants in .sdata2, read through r2 by the EGG::StateGX fog-quad code. Unlike the
# .sbss globals above, the r2 offset of these three is 8 LARGER in NTSC-U than in PAL, while
# NTSC-J and NTSC-K keep PAL's offset. The values settle it: at the addresses below all four
# regions read 0.0 / 0.99 / 0.5, whereas NTSC-U at PAL's offset reads 176.0 / 176.0 / 1.0 -
# so the runtime's old hardcoded PAL r2 offsets were reading the wrong constants on NTSC-U only.
80388DD0 80384A48 EGG@8022D3F0 lfs r0,-0x61D8(r2); EGG@8022D71C lfs r2,-0x61D8(r2); EGG@8022D7A0 lfs r5,-0x61D8(r2); EGG@8022DB0C lfs r31,-0x61D8(r2) # EGG fog origin (m!=2), 9 sites in RMCE01
80388DE0 80384A58 EGG@8022DB24 lfs r0,-0x61C8(r2); EGG@8022DEBC lfs r0,-0x61C8(r2); EGG@8022E018 lfs r0,-0x61C8(r2) # EGG fog span, 3 sites in RMCE01
80388DF4 80384A6C EGG@8022DE7C lfs r31,-0x61B4(r2); EGG@8022DE90 lfs r30,-0x61B4(r2); EGG@8022DFD8 lfs r31,-0x61B4(r2); EGG@8022DFEC lfs r30,-0x61B4(r2) # EGG fog origin (m==2), 4 sites in RMCE01
8029CEB0 80298B70 SCGetProductArea@801b2320 lis r30,0x802a; @801b2324 addi r30,r30,-0x7490 (=0x80298B70, SC area table; 13 records identical to PAL)
8029CEF8 80298BB8 SCGetProductGameRegion@801b2448 lis r30,0x802a; @801b244c addi r30,r30,-0x7448 (=0x80298BB8, SC game region table; 4 records identical to PAL)
-72
View File
@@ -1,72 +0,0 @@
schema_version: 1
workspace_root: ../..
# Mario Kart Wii NTSC-U (RMCE01). The same translation surface as projects/mkwii/recomp.yml, with
# the facts that differ per region: the small-data bases the DOL's __init_registers installs, the
# StaticR.rel load address and the clean-input digests. The function map is the PAL map ported
# through the community region table and validated against these binaries.
project:
id: mkwii-rmce01
display_name: Mario Kart Wii NTSC-U
game_id: RMCE01
region: E
base_manifest_format: mkwii-recomp-base-manifest
base_manifest_stem: mkwii_base
memory:
base: 0x80000000
size: 0x01A00000
sda_base: 0x80388880
sda2_base: 0x8038AC20
inputs:
dol:
path: Assets/main.dol
sha256: d2beec1b1645fcd134efe9e7e63774b546667764ed8d431029daccd725995694
rel:
path: Assets/StaticR.rel
load_address: 0x8050BF60
sha256: 1168107f8fdef27a356df76036db55afe4fbf7752606dbac991726701133a617
translation:
entry_points:
- 0x800060A4
function_map:
path: projects/mkwii-ntsc-u/MAP.txt
allow_unsupported_instructions: false
runtime:
native_registration_root: runtime/src
native_abi_directories:
- runtime/src/hle/gx
output:
root: generated
functions: functions
runtime_config: RuntimeConfig.h
data_initializer: data_sections_init.cpp
base_manifest: ../build/base/mkwii_base_manifest.json
# Code.pul carries a separate chunk for this region. The regional link base determines
# the branch bytes checked at startup; execution is remapped to module_guest_base.
profiles:
retro-rewind:
enabled: true
requires_game_id: RMCE01
requires_dol_sha256: d2beec1b1645fcd134efe9e7e63774b546667764ed8d431029daccd725995694
code_pul: PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul
mod_root: PulsarPacks/completed/RetroRewind/RetroRewind6
region: E
module_guest_base: 0x81800000
module_link_base: 0x80394F60
output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true
retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCED00
retro_wfc_legacy_bootstrap_hook: 0x800ED648
riivolution:
xml: xml/RetroRewind6.xml
options:
- section: Retro Rewind
option: Pack
choice: 1
File diff suppressed because it is too large Load Diff
+124 -122
View File
@@ -1,79 +1,27 @@
cmake_minimum_required(VERSION 3.25)
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
# initializes the Apple toolchain so direct developer CMake invocations cannot
# accidentally inherit the running SDK's deployment version. This cache entry
# is harmless on non-Apple platforms and remains overridable by a caller.
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
"Minimum macOS version supported by WiiCompiled" FORCE)
endif()
cmake_minimum_required(VERSION 3.25)
project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
message(FATAL_ERROR
"WiiCompiled supports one macOS architecture per build directory; "
"configure separate arm64 and x86_64 build directories")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 _mkw_osx_arch)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}" CACHE STRING "Target processor architecture" FORCE)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}")
endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
set(MKW_PLATFORM_MACOS TRUE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
else()
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS 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
@@ -94,8 +42,6 @@ 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
@@ -113,6 +59,20 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
if(MKW_PLATFORM_LINUX)
add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco)
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
endif()
# Runtime configuration is real TOML, parsed by toml11 rather than a project-
# specific line parser. Keep it header-only and vendored so disconnected release
# builds have exactly the same parser as developer builds.
@@ -209,18 +169,6 @@ endif()
# Fast-math may erase them and change guest-visible integer conversions.
set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off)
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/fpu_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
# Match the optimization policy of the translated callers as well as their FP policy.
set_property(SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp" APPEND PROPERTY
COMPILE_OPTIONS -O2 -fno-slp-vectorize)
# ----------------------------------------------------------------------
# Third-party: aurora-main (provides SDL3 + GPU backends)
# ----------------------------------------------------------------------
@@ -275,19 +223,6 @@ else()
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST OFF CACHE BOOL "" FORCE)
# Aurora components the runtime does not use. They would otherwise fetch
# extra dependencies at configure time, which offline builds cannot do.
set(AURORA_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
set(AURORA_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(AURORA_ENABLE_DVD OFF CACHE BOOL "" FORCE)
set(AURORA_ENABLE_THP OFF CACHE BOOL "" FORCE)
set(AURORA_ENABLE_RMLUI OFF CACHE BOOL "" FORCE)
set(AURORA_ENABLE_CARD OFF CACHE BOOL "" FORCE)
# The runtime implements the Wii SDK itself; Aurora stays on its GameCube SDK.
set(AURORA_TARGET_RVL OFF CACHE BOOL "" FORCE)
# Every dependency comes from the pinned sources, never the build host.
set(AURORA_USE_SYSTEM_DEPENDENCIES OFF CACHE BOOL "" FORCE)
# MinGW/WSL can choke on SDL precompiled headers; disable PCH for aurora/SDL.
set(CMAKE_DISABLE_PRECOMPILE_HEADERS ON)
# Isolate aurora build products to a subdirectory under the main build dir.
@@ -324,16 +259,20 @@ 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;
# * 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.
# * 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.
if(MKW_PROJECT_COMPILE_DEFINITIONS)
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
endif()
@@ -346,14 +285,12 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
@@ -362,25 +299,91 @@ 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
# the runtime library. See cmake/PublicProducts.cmake and the file's own header.
# mkw_runtime_common. 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})
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
# 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)
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/RuntimeLibrary.cmake")
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
# The prebuilt export also carries the game-independent runtime as a static
# archive (x86_64 only; AArch64 compiles it locally with -mcpu=native). It is
# defined here, after the project definitions, so its flags match the products'.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT MKW_NATIVE_PREBUILT_DIR)
if(MKW_HOST_IS_X86_64)
mkw_add_runtime_library(mkw_runtime STATIC ${SOURCES})
set_target_properties(mkw_runtime PROPERTIES EXCLUDE_FROM_ALL ON)
endif()
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming
@@ -390,8 +393,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 and the runtime
# configured above, so a packaging machine without a translation stops here.
# The prebuilt export only needs the aurora/third-party closure configured above, so a
# packaging machine without a translation stops here instead of failing.
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()
@@ -408,13 +411,20 @@ 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.
# runtime/src is game-independent; per-game glue lives in runtime/game.
add_library(mkw_macos_native_compile OBJECT ${SOURCES})
# 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})
target_include_directories(mkw_macos_native_compile PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/.." "${MKW_AURORA_DIR}/include")
@@ -422,15 +432,7 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
target_compile_options(mkw_macos_native_compile PRIVATE
-march=x86-64-${MKW_X86_CPU_PROFILE})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(mkw_macos_native_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
mkw::pugixml mkw::toml11 mkw::cryptopp)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
Binary file not shown.
-29
View File
@@ -1,29 +0,0 @@
# 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()
+4 -8
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. 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.
# 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.
#
# The aurora and dependency source trees are still present in the workspace - the
# runtime includes aurora headers directly - so the package records include
@@ -42,10 +42,6 @@ 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,11 +87,6 @@ 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.
@@ -173,12 +168,5 @@ 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}")
-198
View File
@@ -1,198 +0,0 @@
get_filename_component(MKW_TEST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${MKW_TEST_RUNTIME_DIR}/tests/psq_helpers_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/tests/psq_memory"
"${MKW_TEST_RUNTIME_DIR}/include/isa"
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 -fno-fast-math -ffp-contract=off -fno-slp-vectorize)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_compile_options(mkw_psq_helpers_tests PRIVATE
-march=x86-64-${MKW_X86_CPU_PROFILE})
endif()
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
-P "${MKW_TEST_RUNTIME_DIR}/tests/psq_reserved_tests.cmake")
endif()
add_executable(mkw_platform_paths_tests "${MKW_TEST_RUNTIME_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_runtime_config_tests "${MKW_TEST_RUNTIME_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/toml11")
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
add_executable(mkw_nand_save_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${MKW_TEST_RUNTIME_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${MKW_TEST_RUNTIME_DIR}/tests/test_expr.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# The emulated Bluetooth controller is pure host code.
add_executable(mkw_bluetooth_controller_tests
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_controller.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_remote_link.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_sdp.cpp")
target_include_directories(mkw_bluetooth_controller_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_bluetooth_controller_tests PRIVATE cxx_std_20)
add_test(NAME mkw_bluetooth_controller_tests COMMAND mkw_bluetooth_controller_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_WINDOWS)
add_executable(mkw_windows_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_windows_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_windows_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_windows_host_context_tests COMMAND mkw_windows_host_context_tests)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# Probe at the base ISA, including OS support for AVX register state. Cross
# builds execute target code only through a configured emulator.
if(NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
include(CheckCXXSourceRuns)
include(CMakePushCheckState)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_FLAGS "-march=x86-64")
check_cxx_source_runs([=[
#include <cpuid.h>
int main() {
unsigned a, b, c, d;
if (__get_cpuid_max(0, nullptr) < 7 ||
__get_cpuid_max(0x80000000u, nullptr) < 0x80000001u)
return 1;
__cpuid_count(1, 0, a, b, c, d);
// v2 plus FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C.
const unsigned leaf1 = (1u << 0) | (1u << 9) | (1u << 12) |
(1u << 13) | (1u << 19) | (1u << 20) | (1u << 22) |
(1u << 23) | (1u << 26) | (1u << 27) | (1u << 28) | (1u << 29);
if ((c & leaf1) != leaf1) return 1;
__cpuid_count(7, 0, a, b, c, d);
const unsigned leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
if ((b & leaf7) != leaf7) return 1;
__cpuid_count(0x80000001u, 0, a, b, c, d);
if ((c & 0x21u) != 0x21u) return 1; // LAHF-SAHF and LZCNT.
__asm__ __volatile__("xgetbv" : "=a"(a), "=d"(d) : "c"(0));
return (a & 0x6u) == 0x6u ? 0 : 1;
}
]=] MKW_HOST_SUPPORTS_X86_V3)
cmake_pop_check_state()
endif()
# Compile the same oracle cases for both paths. The v2 binary proves the
# scalar fallback stays free of FMA; the v3 binary checks the existing
# vector intrinsic path against the same strict result.
foreach(profile IN ITEMS v2 v3)
add_executable(mkw_ppc_pair_fma_${profile}_tests
"${MKW_TEST_RUNTIME_DIR}/tests/ppc_pair_fma_tests.cpp")
target_include_directories(mkw_ppc_pair_fma_${profile}_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_ppc_pair_fma_${profile}_tests PRIVATE cxx_std_17)
target_compile_options(mkw_ppc_pair_fma_${profile}_tests PRIVATE
-march=x86-64-${profile} -fno-fast-math -ffp-contract=off)
if((NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR) AND
(profile STREQUAL "v2" OR MKW_HOST_SUPPORTS_X86_V3))
add_test(NAME mkw_ppc_pair_fma_${profile}_tests COMMAND mkw_ppc_pair_fma_${profile}_tests)
endif()
endforeach()
add_library(mkw_cpu_baseline_v2_compile OBJECT
"${MKW_TEST_RUNTIME_DIR}/src/host_cpu_baseline.cpp")
target_compile_features(mkw_cpu_baseline_v2_compile PRIVATE cxx_std_17)
target_compile_definitions(mkw_cpu_baseline_v2_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
target_compile_options(mkw_cpu_baseline_v2_compile PRIVATE -march=x86-64-v2 -w)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the public host-memory contracts separately from translated products.
if(MKW_PLATFORM_MACOS_ARM64)
# Apple Silicon's context ABI is implemented by the local assembly backend.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_context_abi_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
add_executable(mkw_macos_external_audio_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_external_audio_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
endif()
+116 -97
View File
@@ -1,45 +1,57 @@
# Public WiiCompiled product graph.
# Public WiiCompiled product graph.
#
# The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct
# ABI differs between profiles receive small base/RR variants.
# Per-game data the runtime reads but does not own. Kept out of the runtime
# library so that library stays game-independent (and can be precompiled).
set(MKW_GAME_DATA_SOURCES "")
set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp")
set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S")
if(EXISTS "${DATA_INIT_FILE}")
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_FILE}")
list(APPEND 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 MKW_GAME_DATA_SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
else()
list(APPEND MKW_GAME_DATA_SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
list(APPEND 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 MKW_GAME_DATA_SOURCES "${DATA_INIT_BLOB_ASM}")
list(APPEND SOURCES "${DATA_INIT_BLOB_ASM}")
endif()
# 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})
list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
function(mkw_apply_translated_compile_options target)
target_compile_options(${target} PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
endfunction()
function(mkw_configure_object_target target)
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
# Workspace root, so translator output is spelled "generated/<x>.h"
# instead of a ../ chain whose depth depends on the includer.
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_AURORA_DIR}/include")
target_compile_definitions(${target} PRIVATE
TARGET_PC)
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
endfunction()
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
# comparatively small. A dedicated Ninja job pool caps how many of them run at once so the
# global parallelism can use every core for the cheap TUs without the memory ceiling being a
@@ -55,77 +67,89 @@ function(mkw_bound_translated_compiles target)
endif()
endfunction()
# The region the translator built for; translated code and runtime/game bind addresses to it.
if(NOT DEFINED MKW_TRANSLATED_GUEST_REGION)
message(FATAL_ERROR "The translator shard manifest names no guest region; re-run emit-build-shards.")
endif()
function(mkw_configure_translated_target target)
mkw_configure_object_target(${target})
target_compile_definitions(${target} PRIVATE MKW_TRANSLATED_GUEST_REGION=${MKW_TRANSLATED_GUEST_REGION})
mkw_apply_translated_compile_options(${target})
mkw_bound_translated_compiles(${target})
endfunction()
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
MKW_TRANSLATED_GUEST_REGION=${MKW_TRANSLATED_GUEST_REGION}
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
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
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}\"")
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
target_include_directories(mkw_runtime_common PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
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)
# 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)
# Keep runtime unity units small and semantically related. The old generated-TU
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
foreach(source IN LISTS SOURCES)
get_filename_component(source_name "${source}" NAME_WE)
string(REPLACE "\\" "/" source_normalized "${source}")
if(source_normalized MATCHES "/hle/gx/")
set(runtime_group "gx_bridge")
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
set(runtime_group "network_ios")
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
set(runtime_group "guest_system")
elseif(source_name MATCHES "debug|trace|prof")
set(runtime_group "diagnostics")
else()
string(SHA256 source_hash "${source_name}")
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
set(runtime_group "runtime_${runtime_bucket}")
endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach()
# These translation units implement guest-visible floating-point bit
# semantics. Keep them out of the fast-math runtime unity groups and apply
# the same contraction/rounding policy as translated PPC shards.
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp"
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# this object deliberately keeps the plain baseline ISA and checks the CPU
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# floor to probe: NEON/FMA are architectural requirements.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
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_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)
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
if(MKW_BASE_PORTABLE_SENSITIVE_SHARDS)
add_library(mkw_base_sensitive OBJECT ${MKW_BASE_PORTABLE_SENSITIVE_SHARDS})
@@ -148,31 +172,16 @@ 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)
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>)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# 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(MKW_HOST_IS_X86_64)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
endif()
target_include_directories(${target} PRIVATE
@@ -186,6 +195,9 @@ 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)
@@ -193,9 +205,7 @@ function(mkw_configure_product target)
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}" "${MKW_COREAUDIO_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -208,15 +218,15 @@ function(mkw_configure_product target)
(MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR
MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY"))
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}> VERBATIM)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp bthprops)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer
@@ -242,7 +252,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}> VERBATIM)
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
endforeach()
endif()
@@ -251,7 +261,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" VERBATIM)
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
@@ -263,7 +273,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" VERBATIM)
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin")
# Aurora imports this portable recipe database into each user's writable
# pipeline cache. Keep the upstream filename so its default resourcesPath
@@ -275,7 +285,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" VERBATIM)
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
# 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
@@ -289,13 +299,12 @@ 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" VERBATIM)
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
endif()
endfunction()
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}")
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
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)
@@ -303,10 +312,9 @@ if(TARGET mkw_base_sensitive)
endif()
if(MKW_HAVE_RETRO_REWIND)
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
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()
@@ -320,12 +328,23 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif()
# mkw_runtime_common gets the same flags from mkw_add_runtime_library.
# 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()
set(MKW_ALL_BUILD_TARGETS
mkw_game_data mkw_base_shared mkw_base_registration mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions mkw_retro_registration WiiCompiled RetroRewind)
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions WiiCompiled RetroRewind)
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target})
mkw_apply_baseline_arch(${target})
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
endif()
endforeach()
-98
View File
@@ -1,98 +0,0 @@
# 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
@@ -1,180 +0,0 @@
# The game-independent native runtime (runtime/src). One definition serves
# three uses: the OBJECT library the products link from source, the STATIC
# archive the native prebuilt export ships (mkw_runtime), and that archive
# imported in place of compiling it (mkw_runtime_prebuilt).
include("${CMAKE_CURRENT_LIST_DIR}/RuntimeFingerprint.cmake")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_HOST_IS_X86_64 TRUE)
else()
set(MKW_HOST_IS_X86_64 FALSE)
endif()
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
# builds are compiled locally for the host that will run them, so both Linux
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
# target-specific performance on the table.
if(MKW_HOST_IS_X86_64)
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
set(MKW_BASELINE_ARCH_FLAG "")
endif()
if(MKW_PLATFORM_MACOS)
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
endif()
# CMake decides WHOLE_ARCHIVE's form by probing ld.lld in ELF mode, which
# accepts --push-state; the MinGW lld driver rejects it at link time.
if(MKW_PLATFORM_WINDOWS)
set(CMAKE_CXX_LINK_LIBRARY_USING_WHOLE_ARCHIVE
"LINKER:--whole-archive" "<LINK_ITEM>" "LINKER:--no-whole-archive")
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
function(mkw_apply_baseline_arch target)
if(MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
endfunction()
function(mkw_configure_object_target target)
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
# Workspace root, so translator output is spelled "generated/<x>.h"
# instead of a ../ chain whose depth depends on the includer.
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_AURORA_DIR}/include")
target_compile_definitions(${target} PRIVATE
TARGET_PC)
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
endfunction()
# scope is PRIVATE for a compiled target and INTERFACE for the imported archive.
function(mkw_runtime_link_dependencies target scope)
target_link_libraries(${target} ${scope}
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} ${scope} shell32 windowsapp bthprops)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
target_link_libraries(${target} ${scope} mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_PLATFORM_MACOS)
# CoreAudio framework is required for automatic music muting on macOS.
target_link_libraries(${target} ${scope} "${MKW_COREAUDIO_FRAMEWORK}")
if(MKW_PLATFORM_MACOS_X86_64)
# libco is used by Intel macOS. Apple Silicon uses the local
# x18-safe assembly backend and therefore does not define mkw::libco.
target_link_libraries(${target} ${scope} mkw::libco)
endif()
endif()
endfunction()
# Compiles the runtime sources as an OBJECT or STATIC library.
function(mkw_add_runtime_library target type)
set(_sources ${ARGN})
add_library(${target} ${type} ${_sources})
mkw_configure_object_target(${target})
target_compile_features(${target} PRIVATE cxx_std_20)
target_compile_definitions(${target} PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
mkw_runtime_link_dependencies(${target} PRIVATE)
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
target_include_directories(${target} PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
# Keep runtime unity units small and semantically related. The old generated-TU
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
foreach(source IN LISTS _sources)
get_filename_component(source_name "${source}" NAME_WE)
string(REPLACE "\\" "/" source_normalized "${source}")
if(source_normalized MATCHES "/hle/gx/")
set(runtime_group "gx_bridge")
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
set(runtime_group "network_ios")
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
set(runtime_group "guest_system")
elseif(source_name MATCHES "debug|trace|prof")
set(runtime_group "diagnostics")
else()
string(SHA256 source_hash "${source_name}")
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
set(runtime_group "runtime_${runtime_bucket}")
endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach()
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
set_target_properties(${target} PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
target_precompile_headers(${target} PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(${target})
mkw_apply_baseline_arch(${target})
endfunction()
function(mkw_import_prebuilt_runtime target archive)
add_library(${target} STATIC IMPORTED GLOBAL)
set_target_properties(${target} PROPERTIES IMPORTED_LOCATION "${archive}")
mkw_runtime_link_dependencies(${target} INTERFACE)
endfunction()
# Sets MKW_RUNTIME_USE_PREBUILT in the caller. The archive replaces the
# runtime's objects only when it was built from exactly this workspace's
# runtime inputs for the same x86-64 profile.
function(mkw_select_runtime_provider)
set(_archive "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
set(_exists FALSE)
if(_archive AND EXISTS "${_archive}")
set(_exists TRUE)
endif()
set(_fingerprint "")
if(_exists AND MKW_USE_PREBUILT_RUNTIME AND MKW_HOST_IS_X86_64 AND
MKW_NP_RUNTIME_CPU_PROFILE STREQUAL MKW_X86_CPU_PROFILE)
mkw_runtime_fingerprint(_fingerprint "${MKW_RUNTIME_SOURCE_DIR}"
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
endif()
mkw_runtime_prebuilt_decision(_use _reason
ARCHIVE "${_archive}"
ENABLED "${MKW_USE_PREBUILT_RUNTIME}"
X86_64 "${MKW_HOST_IS_X86_64}"
ARCHIVE_EXISTS "${_exists}"
PACKAGE_PROFILE "${MKW_NP_RUNTIME_CPU_PROFILE}"
PROFILE "${MKW_X86_CPU_PROFILE}"
PACKAGE_FINGERPRINT "${MKW_NP_RUNTIME_FINGERPRINT}"
FINGERPRINT "${_fingerprint}")
if(NOT MKW_NATIVE_PREBUILT_DIR)
set(_reason "no native prebuilt package is configured")
endif()
if(_use)
message(STATUS "Native runtime: precompiled archive ${_archive} (${_reason})")
# Any runtime edit must re-run configure so the mismatch is noticed.
mkw_runtime_fingerprint_inputs(_inputs "${MKW_RUNTIME_SOURCE_DIR}")
list(TRANSFORM _inputs PREPEND "${MKW_RUNTIME_SOURCE_DIR}/")
if(MKW_CPPWINRT_INCLUDE_DIR)
list(APPEND _inputs "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
endif()
set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_inputs})
else()
message(STATUS "Native runtime: compiled from source (${_reason})")
endif()
set(MKW_RUNTIME_USE_PREBUILT ${_use} PARENT_SCOPE)
endfunction()
-9
View File
@@ -1,9 +0,0 @@
#include "guest_abi/guest_region.h"
#if !defined(MKW_TRANSLATED_GUEST_REGION)
#error "MKW_TRANSLATED_GUEST_REGION comes from the translator's shard manifest (emit-build-shards)"
#endif
static_assert(MKW_TRANSLATED_GUEST_REGION >= 0 && MKW_TRANSLATED_GUEST_REGION < GuestRegion::kCount);
extern "C" const uint8_t kMkwGuestRegion = MKW_TRANSLATED_GUEST_REGION;
-8
View File
@@ -1,8 +0,0 @@
#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
@@ -1,7 +0,0 @@
// 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
+635 -11
View File
@@ -18,14 +18,638 @@
#include <tuple>
#include <type_traits>
#include "guest_abi/dispatch.h"
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
// 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();
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);
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);
}
template <typename Signature>
class AbiTrampoline;
@@ -157,14 +781,14 @@ public:
#define MKW_DETAIL_CAT(a, b) a##b
#define MKW_DETAIL_MAKE_UNIQUE(a, b) MKW_DETAIL_CAT(a, b)
// Three hand-written registration macros, given the PAL address. Generated code never registers
// per function; every translated function reaches the registry via the bulk
// BulkTranslatedFunctionRecord tables in the translator's *_registration TUs.
// Three hand-written registration macros. Generated code never registers per function; every
// translated function reaches the registry via the bulk BulkTranslatedFunctionRecord tables
// in the translator's *_registration TUs.
#define REGISTER_TRANSLATED_FUNCTION(address, fn) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_trampoline_, __COUNTER__)(::GuestRegion::Translate(address), #fn, fn, FunctionKind::BaseTranslated, false, kPpcAllNonvolatileFprMask, 0, 0, nullptr, KnownTranslatedCpuCall<address>::kMustRemainDynamicallyDispatchable)
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::BaseTranslated, false, kPpcAllNonvolatileFprMask, 0, 0, nullptr, KnownTranslatedCpuCall<address>::kMustRemainDynamicallyDispatchable)
#define REGISTER_NATIVE_FUNCTION(address, fn) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_, __COUNTER__)(::GuestRegion::Translate(address), #fn, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
#define REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(::GuestRegion::Translate(address), pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(address, pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
-31
View File
@@ -1,9 +1,7 @@
#pragma once
#include "friend_code_keyboard.h"
#include "settings_overlay.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -119,7 +117,6 @@ inline void ProcessAuroraEvents(const AuroraEvent* events) {
g_mkwDynamicAspectSurfacePending.store(true, std::memory_order_release);
}
settings_overlay::HandleEvents(events);
FriendCodeKeyboard::HandleEvents(events);
}
inline void ApplyPendingMkwDynamicAspectSurface() {
@@ -134,42 +131,14 @@ inline void ApplyPendingMkwDynamicAspectSurface() {
UpdateMkwDynamicAspectSurface(surfaceWidth, surfaceHeight);
}
void GxWorkerBeginFrame();
void GxWorkerEndFrame(AuroraImguiFrame* imgui);
void SetAuroraFrameGxBegun(bool begun);
inline bool BeginAuroraFrame() {
if (GxWorker::ShouldDefer()) {
// The window half can touch the surface and render targets the worker draws into.
GxWorker::Sync();
if (!aurora_begin_frame_window()) {
return false;
}
GxWorker::Submit([] { GxWorkerBeginFrame(); });
ApplyPendingMkwDynamicAspectSurface();
return true;
}
if (!aurora_begin_frame()) {
return false;
}
SetAuroraFrameGxBegun(true);
ApplyPendingMkwDynamicAspectSurface();
return true;
}
// Ends the frame; with the GX worker, the game thread joins it here so the next frame never
// overlaps guest memory the worker may still read.
inline void EndAuroraFrame() {
if (GxWorker::ShouldDefer()) {
AuroraImguiFrame* imgui = aurora_end_frame_window();
GxWorker::Submit([imgui] { GxWorkerEndFrame(imgui); });
GxWorker::Sync();
return;
}
aurora_end_frame();
SetAuroraFrameGxBegun(false);
}
// Poll Aurora events and update cached window/framebuffer dimensions.
inline void UpdateAuroraAndProcessEvents() {
ProcessAuroraEvents(aurora_update());
-19
View File
@@ -1,19 +0,0 @@
#pragma once
// HCI and L2CAP fields are little-endian.
#include <cstdint>
#include <vector>
namespace Bluetooth {
inline uint16_t Le16(const uint8_t* p) {
return static_cast<uint16_t>(p[0] | (p[1] << 8));
}
inline void PutLe16(std::vector<uint8_t>& out, uint16_t v) {
out.push_back(static_cast<uint8_t>(v));
out.push_back(static_cast<uint8_t>(v >> 8));
}
} // namespace Bluetooth
-153
View File
@@ -1,153 +0,0 @@
#pragma once
// Emulated Bluetooth controller behind the Wii's internal USB module
// (/dev/usb/oh1/57e/305). The game's own Broadcom stack (BTE), WUD, WPAD and KPAD
// talk HCI to it; each of the four virtual Wii Remotes it hosts is backed by a
// physical remote on the PC (see RemoteHost), whose HID reports are passed
// through unchanged. Pure host code: no guest memory, so it is unit-testable.
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <vector>
namespace Bluetooth {
// Bluetooth device address in HCI (least significant byte first) order.
using BdAddr = std::array<uint8_t, 6>;
inline constexpr size_t kRemoteSlots = 4;
enum class LinkState : uint8_t {
Down, // no baseband connection to the game
Connecting, // baseband up, HID channels being opened
Ready, // HID control and interrupt channels open
};
// The physical side of the virtual remotes. Called from the thread that drives
// the Controller; implementations synchronize with their own I/O threads.
class RemoteHost {
public:
virtual ~RemoteHost() = default;
// A physical remote is attached to `slot` and can carry a connection.
virtual bool IsAttached(size_t slot) const = 0;
// True once each time the remote on `slot` asks to connect (it was just
// attached, or a button was pressed while it had no connection).
virtual bool ConsumeWake(size_t slot) = 0;
// Next input report (report id first, no HID header), false when none.
virtual bool PopInputReport(size_t slot, std::vector<uint8_t>& report) = 0;
// Output report from the game (report id first, no HID header).
virtual void SendOutputReport(size_t slot, const uint8_t* report, size_t size) = 0;
virtual void OnLinkStateChanged(size_t slot, LinkState state) = 0;
// The game ended a working connection; a real remote powers off here.
virtual void OnGuestDisconnect(size_t slot) = 0;
// The game is searching for remotes (its 1+2 / reconnect screens).
virtual void OnGuestInquiry(bool active) = 0;
// "Nintendo RVL-CNT-01" or "Nintendo RVL-CNT-01-TR".
virtual const char* DeviceName(size_t slot) const = 0;
};
class RemoteLink;
class Controller {
public:
explicit Controller(RemoteHost& host);
~Controller();
Controller(const Controller&) = delete;
Controller& operator=(const Controller&) = delete;
// The guest opened / closed the USB device.
void Open(uint64_t nowUs);
void Close();
bool IsOpen() const { return m_open; }
// Raw HCI command packet (opcode first, no H4 byte).
void ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs);
// Raw ACL packet from the guest (handle first).
void ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs);
// Advances timers, connection attempts, input forwarding and flow control.
void Tick(uint64_t nowUs);
bool HasEvent() const { return !m_events.empty(); }
bool PopEvent(std::vector<uint8_t>& event);
// ACL data is only handed out once every queued event has been delivered,
// so the guest always sees a connection before data on it.
bool PopAcl(std::vector<uint8_t>& packet);
size_t AclQueued() const { return m_acl.size(); }
LinkState SlotLinkState(size_t slot) const;
bool PageScanEnabled() const { return (m_scanEnable & 0x02) != 0; }
bool InquiryActive() const { return m_inquiry.active; }
static BdAddr LocalAddress();
static BdAddr RemoteAddress(size_t slot);
static uint16_t ConnectionHandle(size_t slot);
// Used by RemoteLink.
void QueueEvent(std::vector<uint8_t> event);
void QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame);
RemoteHost& Host() { return m_host; }
uint32_t PageTimeoutUs() const { return static_cast<uint32_t>(m_pageTimeoutSlots) * 625u; }
private:
struct StoredKey {
BdAddr addr{};
std::array<uint8_t, 16> key{};
};
struct Inquiry {
bool active = false;
uint64_t endUs = 0;
uint8_t maxResponses = 0;
uint8_t reported = 0;
std::array<bool, kRemoteSlots> slotReported{};
};
struct Reassembly {
std::vector<uint8_t> frame;
size_t expected = 0;
};
// A reply the controller sends after a delay (page / name request timeouts).
struct PendingEvent {
uint64_t dueUs = 0;
std::vector<uint8_t> event;
};
void ResetState(bool keepStoredKeys);
void CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params);
void CommandStatus(uint16_t opcode, uint8_t status);
void TickInquiry(uint64_t nowUs);
void FinishInquiry();
void SendNumberOfCompletedPackets();
int SlotForAddress(const BdAddr& addr) const;
int SlotForHandle(uint16_t handle) const;
const StoredKey* FindStoredKey(const BdAddr& addr) const;
void HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
void HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n);
void HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
void HandleInformational(uint16_t opcode, const uint8_t* p, size_t n);
void HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n);
void HandleVendor(uint16_t opcode, const uint8_t* p, size_t n);
void UnknownCommand(uint16_t opcode);
RemoteHost& m_host;
bool m_open = false;
uint8_t m_scanEnable = 0;
uint16_t m_pageTimeoutSlots = 0x2000;
uint8_t m_inquiryMode = 0;
std::array<uint8_t, 3> m_classOfDevice{};
std::array<char, 248> m_localName{};
Inquiry m_inquiry;
bool m_hostInquiryNotified = false;
std::vector<StoredKey> m_storedKeys;
std::array<std::unique_ptr<RemoteLink>, kRemoteSlots> m_links;
std::array<uint16_t, kRemoteSlots> m_completedPackets{};
std::array<Reassembly, kRemoteSlots> m_reassembly;
std::array<bool, kRemoteSlots> m_authPending{};
std::vector<PendingEvent> m_pending;
std::deque<std::vector<uint8_t>> m_events;
std::deque<std::vector<uint8_t>> m_acl;
};
} // namespace Bluetooth
-107
View File
@@ -1,107 +0,0 @@
#pragma once
// One emulated Wii Remote as seen from the controller: its baseband connection,
// L2CAP channels (SDP, HID control, HID interrupt) and the HID transaction layer
// that carries the physical remote's reports.
#include "bluetooth/bt_controller.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Bluetooth {
class RemoteLink {
public:
enum class Baseband : uint8_t {
Idle,
Requesting, // Connection Request sent, waiting for the guest to accept
Paging, // the guest paged this remote (Create Connection)
Connected,
};
RemoteLink(Controller& controller, size_t slot);
// Drops every connection without telling the guest (controller reset/close).
void Reset();
void Tick(uint64_t nowUs);
// Remote side wants a connection and should be offered to the guest's inquiry.
bool Discoverable() const;
bool ConnectWanted() const { return m_connectWanted; }
// Guest accepted / rejected this remote's Connection Request.
bool IsRequesting() const { return m_baseband == Baseband::Requesting; }
void OnAccepted(uint8_t role);
void OnRejected(uint8_t reason, uint64_t nowUs);
// Guest paged this remote. Returns false when the remote cannot answer.
bool OnPaged(uint64_t nowUs);
// Baseband went down. `byGuest` = the guest's HCI Disconnect.
void OnDisconnected(bool byGuest, uint64_t nowUs);
// The physical remote vanished while connected: tells the guest.
void DropFromRemote(uint8_t reason);
void ReceiveL2cap(const uint8_t* frame, size_t size);
Baseband BasebandState() const { return m_baseband; }
LinkState State() const { return m_state; }
private:
struct Channel {
uint16_t psm = 0;
uint16_t localCid = 0;
uint16_t remoteCid = 0;
uint16_t remoteMtu = 672;
bool weInitiated = false;
bool connected = false; // connection response success seen/sent
bool configSent = false; // our Configuration Request is out
bool localConfigured = false; // the guest accepted our configuration
bool remoteConfigured = false; // we accepted the guest's configuration
uint8_t connectIdent = 0;
uint8_t configIdent = 0;
uint64_t requestUs = 0;
std::vector<uint8_t> pendingOptions; // continued Configuration Request
bool Open() const { return connected && localConfigured && remoteConfigured; }
};
Channel* FindByLocal(uint16_t cid);
Channel* FindByPsm(uint16_t psm);
uint16_t AllocateCid() const;
uint8_t NextIdent();
void SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data);
void SendData(const Channel& channel, const std::vector<uint8_t>& payload);
void RequestChannel(uint16_t psm, uint64_t nowUs);
void SendConfigRequest(Channel& channel);
void HandleSignalling(const uint8_t* data, size_t size);
void HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n);
void HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n);
void HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleDisconnectionResponse(const uint8_t* p, size_t n);
void HandleChannelData(Channel& channel, const uint8_t* data, size_t size);
void HandleHidControl(Channel& channel, const uint8_t* data, size_t size);
void UpdateState();
void SetState(LinkState state);
void ForwardInput();
void SendInput(const Channel& interrupt, const std::vector<uint8_t>& report);
void ClearConnection();
Controller& m_controller;
size_t m_slot;
Baseband m_baseband = Baseband::Idle;
LinkState m_state = LinkState::Down;
bool m_connectWanted = false;
bool m_guestInitiated = false;
bool m_everReady = false;
uint8_t m_ident = 0;
uint64_t m_stateSinceUs = 0;
uint64_t m_retryAtUs = 0;
uint64_t m_nowUs = 0;
std::vector<Channel> m_channels;
std::vector<uint8_t> m_report;
};
} // namespace Bluetooth
-19
View File
@@ -1,19 +0,0 @@
#pragma once
// SDP server of an emulated Wii Remote: the HID service record (0x00010000)
// and the Device ID record (0x00010001) a real RVL-CNT-01 publishes.
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Bluetooth::Sdp {
// The Wii Remote's HID report descriptor, as the game also embeds it.
const std::vector<uint8_t>& WiiRemoteHidDescriptor();
// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the
// guest's L2CAP MTU on the SDP channel).
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse);
} // namespace Bluetooth::Sdp
-14
View File
@@ -1,14 +0,0 @@
#pragma once
namespace MusicAttenuation {
struct MacOSAudioStatus {
bool available = false;
bool playing = false;
};
// Queries output activity without capturing audio. Requires Core Audio process
// objects (macOS 14.2+); older systems return an unavailable sample.
MacOSAudioStatus QueryMacOSExternalAudio() noexcept;
} // namespace MusicAttenuation
-8
View File
@@ -1,8 +0,0 @@
#pragma once
#include <aurora/event.h>
// Lets the keyboard type into Retro Rewind's friend code entry page.
namespace FriendCodeKeyboard {
void HandleEvents(const AuroraEvent* events) noexcept;
} // namespace FriendCodeKeyboard
-4
View File
@@ -1,4 +0,0 @@
#pragma once
// Bump whenever guest_abi/, isa/ or ppc_isa_memory.h changes what translated code compiles against.
#define MKW_GUEST_ABI_VERSION 2
-84
View File
@@ -1,84 +0,0 @@
#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;
}
-617
View File
@@ -1,617 +0,0 @@
#pragma once
#include "guest_abi/cpu_context.h"
#include "guest_abi/exec_guard.h"
#include "guest_abi/guest_region.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 (::GuestRegion::Is<0x8023BD38u>(target)) {
// 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_;
};
// OSLoadContext 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 bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
return info->kind == FunctionKind::Native && info->address != MKW_GADDR(801A1F58);
}
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
@@ -1,48 +0,0 @@
#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
@@ -1,59 +0,0 @@
#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
@@ -1,19 +0,0 @@
#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"
@@ -1,7 +0,0 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <cmath>
#include "guest_abi/guest_abi.h"
-66
View File
@@ -1,66 +0,0 @@
#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;
}
-166
View File
@@ -1,166 +0,0 @@
#pragma once
// Guest addresses are named by their PAL (RMCP01) address. Which address that identity has
// depends on the region the game was translated from, which runtime/game supplies at link
// time, so one compiled runtime serves every region.
//
// MKW_GADDR(801A9E84) address of that identity in the running region
// MKW_GADDR_CT(801A9E84) same, as a constant expression; translated code only
//
// guest_versions.inc maps PAL ranges to the other regions. A PAL address outside every range
// of a region does not compile.
#include <cstddef>
#include <cstdint>
namespace GuestRegion {
enum class Id : uint8_t {
Pal = 0,
NtscU = 1,
NtscJ = 2,
};
inline constexpr size_t kCount = 3;
struct Row {
uint32_t address[kCount];
constexpr uint32_t In(Id region) const { return address[static_cast<size_t>(region)]; }
};
struct Range {
Id region;
uint32_t first;
uint32_t last;
int32_t delta;
};
inline constexpr Range kRanges[] = {
#define MKW_REGION_RANGE(region, first, last, delta) {Id::region, 0x##first##u, 0x##last##u, delta},
#include "guest_abi/guest_versions.inc"
#undef MKW_REGION_RANGE
};
inline constexpr size_t kRangeCount = sizeof(kRanges) / sizeof(kRanges[0]);
constexpr bool Precedes(Id region, uint32_t address, const Range& range) {
return region < range.region || (region == range.region && address < range.first);
}
constexpr bool RangesAreOrdered() {
for (size_t i = 0; i < kRangeCount; ++i) {
const Range& range = kRanges[i];
if (range.region == Id::Pal || range.first > range.last) {
return false;
}
if (i != 0 && !Precedes(kRanges[i - 1].region, kRanges[i - 1].last, range)) {
return false;
}
}
return true;
}
static_assert(RangesAreOrdered(), "guest_versions.inc ranges must be sorted by region and PAL address without overlaps");
struct Mapping {
bool found;
uint32_t address;
};
constexpr Mapping Map(uint32_t pal, Id region) {
if (region == Id::Pal) {
return {true, pal};
}
size_t lo = 0;
size_t hi = kRangeCount;
while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
if (Precedes(region, pal, kRanges[mid])) {
hi = mid;
} else {
lo = mid + 1;
}
}
if (lo == 0) {
return {false, 0};
}
const Range& range = kRanges[lo - 1];
if (range.region != region || pal > range.last) {
return {false, 0};
}
return {true, static_cast<uint32_t>(static_cast<int64_t>(pal) + range.delta)};
}
constexpr Row RowOf(uint32_t pal) {
const Mapping ntscU = Map(pal, Id::NtscU);
const Mapping ntscJ = Map(pal, Id::NtscJ);
return ntscU.found && ntscJ.found ? Row{{pal, ntscU.address, ntscJ.address}}
: throw "PAL address outside guest_abi/guest_versions.inc";
}
} // namespace GuestRegion
// Defined by runtime/game for the translated region.
extern "C" const uint8_t kMkwGuestRegion;
namespace GuestRegion {
inline Id Current() noexcept { return static_cast<Id>(kMkwGuestRegion); }
template <uint32_t Pal>
inline uint32_t Address() noexcept {
constexpr Row row = RowOf(Pal);
if constexpr (row.address[1] == Pal && row.address[2] == Pal) {
return Pal;
} else {
return row.In(Current());
}
}
// Folds to false at compile time for a constant target that no region uses for Pal.
template <uint32_t Pal>
inline bool Is(uint32_t target) noexcept {
constexpr Row row = RowOf(Pal);
return (target == row.address[0] || target == row.address[1] || target == row.address[2]) &&
target == row.In(Current());
}
[[noreturn]] void FatalUnknownAddress(uint32_t pal) noexcept;
// For hand-written registrations, which name the PAL address.
inline uint32_t Translate(uint32_t pal) noexcept {
const Mapping mapping = Map(pal, Current());
if (!mapping.found) {
FatalUnknownAddress(pal);
}
return mapping.address;
}
struct Facts {
const char* gameId;
char letter;
uint32_t gameCode;
uint32_t viTvFormat;
uint32_t mem1ArenaLo;
// setting.txt defaults for a new NAND
const char* area;
const char* model;
const char* productCode;
const char* video;
const char* gameRegion;
};
inline constexpr Facts kFacts[kCount] = {
{"RMCP01", 'P', 0x524D4350u, 1u, 0x80399180u, "EUR", "RVL-001(EUR)", "LEH", "PAL", "EU"},
{"RMCE01", 'E', 0x524D4345u, 0u, 0x80394E00u, "USA", "RVL-001(USA)", "LU", "NTSC", "US"},
{"RMCJ01", 'J', 0x524D434Au, 0u, 0x80398B00u, "JPN", "RVL-001(JPN)", "LJ", "NTSC", "JP"},
};
inline const Facts& CurrentFacts() noexcept { return kFacts[static_cast<size_t>(Current())]; }
} // namespace GuestRegion
#define MKW_GADDR(pal) (::GuestRegion::Address<0x##pal##u>())
#if defined(MKW_TRANSLATED_GUEST_REGION)
#define MKW_GADDR_CT(pal) \
(::GuestRegion::RowOf(0x##pal##u).In(static_cast<::GuestRegion::Id>(MKW_TRANSLATED_GUEST_REGION)))
#endif
@@ -1,416 +0,0 @@
// PAL address ranges -> address in each other region, in the style of Retro Rewind's
// versions.txt: MKW_REGION_RANGE(region, first PAL, last PAL (inclusive), delta).
// PAL maps to itself. Code ranges come from mkw-sp's port.py chunk table (MIT, Pablo Stebler),
// data ranges from Retro Rewind's versions.txt plus the verified data_addresses.txt anchors.
// An address outside every range has no known counterpart and fails to compile.
MKW_REGION_RANGE(NtscU, 80000000, 8000802F, +0x0)
MKW_REGION_RANGE(NtscU, 8000808C, 8000ADBB, -0x40)
MKW_REGION_RANGE(NtscU, 8000AF24, 8000B6B3, -0xA0)
MKW_REGION_RANGE(NtscU, 8000B6B4, 8000C173, +0x15994)
MKW_REGION_RANGE(NtscU, 8000C174, 80021BA7, -0xB60)
MKW_REGION_RANGE(NtscU, 80021BA8, 80225F1B, -0xA0)
MKW_REGION_RANGE(NtscU, 802261F8, 802402DF, -0x384)
MKW_REGION_RANGE(NtscU, 802402E0, 80240E17, +0x3F1C)
MKW_REGION_RANGE(NtscU, 80240E18, 80244DD3, -0xEBC)
MKW_REGION_RANGE(NtscU, 80244DDA, 80244EBF, -0xA0)
MKW_REGION_RANGE(NtscU, 80244EC0, 80244ECF, -0x80)
MKW_REGION_RANGE(NtscU, 802572BF, 802575DF, -0x320)
MKW_REGION_RANGE(NtscU, 80257E64, 80258184, -0x320)
MKW_REGION_RANGE(NtscU, 80276054, 8029CEFB, -0x4340)
MKW_REGION_RANGE(NtscU, 802A3D60, 802A407F, -0x4368)
MKW_REGION_RANGE(NtscU, 802A4080, 80386F27, -0x4380)
MKW_REGION_RANGE(NtscU, 80386F48, 80386F8F, -0x4388)
MKW_REGION_RANGE(NtscU, 80386FA0, 80388BE3, -0x4380)
MKW_REGION_RANGE(NtscU, 80388DD0, 80388DF7, -0x4388)
MKW_REGION_RANGE(NtscU, 80389140, 8038917B, -0x4380)
MKW_REGION_RANGE(NtscU, 80399200, 80399400, -0x4380)
MKW_REGION_RANGE(NtscU, 805103B4, 80510A8F, -0x4380)
MKW_REGION_RANGE(NtscU, 80510B84, 8052D297, -0x4474)
MKW_REGION_RANGE(NtscU, 8052D298, 8052D96B, +0x22568)
MKW_REGION_RANGE(NtscU, 8052D96C, 8053D97B, -0x4B48)
MKW_REGION_RANGE(NtscU, 8053D97C, 8053E36F, +0x12558)
MKW_REGION_RANGE(NtscU, 8053E370, 8054FB2B, -0x553C)
MKW_REGION_RANGE(NtscU, 8054FB2C, 80550547, +0xD9C)
MKW_REGION_RANGE(NtscU, 80550548, 805537CB, -0x5F58)
MKW_REGION_RANGE(NtscU, 80553894, 8055572B, -0x6020)
MKW_REGION_RANGE(NtscU, 8055572C, 8056AB6B, -0x4380)
MKW_REGION_RANGE(NtscU, 8056AB6C, 8056B63B, +0x4357C)
MKW_REGION_RANGE(NtscU, 8056B63C, 8057402F, -0x4E50)
MKW_REGION_RANGE(NtscU, 80574030, 805758AB, +0x3AB88)
MKW_REGION_RANGE(NtscU, 80575A44, 80583F2B, -0x6864)
MKW_REGION_RANGE(NtscU, 80583F2C, 8059B5A3, -0x6824)
MKW_REGION_RANGE(NtscU, 8059B5A4, 8059EAA3, +0x14FE8)
MKW_REGION_RANGE(NtscU, 8059EAA4, 805A0557, -0x9D24)
MKW_REGION_RANGE(NtscU, 805A068C, 805A1477, +0x1343C)
MKW_REGION_RANGE(NtscU, 805A1488, 805A1863, +0x1342C)
MKW_REGION_RANGE(NtscU, 805A1864, 805A9EDB, -0xB038)
MKW_REGION_RANGE(NtscU, 805A9EDC, 805AB573, -0xB030)
MKW_REGION_RANGE(NtscU, 805AB574, 805ABC8F, -0xAF70)
MKW_REGION_RANGE(NtscU, 805ABC90, 805ADD97, -0xAF68)
MKW_REGION_RANGE(NtscU, 805ADD98, 805B900F, -0xAF28)
MKW_REGION_RANGE(NtscU, 805B9010, 805B92FF, -0x4380)
MKW_REGION_RANGE(NtscU, 805B9300, 805BAB3F, +0x664F4)
MKW_REGION_RANGE(NtscU, 805BAB40, 805BB2B7, -0x5BC0)
MKW_REGION_RANGE(NtscU, 805BB2B8, 805BD2D7, +0x65D7C)
MKW_REGION_RANGE(NtscU, 805BD2D8, 805BD2E3, -0x697C)
MKW_REGION_RANGE(NtscU, 805BD2E4, 805BD39B, +0x65D70)
MKW_REGION_RANGE(NtscU, 805BD39C, 805BE5FF, -0x7CA4)
MKW_REGION_RANGE(NtscU, 805BE600, 805BE61B, -0x7C98)
MKW_REGION_RANGE(NtscU, 805BE66C, 805BE7F3, +0x64AA0)
MKW_REGION_RANGE(NtscU, 805BE7F4, 805BE84B, -0x7124)
MKW_REGION_RANGE(NtscU, 805BE84C, 805BED67, +0x64A48)
MKW_REGION_RANGE(NtscU, 805BED68, 805BED73, -0x7948)
MKW_REGION_RANGE(NtscU, 805BED74, 805BF2D7, +0x64A3C)
MKW_REGION_RANGE(NtscU, 805BF2D8, 805BF2DB, -0x613C)
MKW_REGION_RANGE(NtscU, 805BF3CC, 805BFE1B, -0x89FC)
MKW_REGION_RANGE(NtscU, 805BFE1C, 805C00BF, -0x89F0)
MKW_REGION_RANGE(NtscU, 805C00C0, 805C1B33, -0x8998)
MKW_REGION_RANGE(NtscU, 805C1B34, 805C35F7, -0x8994)
MKW_REGION_RANGE(NtscU, 805C35F8, 805C3BCB, +0x60810)
MKW_REGION_RANGE(NtscU, 805C3BCC, 805C4767, -0x8F68)
MKW_REGION_RANGE(NtscU, 805C4768, 805C579B, +0x5FC74)
MKW_REGION_RANGE(NtscU, 805C57B0, 805CADDB, -0x9FB0)
MKW_REGION_RANGE(NtscU, 805CADDC, 805CC0A7, +0x5A648)
MKW_REGION_RANGE(NtscU, 805CC1B4, 805CD94B, +0x5A5A4)
MKW_REGION_RANGE(NtscU, 805CD94C, 805D1887, -0xCB20)
MKW_REGION_RANGE(NtscU, 805D1888, 805D1E83, +0x56668)
MKW_REGION_RANGE(NtscU, 805D1E84, 805D7F77, -0xD11C)
MKW_REGION_RANGE(NtscU, 805D7F78, 805DBFDB, +0x50574)
MKW_REGION_RANGE(NtscU, 805DBFDC, 805DC033, +0x2A620)
MKW_REGION_RANGE(NtscU, 805DC034, 805DE837, +0x5051C)
MKW_REGION_RANGE(NtscU, 805DE838, 805DE843, +0x5C00)
MKW_REGION_RANGE(NtscU, 805DE844, 805E0C37, +0x50510)
MKW_REGION_RANGE(NtscU, 805E0C38, 805E6B4F, -0x15DDC)
MKW_REGION_RANGE(NtscU, 805E6B50, 805E745F, +0x4A5F8)
MKW_REGION_RANGE(NtscU, 805E7460, 805EA77F, -0x166EC)
MKW_REGION_RANGE(NtscU, 805EA780, 805EEB67, +0x472D8)
MKW_REGION_RANGE(NtscU, 805EEB68, 805F2D23, -0x1AAD4)
MKW_REGION_RANGE(NtscU, 805F2D24, 805F8B33, +0x4311C)
MKW_REGION_RANGE(NtscU, 805F8B34, 805FB5BB, -0x208E4)
MKW_REGION_RANGE(NtscU, 805FB5BC, 805FB81F, +0x40694)
MKW_REGION_RANGE(NtscU, 805FB820, 805FD517, -0x20B48)
MKW_REGION_RANGE(NtscU, 805FD518, 806012AF, +0x3E99C)
MKW_REGION_RANGE(NtscU, 806012B0, 80608D17, -0x248E0)
MKW_REGION_RANGE(NtscU, 80608D18, 8060A72B, -0x248D4)
MKW_REGION_RANGE(NtscU, 8060A72C, 8061186B, +0x35520)
MKW_REGION_RANGE(NtscU, 8061186C, 80616843, -0x2BA14)
MKW_REGION_RANGE(NtscU, 80616844, 8061AE6B, +0x30548)
MKW_REGION_RANGE(NtscU, 8061AE6C, 8061E897, -0x3003C)
MKW_REGION_RANGE(NtscU, 8061E898, 8061F6E7, +0x2CB1C)
MKW_REGION_RANGE(NtscU, 8061F6E8, 80620CB3, -0x30E8C)
MKW_REGION_RANGE(NtscU, 80620D7C, 80634F47, -0x30EB4)
MKW_REGION_RANGE(NtscU, 80634F48, 80637493, -0x30E98)
MKW_REGION_RANGE(NtscU, 80637494, 80637513, -0x30DF0)
MKW_REGION_RANGE(NtscU, 80637514, 80637A2F, +0x14CF0)
MKW_REGION_RANGE(NtscU, 80637AC0, 8063BCF7, -0x313A0)
MKW_REGION_RANGE(NtscU, 8063BE40, 806453E3, -0x31420)
MKW_REGION_RANGE(NtscU, 806453E4, 806467D3, +0x73A8)
MKW_REGION_RANGE(NtscU, 806467D4, 8064A3F3, -0x32810)
MKW_REGION_RANGE(NtscU, 8064A3F4, 8064AEE3, +0x3788)
MKW_REGION_RANGE(NtscU, 8064AEF8, 80651BEB, -0x33314)
MKW_REGION_RANGE(NtscU, 80651BEC, 806522FF, -0x3580)
MKW_REGION_RANGE(NtscU, 80652300, 80653207, -0x33A14)
MKW_REGION_RANGE(NtscU, 80653208, 8065B353, -0x4488)
MKW_REGION_RANGE(NtscU, 8065B4E8, 8065C0EB, -0x461C)
MKW_REGION_RANGE(NtscU, 8065C0EC, 8065FA13, +0xC88)
MKW_REGION_RANGE(NtscU, 8065FA4C, 8065FE4B, -0x7F7C)
MKW_REGION_RANGE(NtscU, 8065FE4C, 80662777, -0x7F3C)
MKW_REGION_RANGE(NtscU, 80662778, 80663193, -0x20E4)
MKW_REGION_RANGE(NtscU, 80663194, 80665537, -0x8958)
MKW_REGION_RANGE(NtscU, 80665538, 80668813, +0xDC50)
MKW_REGION_RANGE(NtscU, 80668814, 8067906B, -0x7764)
MKW_REGION_RANGE(NtscU, 8067906C, 8067937F, -0x2C08)
MKW_REGION_RANGE(NtscU, 80679380, 8067ABFF, -0x7A78)
MKW_REGION_RANGE(NtscU, 8067AC00, 806BF4C7, -0x4488)
MKW_REGION_RANGE(NtscU, 806BF4C8, 806BFB13, +0x22BE0)
MKW_REGION_RANGE(NtscU, 806BFB14, 806C050B, -0x4AD4)
MKW_REGION_RANGE(NtscU, 806C050C, 806C35A7, +0x221E8)
MKW_REGION_RANGE(NtscU, 806C35A8, 806C3AA3, -0x7B70)
MKW_REGION_RANGE(NtscU, 806C3AA4, 806C4ED3, +0x21CEC)
MKW_REGION_RANGE(NtscU, 806C4EF0, 806C63A7, +0x21CD0)
MKW_REGION_RANGE(NtscU, 806C63B0, 806C6B43, +0x21CC8)
MKW_REGION_RANGE(NtscU, 806C6B44, 806CCE8F, -0xABF4)
MKW_REGION_RANGE(NtscU, 806CCE90, 806CE81F, +0x1B97C)
MKW_REGION_RANGE(NtscU, 806CE828, 806D02BB, -0xC58C)
MKW_REGION_RANGE(NtscU, 806D02BC, 806D28F3, +0x19EE0)
MKW_REGION_RANGE(NtscU, 806D2908, 806D5C5B, -0xEBD0)
MKW_REGION_RANGE(NtscU, 806D5C60, 806D5ED7, +0x16B74)
MKW_REGION_RANGE(NtscU, 806D5ED8, 806DA913, -0xEE40)
MKW_REGION_RANGE(NtscU, 806DA914, 806DB183, +0x12144)
MKW_REGION_RANGE(NtscU, 806DB184, 806DDA83, -0xF6B0)
MKW_REGION_RANGE(NtscU, 806DDA84, 806DEB3F, +0xF844)
MKW_REGION_RANGE(NtscU, 806DEB40, 806DF7CF, -0x1076C)
MKW_REGION_RANGE(NtscU, 806DF7D0, 806DFD13, +0xEBB4)
MKW_REGION_RANGE(NtscU, 806DFD14, 806E3A8B, -0x10CB0)
MKW_REGION_RANGE(NtscU, 806E3A8C, 806E3E1F, +0xAE3C)
MKW_REGION_RANGE(NtscU, 806E3E20, 806E95AF, -0x11044)
MKW_REGION_RANGE(NtscU, 806E95B0, 806EC7BF, +0x56AC)
MKW_REGION_RANGE(NtscU, 806EC7C0, 806ED53B, -0x1425C)
MKW_REGION_RANGE(NtscU, 806ED53C, 806F62FB, -0x14254)
MKW_REGION_RANGE(NtscU, 806F62FC, 806F7697, -0x4488)
MKW_REGION_RANGE(NtscU, 806F77C4, 806F7A53, +0x1C2B4)
MKW_REGION_RANGE(NtscU, 806F7AA8, 806F821F, +0x1C260)
MKW_REGION_RANGE(NtscU, 806F8220, 806F8933, -0x5010)
MKW_REGION_RANGE(NtscU, 806F8934, 806FA3FB, -0x4FBC)
MKW_REGION_RANGE(NtscU, 806FA3FC, 806FE23F, -0x4F68)
MKW_REGION_RANGE(NtscU, 806FE240, 806FE9DF, -0x4F10)
MKW_REGION_RANGE(NtscU, 806FE9E0, 807001A3, +0x15AA0)
MKW_REGION_RANGE(NtscU, 80700230, 80700473, +0x15A14)
MKW_REGION_RANGE(NtscU, 80700474, 8070E7B7, -0x69A4)
MKW_REGION_RANGE(NtscU, 8070E7B8, 8070F8B7, +0x76D0)
MKW_REGION_RANGE(NtscU, 8070F8B8, 807179C3, -0x7AA4)
MKW_REGION_RANGE(NtscU, 807179C4, 80717E33, -0x7A18)
MKW_REGION_RANGE(NtscU, 80717E34, 807182E7, -0xEAC)
MKW_REGION_RANGE(NtscU, 807182E8, 8071B86B, -0x7ECC)
MKW_REGION_RANGE(NtscU, 8071B86C, 80726573, -0x4430)
MKW_REGION_RANGE(NtscU, 80726574, 8072820B, +0x16068)
MKW_REGION_RANGE(NtscU, 8072821C, 807285C7, +0x16058)
MKW_REGION_RANGE(NtscU, 807285CC, 80729337, +0x16054)
MKW_REGION_RANGE(NtscU, 80729350, 80729B87, -0x720C)
MKW_REGION_RANGE(NtscU, 80729B88, 8072A893, -0x7204)
MKW_REGION_RANGE(NtscU, 8072A894, 8072B95B, -0x71FC)
MKW_REGION_RANGE(NtscU, 8072B95C, 8072DE63, -0x71EC)
MKW_REGION_RANGE(NtscU, 8072DFCC, 8072FF5F, +0x113C4)
MKW_REGION_RANGE(NtscU, 8072FF78, 80730197, +0x113AC)
MKW_REGION_RANGE(NtscU, 80730198, 80730A7F, -0x93B0)
MKW_REGION_RANGE(NtscU, 80730B40, 8073195F, +0x10A04)
MKW_REGION_RANGE(NtscU, 80731960, 80735947, -0xA1B8)
MKW_REGION_RANGE(NtscU, 80735948, 80738DB7, +0xCA1C)
MKW_REGION_RANGE(NtscU, 80738DB8, 8073C54B, -0xD628)
MKW_REGION_RANGE(NtscU, 8073C54C, 8073EDEF, +0x9288)
MKW_REGION_RANGE(NtscU, 8073EDF0, 8074C4A7, -0xFECC)
MKW_REGION_RANGE(NtscU, 8074C4A8, 8074D5B7, +0x20A04)
MKW_REGION_RANGE(NtscU, 8074D5B8, 807519C7, -0x5540)
MKW_REGION_RANGE(NtscU, 807519C8, 80754103, +0x1C5F4)
MKW_REGION_RANGE(NtscU, 80754104, 80758BDB, -0x7C7C)
MKW_REGION_RANGE(NtscU, 80758BDC, 8075DB23, +0x17B1C)
MKW_REGION_RANGE(NtscU, 8075DB3C, 8075E78B, -0xCBDC)
MKW_REGION_RANGE(NtscU, 8075E78C, 8075EAFB, +0x16ECC)
MKW_REGION_RANGE(NtscU, 8075EAFC, 80765C93, -0xCF4C)
MKW_REGION_RANGE(NtscU, 80765C94, 807678F3, +0xFD34)
MKW_REGION_RANGE(NtscU, 807678F4, 80768D1F, -0xEBAC)
MKW_REGION_RANGE(NtscU, 80768D20, 807693EB, +0xE908)
MKW_REGION_RANGE(NtscU, 8076960C, 8076C85B, +0xE85C)
MKW_REGION_RANGE(NtscU, 8076C85C, 8076EBDB, -0x126E8)
MKW_REGION_RANGE(NtscU, 8076EBE0, 8076F2DB, +0xC4D8)
MKW_REGION_RANGE(NtscU, 8076F2DC, 807726C3, -0x12DE8)
MKW_REGION_RANGE(NtscU, 80772704, 80773C13, +0x90B4)
MKW_REGION_RANGE(NtscU, 80773C1C, 8077439B, -0x14340)
MKW_REGION_RANGE(NtscU, 807743A0, 807787EF, -0x14294)
MKW_REGION_RANGE(NtscU, 807787F0, 8077902B, +0x4520)
MKW_REGION_RANGE(NtscU, 8077902C, 8077CE87, -0x14AD0)
MKW_REGION_RANGE(NtscU, 8077CEC8, 8077DF23, +0x684)
MKW_REGION_RANGE(NtscU, 8077DF24, 807829D7, -0x15B2C)
MKW_REGION_RANGE(NtscU, 807829D8, 80787D83, -0x4430)
MKW_REGION_RANGE(NtscU, 80787D84, 8078C95F, +0x48598)
MKW_REGION_RANGE(NtscU, 8078C960, 807A81B3, -0x900C)
MKW_REGION_RANGE(NtscU, 807A81B4, 807A9D6F, +0x2CDC0)
MKW_REGION_RANGE(NtscU, 807A9E50, 807A9E53, +0x2CCAC)
MKW_REGION_RANGE(NtscU, 807A9EB8, 807AF13F, -0xACA8)
MKW_REGION_RANGE(NtscU, 807AF140, 807B2EF7, +0x27A54)
MKW_REGION_RANGE(NtscU, 807B2EF8, 807D976B, -0xEA60)
MKW_REGION_RANGE(NtscU, 807D976C, 807D9B7F, +0x11E0)
MKW_REGION_RANGE(NtscU, 807D9B98, 807DA5BF, -0xEE8C)
MKW_REGION_RANGE(NtscU, 807DA5C0, 807DBCCB, +0x7B8)
MKW_REGION_RANGE(NtscU, 807DBCCC, 807DC8C7, -0x10598)
MKW_REGION_RANGE(NtscU, 807DC950, 807E093B, -0x10620)
MKW_REGION_RANGE(NtscU, 807E093C, 807E251F, +0x5BDB4)
MKW_REGION_RANGE(NtscU, 807E259C, 807E560F, -0x6090)
MKW_REGION_RANGE(NtscU, 807E5654, 807E6413, -0x6070)
MKW_REGION_RANGE(NtscU, 807E6414, 807E9C43, +0x57F28)
MKW_REGION_RANGE(NtscU, 807E9C50, 807EDD97, -0x98AC)
MKW_REGION_RANGE(NtscU, 807EDD98, 807EE23B, +0x53DD4)
MKW_REGION_RANGE(NtscU, 807EE250, 807EE467, -0x9D64)
MKW_REGION_RANGE(NtscU, 807EE474, 807EEA13, +0x53B9C)
MKW_REGION_RANGE(NtscU, 807EEA14, 807EF9F3, -0xA310)
MKW_REGION_RANGE(NtscU, 807EFD0C, 807F76EB, -0xA628)
MKW_REGION_RANGE(NtscU, 807F76EC, 807F7BA3, +0x4B1FC)
MKW_REGION_RANGE(NtscU, 807F7BC4, 807F890B, -0xAB00)
MKW_REGION_RANGE(NtscU, 807F8968, 807F927F, +0x4A438)
MKW_REGION_RANGE(NtscU, 807F9280, 807F9387, -0xB3F0)
MKW_REGION_RANGE(NtscU, 807F9580, 807FAB57, +0x4A320)
MKW_REGION_RANGE(NtscU, 807FAB58, 807FEB67, -0xCBC0)
MKW_REGION_RANGE(NtscU, 807FEB68, 807FFADF, +0x46310)
MKW_REGION_RANGE(NtscU, 807FFB20, 80805A0B, -0xDB78)
MKW_REGION_RANGE(NtscU, 80805A0C, 8080761B, +0x403E4)
MKW_REGION_RANGE(NtscU, 8080761C, 80809447, -0xF748)
MKW_REGION_RANGE(NtscU, 80809448, 8080AD1F, +0x3E5B8)
MKW_REGION_RANGE(NtscU, 8080AD20, 80811E47, -0x11020)
MKW_REGION_RANGE(NtscU, 80811E48, 80813BD3, +0x37490)
MKW_REGION_RANGE(NtscU, 80813BD4, 8081E283, -0x12DAC)
MKW_REGION_RANGE(NtscU, 8081E284, 8081EFEB, +0x2CDE0)
MKW_REGION_RANGE(NtscU, 8081EFEC, 8082E53F, -0x13B14)
MKW_REGION_RANGE(NtscU, 8082E540, 8082E853, +0x1D88C)
MKW_REGION_RANGE(NtscU, 8082E854, 8082F407, -0x13E28)
MKW_REGION_RANGE(NtscU, 8082F408, 8083349F, +0x1CCD8)
MKW_REGION_RANGE(NtscU, 808334E0, 80833AFF, -0x17F00)
MKW_REGION_RANGE(NtscU, 80833B00, 80838E5F, -0x17EF8)
MKW_REGION_RANGE(NtscU, 80838E60, 8083B0CB, +0x17358)
MKW_REGION_RANGE(NtscU, 8083B0CC, 8083CB43, -0x1A178)
MKW_REGION_RANGE(NtscU, 8083CB44, 8083D42B, +0x158E0)
MKW_REGION_RANGE(NtscU, 8083D42C, 8084233F, -0x1AA60)
MKW_REGION_RANGE(NtscU, 80842340, 808447AB, +0x109CC)
MKW_REGION_RANGE(NtscU, 808447AC, 8084A99F, -0x1CED8)
MKW_REGION_RANGE(NtscU, 8084A9A0, 8084D0DB, +0xA7E4)
MKW_REGION_RANGE(NtscU, 8084D0DC, 80851D2B, -0x1F614)
MKW_REGION_RANGE(NtscU, 80851D38, 80852C5F, +0x5B88)
MKW_REGION_RANGE(NtscU, 80852C60, 80853CA3, -0x20548)
MKW_REGION_RANGE(NtscU, 80853CA4, 808551EB, +0x4B50)
MKW_REGION_RANGE(NtscU, 808551EC, 8085C3CB, -0x21A90)
MKW_REGION_RANGE(NtscU, 8085C3CC, 8085E673, -0x2690)
MKW_REGION_RANGE(NtscU, 8085E674, 8085F0AB, -0x23D24)
MKW_REGION_RANGE(NtscU, 8085F0AC, 8085FFD3, -0x30C8)
MKW_REGION_RANGE(NtscU, 8085FFD4, 80860F2B, -0x24C4C)
MKW_REGION_RANGE(NtscU, 80860F2C, 80862E23, -0x4020)
MKW_REGION_RANGE(NtscU, 80863234, 8086708B, -0x4430)
MKW_REGION_RANGE(NtscU, 8086708C, 808676DF, -0x2354)
MKW_REGION_RANGE(NtscU, 808676E0, 808697BB, -0x4A84)
MKW_REGION_RANGE(NtscU, 808697BC, 8086A253, -0x4430)
MKW_REGION_RANGE(NtscU, 8086A254, 8086C097, +0xC4A0)
MKW_REGION_RANGE(NtscU, 8086C108, 8086C987, +0xC430)
MKW_REGION_RANGE(NtscU, 8086CA40, 80872CA3, +0xC378)
MKW_REGION_RANGE(NtscU, 80872CA4, 808739AF, -0xCE80)
MKW_REGION_RANGE(NtscU, 808739B0, 80875453, -0xCDC8)
MKW_REGION_RANGE(NtscU, 80875454, 8088344B, -0xCD58)
MKW_REGION_RANGE(NtscU, 8088344C, 8088F70F, -0x4430)
MKW_REGION_RANGE(NtscU, 8088FDB8, 8088FDBB, -0x4458)
MKW_REGION_RANGE(NtscU, 80890030, 80890215, -0x4470)
MKW_REGION_RANGE(NtscU, 80890490, 80890977, -0x4578)
MKW_REGION_RANGE(NtscU, 808913C0, 808913C3, -0x4448)
MKW_REGION_RANGE(NtscU, 80891750, 8089175F, -0x44D0)
MKW_REGION_RANGE(NtscU, 80891A28, 80891C28, -0x4518)
MKW_REGION_RANGE(NtscU, 80892F2C, 80892F96, -0x1B0)
MKW_REGION_RANGE(NtscU, 80893217, 8089321D, -0x4A30)
MKW_REGION_RANGE(NtscU, 808944DE, 80894769, -0x50E8)
MKW_REGION_RANGE(NtscU, 80894B98, 80894B9C, -0x1330)
MKW_REGION_RANGE(NtscU, 80895238, 808952B7, -0x58F8)
MKW_REGION_RANGE(NtscU, 80895A08, 80895C8C, -0x1930)
MKW_REGION_RANGE(NtscU, 80895CBC, 80895CF0, -0x5FA8)
MKW_REGION_RANGE(NtscU, 8089647A, 808966D4, -0x1CA0)
MKW_REGION_RANGE(NtscU, 80896820, 80897040, -0x6678)
MKW_REGION_RANGE(NtscU, 80897C63, 80897C67, -0x6DA0)
MKW_REGION_RANGE(NtscU, 80897DD3, 80897DEF, -0x6DA8)
MKW_REGION_RANGE(NtscU, 80898955, 80898959, -0x7388)
MKW_REGION_RANGE(NtscU, 80899692, 8089973A, -0x7730)
MKW_REGION_RANGE(NtscU, 808998B3, 808998CA, -0x3FB0)
MKW_REGION_RANGE(NtscU, 80899A36, 80899EEF, -0x7890)
MKW_REGION_RANGE(NtscU, 8089A194, 8089A1B7, -0x46D0)
MKW_REGION_RANGE(NtscU, 808A1D80, 808A1E6F, -0x4A08)
MKW_REGION_RANGE(NtscU, 808A22E0, 808A22EF, -0x46D0)
MKW_REGION_RANGE(NtscU, 808A4E00, 808A56FF, -0x49C0)
MKW_REGION_RANGE(NtscU, 808A587C, 808A5897, -0x2B20)
MKW_REGION_RANGE(NtscU, 808A5A30, 808A5DC3, -0x4AD8)
MKW_REGION_RANGE(NtscU, 808A7DE0, 808A85F3, -0x4CD8)
MKW_REGION_RANGE(NtscU, 808A98D0, 808A98DF, -0x5728)
MKW_REGION_RANGE(NtscU, 808A9AF8, 808A9AFF, +0x780)
MKW_REGION_RANGE(NtscU, 808A9C0A, 808A9CC8, -0x58C8)
MKW_REGION_RANGE(NtscU, 808A9FEC, 808AA1AF, -0x5928)
MKW_REGION_RANGE(NtscU, 808AAA04, 808AAA10, -0x5980)
MKW_REGION_RANGE(NtscU, 808ACB3F, 808ACB43, -0x5F10)
MKW_REGION_RANGE(NtscU, 808ACFDF, 808AD02B, -0x60C0)
MKW_REGION_RANGE(NtscU, 808AD3C4, 808AD3C7, -0x60C0)
MKW_REGION_RANGE(NtscU, 808AD48E, 808AD492, -0x2980)
MKW_REGION_RANGE(NtscU, 808AD9F7, 808AD9FC, -0x6320)
MKW_REGION_RANGE(NtscU, 808ADCBC, 808ADD2C, -0x2FA8)
MKW_REGION_RANGE(NtscU, 808AEB45, 808AEB55, -0x68A8)
MKW_REGION_RANGE(NtscU, 808AF8E0, 808AF95F, -0x6AC0)
MKW_REGION_RANGE(NtscU, 808AFC18, 808AFC1B, -0x46B8)
MKW_REGION_RANGE(NtscU, 808B30C8, 808B3108, -0x46B0)
MKW_REGION_RANGE(NtscU, 808B35A0, 808B35AF, -0x46B0)
MKW_REGION_RANGE(NtscU, 808B3984, 808B3987, -0x4850)
MKW_REGION_RANGE(NtscU, 808B3A90, 808B3A9F, -0x4850)
MKW_REGION_RANGE(NtscU, 808B4C20, 808B4DEF, -0x46B0)
MKW_REGION_RANGE(NtscU, 808B54B8, 808B550F, -0x4738)
MKW_REGION_RANGE(NtscU, 808B59F4, 808B5CCF, -0x48C0)
MKW_REGION_RANGE(NtscU, 808B5CD8, 808B5CDB, -0x48BC)
MKW_REGION_RANGE(NtscU, 808BB034, 808BB097, -0x60F8)
MKW_REGION_RANGE(NtscU, 808BBEC8, 808BE908, -0x6430)
MKW_REGION_RANGE(NtscU, 808BFAF0, 808BFAFF, -0x65B0)
MKW_REGION_RANGE(NtscU, 808C069C, 808C06FF, -0x66B0)
MKW_REGION_RANGE(NtscU, 808CB550, 808CB553, -0x5508)
MKW_REGION_RANGE(NtscU, 808CB700, 808CB70F, -0x5508)
MKW_REGION_RANGE(NtscU, 808D1DD0, 808D1E2B, -0x3A98)
MKW_REGION_RANGE(NtscU, 808D30D8, 808D324F, -0x4908)
MKW_REGION_RANGE(NtscU, 808D3698, 808D369B, +0x1AB0)
MKW_REGION_RANGE(NtscU, 808D36CC, 808D36CF, +0x1AB0)
MKW_REGION_RANGE(NtscU, 808D36D4, 808D36D7, +0x1AB0)
MKW_REGION_RANGE(NtscU, 808D3744, 808D3747, +0x1AB0)
MKW_REGION_RANGE(NtscU, 808D374C, 808D374F, +0x1AB0)
MKW_REGION_RANGE(NtscU, 808D3C78, 808D3D1B, +0x1610)
MKW_REGION_RANGE(NtscU, 808D3E14, 808D3F2F, -0x4F68)
MKW_REGION_RANGE(NtscU, 808D9A58, 808D9A98, -0x60F0)
MKW_REGION_RANGE(NtscU, 808D9BC8, 808D9C5F, -0x36C0)
MKW_REGION_RANGE(NtscU, 808D9DB0, 808D9F97, -0x6220)
MKW_REGION_RANGE(NtscU, 808DA070, 808DA07F, -0x6220)
MKW_REGION_RANGE(NtscU, 808DA318, 808DA367, -0x63B8)
MKW_REGION_RANGE(NtscU, 808DA5DC, 808DA63F, -0x64C0)
MKW_REGION_RANGE(NtscU, 808DAD60, 808DAD6F, -0x6778)
MKW_REGION_RANGE(NtscU, 808DC540, 808DC54F, -0x4138)
MKW_REGION_RANGE(NtscU, 808E2BBC, 808E2BBE, +0x0)
MKW_REGION_RANGE(NtscU, 809BD6E0, 809C186B, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C1874, 809C1877, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C18F8, 809C18FB, -0x47E8)
MKW_REGION_RANGE(NtscU, 809C1918, 809C191B, -0x47E8)
MKW_REGION_RANGE(NtscU, 809C1980, 809C198F, -0x4610)
MKW_REGION_RANGE(NtscU, 809C19A0, 809C19BB, -0x4820)
MKW_REGION_RANGE(NtscU, 809C1E38, 809C1E3B, -0x4930)
MKW_REGION_RANGE(NtscU, 809C1F50, 809C20EF, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C20F0, 809C20F7, -0x47C8)
MKW_REGION_RANGE(NtscU, 809C20F8, 809C20FB, -0x47A8)
MKW_REGION_RANGE(NtscU, 809C2100, 809C2107, -0x47D0)
MKW_REGION_RANGE(NtscU, 809C2108, 809C210B, -0x47B0)
MKW_REGION_RANGE(NtscU, 809C210C, 809C210F, -0x47D0)
MKW_REGION_RANGE(NtscU, 809C2110, 809C211B, -0x47D8)
MKW_REGION_RANGE(NtscU, 809C21D0, 809C21D3, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C21D4, 809C21D7, +0xB840)
MKW_REGION_RANGE(NtscU, 809C21D8, 809C21DB, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C2328, 809C232B, -0x47C8)
MKW_REGION_RANGE(NtscU, 809C26B8, 809C26BB, -0x4700)
MKW_REGION_RANGE(NtscU, 809C27F0, 809C289B, -0x4C40)
MKW_REGION_RANGE(NtscU, 809C28B8, 809C28BB, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C2BE8, 809C2BEB, -0x4850)
MKW_REGION_RANGE(NtscU, 809C2CB8, 809C2CBB, -0x47E0)
MKW_REGION_RANGE(NtscU, 809C2EF0, 809C2EF3, -0x47C0)
MKW_REGION_RANGE(NtscU, 809C2F38, 809C361B, -0x47F8)
MKW_REGION_RANGE(NtscU, 809C3670, 809C38CF, -0x4808)
MKW_REGION_RANGE(NtscU, 809C4330, 809C4333, -0x4840)
MKW_REGION_RANGE(NtscU, 809C4680, 809C4683, -0x48C0)
MKW_REGION_RANGE(NtscU, 809C4730, 809C474F, -0x47B0)
MKW_REGION_RANGE(NtscU, 809C496C, 809C496F, -0x4820)
MKW_REGION_RANGE(NtscJ, 80000000, 80008023, +0x0)
MKW_REGION_RANGE(NtscJ, 800080E8, 8000ADBF, -0xA4)
MKW_REGION_RANGE(NtscJ, 8000AF08, 80021BAB, -0xDC)
MKW_REGION_RANGE(NtscJ, 80021BAC, 80244EBF, -0xE0)
MKW_REGION_RANGE(NtscJ, 802572BF, 802575DF, -0x660)
MKW_REGION_RANGE(NtscJ, 80257F40, 80258184, -0x660)
MKW_REGION_RANGE(NtscJ, 80276054, 802A407F, -0x660)
MKW_REGION_RANGE(NtscJ, 802A4080, 8038917B, -0x680)
MKW_REGION_RANGE(NtscJ, 80399200, 80399400, -0x680)
MKW_REGION_RANGE(NtscJ, 805103B4, 805CC0A7, -0x680)
MKW_REGION_RANGE(NtscJ, 805CC1B4, 805FA33B, -0x724)
MKW_REGION_RANGE(NtscJ, 805FA344, 805FF6E7, -0x724)
MKW_REGION_RANGE(NtscJ, 805FFD70, 806003E7, -0x848)
MKW_REGION_RANGE(NtscJ, 80600C78, 80620CB3, -0x88C)
MKW_REGION_RANGE(NtscJ, 80620D7C, 80637A23, -0x8B4)
MKW_REGION_RANGE(NtscJ, 80637A80, 8063BCF7, -0x914)
MKW_REGION_RANGE(NtscJ, 8063BD28, 8063BD2F, -0x920)
MKW_REGION_RANGE(NtscJ, 8063BD38, 8063BD3F, -0x920)
MKW_REGION_RANGE(NtscJ, 8063BE40, 8088F70F, -0x994)
MKW_REGION_RANGE(NtscJ, 8088F720, 8088FDBB, -0x9A0)
MKW_REGION_RANGE(NtscJ, 80890208, 80890977, -0x9B0)
MKW_REGION_RANGE(NtscJ, 808913C0, 808913C3, -0x9B0)
MKW_REGION_RANGE(NtscJ, 80891750, 8089175F, -0x9B0)
MKW_REGION_RANGE(NtscJ, 80891A60, 80891A6F, -0x9B0)
MKW_REGION_RANGE(NtscJ, 80891B28, 80891C28, -0x9B0)
MKW_REGION_RANGE(NtscJ, 80892F2C, 80897DEF, -0x9B0)
MKW_REGION_RANGE(NtscJ, 80898955, 80898959, -0xE60)
MKW_REGION_RANGE(NtscJ, 80899692, 808B5CCF, -0xEA0)
MKW_REGION_RANGE(NtscJ, 808B5CD8, 808B5CDB, -0xE9C)
MKW_REGION_RANGE(NtscJ, 808BB034, 808BB097, -0xEB0)
MKW_REGION_RANGE(NtscJ, 808BBEC8, 808DA617, -0xEB0)
MKW_REGION_RANGE(NtscJ, 808DC540, 808DC54F, -0xEB0)
MKW_REGION_RANGE(NtscJ, 808E2BBC, 808E2BBE, +0x0)
MKW_REGION_RANGE(NtscJ, 809BD6E8, 809C474B, -0xFA0)
MKW_REGION_RANGE(NtscJ, 809C496C, 809C496F, -0xFA0)
-114
View File
@@ -1,114 +0,0 @@
#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
@@ -1,732 +0,0 @@
#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,105 +0,0 @@
// Native call catalog used by InvokeDirectCpu. Keep project-specific HLE
// 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 dispatch.h. It
// cannot compile standalone and must never be included anywhere else.
extern "C" void nw4r__lyt__detail__DrawQuad_800847c0(CpuContext* ctx);
extern "C" void nw4r__lyt__detail__DrawQuad_80084d20(CpuContext* ctx);
extern "C" void GX__SetVtxDesc_8016d3a4(uint32_t a, uint32_t t);
extern "C" void GX__ClearVtxDesc_8016dc34();
extern "C" void GX__SetVtxAttrFmt_8016dc68(uint32_t vf, uint32_t a, uint32_t c, uint32_t t, uint32_t fr);
extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm);
extern "C" void GX__SetNumTexGens_8016e5a4(uint32_t n);
extern "C" void GX__DrawDone_8016eab0();
extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv);
extern "C" void GX__SetLineWidth_8016f314(uint32_t width, uint32_t texOffsets);
extern "C" void GX__SetPointSize_8016f348(uint32_t pointSize, uint32_t texOffsets);
extern "C" void GX__SetCullMode_8016f3b8(uint32_t m);
extern "C" void GX__SetNumChans_8017054c(uint32_t n);
extern "C" void GX__SetChanCtrl_80170570(uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af);
extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m);
extern "C" void GX__InitTexObjLOD_80170a4c(uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma);
extern "C" void GX__InvalidateTexAll_80171110();
extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid);
extern "C" void GX__SetNumIndStages_80171b38(uint32_t n);
extern "C" void GX__SetTevDirect_80171b58(uint32_t s);
extern "C" void GX__SetTevColorIn_80171ce0(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d);
extern "C" void GX__SetTevAlphaIn_80171d20(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d);
extern "C" void GX__SetTevColorOp_80171d60(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_);
extern "C" void GX__SetTevAlphaOp_80171db8(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_);
extern "C" void GX__SetTevColorS10_80171e70(uint32_t id, uint32_t cp);
extern "C" void GX__SetTevKColor_80171ed4(uint32_t id, uint32_t cp);
extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel);
extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel);
extern "C" void GX__SetTevSwapMode_80171fd0(uint32_t s, uint32_t rs, uint32_t ts);
extern "C" void GX__SetTevSwapModeTable_8017200c(uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a);
extern "C" void GX__SetTevOrder_8017214c(uint32_t s, uint32_t c, uint32_t m, uint32_t col);
extern "C" void GX__SetNumTevStages_801722a8(uint32_t n);
extern "C" void GX__SetBlendMode_8017277c(uint32_t t, uint32_t s, uint32_t d, uint32_t op);
extern "C" void GX__LoadPosMtxImm_8017310c(uint32_t ma, uint32_t id);
extern "C" void GX__SetCurrentMtx_80173214(uint32_t id);
// Keyed by the translated region's addresses, so only translated code sees them;
// runtime calls resolve through the registry.
#if defined(MKW_TRANSLATED_GUEST_REGION)
#define MKW_KNOWN_TYPED_NATIVE(addr, fn, ...) \
template <> struct KnownTypedNativeCpuCall<MKW_GADDR_CT(addr)> { \
static constexpr bool kAvailable = true; \
static void Invoke(CpuContext* cpu) { (void)cpu; fn(__VA_ARGS__); } \
}
MKW_KNOWN_TYPED_NATIVE(8016D3A4, GX__SetVtxDesc_8016d3a4, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(8016DC34, GX__ClearVtxDesc_8016dc34);
MKW_KNOWN_TYPED_NATIVE(8016DC68, GX__SetVtxAttrFmt_8016dc68, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(8016E37C, GX__SetTexCoordGen2_8016e37c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(8016E5A4, GX__SetNumTexGens_8016e5a4, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(8016EAB0, GX__DrawDone_8016eab0);
MKW_KNOWN_TYPED_NATIVE(8016F0F0, GX__Begin_8016f0f0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5]);
MKW_KNOWN_TYPED_NATIVE(8016F314, GX__SetLineWidth_8016f314, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(8016F348, GX__SetPointSize_8016f348, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(8016F3B8, GX__SetCullMode_8016f3b8, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(8017054C, GX__SetNumChans_8017054c, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(80170570, GX__SetChanCtrl_80170570, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8], cpu->gpr[9]);
MKW_KNOWN_TYPED_NATIVE(801707F8, GX__InitTexObj_801707f8, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8], cpu->gpr[9], cpu->gpr[10]);
MKW_KNOWN_TYPED_NATIVE(80170A4C, GX__InitTexObjLOD_80170a4c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], static_cast<float>(cpu->fpr[1].d), static_cast<float>(cpu->fpr[2].d), static_cast<float>(cpu->fpr[3].d), cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(80171110, GX__InvalidateTexAll_80171110);
MKW_KNOWN_TYPED_NATIVE(80170F2C, GX__LoadTexObj_80170f2c, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80171B38, GX__SetNumIndStages_80171b38, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(80171B58, GX__SetTevDirect_80171b58, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(80171CE0, GX__SetTevColorIn_80171ce0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(80171D20, GX__SetTevAlphaIn_80171d20, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(80171D60, GX__SetTevColorOp_80171d60, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(80171DB8, GX__SetTevAlphaOp_80171db8, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(80171E70, GX__SetTevColorS10_80171e70, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80171ED4, GX__SetTevKColor_80171ed4, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80171F30, GX__SetTevKColorSel_80171f30, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80171F80, GX__SetTevKAlphaSel_80171f80, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80171FD0, GX__SetTevSwapMode_80171fd0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5]);
MKW_KNOWN_TYPED_NATIVE(8017200C, GX__SetTevSwapModeTable_8017200c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(8017214C, GX__SetTevOrder_8017214c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6]);
MKW_KNOWN_TYPED_NATIVE(801722A8, GX__SetNumTevStages_801722a8, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(8017277C, GX__SetBlendMode_8017277c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6]);
MKW_KNOWN_TYPED_NATIVE(8017310C, GX__LoadPosMtxImm_8017310c, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(80173214, GX__SetCurrentMtx_80173214, cpu->gpr[3]);
#undef MKW_KNOWN_TYPED_NATIVE
#define MKW_KNOWN_NATIVE_CPU_CALL(addr, fn) \
template <> struct KnownNativeCpuCall<MKW_GADDR_CT(addr)> { \
static constexpr bool kAvailable = true; \
static constexpr uint32_t kNonvolatileFprWriteMask = 0; \
static constexpr void (*Entry)(CpuContext*) = &fn; \
}
MKW_KNOWN_NATIVE_CPU_CALL(800847C0, nw4r__lyt__detail__DrawQuad_800847c0);
MKW_KNOWN_NATIVE_CPU_CALL(80084D20, nw4r__lyt__detail__DrawQuad_80084d20);
#undef MKW_KNOWN_NATIVE_CPU_CALL
#endif
+50 -2
View File
@@ -9,10 +9,39 @@
#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,
@@ -36,6 +65,25 @@ 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
-21
View File
@@ -1,21 +0,0 @@
#pragma once
// Base translations the runtime calls directly, named by PAL identity and resolved through the
// function registry on first use.
// MKW_GUEST_FUNC(801A0620)(ctx);
#include "guest_abi/cpu_context.h"
#include "guest_abi/guest_region.h"
namespace GuestFunctions {
using Entry = void (*)(CpuContext*);
// The base translation at a guest address, also where a native or mod overrides it.
Entry BaseTranslation(uint32_t address);
} // namespace GuestFunctions
#define MKW_GUEST_FUNC(pal) \
(*[] { \
static const ::GuestFunctions::Entry entry = ::GuestFunctions::BaseTranslation(MKW_GADDR(pal)); \
return entry; \
}())
-10
View File
@@ -1,10 +0,0 @@
#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
-114
View File
@@ -1,114 +0,0 @@
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <new>
#include <type_traits>
#include <utility>
// Dedicated GX thread (Dolphin "dual core"): the game thread records GX HLE calls in order and
// the worker executes them. While active, GX work runs on the worker except inside inline scopes,
// which run on the game thread after the worker has caught up.
namespace GxWorker {
namespace detail {
inline constexpr size_t kAlign = 16;
extern std::atomic<bool> g_active;
extern constinit thread_local bool t_onWorker;
extern constinit thread_local int t_inlineDepth;
extern bool g_fifoPending;
void* Reserve(size_t payloadSize, void (*run)(void*));
void Commit();
void FlushFifo();
} // namespace detail
// Starts the worker unless [video] gx_worker disables it. Call once Aurora is up.
void Start();
void Stop();
// Stops without running queued work; bounded, for fatal exits.
void Abandon();
inline bool Active() noexcept { return detail::g_active.load(std::memory_order_relaxed); }
inline bool OnWorker() noexcept { return detail::t_onWorker; }
// True when a GX entry point on this thread must be recorded instead of executed.
inline bool ShouldDefer() noexcept {
return Active() && !detail::t_onWorker && detail::t_inlineDepth == 0;
}
template <typename F>
void Submit(F&& fn) {
using Fn = std::decay_t<F>;
static_assert(alignof(Fn) <= detail::kAlign, "over-aligned GX command");
if (detail::g_fifoPending) {
detail::FlushFifo();
}
void* storage = detail::Reserve(sizeof(Fn), [](void* p) {
struct Destroy {
Fn* f;
~Destroy() { f->~Fn(); }
} guard{static_cast<Fn*>(p)};
(*guard.f)();
});
new (storage) Fn(std::forward<F>(fn));
detail::Commit();
}
// Records fn when deferring, otherwise runs it now. Read call-time guest memory before calling.
template <typename F>
void Run(F&& fn) {
if (ShouldDefer()) {
Submit(std::forward<F>(fn));
} else {
fn();
}
}
// Gather-pipe writes, coalesced into one record per run of consecutive writes and replayed
// with their original sizes.
void FifoWrite(uint32_t value, uint32_t sizeBytes);
void FifoBurst(const uint8_t* data, uint32_t sizeBytes);
// Called about every millisecond while the game thread waits in Sync.
void SetWaitCallback(void (*callback)());
// Blocks until every recorded command has executed.
void Sync();
// GX entry points run inline on the game thread while inside (after a Sync), for work whose
// results the guest needs immediately.
void BeginInline();
void EndInline();
class InlineScope {
public:
InlineScope() { BeginInline(); }
~InlineScope() { EndInline(); }
InlineScope(const InlineScope&) = delete;
InlineScope& operator=(const InlineScope&) = delete;
};
} // namespace GxWorker
// Records the enclosing call for the GX worker and returns, or falls through when it should run
// here (worker thread, inline scope, worker disabled).
#define GX_DEFER_CALL(fn, ...) \
do { \
if (GxWorker::ShouldDefer()) { \
GxWorker::Submit([=] { fn(__VA_ARGS__); }); \
return; \
} \
} while (false)
#define GX_DEFER_CALL0(fn) \
do { \
if (GxWorker::ShouldDefer()) { \
GxWorker::Submit([] { fn(); }); \
return; \
} \
} while (false)
// Runs the rest of the enclosing function on this thread once the worker has caught up.
#define GX_SYNC_INLINE() GxWorker::InlineScope gxInlineScope_
-22
View File
@@ -1,22 +0,0 @@
#pragma once
// IOS side of the emulated Bluetooth module (/dev/usb/oh1/57e/305): routes the
// guest's USB control / bulk / interrupt transfers to Bluetooth::Controller and
// completes the held event and ACL reads from the alarm pump.
#include <cstdint>
struct CpuContext;
// Returns the fd for the Bluetooth device path, 0 when `path` is not ours.
int32_t Bluetooth_HLE_OpenDevice(const char* path);
bool Bluetooth_HLE_IsFd(uint32_t fd);
// Cancels the held reads (their callbacks are queued) and closes the device.
int32_t Bluetooth_HLE_Close(uint32_t fd);
// True when the request completed now with `result`; false when it is held
// until data arrives (its callback is queued later by the pump).
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
uint32_t callback, uint32_t callbackArg, int32_t& result);
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr);
// Runs the controller and completes held reads. Emulation thread only.
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu);
@@ -41,3 +41,42 @@ inline GuestStatus Encode(const Fields& fields)
}
} // namespace PadStatusContract
namespace WpadContract {
inline constexpr std::size_t kChannelCount = 4;
inline constexpr int32_t kStatusDisabled = 0;
inline constexpr int32_t kStatusReady = 3;
inline constexpr int32_t kErrorNoController = -1;
inline constexpr int32_t kErrorNotReady = -2;
inline constexpr int32_t kErrorBadChannel = -6;
inline constexpr int32_t kExtensionCore = 0;
class State {
public:
void Initialize() { m_initialized = true; }
bool IsInitialized() const { return m_initialized; }
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
int32_t GetDataFormat(uint32_t chan) const
{
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? 0 : kErrorNotReady;
}
int32_t SetDataFormat(uint32_t chan, int32_t format) const
{
(void)format;
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? kErrorNoController : kErrorNotReady;
}
private:
bool m_initialized = false;
};
} // namespace WpadContract
-10
View File
@@ -1,10 +0,0 @@
#pragma once
#include <cstdint>
// HW_GPIOB_OUT, the GPIO outputs the PPC owns. WPAD sets bit 8 to power the
// sensor bar; there is nothing behind it on a PC, so the register only latches.
inline constexpr uint32_t kHollywoodGpioBOut = 0xCD0000C0u;
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value);
bool HollywoodGpio_Write32(uint32_t address, uint32_t value);
+2 -4
View File
@@ -38,12 +38,10 @@ extern "C" void OS_HLE_EndDeferredGuestCallbacks();
// renaming requires updating Translator.Cli/Program.cs, RuntimeNativeGuestEffectAnalyzer.cs,
// TranslatedBuildShardEmitter.cs and RuntimeNativeFunctionAbiProvider.cs together.
// addr_hex is the PAL address (see guest_abi/guest_region.h). The translator reads arg_list as
// the guest ABI, so the unused wrapper keeps it checked against the implementation.
#define PPC_NATIVE_OVERRIDE(addr_hex, name, ret_type, arg_list, call_list) \
[[maybe_unused]] static ret_type native_signature_##addr_hex arg_list { return name call_list; } \
extern "C" ret_type func_##addr_hex arg_list { return name call_list; } \
REGISTER_NATIVE_FUNCTION(0x##addr_hex, name)
#define PPC_NATIVE_OVERRIDE_VOID(addr_hex, name, arg_list, call_list) \
[[maybe_unused]] static void native_signature_##addr_hex arg_list { name call_list; } \
extern "C" void func_##addr_hex arg_list { name call_list; } \
REGISTER_NATIVE_FUNCTION(0x##addr_hex, name)

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