Compare commits

..

14 Commits

Author SHA1 Message Date
patchzyy cc6abd49ab faster deterministic translation (#358) 2026-10-25 10:30:08 +01:00
patchzyy bb5e2b1166 Fix prebuilt aurora package being rejected (#357)
* fix prebuilt package rejected on user machines

* Time out a hung translator step

The translator can hang after a crash instead of exiting, and the
installer's clean retry runs only on a failed exit. Each translator
step now gets 1800 s; past that its process tree is killed and the
step fails, so the retry runs.

* time out mod awareness check too

---------

Co-authored-by: therenow9 <60898774+therenow9@users.noreply.github.com>
2026-10-25 09:47:03 +01:00
patchzyy 4993b2226c update to 0.9.2 2026-10-24 17:45:02 +02:00
patchzyy e15be3bf55 Fix online sha 2026-10-24 17:31:12 +02:00
patchzyy 85992f85b0 add --headless for boot races (#351)
* add headless race option

* bump aurora
2026-10-24 16:51:34 +02:00
patchzyy 5a930903fb update to 0.9.1 2026-10-24 16:00:30 +02:00
patchzyy f8944f239a Fix macOS build (#349)
* fix macos build with homebrew clang

* fingerprint compiler paths
2026-10-24 15:55:31 +02:00
patchzyy 7678608943 bump aurora for unset texgen fix (#348) 2026-10-24 14:54:46 +02:00
patchzyy 4e0e1edb86 Prompt restart after toggling Wii Remotes (#347)
* prompt restart after toggling wii remotes

* dont prompt restart when save fails
2026-10-24 14:27:30 +02:00
patchzyy 1d9cae387a fix ntsc-u crash on courses with torches (#346)
* fix ntsc-u map for vtable-only functions

* add --race and --race-cpu boot options

* add MKW_PERF_STATS frame busy timing and --race-cpu

* fix race seeds for --race-cpu benchmarks

* bind interrupt natives at compile time without register guards

* inline fast path for unguarded indirect calls

* batch gx worker head publishes

* split cold paths out of translated functions

* cache observed gx worker tail on the producer

* print game id in fatal log

* fix ntsc-u map verdict counts
2026-10-24 13:48:29 +02:00
Michael G 0aba3cb3ea Use the new app icons on macOS (#340) 2026-10-24 11:14:39 +02:00
patchzyy ac99413d45 v0.9.0 (v1.0 beta) (#338)
* move aurora to the aurora-wiicompiled submodule (#315)

* Update settings_overlay.cpp (#257)

* Simplify controller settings menu (#317)

* simplify controller settings menu

* platform pairing hint

* build for x86-64-v2 on older linux cpus (#319)

* Real Wii Remote support over Bluetooth (#323)

* pass real wii remotes through emulated bluetooth

* build bluetooth tests on macos ci

* address review comments

* cancel replaced bluetooth reads

* Run GX on its own thread (#325)

* NTSC-U and NTSC-J support (#326)

* add ntsc-u and ntsc-j support

Co-authored-by: DarthMDev <mgracer48@yahoo.com>
Co-authored-by: rooklz <rookieb@muzo-works.com>

* hash inc files and require project for shards

* port remaining stale rr us hooks

* drop map report generation

* drop one-off region porters

---------

Co-authored-by: DarthMDev <mgracer48@yahoo.com>
Co-authored-by: rooklz <rookieb@muzo-works.com>

* Keyboard input for friend code entry (#327)

* keyboard input for friend code entry

* add new guest imports to synthetic test

* replace per-address region table with range map (#328)

* paste friend codes from clipboard (#329)

* Verify dependency archive hashes (#330)

* verify dependency archive hashes

* verify cached cppwinrt tool

* check dependency path types

* Check CPU support before building (#331)

* check cpu support before building

* build for x86-64-v2 on windows and intel macs

* bump aurora for fog fsel fix

* add vsync option (#333)

* Add exe icons (#334)

* add exe icons

* rebuild icon resource on ico change

* drop icon rebuild tracking

* Fix bugs found in forks (#336)

* port fixes found in forks

* fill whole disk id

* dump kamek overlay on crash (#335)

* bump aurora and version to 0.9.0

---------

Co-authored-by: DarthMDev <mgracer48@yahoo.com>
Co-authored-by: rooklz <rookieb@muzo-works.com>
2026-10-23 18:00:55 +02:00
patchzyy 4c166a7eb1 update to 0.2.36 2026-10-19 22:03:44 +02:00
patchzyy 29155eefbd Only trust relocated module pointers (#321)
* only trust relocated module pointers

* match relocation value too
2026-10-19 22:02:06 +02:00
78 changed files with 3318 additions and 1669 deletions
+2 -3
View File
@@ -5,9 +5,8 @@
*.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
# Region tables reproduced by tools/region/
# 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_address_table.inc linguist-generated=true
runtime/include/guest_function_imports.inc linguist-generated=true
runtime/include/guest_abi/guest_versions.inc linguist-generated=true
-1
View File
@@ -67,7 +67,6 @@ project.lock.json
/.vscode/
/.idea/
/tools/*
!/tools/region/
__pycache__/
/TEMP/
/tmp/
+18
View File
@@ -278,6 +278,24 @@ $packagedRuntimeFingerprint = (Get-Content -LiteralPath (Join-Path $workspace 'D
if ($stagedRuntimeFingerprint -ne $packagedRuntimeFingerprint) {
throw "The staged runtime ($stagedRuntimeFingerprint) does not match the precompiled runtime archive ($packagedRuntimeFingerprint)."
}
# Users' builds run LocalBuild.ps1 under Windows PowerShell 5.1, whatever shell built this
# installer, so the staged package is accepted (or not) exactly the way theirs will be.
$prebuiltGate = Join-Path $workRoot 'native-prebuilt-gate.ps1'
@'
param([string]$Workspace, [string]$Toolkit)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
. (Join-Path $Workspace 'NativeBuildFlags.ps1')
$package = Resolve-MkwNativePrebuiltDirectory (Join-Path $Workspace 'Dependencies') `
(Join-Path $Toolkit 'llvm-mingw\bin') (Join-Path $Workspace 'aurora-main') (Join-Path $Workspace 'runtime\third_party')
if (-not $package) { exit 3 }
'@ | Set-Content -LiteralPath $prebuiltGate -Encoding UTF8
& (Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe') -NoLogo -NoProfile -NonInteractive `
-ExecutionPolicy Bypass -File $prebuiltGate -Workspace $workspace -Toolkit $toolkit
if ($LASTEXITCODE -ne 0) {
throw "Windows PowerShell rejects the staged precompiled aurora package (exit $LASTEXITCODE); every installation would build aurora from source."
}
Remove-Item -LiteralPath $prebuiltGate -Force
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
+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.9.2</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+32 -73
View File
@@ -21,7 +21,8 @@ param(
# Discards every cache first. Used when a product was reported broken/blocked, so a possibly
# corrupted translation or build directory can never contribute to the repaired product.
[switch]$ForceCleanBuild,
[int]$Parallel = 0
[int]$Parallel = 0,
[ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3'
)
$ErrorActionPreference = 'Stop'
@@ -33,65 +34,6 @@ Set-StrictMode -Version 3.0
# See NativeBuildFlags.ps1; Build-Installer.ps1 stages it beside this script.
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
function Resolve-NativePrebuiltDirectory([string]$DependenciesDirectory, [string]$ToolchainBin, [string]$AuroraDirectory, [string]$ThirdPartyDirectory) {
<#
The shipped precompiled aurora/third-party package, verified against its recorded provenance
(exact compiler, exact flags) before use. Anything unexpected falls back to building from source.
#>
$package = Join-Path $DependenciesDirectory 'native_prebuilt'
$provenancePath = Join-Path $package 'provenance.json'
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) {
Write-Host 'MKWCBUILD: No precompiled aurora package was shipped; building it from source'
return ''
}
try {
$provenance = Get-Content -LiteralPath $provenancePath -Raw | ConvertFrom-Json
$compiler = Join-Path $ToolchainBin 'clang-22.exe'
$compilerHash = Get-MkwFileSha256 $compiler
if ($provenance.CompilerSha256 -ne $compilerHash) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built by a different compiler; building it from source'
return ''
}
if ($provenance.FlagFingerprint -ne (Get-MkwNativeFlagFingerprint)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built with different flags; building it from source'
return ''
}
# The archives embed Dawn's headers and link its import library, so a
# dawn_prebuilt tree that is not the one they were harvested against is
# an ABI mismatch that only shows up as a crash inside the GPU backend.
$recordedDawn = $provenance.PSObject.Properties['DawnRuntimeSha256']
if ($null -eq $recordedDawn -or
$recordedDawn.Value -ne (Get-MkwDawnRuntimeSha256 $DependenciesDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built against a different Dawn; building it from source'
return ''
}
# The archives are aurora; the runtime compiled here calls its headers.
# If this workspace's aurora sources are not the ones that were harvested
# (a newer toolkit, or a locally modified tree), linking the package would
# mix two versions of aurora, so build it from source instead.
$recordedAurora = $provenance.PSObject.Properties['AuroraSourceFingerprint']
if ($null -eq $recordedAurora -or
$recordedAurora.Value -ne (Get-MkwAuroraSourceFingerprint $AuroraDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different aurora sources; building it from source'
return ''
}
# Same reasoning for the vendored Crypto++ the package also carries:
# first-party TUs compile against this workspace's third_party headers,
# so a package harvested from an older tree must not be linked here.
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
if ($null -eq $recordedThirdParty -or
$recordedThirdParty.Value -ne (Get-MkwThirdPartySourceFingerprint $ThirdPartyDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different vendored third-party sources; building it from source'
return ''
}
} catch {
Write-Host "MKWCBUILD: The precompiled aurora package could not be validated ($($_.Exception.Message)); building it from source"
return ''
}
Write-Host 'MKWCBUILD: Using the precompiled aurora and third-party libraries'
return $package
}
function Write-MkwBuildStep([string]$StepId, [string]$Message) {
<#
Announces a build step. The identifier is the contract (BuildStepIds in
@@ -159,6 +101,8 @@ if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and
$RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length)
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
# A translator step takes 3-19 s on 16 threads; past this it has hung, and failing lets the clean retry run.
$translatorTimeoutSeconds = 1800
$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
@@ -207,8 +151,9 @@ if ($Parallel -gt 0) {
$globalJobs = $Parallel
} else {
$translatorThreads = [math]::Max(1, [math]::Min([Environment]::ProcessorCount, 16))
# A translated TU peaks around 0.26 GB in clang; 0.5 GiB each after a 2 GiB reserve.
$translatedJobs = [math]::Max(1, [math]::Min([Environment]::ProcessorCount,
[math]::Floor($memoryGiB / 2)))
[math]::Floor(($memoryGiB - 2) / 0.5)))
$globalJobs = [math]::Max($translatedJobs, [Environment]::ProcessorCount)
}
@@ -285,11 +230,18 @@ try {
# same translated functions produces the same base tree. Only the translator can
# decide that - it takes parsing the Kamek patch set against the recorded translated
# function ranges - and it fails closed, so anything but exit 0 retranslates.
& $translator @(
'check-base-mod-awareness', '--project', $project, '--profile', 'retro-rewind',
'--translation-output-metadata', $baseMetadata, '--code-pul', $retroCodePul
) | Write-Host
if ($LASTEXITCODE -ne 0) {
$modAware = $true
try {
Invoke-Checked $translator @(
'check-base-mod-awareness', '--project', $project, '--profile', 'retro-rewind',
'--translation-output-metadata', $baseMetadata, '--code-pul', $retroCodePul
) 'Checking whether the base translation covers the new Code.pul' `
-TimeoutSeconds $translatorTimeoutSeconds
} catch {
Write-Host "MKWCBUILD: $($_.Exception.Message)"
$modAware = $false
}
if (-not $modAware) {
Write-MkwBuildStep 'retranslate-base' 'The base translation is stale; retranslating the base game for the new Code.pul'
$reuseBase = $false
}
@@ -312,12 +264,14 @@ try {
'--outdir', $functions, '--output-metadata', $baseMetadata,
'--production-source-bundle', (Join-Path $generated 'base_translation_sources.bin'),
'--no-function-files', '--prune-stale', '--threads', $translatorThreads
) 'Translating the user-owned base game' -StepId 'translate-base'
) 'Translating the user-owned base game' -StepId 'translate-base' `
-TimeoutSeconds $translatorTimeoutSeconds
Invoke-Checked $translator @(
'emit-base-manifest', '--project', $project, '--out', $baseManifestDir,
'--functions-dir', $functions, '--translation-output-metadata', $baseMetadata, '--region', $pins.Region
) 'Creating the local base translation manifest' -StepId 'emit-base-manifest'
) 'Creating the local base translation manifest' -StepId 'emit-base-manifest' `
-TimeoutSeconds $translatorTimeoutSeconds
if (-not [string]::IsNullOrWhiteSpace($TranslationFingerprint)) {
[ordered]@{ SchemaVersion = 1; TranslationFingerprint = $TranslationFingerprint; DolSha256 = $pins.DolSha256 } |
@@ -347,11 +301,12 @@ try {
throw 'Retro Rewind requires either -RetroWfcOfflineDirectory or -SkipRetroWfcPayload.'
}
Invoke-Checked $translator $translateModArguments 'Translating the selected Retro Rewind Code.pul' `
-StepId 'translate-mod'
-StepId 'translate-mod' -TimeoutSeconds $translatorTimeoutSeconds
}
Invoke-Checked $translator @('generate-data-init', '--project', $project) `
'Generating local game data initialization' -StepId 'generate-data-init'
'Generating local game data initialization' -StepId 'generate-data-init' `
-TimeoutSeconds $translatorTimeoutSeconds
$shardArgs = @(
'emit-build-shards', '--project', $project, '--base-metadata', $baseMetadata,
@@ -363,7 +318,8 @@ try {
$shardArgs += @('--resolved-profile', (Join-Path $retroOut 'resolved_dispatch_profile.json'),
'--retro-cpp-dir', (Join-Path $retroOut 'cpp'))
}
Invoke-Checked $translator $shardArgs 'Preparing local native build shards' -StepId 'emit-build-shards'
Invoke-Checked $translator $shardArgs 'Preparing local native build shards' -StepId 'emit-build-shards' `
-TimeoutSeconds $translatorTimeoutSeconds
# The build directory is kept whenever the exact toolchain that filled it is the one
# installed now and its CMake cache still belongs to this workspace path. Object files are
@@ -395,13 +351,16 @@ try {
}
}
$dependencies = Join-Path $Workspace 'Dependencies'
$nativePrebuilt = Resolve-NativePrebuiltDirectory $dependencies $toolchainBin `
$nativePrebuilt = Resolve-MkwNativePrebuiltDirectory $dependencies $toolchainBin `
(Join-Path $Workspace 'aurora-main') (Join-Path $Workspace 'runtime\third_party')
if ($X86CpuProfile -eq 'v2') {
Write-Host 'MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2'
}
$configure = Get-MkwNativeConfigureArguments `
-SourceDirectory (Join-Path $Workspace 'runtime') -BuildDirectory $build `
-Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres `
-DependenciesDirectory $dependencies -NativePrebuiltDirectory $nativePrebuilt `
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs")
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs", "-DMKW_X86_CPU_PROFILE=$X86CpuProfile")
Invoke-Checked $cmake $configure 'Configuring the bundled native toolchain' -StepId 'configure-native'
if (-not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint)) {
[ordered]@{ SchemaVersion = 1; NativeToolchainFingerprint = $NativeToolchainFingerprint } |
+118 -54
View File
@@ -157,7 +157,8 @@ function Resolve-MkwProjectForAssets([string]$Workspace, [string]$AssetsDirector
}
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true,
[int]$TimeoutSeconds = 0) {
<#
Runs a build tool and turns a non-zero exit code into a described failure. By default,
Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
@@ -166,6 +167,7 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
-TimeoutSeconds waits on the process itself and kills its tree when time runs out.
#>
if ($StepId) { Write-Host "${LogPrefix}:STEP:${StepId} $Description" }
else { Write-Host "${LogPrefix}: $Description" }
@@ -174,7 +176,17 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
# Backslashes directly before the closing quote would otherwise escape it.
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
})
if ($WaitForProcessTree) {
if ($TimeoutSeconds -gt 0) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments -NoNewWindow -PassThru
# Holding the handle keeps ExitCode readable once the process has exited.
$null = $process.Handle
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
# taskkill's stderr would end the script under 'Stop' before the message below.
try { & taskkill.exe /T /F /PID $process.Id 2>&1 | Out-Null } catch {}
throw "$Description did not finish within $TimeoutSeconds seconds and was stopped."
}
$exitCode = $process.ExitCode
} elseif ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
@@ -236,69 +248,121 @@ function Get-MkwDawnRuntimeSha256([string]$DependenciesDirectory) {
return Get-MkwFileSha256 $runtime
}
function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
function ConvertTo-MkwHex([byte[]]$Bytes) {
return [BitConverter]::ToString($Bytes).Replace('-', '').ToLowerInvariant()
}
function Get-MkwSourceTreeFingerprint([string]$Directory, [string[]]$ExcludedTopDirectories = @()) {
<#
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.
SHA-256 over "relative-path content-sha256" lines of a source tree. It must come out the same
under powershell.exe 5.1 and pwsh 7 and for any checkout's line endings, because the package is
harvested on a release machine and re-checked on every user's: paths are sorted ordinally (not
with Sort-Object, whose culture collation differs between NLS and ICU) and text content is hashed
with CRLF folded to LF. Dot-paths are skipped by name, not by the hidden attribute, which the
payload zip drops. Returns '' when the tree is 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('.') }) } |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
if ([string]::IsNullOrWhiteSpace($Directory)) { return '' }
if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { return '' }
$root = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
$relatives = [Collections.Generic.List[string]]::new()
foreach ($file in [IO.Directory]::EnumerateFiles($root, '*', [IO.SearchOption]::AllDirectories)) {
$relative = $file.Substring($root.Length + 1).Replace('\', '/')
$parts = $relative.Split('/')
if ($parts.Length -gt 1 -and $ExcludedTopDirectories -contains $parts[0]) { continue }
$hidden = $false
foreach ($part in $parts) { if ($part.StartsWith('.', [StringComparison]::Ordinal)) { $hidden = $true; break } }
if (-not $hidden) { $relatives.Add($relative) }
}
$sorted = $relatives.ToArray()
[Array]::Sort($sorted, [StringComparer]::Ordinal)
$latin1 = [Text.Encoding]::GetEncoding(28591)
$sha = [Security.Cryptography.SHA256]::Create()
try {
$builder = [Text.StringBuilder]::new()
foreach ($file in $files) {
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
$content = Get-MkwFileSha256 $file.FullName
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
foreach ($relative in $sorted) {
$bytes = [IO.File]::ReadAllBytes($root + '\' + $relative.Replace('/', '\'))
# Latin-1 maps bytes 1:1, so the round trip only changes the line endings.
$text = $latin1.GetString($bytes)
if ($text.IndexOf([char]0) -lt 0 -and $text.Contains("`r`n")) {
$bytes = $latin1.GetBytes($text.Replace("`r`n", "`n"))
}
[void]$builder.Append($relative).Append(' ').Append((ConvertTo-MkwHex $sha.ComputeHash($bytes))).Append("`n")
}
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
return ConvertTo-MkwHex $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
} finally { $sha.Dispose() }
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
}
function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
<#
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.
#>
return Get-MkwSourceTreeFingerprint $ThirdPartyDirectory
}
function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
<#
SHA-256 over the aurora sources the precompiled archives were harvested from. The compiler,
flag, and Dawn digests only prove the package agrees with its target environment; none notice
aurora itself moving on, and since the runtime calls aurora headers directly, a package
harvested before an aurora change links old code against new call sites (undefined symbol at
best, silently stale renderer at worst). extern/ is excluded because the payload ships those
trees separately. Returns '' when the tree is absent.
The aurora sources the precompiled archives were harvested from. The compiler, flag, and Dawn
digests only prove the package agrees with its target environment; none notice aurora itself
moving on, and since the runtime calls aurora headers directly, a package harvested before an
aurora change links old code against new call sites. extern/ is excluded because the payload
ships those trees separately.
#>
if ([string]::IsNullOrWhiteSpace($AuroraDirectory)) { return '' }
if (-not (Test-Path -LiteralPath $AuroraDirectory -PathType Container)) { return '' }
$root = [IO.Path]::GetFullPath($AuroraDirectory).TrimEnd('\')
$excludedPrefixes = @(
(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 |
Where-Object {
$full = $_.FullName
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
} |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
return Get-MkwSourceTreeFingerprint $AuroraDirectory @('extern', 'build')
}
function Resolve-MkwNativePrebuiltDirectory([string]$DependenciesDirectory, [string]$ToolchainBin,
[string]$AuroraDirectory, [string]$ThirdPartyDirectory) {
<#
The shipped precompiled aurora/third-party package, verified against its recorded provenance
(exact compiler, exact flags, sources) before use. Anything unexpected falls back to building
from source. Shared with Build-Installer.ps1, which runs it against the staged payload under
powershell.exe so a package users would reject never ships.
#>
$package = Join-Path $DependenciesDirectory 'native_prebuilt'
$provenancePath = Join-Path $package 'provenance.json'
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) {
Write-Host 'MKWCBUILD: No precompiled aurora package was shipped; building it from source'
return ''
}
try {
$builder = [Text.StringBuilder]::new()
foreach ($file in $files) {
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
$content = Get-MkwFileSha256 $file.FullName
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
$provenance = Get-Content -LiteralPath $provenancePath -Raw | ConvertFrom-Json
if ($provenance.CompilerSha256 -ne (Get-MkwFileSha256 (Join-Path $ToolchainBin 'clang-22.exe'))) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built by a different compiler; building it from source'
return ''
}
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
} finally { $sha.Dispose() }
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
if ($provenance.FlagFingerprint -ne (Get-MkwNativeFlagFingerprint)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built with different flags; building it from source'
return ''
}
# The archives embed Dawn's headers and link its import library; a different Dawn is an
# ABI mismatch that only shows up as a crash inside the GPU backend.
$recordedDawn = $provenance.PSObject.Properties['DawnRuntimeSha256']
if ($null -eq $recordedDawn -or
$recordedDawn.Value -ne (Get-MkwDawnRuntimeSha256 $DependenciesDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built against a different Dawn; building it from source'
return ''
}
$recordedAurora = $provenance.PSObject.Properties['AuroraSourceFingerprint']
if ($null -eq $recordedAurora -or
$recordedAurora.Value -ne (Get-MkwAuroraSourceFingerprint $AuroraDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different aurora sources; building it from source'
return ''
}
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
if ($null -eq $recordedThirdParty -or
$recordedThirdParty.Value -ne (Get-MkwThirdPartySourceFingerprint $ThirdPartyDirectory)) {
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different vendored third-party sources; building it from source'
return ''
}
} catch {
Write-Host "MKWCBUILD: The precompiled aurora package could not be validated ($($_.Exception.Message)); building it from source"
return ''
}
Write-Host 'MKWCBUILD: Using the precompiled aurora and third-party libraries'
return $package
}
function ConvertTo-MkwCMakePath([string]$Path) {
@@ -347,9 +411,9 @@ function Get-MkwNativeConfigureArguments {
}
}
if (-not [string]::IsNullOrWhiteSpace($NativePrebuiltDirectory)) {
$arguments += "-DMKW_NATIVE_PREBUILT_DIR=$(ConvertTo-MkwCMakePath $NativePrebuiltDirectory)"
}
# Always set, so a reused CMake cache can never keep a package this run rejected.
$arguments += if ([string]::IsNullOrWhiteSpace($NativePrebuiltDirectory)) { '-DMKW_NATIVE_PREBUILT_DIR=' }
else { "-DMKW_NATIVE_PREBUILT_DIR=$(ConvertTo-MkwCMakePath $NativePrebuiltDirectory)" }
$arguments += $AdditionalArguments
return $arguments
+57 -14
View File
@@ -33,28 +33,33 @@ if (-not (Test-Path -LiteralPath $auroraCMake -PathType Leaf)) {
$packages = @(
[pscustomobject]@{
Name = 'SDL'; File = 'SDL3-3.4.10.tar.gz'
Sha256 = '12b34280415ec8418c864408b93d008a20a6530687ee613d60bfbd20411f2785'
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.10/SDL3-3.4.10.tar.gz')
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_SDL3_REF "refs/tags/release-3.4.10"' },
@{ File = $auroraSdl; Text = 'URL "https://github.com/libsdl-org/SDL/archive/${AURORA_SDL3_REF}.tar.gz"' })
},
[pscustomobject]@{
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
Sha256 = 'f50e5ac311a81382da7fa75b97310e4b9006474f9560ac46f54a9967f07d4ae3'
Uris = @('https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz')
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'
Sha256 = '42a7a295c87b2e8a36312e2f84a5823aac63325e8e1d271bc7ced6f83bfb075f'
Uris = @('https://github.com/encounter/dawn/releases/download/v20260807.225922/dawn-windows-amd64.tar.gz')
Pins = @(@{ File = $auroraVersions; Text = '_aurora_dependency_version(AURORA_DAWN_VERSION "v20260807.225922"' },
@{ File = $auroraDawn; Text = 'SHA256=42a7a295c87b2e8a36312e2f84a5823aac63325e8e1d271bc7ced6f83bfb075f' })
},
[pscustomobject]@{
Name = 'fmt'; File = 'fmt-12.1.0.tar.gz'
Sha256 = 'ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea'
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz' })
},
[pscustomobject]@{
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
Sha256 = 'e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1'
Uris = @('https://files.twilitrealm.dev/freetype-2.14.3.tar.gz',
'https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz',
'https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz')
@@ -62,42 +67,50 @@ $packages = @(
},
[pscustomobject]@{
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
Sha256 = '466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5'
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' })
},
[pscustomobject]@{
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
Sha256 = 'fea36f34f9156400209595e300840767ab1a385ede1dc7ee893015aea9c6dbaf'
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"' },
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
},
[pscustomobject]@{
Name = 'png'; File = 'libpng-1.6.58.tar.gz'
Sha256 = 'a9d4df463d36a6e5f9c29bd6f4967312d17e996c1854f3511f833924eb1993cf'
Uris = @('https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz' })
},
[pscustomobject]@{
Name = 'sqlite3'; File = 'sqlite-amalgamation-3510300.zip'
Sha256 = 'acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4'
Uris = @('https://sqlite.org/2026/sqlite-amalgamation-3510300.zip')
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
},
[pscustomobject]@{
Name = 'tracy'; File = 'tracy-0.14.1.zip'
Sha256 = '908f3a2917fa86a247abfcf85dcf04bad1db6986a4d40f94b70512f3e9e98d5b'
Uris = @('https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/refs/tags/v0.14.1.zip' })
},
[pscustomobject]@{
Name = 'xxhash'; File = 'xxHash-0.8.4.tar.gz'
Sha256 = '5738270935e7c3d38a79b3adf7c9692566ce7895a25f67de43ad52ab504acd32'
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz')
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'
Sha256 = 'f9c65aa9c852eb8255b636fd9f07ce1c406f061ec19a2e7d508b318ca0c907d1'
Uris = @('https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz' })
},
[pscustomobject]@{
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
Sha256 = 'eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3'
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
}
@@ -106,6 +119,7 @@ $packages = @(
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
$cppWinRtTool = [pscustomobject]@{
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
Sha256 = 'a8993608ae9263a8288e1a1d9ee2684a5632f4acc59e1e89ddd86b3b889a7367'
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
Pins = @()
}
@@ -128,11 +142,20 @@ function Assert-Pinned($Package) {
}
}
function Get-Sha256([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }
# Every archive this returns, cached or downloaded, matches its pinned Sha256.
function Get-Archive($Package) {
$archive = Join-Path $downloads $Package.File
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
if (Test-Path -LiteralPath $archive -PathType Leaf) {
$actual = Get-Sha256 $archive
if ($actual -eq $Package.Sha256) { return $archive }
Write-Warning "$($Package.Name): cached $archive has SHA-256 $actual, expected $($Package.Sha256); discarding it and downloading again."
Remove-Item -LiteralPath $archive -Force
}
$temporary = $archive + '.partial'
$failures = @()
$mismatch = $false
foreach ($uri in $Package.Uris) {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
Write-Host "Downloading $($Package.Name) from $uri..."
@@ -142,11 +165,21 @@ function Get-Archive($Package) {
$failures += "$uri ($($_.Exception.Message))"
continue
}
$actual = Get-Sha256 $temporary
if ($actual -ne $Package.Sha256) {
$failures += "$uri (SHA-256 $actual, expected $($Package.Sha256))"
$mismatch = $true
continue
}
Move-Item -LiteralPath $temporary -Destination $archive -Force
return $archive
}
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
$hint = ''
if ($mismatch) {
$hint = ' If upstream republished this archive, check the new file against an upstream-published checksum before updating its Sha256 in Prepare-Dependencies.ps1.'
}
throw "$($Package.Name) could not be fetched: $($failures -join '; ').$hint"
}
function Expand-Package([string]$Archive, [string]$Target) {
@@ -170,25 +203,34 @@ function Expand-Package([string]$Archive, [string]$Target) {
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
}
# Records which archive a tree came from, so a pin change replaces a stale tree.
function Get-Stamp($Package) { "$($Package.File) $($Package.Sha256)" }
function Test-Stamp([string]$Target, [string]$Text) {
$stamp = $Target + '.source'
(Test-Path -LiteralPath $stamp -PathType Leaf) -and ([IO.File]::ReadAllText($stamp) -eq $Text)
}
function Install-Package($Package, [string]$Target, [string]$RequiredFile) {
$ready = if ($RequiredFile) { Test-Path -LiteralPath $RequiredFile -PathType Leaf } else { Test-Path -LiteralPath $Target -PathType Container }
if ($ready -and (Test-Stamp $Target (Get-Stamp $Package))) { return }
Expand-Package (Get-Archive $Package) $Target
[IO.File]::WriteAllText($Target + '.source', (Get-Stamp $Package))
Write-Host "Prepared $($Package.Name)"
}
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 }
Expand-Package (Get-Archive $package) $target
[IO.File]::WriteAllText($stamp, $package.File)
Write-Host "Prepared $($package.Name)"
Install-Package $package (Join-Path $Destination $package.Name)
}
$cppWinRt = Join-Path $Destination 'cppwinrt'
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
$cppWinRtCurrent = (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) -and
(Test-Stamp $cppWinRt (Get-Stamp $cppWinRtTool))
if (-not $cppWinRtCurrent) {
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
}
Install-Package $cppWinRtTool $toolRoot $compiler
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
@@ -199,6 +241,7 @@ if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
}
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
[IO.File]::WriteAllText($cppWinRt + '.source', (Get-Stamp $cppWinRtTool))
Write-Host 'Prepared cppwinrt'
}
+10 -17
View File
@@ -5,7 +5,8 @@ param(
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$StageDirectory = 'build/recomp-test',
[ValidateRange(1, 64)] [int]$Parallel = 3
[ValidateRange(1, 64)] [int]$Parallel = 3,
[ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3'
)
$ErrorActionPreference = 'Stop'
@@ -54,15 +55,10 @@ foreach ($relative in $sourceFiles | Sort-Object -Unique) {
}
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
# 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.
# Guest functions the runtime calls directly resolve through the registry at run time, so the
# product links without any game code.
[uint32]$entry = 0x80001000L
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
)
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
$textSize = 0x10
$dataOffset = 0x100 + $textSize
$dol = [byte[]]::new($dataOffset + 4)
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
@@ -73,7 +69,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 (unreachable gaps are zero)
Write-BigEndian32 0x90 $textSize # text[0] length
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
Write-BigEndian32 0xAC 4 # data[0] length
@@ -84,13 +80,10 @@ 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 = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
$entryPoints = '0x{0:X8}' -f $entry
$functionMap = '{0:X8} func_{0:X8}' -f $entry
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
$manifest = Join-Path $stage 'recomp.yml'
[IO.File]::WriteAllText($manifest, @"
@@ -128,7 +121,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) + $guestCallbacks) {
foreach ($address in @($entry)) {
if ($address -notin $translated.functions.entryPoint) {
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
}
@@ -144,7 +137,7 @@ try {
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON', "-DMKW_X86_CPU_PROFILE=$X86CpuProfile")
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
@@ -0,0 +1,99 @@
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
namespace WiiCompiled.Setup.Common;
public enum X86CpuProfile
{
V2,
V3
}
// Setup-time copy of runtime/src/host_cpu_baseline.cpp, checked before anything is built.
public static class HostCpuRequirement
{
private readonly record struct Feature(string Name, uint Leaf, int Register, int Bit, X86CpuProfile Profile);
// Same table and order as the runtime: register 0..3 = eax, ebx, ecx, edx.
private static readonly Feature[] Features =
[
new("SSE3", 1, 2, 0, X86CpuProfile.V2),
new("SSSE3", 1, 2, 9, X86CpuProfile.V2),
new("CMPXCHG16B", 1, 2, 13, X86CpuProfile.V2),
new("SSE4.1", 1, 2, 19, X86CpuProfile.V2),
new("SSE4.2", 1, 2, 20, X86CpuProfile.V2),
new("POPCNT", 1, 2, 23, X86CpuProfile.V2),
new("LAHF-SAHF", 0x80000001u, 2, 0, X86CpuProfile.V2),
new("FMA", 1, 2, 12, X86CpuProfile.V3),
new("MOVBE", 1, 2, 22, X86CpuProfile.V3),
new("OSXSAVE", 1, 2, 27, X86CpuProfile.V3),
new("AVX", 1, 2, 28, X86CpuProfile.V3),
new("F16C", 1, 2, 29, X86CpuProfile.V3),
new("BMI1", 7, 1, 3, X86CpuProfile.V3),
new("AVX2", 7, 1, 5, X86CpuProfile.V3),
new("BMI2", 7, 1, 8, X86CpuProfile.V3),
new("LZCNT", 0x80000001u, 2, 5, X86CpuProfile.V3),
];
public static void EnsureSupported(X86CpuProfile profile)
{
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
return;
// The JIT only reports AVX when the OS also saves the YMM state (XCR0), which is the
// runtime's last check; there is no managed XGETBV.
var missing = FindMissingFeatures(profile, CpuId, Avx.IsSupported);
if (missing.Count > 0)
throw new PlatformNotSupportedException(Describe(profile, missing));
}
// The highest profile this machine runs; builds fall back to v2 when v3 is missing.
public static X86CpuProfile SelectBuildProfile() =>
RuntimeInformation.ProcessArchitecture != Architecture.X64
? X86CpuProfile.V3
: SelectBuildProfile(CpuId, Avx.IsSupported);
public static X86CpuProfile SelectBuildProfile(Func<uint, uint[]> cpuid, bool osSavesAvxState) =>
FindMissingFeatures(X86CpuProfile.V3, cpuid, osSavesAvxState).Count == 0
? X86CpuProfile.V3
: X86CpuProfile.V2;
public static IReadOnlyList<string> FindMissingFeatures(X86CpuProfile profile,
Func<uint, uint[]> cpuid, bool osSavesAvxState)
{
var maxBasic = cpuid(0)[0];
var maxExtended = cpuid(0x80000000u)[0];
var missing = new List<string>();
foreach (var feature in Features)
{
if (feature.Profile > profile)
continue;
var leafAvailable = (feature.Leaf & 0x80000000u) != 0
? feature.Leaf <= maxExtended
: feature.Leaf <= maxBasic;
if (!leafAvailable || (cpuid(feature.Leaf)[feature.Register] & (1u << feature.Bit)) == 0)
missing.Add(feature.Name);
}
if (missing.Count == 0 && profile >= X86CpuProfile.V3 && !osSavesAvxState)
missing.Add("operating system support for AVX register state (XCR0 YMM bits)");
return missing;
}
public static string Describe(X86CpuProfile profile, IReadOnlyList<string> missing) =>
profile == X86CpuProfile.V2
? "This build needs a processor that supports the x86-64-v2 instruction set.\n\n" +
"Missing on this machine: " + string.Join(", ", missing) + "\n\n" +
"x86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors " +
"from Jaguar (2013) onward."
: "This build needs a processor that supports the x86-64-v3 instruction set.\n\n" +
"Missing on this machine: " + string.Join(", ", missing) + "\n\n" +
"x86-64-v3 covers Intel Core processors from Haswell (4th generation, 2013) onward " +
"and AMD processors from Excavator (2015) onward.";
private static uint[] CpuId(uint leaf)
{
var (eax, ebx, ecx, edx) = X86Base.CpuId(unchecked((int)leaf), 0);
return [unchecked((uint)eax), unchecked((uint)ebx), unchecked((uint)ecx), unchecked((uint)edx)];
}
}
@@ -74,6 +74,8 @@ internal static class Program
private static async Task InstallAsync(Dictionary<string, string?> flags, IInstallReporter reporter, CancellationToken token)
{
// local-build.sh falls back to an x86-64-v2 build when this CPU lacks v3.
HostCpuRequirement.EnsureSupported(X86CpuProfile.V2);
var retroDir = flags.GetValueOrDefault("retro-dir");
var installsRetro = !string.IsNullOrEmpty(retroDir);
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
@@ -60,7 +60,8 @@ internal sealed class LocalBuildService
BuildProfile.RetroRewind => "retro-rewind",
_ => "both"
},
"-OutputDirectory", outputDirectory
"-OutputDirectory", outputDirectory,
"-X86CpuProfile", HostCpuRequirement.SelectBuildProfile() == X86CpuProfile.V2 ? "v2" : "v3"
};
if (baseOutputDirectory is not null)
{
@@ -64,6 +64,8 @@ internal static class Program
if (command.Mode is AppMode.SilentInstall or AppMode.RepairProducts)
{
// CPUs without x86-64-v3 get an x86-64-v2 build.
HostCpuRequirement.EnsureSupported(X86CpuProfile.V2);
using var cancellationSignal = CancellationSignal.ObserveEnvironment();
return command.Mode == AppMode.SilentInstall
? ConsoleCommands.Install(command, cancellationSignal.Token).GetAwaiter().GetResult()
@@ -36,6 +36,7 @@ internal static class SelfTests
Test("Toolkit accompanies the workspace", TestToolkitRefreshDecision, failures);
Test("Retro-WFC payload snapshot reconciliation", TestRetroWfcPayloadSnapshotReconciliation, failures);
Test("Retro-WFC transient retry policy", TestRetroWfcTransientRetryPolicy, failures);
Test("Host CPU requirement", TestHostCpuRequirement, failures);
Test("Shared runtime configuration", () => TestPathSettingRoundTrip("dvd_root",
RuntimeConfiguration.SetDvdRoot, RuntimeConfiguration.RemoveDvdRootIfOwned), failures);
Test("Canonical Retro Rewind configuration", () => TestPathSettingRoundTrip("retro_rewind_root",
@@ -994,6 +995,57 @@ internal static class SelfTests
throw new Exception("A non-I/O failure would be retried.");
}
private static void TestHostCpuRequirement()
{
// Leaf 1 ecx, leaf 7 ebx, leaf 0x80000001 ecx of a v2-only and a v3 processor.
const uint v2Leaf1 = (1u << 0) | (1u << 9) | (1u << 13) | (1u << 19) | (1u << 20) | (1u << 23);
const uint v3Leaf1 = v2Leaf1 | (1u << 12) | (1u << 22) | (1u << 27) | (1u << 28) | (1u << 29);
const uint v3Leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
Func<uint, uint[]> Cpu(uint maxBasic, uint leaf1, uint leaf7, uint extended) => leaf => leaf switch
{
0 => [maxBasic, 0, 0, 0],
1 => [0, 0, leaf1, 0],
7 => [0, leaf7, 0, 0],
0x80000000u => [0x80000001u, 0, 0, 0],
0x80000001u => [0, 0, extended, 0],
_ => [0, 0, 0, 0]
};
var nehalem = Cpu(11, v2Leaf1, 0, 1u);
var haswell = Cpu(13, v3Leaf1, v3Leaf7, 1u | (1u << 5));
void Expect(IReadOnlyList<string> actual, string expected, string what)
{
if (string.Join(", ", actual) != expected)
throw new Exception($"{what}: expected [{expected}], got [{string.Join(", ", actual)}].");
}
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, nehalem, false), "",
"A v2 processor was rejected for the v2 build");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, nehalem, false),
"FMA, MOVBE, OSXSAVE, AVX, F16C, BMI1, AVX2, BMI2, LZCNT", "A v2 processor for the v3 build");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, haswell, true), "",
"A v3 processor was rejected");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, haswell, false), "",
"A v3 processor was rejected for the v2 build");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, haswell, false),
"operating system support for AVX register state (XCR0 YMM bits)", "Disabled AVX state");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, Cpu(6, v3Leaf1, v3Leaf7, 1u | (1u << 5)), true),
"BMI1, AVX2, BMI2", "Unavailable leaf 7");
Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, Cpu(11, v2Leaf1, 0, 0), false),
"LAHF-SAHF", "Missing LAHF/SAHF");
if (HostCpuRequirement.SelectBuildProfile(nehalem, false) != X86CpuProfile.V2)
throw new Exception("A v2 processor was not given the v2 build.");
if (HostCpuRequirement.SelectBuildProfile(haswell, true) != X86CpuProfile.V3)
throw new Exception("A v3 processor was not given the v3 build.");
if (HostCpuRequirement.SelectBuildProfile(haswell, false) != X86CpuProfile.V2)
throw new Exception("A v3 processor without AVX state was given the v3 build.");
var message = HostCpuRequirement.Describe(X86CpuProfile.V3, ["AVX2"]);
if (!message.Contains("x86-64-v3") || !message.Contains("Missing on this machine: AVX2"))
throw new Exception("The unsupported-processor message lost its detail.");
}
private static void TestRetroWfcTransientRetryPolicy()
{
if (!InputValidation.IsTransientRetroWfcDownloadFailure(
@@ -43,7 +43,7 @@ internal static class ToolkitFingerprint
[
InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1",
// Maps the PAL-keyed native index to the installed release's addresses.
InstalledLayout.WorkspaceEntryPrefix + "runtime/include/guest_abi/guest_address_table.inc"
InstalledLayout.WorkspaceEntryPrefix + "runtime/include/guest_abi/guest_versions.inc"
];
private const string NativeSourcePrefix = InstalledLayout.WorkspaceEntryPrefix + "runtime/src/";
+1 -1
View File
@@ -77,7 +77,7 @@ mkw_translation_identity() {
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_address_table.inc")"
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
+17 -3
View File
@@ -80,12 +80,26 @@ if (( builds_retro )); then
[[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode'
[[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload'
fi
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
# Apple clang must match the SDK headers; another clang earlier on PATH may not.
cc_bin=$(xcrun -f clang 2>/dev/null) || fail 'Apple clang not found; install the Xcode Command Line Tools (xcode-select --install)'
cxx_bin=$(xcrun -f clang++ 2>/dev/null) || fail 'Apple clang++ not found; install the Xcode Command Line Tools (xcode-select --install)'
for tool in "$cmake_bin" "$ninja_bin" "$cc_bin" "$cxx_bin" shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
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"
x86_cpu_profile=v3
if [[ "$macos_arch" == x86_64 ]]; then
native_macros=$("$cc_bin" -march=native -dM -E -x c /dev/null 2>/dev/null || true)
for macro in __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__; do
if ! grep -q "^#define $macro " <<<"$native_macros"; then
x86_cpu_profile=v2
printf 'MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2\n'
break
fi
done
fi
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --workspace "$workspace" --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.
@@ -164,7 +178,7 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
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")
toolchain_fingerprint=$(printf '%s\n' "$(mkw_toolchain_identity "$workspace" "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin")" "$cc_bin" "$cxx_bin" | mkw_sha256)
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"
@@ -172,7 +186,7 @@ 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"
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DMKW_X86_CPU_PROFILE="$x86_cpu_profile" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
+6
View File
@@ -37,6 +37,10 @@ for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >
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
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
icon="$script_dir/../../runtime/resources/$product.icns"
[[ -f "$icon" ]] || fail "missing app icon: $icon"
app="$output_dir/$product.app"
macos="$app/Contents/MacOS"
frameworks="$app/Contents/Frameworks"
@@ -51,6 +55,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundleExecutable</key><string>$product</string>
<key>CFBundleIdentifier</key><string>org.wiicompiled.$product</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleIconFile</key><string>$product.icns</string>
<key>CFBundleName</key><string>$product</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
@@ -59,6 +64,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
ditto "$icon" "$resources/$product.icns"
ditto "$build_dir/$product" "$macos/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ditto "$build_dir/$asset" "$resources/$asset"
+22
View File
@@ -41,6 +41,28 @@ done
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
# Same x86-64-v2 check as runtime/src/host_cpu_baseline.cpp, before anything is built;
# local-build-macos.command builds for v2 when the CPU lacks v3.
if [[ "$host_arch" == x86_64 ]]; then
cpu_features=$(/usr/sbin/sysctl -n machdep.cpu.features machdep.cpu.extfeatures 2>/dev/null | tr '\n' ' ' || true)
if [[ -n "${cpu_features// /}" ]]; then
missing_features=""
for feature in SSE3=SSE3 SSSE3=SSSE3 CX16=CMPXCHG16B SSE4.1=SSE4.1 SSE4.2=SSE4.2 POPCNT=POPCNT \
LAHF=LAHF-SAHF; do
[[ " $cpu_features " == *" ${feature%%=*} "* ]] || missing_features+="${missing_features:+, }${feature#*=}"
done
if [[ -n "$missing_features" ]]; then
message="This build needs a processor that supports the x86-64-v2 instruction set.
Missing on this machine: $missing_features
x86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors from Jaguar (2013) onward."
printf 'WiiCompiled Setup: %s\n' "$message" >&2
notice "$message"
exit 1
fi
fi
fi
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
+11 -1
View File
@@ -1,6 +1,6 @@
# NTSC-U (RMCE01) function map, ported from ../mkwii/MAP.txt (PAL RMCP01).
# Every entry was validated against the RMCE01 main.dol/StaticR.rel.
# Verdicts: PROVEN=12178, REFERENCED=12573, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29409
# Verdicts: PROVEN=12178, REFERENCED=12583, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29419
800018a8 0x800018a8
80001d50 0x80001d50
80001d6c 0x80001d6c
@@ -10702,6 +10702,14 @@
80243e80 nw4r::g3d::ScnRfl::GetTypeName
80243eb0 nw4r::g3d::ScnRfl::IsDerivedFrom
80243f18 0x80243f18
80243f68 0x80243f68
80243fa8 0x80243fa8
80244010 0x80244010
80244064 0x80244064
802440a0 0x802440a0
802440f8 0x802440f8
802441ac 0x802441ac
802441f0 0x802441f0
802441fc shadTex_802402E0
80244274 EGG::EGG__GlobalScreenCapture_Take
80244760 EGG::EGG__GlobalScreenCapture_Release
@@ -20501,6 +20509,7 @@
806c3c40 Objects::Heyho::vf_0xc
806c3c48 Objects::Heyho::UpdateCollision
806c3d28 Objects::Heyho::GetPropertiesBitfield
806c3d30 0x806c3d30
806c3d38 Objects::HighwayManager::__ct
806c4418 Objects::HighwayManager::__dt
806c44ec 0x806c44ec
@@ -21390,6 +21399,7 @@
806f1e54 Objects::HwanwanBall::LoadRoute
806f1e58 Objects::HwanwanBall::GetSubFileName
806f1e64 Objects::HwanwanBall::GetPropertiesBitfield
806f1e6c Objects::HwanwanBall::__dt(thunk)
806f1e74 0x806f1e74
806f2024 0x806f2024
806f2194 0x806f2194
+12 -7
View File
@@ -33,9 +33,14 @@
],
[
"0x80021BA8",
"0x80225F1C",
"0x80225F14",
"0x80021B08"
],
[
"0x80225F14",
"0x802261F8",
"0x80243F18"
],
[
"0x802261F8",
"0x802402E0",
@@ -608,13 +613,13 @@
],
[
"0x806CE828",
"0x806D02BC",
"0x806D02C4",
"0x806C229C"
],
[
"0x806D02BC",
"0x806D02C4",
"0x806D28F4",
"0x806EA19C"
"0x806EA1A4"
],
[
"0x806D2908",
@@ -673,13 +678,13 @@
],
[
"0x806E95B0",
"0x806EC7C0",
"0x806EC7C8",
"0x806EEC5C"
],
[
"0x806EC7C0",
"0x806EC7C8",
"0x806ED53C",
"0x806D8564"
"0x806D856C"
],
[
"0x806ED53C",
+15 -1
View File
@@ -37,7 +37,7 @@ endif()
function(mkw_apply_translated_compile_options target)
target_compile_options(${target} PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -mllvm -hot-cold-split=true -w -pipe)
endfunction()
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
@@ -293,8 +293,21 @@ function(mkw_configure_product target)
endif()
endfunction()
if(MKW_PLATFORM_WINDOWS)
enable_language(RC)
endif()
# Own object library so the products' C++ options never reach the resource compiler.
function(mkw_add_product_icon target)
if(MKW_PLATFORM_WINDOWS)
add_library(${target}_icon OBJECT "${MKW_RUNTIME_SOURCE_DIR}/resources/${target}.rc")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:${target}_icon>)
endif()
endfunction()
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}")
mkw_configure_product(WiiCompiled)
mkw_add_product_icon(WiiCompiled)
target_sources(WiiCompiled PRIVATE $<TARGET_OBJECTS:mkw_base_registration>)
target_precompile_headers(WiiCompiled PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
@@ -305,6 +318,7 @@ endif()
if(MKW_HAVE_RETRO_REWIND)
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
mkw_configure_product(RetroRewind)
mkw_add_product_icon(RetroRewind)
target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled)
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_registration>)
if(TARGET mkw_retro_sensitive)
-17
View File
@@ -1,5 +1,4 @@
#include "guest_abi/guest_region.h"
#include "guest_function_imports.h"
#if !defined(MKW_TRANSLATED_GUEST_REGION)
#error "MKW_TRANSLATED_GUEST_REGION comes from the translator's shard manifest (emit-build-shards)"
@@ -8,19 +7,3 @@
static_assert(MKW_TRANSLATED_GUEST_REGION >= 0 && MKW_TRANSLATED_GUEST_REGION < GuestRegion::kCount);
extern "C" const uint8_t kMkwGuestRegion = MKW_TRANSLATED_GUEST_REGION;
#define MKW_DEFINE_GUEST_FUNCTION_IMPORT(pal, symbol) \
extern "C" void func_##symbol(CpuContext*); \
extern "C" void (*const mkw_guest_function_##pal)(CpuContext*) = &func_##symbol; \
static_assert(MKW_GADDR_CT(pal) == 0x##symbol##u, "guest_function_imports.inc disagrees with " \
"guest_address_table.inc");
#if MKW_TRANSLATED_GUEST_REGION == 0
#define MKW_GUEST_FUNCTION_IMPORT(pal, ntscU, ntscJ) MKW_DEFINE_GUEST_FUNCTION_IMPORT(pal, pal)
#elif MKW_TRANSLATED_GUEST_REGION == 1
#define MKW_GUEST_FUNCTION_IMPORT(pal, ntscU, ntscJ) MKW_DEFINE_GUEST_FUNCTION_IMPORT(pal, ntscU)
#else
#define MKW_GUEST_FUNCTION_IMPORT(pal, ntscU, ntscJ) MKW_DEFINE_GUEST_FUNCTION_IMPORT(pal, ntscJ)
#endif
#include "guest_function_imports.inc"
#undef MKW_GUEST_FUNCTION_IMPORT
+3
View File
@@ -22,6 +22,8 @@ public:
// Applied to the final host output, covering both AX and direct AI DMA.
void SetMasterVolume(float volume);
void SetMuted(bool muted);
// Mutes output without touching the user's persisted mute setting.
void SetSilenced(bool silenced);
private:
AudioBackend() = default;
@@ -43,6 +45,7 @@ private:
bool m_initialized = false;
float m_masterVolume = 1.0f;
bool m_muted = false;
bool m_silenced = false;
bool m_reportedDroppedBlock = false;
std::vector<int16_t> m_convertBuffer;
};
+9 -1
View File
@@ -1,8 +1,10 @@
#pragma once
#include "friend_code_keyboard.h"
#include "settings_overlay.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include "perf_stats.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -118,6 +120,7 @@ inline void ProcessAuroraEvents(const AuroraEvent* events) {
g_mkwDynamicAspectSurfacePending.store(true, std::memory_order_release);
}
settings_overlay::HandleEvents(events);
FriendCodeKeyboard::HandleEvents(events);
}
inline void ApplyPendingMkwDynamicAspectSurface() {
@@ -140,7 +143,12 @@ 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()) {
bool begun = false;
{
PerfStats::WaitScope wait;
begun = aurora_begin_frame_window();
}
if (!begun) {
return false;
}
GxWorker::Submit([] { GxWorkerBeginFrame(); });
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string_view>
// `--race[=<course id>]`: boot straight into a 1P VS race instead of the title screen.
// `--race-cpu`: the same, with the local racer driven by the CPU.
namespace BootRace {
// False when `arg` is not a boot-race option; throws on a malformed one.
bool ParseArgument(std::string_view arg);
bool Requested();
} // namespace BootRace
+8
View File
@@ -0,0 +1,8 @@
#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
+44 -9
View File
@@ -291,6 +291,11 @@ struct KnownTypedNativeCpuCall {
static constexpr bool kAvailable = false;
};
template <typename T, typename = void>
struct IsPureTypedNativeCpuCall : std::false_type {};
template <typename T>
struct IsPureTypedNativeCpuCall<T, std::void_t<decltype(T::kPure)>> : std::bool_constant<T::kPure> {};
#include "guest_abi/native_cpu_calls.inc"
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
@@ -554,7 +559,10 @@ inline void InvokeDirectCpu(CpuContext* ctx) {
return;
}
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
if constexpr (IsPureTypedNativeCpuCall<KnownTypedNativeCpuCall<Target>>::value) {
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
return;
} else if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
@@ -581,12 +589,20 @@ inline void InvokeDirectCpu(CpuContext* ctx) {
// 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();
// The common indirect case: an unguarded record called from its own CpuContext, so the full
// TryDispatchRawCpuTarget would only set the execution address and call.
MKW_PPC_FORCE_INLINE bool TryDispatchUnguardedRawCpuTarget(uint32_t target, CpuContext* cpu) {
const RawDispatchRecord* record = TranslatedFunctionRegistry::FindRawByAddressPtr(target);
if (record == nullptr || record->entry == nullptr || record->preserveNonvolatileGprs ||
record->nonvolatileFprWriteMask != 0 || TryGetCpuContext() != cpu) {
return false;
}
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
record->entry(cpu);
return true;
}
MKW_PPC_NO_INLINE inline void InvokeIndirectJumpSlow(uint32_t target, CpuContext* cpu) {
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
@@ -601,12 +617,22 @@ inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
ReportMissingIndirectJumpTarget(target, cpu);
}
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
// 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 (TryDispatchUnguardedRawCpuTarget(target, cpu)) {
return;
}
InvokeIndirectJumpSlow(target, cpu);
}
MKW_PPC_NO_INLINE inline void InvokeIndirectCpuSlow(uint32_t target, CpuContext* cpu) {
if (target == 0) {
ReportMissingCpuTarget(target, cpu);
}
ApplyRuntimeCallOptions(target, cpu);
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
@@ -615,3 +641,12 @@ inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
}
ReportMissingCpuTarget(target, cpu);
}
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
ApplyRuntimeCallOptions(target, cpu);
if (TryDispatchUnguardedRawCpuTarget(target, cpu)) {
return;
}
InvokeIndirectCpuSlow(target, cpu);
}
@@ -1,744 +0,0 @@
// AUTO-GENERATED by tools/region/gen_guest_address_table.py - DO NOT EDIT.
// PAL identity -> address in each region: MKW_GUEST_ADDRESS(PAL, NTSC-U, NTSC-J).
// Sorted by PAL address; guest_abi/guest_region.h relies on that.
MKW_GUEST_ADDRESS(80006348, 80006348, 80006348) // __init_hardware
MKW_GUEST_ADDRESS(800077C8, 800077C8, 800077C8) // StrapScene::CheckInput
MKW_GUEST_ADDRESS(8000A000, 80009FC0, 80009F5C)
MKW_GUEST_ADDRESS(8000A400, 8000A3C0, 8000A35C)
MKW_GUEST_ADDRESS(80015500, 800149A0, 80015424) // __write_console
MKW_GUEST_ADDRESS(8002001C, 8001F4BC, 8001FF40)
MKW_GUEST_ADDRESS(80044B30, 80044A90, 80044A50) // GXEnd
MKW_GUEST_ADDRESS(80048C30, 80048B90, 80048B50) // GXEnd
MKW_GUEST_ADDRESS(800847C0, 80084720, 800846E0) // nw4r::lyt::detail::DrawQuad
MKW_GUEST_ADDRESS(80084D20, 80084C80, 80084C40) // nw4r::lyt::detail::DrawQuad
MKW_GUEST_ADDRESS(800A35E0, 800A3540, 800A3500) // nw4r::snd::SoundPlayer::SetVolume
MKW_GUEST_ADDRESS(800BD860, 800BD7C0, 800BD780) // thunk_0x800bc440
MKW_GUEST_ADDRESS(80123F88, 80123EE8, 80123EA8) // AIRegisterDMACallback
MKW_GUEST_ADDRESS(80123FCC, 80123F2C, 80123EEC) // AIInitDMA
MKW_GUEST_ADDRESS(80124048, 80123FA8, 80123F68) // AIStartDMA
MKW_GUEST_ADDRESS(8012405C, 80123FBC, 80123F7C) // AIGetDMABytesLeft
MKW_GUEST_ADDRESS(8012406C, 80123FCC, 80123F8C) // AIGetDMAStartAddr
MKW_GUEST_ADDRESS(80124084, 80123FE4, 80123FA4) // AIGetDMALength
MKW_GUEST_ADDRESS(80124094, 80123FF4, 80123FB4) // AICheckInit
MKW_GUEST_ADDRESS(8012409C, 80123FFC, 80123FBC) // AIGetDSPSampleRate
MKW_GUEST_ADDRESS(801240B0, 80124010, 80123FD0) // AIInit
MKW_GUEST_ADDRESS(80126948, 801268A8, 80126868) // __AXDSPInitCallback
MKW_GUEST_ADDRESS(80126954, 801268B4, 80126874) // __AXDSPResumeCallback
MKW_GUEST_ADDRESS(801269A8, 80126908, 801268C8) // __AXDSPRequestCallback
MKW_GUEST_ADDRESS(801269B8, 80126918, 801268D8)
MKW_GUEST_ADDRESS(801269BC, 8012691C, 801268DC) // __AXOutInitDSP
MKW_GUEST_ADDRESS(8012B830, 8012B790, 8012B750) // AXFXReverbStdExpCallback
MKW_GUEST_ADDRESS(8012E574, 8012E4D4, 8012E494) // PPCMfhid0
MKW_GUEST_ADDRESS(8012E57C, 8012E4DC, 8012E49C) // PPCMthid0
MKW_GUEST_ADDRESS(8012E594, 8012E4F4, 8012E4B4) // PPCMtdec
MKW_GUEST_ADDRESS(8012E59C, 8012E4FC, 8012E4BC) // PPCSync
MKW_GUEST_ADDRESS(8012E5A4, 8012E504, 8012E4C4) // PPCHalt
MKW_GUEST_ADDRESS(8012E5B8, 8012E518, 8012E4D8) // PPCMtmmcr0
MKW_GUEST_ADDRESS(8012E5C0, 8012E520, 8012E4E0) // PPCMtmmcr1
MKW_GUEST_ADDRESS(8012E5C8, 8012E528, 8012E4E8) // PPCMtpmc1
MKW_GUEST_ADDRESS(8012E5D0, 8012E530, 8012E4F0) // PPCMtpmc2
MKW_GUEST_ADDRESS(8012E5D8, 8012E538, 8012E4F8) // PPCMtpmc3
MKW_GUEST_ADDRESS(8012E5E0, 8012E540, 8012E500) // PPCMtpmc4
MKW_GUEST_ADDRESS(8012E630, 8012E590, 8012E550) // PPCMfhid2
MKW_GUEST_ADDRESS(8012E638, 8012E598, 8012E558) // PPCMthid2
MKW_GUEST_ADDRESS(8012E640, 8012E5A0, 8012E560) // PPCMfwpar
MKW_GUEST_ADDRESS(8012E64C, 8012E5AC, 8012E56C) // PPCMtwpar
MKW_GUEST_ADDRESS(8012E654, 8012E5B4, 8012E574) // PPCDisableSpeculation
MKW_GUEST_ADDRESS(8012E684, 8012E5E4, 8012E5A4) // PPCMthid4
MKW_GUEST_ADDRESS(801301B4, 80130114, 801300D4) // GKI_delay
MKW_GUEST_ADDRESS(8015BC80, 8015BBE0, 8015BBA0) // contentFastOpenNAND
MKW_GUEST_ADDRESS(8015BCF8, 8015BC58, 8015BC18) // contentReadNAND
MKW_GUEST_ADDRESS(8015D3FC, 8015D35C, 8015D31C) // DSPCheckMailToDSP
MKW_GUEST_ADDRESS(8015D40C, 8015D36C, 8015D32C) // RVL::DSPCheckMailFromDSP
MKW_GUEST_ADDRESS(8015D41C, 8015D37C, 8015D33C) // RVL::DSPReadMailFromDSP
MKW_GUEST_ADDRESS(8015D430, 8015D390, 8015D350) // DSPSendMailToDSP
MKW_GUEST_ADDRESS(8015D444, 8015D3A4, 8015D364) // DSPInit
MKW_GUEST_ADDRESS(8015D504, 8015D464, 8015D424) // DSPCheckInit
MKW_GUEST_ADDRESS(8015D50C, 8015D46C, 8015D42C) // DSPAddTask
MKW_GUEST_ADDRESS(8015D57C, 8015D4DC, 8015D49C) // DSPAssertTask
MKW_GUEST_ADDRESS(8015DC60, 8015DBC0, 8015DB80) // __DSP_boot_task
MKW_GUEST_ADDRESS(8015DF1C, 8015DE7C, 8015DE3C) // __DVDFSInit
MKW_GUEST_ADDRESS(8015E74C, 8015E6AC, 8015E66C) // DVD::ReadAsyncPrio
MKW_GUEST_ADDRESS(8015E834, 8015E794, 8015E754) // DVDReadPrio
MKW_GUEST_ADDRESS(8015EA1C, 8015E97C, 8015E93C) // DVD::Init
MKW_GUEST_ADDRESS(801628CC, 8016282C, 801627EC) // DVD::ReadAbsAsyncPrio
MKW_GUEST_ADDRESS(801643FC, 8016435C, 8016431C) // __DVDCheckDevice
MKW_GUEST_ADDRESS(80164848, 801647A8, 80164768) // DVD::LowInit
MKW_GUEST_ADDRESS(80164AAC, 80164A0C, 801649CC) // DVD::LowReadDiskID
MKW_GUEST_ADDRESS(80165708, 80165668, 80165628) // DVD::LowUnencryptedRead
MKW_GUEST_ADDRESS(80165A30, 80165990, 80165950) // DVD::LowInquiry
MKW_GUEST_ADDRESS(80166330, 80166290, 80166250) // DVD::LowRead
MKW_GUEST_ADDRESS(80166964, 801668C4, 80166884) // DVD::LowClearCoverInterrupt
MKW_GUEST_ADDRESS(801671D0, 80167130, 801670F0) // ESP_InitLib
MKW_GUEST_ADDRESS(80167224, 80167184, 80167144) // ESP_CloseLib
MKW_GUEST_ADDRESS(80167904, 80167864, 80167824) // ESP_GetDataDir
MKW_GUEST_ADDRESS(8016799C, 801678FC, 801678BC) // ESP_GetTitleId
MKW_GUEST_ADDRESS(80167E78, 80167DD8, 80167D98) // SetExiInterruptMask
MKW_GUEST_ADDRESS(80167F68, 80167EC8, 80167E88) // RVL::EXIImm
MKW_GUEST_ADDRESS(80168288, 801681E8, 801681A8) // RVL::EXIDma
MKW_GUEST_ADDRESS(80168380, 801682E0, 801682A0) // RVL::EXISync
MKW_GUEST_ADDRESS(801689D0, 80168930, 801688F0) // RVL::EXISelect
MKW_GUEST_ADDRESS(80168B00, 80168A60, 80168A20) // RVL::EXIDeselect
MKW_GUEST_ADDRESS(80168FA0, 80168F00, 80168EC0) // EXIInit
MKW_GUEST_ADDRESS(80169260, 801691C0, 80169180) // RVL::EXIUnlock
MKW_GUEST_ADDRESS(80169BCC, 80169B2C, 80169AEC) // ISFS::OpenLib
MKW_GUEST_ADDRESS(8016B49C, 8016B3FC, 8016B3BC) // __GX::DefaultTexRegionCallback
MKW_GUEST_ADDRESS(8016B590, 8016B4F0, 8016B4B0) // __GX::DefaultTlutRegionCallback
MKW_GUEST_ADDRESS(8016B5B4, 8016B514, 8016B4D4) // __GX::Shutdown
MKW_GUEST_ADDRESS(8016B720, 8016B680, 8016B640) // __GX::InitRevisionBits
MKW_GUEST_ADDRESS(8016B850, 8016B7B0, 8016B770) // GX::Init
MKW_GUEST_ADDRESS(8016C668, 8016C5C8, 8016C588) // GX::CPInterruptHandler
MKW_GUEST_ADDRESS(8016C94C, 8016C8AC, 8016C86C) // GX::SetCPUFifo
MKW_GUEST_ADDRESS(8016CB2C, 8016CA8C, 8016CA4C) // GX::SetGPFifo
MKW_GUEST_ADDRESS(8016CDBC, 8016CD1C, 8016CCDC) // __GX::SaveFifo
MKW_GUEST_ADDRESS(8016CF10, 8016CE70, 8016CE30) // GX::GetCPUFifo
MKW_GUEST_ADDRESS(8016D054, 8016CFB4, 8016CF74) // GX::SetBreakPtCallback
MKW_GUEST_ADDRESS(8016D098, 8016CFF8, 8016CFB8) // GX::EnableBreakPt
MKW_GUEST_ADDRESS(8016D138, 8016D098, 8016D058) // GX::DisableBreakPt
MKW_GUEST_ADDRESS(8016D1FC, 8016D15C, 8016D11C) // __GX::CleanGPFifo
MKW_GUEST_ADDRESS(8016D3A4, 8016D304, 8016D2C4) // GX::SetVtxDesc
MKW_GUEST_ADDRESS(8016D3C0, 8016D320, 8016D2E0) // GX::SetVtxDesc_switch
MKW_GUEST_ADDRESS(8016D3C4, 8016D324, 8016D2E4) // GX::SetVtxDesc_caseD_0
MKW_GUEST_ADDRESS(8016D3D8, 8016D338, 8016D2F8) // GX::SetVtxDesc_caseD_1
MKW_GUEST_ADDRESS(8016D3EC, 8016D34C, 8016D30C) // GX::SetVtxDesc_caseD_2
MKW_GUEST_ADDRESS(8016D400, 8016D360, 8016D320) // GX::SetVtxDesc_caseD_3
MKW_GUEST_ADDRESS(8016D414, 8016D374, 8016D334) // GX::SetVtxDesc_caseD_4
MKW_GUEST_ADDRESS(8016D428, 8016D388, 8016D348) // GX::SetVtxDesc_caseD_5
MKW_GUEST_ADDRESS(8016D43C, 8016D39C, 8016D35C) // GX::SetVtxDesc_caseD_6
MKW_GUEST_ADDRESS(8016D450, 8016D3B0, 8016D370) // GX::SetVtxDesc_caseD_7
MKW_GUEST_ADDRESS(8016D464, 8016D3C4, 8016D384) // GX::SetVtxDesc_caseD_8
MKW_GUEST_ADDRESS(8016D478, 8016D3D8, 8016D398) // GX::SetVtxDesc_caseD_9
MKW_GUEST_ADDRESS(8016D48C, 8016D3EC, 8016D3AC) // GX::SetVtxDesc_caseD_a
MKW_GUEST_ADDRESS(8016D4C0, 8016D420, 8016D3E0) // GX::SetVtxDesc_caseD_19
MKW_GUEST_ADDRESS(8016D4F4, 8016D454, 8016D414) // GX::SetVtxDesc_caseD_b
MKW_GUEST_ADDRESS(8016D508, 8016D468, 8016D428) // GX::SetVtxDesc_caseD_c
MKW_GUEST_ADDRESS(8016D51C, 8016D47C, 8016D43C) // GX::SetVtxDesc_caseD_d
MKW_GUEST_ADDRESS(8016D530, 8016D490, 8016D450) // GX::SetVtxDesc_caseD_e
MKW_GUEST_ADDRESS(8016D544, 8016D4A4, 8016D464) // GX::SetVtxDesc_caseD_f
MKW_GUEST_ADDRESS(8016D558, 8016D4B8, 8016D478) // GX::SetVtxDesc_caseD_10
MKW_GUEST_ADDRESS(8016D56C, 8016D4CC, 8016D48C) // GX::SetVtxDesc_caseD_11
MKW_GUEST_ADDRESS(8016D580, 8016D4E0, 8016D4A0) // GX::SetVtxDesc_caseD_12
MKW_GUEST_ADDRESS(8016D594, 8016D4F4, 8016D4B4) // GX::SetVtxDesc_caseD_13
MKW_GUEST_ADDRESS(8016D5A8, 8016D508, 8016D4C8) // GX::SetVtxDesc_caseD_14
MKW_GUEST_ADDRESS(8016D5B8, 8016D518, 8016D4D8) // GX::SetVtxDesc_caseD_15
MKW_GUEST_ADDRESS(8016D608, 8016D568, 8016D528) // GX::SetVtxDescv
MKW_GUEST_ADDRESS(8016D814, 8016D774, 8016D734) // __GX::SetVCD
MKW_GUEST_ADDRESS(8016D8C4, 8016D824, 8016D7E4) // __GX::CalculateVLim
MKW_GUEST_ADDRESS(8016D9F0, 8016D950, 8016D910) // GX::GetVtxDesc
MKW_GUEST_ADDRESS(8016DA0C, 8016D96C, 8016D92C) // GX::GetVtxDesc_switch
MKW_GUEST_ADDRESS(8016DA10, 8016D970, 8016D930) // GX::GetVtxDesc_caseD_0
MKW_GUEST_ADDRESS(8016DA20, 8016D980, 8016D940) // GX::GetVtxDesc_caseD_1
MKW_GUEST_ADDRESS(8016DA30, 8016D990, 8016D950) // GX::GetVtxDesc_caseD_2
MKW_GUEST_ADDRESS(8016DA40, 8016D9A0, 8016D960) // GX::GetVtxDesc_caseD_3
MKW_GUEST_ADDRESS(8016DA50, 8016D9B0, 8016D970) // GX::GetVtxDesc_caseD_4
MKW_GUEST_ADDRESS(8016DA60, 8016D9C0, 8016D980) // GX::GetVtxDesc_caseD_5
MKW_GUEST_ADDRESS(8016DA70, 8016D9D0, 8016D990) // GX::GetVtxDesc_caseD_6
MKW_GUEST_ADDRESS(8016DA80, 8016D9E0, 8016D9A0) // GX::GetVtxDesc_caseD_7
MKW_GUEST_ADDRESS(8016DA90, 8016D9F0, 8016D9B0) // GX::GetVtxDesc_caseD_8
MKW_GUEST_ADDRESS(8016DAA0, 8016DA00, 8016D9C0) // GX::GetVtxDesc_caseD_9
MKW_GUEST_ADDRESS(8016DAB0, 8016DA10, 8016D9D0) // GX::GetVtxDesc_caseD_a
MKW_GUEST_ADDRESS(8016DAD4, 8016DA34, 8016D9F4) // GX::GetVtxDesc_caseD_19
MKW_GUEST_ADDRESS(8016DAF8, 8016DA58, 8016DA18) // GX::GetVtxDesc_caseD_b
MKW_GUEST_ADDRESS(8016DB08, 8016DA68, 8016DA28) // GX::GetVtxDesc_caseD_c
MKW_GUEST_ADDRESS(8016DB18, 8016DA78, 8016DA38) // GX::GetVtxDesc_caseD_d
MKW_GUEST_ADDRESS(8016DB28, 8016DA88, 8016DA48) // GX::GetVtxDesc_caseD_e
MKW_GUEST_ADDRESS(8016DB38, 8016DA98, 8016DA58) // GX::GetVtxDesc_caseD_f
MKW_GUEST_ADDRESS(8016DB48, 8016DAA8, 8016DA68) // GX::GetVtxDesc_caseD_10
MKW_GUEST_ADDRESS(8016DB58, 8016DAB8, 8016DA78) // GX::GetVtxDesc_caseD_11
MKW_GUEST_ADDRESS(8016DB68, 8016DAC8, 8016DA88) // GX::GetVtxDesc_caseD_12
MKW_GUEST_ADDRESS(8016DB78, 8016DAD8, 8016DA98) // GX::GetVtxDesc_caseD_13
MKW_GUEST_ADDRESS(8016DB88, 8016DAE8, 8016DAA8) // GX::GetVtxDesc_caseD_14
MKW_GUEST_ADDRESS(8016DB98, 8016DAF8, 8016DAB8) // GX::GetVtxDesc_caseD_15
MKW_GUEST_ADDRESS(8016DBA4, 8016DB04, 8016DAC4) // GX::GetVtxDescv
MKW_GUEST_ADDRESS(8016DC34, 8016DB94, 8016DB54) // GX::ClearVtxDesc
MKW_GUEST_ADDRESS(8016DC68, 8016DBC8, 8016DB88) // GX::SetVtxAttrFmt
MKW_GUEST_ADDRESS(8016DC94, 8016DBF4, 8016DBB4) // GX::SetVtxAttrFmt_switch
MKW_GUEST_ADDRESS(8016DC98, 8016DBF8, 8016DBB8) // GX::SetVtxAttrFmt_caseD_9
MKW_GUEST_ADDRESS(8016DCB0, 8016DC10, 8016DBD0) // GX::SetVtxAttrFmt_caseD_19
MKW_GUEST_ADDRESS(8016DCEC, 8016DC4C, 8016DC0C) // GX::SetVtxAttrFmt_caseD_b
MKW_GUEST_ADDRESS(8016DD00, 8016DC60, 8016DC20) // GX::SetVtxAttrFmt_caseD_c
MKW_GUEST_ADDRESS(8016DD14, 8016DC74, 8016DC34) // GX::SetVtxAttrFmt_caseD_d
MKW_GUEST_ADDRESS(8016DD2C, 8016DC8C, 8016DC4C) // GX::SetVtxAttrFmt_caseD_e
MKW_GUEST_ADDRESS(8016DD44, 8016DCA4, 8016DC64) // GX::SetVtxAttrFmt_caseD_f
MKW_GUEST_ADDRESS(8016DD5C, 8016DCBC, 8016DC7C) // GX::SetVtxAttrFmt_caseD_10
MKW_GUEST_ADDRESS(8016DD74, 8016DCD4, 8016DC94) // GX::SetVtxAttrFmt_caseD_11
MKW_GUEST_ADDRESS(8016DD94, 8016DCF4, 8016DCB4) // GX::SetVtxAttrFmt_caseD_12
MKW_GUEST_ADDRESS(8016DDAC, 8016DD0C, 8016DCCC) // GX::SetVtxAttrFmt_caseD_13
MKW_GUEST_ADDRESS(8016DDC4, 8016DD24, 8016DCE4) // GX::SetVtxAttrFmt_caseD_14
MKW_GUEST_ADDRESS(8016DDD8, 8016DD38, 8016DCF8) // GX::SetVtxAttrFmt_caseD_15
MKW_GUEST_ADDRESS(8016DE08, 8016DD68, 8016DD28) // GX::SetVtxAttrFmtv
MKW_GUEST_ADDRESS(8016DE48, 8016DDA8, 8016DD68) // GX::SetVtxAttrFmtv_switch
MKW_GUEST_ADDRESS(8016DE4C, 8016DDAC, 8016DD6C) // GX::SetVtxAttrFmtv_caseD_9
MKW_GUEST_ADDRESS(8016DE64, 8016DDC4, 8016DD84) // GX::SetVtxAttrFmtv_caseD_19
MKW_GUEST_ADDRESS(8016DEA0, 8016DE00, 8016DDC0) // GX::SetVtxAttrFmtv_caseD_b
MKW_GUEST_ADDRESS(8016DEB4, 8016DE14, 8016DDD4) // GX::SetVtxAttrFmtv_caseD_c
MKW_GUEST_ADDRESS(8016DEC8, 8016DE28, 8016DDE8) // GX::SetVtxAttrFmtv_caseD_d
MKW_GUEST_ADDRESS(8016DEE0, 8016DE40, 8016DE00) // GX::SetVtxAttrFmtv_caseD_e
MKW_GUEST_ADDRESS(8016DEF8, 8016DE58, 8016DE18) // GX::SetVtxAttrFmtv_caseD_f
MKW_GUEST_ADDRESS(8016DF10, 8016DE70, 8016DE30) // GX::SetVtxAttrFmtv_caseD_10
MKW_GUEST_ADDRESS(8016DF28, 8016DE88, 8016DE48) // GX::SetVtxAttrFmtv_caseD_11
MKW_GUEST_ADDRESS(8016DF48, 8016DEA8, 8016DE68) // GX::SetVtxAttrFmtv_caseD_12
MKW_GUEST_ADDRESS(8016DF60, 8016DEC0, 8016DE80) // GX::SetVtxAttrFmtv_caseD_13
MKW_GUEST_ADDRESS(8016DF78, 8016DED8, 8016DE98) // GX::SetVtxAttrFmtv_caseD_14
MKW_GUEST_ADDRESS(8016DF8C, 8016DEEC, 8016DEAC) // GX::SetVtxAttrFmtv_caseD_15
MKW_GUEST_ADDRESS(8016DFCC, 8016DF2C, 8016DEEC) // __GX::SetVAT
MKW_GUEST_ADDRESS(8016E04C, 8016DFAC, 8016DF6C) // GX::GetVtxAttrFmt
MKW_GUEST_ADDRESS(8016E078, 8016DFD8, 8016DF98) // GX::GetVtxAttrFmt_switch
MKW_GUEST_ADDRESS(8016E07C, 8016DFDC, 8016DF9C) // GX::GetVtxAttrFmt_caseD_9
MKW_GUEST_ADDRESS(8016E0A4, 8016E004, 8016DFC4) // GX::GetVtxAttrFmt_caseD_19
MKW_GUEST_ADDRESS(8016E118, 8016E078, 8016E038) // GX::GetVtxAttrFmt_caseD_b
MKW_GUEST_ADDRESS(8016E13C, 8016E09C, 8016E05C) // GX::GetVtxAttrFmt_caseD_c
MKW_GUEST_ADDRESS(8016E160, 8016E0C0, 8016E080) // GX::GetVtxAttrFmt_caseD_d
MKW_GUEST_ADDRESS(8016E188, 8016E0E8, 8016E0A8) // GX::GetVtxAttrFmt_caseD_e
MKW_GUEST_ADDRESS(8016E1B0, 8016E110, 8016E0D0) // GX::GetVtxAttrFmt_caseD_f
MKW_GUEST_ADDRESS(8016E1C4, 8016E124, 8016E0E4)
MKW_GUEST_ADDRESS(8016E1D8, 8016E138, 8016E0F8) // GX::GetVtxAttrFmt_caseD_10
MKW_GUEST_ADDRESS(8016E200, 8016E160, 8016E120) // GX::GetVtxAttrFmt_caseD_11
MKW_GUEST_ADDRESS(8016E228, 8016E188, 8016E148) // GX::GetVtxAttrFmt_caseD_12
MKW_GUEST_ADDRESS(8016E250, 8016E1B0, 8016E170) // GX::GetVtxAttrFmt_caseD_13
MKW_GUEST_ADDRESS(8016E278, 8016E1D8, 8016E198) // GX::GetVtxAttrFmt_caseD_14
MKW_GUEST_ADDRESS(8016E2A0, 8016E200, 8016E1C0) // GX::GetVtxAttrFmt_caseD_15
MKW_GUEST_ADDRESS(8016E2B8, 8016E218, 8016E1D8) // GX::GetVtxAttrFmtv
MKW_GUEST_ADDRESS(8016E32C, 8016E28C, 8016E24C) // GX::SetArray
MKW_GUEST_ADDRESS(8016E37C, 8016E2DC, 8016E29C) // GX::SetTexCoordGen2
MKW_GUEST_ADDRESS(8016E3A4, 8016E304, 8016E2C4) // GX::SetTexCoordGen2_switch
MKW_GUEST_ADDRESS(8016E3A8, 8016E308, 8016E2C8) // GX::SetTexCoordGen2_caseD_0
MKW_GUEST_ADDRESS(8016E3B4, 8016E314, 8016E2D4) // GX::SetTexCoordGen2_caseD_1
MKW_GUEST_ADDRESS(8016E3C0, 8016E320, 8016E2E0) // GX::SetTexCoordGen2_caseD_2
MKW_GUEST_ADDRESS(8016E3CC, 8016E32C, 8016E2EC) // GX::SetTexCoordGen2_caseD_3
MKW_GUEST_ADDRESS(8016E3D8, 8016E338, 8016E2F8) // GX::SetTexCoordGen2_caseD_13
MKW_GUEST_ADDRESS(8016E3E0, 8016E340, 8016E300) // GX::SetTexCoordGen2_caseD_14
MKW_GUEST_ADDRESS(8016E3E8, 8016E348, 8016E308) // GX::SetTexCoordGen2_caseD_4
MKW_GUEST_ADDRESS(8016E3F0, 8016E350, 8016E310) // GX::SetTexCoordGen2_caseD_5
MKW_GUEST_ADDRESS(8016E3F8, 8016E358, 8016E318) // GX::SetTexCoordGen2_caseD_6
MKW_GUEST_ADDRESS(8016E400, 8016E360, 8016E320) // GX::SetTexCoordGen2_caseD_7
MKW_GUEST_ADDRESS(8016E408, 8016E368, 8016E328) // GX::SetTexCoordGen2_caseD_8
MKW_GUEST_ADDRESS(8016E410, 8016E370, 8016E330) // GX::SetTexCoordGen2_caseD_9
MKW_GUEST_ADDRESS(8016E418, 8016E378, 8016E338) // GX::SetTexCoordGen2_caseD_a
MKW_GUEST_ADDRESS(8016E420, 8016E380, 8016E340) // GX::SetTexCoordGen2_caseD_b
MKW_GUEST_ADDRESS(8016E424, 8016E384, 8016E344) // GX::SetTexCoordGen2_caseD_10
MKW_GUEST_ADDRESS(8016E514, 8016E474, 8016E434) // GX::SetTexCoordGen2_switch
MKW_GUEST_ADDRESS(8016E518, 8016E478, 8016E438) // GX::SetTexCoordGen2_caseD_0
MKW_GUEST_ADDRESS(8016E528, 8016E488, 8016E448) // GX::SetTexCoordGen2_caseD_1
MKW_GUEST_ADDRESS(8016E538, 8016E498, 8016E458) // GX::SetTexCoordGen2_caseD_2
MKW_GUEST_ADDRESS(8016E548, 8016E4A8, 8016E468) // GX::SetTexCoordGen2_caseD_3
MKW_GUEST_ADDRESS(8016E558, 8016E4B8, 8016E478) // GX::SetTexCoordGen2_caseD_4
MKW_GUEST_ADDRESS(8016E568, 8016E4C8, 8016E488) // GX::SetTexCoordGen2_caseD_5
MKW_GUEST_ADDRESS(8016E578, 8016E4D8, 8016E498) // GX::SetTexCoordGen2_caseD_6
MKW_GUEST_ADDRESS(8016E588, 8016E4E8, 8016E4A8)
MKW_GUEST_ADDRESS(8016E5A4, 8016E504, 8016E4C4) // GX::SetNumTexGens
MKW_GUEST_ADDRESS(8016E654, 8016E5B4, 8016E574) // GX::Flush
MKW_GUEST_ADDRESS(8016E6B0, 8016E610, 8016E5D0) // GX::ResetWriteGatherPipe
MKW_GUEST_ADDRESS(8016E6E4, 8016E644, 8016E604) // __GX::Abort
MKW_GUEST_ADDRESS(8016E848, 8016E7A8, 8016E768) // GX::AbortFrame
MKW_GUEST_ADDRESS(8016E9FC, 8016E95C, 8016E91C) // GX::SetDrawSync
MKW_GUEST_ADDRESS(8016EAB0, 8016EA10, 8016E9D0) // GX::DrawDone
MKW_GUEST_ADDRESS(8016EB70, 8016EAD0, 8016EA90) // GX::PixModeSync
MKW_GUEST_ADDRESS(8016EB94, 8016EAF4, 8016EAB4) // GX::PokeAlphaMode
MKW_GUEST_ADDRESS(8016EBA4, 8016EB04, 8016EAC4) // GX::PokeAlphaUpdate
MKW_GUEST_ADDRESS(8016EBBC, 8016EB1C, 8016EADC) // GX::PokeAlphaUpdate
MKW_GUEST_ADDRESS(8016EBD0, 8016EB30, 8016EAF0) // GX::PokeBlendMode
MKW_GUEST_ADDRESS(8016EC2C, 8016EB8C, 8016EB4C) // GX::PokeColorUpdate
MKW_GUEST_ADDRESS(8016EC40, 8016EBA0, 8016EB60) // GX::PokeDstAlpha
MKW_GUEST_ADDRESS(8016EC58, 8016EBB8, 8016EB78) // GX::PokeDither
MKW_GUEST_ADDRESS(8016EC6C, 8016EBCC, 8016EB8C) // GX::PokeZMode
MKW_GUEST_ADDRESS(8016ECCC, 8016EC2C, 8016EBEC) // GX::TokenInterruptHandler
MKW_GUEST_ADDRESS(8016ED50, 8016ECB0, 8016EC70) // GX::SetDrawDoneCallback
MKW_GUEST_ADDRESS(8016ED94, 8016ECF4, 8016ECB4) // GX::FinishInterruptHandler
MKW_GUEST_ADDRESS(8016EE14, 8016ED74, 8016ED34) // __GX::PEInit
MKW_GUEST_ADDRESS(8016EE78, 8016EDD8, 8016ED98) // __GX::SetDirtyState
MKW_GUEST_ADDRESS(8016F0F0, 8016F050, 8016F010) // GX::Begin
MKW_GUEST_ADDRESS(8016F23C, 8016F19C, 8016F15C) // __GX::SendFlushPrim
MKW_GUEST_ADDRESS(8016F314, 8016F274, 8016F234) // GX::SetLineWidth
MKW_GUEST_ADDRESS(8016F348, 8016F2A8, 8016F268) // GX::SetPointSize
MKW_GUEST_ADDRESS(8016F37C, 8016F2DC, 8016F29C) // GX::EnableTexOffsets
MKW_GUEST_ADDRESS(8016F3B8, 8016F318, 8016F2D8) // GX::SetCullMode
MKW_GUEST_ADDRESS(8016F3E0, 8016F340, 8016F300) // GX::SetCoPlanar
MKW_GUEST_ADDRESS(8016F414, 8016F374, 8016F334) // __GX::SetGenMode
MKW_GUEST_ADDRESS(8016F438, 8016F398, 8016F358) // GX::SetDispCopySrc
MKW_GUEST_ADDRESS(8016F478, 8016F3D8, 8016F398) // GX::SetTexCopySrc
MKW_GUEST_ADDRESS(8016F4B8, 8016F418, 8016F3D8) // GX::SetDispCopyDst
MKW_GUEST_ADDRESS(8016F4DC, 8016F43C, 8016F3FC) // GX::SetTexCopyDst
MKW_GUEST_ADDRESS(8016F5F8, 8016F558, 8016F518) // GX::SetDispCopyFrame2Field
MKW_GUEST_ADDRESS(8016F618, 8016F578, 8016F538) // GX::SetCopyClamp
MKW_GUEST_ADDRESS(8016FA40, 8016F9A0, 8016F960) // GX::SetCopyFilter
MKW_GUEST_ADDRESS(8016FC24, 8016FB84, 8016FB44) // GX::SetDispCopyGamma
MKW_GUEST_ADDRESS(8016FC38, 8016FB98, 8016FB58) // GX::CopyDisp
MKW_GUEST_ADDRESS(8016FD74, 8016FCD4, 8016FC94) // GX::CopyTex
MKW_GUEST_ADDRESS(8016FECC, 8016FE2C, 8016FDEC) // GX::ClearBoundingBox
MKW_GUEST_ADDRESS(80170320, 80170280, 80170240) // GX::LoadLightObjImm
MKW_GUEST_ADDRESS(8017039C, 801702FC, 801702BC) // GX::SetChanAmbColor
MKW_GUEST_ADDRESS(80170474, 801703D4, 80170394) // GX::SetChanMatColor
MKW_GUEST_ADDRESS(8017054C, 801704AC, 8017046C) // GX::SetNumChans
MKW_GUEST_ADDRESS(80170570, 801704D0, 80170490) // GX::SetChanCtrl
MKW_GUEST_ADDRESS(801707F8, 80170758, 80170718) // GX::InitTexObj
MKW_GUEST_ADDRESS(80170938, 80170898, 80170858) // GX::InitTexObj_switch
MKW_GUEST_ADDRESS(8017093C, 8017089C, 8017085C) // GX::InitTexObj_caseD_0
MKW_GUEST_ADDRESS(80170950, 801708B0, 80170870) // GX::InitTexObj_caseD_1
MKW_GUEST_ADDRESS(80170964, 801708C4, 80170884) // GX::InitTexObj_caseD_3
MKW_GUEST_ADDRESS(80170978, 801708D8, 80170898) // GX::InitTexObj_caseD_6
MKW_GUEST_ADDRESS(8017098C, 801708EC, 801708AC) // GX::InitTexObj_caseD_e
MKW_GUEST_ADDRESS(801709A0, 80170900, 801708C0) // GX::InitTexObj_caseD_7
MKW_GUEST_ADDRESS(80170A04, 80170964, 80170924) // GX::InitTexObjCI
MKW_GUEST_ADDRESS(80170A4C, 801709AC, 8017096C) // GX::InitTexObjLOD
MKW_GUEST_ADDRESS(80170B50, 80170AB0, 80170A70) // GX::InitTexObjWrapMode
MKW_GUEST_ADDRESS(80170B64, 80170AC4, 80170A84) // GX::InitTexObjTlut
MKW_GUEST_ADDRESS(80170B6C, 80170ACC, 80170A8C) // GX::InitTexObjFilter
MKW_GUEST_ADDRESS(80170B94, 80170AF4, 80170AB4) // GX::InitTexObjLODBias
MKW_GUEST_ADDRESS(80170BE8, 80170B48, 80170B08) // GX::InitTexObjUserData
MKW_GUEST_ADDRESS(80170BF8, 80170B58, 80170B18) // GX::GetTexObjAll
MKW_GUEST_ADDRESS(80170CBC, 80170C1C, 80170BDC) // GX::GetTexObjLODAll
MKW_GUEST_ADDRESS(80170DC8, 80170D28, 80170CE8) // GX::LoadTexObjPreLoaded
MKW_GUEST_ADDRESS(80170F2C, 80170E8C, 80170E4C) // GX::LoadTexObj
MKW_GUEST_ADDRESS(80170F80, 80170EE0, 80170EA0) // GX::InitTlutObj
MKW_GUEST_ADDRESS(80170FA8, 80170F08, 80170EC8) // GX::LoadTlut
MKW_GUEST_ADDRESS(80171110, 80171070, 80171030) // GX::InvalidateTexAll
MKW_GUEST_ADDRESS(80171180, 801710E0, 801710A0) // GX::SetTexCoordScaleManually
MKW_GUEST_ADDRESS(801711FC, 8017115C, 8017111C) // GX::SetTexCoordBias
MKW_GUEST_ADDRESS(801712F0, 80171250, 80171210) // __GX::SetSUTexRegs
MKW_GUEST_ADDRESS(80171458, 801713B8, 80171378) // __GX::SetTmemConfig
MKW_GUEST_ADDRESS(801717AC, 8017170C, 801716CC) // GX::SetTevIndirect
MKW_GUEST_ADDRESS(80171814, 80171774, 80171734) // GX::SetIndTexMtx
MKW_GUEST_ADDRESS(80171968, 801718C8, 80171888) // GX::SetIndTexCoordScale
MKW_GUEST_ADDRESS(80171A6C, 801719CC, 8017198C) // GX::SetIndTexOrder
MKW_GUEST_ADDRESS(80171B38, 80171A98, 80171A58) // GX::SetNumIndStages
MKW_GUEST_ADDRESS(80171B58, 80171AB8, 80171A78) // GX::SetTevDirect
MKW_GUEST_ADDRESS(80171BA0, 80171B00, 80171AC0) // GX::SetTevIndWarp
MKW_GUEST_ADDRESS(80171C28, 80171B88, 80171B48) // __GX::FlushTextureState
MKW_GUEST_ADDRESS(80171C4C, 80171BAC, 80171B6C) // GX::SetTevOp
MKW_GUEST_ADDRESS(80171CE0, 80171C40, 80171C00) // GX::SetTevColorIn
MKW_GUEST_ADDRESS(80171D20, 80171C80, 80171C40) // GX::SetTevAlphaIn
MKW_GUEST_ADDRESS(80171D60, 80171CC0, 80171C80) // GX::SetTevColorOp
MKW_GUEST_ADDRESS(80171DB8, 80171D18, 80171CD8) // GX::SetTevAlphaOp
MKW_GUEST_ADDRESS(80171E10, 80171D70, 80171D30) // GX::SetTevColor
MKW_GUEST_ADDRESS(80171E70, 80171DD0, 80171D90) // GX::SetTevColorS10
MKW_GUEST_ADDRESS(80171ED4, 80171E34, 80171DF4) // GX::SetTevKColor
MKW_GUEST_ADDRESS(80171F30, 80171E90, 80171E50) // GX::SetTevKColorSel
MKW_GUEST_ADDRESS(80171F80, 80171EE0, 80171EA0) // GX::SetTevKAlphaSel
MKW_GUEST_ADDRESS(80171FD0, 80171F30, 80171EF0) // GX::SetTevSwapMode
MKW_GUEST_ADDRESS(8017200C, 80171F6C, 80171F2C) // GX::SetTevSwapModeTable
MKW_GUEST_ADDRESS(80172088, 80171FE8, 80171FA8) // GX::SetAlphaCompare
MKW_GUEST_ADDRESS(801720C0, 80172020, 80171FE0) // GX::SetZTexture
MKW_GUEST_ADDRESS(8017214C, 801720AC, 8017206C) // GX::SetTevOrder
MKW_GUEST_ADDRESS(801722A8, 80172208, 801721C8) // GX::SetNumTevStages
MKW_GUEST_ADDRESS(801722CC, 8017222C, 801721EC) // GX::SetFog
MKW_GUEST_ADDRESS(801724F8, 80172458, 80172418) // GX::InitFogAdjTable
MKW_GUEST_ADDRESS(8017277C, 801726DC, 8017269C) // GX::SetBlendMode
MKW_GUEST_ADDRESS(801727CC, 8017272C, 801726EC) // GX::SetColorUpdate
MKW_GUEST_ADDRESS(801727F8, 80172758, 80172718) // GX::SetAlphaUpdate
MKW_GUEST_ADDRESS(80172824, 80172784, 80172744) // GX::SetZMode
MKW_GUEST_ADDRESS(80172858, 801727B8, 80172778) // GX::SetZCompLoc
MKW_GUEST_ADDRESS(80172888, 801727E8, 801727A8) // GX::SetPixelFmt
MKW_GUEST_ADDRESS(80172930, 80172890, 80172850) // GX::SetDither
MKW_GUEST_ADDRESS(8017295C, 801728BC, 8017287C) // GX::SetDstAlpha
MKW_GUEST_ADDRESS(80172A30, 80172990, 80172950) // GX::DrawSphere
MKW_GUEST_ADDRESS(80172E00, 80172D60, 80172D20) // GX::BeginDisplayList
MKW_GUEST_ADDRESS(80172EB4, 80172E14, 80172DD4) // GX::EndDisplayList
MKW_GUEST_ADDRESS(80172F64, 80172EC4, 80172E84) // GX::CallDisplayList
MKW_GUEST_ADDRESS(80172FD8, 80172F38, 80172EF8) // __GX::SetProjection
MKW_GUEST_ADDRESS(8017301C, 80172F7C, 80172F3C) // GX::SetProjection
MKW_GUEST_ADDRESS(80173080, 80172FE0, 80172FA0) // GX::SetProjectionv
MKW_GUEST_ADDRESS(801730CC, 8017302C, 80172FEC) // GX::GetProjectionv
MKW_GUEST_ADDRESS(8017310C, 8017306C, 8017302C) // GX::LoadPosMtxImm
MKW_GUEST_ADDRESS(8017315C, 801730BC, 8017307C) // GX::LoadPosMtxIndx
MKW_GUEST_ADDRESS(80173188, 801730E8, 801730A8) // GX::LoadNrmMtxImm
MKW_GUEST_ADDRESS(801731E0, 80173140, 80173100) // GX::LoadNrmMtxIndx3x3
MKW_GUEST_ADDRESS(80173214, 80173174, 80173134) // GX::SetCurrentMtx
MKW_GUEST_ADDRESS(80173234, 80173194, 80173154) // GX::LoadTexMtxImm
MKW_GUEST_ADDRESS(801732E8, 80173248, 80173208) // __GX::SetViewport
MKW_GUEST_ADDRESS(80173378, 801732D8, 80173298) // GX::SetViewportJitter
MKW_GUEST_ADDRESS(801733B4, 80173314, 801732D4) // GX::SetViewport
MKW_GUEST_ADDRESS(801733E0, 80173340, 80173300) // GX::GetViewportv
MKW_GUEST_ADDRESS(80173400, 80173360, 80173320) // GX::SetZScaleOffset
MKW_GUEST_ADDRESS(80173430, 80173390, 80173350) // GX::SetScissor
MKW_GUEST_ADDRESS(801734E0, 80173440, 80173400) // GX::SetScissorBoxOffset
MKW_GUEST_ADDRESS(8017351C, 8017347C, 8017343C) // GX::SetClipMode
MKW_GUEST_ADDRESS(80173544, 801734A4, 80173464) // __GX::SetMatrixIndex
MKW_GUEST_ADDRESS(801735CC, 8017352C, 801734EC) // GX::SetGPMetric
MKW_GUEST_ADDRESS(801736F4, 80173654, 80173614) // GX::SetGPMetric_switch
MKW_GUEST_ADDRESS(801736F8, 80173658, 80173618) // GX::SetGPMetric_caseD_0
MKW_GUEST_ADDRESS(80173718, 80173678, 80173638) // GX::SetGPMetric_caseD_1
MKW_GUEST_ADDRESS(80173738, 80173698, 80173658) // GX::SetGPMetric_caseD_2
MKW_GUEST_ADDRESS(80173758, 801736B8, 80173678) // GX::SetGPMetric_caseD_3
MKW_GUEST_ADDRESS(80173778, 801736D8, 80173698) // GX::SetGPMetric_caseD_4
MKW_GUEST_ADDRESS(80173798, 801736F8, 801736B8) // GX::SetGPMetric_caseD_5
MKW_GUEST_ADDRESS(801737B8, 80173718, 801736D8) // GX::SetGPMetric_caseD_6
MKW_GUEST_ADDRESS(801737D8, 80173738, 801736F8) // GX::SetGPMetric_caseD_7
MKW_GUEST_ADDRESS(801737F8, 80173758, 80173718) // GX::SetGPMetric_caseD_8
MKW_GUEST_ADDRESS(80173818, 80173778, 80173738) // GX::SetGPMetric_caseD_9
MKW_GUEST_ADDRESS(80173838, 80173798, 80173758) // GX::SetGPMetric_caseD_22
MKW_GUEST_ADDRESS(80173858, 801737B8, 80173778) // GX::SetGPMetric_caseD_a
MKW_GUEST_ADDRESS(80173878, 801737D8, 80173798) // GX::SetGPMetric_caseD_b
MKW_GUEST_ADDRESS(80173894, 801737F4, 801737B4) // GX::SetGPMetric_caseD_c
MKW_GUEST_ADDRESS(801738B0, 80173810, 801737D0) // GX::SetGPMetric_caseD_d
MKW_GUEST_ADDRESS(801738CC, 8017382C, 801737EC) // GX::SetGPMetric_caseD_e
MKW_GUEST_ADDRESS(801738E8, 80173848, 80173808) // GX::SetGPMetric_caseD_f
MKW_GUEST_ADDRESS(80173904, 80173864, 80173824) // GX::SetGPMetric_caseD_10
MKW_GUEST_ADDRESS(80173920, 80173880, 80173840) // GX::SetGPMetric_caseD_11
MKW_GUEST_ADDRESS(8017393C, 8017389C, 8017385C) // GX::SetGPMetric_caseD_12
MKW_GUEST_ADDRESS(80173958, 801738B8, 80173878) // GX::SetGPMetric_caseD_13
MKW_GUEST_ADDRESS(80173974, 801738D4, 80173894) // GX::SetGPMetric_caseD_14
MKW_GUEST_ADDRESS(80173990, 801738F0, 801738B0) // GX::SetGPMetric_caseD_15
MKW_GUEST_ADDRESS(801739AC, 8017390C, 801738CC) // GX::SetGPMetric_caseD_16
MKW_GUEST_ADDRESS(801739C8, 80173928, 801738E8) // GX::SetGPMetric_caseD_17
MKW_GUEST_ADDRESS(801739E4, 80173944, 80173904) // GX::SetGPMetric_caseD_18
MKW_GUEST_ADDRESS(80173A00, 80173960, 80173920) // GX::SetGPMetric_caseD_19
MKW_GUEST_ADDRESS(80173A1C, 8017397C, 8017393C) // GX::SetGPMetric_caseD_1a
MKW_GUEST_ADDRESS(80173A38, 80173998, 80173958) // GX::SetGPMetric_caseD_1b
MKW_GUEST_ADDRESS(80173A54, 801739B4, 80173974) // GX::SetGPMetric_caseD_1c
MKW_GUEST_ADDRESS(80173A70, 801739D0, 80173990) // GX::SetGPMetric_caseD_1d
MKW_GUEST_ADDRESS(80173A8C, 801739EC, 801739AC) // GX::SetGPMetric_caseD_1e
MKW_GUEST_ADDRESS(80173AA8, 80173A08, 801739C8) // GX::SetGPMetric_caseD_1f
MKW_GUEST_ADDRESS(80173AC4, 80173A24, 801739E4) // GX::SetGPMetric_caseD_20
MKW_GUEST_ADDRESS(80173AE0, 80173A40, 80173A00) // GX::SetGPMetric_caseD_21
MKW_GUEST_ADDRESS(80173AF8, 80173A58, 80173A18) // GX::SetGPMetric_caseD_23
MKW_GUEST_ADDRESS(80173B18, 80173A78, 80173A38) // GX::SetGPMetric_switch
MKW_GUEST_ADDRESS(80173B1C, 80173A7C, 80173A3C) // GX::SetGPMetric_caseD_0
MKW_GUEST_ADDRESS(80173B38, 80173A98, 80173A58) // GX::SetGPMetric_caseD_1
MKW_GUEST_ADDRESS(80173B54, 80173AB4, 80173A74) // GX::SetGPMetric_caseD_2
MKW_GUEST_ADDRESS(80173B70, 80173AD0, 80173A90) // GX::SetGPMetric_caseD_3
MKW_GUEST_ADDRESS(80173B8C, 80173AEC, 80173AAC) // GX::SetGPMetric_caseD_8
MKW_GUEST_ADDRESS(80173BA8, 80173B08, 80173AC8) // GX::SetGPMetric_caseD_15
MKW_GUEST_ADDRESS(80173BC4, 80173B24, 80173AE4) // GX::SetGPMetric_caseD_4
MKW_GUEST_ADDRESS(80173BE0, 80173B40, 80173B00) // GX::SetGPMetric_caseD_5
MKW_GUEST_ADDRESS(80173BFC, 80173B5C, 80173B1C) // GX::SetGPMetric_caseD_6
MKW_GUEST_ADDRESS(80173C18, 80173B78, 80173B38) // GX::SetGPMetric_caseD_7
MKW_GUEST_ADDRESS(80173C34, 80173B94, 80173B54) // GX::SetGPMetric_caseD_9
MKW_GUEST_ADDRESS(80173C64, 80173BC4, 80173B84) // GX::SetGPMetric_caseD_a
MKW_GUEST_ADDRESS(80173C94, 80173BF4, 80173BB4) // GX::SetGPMetric_caseD_b
MKW_GUEST_ADDRESS(80173CC4, 80173C24, 80173BE4) // GX::SetGPMetric_caseD_c
MKW_GUEST_ADDRESS(80173CF4, 80173C54, 80173C14) // GX::SetGPMetric_caseD_d
MKW_GUEST_ADDRESS(80173D24, 80173C84, 80173C44) // GX::SetGPMetric_caseD_e
MKW_GUEST_ADDRESS(80173D54, 80173CB4, 80173C74) // GX::SetGPMetric_caseD_f
MKW_GUEST_ADDRESS(80173D84, 80173CE4, 80173CA4) // GX::SetGPMetric_caseD_10
MKW_GUEST_ADDRESS(80173DB0, 80173D10, 80173CD0) // GX::SetGPMetric_caseD_11
MKW_GUEST_ADDRESS(80173DC0, 80173D20, 80173CE0) // GX::SetGPMetric_caseD_12
MKW_GUEST_ADDRESS(80173DD0, 80173D30, 80173CF0) // GX::SetGPMetric_caseD_13
MKW_GUEST_ADDRESS(80173DE0, 80173D40, 80173D00) // GX::SetGPMetric_caseD_14
MKW_GUEST_ADDRESS(80173DEC, 80173D4C, 80173D0C) // GX::SetGPMetric_caseD_16
MKW_GUEST_ADDRESS(80173DF8, 80173D58, 80173D18) // GX::ClearGPMetric
MKW_GUEST_ADDRESS(80192F7C, 80192EDC, 80192E9C) // IPCInit
MKW_GUEST_ADDRESS(80193010, 80192F70, 80192F30) // IPCReadReg
MKW_GUEST_ADDRESS(80193020, 80192F80, 80192F40) // IPCWriteReg
MKW_GUEST_ADDRESS(80193040, 80192FA0, 80192F60) // IPCSetBufferLo
MKW_GUEST_ADDRESS(80193478, 801933D8, 80193398) // IPCCltInit
MKW_GUEST_ADDRESS(801937E0, 80193740, 80193700) // IOS::OpenAsync
MKW_GUEST_ADDRESS(801938F8, 80193858, 80193818) // IOS::Open
MKW_GUEST_ADDRESS(801938FC, 8019385C, 8019381C)
MKW_GUEST_ADDRESS(80193A18, 80193978, 80193938) // IOS::CloseAsync
MKW_GUEST_ADDRESS(80193AD8, 80193A38, 801939F8) // IOS::Close
MKW_GUEST_ADDRESS(80193B80, 80193AE0, 80193AA0) // IOS::ReadAsync
MKW_GUEST_ADDRESS(80193C80, 80193BE0, 80193BA0) // IOS::Read
MKW_GUEST_ADDRESS(80193D88, 80193CE8, 80193CA8) // IOS::WriteAsync
MKW_GUEST_ADDRESS(80193E88, 80193DE8, 80193DA8) // IOS::Write
MKW_GUEST_ADDRESS(80193F90, 80193EF0, 80193EB0) // IOS::SeekAsync
MKW_GUEST_ADDRESS(80194070, 80193FD0, 80193F90) // IOS::Seek
MKW_GUEST_ADDRESS(80194158, 801940B8, 80194078) // IOS::IoctlAsync
MKW_GUEST_ADDRESS(80194290, 801941F0, 801941B0) // IOS::Ioctl
MKW_GUEST_ADDRESS(801944FC, 8019445C, 8019441C) // IOS::IoctlvAsync
MKW_GUEST_ADDRESS(801945E0, 80194540, 80194500) // IOS::Ioctlv
MKW_GUEST_ADDRESS(801949B8, 80194918, 801948D8) // iosCreateHeap
MKW_GUEST_ADDRESS(8019B43C, 8019B39C, 8019B35C) // NAND::Create
MKW_GUEST_ADDRESS(8019B524, 8019B484, 8019B444) // NAND::Private::CreateAsync
MKW_GUEST_ADDRESS(8019B59C, 8019B4FC, 8019B4BC) // NAND::Delete
MKW_GUEST_ADDRESS(8019B6E4, 8019B644, 8019B604) // NAND::Private::DeleteAsync
MKW_GUEST_ADDRESS(8019B7A4, 8019B704, 8019B6C4) // NAND::Read
MKW_GUEST_ADDRESS(8019B80C, 8019B76C, 8019B72C) // NAND::ReadAsync
MKW_GUEST_ADDRESS(8019B884, 8019B7E4, 8019B7A4) // NAND::Write
MKW_GUEST_ADDRESS(8019B8EC, 8019B84C, 8019B80C) // NAND::WriteAsync
MKW_GUEST_ADDRESS(8019B964, 8019B8C4, 8019B884) // NAND::Seek
MKW_GUEST_ADDRESS(8019BA04, 8019B964, 8019B924) // NAND::SeekAsync
MKW_GUEST_ADDRESS(8019BBE0, 8019BB40, 8019BB00) // NAND::CreateDir
MKW_GUEST_ADDRESS(8019BCC8, 8019BC28, 8019BBE8) // NAND::Private::CreateDirAsync
MKW_GUEST_ADDRESS(8019BEE8, 8019BE48, 8019BE08) // NAND::Move
MKW_GUEST_ADDRESS(8019BF4C, 8019BEAC, 8019BE6C) // NAND::GetLength
MKW_GUEST_ADDRESS(8019C048, 8019BFA8, 8019BF68) // NAND::GetLengthAsync
MKW_GUEST_ADDRESS(8019C380, 8019C2E0, 8019C2A0) // NAND::GetStatus
MKW_GUEST_ADDRESS(8019C448, 8019C3A8, 8019C368) // NAND::Private::GetStatusAsync
MKW_GUEST_ADDRESS(8019C800, 8019C760, 8019C720) // NAND::Open
MKW_GUEST_ADDRESS(8019C918, 8019C878, 8019C838) // NAND::OpenAsync
MKW_GUEST_ADDRESS(8019C990, 8019C8F0, 8019C8B0) // NAND::Private::OpenAsync
MKW_GUEST_ADDRESS(8019CA80, 8019C9E0, 8019C9A0) // NAND::Close
MKW_GUEST_ADDRESS(8019CAEC, 8019CA4C, 8019CA0C) // NAND::CloseAsync
MKW_GUEST_ADDRESS(8019CB74, 8019CAD4, 8019CA94) // NAND::SafeOpen
MKW_GUEST_ADDRESS(8019CF28, 8019CE88, 8019CE48) // NAND::SafeClose
MKW_GUEST_ADDRESS(8019D104, 8019D064, 8019D024) // NAND::Private::SafeOpenAsync
MKW_GUEST_ADDRESS(8019D720, 8019D680, 8019D640) // NAND::SafeCloseAsync
MKW_GUEST_ADDRESS(8019E18C, 8019E0EC, 8019E0AC) // NAND::Init
MKW_GUEST_ADDRESS(8019E390, 8019E2F0, 8019E2B0) // NAND::GetCurrentDir
MKW_GUEST_ADDRESS(8019E770, 8019E6D0, 8019E690) // NAND::GetType
MKW_GUEST_ADDRESS(8019E7B4, 8019E714, 8019E6D4) // NAND::Private::GetTypeAsync
MKW_GUEST_ADDRESS(8019EAD0, 8019EA30, 8019E9F0) // NAND::Check
MKW_GUEST_ADDRESS(8019F33C, 8019F29C, 8019F25C) // OS::GetConsoleType
MKW_GUEST_ADDRESS(801A00E0, 801A0040, 801A0000) // OS::ExceptionInit
MKW_GUEST_ADDRESS(801A04A0, 801A0400, 801A03C0) // OS::__PSInit
MKW_GUEST_ADDRESS(801A0620, 801A0580, 801A0540) // InsertAlarm
MKW_GUEST_ADDRESS(801A0870, 801A07D0, 801A0790) // OS::SetAlarm
MKW_GUEST_ADDRESS(801A08E0, 801A0840, 801A0800) // OS::SetPeriodicAlarm
MKW_GUEST_ADDRESS(801A1138, 801A1098, 801A1058) // __AIClockInit
MKW_GUEST_ADDRESS(801A1358, 801A12B8, 801A1278) // OS::__InitAudioSystem
MKW_GUEST_ADDRESS(801A1520, 801A1480, 801A1440) // OS::__StopAudioSystem
MKW_GUEST_ADDRESS(801A15EC, 801A154C, 801A150C) // DCEnable
MKW_GUEST_ADDRESS(801A1600, 801A1560, 801A1520) // DCInvalidateRange
MKW_GUEST_ADDRESS(801A162C, 801A158C, 801A154C) // DCFlushRange
MKW_GUEST_ADDRESS(801A165C, 801A15BC, 801A157C) // DCStoreRange
MKW_GUEST_ADDRESS(801A168C, 801A15EC, 801A15AC) // DCFlushRangeNoSync
MKW_GUEST_ADDRESS(801A16B8, 801A1618, 801A15D8) // DCStoreRangeNoSync
MKW_GUEST_ADDRESS(801A16E4, 801A1644, 801A1604) // DCZeroRange
MKW_GUEST_ADDRESS(801A1710, 801A1670, 801A1630) // ICInvalidateRange
MKW_GUEST_ADDRESS(801A1744, 801A16A4, 801A1664) // ICFlashInvalidate
MKW_GUEST_ADDRESS(801A1754, 801A16B4, 801A1674) // ICEnable
MKW_GUEST_ADDRESS(801A1768, 801A16C8, 801A1688) // __LCEnable
MKW_GUEST_ADDRESS(801A1834, 801A1794, 801A1754) // LCEnable
MKW_GUEST_ADDRESS(801A186C, 801A17CC, 801A178C) // LCDisable
MKW_GUEST_ADDRESS(801A1894, 801A17F4, 801A17B4) // LCLoadBlocks
MKW_GUEST_ADDRESS(801A18B8, 801A1818, 801A17D8) // LCStoreBlocks
MKW_GUEST_ADDRESS(801A18DC, 801A183C, 801A17FC) // LCStoreData
MKW_GUEST_ADDRESS(801A197C, 801A18DC, 801A189C) // LCQueueLength
MKW_GUEST_ADDRESS(801A1988, 801A18E8, 801A18A8) // LCQueueWait
MKW_GUEST_ADDRESS(801A1AE4, 801A1A44, 801A1A04) // OS::__CacheInit
MKW_GUEST_ADDRESS(801A1E70, 801A1DD0, 801A1D90) // OS::SetCurrentContext
MKW_GUEST_ADDRESS(801A1ED8, 801A1E38, 801A1DF8) // OS::SaveContext
MKW_GUEST_ADDRESS(801A1F58, 801A1EB8, 801A1E78) // OS::LoadContext
MKW_GUEST_ADDRESS(801A2098, 801A1FF8, 801A1FB8) // OS::ClearContext
MKW_GUEST_ADDRESS(801A20BC, 801A201C, 801A1FDC) // OS::InitContext
MKW_GUEST_ADDRESS(801A25D0, 801A2530, 801A24F0) // OS::Report
MKW_GUEST_ADDRESS(801A2660, 801A25C0, 801A2580) // OS::Panic
MKW_GUEST_ADDRESS(801A4EC4, 801A4E24, 801A4DE4) // OS::Fatal
MKW_GUEST_ADDRESS(801A6000, 801A5F60, 801A5F20)
MKW_GUEST_ADDRESS(801A65AC, 801A650C, 801A64CC) // OS::DisableInterrupts
MKW_GUEST_ADDRESS(801A65C0, 801A6520, 801A64E0) // OS::EnableInterrupts
MKW_GUEST_ADDRESS(801A65D4, 801A6534, 801A64F4) // OS::RestoreInterrupts
MKW_GUEST_ADDRESS(801A65F8, 801A6558, 801A6518) // OS::__SetInterruptHandler
MKW_GUEST_ADDRESS(801A661C, 801A657C, 801A653C) // OS::__InterruptInit
MKW_GUEST_ADDRESS(801A66E0, 801A6640, 801A6600) // SetInterruptMask
MKW_GUEST_ADDRESS(801A693C, 801A689C, 801A685C) // OS::__MaskInterrupts
MKW_GUEST_ADDRESS(801A69BC, 801A691C, 801A68DC) // OS::__UnmaskInterrupts
MKW_GUEST_ADDRESS(801A7000, 801A6F60, 801A6F20)
MKW_GUEST_ADDRESS(801A72FC, 801A725C, 801A721C) // OS::InitMessageQueue
MKW_GUEST_ADDRESS(801A735C, 801A72BC, 801A727C) // OS::SendMessage
MKW_GUEST_ADDRESS(801A7424, 801A7384, 801A7344) // OS::ReceiveMessage
MKW_GUEST_ADDRESS(801A7500, 801A7460, 801A7420) // OS::JamMessage
MKW_GUEST_ADDRESS(801A7DFC, 801A7D5C, 801A7D1C) // OS::__InitMemoryProtection
MKW_GUEST_ADDRESS(801A7EE4, 801A7E44, 801A7E04) // OS::LockMutex
MKW_GUEST_ADDRESS(801A7FC0, 801A7F20, 801A7EE0) // OS::UnlockMutex
MKW_GUEST_ADDRESS(801A8088, 801A7FE8, 801A7FA8) // OS::__UnlockAllMutex
MKW_GUEST_ADDRESS(801A8A50, 801A89B0, 801A8970) // OS::GetResetCode
MKW_GUEST_ADDRESS(801A8A80, 801A89E0, 801A89A0) // OS::ResetSystem
MKW_GUEST_ADDRESS(801A961C, 801A957C, 801A953C) // OS::__ThreadInit
MKW_GUEST_ADDRESS(801A98B0, 801A9810, 801A97D0) // OS::GetCurrentThread
MKW_GUEST_ADDRESS(801A9C08, 801A9B68, 801A9B28) // SelectThread
MKW_GUEST_ADDRESS(801A9E84, 801A9DE4, 801A9DA4) // OS::CreateThread
MKW_GUEST_ADDRESS(801AA0F0, 801AA050, 801AA010) // RVL::OS::ExitThread
MKW_GUEST_ADDRESS(801AA1D4, 801AA134, 801AA0F4) // OS::CancelThread
MKW_GUEST_ADDRESS(801AA3AC, 801AA30C, 801AA2CC) // OS::JoinThread
MKW_GUEST_ADDRESS(801AA4EC, 801AA44C, 801AA40C) // OS::DetachThread
MKW_GUEST_ADDRESS(801AA58C, 801AA4EC, 801AA4AC) // OS::ResumeThread
MKW_GUEST_ADDRESS(801AA6A8, 801AA608, 801AA5C8)
MKW_GUEST_ADDRESS(801AA9B8, 801AA918, 801AA8D8) // OS::SleepThread
MKW_GUEST_ADDRESS(801AAAA4, 801AAA04, 801AA9C4) // OS::WakeupThread
MKW_GUEST_ADDRESS(801AACA8, 801AAC08, 801AABC8) // OS::SleepTicks
MKW_GUEST_ADDRESS(801AAD5C, 801AACBC, 801AAC7C) // OS::GetTime
MKW_GUEST_ADDRESS(801AAD7C, 801AACDC, 801AAC9C) // OS::__GetSystemTime
MKW_GUEST_ADDRESS(801AADE0, 801AAD40, 801AAD00) // OS::__TimeToSystemTime
MKW_GUEST_ADDRESS(801AB75C, 801AB6BC, 801AB67C) // OS::SetPowerCallback
MKW_GUEST_ADDRESS(801AB848, 801AB7A8, 801AB768) // OS::__InitSTM
MKW_GUEST_ADDRESS(801ABC0C, 801ABB6C, 801ABB2C) // OS::__DefaultPowerCallback
MKW_GUEST_ADDRESS(801AD1D4, 801AD134, 801AD0F4) // OS::CheckInstall
MKW_GUEST_ADDRESS(801AE4A0, 801AE400, 801AE3C0) // OS::IsTitleInstalled
MKW_GUEST_ADDRESS(801AE58C, 801AE4EC, 801AE4AC) // exit
MKW_GUEST_ADDRESS(801AF0DC, 801AF03C, 801AEFFC) // PAD::Reset
MKW_GUEST_ADDRESS(801AF1E4, 801AF144, 801AF104) // PAD::Recalibrate
MKW_GUEST_ADDRESS(801AF2F0, 801AF250, 801AF210) // PAD::Init
MKW_GUEST_ADDRESS(801AF44C, 801AF3AC, 801AF36C) // PAD::Read
MKW_GUEST_ADDRESS(801AF908, 801AF868, 801AF828) // PAD::ControlMotor
MKW_GUEST_ADDRESS(801B0220, 801B0180, 801B0140) // SCCheckStatus
MKW_GUEST_ADDRESS(801B1BE4, 801B1B44, 801B1B04) // SCGetAspectRatio
MKW_GUEST_ADDRESS(801B1CAC, 801B1C0C, 801B1BCC) // SCGetEuRgb60Mode
MKW_GUEST_ADDRESS(801B1ED0, 801B1E30, 801B1DF0)
MKW_GUEST_ADDRESS(801B1EDC, 801B1E3C, 801B1DFC)
MKW_GUEST_ADDRESS(801B1EE8, 801B1E48, 801B1E08)
MKW_GUEST_ADDRESS(801B1EF4, 801B1E54, 801B1E14) // SCSetBtCmpDevInfoArray
MKW_GUEST_ADDRESS(801B1F00, 801B1E60, 801B1E20) // SCGetBtDpdSensibility
MKW_GUEST_ADDRESS(801B1F68, 801B1EC8, 801B1E88) // SCGetWpadMotorMode
MKW_GUEST_ADDRESS(801B1FBC, 801B1F1C, 801B1EDC) // SCSetWpadMotorMode
MKW_GUEST_ADDRESS(801B1FC4, 801B1F24, 801B1EE4) // SCGetWpadSensorBarPosition
MKW_GUEST_ADDRESS(801B2018, 801B1F78, 801B1F38) // SCGetWpadSpeakerVolume
MKW_GUEST_ADDRESS(801B206C, 801B1FCC, 801B1F8C) // SCSetWpadSpeakerVolume
MKW_GUEST_ADDRESS(801B23A0, 801B2300, 801B22C0) // SCGetProductArea
MKW_GUEST_ADDRESS(801B2424, 801B2384, 801B2344) // SCGetProductCode
MKW_GUEST_ADDRESS(801B2460, 801B23C0, 801B2380) // SCGetProductSN
MKW_GUEST_ADDRESS(801B24C8, 801B2428, 801B23E8) // SCGetProductGameRegion
MKW_GUEST_ADDRESS(801B2DE0, 801B2D40, 801B2D00) // SIInit
MKW_GUEST_ADDRESS(801B3ACC, 801B3A2C, 801B39EC) // SISetSamplingRate
MKW_GUEST_ADDRESS(801B90F4, 801B9054, 801B9014) // VISetPreRetraceCallback
MKW_GUEST_ADDRESS(801B9138, 801B9098, 801B9058) // VISetPostRetraceCallback
MKW_GUEST_ADDRESS(801B9294, 801B91F4, 801B91B4) // __VIInit
MKW_GUEST_ADDRESS(801B94A4, 801B9404, 801B93C4) // VIInit
MKW_GUEST_ADDRESS(801B9934, 801B9894, 801B9854) // VIInit_caseD_0
MKW_GUEST_ADDRESS(801B993C, 801B989C, 801B985C) // VIInit_caseD_1
MKW_GUEST_ADDRESS(801B9944, 801B98A4, 801B9864) // VIInit_caseD_2
MKW_GUEST_ADDRESS(801B99EC, 801B994C, 801B990C) // VIWaitForRetrace
MKW_GUEST_ADDRESS(801B9F6C, 801B9ECC, 801B9E8C) // VIConfigure
MKW_GUEST_ADDRESS(801BA9A4, 801BA904, 801BA8C4) // VIFlush
MKW_GUEST_ADDRESS(801BAAB8, 801BAA18, 801BA9D8) // VISetNextFrameBuffer
MKW_GUEST_ADDRESS(801BAB24, 801BAA84, 801BAA44) // VIGetNextFrameBuffer
MKW_GUEST_ADDRESS(801BAB2C, 801BAA8C, 801BAA4C) // VISetBlack
MKW_GUEST_ADDRESS(801BABA4, 801BAB04, 801BAAC4) // VIGetRetraceCount
MKW_GUEST_ADDRESS(801BABAC, 801BAB0C, 801BAACC) // VIGetNextField
MKW_GUEST_ADDRESS(801BAC48, 801BABA8, 801BAB68) // VIGetCurrentLine
MKW_GUEST_ADDRESS(801BAD38, 801BAC98, 801BAC58) // VIGetDTVStatus
MKW_GUEST_ADDRESS(801D8D30, 801D8C90, 801D8C50) // NHTTPStartup
MKW_GUEST_ADDRESS(801D9E94, 801D9DF4, 801D9DB4) // NHTTPi_GetSystemInfoP
MKW_GUEST_ADDRESS(8020FF9C, 8020FEFC, 8020FEBC) // EGG::AsyncDisplay::endRender
MKW_GUEST_ADDRESS(80218C2C, 80218B8C, 80218B4C) // EGG::Decomp::decodeSZS
MKW_GUEST_ADDRESS(80219FB4, 80219F14, 80219ED4) // EGG::Display::copyEFBtoXFB
MKW_GUEST_ADDRESS(8021B344, 8021B2A4, 8021B264) // EGG::DrawGX::SetVtxState_switch
MKW_GUEST_ADDRESS(8021B688, 8021B5E8, 8021B5A8)
MKW_GUEST_ADDRESS(8022E2BC, 8022DF38, 8022E1DC) // EGG::LightTexture::SetupTevFinish
MKW_GUEST_ADDRESS(8023BD38, 8023B9B4, 8023BC58) // EGG::ScnRenderer::CreatePath
MKW_GUEST_ADDRESS(8023E53C, 8023E1B8, 8023E45C) // EGG::Screen::SetTVModeDefault
MKW_GUEST_ADDRESS(80242D7C, 80241EC0, 80242C9C) // EGG::TaskThread::Run
MKW_GUEST_ADDRESS(8024373C, 80242880, 8024365C) // EGG::Thread::Run
MKW_GUEST_ADDRESS(80244DE0, 80244D40, 80244D00)
MKW_GUEST_ADDRESS(80244EA0, 80244E00, 80244DC0)
MKW_GUEST_ADDRESS(802574A0, 80257180, 80256E40)
MKW_GUEST_ADDRESS(802574E0, 802571C0, 80256E80)
MKW_GUEST_ADDRESS(80257520, 80257200, 80256EC0)
MKW_GUEST_ADDRESS(80257540, 80257220, 80256EE0)
MKW_GUEST_ADDRESS(8027F820, 8027B4E0, 8027F1C0)
MKW_GUEST_ADDRESS(8029CEB0, 80298B70, 8029C850)
MKW_GUEST_ADDRESS(8029CEF8, 80298BB8, 8029C898)
MKW_GUEST_ADDRESS(802A3EE8, 8029FB80, 802A3888)
MKW_GUEST_ADDRESS(802A3EF4, 8029FB8C, 802A3894)
MKW_GUEST_ADDRESS(802A3F0C, 8029FBA4, 802A38AC)
MKW_GUEST_ADDRESS(802F81A0, 802F3E20, 802F7B20)
MKW_GUEST_ADDRESS(802F8200, 802F3E80, 802F7B80)
MKW_GUEST_ADDRESS(80343230, 8033EEB0, 80342BB0)
MKW_GUEST_ADDRESS(803434E0, 8033F160, 80342E60)
MKW_GUEST_ADDRESS(80343740, 8033F3C0, 803430C0)
MKW_GUEST_ADDRESS(803437C0, 8033F440, 80343140)
MKW_GUEST_ADDRESS(80343DC0, 8033FA40, 80343740)
MKW_GUEST_ADDRESS(80343DE4, 8033FA64, 80343764)
MKW_GUEST_ADDRESS(80344090, 8033FD10, 80343A10)
MKW_GUEST_ADDRESS(80344094, 8033FD14, 80343A14)
MKW_GUEST_ADDRESS(80344098, 8033FD18, 80343A18)
MKW_GUEST_ADDRESS(803440A4, 8033FD24, 80343A24)
MKW_GUEST_ADDRESS(803440A8, 8033FD28, 80343A28)
MKW_GUEST_ADDRESS(803440AC, 8033FD2C, 80343A2C)
MKW_GUEST_ADDRESS(80344110, 8033FD90, 80343A90)
MKW_GUEST_ADDRESS(80344710, 80340390, 80344090)
MKW_GUEST_ADDRESS(80346D20, 803429A0, 803466A0)
MKW_GUEST_ADDRESS(80347130, 80342DB0, 80346AB0)
MKW_GUEST_ADDRESS(80347498, 80343118, 80346E18)
MKW_GUEST_ADDRESS(803477B0, 80343430, 80347130)
MKW_GUEST_ADDRESS(803478B0, 80343530, 80347230)
MKW_GUEST_ADDRESS(80350864, 8034C4E4, 803501E4)
MKW_GUEST_ADDRESS(80350866, 8034C4E6, 803501E6)
MKW_GUEST_ADDRESS(80350872, 8034C4F2, 803501F2)
MKW_GUEST_ADDRESS(8035087C, 8034C4FC, 803501FC)
MKW_GUEST_ADDRESS(80350890, 8034C510, 80350210)
MKW_GUEST_ADDRESS(80385800, 80381480, 80385180)
MKW_GUEST_ADDRESS(80385802, 80381482, 80385182)
MKW_GUEST_ADDRESS(80385804, 80381484, 80385184)
MKW_GUEST_ADDRESS(80385920, 803815A0, 803852A0) // dev/fs_fd
MKW_GUEST_ADDRESS(80385928, 803815A8, 803852A8)
MKW_GUEST_ADDRESS(80385AA8, 80381728, 80385428)
MKW_GUEST_ADDRESS(80385AE0, 80381760, 80385460)
MKW_GUEST_ADDRESS(80386000, 80381C80, 80385980) // SystemManager
MKW_GUEST_ADDRESS(80386298, 80381F18, 80385C18)
MKW_GUEST_ADDRESS(80386448, 803820C8, 80385DC8)
MKW_GUEST_ADDRESS(8038644C, 803820CC, 80385DCC)
MKW_GUEST_ADDRESS(8038647C, 803820FC, 80385DFC)
MKW_GUEST_ADDRESS(80386480, 80382100, 80385E00)
MKW_GUEST_ADDRESS(80386524, 803821A4, 80385EA4)
MKW_GUEST_ADDRESS(80386528, 803821A8, 80385EA8)
MKW_GUEST_ADDRESS(80386608, 80382288, 80385F88)
MKW_GUEST_ADDRESS(80386610, 80382290, 80385F90)
MKW_GUEST_ADDRESS(80386614, 80382294, 80385F94)
MKW_GUEST_ADDRESS(80386618, 80382298, 80385F98)
MKW_GUEST_ADDRESS(8038661C, 8038229C, 80385F9C)
MKW_GUEST_ADDRESS(80386620, 803822A0, 80385FA0)
MKW_GUEST_ADDRESS(80386624, 803822A4, 80385FA4)
MKW_GUEST_ADDRESS(80386664, 803822E4, 80385FE4)
MKW_GUEST_ADDRESS(80386668, 803822E8, 80385FE8)
MKW_GUEST_ADDRESS(80386670, 803822F0, 80385FF0)
MKW_GUEST_ADDRESS(8038667C, 803822FC, 80385FFC)
MKW_GUEST_ADDRESS(803866A0, 80382320, 80386020)
MKW_GUEST_ADDRESS(803866A8, 80382328, 80386028)
MKW_GUEST_ADDRESS(803866F0, 80382370, 80386070)
MKW_GUEST_ADDRESS(80386720, 803823A0, 803860A0)
MKW_GUEST_ADDRESS(80386724, 803823A4, 803860A4)
MKW_GUEST_ADDRESS(80386725, 803823A5, 803860A5)
MKW_GUEST_ADDRESS(80386770, 803823F0, 803860F0)
MKW_GUEST_ADDRESS(80386774, 803823F4, 803860F4)
MKW_GUEST_ADDRESS(8038677C, 803823FC, 803860FC)
MKW_GUEST_ADDRESS(80386780, 80382400, 80386100)
MKW_GUEST_ADDRESS(80386784, 80382404, 80386104)
MKW_GUEST_ADDRESS(803867B0, 80382430, 80386130)
MKW_GUEST_ADDRESS(803867B1, 80382431, 80386131)
MKW_GUEST_ADDRESS(803867C0, 80382440, 80386140)
MKW_GUEST_ADDRESS(803867C4, 80382444, 80386144)
MKW_GUEST_ADDRESS(803867D0, 80382450, 80386150)
MKW_GUEST_ADDRESS(803867D8, 80382458, 80386158)
MKW_GUEST_ADDRESS(803867EC, 8038246C, 8038616C)
MKW_GUEST_ADDRESS(803867F0, 80382470, 80386170)
MKW_GUEST_ADDRESS(80386848, 803824C8, 803861C8)
MKW_GUEST_ADDRESS(803868A0, 80382520, 80386220)
MKW_GUEST_ADDRESS(803868F8, 80382578, 80386278)
MKW_GUEST_ADDRESS(80386918, 80382598, 80386298)
MKW_GUEST_ADDRESS(8038691C, 8038259C, 8038629C)
MKW_GUEST_ADDRESS(80386920, 803825A0, 803862A0)
MKW_GUEST_ADDRESS(80386934, 803825B4, 803862B4)
MKW_GUEST_ADDRESS(80386938, 803825B8, 803862B8)
MKW_GUEST_ADDRESS(8038693C, 803825BC, 803862BC)
MKW_GUEST_ADDRESS(80386940, 803825C0, 803862C0)
MKW_GUEST_ADDRESS(80386948, 803825C8, 803862C8)
MKW_GUEST_ADDRESS(803869E0, 80382660, 80386360)
MKW_GUEST_ADDRESS(80386B38, 803827B8, 803864B8)
MKW_GUEST_ADDRESS(80386B44, 803827C4, 803864C4)
MKW_GUEST_ADDRESS(80386BA0, 80382820, 80386520)
MKW_GUEST_ADDRESS(80386BA8, 80382828, 80386528)
MKW_GUEST_ADDRESS(80386BB4, 80382834, 80386534)
MKW_GUEST_ADDRESS(80386BB8, 80382838, 80386538)
MKW_GUEST_ADDRESS(80386BC0, 80382840, 80386540)
MKW_GUEST_ADDRESS(80386BE4, 80382864, 80386564)
MKW_GUEST_ADDRESS(80386F14, 80382B94, 80386894)
MKW_GUEST_ADDRESS(80386F20, 80382BA0, 803868A0)
MKW_GUEST_ADDRESS(80386F24, 80382BA4, 803868A4)
MKW_GUEST_ADDRESS(80386F60, 80382BD8, 803868E0)
MKW_GUEST_ADDRESS(80388588, 80384208, 80387F08)
MKW_GUEST_ADDRESS(8038858C, 8038420C, 80387F0C)
MKW_GUEST_ADDRESS(803886C8, 80384348, 80388048)
MKW_GUEST_ADDRESS(80388B80, 80384800, 80388500)
MKW_GUEST_ADDRESS(80388BA0, 80384820, 80388520)
MKW_GUEST_ADDRESS(80388BC0, 80384840, 80388540)
MKW_GUEST_ADDRESS(80388BE0, 80384860, 80388560)
MKW_GUEST_ADDRESS(80388DD0, 80384A48, 80388750)
MKW_GUEST_ADDRESS(80388DE0, 80384A58, 80388760)
MKW_GUEST_ADDRESS(80388DF4, 80384A6C, 80388774)
MKW_GUEST_ADDRESS(80529D68, 805258F4, 805296E8) // MovieManager::PrepareAsync
MKW_GUEST_ADDRESS(80550CC0, 8054AD68, 80550640) // THP::PlayerOpen
MKW_GUEST_ADDRESS(80550F48, 8054AFF0, 805508C8) // THP::PlayerClose
MKW_GUEST_ADDRESS(80550F9C, 8054B044, 8055091C) // THP::PlayerCalcNeedMemory
MKW_GUEST_ADDRESS(80551054, 8054B0FC, 805509D4) // THP::PlayerSetBuffer
MKW_GUEST_ADDRESS(80551378, 8054B420, 80550CF8) // THP::PlayerPrepare
MKW_GUEST_ADDRESS(80551658, 8054B700, 80550FD8) // THP::PlayerStop
MKW_GUEST_ADDRESS(80551D38, 8054BDE0, 805516B8) // THP::PlayerGetVideoInfo
MKW_GUEST_ADDRESS(805529A8, 8054CA50, 80552328) // THP::VideoDecoder
MKW_GUEST_ADDRESS(80552A74, 8054CB1C, 805523F4) // THP::VideoDecoderOnMemory
MKW_GUEST_ADDRESS(8055531C, 8054F2FC, 80554C9C) // RelProlog
MKW_GUEST_ADDRESS(805653D0, 80561050, 80564D50)
MKW_GUEST_ADDRESS(8088F400, 8088AFD0, 8088EA6C)
MKW_GUEST_ADDRESS(8088F704, 8088B2D4, 8088ED70)
MKW_GUEST_ADDRESS(8088FDB8, 8088B960, 8088F418)
MKW_GUEST_ADDRESS(808B4C20, 808B0570, 808B3D80)
MKW_GUEST_ADDRESS(809BEBA0, 809BA3E0, 809BDC00)
MKW_GUEST_ADDRESS(809BEBA4, 809BA3E4, 809BDC04)
MKW_GUEST_ADDRESS(809BEBA5, 809BA3E5, 809BDC05)
MKW_GUEST_ADDRESS(809BEBA6, 809BA3E6, 809BDC06)
MKW_GUEST_ADDRESS(809BEBA7, 809BA3E7, 809BDC07)
MKW_GUEST_ADDRESS(809BECF0, 809BA530, 809BDD50)
MKW_GUEST_ADDRESS(809C1830, 809BD070, 809C0890)
MKW_GUEST_ADDRESS(809C183C, 809BD07C, 809C089C)
MKW_GUEST_ADDRESS(809C2898, 809BDC58, 809C18F8)
+55 -26
View File
@@ -7,8 +7,8 @@
// MKW_GADDR(801A9E84) address of that identity in the running region
// MKW_GADDR_CT(801A9E84) same, as a constant expression; translated code only
//
// A PAL address missing from guest_address_table.inc does not compile; regenerate the table
// with tools/region/gen_guest_address_table.py.
// 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>
@@ -28,39 +28,72 @@ struct Row {
constexpr uint32_t In(Id region) const { return address[static_cast<size_t>(region)]; }
};
inline constexpr Row kRows[] = {
#define MKW_GUEST_ADDRESS(pal, ntscU, ntscJ) {{0x##pal##u, 0x##ntscU##u, 0x##ntscJ##u}},
#include "guest_abi/guest_address_table.inc"
#undef MKW_GUEST_ADDRESS
struct Range {
Id region;
uint32_t first;
uint32_t last;
int32_t delta;
};
inline constexpr size_t kRowCount = sizeof(kRows) / sizeof(kRows[0]);
constexpr bool RowsAreSorted() {
for (size_t i = 1; i < kRowCount; ++i) {
if (kRows[i - 1].address[0] >= kRows[i].address[0]) {
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(RowsAreSorted(), "guest_address_table.inc must be sorted by PAL address without duplicates");
static_assert(RangesAreOrdered(), "guest_versions.inc ranges must be sorted by region and PAL address without overlaps");
constexpr const Row* Find(uint32_t pal) {
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 = kRowCount;
size_t hi = kRangeCount;
while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
if (kRows[mid].address[0] < pal) {
lo = mid + 1;
} else {
if (Precedes(region, pal, kRanges[mid])) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo < kRowCount && kRows[lo].address[0] == pal ? &kRows[lo] : nullptr;
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 const Row& RowOf(uint32_t pal) {
return Find(pal) ? *Find(pal) : throw "PAL address missing from guest_abi/guest_address_table.inc";
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
@@ -94,15 +127,11 @@ inline bool Is(uint32_t target) noexcept {
// For hand-written registrations, which name the PAL address.
inline uint32_t Translate(uint32_t pal) noexcept {
const Id region = Current();
if (region == Id::Pal) {
return pal;
}
const Row* row = Find(pal);
if (!row) {
const Mapping mapping = Map(pal, Current());
if (!mapping.found) {
FatalUnknownAddress(pal);
}
return row->In(region);
return mapping.address;
}
struct Facts {
+417
View File
@@ -0,0 +1,417 @@
// 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, 80225F13, -0xA0)
MKW_REGION_RANGE(NtscU, 80225F14, 802261F7, +0x1E004)
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, 806D02C3, -0xC58C)
MKW_REGION_RANGE(NtscU, 806D02C4, 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, 806EC7C7, +0x56AC)
MKW_REGION_RANGE(NtscU, 806EC7C8, 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)
@@ -43,6 +43,9 @@ 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);
extern "C" int32_t OS__DisableInterrupts_801a65ac();
extern "C" int32_t OS__EnableInterrupts_801a65c0();
extern "C" int32_t OS__RestoreInterrupts_801a65d4(int32_t level);
// Keyed by the translated region's addresses, so only translated code sees them;
// runtime calls resolve through the registry.
@@ -90,6 +93,20 @@ MKW_KNOWN_TYPED_NATIVE(80173214, GX__SetCurrentMtx_80173214, cpu->gpr[3]);
#undef MKW_KNOWN_TYPED_NATIVE
// Natives that neither touch CpuContext nor re-enter guest code: no register guard or context scope.
#define MKW_KNOWN_PURE_TYPED_NATIVE_R3(addr, fn, ...) \
template <> struct KnownTypedNativeCpuCall<MKW_GADDR_CT(addr)> { \
static constexpr bool kAvailable = true; \
static constexpr bool kPure = true; \
static void Invoke(CpuContext* cpu) { cpu->gpr[3] = static_cast<uint32_t>(fn(__VA_ARGS__)); } \
}
MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65AC, OS__DisableInterrupts_801a65ac);
MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65C0, OS__EnableInterrupts_801a65c0);
MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65D4, OS__RestoreInterrupts_801a65d4, static_cast<int32_t>(cpu->gpr[3]));
#undef MKW_KNOWN_PURE_TYPED_NATIVE_R3
#define MKW_KNOWN_NATIVE_CPU_CALL(addr, fn) \
template <> struct KnownNativeCpuCall<MKW_GADDR_CT(addr)> { \
static constexpr bool kAvailable = true; \
+14 -6
View File
@@ -1,13 +1,21 @@
#pragma once
// Base translations the runtime calls directly, named by PAL identity. runtime/game binds each
// to the translated region's symbol.
// 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"
#define MKW_GUEST_FUNC(pal) (*::mkw_guest_function_##pal)
namespace GuestFunctions {
using Entry = void (*)(CpuContext*);
#define MKW_GUEST_FUNCTION_IMPORT(pal, ntscU, ntscJ) extern "C" void (*const mkw_guest_function_##pal)(CpuContext*);
#include "guest_function_imports.inc"
#undef MKW_GUEST_FUNCTION_IMPORT
// 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; \
}())
@@ -1,10 +0,0 @@
// AUTO-GENERATED by tools/region/gen_guest_address_table.py - DO NOT EDIT.
// Base translations the runtime calls directly: MKW_GUEST_FUNCTION_IMPORT(PAL, NTSC-U, NTSC-J).
MKW_GUEST_FUNCTION_IMPORT(8012B830, 8012B790, 8012B750) // AXFXReverbStdExpCallback
MKW_GUEST_FUNCTION_IMPORT(801A0620, 801A0580, 801A0540) // InsertAlarm
MKW_GUEST_FUNCTION_IMPORT(801A1ED8, 801A1E38, 801A1DF8) // OS::SaveContext
MKW_GUEST_FUNCTION_IMPORT(801A961C, 801A957C, 801A953C) // OS::__ThreadInit
MKW_GUEST_FUNCTION_IMPORT(801AADE0, 801AAD40, 801AAD00) // OS::__TimeToSystemTime
MKW_GUEST_FUNCTION_IMPORT(801D8D30, 801D8C90, 801D8C50) // NHTTPStartup
MKW_GUEST_FUNCTION_IMPORT(801D9E94, 801D9DF4, 801D9DB4) // NHTTPi_GetSystemInfoP
MKW_GUEST_FUNCTION_IMPORT(8055531C, 8054F2FC, 80554C9C) // RelProlog
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <chrono>
// MKW_PERF_STATS=1: logs the game thread's busy time per presented frame, i.e. wall time minus the
// time it spends blocked on VI pacing, the GX worker or the audio mixer.
namespace PerfStats {
bool Enabled() noexcept;
void AddWait(std::chrono::steady_clock::duration wait) noexcept;
void OnFramePresented() noexcept;
class WaitScope {
public:
WaitScope() noexcept : m_start(Enabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}) {}
~WaitScope() {
if (m_start != std::chrono::steady_clock::time_point{}) {
AddWait(std::chrono::steady_clock::now() - m_start);
}
}
WaitScope(const WaitScope&) = delete;
WaitScope& operator=(const WaitScope&) = delete;
private:
std::chrono::steady_clock::time_point m_start;
};
} // namespace PerfStats
+17 -1
View File
@@ -44,6 +44,7 @@ struct RuntimeUserConfig {
std::optional<uint32_t> frameInterpolationFps;
std::optional<bool> metalFxSpatialUpscaling;
std::optional<bool> skipUnreadyPipelines;
std::optional<bool> vsync;
std::optional<bool> gxWorker;
std::optional<bool> disableCopyFilter;
std::optional<bool> textureReplacements;
@@ -276,6 +277,7 @@ inline void EnsureConfigFile() {
"display_mode = \"windowed\"\n"
"graphics_api = \"auto\"\n"
"skip_unready_pipelines = true\n"
"vsync = false\n"
"# Runs GX command processing on its own thread. Read once, at startup.\n"
"gx_worker = true\n"
"disable_copy_filter = true\n"
@@ -435,6 +437,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.metalFxSpatialUpscaling =
FindConfigValue<bool>(document, "video", "metalfx_spatial_upscaling");
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
config.vsync = FindConfigValue<bool>(document, "video", "vsync");
config.gxWorker = FindConfigValue<bool>(document, "video", "gx_worker");
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
@@ -640,6 +643,11 @@ inline bool SetSkipUnreadyPipelines(bool value) {
return WriteSetting("video", "skip_unready_pipelines", value ? "true" : "false");
}
inline bool SetVsync(bool value) {
Mutable().vsync = value;
return WriteSetting("video", "vsync", value ? "true" : "false");
}
inline bool SetDisableCopyFilter(bool value) {
Mutable().disableCopyFilter = value;
return WriteSetting("video", "disable_copy_filter", value ? "true" : "false");
@@ -831,8 +839,9 @@ inline bool WiiRemotesEnabled(bool fallback = false) {
}
inline bool SetWiiRemotesEnabled(bool value) {
if (!WriteSetting("controller", "wii_remotes", value ? "true" : "false")) return false;
Mutable().wiiRemotes = value;
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
return true;
}
inline bool WiiAutoPairEnabled(bool fallback = true) {
@@ -891,6 +900,10 @@ inline bool SkipUnreadyPipelines(bool fallback = true) {
return Get().skipUnreadyPipelines.value_or(fallback);
}
inline bool Vsync(bool fallback = false) {
return Get().vsync.value_or(fallback);
}
inline bool DisableCopyFilter(bool fallback = true) {
return Get().disableCopyFilter.value_or(fallback);
}
@@ -1003,6 +1016,9 @@ inline void LogLoadedConfig() {
if (config.skipUnreadyPipelines) {
std::cout << " skip_unready_pipelines=" << (*config.skipUnreadyPipelines ? "true" : "false");
}
if (config.vsync) {
std::cout << " vsync=" << (*config.vsync ? "true" : "false");
}
if (config.disableCopyFilter) {
std::cout << " disable_copy_filter=" << (*config.disableCopyFilter ? "true" : "false");
}
+2 -1
View File
@@ -84,7 +84,8 @@ public:
// Crash-path helpers shared by the centralized reporter in main.cpp.
// DumpCrashHeuristics prints plain-language "likely cause" hints derived
// from the register state; WriteGuestMemorySnapshot writes MEM1 to
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
// `mem1Path`, MEM2 to `mem1Path + ".mem2"` and the Kamek overlay to
// `mem1Path + ".kamek"`, logging outcomes to `os`.
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
const uint32_t* missingGuestTarget);
static void WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path);
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+1
View File
@@ -0,0 +1 @@
1 ICON "RetroRewind.ico"
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

+1
View File
@@ -0,0 +1 @@
1 ICON "WiiCompiled.ico"
+26
View File
@@ -12,6 +12,7 @@
#include <sstream>
#include <unordered_map>
#include <vector>
#include "guest_function_imports.h"
#include "guest_small_data.h"
#include "runtime_log.h"
@@ -425,6 +426,31 @@ const TranslatedFunctionInfo* TranslatedFunctionRegistry::FindByAddressPtrSlow(u
return nullptr;
}
GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
if (!RegistryFrozen().load(std::memory_order_acquire)) {
lock.lock();
}
for (const auto& entry : Registry()) {
if (entry.address == address && entry.kind == FunctionKind::BaseTranslated && entry.rawCpuInvoker) {
return entry.rawCpuInvoker;
}
}
if (lock.owns_lock()) {
lock.unlock();
}
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no base translation registered at 0x" << std::hex << address << std::dec
<< std::endl;
std::ostringstream message;
message << "The runtime calls the translated function at guest address 0x" << std::hex << address
<< ", but it was not translated.";
RuntimeCrash::WriteCrashArtifacts("missing_guest_function", message.str(), &address);
SetRuntimeExitCode(EXIT_FAILURE);
ShowRuntimeFatalPopup("Missing translated function", message.str());
MarkFatalErrorReported();
std::exit(EXIT_FAILURE);
}
const RawDispatchRecord* TranslatedFunctionRegistry::FindRawByAddressPtrSlow(uint32_t address) {
if (!RegistryFrozen().load(std::memory_order_acquire)) {
return nullptr;
+7 -1
View File
@@ -15,7 +15,7 @@ AudioBackend& AudioBackend::Instance() {
}
float AudioBackend::EffectiveGainLocked() const {
return m_muted ? 0.0f : m_masterVolume;
return m_muted || m_silenced ? 0.0f : m_masterVolume;
}
void AudioBackend::ApplyGainLocked() {
@@ -36,6 +36,12 @@ void AudioBackend::SetMuted(bool muted) {
ApplyGainLocked();
}
void AudioBackend::SetSilenced(bool silenced) {
std::lock_guard<std::mutex> lock(m_mutex);
m_silenced = silenced;
ApplyGainLocked();
}
bool AudioBackend::EnsureInitializedLocked(uint32_t sampleRate, uint32_t channels) {
if (m_initialized && m_sampleRate == sampleRate && m_channels == channels) {
return true;
+168
View File
@@ -0,0 +1,168 @@
#include "friend_code_keyboard.h"
#include "abi_bridge.h"
#include "guest_abi/guest_region.h"
#include "guest_function_imports.h"
#include "input_bindings.h"
#include "memory.h"
#include "runtime_product.h"
#include <SDL3/SDL_clipboard.h>
#include <SDL3/SDL_events.h>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <optional>
#include <vector>
namespace {
// UI::RegisterFriendPage
constexpr uint32_t kDigitButtonsOffset = 0x44;
constexpr uint32_t kPushButtonSize = 0x254;
constexpr uint32_t kDigitButtonCount = 10;
constexpr uint32_t kButtonDigitOffset = 0x240;
constexpr uint32_t kPageStateOffset = 0x8;
constexpr uint32_t kPageStateActive = 4;
constexpr uint32_t kVtableGetTypeInfoOffset = 0x60;
constexpr int kBackspace = -1;
constexpr size_t kMaxPendingKeys = 32;
constexpr size_t kFriendCodeDigits = 12;
std::mutex g_pendingMutex;
std::vector<int> g_pending;
std::atomic_bool g_pageFocused{false};
bool IsPaste(const SDL_KeyboardEvent& key) {
return !key.repeat && key.key == SDLK_V && (key.mod & (SDL_KMOD_CTRL | SDL_KMOD_GUI)) != 0 &&
(key.mod & SDL_KMOD_ALT) == 0;
}
void AppendClipboardDigits(std::vector<int>& keys) {
char* text = SDL_GetClipboardText();
if (!text) {
return;
}
size_t digits = 0;
for (const char* c = text; *c && digits < kFriendCodeDigits; ++c) {
if (*c >= '0' && *c <= '9') {
keys.push_back(*c - '0');
++digits;
}
}
SDL_free(text);
}
std::optional<int> KeyValue(const SDL_KeyboardEvent& key) {
if ((key.mod & (SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI)) != 0) {
return std::nullopt;
}
if (key.scancode == SDL_SCANCODE_BACKSPACE) {
return kBackspace;
}
if (key.repeat) {
return std::nullopt;
}
if (key.scancode == SDL_SCANCODE_0 || key.scancode == SDL_SCANCODE_KP_0) {
return 0;
}
if (key.scancode >= SDL_SCANCODE_1 && key.scancode <= SDL_SCANCODE_9) {
return 1 + (key.scancode - SDL_SCANCODE_1);
}
if (key.scancode >= SDL_SCANCODE_KP_1 && key.scancode <= SDL_SCANCODE_KP_9) {
return 1 + (key.scancode - SDL_SCANCODE_KP_1);
}
return std::nullopt;
}
bool IsFocusedRegisterFriendPage(uint32_t page) {
try {
const uint32_t vtable = Memory::Read32(page);
return Memory::Read32(vtable + kVtableGetTypeInfoOffset) == MKW_GADDR(8061E804) &&
Memory::Read32(page + kPageStateOffset) == kPageStateActive;
} catch (const Memory::AccessViolation&) {
return false;
}
}
std::optional<uint32_t> DigitButton(uint32_t page, int digit) {
for (uint32_t i = 0; i < kDigitButtonCount; ++i) {
const uint32_t button = page + kDigitButtonsOffset + i * kPushButtonSize;
if (static_cast<int>(Memory::Read32(button + kButtonDigitOffset)) == digit) {
return button;
}
}
return std::nullopt;
}
void Type(CpuContext* ctx, uint32_t page, int key) {
if (key == kBackspace) {
ctx->gpr[3] = page;
MKW_GUEST_FUNC(8061E3B4)(ctx);
return;
}
const std::optional<uint32_t> button = DigitButton(page, key);
if (!button) {
return;
}
ctx->gpr[3] = page;
ctx->gpr[4] = *button;
ctx->gpr[5] = 0;
MKW_GUEST_FUNC(8061E348)(ctx);
}
} // namespace
namespace FriendCodeKeyboard {
void HandleEvents(const AuroraEvent* events) noexcept {
if (!events || !g_pageFocused.load(std::memory_order_acquire) || InputBindings::InputBlocked()) {
return;
}
std::vector<int> keys;
for (const AuroraEvent* ev = events; ev->type != AURORA_NONE; ++ev) {
if (ev->type != AURORA_SDL_EVENT || ev->sdl.type != SDL_EVENT_KEY_DOWN) {
continue;
}
if (IsPaste(ev->sdl.key)) {
AppendClipboardDigits(keys);
} else if (const std::optional<int> key = KeyValue(ev->sdl.key)) {
keys.push_back(*key);
}
}
if (keys.empty()) {
return;
}
std::lock_guard lock(g_pendingMutex);
for (const int key : keys) {
if (g_pending.size() >= kMaxPendingKeys) {
break;
}
g_pending.push_back(key);
}
}
} // namespace FriendCodeKeyboard
// Page::CheckActions, only ever called for the topmost page.
extern "C" void Page_CheckActions_80601c64(CpuContext* ctx) {
const uint32_t page = ctx->gpr[3];
MKW_GUEST_FUNC(80601C64)(ctx);
const bool focused = RuntimeProduct::IsRetroRewind() && IsFocusedRegisterFriendPage(page);
g_pageFocused.store(focused, std::memory_order_release);
std::vector<int> keys;
{
std::lock_guard lock(g_pendingMutex);
keys.swap(g_pending);
}
if (!focused || InputBindings::InputBlocked()) {
return;
}
for (const int key : keys) {
Type(ctx, page, key);
}
}
REGISTER_NATIVE_FUNCTION_AS(0x80601C64, Page_CheckActions_80601c64, "Page_CheckActions_80601c64");
+1 -1
View File
@@ -6,7 +6,7 @@
namespace GuestRegion {
void FatalUnknownAddress(uint32_t pal) noexcept {
std::fprintf(stderr, "Guest region: PAL address 0x%08X has no %s address in guest_address_table.inc\n",
std::fprintf(stderr, "Guest region: PAL address 0x%08X has no %s address in guest_versions.inc\n",
pal, CurrentFacts().gameId);
std::fflush(stderr);
std::abort();
+2
View File
@@ -3,6 +3,7 @@
#include "ax_internal.h"
#include "abi_bridge.h"
#include "perf_stats.h"
#include "audio_backend.h"
#include "ax_mix_kernels.h"
#include "isa/big_endian.h"
@@ -122,6 +123,7 @@ public:
void JoinMix() {
if (m_mixThread.joinable()) {
PerfStats::WaitScope wait;
std::unique_lock<std::mutex> lock(m_mixMutex);
m_mixIdle.wait(lock, [this] { return !m_mixPending && !m_mixBusy; });
}
+220
View File
@@ -0,0 +1,220 @@
#include "boot_race.h"
#include "abi_bridge.h"
#include "guest_abi/guest_region.h"
#include "guest_function_imports.h"
#include "memory.h"
#include "runtime_log.h"
#include <array>
#include <charconv>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <string>
namespace {
constexpr std::string_view kOption = "--race";
constexpr uint32_t kLuigiCircuit = 0x08;
constexpr uint32_t kLastRaceCourse = 0x1F;
// Racedata (menu scenario, copied into the race scenario by Racedata::InitRace)
constexpr uint32_t kMenuScenarioOffset = 0xC10;
constexpr uint32_t kPlayersOffset = 0x8;
constexpr uint32_t kPlayerSize = 0xF0;
constexpr uint32_t kPlayerKartOffset = 0x8;
constexpr uint32_t kPlayerCharacterOffset = 0xC;
constexpr uint32_t kPlayerTypeOffset = 0x10;
constexpr uint32_t kPlayerControllerChannelOffset = 0x6;
constexpr uint32_t kPlayerCount = 12;
constexpr uint32_t kSettingsOffset = 0xB48;
constexpr uint32_t kSectionPadOffset = 0x34;
constexpr uint32_t kFirstGcnControllerId = 0x124;
constexpr uint32_t kSectionP1Vs = 0x20;
enum : uint32_t { kPlayerRealLocal = 0, kPlayerCpu = 1 };
enum : uint32_t { kGameModeVs = 1, kCc150 = 2, kCpuHard = 2, kItemsBalanced = 0 };
struct Racer {
uint32_t character;
uint32_t kart;
};
// Standard kart matching each character's weight class.
constexpr std::array<Racer, 12> kRacers{{
{0x00, 0x01}, // Mario
{0x07, 0x01}, // Luigi
{0x10, 0x01}, // Peach
{0x0A, 0x01}, // Yoshi
{0x0F, 0x01}, // Daisy
{0x08, 0x00}, // Toad
{0x06, 0x00}, // Baby Mario
{0x0E, 0x00}, // Koopa Troopa
{0x03, 0x02}, // Bowser
{0x09, 0x02}, // Donkey Kong
{0x0B, 0x02}, // Wario
{0x16, 0x02}, // Funky Kong
}};
std::optional<uint32_t> g_course;
bool g_cpuDriven = false;
uint32_t ParseCourse(std::string_view text) {
int base = 10;
if (text.starts_with("0x") || text.starts_with("0X")) {
text.remove_prefix(2);
base = 16;
}
uint32_t course = 0;
const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), course, base);
if (text.empty() || ec != std::errc{} || end != text.data() + text.size() || course > kLastRaceCourse) {
throw std::invalid_argument("--race expects a race course id from 0 to 0x1F");
}
return course;
}
void SetUpMenuScenario(uint32_t course) {
const uint32_t racedata = Memory::Read32(MKW_GADDR(809BD728));
if (racedata == 0) {
throw std::runtime_error("--race: Racedata does not exist yet at SectionMgr::Init");
}
const uint32_t scenario = racedata + kMenuScenarioOffset;
for (uint32_t i = 0; i < kRacers.size(); ++i) {
const uint32_t player = scenario + kPlayersOffset + i * kPlayerSize;
Memory::Write32(player + kPlayerKartOffset, kRacers[i].kart);
Memory::Write32(player + kPlayerCharacterOffset, kRacers[i].character);
Memory::Write32(player + kPlayerTypeOffset, i == 0 ? kPlayerRealLocal : kPlayerCpu);
}
const uint32_t settings = scenario + kSettingsOffset;
Memory::Write32(settings + 0x00, course);
Memory::Write32(settings + 0x04, kCc150);
Memory::Write32(settings + 0x08, kGameModeVs);
Memory::Write32(settings + 0x0C, 0);
Memory::Write32(settings + 0x14, kCpuHard);
Memory::Write32(settings + 0x18, kItemsBalanced);
Memory::Write8(settings + 0x24, 0);
Memory::Write8(settings + 0x25, 3);
Memory::Write32(settings + 0x28, 0);
}
} // namespace
namespace BootRace {
bool ParseArgument(std::string_view arg) {
if (arg == "--race-cpu") {
g_cpuDriven = true;
if (!g_course) {
g_course = kLuigiCircuit;
}
return true;
}
if (!arg.starts_with(kOption)) {
return false;
}
const std::string_view rest = arg.substr(kOption.size());
if (rest.empty()) {
g_course = kLuigiCircuit;
} else if (rest.front() == '=') {
g_course = ParseCourse(rest.substr(1));
} else {
return false;
}
return true;
}
bool Requested() {
return g_course.has_value();
}
} // namespace BootRace
// ArgumentParser::parse, picks the boot section in SectionMgr::Init.
extern "C" void ArgumentParser_Parse_805b9344(CpuContext* ctx) {
static bool consumed = false;
if (!g_course || consumed) {
MKW_GUEST_FUNC(805B9344)(ctx);
return;
}
consumed = true;
SetUpMenuScenario(*g_course);
if (!g_cpuDriven) {
const uint32_t sectionMgr = Memory::Read32(MKW_GADDR(809C1E38));
if (sectionMgr == 0) {
throw std::runtime_error("--race: SectionMgr instance is not set at SectionMgr::Init");
}
ctx->gpr[3] = sectionMgr + kSectionPadOffset;
ctx->gpr[4] = 0;
ctx->gpr[5] = kFirstGcnControllerId;
MKW_GUEST_FUNC(8061B490)(ctx);
}
RT_LOGF("boot-race", "booting into 1P VS on course 0x%02X%s\n", *g_course, g_cpuDriven ? " (CPU driven)" : "");
ctx->gpr[3] = kSectionP1Vs;
}
REGISTER_NATIVE_FUNCTION_AS(0x805B9344, ArgumentParser_Parse_805b9344, "ArgumentParser_Parse_805b9344");
// --race-cpu hands the local racer to the CPU driver while it keeps its camera and HUD.
// RacedataScenario::InitScreens; local racers keep no real controller channel.
extern "C" void RacedataScenario_InitScreens_8052f4e8(CpuContext* ctx) {
if (!g_cpuDriven) {
MKW_GUEST_FUNC(8052F4E8)(ctx);
return;
}
const uint32_t players = ctx->gpr[3] + kPlayersOffset;
std::array<uint8_t, kPlayerCount> channels{};
for (uint32_t i = 0; i < kPlayerCount; ++i) {
channels[i] = Memory::Read8(players + i * kPlayerSize + kPlayerControllerChannelOffset);
}
MKW_GUEST_FUNC(8052F4E8)(ctx);
for (uint32_t i = 0; i < kPlayerCount; ++i) {
const uint32_t player = players + i * kPlayerSize;
if (Memory::Read32(player + kPlayerTypeOffset) == kPlayerRealLocal) {
Memory::Write8(player + kPlayerControllerChannelOffset, channels[i]);
}
}
}
REGISTER_NATIVE_FUNCTION_AS(0x8052F4E8, RacedataScenario_InitScreens_8052f4e8, "RacedataScenario_InitScreens_8052f4e8");
// KartAIController::IsCPU
extern "C" void KartAIController_IsCPU_8072624c(CpuContext* ctx) {
MKW_GUEST_FUNC(8072624C)(ctx);
if (g_cpuDriven) {
ctx->gpr[3] = 1;
}
}
REGISTER_NATIVE_FUNCTION_AS(0x8072624C, KartAIController_IsCPU_8072624c, "KartAIController_IsCPU_8072624c");
// Kart::Link::IsCPU; bit 0 marks a CPU racer, bit 1 the local one.
extern "C" void KartLink_IsCPU_80590664(CpuContext* ctx) {
MKW_GUEST_FUNC(80590664)(ctx);
if (g_cpuDriven) {
ctx->gpr[3] = ctx->gpr[0] & 3;
}
}
REGISTER_NATIVE_FUNCTION_AS(0x80590664, KartLink_IsCPU_80590664, "KartLink_IsCPU_80590664");
// RacedataScenario::InitRNG; fixed seeds keep --race-cpu runs repeatable for benchmarking.
extern "C" void RacedataScenario_InitRng_8052f924(CpuContext* ctx) {
const uint32_t scenario = ctx->gpr[3];
MKW_GUEST_FUNC(8052F924)(ctx);
if (g_cpuDriven) {
Memory::Write32(scenario + kSettingsOffset + 0x2C, 0x74A1B095u);
Memory::Write32(scenario + kSettingsOffset + 0x30, 0x1234567u);
}
}
REGISTER_NATIVE_FUNCTION_AS(0x8052F924, RacedataScenario_InitRng_8052f924, "RacedataScenario_InitRng_8052f924");
// SectionPad::GetFirstDeactivatedPad; a CPU driven racer needs no controller.
extern "C" void SectionPad_GetFirstDeactivatedPad_8061b9bc(CpuContext* ctx) {
MKW_GUEST_FUNC(8061B9BC)(ctx);
if (g_cpuDriven) {
ctx->gpr[3] = static_cast<uint32_t>(-1);
}
}
REGISTER_NATIVE_FUNCTION_AS(0x8061B9BC, SectionPad_GetFirstDeactivatedPad_8061b9bc,
"SectionPad_GetFirstDeactivatedPad_8061b9bc");
+7 -12
View File
@@ -807,21 +807,17 @@ struct DlMayContainDrawVisitor {
bool OnNop(uint8_t) { return true; }
bool OnBpReg(const uint8_t*, uint32_t) { return true; }
// An array base/stride write means the list is setting up indexed geometry.
bool OnCpReg(const uint8_t*, uint8_t reg, uint32_t) { return !(reg >= 0xA0 && reg <= 0xBF); }
// CP writes must reach the HLE vertex state, and indexed XF loads need their
// array resolved, so neither may be passed through raw.
bool OnCpReg(const uint8_t*, uint8_t, uint32_t) { return false; }
bool OnXfReg(const uint8_t*, uint32_t) { return true; }
bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return true; }
bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return false; }
bool OnInvalidateVertexCache(uint8_t) { return true; }
// Draw opcodes reach this too (kHandlesDraw is false): a draw opcode, or a
// command byte this walker does not model, both mean "assume it draws".
bool OnUnknownCommand(uint8_t) { return false; }
bool OnCallDisplayList(uint32_t addr, uint32_t size, int depth) {
if (addr == 0 || size == 0) return true;
const uint8_t* nested = static_cast<const uint8_t*>(GuestToHostPtr(addr, size));
if (!nested) return false;
DlMayContainDrawVisitor child;
return WalkDisplayList(nested, size, child, depth + 1);
}
// Aurora skips nested calls, so the child has to be flattened.
bool OnCallDisplayList(uint32_t addr, uint32_t size, int) { return addr == 0 || size == 0; }
};
static bool DisplayListMayContainDraw(const uint8_t* data, uint32_t nbytes) {
@@ -1142,8 +1138,7 @@ static void ApplyAuroraVtxDesc() {
// GXGetVtxDesc only decodes GX_VA_PNMTXIDX..GX_VA_TEX7; the XF array
// pseudo-attributes above GX_VA_TEX7 have no CP descriptor bits, so
// probing them would always report GX_NONE and republish a no-op.
if (!needsPublish && !IsMatrixIndexAttr(gxAttr) && gxAttr <= GX_VA_TEX7 &&
(type == GX_INDEX8 || type == GX_INDEX16)) {
if (!needsPublish && gxAttr <= GX_VA_TEX7) {
needsPublish = !AuroraVtxDescMatchesLive(gxAttr, type);
}
if (needsPublish) {
+9 -2
View File
@@ -490,9 +490,16 @@ void HleFifoWrite(u32 val, uint32_t sizeBytes) {
const uint16_t vtxCount = ReadBE16(data + 1);
const GXVtxFmt vtxFmt = static_cast<GXVtxFmt>(cmd & GX_VAT_MASK_CMD);
const GXPrimitive prim = OpcodeToGXPrimitive(cmd);
if (vtxCount == 0) {
if (!consumeBytes(3, sink)) break;
continue;
}
uint32_t rawVertexSize = 0;
if (TryGetRawDirectFifoVertexSize(vtxFmt, rawVertexSize)) {
const uint32_t packetBytes = 3u + static_cast<uint32_t>(vtxCount) * rawVertexSize;
const uint32_t packetBytes = TryGetRawDirectFifoVertexSize(vtxFmt, rawVertexSize)
? 3u + static_cast<uint32_t>(vtxCount) * rawVertexSize
: 0u;
// A packet that cannot fit the buffer is streamed per vertex instead.
if (packetBytes != 0 && packetBytes <= g_hleGxState.fifoBytes.size()) {
if (g_hleGxState.fifoByteCount < packetBytes) {
break;
}
+5 -2
View File
@@ -703,8 +703,11 @@ extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) {
}
PPC_NATIVE_OVERRIDE_VOID(80170f2c, GX__LoadTexObj_80170f2c, (uint32_t oa, uint32_t tid), (oa, tid));
extern "C" void GX__LoadTexObjPreLoaded_80170dc8(uint32_t oa, uint32_t tid) { GX__LoadTexObj_80170f2c(oa, tid); }
PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t oa, uint32_t tid), (oa, tid));
extern "C" void GX__LoadTexObjPreLoaded_80170dc8(uint32_t oa, uint32_t region, uint32_t tid) {
(void)region;
GX__LoadTexObj_80170f2c(oa, tid);
}
PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t oa, uint32_t region, uint32_t tid), (oa, region, tid));
// ============================================================================
// Texture Object Getters
+2 -2
View File
@@ -150,7 +150,7 @@ extern "C" void GX__SetVtxAttrFmtv_8016de08(uint32_t vf, uint32_t la) {
while(true){
uint32_t a=Memory::Read32(p);
if(a==0xFFu) break;
list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu});
list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read8(p+12)});
p+=16;
}
GxWorker::Run([vf, list = std::move(list)] {
@@ -326,7 +326,7 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) {
static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv) {
ApplyAuroraVtxStateForBegin(static_cast<GXVtxFmt>(vf));
g_hleGxState.currentVtxFmt=(GXVtxFmt)vf; g_hleGxState.currentPrim=(GXPrimitive)t;
g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=true; g_hleGxState.auroraBeginCalled=false;
g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=nv!=0; g_hleGxState.auroraBeginCalled=false;
g_hleGxState.fifoReadOffset = 0;
g_hleGxState.fifoByteCount=0; g_hleGxState.ResetVertex();
}
+43 -7
View File
@@ -1,5 +1,6 @@
#include "gx_worker.h"
#include "perf_stats.h"
#include "runtime_config.h"
#include "runtime_log.h"
@@ -84,12 +85,37 @@ void WaitTailBriefly(std::unique_lock<std::mutex>& lock) {
#endif
}
// Producer-only: the last write position handed to the worker. Publishing every record kept the
// head cache line bouncing between the game thread and the polling worker.
uint64_t g_published = 0;
constexpr uint64_t kPublishBytes = 2048;
void Publish() {
if (g_published == g_write) {
return;
}
g_published = g_write;
g_head.store(g_write, std::memory_order_seq_cst);
if (g_consumerWaiting.load(std::memory_order_seq_cst)) {
std::lock_guard lock{g_headMutex};
g_headCv.notify_one();
}
}
void WaitForTail(uint64_t minTail, bool service) {
for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) {
CpuRelax();
if (g_tail.load(std::memory_order_acquire) >= minTail) {
return;
}
Publish();
{
PerfStats::WaitScope wait;
for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) {
CpuRelax();
}
}
while (g_tail.load(std::memory_order_acquire) < minTail) {
{
PerfStats::WaitScope wait;
std::unique_lock lock{g_tailMutex};
g_producerWaiting.store(true, std::memory_order_seq_cst);
if (g_tail.load(std::memory_order_seq_cst) < minTail) {
@@ -103,9 +129,19 @@ void WaitForTail(uint64_t minTail, bool service) {
}
}
// Producer-only copy of a tail already observed; avoids reading the worker-written line per record.
uint64_t g_tailSeen = 0;
void WaitForSpace(size_t bytes) {
if (g_write + bytes > kCapacity) {
WaitForTail(g_write + bytes - kCapacity, false);
const uint64_t minTail = g_write + bytes - kCapacity;
if (g_tailSeen >= minTail) {
return;
}
g_tailSeen = g_tail.load(std::memory_order_acquire);
if (g_tailSeen < minTail) {
WaitForTail(minTail, false);
}
}
}
@@ -234,11 +270,10 @@ void* Reserve(size_t payloadSize, void (*run)(void*)) {
return g_ring + offset + kHeaderSize;
}
// Anything that waits on the worker publishes first (WaitForTail), so batching here only delays it.
void Commit() {
g_head.store(g_write, std::memory_order_seq_cst);
if (g_consumerWaiting.load(std::memory_order_seq_cst)) {
std::lock_guard lock{g_headMutex};
g_headCv.notify_one();
if (g_write - g_published >= kPublishBytes) {
Publish();
}
}
@@ -333,6 +368,7 @@ void Stop() {
detail::g_active.store(false, std::memory_order_release);
g_stop.store(true, std::memory_order_release);
Submit([] {});
Publish();
if (g_thread != nullptr) {
g_thread->join();
delete g_thread;
+23 -8
View File
@@ -1031,31 +1031,46 @@ extern "C" int32_t DVDLowInit_80164848() {
}
PPC_NATIVE_OVERRIDE(80164848, DVDLowInit_80164848, int32_t, (), ());
extern "C" int32_t DVDLowInquiry_80165A30(uint32_t cmdBlockPtr, uint32_t callback)
// DVDLowInquiry(DVDDriveInfo* info, DVDLowCallback cb); reports a retail drive.
extern "C" int32_t DVDLowInquiry_80165A30(uint32_t driveInfoPtr, uint32_t callback)
{
// HLE: acknowledge the drive is present. Must mark the command block complete
// (State = 0) or callers polling this address hang waiting for "Busy" to clear.
if (cmdBlockPtr) {
Memory::Write32(cmdBlockPtr + DVD_CB_OFFSET_STATE, DVD_STATE_END);
if (driveInfoPtr && Memory::Contains(driveInfoPtr, 0x20)) {
Memory::Write32(driveInfoPtr + 0x0, 0x00000002); // revision level, device code
Memory::Write32(driveInfoPtr + 0x4, 0x20060526); // release date
Memory::Write32(driveInfoPtr + 0x8, 0x41000000);
}
InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete);
CompleteDvdCancelState();
return 1; // Return 1 to indicate the command was successfully issued.
return 1;
}
PPC_NATIVE_OVERRIDE(80165A30, DVDLowInquiry_80165A30, int32_t, (uint32_t b, uint32_t c), (b, c));
// 0x80164AAC -> DVDLowReadDiskID
extern "C" int32_t DVDLowReadDiskID_80164AAC(uint32_t diskIdPtr, uint32_t callback) {
if (diskIdPtr) {
if (diskIdPtr && Memory::Contains(diskIdPtr, 0x20)) {
for (uint32_t i = 0x07; i < 0x20; ++i) {
Memory::Write8(diskIdPtr + i, 0);
}
Memory::Write32(diskIdPtr + 0x00, CurrentDiscGameCode());
Memory::Write16(diskIdPtr + 0x04, 0x3031);
Memory::Write8 (diskIdPtr + 0x06, 0x01);
Memory::Write32(diskIdPtr + 0x18, 0x5D1C9EA3); // Wii disc magic
}
InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete);
CompleteDvdCancelState();
return 1; // Success
}
PPC_NATIVE_OVERRIDE(80164AAC, DVDLowReadDiskID_80164AAC, int32_t, (uint32_t p, uint32_t c), (p, c));
// Host files need no head positioning.
extern "C" int32_t DVDLowSeek_801664E0(uint32_t offset, uint32_t callback) {
(void)offset;
InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete);
CompleteDvdCancelState();
return 1;
}
PPC_NATIVE_OVERRIDE(801664E0, DVDLowSeek_801664E0, int32_t, (uint32_t o, uint32_t c), (o, c));
// 0x80166330 -> DVDLowRead (And 0x80165708 UnencryptedRead)
// The game calls this to read the Disk Header (offset 0) or raw data.
extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_t offset, uint32_t callback)
+1 -1
View File
@@ -964,7 +964,7 @@ static int32_t HandleIsfsReadDir(uint32_t numIn, uint32_t numOut, uint32_t vecto
}
const std::filesystem::path hostPath = TranslateNandPath(wiiPath.c_str());
if (!IsDirectory(hostPath)) {
return ISFS_ENOENT;
return PathExists(hostPath) ? ISFS_EINVAL : ISFS_ENOENT;
}
// NAND names are at most 12 characters; longer host names cannot exist on
+3
View File
@@ -8,6 +8,7 @@
#include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h"
#include "perf_stats.h"
#include <dolphin/vi.h>
@@ -161,6 +162,7 @@ constexpr std::chrono::microseconds kFinalSpinWindow{500};
void SleepPreciselyUntil(Clock::time_point deadline, bool finishWithSpin = false,
std::chrono::microseconds spinWindow = 750us) {
PerfStats::WaitScope wait;
const auto now = Clock::now();
if (now >= deadline) {
return;
@@ -584,6 +586,7 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
s_lastPacedRetraceCount = g_vi.retraceCount;
}
settings_overlay::AdvancePresentedFrame();
PerfStats::OnFramePresented();
g_auroraFrameActive.store(false, std::memory_order_release);
g_auroraFrameHadWork.store(false, std::memory_order_release);
if (presentedXfb) {
+28 -6
View File
@@ -57,6 +57,9 @@
#endif
#include "abi_bridge.h"
#include "audio_backend.h"
#include "boot_race.h"
#include "guest_abi/guest_region.h"
#include "guest_flat_memory.h"
#include "gx_guest_write.h"
#include "memory.h"
@@ -165,6 +168,7 @@ std::atomic_bool g_auroraInitialized{false};
std::atomic_flag g_abortSignalHandled = ATOMIC_FLAG_INIT;
std::atomic_bool g_fatalErrorReported{false};
std::atomic_bool g_fatalPopupShown{false};
bool g_headless = false;
std::atomic<int> g_lastExitCode{0};
std::atomic_bool g_exitCodeSet{false};
@@ -696,6 +700,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
out << "[runtime] fatal log" << std::endl;
out << "[runtime] reason: " << reason << std::endl;
out << "[runtime] timestamp(seconds): " << nowSecs << std::endl;
out << "[runtime] game: " << GuestRegion::CurrentFacts().gameId << std::endl;
out << "[runtime] entry: " << (g_lastEntryLabel.empty() ? "<unknown>" : g_lastEntryLabel) << std::endl;
if (!extraDetails.empty()) {
out << "[runtime] details: " << extraDetails << std::endl;
@@ -770,7 +775,7 @@ void MarkFatalErrorReported() {
}
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept {
if (g_fatalPopupShown.exchange(true, std::memory_order_acq_rel)) {
if (g_headless || g_fatalPopupShown.exchange(true, std::memory_order_acq_rel)) {
return;
}
@@ -1342,12 +1347,25 @@ int RuntimeMain(int argc, char** argv) {
std::string currentEntryLabel;
// `--headless`: with a boot race, run without a window, sound or dialogs.
g_headless = std::any_of(argv + 1, argv + argc,
[](const char* arg) { return std::string_view(arg) == "--headless"; });
try {
if (argc != 1) {
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
for (int i = 1; i < argc; ++i) {
if (std::string_view(argv[i]) != "--headless" && !BootRace::ParseArgument(argv[i])) {
throw std::invalid_argument(std::string("Unknown command-line option '") + argv[i] +
"'; the game runtime only accepts --race[=<course id>], --race-cpu and --headless, use Config.toml through the installed host.");
}
}
if (g_headless && !BootRace::Requested()) {
throw std::invalid_argument("--headless needs --race[=<course id>] or --race-cpu");
}
if (g_headless) {
AudioBackend::Instance().SetSilenced(true);
}
RuntimeConfigFile::LogLoadedConfig();
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
if (!g_headless && RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
}
SystemBridge::Initialize();
@@ -1381,7 +1399,8 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
auroraConfig.hasWindowPosition = RuntimeConfigFile::WindowPosition(
auroraConfig.windowPosX, auroraConfig.windowPosY);
auroraConfig.allowJoystickBackgroundEvents = true;
auroraConfig.allowJoystickBackgroundEvents = !g_headless;
auroraConfig.startHidden = g_headless;
auroraConfig.disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
// Dolphin-style custom textures. Aurora indexes <userPath>/texture_replacements
// once during aurora_initialize, so both knobs only take effect on the next launch.
@@ -1390,7 +1409,7 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.allowTextureReplacements = RuntimeConfigFile::TextureReplacements(false);
auroraConfig.allowTextureDumps = auroraConfig.allowTextureReplacements &&
RuntimeConfigFile::TextureDumps(false);
// No vsync knob: aurora always configures a non-blocking present mode.
auroraConfig.vsync = RuntimeConfigFile::Vsync();
auroraConfig.desiredBackend = BACKEND_AUTO;
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
ConfigureMkwDynamicAspect(configWidescreen, forceAspect169,
@@ -1460,6 +1479,9 @@ int RuntimeMain(int argc, char** argv) {
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
if (BootRace::Requested()) {
aurora_set_skip_unready_pipelines(false);
}
RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false")
<< " force_16_9=" << (forceAspect169 ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen || forceAspect169 ? 1 : 0)
+67
View File
@@ -0,0 +1,67 @@
#include "perf_stats.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
namespace PerfStats {
namespace {
using Clock = std::chrono::steady_clock;
constexpr size_t kWindow = 240;
thread_local Clock::duration t_wait{};
struct FrameWindow {
Clock::time_point last{};
std::array<double, kWindow> busyMs{};
size_t count = 0;
};
FrameWindow g_window;
} // namespace
bool Enabled() noexcept {
static const bool enabled = [] {
const char* value = std::getenv("MKW_PERF_STATS");
return value != nullptr && std::strcmp(value, "0") != 0;
}();
return enabled;
}
void AddWait(Clock::duration wait) noexcept { t_wait += wait; }
void OnFramePresented() noexcept {
if (!Enabled()) {
return;
}
const auto now = Clock::now();
const Clock::duration wait = t_wait;
t_wait = {};
if (g_window.last == Clock::time_point{}) {
g_window.last = now;
return;
}
const auto busy = (now - g_window.last) - wait;
g_window.last = now;
g_window.busyMs[g_window.count++] = std::chrono::duration<double, std::milli>(busy).count();
if (g_window.count < kWindow) {
return;
}
std::array<double, kWindow> sorted = g_window.busyMs;
std::sort(sorted.begin(), sorted.end());
double sum = 0;
for (const double ms : sorted) {
sum += ms;
}
RT_LOGF("perf", "busy ms/frame over %zu frames: avg %.3f median %.3f p95 %.3f max %.3f\n", kWindow,
sum / kWindow, sorted[kWindow / 2], sorted[kWindow * 95 / 100], sorted[kWindow - 1]);
g_window.count = 0;
}
} // namespace PerfStats
+55 -7
View File
@@ -75,6 +75,7 @@ const char* GraphicsApiDisplayName() {
bool g_topBarVisible = false;
bool g_exitPromptOpen = false;
bool g_restartPromptOpen = false;
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
int g_controllerPort = 0;
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
@@ -109,6 +110,7 @@ int g_displayMode = [] {
return static_cast<int>(AURORA_DISPLAY_MODE_WINDOWED);
}();
bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_vsync = RuntimeConfigFile::Vsync();
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
@@ -298,6 +300,8 @@ void ApplyConfiguredMappings() {
}
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled();
const bool g_wiiRemotesAtLaunch = g_wiiRemotesEnabled;
bool g_wiiRemotesSaveFailed = false;
bool g_wiiAutoPair = RuntimeConfigFile::WiiAutoPairEnabled(true);
bool g_wiiSensorBarAbove = RuntimeConfigFile::WiiSensorBarAbove(false);
int g_wiiIrSensitivity = RuntimeConfigFile::WiiIrSensitivity(3);
@@ -378,10 +382,18 @@ void DrawWiiRemoteSetup() {
ImGui::SetTooltip("The sensor bar sensitivity from the Wii settings.\nTakes effect the next time the game starts.");
}
if (ImGui::Checkbox("Use Wii Remotes", &g_wiiRemotesEnabled)) {
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
g_wiiRemotesSaveFailed = !RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
if (g_wiiRemotesSaveFailed) g_wiiRemotesEnabled = !g_wiiRemotesEnabled;
g_restartPromptOpen = g_wiiRemotesEnabled != g_wiiRemotesAtLaunch && !g_wiiRemotesSaveFailed;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Takes effect on the next launch. Turn this off to use a Wii U Pro Controller as a gamepad.");
ImGui::SetTooltip("Takes effect after restarting the game. Turn this off to use a Wii U Pro Controller as a gamepad.");
}
if (g_wiiRemotesSaveFailed) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Could not save this setting to Config.toml.");
}
if (g_wiiRemotesEnabled != g_wiiRemotesAtLaunch) {
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Restart the game to apply this change.");
}
ImGui::Unindent();
}
@@ -1189,6 +1201,10 @@ void DrawGraphicsSettings() {
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
RuntimeConfigFile::SetSkipUnreadyPipelines(g_skipUnreadyPipelines);
}
if (ImGui::Checkbox("VSync", &g_vsync)) {
aurora_enable_vsync(g_vsync);
RuntimeConfigFile::SetVsync(g_vsync);
}
if (ImGui::Checkbox("Show FPS", &g_showFps)) {
RuntimeConfigFile::SetShowFps(g_showFps);
}
@@ -1199,7 +1215,12 @@ void DrawGraphicsSettings() {
void DrawFpsOverlay() {
AuroraPresentTiming presentTiming{};
aurora_get_present_timing(&presentTiming);
if (!g_showFps) {
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
const double refreshRate = aurora_get_display_refresh_rate();
const bool interpolationInactive = targetFps > 60 && !aurora_display_shows_interpolation();
const bool unevenPacing =
!g_vsync && !interpolationInactive && refreshRate > 0.0 && targetFps > refreshRate + 1.0;
if (!g_showFps && !unevenPacing && !interpolationInactive) {
return;
}
@@ -1216,9 +1237,9 @@ void DrawFpsOverlay() {
ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings;
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
if (presentTiming.sampleCount == 0) {
if (g_showFps && presentTiming.sampleCount == 0) {
ImGui::TextUnformatted("FPS: --");
} else {
} else if (g_showFps) {
// Present timing includes the additional frames produced by
// interpolation, so this remains the actual displayed FPS.
ImGui::Text("FPS: %.1f", presentTiming.framesPerSecond);
@@ -1231,6 +1252,17 @@ void DrawFpsOverlay() {
presentTiming.effectiveFramesPerSecond);
}
}
if (interpolationInactive) {
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
"%u FPS interpolation has no effect on a %.0f Hz display.", targetFps,
refreshRate);
}
if (unevenPacing) {
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
"%u FPS interpolation on a %.0f Hz display without VSync is not smooth.\n"
"Enable VSync for smooth motion.",
targetFps, refreshRate);
}
}
ImGui::End();
}
@@ -1336,6 +1368,19 @@ void DrawExitPrompt() {
ImGui::EndPopup();
}
void DrawRestartPrompt() {
constexpr const char* kTitle = "Restart required";
if (g_restartPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
if (!ImGui::BeginPopupModal(kTitle, &g_restartPromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
ImGui::TextUnformatted(g_wiiRemotesEnabled ? "Wii Remotes will be used after you restart the game."
: "Wii Remotes will stop being used after you restart the game.");
if (ImGui::Button("Exit now", ImVec2(140.0f, 0.0f))) ExitForAuroraWindowClose();
ImGui::SameLine();
if (ImGui::Button("Restart later", ImVec2(140.0f, 0.0f))) g_restartPromptOpen = false;
if (!g_restartPromptOpen) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void DrawTopBar() {
if (!g_topBarVisible) {
return;
@@ -1474,7 +1519,7 @@ void PersistDisplayModeIfChanged() {
void ApplyInputBlockState() {
const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active ||
g_exitPromptOpen || g_topBarVisible || StartupScreenVisible();
g_exitPromptOpen || g_restartPromptOpen || g_topBarVisible || StartupScreenVisible();
PADBlockInput(blocked);
InputBindings::SetInputBlocked(blocked);
}
@@ -1540,7 +1585,9 @@ void HandleEvents(const AuroraEvent* events) noexcept {
RuntimeConfigFile::SetAudioMuted(g_audioMuted);
}
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) {
if (g_exitPromptOpen) {
if (g_restartPromptOpen) {
g_restartPromptOpen = false;
} else if (g_exitPromptOpen) {
g_exitPromptOpen = false;
} else if (g_topBarVisible) {
SetTopBarVisible(false);
@@ -1589,6 +1636,7 @@ void Draw() noexcept {
DrawFpsOverlay();
DrawTopBar();
DrawExitPrompt();
DrawRestartPrompt();
controller_mapping_wizard::Draw();
ApplyInputBlockState();
DrawStartupScreen();
+17 -10
View File
@@ -413,14 +413,18 @@ void SystemBridge::Initialize() {
}
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path) {
auto writeRegion = [&](const char* label, uint32_t base, uint32_t size,
const std::filesystem::path& path) {
std::ofstream dump(path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(base, size)), size);
os << "[runtime] " << label << " snapshot written to " << RuntimeConfigFile::PathToUtf8(path)
<< (dump.good() ? "" : " (write failed)") << std::endl;
};
constexpr uint32_t kMem1Base = 0x80000000u;
constexpr uint32_t kMem1Size = 0x01800000u;
if (Memory::Contains(kMem1Base, kMem1Size)) {
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
kMem1Size);
os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path)
<< (dump.good() ? "" : " (write failed)") << std::endl;
writeRegion("MEM1", kMem1Base, kMem1Size, mem1Path);
}
constexpr uint32_t kMem2Base = 0x90000000u;
for (uint32_t mem2Size : {0x08000000u, 0x04000000u}) {
@@ -429,13 +433,16 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesys
}
std::filesystem::path mem2Path = mem1Path;
mem2Path += ".mem2";
std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
mem2Size);
os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path)
<< (dump.good() ? "" : " (write failed)") << std::endl;
writeRegion("MEM2", kMem2Base, mem2Size, mem2Path);
break;
}
constexpr uint32_t kKamekBase = 0x81800000u;
constexpr uint32_t kKamekSize = 0x00200000u;
if (Memory::Contains(kKamekBase, kKamekSize)) {
std::filesystem::path kamekPath = mem1Path;
kamekPath += ".kamek";
writeRegion("Kamek overlay", kKamekBase, kKamekSize, kamekPath);
}
}
void SystemBridge::DumpCrashHeuristics(std::ostream& os, const CpuContext* cpu,
-182
View File
@@ -1,182 +0,0 @@
#!/usr/bin/env python3
"""Generate runtime/include/guest_abi/guest_address_table.inc and runtime/include/guest_function_imports.inc.
The runtime names every guest address by its PAL (RMCP01) address. This scans the runtime for
those identities and writes one row per identity with its NTSC-U and NTSC-J address:
MKW_GUEST_ADDRESS(<PAL>, <NTSC-U>, <NTSC-J>)
Identities the runtime calls through MKW_GUEST_FUNC also get a MKW_GUEST_FUNCTION_IMPORT row.
Code addresses go through each region's chunk table (projects/<region>/region_port.json) and must
be a function entry in that region's validated MAP.txt, or sit at the same offset inside the same
validated function. Data addresses come from projects/<region>/data_addresses.txt. An identity
that does not resolve aborts the generation.
Usage: python -I tools/region/gen_guest_address_table.py
"""
import bisect
import json
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
RUNTIME = os.path.join(ROOT, "runtime")
OUT = os.path.join(RUNTIME, "include", "guest_abi", "guest_address_table.inc")
IMPORTS_OUT = os.path.join(RUNTIME, "include", "guest_function_imports.inc")
PAL_TEXT = [(0x80004000, 0x80006460), (0x800072C0, 0x80244DE0), (0x805103B4, 0x8088F400)]
# Column order is the GuestRegion::Id order in guest_abi/guest_region.h.
REGIONS = ["mkwii-ntsc-u", "mkwii-ntsc-j"]
HEX = r"([0-9A-Fa-f]{8})(?![0-9A-Fa-f])"
TOKEN_PATTERNS = [re.compile(p) for p in (
r"PPC_NATIVE_OVERRIDE(?:_VOID)?\s*\(\s*" + HEX,
r"GX_FATAL_STUB\s*\(\s*" + HEX,
r"MKW_GADDR\s*\(\s*" + HEX,
r"MKW_GADDR_CT\s*\(\s*" + HEX,
r"MKW_GUEST_FUNC\s*\(\s*" + HEX,
r"MKW_KNOWN_TYPED_NATIVE\s*\(\s*" + HEX,
r"MKW_KNOWN_NATIVE_CPU_CALL\s*\(\s*" + HEX,
r"REGISTER_NATIVE_FUNCTION(?:_AS)?\s*\(\s*0x" + HEX,
r"REGISTER_TRANSLATED_FUNCTION\s*\(\s*0x" + HEX,
r"GuestRegion::(?:Is|Address)<0x" + HEX,
)]
IMPORT_PATTERN = re.compile(r"MKW_GUEST_FUNC\s*\(\s*" + HEX)
COMMENTS = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
LABEL_NAME = re.compile(r"(_switch$|_caseD_|^0x)")
def scan_identities():
found, imports = set(), set()
for directory, _, files in os.walk(RUNTIME):
if directory.startswith(os.path.join(RUNTIME, "third_party")):
continue
for name in files:
path = os.path.join(directory, name)
if not name.endswith((".cpp", ".h", ".hpp", ".inc")) or path in (OUT, IMPORTS_OUT):
continue
with open(path, encoding="utf-8", errors="replace") as f:
text = COMMENTS.sub("", f.read())
for pattern in TOKEN_PATTERNS:
found.update(int(m.group(1), 16) for m in pattern.finditer(text))
imports.update(int(m.group(1), 16) for m in IMPORT_PATTERN.finditer(text))
return sorted(found), sorted(imports)
def is_text(address):
return any(start <= address < end for start, end in PAL_TEXT)
def load_map(path):
entries = []
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
address, _, name = line.partition(" ")
entries.append((int(address, 16), name.strip()))
entries.sort()
return entries
def load_chunks(project):
with open(os.path.join(ROOT, "projects", project, "region_port.json"), encoding="utf-8") as f:
return [tuple(int(v, 16) for v in chunk) for chunk in json.load(f)["chunks"]]
def load_data_table(project):
table = {}
with open(os.path.join(ROOT, "projects", project, "data_addresses.txt"), encoding="utf-8") as f:
for line in f:
parts = line.split("#", 1)[0].split()
if len(parts) >= 2 and parts[1] != "?":
table[int(parts[0], 16)] = int(parts[1], 16)
return table
def containing_function(entries, addresses, address):
i = bisect.bisect_right(addresses, address) - 1
while i >= 0 and LABEL_NAME.search(entries[i][1]):
i -= 1
if i < 0:
return None, None
return entries[i][1], address - entries[i][0]
def port_region(project, identities, pal_entries):
chunks = load_chunks(project)
data = load_data_table(project)
target_entries = load_map(os.path.join(ROOT, "projects", project, "MAP.txt"))
target_starts = {a for a, _ in target_entries}
target_by_name = {}
for address, name in target_entries:
target_by_name.setdefault(name, address)
pal_addresses = [a for a, _ in pal_entries]
mapping, problems = {}, []
for pal in identities:
if not is_text(pal):
if pal in data:
mapping[pal] = data[pal]
else:
problems.append(f"{pal:08X}: data address missing from projects/{project}/data_addresses.txt")
continue
ported = next((pal - s + d for s, e, d in chunks if s <= pal < e), None)
if ported is None:
problems.append(f"{pal:08X}: code address outside every chunk")
continue
if ported not in target_starts:
name, offset = containing_function(pal_entries, pal_addresses, pal)
start = target_by_name.get(name) if name else None
if start is None or start + offset != ported:
problems.append(f"{pal:08X} -> {ported:08X}: not a validated entry of {project}/MAP.txt "
f"(containing function {name} not confirmed at the same offset)")
continue
mapping[pal] = ported
return mapping, problems
def write(path, macro, header, rows, columns, names):
lines = ["// AUTO-GENERATED by tools/region/gen_guest_address_table.py - DO NOT EDIT."] + header
for pal in rows:
row = ", ".join([f"{pal:08X}"] + [f"{column[pal]:08X}" for column in columns])
name = names.get(pal)
lines.append(f"{macro}({row})" + (f" // {name}" if name and not name.startswith("0x") else ""))
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(lines) + "\n")
def main():
identities, imports = scan_identities()
pal_entries = load_map(os.path.join(ROOT, "projects", "mkwii", "MAP.txt"))
names = dict(pal_entries)
columns, failed = [], False
for project in REGIONS:
mapping, problems = port_region(project, identities, pal_entries)
if problems:
failed = True
print(f"{project}: {len(problems)} identities did not resolve:")
for problem in problems:
print(" " + problem)
columns.append(mapping)
if failed:
return 1
write(OUT, "MKW_GUEST_ADDRESS", [
"// PAL identity -> address in each region: MKW_GUEST_ADDRESS(PAL, NTSC-U, NTSC-J).",
"// Sorted by PAL address; guest_abi/guest_region.h relies on that.",
], identities, columns, names)
write(IMPORTS_OUT, "MKW_GUEST_FUNCTION_IMPORT", [
"// Base translations the runtime calls directly: MKW_GUEST_FUNCTION_IMPORT(PAL, NTSC-U, NTSC-J).",
], imports, columns, names)
code = sum(1 for a in identities if is_text(a))
print(f"wrote {len(identities)} identities ({code} code, {len(identities) - code} data), "
f"{len(imports)} function imports")
return 0
if __name__ == "__main__":
sys.exit(main())
+121 -102
View File
@@ -139,6 +139,15 @@ TranslationOptions WithProjectFrontEndPolicy(
LeafInliningBlockedTargets = leafInliningBlockedTargets.Value
};
string? translateModInputCacheDirectory = null;
// translate-mod reads the same (large) base metadata from several places.
var baseMetadataCache = new Dictionary<string, BaseTranslationOutputMetadata>(StringComparer.OrdinalIgnoreCase);
BaseTranslationOutputMetadata ReadBaseMetadataCached(string path)
{
var fullPath = Path.GetFullPath(path);
if (!baseMetadataCache.TryGetValue(fullPath, out var metadata))
baseMetadataCache[fullPath] = metadata = BaseTranslationOutputMetadataFile.Read(fullPath);
return metadata;
}
string? translateModPublishedOutputDirectory = null;
List<AddressRange>? _executableRanges = null;
// Callback discovery now runs on the discovery worker pool, so the range cache
@@ -472,10 +481,6 @@ int RunTranslateRecursive(string[] argsTail)
var pruneStale = HasFlag(argsTail, "--prune-stale");
var threadsOption = OptionValue(argsTail, "--threads");
var maxThreads = threadsOption != null ? Math.Max(1, ParseInt(threadsOption)) : Math.Max(1, Environment.ProcessorCount);
// Discovery batches: cheap front-end-only work items, so the batch is sized
// generously to keep queue overhead off the hot path. Deliberately larger
// than the mod emission wave below, which carries whole C++ emissions.
var batchSize = Math.Max(64, maxThreads * 8);
Console.WriteLine($"[translator] Using up to {maxThreads} worker thread(s).");
// Log start time
@@ -493,7 +498,6 @@ int RunTranslateRecursive(string[] argsTail)
// callees are decoded separately and never appear here). Feeds the
// fall-through interior-entry detection for the base manifest.
var translatedFlowCoverage = new Dictionary<uint, uint[]>();
var gqrUnknownEntryRoots = new HashSet<uint> { startAddr };
var translatedFunctionEnds = new Dictionary<uint, uint>();
// Every map entry is a function start, so the whole boundary set is known
// before the first decode instead of accumulating as discovery proceeds.
@@ -565,13 +569,15 @@ int RunTranslateRecursive(string[] argsTail)
void ProcessQueue(bool speculative = false)
{
// Each round decodes the whole frontier: the output depends only on the final
// boundary set (boundary repair re-decodes stale owners), never on batching.
while (queue.Count > 0)
{
var batch = new List<(uint Address, int Depth, string Name)>(batchSize);
var batch = new List<(uint Address, int Depth, string Name)>(queue.Count);
var batchAddresses = new HashSet<uint>();
var candidates = QueueBatch.Dequeue(
queue,
batchSize,
queue.Count,
next => ShouldSkip(next) || !batchAddresses.Add(next.addr));
foreach (var next in candidates)
{
@@ -598,23 +604,19 @@ int RunTranslateRecursive(string[] argsTail)
}
}
// Batches drain one at a time so a callee of a prior speculative entry arrives with a known
// caller/GQR state instead of as another unknown root. Everything here is speculative, so a
// "call" decoded from an unreached map entry isn't proven code; treat failures as soft, not fatal.
// Everything here is speculative, so a "call" decoded from an unreached map entry
// isn't proven code; treat failures as soft, not fatal.
void ProcessSpeculativeSeeds()
{
while (speculativeSeeds.Count > 0)
{
var batch = new List<(uint Address, int Depth, string Name)>(batchSize);
var batch = new List<(uint Address, int Depth, string Name)>(speculativeSeeds.Count);
var candidates = QueueBatch.Dequeue(
speculativeSeeds,
batchSize,
speculativeSeeds.Count,
address => translated.Contains(address) || !visited.Add(address));
foreach (var address in candidates)
{
// Nothing calls this from the translated set, so its entry
// GQR state is unconstrained.
gqrUnknownEntryRoots.Add(address);
batch.Add((address, 0, ResolveFunctionName(address, preferred: null, $"func_{address:X8}")));
}
@@ -652,22 +654,42 @@ int RunTranslateRecursive(string[] argsTail)
// decoded out of one - is dropped with a counted warning.
void TranslateBatch(List<(uint Address, int Depth, string Name)> batch, bool speculative = false)
{
var results = new FunctionDiscoveryResult?[batch.Count];
IndexedParallel.For(batch.Count, parallelOptions, i => results[i] = Attempt(batch[i], speculative));
var metrics = new TranslationMetrics?[batch.Count];
var entryPoints = new uint[batch.Count];
var discoveryTargets = new uint[batch.Count][];
var swallowedSiblingEntries = new uint[batch.Count][];
var summaries = new GqrFunctionSummary[batch.Count];
var functionEnds = new uint[batch.Count];
var flowCoverage = new uint[batch.Count][];
// Everything kept from a decode is derived in the decode's own pass, so the full
// result is never retained; the few results a newly revealed boundary invalidates
// are simply decoded again. Commits stay serial and in batch order.
void Decode(int i, bool findSwallowedSiblings)
{
var result = Attempt(batch[i], speculative);
metrics[i] = result?.Metrics;
if (result is null)
{
canonicalIrStore.Remove(batch[i].Address);
return;
}
entryPoints[i] = result.EntryPoint;
if (findSwallowedSiblings)
swallowedSiblingEntries[i] = DiscoverSwallowedSiblingEntries(result).ToArray();
discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray();
summaries[i] = GqrFunctionSummary.Create(result.Ssa.Function);
canonicalIrStore.Put(batch[i].Address, result.Ssa.Function);
functionEnds[i] = GetFunctionInstructionEnd(result);
flowCoverage[i] = CoalesceInstructionRanges(result);
}
IndexedParallel.For(batch.Count, parallelOptions, i => Decode(i, findSwallowedSiblings: true));
// Re-run only translations that swallowed a newly revealed sibling entry, or direct
// branches could bypass a mod overlay registered there. Both scans are pure per-result
// so they run on the same worker pool; the dedup filter below keeps first sighting.
var discoveryTargets = new uint[results.Length][];
var swallowedSiblingEntries = new uint[results.Length][];
IndexedParallel.For(results.Length, parallelOptions, i =>
{
if (results[i] is not { } result) return;
discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray();
swallowedSiblingEntries[i] = DiscoverSwallowedSiblingEntries(result).ToArray();
});
var newlyKnownEntries = Enumerable.Range(0, results.Length)
.Where(i => results[i] is not null)
// branches could bypass a mod overlay registered there. The dedup filter keeps first sighting.
var newlyKnownEntries = Enumerable.Range(0, batch.Count)
.Where(i => metrics[i] is not null)
.SelectMany(i => discoveryTargets[i].Concat(swallowedSiblingEntries[i]))
.Where(knownBaseFunctionEntryPoints.Add)
.ToHashSet();
@@ -675,49 +697,39 @@ int RunTranslateRecursive(string[] argsTail)
{
// The boundary set is final for this batch once the filter above has run,
// so the affected re-translations are independent of one another.
var affected = new List<int>();
for (var i = 0; i < results.Length; i++)
{
if (results[i] is not { } result) continue;
var entry = result.EntryPoint;
if (result.Instructions.Any(instruction =>
instruction.Address != entry && newlyKnownEntries.Contains(instruction.Address)))
{
affected.Add(i);
}
}
IndexedParallel.For(affected.Count, parallelOptions, k =>
{
var i = affected[k];
results[i] = Attempt(batch[i], speculative);
if (results[i] is { } result)
{
discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray();
}
});
var sortedNewEntries = newlyKnownEntries.Order().ToArray();
var affected = Enumerable.Range(0, batch.Count)
.Where(i => metrics[i] is not null &&
CoversInteriorInstruction(flowCoverage[i], entryPoints[i], sortedNewEntries))
.ToArray();
IndexedParallel.For(affected.Length, parallelOptions, k => Decode(affected[k], findSwallowedSiblings: false));
}
// The compact GQR projection is a pure walk of the final SSA graph and the
// canonical store serializes its own dictionary, so both move off the serial
// commit. Every address in a batch is distinct and untranslated, so the store
// sees one Put per key regardless of completion order.
var summaries = new GqrFunctionSummary[results.Length];
IndexedParallel.For(results.Length, parallelOptions, i =>
{
if (results[i] is not { } result) return;
summaries[i] = GqrFunctionSummary.Create(result.Ssa.Function);
canonicalIrStore.Put(batch[i].Address, result.Ssa.Function);
});
// Update state sequentially (thread-safe)
for (var i = 0; i < batch.Count; i++)
{
if (results[i] is not { } result) continue;
CommitResult(batch[i], result, discoveryTargets[i], summaries[i]);
if (metrics[i] is not { } functionMetrics) continue;
CommitResult(batch[i], functionMetrics, discoveryTargets[i], summaries[i], functionEnds[i], flowCoverage[i]);
}
}
// True when an instruction of the coalesced [start, end) coverage, other than the
// entry, starts at one of the sorted addresses.
static bool CoversInteriorInstruction(uint[] coverage, uint entry, uint[] sortedAddresses)
{
for (var r = 0; r < coverage.Length; r += 2)
{
var index = Array.BinarySearch(sortedAddresses, coverage[r]);
for (index = index >= 0 ? index : ~index;
index < sortedAddresses.Length && sortedAddresses[index] < coverage[r + 1];
index++)
{
var address = sortedAddresses[index];
if (address != entry && (address & 3u) == 0) return true;
}
}
return false;
}
FunctionDiscoveryResult? Attempt((uint Address, int Depth, string Name) work, bool speculative)
{
if (!speculative)
@@ -773,16 +785,18 @@ int RunTranslateRecursive(string[] argsTail)
void CommitResult(
(uint Address, int Depth, string Name) work,
FunctionDiscoveryResult result,
TranslationMetrics functionMetrics,
IReadOnlyList<uint> discoveryTargets,
GqrFunctionSummary summary)
GqrFunctionSummary summary,
uint functionEnd,
uint[] flowCoverage)
{
// File writing moved to parallel section above
totals.Add(result.Metrics);
totals.Add(functionMetrics);
count++;
translated.Add(work.Address);
translatedFunctionEnds[work.Address] = GetFunctionInstructionEnd(result);
translatedFlowCoverage[work.Address] = CoalesceInstructionRanges(result);
translatedFunctionEnds[work.Address] = functionEnd;
translatedFlowCoverage[work.Address] = flowCoverage;
foreach (var target in discoveryTargets)
{
@@ -821,6 +835,7 @@ int RunTranslateRecursive(string[] argsTail)
// list that existed to paper over the addresses they still missed.
ProcessQueue();
var walkReachable = count;
var walkTranslated = translated.ToHashSet();
ProcessSpeculativeSeeds();
if (functionMap is not null)
{
@@ -864,26 +879,31 @@ int RunTranslateRecursive(string[] argsTail)
if (boundaryRepairAddresses.Length != 0)
{
Console.WriteLine($"[translator] Reanalyzing {boundaryRepairAddresses.Length:N0} function(s) affected by later-discovered boundaries...");
var repaired = new FunctionDiscoveryResult[boundaryRepairAddresses.Length];
var repairedSummaries = new GqrFunctionSummary[boundaryRepairAddresses.Length];
var repairedEnds = new uint[boundaryRepairAddresses.Length];
var repairedCoverage = new uint[boundaryRepairAddresses.Length][];
IndexedParallel.For(boundaryRepairAddresses.Length, parallelOptions, i =>
{
var address = boundaryRepairAddresses[i];
var work = baseWork[address];
repaired[i] = translator.Value.Discover(address, WithProjectFrontEndPolicy(new TranslationOptions(
var repaired = translator.Value.Discover(address, WithProjectFrontEndPolicy(new TranslationOptions(
work.Name,
MaxInstructions: work.MaxInstructions,
MaxBytes: work.MaxBytes,
AllowUnsupportedInstructions: work.AllowUnsupported,
KnownFunctionEntryPoints: knownBaseFunctionEntryPoints),
RequireProject().Translation));
repairedSummaries[i] = GqrFunctionSummary.Create(repaired.Ssa.Function);
canonicalIrStore.Put(address, repaired.Ssa.Function);
repairedEnds[i] = GetFunctionInstructionEnd(repaired);
repairedCoverage[i] = CoalesceInstructionRanges(repaired);
});
for (var i = 0; i < boundaryRepairAddresses.Length; i++)
{
var address = boundaryRepairAddresses[i];
baseSummaries[address] = GqrFunctionSummary.Create(repaired[i].Ssa.Function);
canonicalIrStore.Put(address, repaired[i].Ssa.Function);
translatedFunctionEnds[address] = GetFunctionInstructionEnd(repaired[i]);
translatedFlowCoverage[address] = CoalesceInstructionRanges(repaired[i]);
baseSummaries[address] = repairedSummaries[i];
translatedFunctionEnds[address] = repairedEnds[i];
translatedFlowCoverage[address] = repairedCoverage[i];
}
}
@@ -906,6 +926,12 @@ int RunTranslateRecursive(string[] argsTail)
}
Console.WriteLine("[translator] Computing interprocedural GQR constants and callee write summaries...");
// Map entries the walk never reached have an unconstrained entry GQR state unless some
// translated function calls them directly.
var gqrUnknownEntryRoots = GqrInterproceduralAnalysis.SelectUnknownEntryRoots(
baseSummaries,
[startAddr],
(functionMap?.Addresses ?? Array.Empty<uint>()).Where(address => !walkTranslated.Contains(address)));
var gqrAnalysis = GqrInterproceduralAnalysis.Analyze(
baseSummaries,
gqrUnknownEntryRoots);
@@ -1183,7 +1209,7 @@ int RunTranslateRecursive(string[] argsTail)
// Populate the recursive ABI inference cache once, in stable global order, before
// refinement and parallel emission, so cyclic call graphs don't depend on thread count.
LogPhase("state-free selection + leaf residency");
ReportAbiPrewarm(inferredGuestAbi.Value.Prewarm(emissionAddresses));
ReportAbiPrewarm(inferredGuestAbi.Value.Prewarm(emissionAddresses, maxThreads));
LogPhase("ABI prewarm");
// State-free entries use the transformed leaf's exact live-at-entry
@@ -1404,6 +1430,7 @@ int RunTranslateRecursive(string[] argsTail)
// source visible. `completionPulse` only wakes the writer; it carries
// no state of its own.
var emissionDone = new bool[pendingSources.Length];
var unsupportedByIndex = new int[emissionAddresses.Length];
using var completionPulse = new ManualResetEventSlim(false);
var emissionAborted = false;
Task? pendingBundleWrite = null;
@@ -1459,6 +1486,8 @@ int RunTranslateRecursive(string[] argsTail)
var options = BuildEmissionOptions(address, stateFreeAbiContracts);
if (!canonicalIrStore.TryGet(address, out var canonicalSsa))
throw new InvalidOperationException($"Missing canonical SSA for final lowering at 0x{address:X8}.");
unsupportedByIndex[i] = canonicalSsa.Blocks.Sum(block =>
block.Instructions.Count(static instruction => instruction is IrUndefined));
var emitted = translator.Value.LowerCanonical(address, canonicalSsa, options);
var path = work.Path;
if (outputMetadata is not null)
@@ -1517,16 +1546,7 @@ int RunTranslateRecursive(string[] argsTail)
: Path.GetRelativePath(
Path.GetDirectoryName(Path.GetFullPath(outputMetadataPath!))!,
Path.GetFullPath(productionSourceBundlePath)).Replace('\\', '/');
var metadataUnsupportedInstructionCount = emissionAddresses.Sum(address =>
{
if (!canonicalIrStore.TryGet(address, out var canonicalSsa))
{
throw new InvalidOperationException(
$"Missing canonical SSA while computing translation quality for 0x{address:X8}.");
}
return canonicalSsa.Blocks.Sum(block =>
block.Instructions.Count(static instruction => instruction is IrUndefined));
});
var metadataUnsupportedInstructionCount = unsupportedByIndex.Sum();
completedOutputMetadata = BaseTranslationOutputMetadata.Create(
outputMetadata,
new TranslationQualityMetadata(
@@ -1701,7 +1721,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
// win (this shipped once as a character-select LinkList.h:573 panic).
if (File.Exists(baseTranslationOutputMetadataPath) && File.Exists(codePul))
{
var guardedBaseMetadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath);
var guardedBaseMetadata = ReadBaseMetadataCached(baseTranslationOutputMetadataPath);
var codePulSha256 = ChecksumUtilities.Sha256HexOfFile(codePul);
var baseKnowsThisPul = guardedBaseMetadata.ModPatchAwareness is { } awareness &&
awareness.Any(entry => string.Equals(entry.CodePulSha256, codePulSha256, StringComparison.OrdinalIgnoreCase));
@@ -1780,6 +1800,8 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
var pul = KamekPulFile.Load(codePul);
var selected = pul.SelectRegion(region);
// The server validates the hash of the unmodified chunk.
var sourceKamekCodeSha1 = SHA1.HashData(selected.CodeBlob);
if (string.Equals(profile?.Name, "retro-rewind", StringComparison.OrdinalIgnoreCase))
{
var corrected = pul.IsCombined && !string.Equals(region, "P", StringComparison.OrdinalIgnoreCase)
@@ -1988,7 +2010,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
baseManifest,
dvdOverlayRoots,
relocatedModuleImage,
SHA1.HashData(selected.CodeBlob),
sourceKamekCodeSha1,
retroWfcLoweringPlan?.StaticPointers,
selected.CodeSize,
selected.BssSize,
@@ -2077,7 +2099,7 @@ void WriteResolvedDispatchProfile(
var metadataPath = baseTranslationOutputMetadataPath;
if (File.Exists(metadataPath))
{
var metadata = BaseTranslationOutputMetadataFile.Read(metadataPath);
var metadata = ReadBaseMetadataCached(metadataPath);
if (!string.IsNullOrWhiteSpace(metadata.SourceBundlePath))
{
var bundlePath = Path.IsPathRooted(metadata.SourceBundlePath)
@@ -2152,7 +2174,7 @@ void WriteResolvedDispatchProfile(
BaseTranslationOutputMetadata? metadata = null;
if (File.Exists(baseTranslationOutputMetadataPath))
{
metadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath);
metadata = ReadBaseMetadataCached(baseTranslationOutputMetadataPath);
}
if (metadata is null || metadata.Functions.Any(static function => function.Build is null))
{
@@ -2324,12 +2346,9 @@ int EmitModCpp(
: new CompositeGuestFunctionAbiProvider(inferredAbi);
var modTranslator = new FunctionTranslator(modImage, abiProvider);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = maxThreads };
// Mod emission waves: each item is a full translate+emit, and each wave is a
// barrier that also drains newly discovered continuations, so it is
// deliberately smaller than the discovery batch above - a long wave delays
// the discoveries the next wave has to translate.
var waveSize = Math.Max(16, maxThreads * 4);
Console.WriteLine($" translating mod C++ with up to {maxThreads} worker thread(s) in waves of {waveSize}");
// Each wave drains its whole queue: a wave is a barrier that publishes newly discovered
// entries, and draining everything makes the output independent of any wave size.
Console.WriteLine($" translating mod C++ with up to {maxThreads} worker thread(s)");
var cppRoot = Path.Combine(outDir, "cpp");
if (Directory.Exists(cppRoot))
@@ -2408,7 +2427,7 @@ int EmitModCpp(
// Some dynamically discovered module/continuation waves bypass
// ProcessBoundedWaves. Prewarm every parallel emission boundary as a
// final deterministic guard; phase-prewarmed entries are cheap hits.
ReportAbiPrewarm(inferredAbi.Prewarm(wave.Select(static work => work.Address)));
ReportAbiPrewarm(inferredAbi.Prewarm(wave.Select(static work => work.Address), maxThreads));
return ModTranslationWaveRunner.Translate(() => modTranslator, wave, parallelOptions);
}
@@ -2445,7 +2464,7 @@ int EmitModCpp(
// Populate the recursive ABI cache in a stable global order before any worker emits C++,
// so shared closure reuse and cyclic call graphs never depend on thread count, wave size,
// or worker order.
ReportAbiPrewarm(inferredAbi.Prewarm(ordered.Select(static work => work.Address)));
ReportAbiPrewarm(inferredAbi.Prewarm(ordered.Select(static work => work.Address), maxThreads));
// Commit callbacks here only append to the discovered-continuation queue that a later
// phase drains, so nothing in this phase reads back its own commits; wave barriers would
// only cost idle workers, so it runs as one parallel batch and the runner re-sorts by the
@@ -2590,7 +2609,7 @@ int EmitModCpp(
{
while (discoveredContinuationQueue.Count > 0)
{
var wave = QueueBatch.Dequeue(discoveredContinuationQueue, waveSize)
var wave = QueueBatch.Dequeue(discoveredContinuationQueue, discoveredContinuationQueue.Count)
.Select(CreateContinuationWork)
.ToArray();
CommitWave(TranslateWave(wave), (result, work) =>
@@ -2609,10 +2628,10 @@ int EmitModCpp(
var emittedModuleStarts = new HashSet<uint>();
while (moduleQueue.Count > 0)
{
var wave = new List<ModTranslationWork>(waveSize);
var wave = new List<ModTranslationWork>(moduleQueue.Count);
foreach (var start in QueueBatch.Dequeue(
moduleQueue,
waveSize,
moduleQueue.Count,
start => !emittedModuleStarts.Add(start.Address)))
{
var name = ModuleFunctionName(start.Address);
@@ -8,6 +8,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>false</TieredPGO>
<VersionPrefix>0.5.0</VersionPrefix>
<Company>WiiCompiled</Company>
<Product>Mario Kart Wii Static Translator</Product>
@@ -253,6 +253,12 @@ public sealed partial class CxxLinearCodeGenerator
? index
: -1;
private static uint ClearPairedBit(uint mask, string destination)
{
var bit = PairedFloatRegisterBit(destination);
return bit >= 0 ? mask & ~(1u << bit) : mask;
}
private static HashSet<string> MaterializePairedFloatSet(uint mask, StringComparer comparer)
{
var set = new HashSet<string>(comparer);
@@ -454,11 +460,21 @@ public sealed partial class CxxLinearCodeGenerator
break;
}
case IrLoad load:
newOut = ClearPairedBit(newOut, load.Destination);
break;
case IrResolvedLoad load:
newOut = ClearPairedBit(newOut, load.Destination);
break;
case IrResolvedLoadPair pair:
newOut = ClearPairedBit(newOut, pair.FirstDestination);
newOut = ClearPairedBit(newOut, pair.SecondDestination);
break;
case IrResolvedPsqLoad load:
{
var destBit = PairedFloatRegisterBit(load.Destination);
if (destBit >= 0)
{
newOut &= ~(1u << destBit);
newOut |= 1u << destBit;
}
break;
}
@@ -102,12 +102,7 @@ public static class GuestMemoryRangeLowering
.Any(group => group.Count() >= minimumAccesses))
return function;
var cfg = IrCfg.Build(function);
var dominators = ComputeDominators(cfg);
var reachability = ComputeReachability(cfg);
var cyclicBlocks = cfg.Blocks.Keys.Where(label =>
cfg.Successors(label).Any(successor => reachability[successor].Contains(label)))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var graph = BlockGraph.Build(function);
var plans = new Dictionary<(int Block, int Instruction), Plan>();
var resolves = new Dictionary<int, List<PlacedResolve>>();
@@ -129,24 +124,35 @@ public static class GuestMemoryRangeLowering
if (definitionSites.Count != 1) continue;
definition = definitionSites[0];
var defined = definitionSites[0];
var definitionBlock = function.Blocks[defined.BlockIndex].Label;
if (remaining.Any(candidate =>
!dominators[candidate.AccessBlockLabel(function)].Contains(definitionBlock) ||
!graph.Dominates(defined.BlockIndex, candidate.BlockIndex) ||
candidate.BlockIndex == defined.BlockIndex &&
candidate.Access.Index <= defined.InstructionIndex))
continue;
}
// The pair predicate is invariant across rounds, so memoize it.
var groupIndex = new Dictionary<FunctionAccess, int>(ReferenceEqualityComparer.Instance);
foreach (var access in remaining) groupIndex.Add(access, groupIndex.Count);
var compatible = remaining.Count <= 4096 ? new sbyte[remaining.Count * remaining.Count] : null;
bool Compatible(FunctionAccess anchor, FunctionAccess candidate)
{
if (compatible is null)
return DominatesAccess(graph, anchor, candidate) &&
!HasInvalidatingBoundaryOnPath(graph, invalidators, anchor, candidate);
var slot = groupIndex[anchor] * groupIndex.Count + groupIndex[candidate];
if (compatible[slot] != 0) return compatible[slot] > 0;
var value = DominatesAccess(graph, anchor, candidate) &&
!HasInvalidatingBoundaryOnPath(graph, invalidators, anchor, candidate);
compatible[slot] = value ? (sbyte)1 : (sbyte)-1;
return value;
}
while (remaining.Count >= minimumAccesses)
{
FunctionAccess? bestAnchor = null;
FunctionAccess[] bestMembers = [];
foreach (var anchor in remaining)
{
var members = remaining.Where(candidate =>
DominatesAccess(function, dominators, anchor, candidate) &&
!HasInvalidatingBoundaryOnPath(
function, reachability, cyclicBlocks,
invalidators, anchor, candidate))
var members = remaining.Where(candidate => Compatible(anchor, candidate))
.OrderBy(candidate => candidate.Access.Index)
.ToList();
// Keep a bounded affine window around the real anchor. A
@@ -248,15 +254,13 @@ public static class GuestMemoryRangeLowering
}
private static bool DominatesAccess(
IrFunction function,
IReadOnlyDictionary<string, HashSet<string>> dominators,
BlockGraph graph,
FunctionAccess anchor,
FunctionAccess consumer)
{
if (anchor.BlockIndex == consumer.BlockIndex)
return anchor.Access.Index <= consumer.Access.Index;
return dominators[consumer.AccessBlockLabel(function)].Contains(
anchor.AccessBlockLabel(function));
return graph.Dominates(anchor.BlockIndex, consumer.BlockIndex);
}
private static bool HasOverlappingOppositeAccess(
@@ -273,27 +277,25 @@ public static class GuestMemoryRangeLowering
}
private static bool HasInvalidatingBoundaryOnPath(
IrFunction function,
IReadOnlyDictionary<string, HashSet<string>> reachability,
IReadOnlySet<string> cyclicBlocks,
BlockGraph graph,
IReadOnlyList<InstructionSite> invalidators,
FunctionAccess anchor,
FunctionAccess consumer)
{
var anchorLabel = anchor.AccessBlockLabel(function);
var consumerLabel = consumer.AccessBlockLabel(function);
var anchorBlock = anchor.BlockIndex;
var consumerBlock = consumer.BlockIndex;
var consumerReentered = anchorBlock != consumerBlock && graph.Cyclic[consumerBlock];
foreach (var invalidator in invalidators)
{
var boundaryLabel = function.Blocks[invalidator.BlockIndex].Label;
var afterAnchor = invalidator.BlockIndex == anchor.BlockIndex
var boundaryBlock = invalidator.BlockIndex;
var afterAnchor = boundaryBlock == anchorBlock
? invalidator.InstructionIndex > anchor.Access.Index
: reachability[anchorLabel].Contains(boundaryLabel);
var beforeConsumer = invalidator.BlockIndex == consumer.BlockIndex
? invalidator.InstructionIndex < consumer.Access.Index ||
anchor.BlockIndex != consumer.BlockIndex &&
cyclicBlocks.Contains(consumerLabel)
: reachability[boundaryLabel].Contains(consumerLabel);
if (afterAnchor && beforeConsumer) return true;
: graph.Reaches(anchorBlock, boundaryBlock);
if (!afterAnchor) continue;
var beforeConsumer = boundaryBlock == consumerBlock
? invalidator.InstructionIndex < consumer.Access.Index || consumerReentered
: graph.Reaches(boundaryBlock, consumerBlock);
if (beforeConsumer) return true;
}
return false;
}
@@ -315,54 +317,117 @@ public static class GuestMemoryRangeLowering
return result;
}
private static Dictionary<string, HashSet<string>> ComputeDominators(IrCfg cfg)
/// <summary>Dominator and reachability sets as bitsets over block ordinals.</summary>
private sealed class BlockGraph
{
var comparer = StringComparer.OrdinalIgnoreCase;
var labels = cfg.Blocks.Keys.ToArray();
var all = labels.ToHashSet(comparer);
var result = labels.ToDictionary(label => label,
label => label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase)
? new HashSet<string>([label], comparer)
: new HashSet<string>(all, comparer), comparer);
bool changed;
do
{
changed = false;
foreach (var label in labels)
{
if (label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase)) continue;
var predecessors = cfg.Predecessors(label);
var next = predecessors.Count == 0
? new HashSet<string>(comparer)
: new HashSet<string>(result[predecessors[0]], comparer);
foreach (var predecessor in predecessors.Skip(1)) next.IntersectWith(result[predecessor]);
next.Add(label);
if (result[label].SetEquals(next)) continue;
result[label] = next;
changed = true;
}
} while (changed);
return result;
}
private readonly ulong[][] _dominators;
private readonly ulong[][] _reach;
private static Dictionary<string, HashSet<string>> ComputeReachability(IrCfg cfg)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var result = new Dictionary<string, HashSet<string>>(comparer);
foreach (var start in cfg.Blocks.Keys)
private BlockGraph(ulong[][] dominators, ulong[][] reach, bool[] cyclic)
{
var reachable = new HashSet<string>(comparer);
var pending = new Stack<string>();
pending.Push(start);
while (pending.Count != 0)
{
var current = pending.Pop();
if (!reachable.Add(current)) continue;
foreach (var successor in cfg.Successors(current)) pending.Push(successor);
}
result[start] = reachable;
_dominators = dominators;
_reach = reach;
Cyclic = cyclic;
}
/// <summary>Blocks that can reach themselves again through a successor.</summary>
public bool[] Cyclic { get; }
public bool Dominates(int dominator, int block) =>
(_dominators[block][dominator >> 6] & (1UL << (dominator & 63))) != 0;
/// <summary>Reflexive: every block reaches itself.</summary>
public bool Reaches(int from, int to) =>
(_reach[from][to >> 6] & (1UL << (to & 63))) != 0;
public static BlockGraph Build(IrFunction function)
{
var cfg = IrCfg.Build(function);
var count = function.Blocks.Count;
var words = (count + 63) >> 6;
var index = new Dictionary<string, int>(count, StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < count; i++) index.Add(function.Blocks[i].Label, i);
var successors = new int[count][];
var predecessors = new int[count][];
for (var i = 0; i < count; i++)
{
var label = function.Blocks[i].Label;
successors[i] = cfg.Successors(label).Select(successor => index[successor]).ToArray();
predecessors[i] = cfg.Predecessors(label).Select(predecessor => index[predecessor]).ToArray();
}
var reach = new ulong[count][];
var pending = new Stack<int>();
for (var start = 0; start < count; start++)
{
var row = new ulong[words];
pending.Push(start);
while (pending.Count != 0)
{
var current = pending.Pop();
ref var word = ref row[current >> 6];
var bit = 1UL << (current & 63);
if ((word & bit) != 0) continue;
word |= bit;
foreach (var successor in successors[current]) pending.Push(successor);
}
reach[start] = row;
}
var cyclic = new bool[count];
for (var i = 0; i < count; i++)
{
foreach (var successor in successors[i])
{
if ((reach[successor][i >> 6] & (1UL << (i & 63))) == 0) continue;
cyclic[i] = true;
break;
}
}
var entry = index[cfg.Entry];
var dominators = new ulong[count][];
for (var i = 0; i < count; i++)
{
dominators[i] = new ulong[words];
if (i == entry)
{
dominators[i][i >> 6] = 1UL << (i & 63);
continue;
}
Array.Fill(dominators[i], ulong.MaxValue);
if ((count & 63) != 0) dominators[i][words - 1] = (1UL << (count & 63)) - 1;
}
var next = new ulong[words];
bool changed;
do
{
changed = false;
for (var i = 0; i < count; i++)
{
if (i == entry) continue;
var preds = predecessors[i];
if (preds.Length == 0)
{
Array.Clear(next);
}
else
{
Array.Copy(dominators[preds[0]], next, words);
for (var p = 1; p < preds.Length; p++)
{
var other = dominators[preds[p]];
for (var w = 0; w < words; w++) next[w] &= other[w];
}
}
next[i >> 6] |= 1UL << (i & 63);
if (next.AsSpan().SequenceEqual(dominators[i])) continue;
next.AsSpan().CopyTo(dominators[i]);
changed = true;
}
} while (changed);
return new BlockGraph(dominators, reach, cyclic);
}
return result;
}
private static IrInstruction ToResolved(IrInstruction instruction, Access access, Plan plan)
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Translator.Core.Analysis;
@@ -72,6 +73,9 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
/// <summary>Reusable backing list for <see cref="FrameTaint"/>; phase A is never reentrant.</summary>
public readonly List<bool> FrameTaintBuffer = new();
/// <summary>Address per <see cref="FrameTaint"/> entry, innermost last.</summary>
public readonly List<uint> FrameAddresses = new();
/// <summary>Addresses already known tainted, shared by every phase-A worker of the current Prewarm
/// call and reset each invocation. Without this memoization an unpublishable address gets
/// re-analyzed, subtree and all, by every caller that reaches it, which compounds combinatorially
@@ -95,14 +99,21 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
return false;
}
var state = _threadState.Value!;
var frameTaint = state.FrameTaint;
if (frameTaint is not null && state.FrameAddresses.Count > 0 && state.FrameAddresses[^1] == address)
{
// Direct self-recursion sees what a sequential run would, even once another thread has published it.
abi = GuestFunctionAbi.Empty;
return false;
}
if (_cache.TryGetValue(address, out abi!))
{
return true;
}
var state = _threadState.Value!;
var inProgress = state.InProgress;
var frameTaint = state.FrameTaint;
if (frameTaint is not null && state.TaintedAddresses!.ContainsKey(address))
{
@@ -151,6 +162,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
// Prewarm phase A: track taint for this frame and publish only if it stayed clean.
frameTaint.Add(false);
state.FrameAddresses.Add(address);
var frameIndex = frameTaint.Count - 1;
bool succeeded;
bool tainted;
@@ -172,6 +184,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
finally
{
inProgress.Remove(address);
state.FrameAddresses.RemoveAt(frameIndex);
tainted = frameTaint[frameIndex];
frameTaint.RemoveAt(frameIndex);
if (tainted && frameIndex > 0)
@@ -274,6 +287,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
var state = _threadState.Value!;
var frames = state.FrameTaintBuffer;
frames.Clear();
state.FrameAddresses.Clear();
state.TaintedAddresses = taintedAddresses;
state.FrameTaint = frames;
try
@@ -418,82 +432,102 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private HashSet<string> InferArgumentRegisters(IrFunction function, IrCfg cfg)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var blockUses = new Dictionary<string, HashSet<string>>(comparer);
var blockDefs = new Dictionary<string, HashSet<string>>(comparer);
foreach (var block in function.Blocks)
// Interned register ids so the liveness fixpoint runs over bitsets.
var ids = new Dictionary<string, int>(comparer);
var names = new List<string>();
int Id(string name)
{
var uses = new HashSet<string>(comparer);
var defs = new HashSet<string>(comparer);
if (!ids.TryGetValue(name, out var id))
{
id = names.Count;
ids.Add(name, id);
names.Add(name);
}
return id;
}
var blockCount = function.Blocks.Count;
var useLists = new List<int>[blockCount];
var defLists = new List<int>[blockCount];
var labelIndex = new Dictionary<string, int>(blockCount, comparer);
for (var b = 0; b < blockCount; b++)
{
var block = function.Blocks[b];
var uses = new List<int>();
var defs = new List<int>();
var defined = new HashSet<int>();
foreach (var instruction in block.Instructions)
{
foreach (var use in InstructionRegisterUsesForArgumentInference(instruction))
{
if (!defs.Contains(use))
{
uses.Add(use);
}
var id = Id(use);
if (!defined.Contains(id))
uses.Add(id);
}
foreach (var def in InstructionRegisterDefsForArgumentInference(instruction))
{
defs.Add(def);
var id = Id(def);
if (defined.Add(id))
defs.Add(id);
}
}
blockUses[block.Label] = uses;
blockDefs[block.Label] = defs;
useLists[b] = uses;
defLists[b] = defs;
}
var liveIn = function.Blocks.ToDictionary(
static block => block.Label,
_ => new HashSet<string>(comparer),
comparer);
var liveOut = function.Blocks.ToDictionary(
static block => block.Label,
_ => new HashSet<string>(comparer),
comparer);
for (var b = 0; b < blockCount; b++)
labelIndex.Add(function.Blocks[b].Label, b);
var words = Math.Max(1, (names.Count + 63) >> 6);
var useBits = new ulong[blockCount * words];
var keepBits = new ulong[blockCount * words];
Array.Fill(keepBits, ulong.MaxValue);
var successors = new int[blockCount][];
for (var b = 0; b < blockCount; b++)
{
foreach (var id in useLists[b]) useBits[b * words + (id >> 6)] |= 1UL << (id & 63);
foreach (var id in defLists[b]) keepBits[b * words + (id >> 6)] &= ~(1UL << (id & 63));
var targets = new List<int>();
foreach (var successor in cfg.Successors(function.Blocks[b].Label))
if (labelIndex.TryGetValue(successor, out var index))
targets.Add(index);
successors[b] = targets.ToArray();
}
var liveIn = new ulong[blockCount * words];
var changed = true;
while (changed)
{
changed = false;
for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; blockIndex--)
for (var b = blockCount - 1; b >= 0; b--)
{
var block = function.Blocks[blockIndex];
var newOut = new HashSet<string>(comparer);
foreach (var successor in cfg.Successors(block.Label))
var row = b * words;
for (var w = 0; w < words; w++)
{
if (liveIn.TryGetValue(successor, out var successorLiveIn))
ulong liveOut = 0;
foreach (var successor in successors[b])
liveOut |= liveIn[successor * words + w];
var value = useBits[row + w] | (liveOut & keepBits[row + w]);
if (value != liveIn[row + w])
{
newOut.UnionWith(successorLiveIn);
liveIn[row + w] = value;
changed = true;
}
}
var newIn = new HashSet<string>(blockUses[block.Label], comparer);
var outMinusDefs = new HashSet<string>(newOut, comparer);
outMinusDefs.ExceptWith(blockDefs[block.Label]);
newIn.UnionWith(outMinusDefs);
if (!newOut.SetEquals(liveOut[block.Label]))
{
liveOut[block.Label] = newOut;
changed = true;
}
if (!newIn.SetEquals(liveIn[block.Label]))
{
liveIn[block.Label] = newIn;
changed = true;
}
}
}
return liveIn.TryGetValue(function.EntryLabel, out var entryLive)
? entryLive
.Where(IsGprOrFprRegister)
.ToHashSet(comparer)
: new HashSet<string>(comparer);
var result = new HashSet<string>(comparer);
if (labelIndex.TryGetValue(function.EntryLabel, out var entry))
{
for (var id = 0; id < names.Count; id++)
{
if ((liveIn[entry * words + (id >> 6)] & (1UL << (id & 63))) != 0 && IsGprOrFprRegister(names[id]))
result.Add(names[id]);
}
}
return result;
}
private IEnumerable<string> InstructionRegisterUsesForArgumentInference(IrInstruction instruction)
@@ -755,45 +789,74 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private HashSet<string> InferScalarFloatArguments(IrFunction function, IrCfg cfg)
{
var scalarArgs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var comparer = StringComparer.OrdinalIgnoreCase;
var inMap = function.Blocks.ToDictionary(
static b => b.Label,
b => b.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase)
? CreateInitialArgumentAliasState()
: new Dictionary<string, HashSet<string>>(comparer),
comparer);
var outMap = function.Blocks.ToDictionary(
static b => b.Label,
b => b.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase)
? CreateInitialArgumentAliasState()
: new Dictionary<string, HashSet<string>>(comparer),
comparer);
var keys = new AliasKeys(function);
var blockCount = function.Blocks.Count;
var blockIndex = new Dictionary<string, int>(blockCount, StringComparer.OrdinalIgnoreCase);
for (var b = 0; b < blockCount; b++)
blockIndex[function.Blocks[b].Label] = b;
var predecessors = new int[blockCount][];
var isEntry = new bool[blockCount];
for (var b = 0; b < blockCount; b++)
{
var block = function.Blocks[b];
isEntry[b] = block.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase);
var list = new List<int>();
foreach (var predecessor in cfg.Predecessors(block.Label))
if (blockIndex.TryGetValue(predecessor, out var index))
list.Add(index);
predecessors[b] = list.ToArray();
}
// Per alias key: -1 when absent, otherwise the mask of ABI float argument sources.
var initial = keys.InitialState();
var inMap = new int[blockCount][];
var outMap = new int[blockCount][];
for (var b = 0; b < blockCount; b++)
{
inMap[b] = isEntry[b] ? (int[])initial.Clone() : keys.EmptyState();
outMap[b] = isEntry[b] ? (int[])initial.Clone() : keys.EmptyState();
}
var changed = true;
while (changed)
{
changed = false;
foreach (var block in function.Blocks)
for (var b = 0; b < blockCount; b++)
{
var newIn = block.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase)
? CreateInitialArgumentAliasState()
: UnionPredecessorStates(cfg.Predecessors(block.Label), outMap);
var newOut = CopyAliasState(newIn);
foreach (var ins in block.Instructions)
int[] newIn;
if (isEntry[b])
{
ProcessScalarArgumentInstruction(ins, newOut, scalarArgs);
newIn = (int[])initial.Clone();
}
else
{
newIn = keys.EmptyState();
foreach (var predecessor in predecessors[b])
{
var predState = outMap[predecessor];
for (var key = 0; key < newIn.Length; key++)
{
if (predState[key] < 0) continue;
newIn[key] = newIn[key] < 0 ? predState[key] : newIn[key] | predState[key];
}
}
}
if (!AliasStatesEqual(newIn, inMap[block.Label]))
var newOut = (int[])newIn.Clone();
foreach (var ins in function.Blocks[b].Instructions)
{
inMap[block.Label] = newIn;
ProcessScalarArgumentInstruction(ins, keys, newOut, scalarArgs);
}
if (!newIn.AsSpan().SequenceEqual(inMap[b]))
{
inMap[b] = newIn;
changed = true;
}
if (!AliasStatesEqual(newOut, outMap[block.Label]))
if (!newOut.AsSpan().SequenceEqual(outMap[b]))
{
outMap[block.Label] = newOut;
outMap[b] = newOut;
changed = true;
}
}
@@ -802,63 +865,153 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
return scalarArgs;
}
/// <summary>Interned float register names the scalar-argument alias analysis can track.
/// Ids 0..12 are the ABI float argument registers, so a source set is a 13-bit mask.</summary>
private sealed class AliasKeys
{
private readonly Dictionary<string, int> _ids = new(StringComparer.OrdinalIgnoreCase);
public AliasKeys(IrFunction function)
{
foreach (var register in AbiFloatArgumentRegisters)
_ids.Add(register, _ids.Count);
foreach (var block in function.Blocks)
{
foreach (var instruction in block.Instructions)
{
switch (instruction)
{
case IrAssign assign:
AddName(assign.Destination);
AddValue(assign.Value);
break;
case IrBinary binary:
AddValue(binary.Left);
AddValue(binary.Right);
AddName(binary.Destination);
break;
case IrLoad load:
AddName(load.Destination);
break;
case IrStore store:
AddValue(store.Source);
break;
case IrCall call:
foreach (var argument in call.Arguments) AddValue(argument);
AddName(call.Destination);
break;
case IrIndirectCall indirectCall:
foreach (var argument in indirectCall.Arguments) AddValue(argument);
AddName(indirectCall.Destination);
break;
case IrSetCrField setCr:
AddValue(setCr.Left);
AddValue(setCr.Right);
break;
case IrPhi phi:
AddName(phi.Destination);
break;
}
}
}
}
public int Count => _ids.Count;
public int[] EmptyState()
{
var state = new int[_ids.Count];
Array.Fill(state, -1);
return state;
}
public int[] InitialState()
{
var state = EmptyState();
for (var i = 0; i < AbiFloatArgumentRegisters.Length; i++)
state[i] = 1 << i;
return state;
}
public bool TryGet(string baseName, out int id) => _ids.TryGetValue(baseName, out id);
private void AddValue(IrValue? value)
{
if (value is { Kind: "register", RegisterName: { } name })
AddName(name);
}
private void AddName(string? name)
{
if (string.IsNullOrEmpty(name)) return;
var baseName = Base(name);
if (IsFloatRegister(baseName))
_ids.TryAdd(baseName, _ids.Count);
}
}
private void ProcessScalarArgumentInstruction(
IrInstruction ins,
Dictionary<string, HashSet<string>> aliases,
AliasKeys keys,
int[] aliases,
HashSet<string> scalarArgs)
{
switch (ins)
{
case IrAssign assign:
PropagateFloatAlias(assign.Destination, assign.Value, aliases);
PropagateFloatAlias(assign.Destination, assign.Value, keys, aliases);
break;
case IrBinary bin:
MarkScalarUse(bin.Left, aliases, scalarArgs);
MarkScalarUse(bin.Right, aliases, scalarArgs);
KillFloatAlias(bin.Destination, aliases);
MarkScalarUse(bin.Left, keys, aliases, scalarArgs);
MarkScalarUse(bin.Right, keys, aliases, scalarArgs);
KillFloatAlias(bin.Destination, keys, aliases);
break;
case IrLoad load:
KillFloatAlias(load.Destination, aliases);
KillFloatAlias(load.Destination, keys, aliases);
break;
case IrStore store:
if (store.SizeBytes is 4 or 8)
{
MarkScalarUse(store.Source, aliases, scalarArgs);
MarkScalarUse(store.Source, keys, aliases, scalarArgs);
}
break;
case IrCall call:
ProcessScalarArgumentCall(call.Target, call.Arguments, aliases, scalarArgs);
KillFloatAlias(call.Destination, aliases);
ProcessScalarArgumentCall(call.Target, call.Arguments, keys, aliases, scalarArgs);
KillFloatAlias(call.Destination, keys, aliases);
break;
case IrIndirectCall icall:
foreach (var arg in icall.Arguments)
{
MarkScalarUse(arg, aliases, scalarArgs);
MarkScalarUse(arg, keys, aliases, scalarArgs);
}
aliases.Remove("f1");
KillFloatAlias(icall.Destination, aliases);
aliases[F1AliasKey] = -1;
KillFloatAlias(icall.Destination, keys, aliases);
break;
case IrSetCrField setCr:
MarkScalarUse(setCr.Left, aliases, scalarArgs);
MarkScalarUse(setCr.Right, aliases, scalarArgs);
MarkScalarUse(setCr.Left, keys, aliases, scalarArgs);
MarkScalarUse(setCr.Right, keys, aliases, scalarArgs);
break;
case IrPhi phi:
KillFloatAlias(phi.Destination, aliases);
KillFloatAlias(phi.Destination, keys, aliases);
break;
}
}
/// <summary>Alias key of f1, the first ABI float argument register.</summary>
private const int F1AliasKey = 0;
private void ProcessScalarArgumentCall(
string target,
IReadOnlyList<IrValue> arguments,
Dictionary<string, HashSet<string>> aliases,
AliasKeys keys,
int[] aliases,
HashSet<string> scalarArgs)
{
if (IsGuestCallTarget(target))
@@ -873,14 +1026,14 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
arg.RegisterName != null &&
abi.HasScalarFloatArgument(Base(arg.RegisterName)))
{
MarkScalarUse(arg, aliases, scalarArgs);
MarkScalarUse(arg, keys, aliases, scalarArgs);
}
}
}
if (writesFloatReturnRegister)
{
aliases.Remove("f1");
aliases[F1AliasKey] = -1;
}
return;
}
@@ -892,7 +1045,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
foreach (var arg in arguments)
{
MarkScalarUse(arg, aliases, scalarArgs);
MarkScalarUse(arg, keys, aliases, scalarArgs);
}
}
@@ -1083,7 +1236,8 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private static void MarkScalarUse(
IrValue value,
IReadOnlyDictionary<string, HashSet<string>> aliases,
AliasKeys keys,
int[] aliases,
HashSet<string> scalarArgs)
{
if (value.Kind != "register" || value.RegisterName == null)
@@ -1092,24 +1246,22 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
}
var baseName = Base(value.RegisterName);
if (!IsFloatRegister(baseName) || !aliases.TryGetValue(baseName, out var sources))
if (!IsFloatRegister(baseName) || !keys.TryGet(baseName, out var key) || aliases[key] < 0)
{
return;
}
foreach (var source in sources)
for (var sources = aliases[key]; sources != 0; sources &= sources - 1)
{
if (AbiFloatArgumentSet.Contains(source))
{
scalarArgs.Add(source);
}
scalarArgs.Add(AbiFloatArgumentRegisters[BitOperations.TrailingZeroCount(sources)]);
}
}
private static void PropagateFloatAlias(
string destination,
IrValue value,
Dictionary<string, HashSet<string>> aliases)
AliasKeys keys,
int[] aliases)
{
var destBase = Base(destination);
if (!IsFloatRegister(destBase))
@@ -1117,24 +1269,27 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
return;
}
keys.TryGet(destBase, out var destKey);
if (value.Kind == "register" &&
value.RegisterName != null &&
IsFloatRegister(Base(value.RegisterName)) &&
aliases.TryGetValue(Base(value.RegisterName), out var srcAliases))
keys.TryGet(Base(value.RegisterName), out var sourceKey) &&
aliases[sourceKey] >= 0)
{
aliases[destBase] = new HashSet<string>(srcAliases, StringComparer.OrdinalIgnoreCase);
aliases[destKey] = aliases[sourceKey];
}
else
{
aliases.Remove(destBase);
aliases[destKey] = -1;
}
}
private static void KillFloatAlias(string destination, Dictionary<string, HashSet<string>> aliases)
private static void KillFloatAlias(string destination, AliasKeys keys, int[] aliases)
{
if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination)))
if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination)) &&
keys.TryGet(Base(destination), out var key))
{
aliases.Remove(Base(destination));
aliases[key] = -1;
}
}
@@ -1146,70 +1301,6 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
}
}
private static Dictionary<string, HashSet<string>> CreateInitialArgumentAliasState()
{
return AbiFloatArgumentRegisters.ToDictionary(
static r => r,
static r => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { r },
StringComparer.OrdinalIgnoreCase);
}
private static Dictionary<string, HashSet<string>> UnionPredecessorStates(
IReadOnlyList<string> predecessors,
IReadOnlyDictionary<string, Dictionary<string, HashSet<string>>> outMap)
{
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pred in predecessors)
{
if (!outMap.TryGetValue(pred, out var predState))
{
continue;
}
foreach (var (register, sources) in predState)
{
if (!result.TryGetValue(register, out var destSources))
{
destSources = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
result[register] = destSources;
}
destSources.UnionWith(sources);
}
}
return result;
}
private static Dictionary<string, HashSet<string>> CopyAliasState(
IReadOnlyDictionary<string, HashSet<string>> state)
{
return state.ToDictionary(
static kv => kv.Key,
static kv => new HashSet<string>(kv.Value, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
}
private static bool AliasStatesEqual(
IReadOnlyDictionary<string, HashSet<string>> left,
IReadOnlyDictionary<string, HashSet<string>> right)
{
if (left.Count != right.Count)
{
return false;
}
foreach (var (key, leftSet) in left)
{
if (!right.TryGetValue(key, out var rightSet) || !leftSet.SetEquals(rightSet))
{
return false;
}
}
return true;
}
private static bool IsGuestCallTarget(string target)
{
if (string.IsNullOrWhiteSpace(target))
@@ -70,7 +70,7 @@ internal static class JumpTableDetector
}
entriesAreRelative = HasRelativeEntryAdd(ordered, loadIndex, mtctrIndex, targetRegister, baseReg.Name);
if (!TryFindUpperBound(ordered, loadIndex, indexReg.Name, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount)
if (!TryFindUpperBound(ordered, loadIndex, indexReg, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount)
{
return false;
}
@@ -381,47 +381,60 @@ internal static class JumpTableDetector
return false;
}
private static bool TryFindUpperBound(IReadOnlyList<PpcInstruction> ordered, int startIndex, string register, out int upperBound)
private static bool TryFindUpperBound(IReadOnlyList<PpcInstruction> ordered, int startIndex, PpcRegisterOperand register, out int upperBound)
{
// Preferred: compare directly on the same register used for lwzx indexing.
// Preferred: the compare on the lwzx index, followed back through the copies and
// shifts that derived it. A compare older than the value's definition bounds
// something else.
var tracked = register.Number;
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
{
var ins = ordered[i];
if (ins.Operands.Count < 2 || ins.Operands[0] is not PpcRegisterOperand reg ||
!string.Equals(reg.Name, register, StringComparison.OrdinalIgnoreCase) ||
ins.Operands[1] is not PpcImmediateOperand imm)
if (TryGetCompareImmediate(ins, out var compared, out var bound))
{
if (compared == tracked)
{
upperBound = bound;
return true;
}
continue;
}
if (ins.IsCall)
{
break;
}
if (!DefinesGpr(ins, tracked))
{
continue;
}
if (ins.Mnemonic is "cmplwi" or "cmpwi")
if (IsIndexDerivation(ins) &&
ins.Operands[0] is PpcRegisterOperand dest && dest.Number == tracked &&
ins.Operands[1] is PpcRegisterOperand source)
{
upperBound = imm.Value;
return true;
tracked = source.Number;
continue;
}
break;
}
// Fallback: some loops compare a logical counter (e.g. r18) distinct from the byte-offset
// register (e.g. r30) that feeds lwzx, so the bound check may not target the index register.
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
{
var ins = ordered[i];
if (ins.Operands.Count < 2 || ins.Operands[1] is not PpcImmediateOperand imm)
if (ordered[i].Operands.Count > 0 && ordered[i].Operands[0] is PpcConditionRegisterOperand)
{
continue;
}
if (!TryGetCompareImmediate(ordered[i], out _, out var bound) || bound < 0 || bound >= MaxEntryCount)
{
continue;
}
if (ins.Mnemonic is not ("cmplwi" or "cmpwi"))
{
continue;
}
if (imm.Value < 0 || imm.Value >= MaxEntryCount)
{
continue;
}
upperBound = imm.Value;
upperBound = bound;
return true;
}
@@ -429,6 +442,70 @@ internal static class JumpTableDetector
return false;
}
// Copies, sign extensions and left shifts (slwi, clrlwi, clrlslwi) keep a bound on
// the source a bound on the result's entry index.
private static bool IsIndexDerivation(PpcInstruction ins) => ins.Mnemonic switch
{
"mr" or "extsb" or "extsh" => ins.Operands.Count >= 2,
"rlwinm" => ins.Operands.Count == 5 &&
ins.Operands[2] is PpcImmediateOperand sh &&
ins.Operands[4] is PpcImmediateOperand me &&
me.Value == 31 - sh.Value,
_ => false,
};
// MayWriteGpr treats both register fields of an X-form as written, which also
// counts sources (or, extsb, stwx, cmpw); read the decoded destination instead.
private static bool DefinesGpr(PpcInstruction ins, int register)
{
if (ins.Word >> 26 != 31)
{
return PpcRegisterEffects.MayWriteGpr(ins.Word, register);
}
var mnemonic = ins.Mnemonic;
if (mnemonic.EndsWith("ux", StringComparison.Ordinal) &&
ins.Operands.Count > 1 && ins.Operands[1] is PpcRegisterOperand updated && updated.Number == register)
{
return true;
}
if (mnemonic.StartsWith("st", StringComparison.Ordinal) ||
mnemonic.StartsWith("cmp", StringComparison.Ordinal) ||
mnemonic.StartsWith("mt", StringComparison.Ordinal) ||
mnemonic.StartsWith("dcb", StringComparison.Ordinal) ||
mnemonic.StartsWith("icb", StringComparison.Ordinal) ||
mnemonic.StartsWith("tw", StringComparison.Ordinal))
{
return false;
}
return ins.Operands.Count > 0 && ins.Operands[0] is PpcRegisterOperand dest && dest.Number == register;
}
private static bool TryGetCompareImmediate(PpcInstruction ins, out int register, out int immediate)
{
register = -1;
immediate = 0;
if (ins.Mnemonic is not ("cmplwi" or "cmpwi"))
{
return false;
}
var operands = ins.Operands;
var first = operands.Count > 0 && operands[0] is PpcConditionRegisterOperand ? 1 : 0;
if (operands.Count < first + 2 ||
operands[first] is not PpcRegisterOperand reg ||
operands[first + 1] is not PpcImmediateOperand imm)
{
return false;
}
register = reg.Number;
immediate = imm.Value;
return true;
}
private static bool TryReadWord(ProgramImage image, uint address, out uint value)
{
if (!image.Contains(address, sizeof(uint)))
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
namespace Translator.Core;
/// <summary>Column order of runtime/include/guest_abi/guest_address_table.inc.</summary>
/// <summary>Region index the runtime uses (GuestRegion::Id in guest_abi/guest_region.h).</summary>
public enum GuestRegion
{
Pal = 0,
@@ -28,53 +28,84 @@ public static class GuestRegions
}
/// <summary>
/// The runtime names guest addresses by their PAL address. This is the table it resolves them
/// through, read from the same file the C++ includes.
/// The runtime names guest addresses by their PAL address. This is the range map it resolves
/// them through (Retro Rewind versions.txt style), read from the same file the C++ includes.
/// </summary>
public sealed partial class GuestAddressTable
{
private readonly Dictionary<uint, uint[]> _rows;
private readonly Dictionary<GuestRegion, Range[]> _ranges;
private GuestAddressTable(Dictionary<uint, uint[]> rows, string sourcePath)
private readonly record struct Range(uint First, uint Last, int Delta);
private GuestAddressTable(Dictionary<GuestRegion, Range[]> ranges, string sourcePath)
{
_rows = rows;
_ranges = ranges;
SourcePath = sourcePath;
}
public string SourcePath { get; }
public int Count => _rows.Count;
public int Count => _ranges.Values.Sum(static ranges => ranges.Length);
/// <summary>The table beside a runtime whose native sources live in <paramref name="nativeSourceDirectory"/>.</summary>
/// <summary>The map beside a runtime whose native sources live in <paramref name="nativeSourceDirectory"/>.</summary>
public static string DefaultPath(string nativeSourceDirectory) =>
Path.GetFullPath(Path.Combine(nativeSourceDirectory, "..", "include", "guest_abi", "guest_address_table.inc"));
Path.GetFullPath(Path.Combine(nativeSourceDirectory, "..", "include", "guest_abi", "guest_versions.inc"));
public static GuestAddressTable Load(string path)
{
if (!File.Exists(path))
throw new FileNotFoundException("The runtime's guest address table was not found.", path);
var rows = new Dictionary<uint, uint[]>();
throw new FileNotFoundException("The runtime's guest address map was not found.", path);
var ranges = new Dictionary<GuestRegion, List<Range>>();
foreach (var line in File.ReadLines(path))
{
var match = RowPattern().Match(line);
if (!match.Success) continue;
var pal = Convert.ToUInt32(match.Groups["pal"].Value, 16);
if (!rows.TryAdd(pal, [pal, Convert.ToUInt32(match.Groups["u"].Value, 16), Convert.ToUInt32(match.Groups["j"].Value, 16)]))
throw new InvalidDataException($"{path} lists PAL address 0x{pal:X8} twice.");
if (!line.TrimStart().StartsWith("MKW_REGION_RANGE", StringComparison.Ordinal)) continue;
var match = RangePattern().Match(line);
if (!match.Success)
throw new InvalidDataException($"{path} has a malformed range: {line.Trim()}");
var regionName = match.Groups["region"].Value;
if (!Enum.TryParse<GuestRegion>(regionName, out var region) || !Enum.IsDefined(region) ||
region == GuestRegion.Pal || char.IsDigit(regionName[0]))
throw new InvalidDataException($"{path} has a range for unknown region '{regionName}'.");
var magnitude = Convert.ToInt64(match.Groups["delta"].Value, 16);
if (magnitude > int.MaxValue)
throw new InvalidDataException($"{path} has an out-of-range delta: {line.Trim()}");
var range = new Range(
Convert.ToUInt32(match.Groups["first"].Value, 16),
Convert.ToUInt32(match.Groups["last"].Value, 16),
(match.Groups["sign"].Value == "-" ? -1 : 1) * (int)magnitude);
if (range.First > range.Last)
throw new InvalidDataException($"{path} has an empty range at 0x{range.First:X8}.");
if (!ranges.TryGetValue(region, out var list))
ranges[region] = list = [];
if (list.Count != 0 && list[^1].Last >= range.First)
throw new InvalidDataException($"{path} ranges for {region} overlap or are unsorted at 0x{range.First:X8}.");
list.Add(range);
}
if (rows.Count == 0)
throw new InvalidDataException($"{path} has no MKW_GUEST_ADDRESS rows.");
return new GuestAddressTable(rows, path);
if (ranges.Count == 0)
throw new InvalidDataException($"{path} has no MKW_REGION_RANGE rows.");
return new GuestAddressTable(ranges.ToDictionary(static pair => pair.Key, static pair => pair.Value.ToArray()), path);
}
public uint Map(uint pal, GuestRegion region)
{
if (_rows.TryGetValue(pal, out var row))
return row[(int)region];
throw new InvalidDataException(
$"PAL address 0x{pal:X8} is missing from {SourcePath}; regenerate it with tools/region/gen_guest_address_table.py.");
if (region == GuestRegion.Pal)
return pal;
if (_ranges.TryGetValue(region, out var ranges))
{
var lo = 0;
var hi = ranges.Length;
while (lo < hi)
{
var mid = lo + (hi - lo) / 2;
if (pal < ranges[mid].First) hi = mid;
else lo = mid + 1;
}
if (lo > 0 && pal <= ranges[lo - 1].Last)
return (uint)(pal + (long)ranges[lo - 1].Delta);
}
throw new InvalidDataException($"PAL address 0x{pal:X8} has no {region} address in {SourcePath}.");
}
[GeneratedRegex(@"^\s*MKW_GUEST_ADDRESS\(\s*(?<pal>[0-9A-Fa-f]{8})\s*,\s*(?<u>[0-9A-Fa-f]{8})\s*,\s*(?<j>[0-9A-Fa-f]{8})\s*\)", RegexOptions.CultureInvariant)]
private static partial Regex RowPattern();
[GeneratedRegex(@"^\s*MKW_REGION_RANGE\(\s*(?<region>\w+)\s*,\s*(?<first>[0-9A-Fa-f]{8})\s*,\s*(?<last>[0-9A-Fa-f]{8})\s*,\s*(?<sign>[+-])0x(?<delta>[0-9A-Fa-f]{1,8})\s*\)\s*(//.*)?$", RegexOptions.CultureInvariant)]
private static partial Regex RangePattern();
}
@@ -69,9 +69,24 @@ public static class ModFunctionDiscovery
// Kamek modules frequently register callbacks through static tables or lists that are
// walked indirectly by module startup code. Seed those native translations ahead of time.
// The Kamek blob is linked at offset zero, so a word inside it only holds a module address
// when a pointer relocation wrote one there; anything else is an instruction or data that
// happens to encode one. Images appended past the blob are not described by chunk commands.
var pointerRelocations = chunk.Commands
.Where(command => command.AddressIsRelative &&
command.Arguments.Count > 0 &&
command.Id is KamekCommandId.Addr32 or KamekCommandId.CondWritePointer)
.Select(command => (command.Address, KamekAddress.Resolve(command.Arguments[0], moduleGuestBase)))
.ToHashSet();
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
{
var value = BinaryPrimitives.ReadUInt32BigEndian(relocatedModuleImage.AsSpan(offset, 4));
// Comparing the value also rejects conditional writes that were skipped.
if ((uint)offset < chunk.CodeSize && !pointerRelocations.Contains(((uint)offset, value)))
{
continue;
}
if (LooksLikeFunctionPointerTarget(value, moduleGuestBase, moduleImageEnd, relocatedModuleImage))
{
AddImageAddress(value, "module data pointer");
@@ -46,7 +46,7 @@ public sealed record RuntimeNativeIndex(
/// <summary>
/// The index is keyed by the PAL addresses the runtime sources name; this is the same index
/// keyed by <paramref name="region"/>'s addresses. Every address must be in the table, PAL too.
/// keyed by <paramref name="region"/>'s addresses. Every address must have a counterpart in that region.
/// </summary>
public RuntimeNativeIndex ForRegion(GuestAddressTable table, GuestRegion region) => new(
Registrations.Select(entry => entry with { Address = table.Map(entry.Address, region) }).ToArray(),
@@ -30,6 +30,14 @@ public sealed class CanonicalIrStore : ICanonicalIrProvider
}
}
public void Remove(uint entryPoint)
{
lock (_gate)
{
_entries.Remove(entryPoint);
}
}
public bool TryGet(uint entryPoint, out IrFunction function)
{
byte[]? encoded;
@@ -55,12 +63,16 @@ internal static class CanonicalIrBinaryCodec
IndirectJump, JumpTable, Return, Comment, TracePpc, Undefined
}
// Reused per thread so encoding doesn't regrow (and re-fault) a fresh buffer per function.
[ThreadStatic] private static MemoryStream? _encodeStream;
public static byte[] Encode(IrFunction function)
{
var strings = BuildStringTable(function);
var ids = strings.Select((value, index) => (value, index))
.ToDictionary(static pair => pair.value, static pair => pair.index, StringComparer.Ordinal);
using var stream = new MemoryStream();
var stream = _encodeStream ??= new MemoryStream();
stream.SetLength(0);
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
writer.Write(Magic);
WriteVarUInt(writer, (uint)CanonicalIrStore.FormatVersion);
@@ -85,9 +97,11 @@ internal static class CanonicalIrBinaryCodec
return stream.ToArray();
}
public static IrFunction Decode(ReadOnlySpan<byte> encoded)
public static IrFunction Decode(ReadOnlySpan<byte> encoded) => Decode(encoded.ToArray());
public static IrFunction Decode(byte[] encoded)
{
using var stream = new MemoryStream(encoded.ToArray(), writable: false);
using var stream = new MemoryStream(encoded, writable: false);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (!reader.ReadBytes(Magic.Length).SequenceEqual(Magic) ||
ReadVarUInt(reader) != CanonicalIrStore.FormatVersion)
@@ -497,12 +497,52 @@ public static class GqrInterproceduralAnalysis
return masks;
}
/// <summary>
/// Unknown-entry roots that depend only on the final call graph, never on discovery order:
/// <paramref name="fixedRoots"/>, every candidate without a direct caller, then (ascending) any
/// candidate the roots still don't reach, so no candidate is left without an entry fact source.
/// </summary>
public static HashSet<uint> SelectUnknownEntryRoots(
IReadOnlyDictionary<uint, GqrFunctionSummary> functions,
IEnumerable<uint> fixedRoots,
IEnumerable<uint> candidates)
{
var called = new HashSet<uint>();
foreach (var summary in functions.Values)
foreach (var target in summary.DirectGuestTargets)
called.Add(target);
var ordered = candidates.Where(functions.ContainsKey).Distinct().OrderBy(static a => a).ToArray();
var roots = fixedRoots.Where(functions.ContainsKey).ToHashSet();
foreach (var address in ordered)
if (!called.Contains(address))
roots.Add(address);
var reachable = ComputeCompactReachable(functions, roots);
foreach (var address in ordered)
{
if (reachable.Contains(address)) continue;
roots.Add(address);
ExtendCompactReachable(functions, reachable, address);
}
return roots;
}
private static HashSet<uint> ComputeCompactReachable(
IReadOnlyDictionary<uint, GqrFunctionSummary> functions,
IReadOnlySet<uint> roots)
{
var reachable = new HashSet<uint>();
var queue = new Queue<uint>(roots);
foreach (var root in roots)
ExtendCompactReachable(functions, reachable, root);
return reachable;
}
private static void ExtendCompactReachable(
IReadOnlyDictionary<uint, GqrFunctionSummary> functions,
HashSet<uint> reachable,
uint root)
{
var queue = new Queue<uint>();
queue.Enqueue(root);
while (queue.TryDequeue(out var address))
{
if (!reachable.Add(address)) continue;
@@ -510,7 +550,6 @@ public static class GqrInterproceduralAnalysis
if (functions.ContainsKey(target) && !reachable.Contains(target))
queue.Enqueue(target);
}
return reachable;
}
private static void TransferCompact(
@@ -538,4 +538,122 @@ public class FunctionTranslatorCoverageTests
Assert.Equal("// generated", result.CxxCode);
Assert.Same(metrics, result.Metrics);
}
[Fact]
public void Translate_JumpTableBoundFollowsShiftedIndexPastStaleCompare()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x20;
const uint case1 = entry + 0x28;
const uint case2 = entry + 0x30;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x2C040001), // cmpwi r4, 1
(entry + 0x04, 0x28030002), // cmplwi r3, 2
(entry + 0x08, 0x5464103A), // slwi r4, r3, 2
(entry + 0x0C, 0x3D808000), // lis r12, 0x8000
(entry + 0x10, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x14, 0x7C0C202E), // lwzx r0, r12, r4
(entry + 0x18, 0x7C0903A6), // mtctr r0
(entry + 0x1C, 0x4E800420), // bctr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x4E800020), // blr
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x4E800020), // blr
(case2 + 0x00, 0x3860000B), // li r3, 11
(case2 + 0x04, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1),
(table + 0x08, case2));
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with
{
MaxBytes = 0x80,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { entry }
});
Assert.Contains("case 0x80000030u:", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_JumpTableBoundSurvivesCopyOfIndex()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x28;
const uint case1 = entry + 0x30;
const uint case2 = entry + 0x38;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x28030002), // cmplwi r3, 2
(entry + 0x04, 0x7C7F1B78), // mr r31, r3
(entry + 0x08, 0x2C040001), // cmpwi r4, 1
(entry + 0x0C, 0x5460103A), // slwi r0, r3, 2
(entry + 0x10, 0x3D808000), // lis r12, 0x8000
(entry + 0x14, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x18, 0x7C0C002E), // lwzx r0, r12, r0
(entry + 0x1C, 0x7C0903A6), // mtctr r0
(entry + 0x20, 0x4E800420), // bctr
(entry + 0x24, 0x4E800020), // blr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x4E800020), // blr
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x4E800020), // blr
(case2 + 0x00, 0x3860000B), // li r3, 11
(case2 + 0x04, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1),
(table + 0x08, case2));
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with
{
MaxBytes = 0x80,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { entry }
});
Assert.Contains("case 0x80000038u:", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_JumpTableBoundAcceptsNonCr0Compare()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x1C;
const uint case1 = entry + 0x24;
const uint case2 = entry + 0x2C;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x28830002), // cmplwi cr1, r3, 2
(entry + 0x04, 0x5463103A), // slwi r3, r3, 2
(entry + 0x08, 0x3D808000), // lis r12, 0x8000
(entry + 0x0C, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x10, 0x7C0C182E), // lwzx r0, r12, r3
(entry + 0x14, 0x7C0903A6), // mtctr r0
(entry + 0x18, 0x4E800420), // bctr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x4E800020), // blr
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x4E800020), // blr
(case2 + 0x00, 0x3860000B), // li r3, 11
(case2 + 0x04, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1),
(table + 0x08, case2));
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with
{
MaxBytes = 0x80,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { entry }
});
Assert.Contains("case 0x8000002Cu:", result.CxxCode, StringComparison.Ordinal);
}
}
@@ -167,6 +167,31 @@ public sealed class GqrConstantPropagationTests
instruction => instruction is IrCall { Target: "PPC_PsqLKnown_00070007" });
}
[Fact]
public void UnknownEntryRootsDependOnlyOnTheCallGraph()
{
IrFunction Calls(params uint[] targets) => Function(targets
.Select(target => (IrInstruction)new IrCall(string.Empty, $"0x{target:X8}", Array.Empty<IrValue>()))
.Append(new IrReturn(null))
.ToArray());
var functions = new Dictionary<uint, IrFunction>
{
[0x80001000] = Calls(),
[0x80002000] = Calls(0x80003000),
[0x80003000] = Calls(),
[0x80004000] = Calls(0x80005000),
[0x80005000] = Calls(0x80004000)
};
var summaries = functions.ToDictionary(pair => pair.Key, pair => GqrFunctionSummary.Create(pair.Value));
uint[] candidates = [0x80002000, 0x80003000, 0x80004000, 0x80005000];
var roots = GqrInterproceduralAnalysis.SelectUnknownEntryRoots(summaries, [0x80001000u], candidates);
var reversed = GqrInterproceduralAnalysis.SelectUnknownEntryRoots(summaries, [0x80001000u], candidates.Reverse());
Assert.Equal(new uint[] { 0x80001000, 0x80002000, 0x80004000 }, roots.Order());
Assert.Equal(roots.Order(), reversed.Order());
}
[Fact]
public void InterproceduralAnalysisIntersectsConflictingCallers()
{
@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Translator.Core;
using Translator.Core.Analysis;
using Xunit;
@@ -22,24 +23,32 @@ public sealed class GuestAddressTableTests
Assert.Throws<InvalidDataException>(() => GuestRegions.FromGameId(gameId));
[Fact]
public void MapsEachColumnAndRejectsUnknownAndDuplicateRows()
public void MapsRangesAndRejectsUnknownAndOverlappingRanges()
{
WithTable("""
// comment
MKW_GUEST_ADDRESS(801A9E84, 801A9DE4, 801A9DA4) // OSCreateThread
MKW_GUEST_ADDRESS(80386664, 80382304, 80385FE4)
MKW_REGION_RANGE(NtscU, 801A9000, 801A9FFF, -0xA0)
MKW_REGION_RANGE(NtscU, 80386000, 80386FFF, -0x4360)
MKW_REGION_RANGE(NtscJ, 801A9000, 801A9FFF, -0xE0)
MKW_REGION_RANGE(NtscJ, 80386000, 80386FFF, -0x680)
""", table =>
{
Assert.Equal(2, table.Count);
Assert.Equal(4, table.Count);
Assert.Equal(0x801A9E84u, table.Map(0x801A9E84, GuestRegion.Pal));
Assert.Equal(0x801A9DE4u, table.Map(0x801A9E84, GuestRegion.NtscU));
Assert.Equal(0x80385FE4u, table.Map(0x80386664, GuestRegion.NtscJ));
Assert.Throws<InvalidDataException>(() => table.Map(0x80000000, GuestRegion.Pal));
Assert.Equal(0x80000000u, table.Map(0x80000000, GuestRegion.Pal));
Assert.Throws<InvalidDataException>(() => table.Map(0x80000000, GuestRegion.NtscU));
Assert.Throws<InvalidDataException>(() => table.Map(0x801AA000, GuestRegion.NtscJ));
});
WithTable("""
MKW_GUEST_ADDRESS(801A9E84, 801A9DE4, 801A9DA4)
MKW_GUEST_ADDRESS(801a9e84, 801A9DE4, 801A9DA4)
MKW_REGION_RANGE(NtscU, 801A9000, 801A9FFF, -0xA0)
MKW_REGION_RANGE(NtscU, 801A9FFF, 801AAFFF, -0xA0)
""", _ => { }, expectFailure: true);
WithTable("MKW_REGION_RANGE(Pal, 801A9000, 801A9FFF, +0x0)\n", _ => { }, expectFailure: true);
WithTable("MKW_REGION_RANGE(1, 801A9000, 801A9FFF, +0x0)\n", _ => { }, expectFailure: true);
WithTable("MKW_REGION_RANGE(NtscU, 801A9000, 801A9FFF, 0xA0)\n", _ => { }, expectFailure: true);
WithTable("MKW_REGION_RANGE(NtscU, 801A9000, 801A9FFF, -0x80000000)\n", _ => { }, expectFailure: true);
WithTable("// no rows\n", _ => { }, expectFailure: true);
}
@@ -53,8 +62,8 @@ public sealed class GuestAddressTableTests
[new RuntimeNativeAbiEntry(0x801A9E84, ["r3"], [])],
[new RuntimeNativeEffectEntry(0x801A9E84, contract, true)]);
WithTable("""
MKW_GUEST_ADDRESS(801A9E84, 801A9DE4, 801A9DA4)
MKW_GUEST_ADDRESS(80386664, 80382304, 80385FE4)
MKW_REGION_RANGE(NtscU, 801A9000, 801A9FFF, -0xA0)
MKW_REGION_RANGE(NtscU, 80386000, 80386FFF, -0x4360)
""", table =>
{
var ntscU = index.ForRegion(table, GuestRegion.NtscU);
@@ -67,9 +76,9 @@ public sealed class GuestAddressTableTests
});
}
// Every address the runtime registers must have a row, or a regional build cannot place it.
// Every address the runtime registers must have a counterpart, or a regional build cannot place it.
[Fact]
public void RepositoryTableCoversEveryRuntimeRegistration()
public void RepositoryMapCoversEveryRuntimeRegistration()
{
var root = ProjectPathsForTests.FindRepositoryRoot();
var sources = Path.Combine(root, "runtime", "src");
@@ -80,29 +89,92 @@ public sealed class GuestAddressTableTests
Assert.Equal(index.Registrations.Length, index.ForRegion(table, region).Registrations.Length);
}
// Distinct PAL identities must stay distinct and in the same order in every region.
[Fact]
public void RepositoryTableIsInjectiveAndOrderedPerRegion()
// Every function both MAPs name uniquely, and every verified data address, must port exactly.
[Theory]
[InlineData(GuestRegion.NtscU, "mkwii-ntsc-u")]
[InlineData(GuestRegion.NtscJ, "mkwii-ntsc-j")]
public void RepositoryMapAgreesWithRegionMapsAndDataAnchors(GuestRegion region, string project)
{
var root = ProjectPathsForTests.FindRepositoryRoot();
var path = Path.Combine(root, "runtime", "include", "guest_abi", "guest_address_table.inc");
var rows = File.ReadLines(path)
.Where(static line => line.StartsWith("MKW_GUEST_ADDRESS(", StringComparison.Ordinal))
.Select(static line => line["MKW_GUEST_ADDRESS(".Length..line.IndexOf(')')]
.Split(',').Select(static value => Convert.ToUInt32(value.Trim(), 16)).ToArray())
.ToArray();
Assert.NotEmpty(rows);
for (var column = 0; column < 3; column++)
var table = GuestAddressTable.Load(GuestAddressTable.DefaultPath(Path.Combine(root, "runtime", "src")));
var pal = UniqueNames(Path.Combine(root, "projects", "mkwii", "MAP.txt"));
var target = UniqueNames(Path.Combine(root, "projects", project, "MAP.txt"));
var compared = 0;
foreach (var (name, address) in pal)
{
for (var i = 1; i < rows.Length; i++)
Assert.True(rows[i - 1][column] < rows[i][column],
$"column {column} row {i}: 0x{rows[i - 1][column]:X8} >= 0x{rows[i][column]:X8}");
if (!target.TryGetValue(name, out var expected))
continue;
Assert.True(expected == table.Map(address, region), $"{name} 0x{address:X8} should port to 0x{expected:X8}");
compared++;
}
Assert.True(compared > 15000, $"only {compared} functions compared");
foreach (var line in File.ReadLines(Path.Combine(root, "projects", project, "data_addresses.txt")))
{
var parts = line.Split('#')[0].Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2 || parts[1] == "?" || parts[0].Length != 8)
continue;
var address = Convert.ToUInt32(parts[0], 16);
Assert.Equal(Convert.ToUInt32(parts[1], 16), table.Map(address, region));
}
}
// Guest functions the runtime calls directly must be real functions in every region.
[Fact]
public void RuntimeGuestCallsAreFunctionEntriesInEveryRegion()
{
var root = ProjectPathsForTests.FindRepositoryRoot();
var table = GuestAddressTable.Load(GuestAddressTable.DefaultPath(Path.Combine(root, "runtime", "src")));
var thirdParty = $"{Path.DirectorySeparatorChar}third_party{Path.DirectorySeparatorChar}";
var calls = Directory.EnumerateFiles(Path.Combine(root, "runtime"), "*.*", SearchOption.AllDirectories)
.Where(path => (path.EndsWith(".cpp", StringComparison.Ordinal) || path.EndsWith(".h", StringComparison.Ordinal)) &&
!path.Contains(thirdParty, StringComparison.Ordinal))
.SelectMany(static path => Regex.Matches(File.ReadAllText(path), @"MKW_GUEST_FUNC\(\s*([0-9A-Fa-f]{8})\s*\)")
.Select(static match => Convert.ToUInt32(match.Groups[1].Value, 16)))
.Distinct()
.ToArray();
Assert.NotEmpty(calls);
var entries = new Dictionary<GuestRegion, HashSet<uint>>
{
[GuestRegion.Pal] = MapEntries(Path.Combine(root, "projects", "mkwii", "MAP.txt")),
[GuestRegion.NtscU] = MapEntries(Path.Combine(root, "projects", "mkwii-ntsc-u", "MAP.txt")),
[GuestRegion.NtscJ] = MapEntries(Path.Combine(root, "projects", "mkwii-ntsc-j", "MAP.txt")),
};
foreach (var pal in calls)
{
foreach (var (region, starts) in entries)
{
var address = table.Map(pal, region);
Assert.True(starts.Contains(address), $"MKW_GUEST_FUNC({pal:X8}) is not a {region} function entry (0x{address:X8})");
}
}
}
private static HashSet<uint> MapEntries(string mapPath) =>
File.ReadLines(mapPath)
.Select(static line => line.Trim())
.Where(static line => line.Length >= 8 && !line.StartsWith('#'))
.Select(static line => Convert.ToUInt32(line[..8], 16))
.ToHashSet();
private static Dictionary<string, uint> UniqueNames(string mapPath)
{
var entries = File.ReadLines(mapPath)
.Select(static line => line.Trim())
.Where(static line => line.Length > 9 && !line.StartsWith('#'))
.Select(static line => (Address: Convert.ToUInt32(line[..8], 16), Name: line[9..].Trim()))
.Where(static entry => !entry.Name.StartsWith("0x", StringComparison.Ordinal) &&
!entry.Name.StartsWith("FUN_", StringComparison.Ordinal) &&
!entry.Name.StartsWith("fn_", StringComparison.Ordinal))
.ToArray();
return entries.GroupBy(static entry => entry.Name)
.Where(static group => group.Count() == 1)
.ToDictionary(static group => group.Key, static group => group.Single().Address);
}
private static void WithTable(string contents, Action<GuestAddressTable> check, bool expectFailure = false)
{
var path = Path.Combine(Path.GetTempPath(), $"mkw-guest-table-{Guid.NewGuid():N}.inc");
var path = Path.Combine(Path.GetTempPath(), $"mkw-guest-versions-{Guid.NewGuid():N}.inc");
File.WriteAllText(path, contents);
try
{
@@ -28,7 +28,7 @@ public sealed class ModFunctionDiscoveryTests
ctorEnd: 0,
chunkSize: (uint)(KamekChunk.HeaderSize + code.Length),
codeBlob: code,
commands: []);
commands: [PointerRelocation(0x40, 0x50)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
@@ -368,7 +368,7 @@ public sealed class ModFunctionDiscoveryTests
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
commands: [PointerRelocation(0x20, 0x100)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
@@ -381,7 +381,7 @@ public sealed class ModFunctionDiscoveryTests
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x280, moduleBase + 0x200); // appended pointer table
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
@@ -655,6 +655,450 @@ public sealed class ModFunctionDiscoveryTests
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x43004]);
}
[Fact]
public void DiscoversVtableOnlyTailCallFunctionNextToInstructionThatEncodesItsTail()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x10000];
// Virtual call whose lwz encodes the address of the tail branch below.
WriteU32(code, 0x0FF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x0FF4, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x0FF8, 0x8061000Cu); // lwz r3,0xc(r1)
WriteU32(code, 0x0FFC, 0x8184BB50u); // lwz r12,-0x44b0(r4)
WriteU32(code, 0x1000, 0x7D8903A6u); // mtctr r12
WriteU32(code, 0x1004, 0x4E800421u); // bctrl
WriteU32(code, 0x1008, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x100C, 0x4E800020u); // blr
WriteU32(code, 0xBAF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0xBAF4, 0x7C0802A6u); // mflr r0
WriteU32(code, 0xBAF8, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0xBAFC, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0xBB00, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0xBB04, 0x4E800020u); // blr
// Frameless virtual reached only through the vtable; it ends in a tail call.
WriteU32(code, 0xBB08, 0x38000008u); // li r0,8
WriteU32(code, 0xBB0C, 0x3880FFFFu); // li r4,-1
WriteU32(code, 0xBB10, 0x90832AB4u); // stw r4,0x2ab4(r3)
WriteU32(code, 0xBB14, 0x38A00000u); // li r5,0
WriteU32(code, 0xBB18, 0x80C3072Cu); // lwz r6,0x72c(r3)
WriteU32(code, 0xBB1C, 0x7C0903A6u); // mtctr r0
WriteU32(code, 0xBB20, 0x80860000u); // lwz r4,0(r6)
WriteU32(code, 0xBB24, 0x38C60004u); // addi r6,r6,4
WriteU32(code, 0xBB28, 0x98A401E9u); // stb r5,0x1e9(r4)
WriteU32(code, 0xBB2C, 0x4200FFF4u); // bdnz -0xc
WriteU32(code, 0xBB30, 0x3C80818Au); // lis r4,0x818a
WriteU32(code, 0xBB34, 0x808441F8u); // lwz r4,0x41f8(r4)
WriteU32(code, 0xBB38, 0x80040024u); // lwz r0,0x24(r4)
WriteU32(code, 0xBB3C, 0x5400103Au); // slwi r0,r0,2
WriteU32(code, 0xBB40, 0x7C000034u); // cntlzw r0,r0
WriteU32(code, 0xBB44, 0x5400D97Eu); // srwi r0,r0,5
WriteU32(code, 0xBB48, 0x980328E1u); // stb r0,0x28e1(r3)
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
// Hook target that follows directly.
WriteU32(code, 0xBB54, 0x3CA0818Au); // lis r5,0x818a
WriteU32(code, 0xBB58, 0x80A541F8u); // lwz r5,0x41f8(r5)
WriteU32(code, 0xBB5C, 0x80050024u); // lwz r0,0x24(r5)
WriteU32(code, 0xBB60, 0x5400103Bu); // slwi. r0,r0,2
WriteU32(code, 0xBB64, 0x41820008u); // beq +8
WriteU32(code, 0xBB68, 0x38800001u); // li r4,1
WriteU32(code, 0xBB6C, EncodeBranch(moduleBase + 0xBB6C, 0x805F0DB4u)); // b base code
WriteU32(code, 0xBB70, 0x9421FFC0u); // stwu r1,-0x40(r1)
WriteU32(code, 0xBB74, 0x7C0802A6u); // mflr r0
WriteU32(code, 0xBB78, 0x4E800020u); // blr
WriteU32(code, 0xF000, moduleBase + 0xBB70); // vtable
WriteU32(code, 0xF004, moduleBase + 0xBB08);
var chunk = CodeChunk(
code,
PointerRelocation(0xF000, 0xBB70),
PointerRelocation(0xF004, 0xBB08),
AbsoluteCommand(KamekCommandId.BranchLink, 0x80601234u, 0xBB54));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0xBB08]);
Assert.Equal("BranchLink", starts[moduleBase + 0xBB54]);
Assert.Equal("module data pointer", starts[moduleBase + 0xBB70]);
Assert.False(starts.ContainsKey(moduleBase + 0xBB50));
}
[Fact]
public void IgnoresUnrelocatedInstructionWordThatEncodesTrampolineAddress()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x10000];
WriteU32(code, 0x100, 0x8184BB50u); // lwz r12,-0x44b0(r4)
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0xBB50);
}
[Fact]
public void IgnoresUnrelocatedWordThatEncodesFunctionEpilogueAddress()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x200, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x204, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x208, 0x93C10008u); // stw r30,8(r1)
WriteU32(code, 0x20C, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0x210, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x214, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x218, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
WriteU32(code, 0x224, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0x228, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x22C, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module prologue scan", starts[moduleBase + 0x200]);
Assert.False(starts.ContainsKey(moduleBase + 0x220));
}
[Fact]
public void DoesNotSplitFunctionAtAnyAddressEncodedByUnrelocatedInstructions()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x3000];
WriteU32(code, 0x2000, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x2004, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x2008, 0x90010014u); // stw r0,0x14(r1)
for (var offset = 0x200C; offset < 0x20F0; offset += 4)
{
WriteU32(code, offset, 0x38630001u); // addi r3,r3,1
}
WriteU32(code, 0x20F0, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0x20F4, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0x20F8, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x20FC, 0x4E800020u); // blr
// lwz r12,disp(r4) for every word of the function body.
for (var offset = 0x2004; offset < 0x2100; offset += 4)
{
WriteU32(code, 0x100 + (offset - 0x2004), 0x81840000u | (uint)offset);
}
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.Contains(starts, start => start.Address == moduleBase + 0x2000);
Assert.DoesNotContain(starts, start => start.Address > moduleBase + 0x2000 && start.Address < moduleBase + 0x2100);
}
[Fact]
public void DiscoversRelocatedPointerToFunctionThatOnlyLooksLikeAnEpilogue()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, moduleBase + 0x220);
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
WriteU32(code, 0x224, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x228, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0x100, 0x220)), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x220]);
}
[Fact]
public void DiscoversConditionallyWrittenModulePointer()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x80];
WriteU32(code, 0x40, moduleBase + 0x50);
WriteU32(code, 0x4C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x50, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x40, 0x50, 0));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresSkippedConditionalPointerWrite()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4), expected value did not match
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
WriteU32(code, 0x300, 0x38600002u); // li r3,2
WriteU32(code, 0x304, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x100, 0x300, 0x12345678u));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
}
[Fact]
public void IgnoresPointerRelocationOverwrittenByLaterWrite()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // Write32 result applied after the relocation
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
WriteU32(code, 0x2FC, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x300, 0x38600002u); // li r3,2
WriteU32(code, 0x304, 0x4E800020u); // blr
var chunk = CodeChunk(
code,
PointerRelocation(0x100, 0x300),
RelativeCommand(KamekCommandId.Write32, 0x100, 0x81840220u));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
}
[Theory]
[InlineData(KamekCommandId.Addr16Lo, 2u)]
[InlineData(KamekCommandId.Addr16Ha, 2u)]
[InlineData(KamekCommandId.Addr16Hi, 2u)]
[InlineData(KamekCommandId.Addr16Lo, 0u)]
[InlineData(KamekCommandId.Addr16Ha, 0u)]
[InlineData(KamekCommandId.Addr16Hi, 0u)]
[InlineData(KamekCommandId.Write32, 0u)]
[InlineData(KamekCommandId.CondWrite32, 0u)]
public void IgnoresWordWhoseOnlyRelocationIsNotAPointerWrite(KamekCommandId id, uint addressDelta)
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(id, 0x100 + addressDelta, 0x220));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220 && start.Reason == "module data pointer");
}
[Fact]
public void IgnoresAbsolutePointerWriteWhenGatingModuleWords()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
// A base-game pointer patch whose address field happens to match the module offset.
var chunk = CodeChunk(code, AbsoluteCommand(KamekCommandId.WritePointer, 0x100, 0x300));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
}
[Fact]
public void HonorsPointerRelocationInLastWordOfCode()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0xFC, moduleBase + 0x50);
WriteU32(code, 0x50, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0xFC, 0x50)), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresUnrelocatedModuleAddressInLastWordOfCode()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0xFC, moduleBase + 0x50);
WriteU32(code, 0x50, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x50);
}
[Fact]
public void ScansFirstAppendedWordWithoutRelocations()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x200];
WriteU32(relocatedImage, 0x50, 0x4E800020u); // blr
WriteU32(relocatedImage, 0x100, moduleBase + 0x50); // first appended word
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresUnrelocatedCodeWordPointingIntoAppendedImage()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x1FC, 0x38630001u); // addi r3,r3,1
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x200);
}
[Fact]
public void StillRejectsRelocatedPointersToData()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x200];
"control\0"u8.ToArray().CopyTo(code, 0x80);
WriteU32(code, 0x20, moduleBase + 0x80);
WriteU32(code, 0x24, moduleBase + 0x180); // zero word
var chunk = CodeChunk(code, PointerRelocation(0x20, 0x80), PointerRelocation(0x24, 0x180));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x80);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x180);
}
[Fact]
public void DiscoversCtorEntriesWithAndWithoutPointerRelocations()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0x40, moduleBase + 0x60);
WriteU32(code, 0x44, moduleBase + 0x70);
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x60, 0x38600000u); // li r3,0
WriteU32(code, 0x64, 0x4E800020u); // blr
WriteU32(code, 0x6C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x70, 0x38600001u); // li r3,1
WriteU32(code, 0x74, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0x40,
ctorEnd: 0x48,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: [PointerRelocation(0x40, 0x60)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("ctor", starts[moduleBase + 0x60]);
Assert.Equal("ctor", starts[moduleBase + 0x70]);
}
[Fact]
public void DiscoversHookAndRelocationBranchTargets()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x200];
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x60, 0x38600001u); // li r3,1
WriteU32(code, 0x64, 0x4E800020u); // blr
WriteU32(code, 0x9C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0xA0, 0x38600002u); // li r3,2
WriteU32(code, 0xA4, 0x4E800020u); // blr
WriteU32(code, 0xDC, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0xE0, 0x38600003u); // li r3,3
WriteU32(code, 0xE4, 0x4E800020u); // blr
WriteU32(code, 0x100, EncodeBranch(moduleBase + 0x100, 0x80001000u) | 1u); // bl base code
var chunk = CodeChunk(
code,
AbsoluteCommand(KamekCommandId.Branch, 0x80001234u, 0x60),
AbsoluteCommand(KamekCommandId.BranchLink, 0x80001238u, 0xA0),
RelativeCommand(KamekCommandId.Rel24, 0x100, 0xE0));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("Branch", starts[moduleBase + 0x60]);
Assert.Equal("BranchLink", starts[moduleBase + 0xA0]);
Assert.Equal("Rel24", starts[moduleBase + 0xE0]);
}
private static KamekChunk CodeChunk(byte[] code, params KamekCommand[] commands) =>
new(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: commands);
private static KamekCommand RelativeCommand(KamekCommandId id, uint offset, params uint[] arguments) =>
new(0, ((uint)id << 24) | offset, id, false, offset, arguments);
private static KamekCommand AbsoluteCommand(KamekCommandId id, uint address, params uint[] arguments) =>
new(0, ((uint)id << 24) | 0x00FFFFFEu, id, true, address, arguments);
private static KamekCommand PointerRelocation(uint offset, uint target) =>
RelativeCommand(KamekCommandId.Addr32, offset, target);
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
@@ -152,4 +152,69 @@ public sealed class PairedFlowCodeGenTests
Assert.Contains("SetCRFloatResident(cr, 0, f31.d, f1.d);", code, StringComparison.Ordinal);
Assert.DoesNotContain("SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f31.d)", code, StringComparison.Ordinal);
}
[Fact]
public void ResolvedScalarLoadEndsPairedStateAcrossEdge()
{
var function = new IrFunction("resolved_load_clears_paired", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("f5", "PPC_PsMul", new[] { IrValue.Register("f1"), IrValue.Register("f2") }),
new IrResolveGuestMemoryRange("range", IrValue.Register("r3"), 0, 8, true, false),
new IrResolvedLoad("f5", "range", new IrAddress("r3", 0), 0, 8),
new IrJump("next")
}),
new IrBasicBlock("next", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("f5"), IrValue.Register("f1"), false),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f5"] = ValueRepresentation.Float64,
["r3"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification("resolved_load_clears_paired", ValueRepresentation.Void);
var code = new CxxLinearCodeGenerator().Emit(
0x80006008, new SsaTransformer().Convert(function), signature, types);
Assert.Contains("SetCRFloatResident(cr, 0, f5.d, f1.d);", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsToScalarInline(f5.d)", code, StringComparison.Ordinal);
}
[Fact]
public void ResolvedPsqLoadStaysPairedAcrossEdge()
{
var function = new IrFunction("resolved_psq_load_paired", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrResolveGuestMemoryRange("range", IrValue.Register("r3"), 0, 8, true, false),
new IrResolvedPsqLoad("f5", "range", IrValue.Register("r3"), 0, 0, 0, 0u),
new IrJump("next")
}),
new IrBasicBlock("next", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("f5"), IrValue.Register("f1"), false),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f5"] = ValueRepresentation.Float64,
["r3"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification("resolved_psq_load_paired", ValueRepresentation.Void);
var code = new CxxLinearCodeGenerator().Emit(
0x8000600C, new SsaTransformer().Convert(function), signature, types);
Assert.Contains("SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f5.d), f1.d);", code, StringComparison.Ordinal);
}
}