Compare commits

..

15 Commits

Author SHA1 Message Date
patchzyy ab929ac42b let wrapper natives run mod overlays 2026-10-25 11:53:31 +01:00
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
70 changed files with 2662 additions and 655 deletions
+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'
}
+3 -2
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'
@@ -136,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(
+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)
+7 -1
View File
@@ -157,7 +157,7 @@ public:
#define MKW_DETAIL_CAT(a, b) a##b
#define MKW_DETAIL_MAKE_UNIQUE(a, b) MKW_DETAIL_CAT(a, b)
// Three hand-written registration macros, given the PAL address. Generated code never registers
// Hand-written registration macros, given the PAL address. Generated code never registers
// per function; every translated function reaches the registry via the bulk
// BulkTranslatedFunctionRecord tables in the translator's *_registration TUs.
#define REGISTER_TRANSLATED_FUNCTION(address, fn) \
@@ -168,3 +168,9 @@ public:
#define REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(::GuestRegion::Translate(address), pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
// A native that calls through with MKW_GUEST_NEXT, so a mod overlay at its address still runs.
#define REGISTER_NATIVE_WRAPPER_AS(address, fn, pretty_name) REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name)
// A native that deliberately takes over its address from any mod overlay there.
#define REGISTER_NATIVE_REPLACEMENT_AS(address, fn, pretty_name) REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name)
+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;
};
+7 -1
View File
@@ -4,6 +4,7 @@
#include "settings_overlay.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include "perf_stats.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -142,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
+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);
}
+6 -5
View File
@@ -9,7 +9,8 @@ MKW_REGION_RANGE(NtscU, 8000808C, 8000ADBB, -0x40)
MKW_REGION_RANGE(NtscU, 8000AF24, 8000B6B3, -0xA0)
MKW_REGION_RANGE(NtscU, 8000B6B4, 8000C173, +0x15994)
MKW_REGION_RANGE(NtscU, 8000C174, 80021BA7, -0xB60)
MKW_REGION_RANGE(NtscU, 80021BA8, 80225F1B, -0xA0)
MKW_REGION_RANGE(NtscU, 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)
@@ -132,8 +133,8 @@ MKW_REGION_RANGE(NtscU, 806C4EF0, 806C63A7, +0x21CD0)
MKW_REGION_RANGE(NtscU, 806C63B0, 806C6B43, +0x21CC8)
MKW_REGION_RANGE(NtscU, 806C6B44, 806CCE8F, -0xABF4)
MKW_REGION_RANGE(NtscU, 806CCE90, 806CE81F, +0x1B97C)
MKW_REGION_RANGE(NtscU, 806CE828, 806D02BB, -0xC58C)
MKW_REGION_RANGE(NtscU, 806D02BC, 806D28F3, +0x19EE0)
MKW_REGION_RANGE(NtscU, 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)
@@ -145,8 +146,8 @@ MKW_REGION_RANGE(NtscU, 806DF7D0, 806DFD13, +0xEBB4)
MKW_REGION_RANGE(NtscU, 806DFD14, 806E3A8B, -0x10CB0)
MKW_REGION_RANGE(NtscU, 806E3A8C, 806E3E1F, +0xAE3C)
MKW_REGION_RANGE(NtscU, 806E3E20, 806E95AF, -0x11044)
MKW_REGION_RANGE(NtscU, 806E95B0, 806EC7BF, +0x56AC)
MKW_REGION_RANGE(NtscU, 806EC7C0, 806ED53B, -0x1425C)
MKW_REGION_RANGE(NtscU, 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)
@@ -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; \
+12
View File
@@ -3,6 +3,8 @@
// Base translations the runtime calls directly, named by PAL identity and resolved through the
// function registry on first use.
// MKW_GUEST_FUNC(801A0620)(ctx);
// A native registered with REGISTER_NATIVE_WRAPPER_AS calls through with MKW_GUEST_NEXT instead,
// so a mod overlay at the same address still runs.
#include "guest_abi/cpu_context.h"
#include "guest_abi/guest_region.h"
@@ -12,6 +14,10 @@ using Entry = void (*)(CpuContext*);
// The base translation at a guest address, also where a native or mod overrides it.
Entry BaseTranslation(uint32_t address);
// The translation a native wrapper sits on top of: the winning mod overlay, else the base
// translation.
Entry NextTranslation(uint32_t address);
} // namespace GuestFunctions
#define MKW_GUEST_FUNC(pal) \
@@ -19,3 +25,9 @@ Entry BaseTranslation(uint32_t address);
static const ::GuestFunctions::Entry entry = ::GuestFunctions::BaseTranslation(MKW_GADDR(pal)); \
return entry; \
}())
#define MKW_GUEST_NEXT(pal) \
(*[] { \
static const ::GuestFunctions::Entry entry = ::GuestFunctions::NextTranslation(MKW_GADDR(pal)); \
return entry; \
}())
+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"
+41 -14
View File
@@ -426,20 +426,10 @@ 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
namespace {
[[noreturn]] void ReportMissingGuestFunction(uint32_t address) {
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no 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
@@ -451,6 +441,43 @@ GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
std::exit(EXIT_FAILURE);
}
} // namespace
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;
}
}
}
ReportMissingGuestFunction(address);
}
GuestFunctions::Entry GuestFunctions::NextTranslation(uint32_t address) {
{
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
if (!RegistryFrozen().load(std::memory_order_acquire)) {
lock.lock();
}
const TranslatedFunctionInfo* best = nullptr;
for (const auto& entry : Registry()) {
if (entry.address == address && entry.kind != FunctionKind::Native && entry.rawCpuInvoker &&
(!best || IsBetterCandidate(entry, *best))) {
best = &entry;
}
}
if (best) {
return best->rawCpuInvoker;
}
}
ReportMissingGuestFunction(address);
}
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;
+37 -4
View File
@@ -7,6 +7,7 @@
#include "memory.h"
#include "runtime_product.h"
#include <SDL3/SDL_clipboard.h>
#include <SDL3/SDL_events.h>
#include <atomic>
@@ -27,11 +28,32 @@ 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;
@@ -98,16 +120,27 @@ void HandleEvents(const AuroraEvent* events) noexcept {
if (!events || !g_pageFocused.load(std::memory_order_acquire) || InputBindings::InputBlocked()) {
return;
}
std::lock_guard lock(g_pendingMutex);
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;
}
const std::optional<int> key = KeyValue(ev->sdl.key);
if (key && g_pending.size() < kMaxPendingKeys) {
g_pending.push_back(*key);
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
+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_NEXT(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_NEXT(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_WRAPPER_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)
+2 -2
View File
@@ -436,7 +436,7 @@ extern "C" void NAND_IOS_OpenBody_HLE_801938FC(CpuContext* ctx) {
ctx->gpr[3] = static_cast<uint32_t>(result);
ctx->gpr[1] = ctx->gpr[1] + 32u;
}
REGISTER_NATIVE_FUNCTION_AS(0x801938FC, NAND_IOS_OpenBody_HLE_801938FC, "NAND_IOS_OpenBody_HLE_801938FC");
REGISTER_NATIVE_REPLACEMENT_AS(0x801938FC, NAND_IOS_OpenBody_HLE_801938FC, "NAND_IOS_OpenBody_HLE_801938FC");
extern "C" int32_t NAND_IOS_Close_HLE(uint32_t fd) {
if (fd == ISFS_DEV_FD) {
@@ -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,
+134 -106
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))
{
@@ -2256,12 +2278,21 @@ void WriteResolvedDispatchProfile(
// functions the runtime must own (hardware, the strap scene). But when the mod's whole point
// is replacing that function, e.g. Retro Rewind's DVDOpen override at 0x8015E2BC, the overlay
// is silently dropped. Name every such address so the tradeoff is a choice, not an accident.
// Wrapper natives still run the overlay, and replacement natives already made that choice.
var nativesKeepingModOverlayDecision = runtimeNativeIndex.Value.Registrations
.Where(static registration => registration.ModOverlay != NativeModOverlay.Unspecified)
.Select(static registration => (registration.Address, registration.Symbol))
.ToHashSet();
var nativeOverriddenModPatches = groups
.Where(group =>
group.Any(entry => entry.Kind == "rr") &&
group.OrderByDescending(entry => entry.Priority)
.ThenByDescending(entry => KindRank(entry.Kind))
.First().Kind == "native")
{
var winner = group.OrderByDescending(entry => entry.Priority)
.ThenByDescending(entry => KindRank(entry.Kind))
.First();
return group.Any(entry => entry.Kind == "rr") &&
winner.Kind == "native" &&
!nativesKeepingModOverlayDecision.Contains((winner.Address, winner.Symbol));
})
.Select(group => new
{
Address = group.Key,
@@ -2324,12 +2355,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 +2436,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 +2473,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 +2618,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 +2637,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)))
@@ -83,9 +83,11 @@ public static partial class GeneratedMarkers
/// <summary>
/// <c>REGISTER_NATIVE_FUNCTION(0x…, symbol)</c> and its <c>_AS</c> alias form;
/// the <c>as</c> group tells the two apart.
/// the <c>as</c> group tells the two apart. <c>REGISTER_NATIVE_WRAPPER_AS</c> and
/// <c>REGISTER_NATIVE_REPLACEMENT_AS</c> are alias forms too, named by the
/// <c>modOverlay</c> group.
/// </summary>
[GeneratedRegex(@"REGISTER_NATIVE_FUNCTION(?<as>_AS)?\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
[GeneratedRegex(@"REGISTER_NATIVE_(?:FUNCTION(?<as>_AS)?|(?<modOverlay>WRAPPER|REPLACEMENT)_AS)\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
public static partial Regex NativeFunctionRegistrationPattern();
[GeneratedRegex(@"REGISTER_TRANSLATED_FUNCTION\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
@@ -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");
@@ -11,7 +11,19 @@ public sealed record RuntimeNativeRegistration(
string Symbol,
string SourceFile,
bool IsTranslatedOverride,
bool ExcludesBaseTranslation);
bool ExcludesBaseTranslation,
NativeModOverlay ModOverlay = NativeModOverlay.Unspecified);
/// <summary>What happens to a mod overlay at a native's address.</summary>
public enum NativeModOverlay
{
/// <summary>Plain registration: the overlay is lost, and reported as such.</summary>
Unspecified,
/// <summary>The native calls through to the overlay (<c>MKW_GUEST_NEXT</c>).</summary>
Wrapper,
/// <summary>The native deliberately takes over the overlay's address.</summary>
Replacement
}
public sealed record RuntimeNativeAbiEntry(
uint Address,
@@ -119,7 +131,17 @@ public static class RuntimeNativeIndexBuilder
{
var source = sourceFile.Content;
foreach (Match match in GeneratedMarkers.NativeFunctionRegistrationPattern().Matches(source))
Add(match, match.Groups["symbol"].Value, false, !match.Groups["as"].Success);
{
var modOverlay = match.Groups["modOverlay"];
Add(match, match.Groups["symbol"].Value, false,
!match.Groups["as"].Success && !modOverlay.Success,
modOverlay.Value switch
{
"WRAPPER" => NativeModOverlay.Wrapper,
"REPLACEMENT" => NativeModOverlay.Replacement,
_ => NativeModOverlay.Unspecified
});
}
foreach (Match match in GeneratedMarkers.TranslatedFunctionRegistrationPattern().Matches(source))
Add(match, match.Groups["symbol"].Value, true, true);
foreach (Match match in GeneratedMarkers.NativeOverridePattern().Matches(source))
@@ -127,10 +149,11 @@ public static class RuntimeNativeIndexBuilder
foreach (Match match in GeneratedMarkers.FatalStubPattern().Matches(source))
Add(match, $"GX_FATAL_STUB_{match.Groups["address"].Value}", false, true);
void Add(Match match, string symbol, bool translated, bool excludesBase) =>
void Add(Match match, string symbol, bool translated, bool excludesBase,
NativeModOverlay modOverlay = NativeModOverlay.Unspecified) =>
registrations.Add(new RuntimeNativeRegistration(
ParseAddress(match.Groups["address"].Value), symbol, sourceFile.RelativePath,
translated, excludesBase));
translated, excludesBase, modOverlay));
}
return registrations
@@ -139,7 +162,8 @@ public static class RuntimeNativeIndexBuilder
.ThenBy(static registration => registration.Symbol, StringComparer.Ordinal)
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
.ThenBy(static registration => registration.IsTranslatedOverride)
.ThenBy(static registration => registration.ExcludesBaseTranslation);
.ThenBy(static registration => registration.ExcludesBaseTranslation)
.ThenBy(static registration => registration.ModOverlay);
}
private static uint ParseAddress(string value) => GuestTargetParser.ParseHexAddress(value);
@@ -14,9 +14,9 @@ namespace Translator.Core;
public static class RuntimeNativeIndexFile
{
public const string Format = "wiicompiled-runtime-native-index";
public const int FormatVersion = 1;
public const int FormatVersion = 2;
// Bump whenever the scanner or the ABI/effect analyzers can produce different output for the same sources.
public const int ExtractorVersion = 1;
public const int ExtractorVersion = 2;
public const string DefaultFileName = "native-index.json";
private static readonly JsonSerializerOptions Options = new()
@@ -59,13 +59,15 @@ public static class RuntimeNativeIndexFile
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
.ThenBy(static registration => registration.IsTranslatedOverride)
.ThenBy(static registration => registration.ExcludesBaseTranslation)
.ThenBy(static registration => registration.ModOverlay)
.Select(static registration => new RegistrationDto
{
Address = FormatAddress(registration.Address),
Symbol = registration.Symbol,
SourceFile = registration.SourceFile,
IsTranslatedOverride = registration.IsTranslatedOverride,
ExcludesBaseTranslation = registration.ExcludesBaseTranslation
ExcludesBaseTranslation = registration.ExcludesBaseTranslation,
ModOverlay = registration.ModOverlay.ToString()
})
.ToList(),
VoidStubAbis = index.VoidStubAbis
@@ -160,7 +162,8 @@ public static class RuntimeNativeIndexFile
Present(registration.Symbol, "symbol"),
Present(registration.SourceFile, "sourceFile"),
registration.IsTranslatedOverride,
registration.ExcludesBaseTranslation))
registration.ExcludesBaseTranslation,
ParseModOverlay(Present(registration.ModOverlay, "modOverlay"))))
.ToArray(),
Present(document.VoidStubAbis, "voidStubAbis")
.Select(static entry => new RuntimeNativeAbiEntry(
@@ -185,6 +188,11 @@ public static class RuntimeNativeIndexFile
private static string FormatAddress(uint address) => $"0x{address:X8}";
private static NativeModOverlay ParseModOverlay(string text) =>
Enum.TryParse<NativeModOverlay>(text, ignoreCase: false, out var value) && Enum.IsDefined(value)
? value
: throw new FormatException($"Unknown modOverlay '{text}'.");
private static uint ParseAddress(string text) => GuestTargetParser.ParseHexAddress(text);
private static T Present<T>(T? value, string name) where T : class =>
@@ -244,6 +252,7 @@ public static class RuntimeNativeIndexFile
public required string SourceFile { get; init; }
public required bool IsTranslatedOverride { get; init; }
public required bool ExcludesBaseTranslation { get; init; }
public required string ModOverlay { get; init; }
}
private sealed class AbiDto
@@ -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()
{
@@ -129,7 +129,7 @@ public sealed class GuestAddressTableTests
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*\)")
.SelectMany(static path => Regex.Matches(File.ReadAllText(path), @"MKW_GUEST_(?:FUNC|NEXT)\(\s*([0-9A-Fa-f]{8})\s*\)")
.Select(static match => Convert.ToUInt32(match.Groups[1].Value, 16)))
.Distinct()
.ToArray();
@@ -145,7 +145,7 @@ public sealed class GuestAddressTableTests
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})");
Assert.True(starts.Contains(address), $"MKW_GUEST_FUNC/NEXT({pal:X8}) is not a {region} function entry (0x{address:X8})");
}
}
}
@@ -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);
}
}
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Translator.Core;
using Translator.Core.CodeGen;
using Xunit;
@@ -40,14 +41,24 @@ public sealed class RuntimeNativeIndexTests
PPC_NATIVE_OVERRIDE_VOID(80000040, Stub, (void), ());
GX_FATAL_STUB(80000050, "Fatal")
// REGISTER_NATIVE_FUNCTION(0x80000060, CommentedOut);
REGISTER_NATIVE_WRAPPER_AS(0x80000070, Wrapper, "wrapper");
REGISTER_NATIVE_REPLACEMENT_AS(0x80000080, Replacement, "replacement");
""");
var registrations = RuntimeNativeIndexBuilder.Build(directory).Registrations;
Assert.Equal(5, registrations.Length);
Assert.Equal(7, registrations.Length);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation);
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation &&
entry.ModOverlay == NativeModOverlay.Unspecified);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation);
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation &&
entry.ModOverlay == NativeModOverlay.Unspecified);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000070u && entry.Symbol == "Wrapper" && !entry.ExcludesBaseTranslation &&
entry.ModOverlay == NativeModOverlay.Wrapper);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000080u && entry.Symbol == "Replacement" && !entry.ExcludesBaseTranslation &&
entry.ModOverlay == NativeModOverlay.Replacement);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000030u && entry.IsTranslatedOverride);
Assert.Contains(registrations, static entry =>
@@ -108,6 +119,8 @@ public sealed class RuntimeNativeIndexTests
var sources = Path.Combine(ProjectPathsForTests.FindRepositoryRoot(), "runtime", "src");
var built = RuntimeNativeIndexBuilder.Build(sources);
Assert.NotEmpty(built.Registrations);
Assert.Contains(built.Registrations, static entry => entry.ModOverlay == NativeModOverlay.Wrapper);
Assert.Contains(built.Registrations, static entry => entry.ModOverlay == NativeModOverlay.Replacement);
Assert.NotEmpty(built.VoidStubAbis);
Assert.NotEmpty(built.Effects);
@@ -126,6 +139,26 @@ public sealed class RuntimeNativeIndexTests
}
}
// A wrapper that calls through with MKW_GUEST_FUNC would silently drop the mod overlay it promises to run.
[Fact]
public void RepositoryWrapperNativesCallThroughToTheNextTranslation()
{
var sources = Path.Combine(ProjectPathsForTests.FindRepositoryRoot(), "runtime", "src");
var wrappers = RuntimeNativeIndexBuilder.Build(sources).Registrations
.Where(static entry => entry.ModOverlay == NativeModOverlay.Wrapper)
.ToArray();
Assert.NotEmpty(wrappers);
foreach (var wrapper in wrappers)
{
var source = File.ReadAllText(Path.Combine(sources, wrapper.SourceFile));
var pal = $"{wrapper.Address:X8}";
Assert.True(Regex.IsMatch(source, $@"MKW_GUEST_NEXT\(\s*{pal}\s*\)", RegexOptions.IgnoreCase),
$"{wrapper.Symbol} is a wrapper but never calls MKW_GUEST_NEXT({pal})");
Assert.False(Regex.IsMatch(source, $@"MKW_GUEST_FUNC\(\s*{pal}\s*\)", RegexOptions.IgnoreCase),
$"{wrapper.Symbol} is a wrapper but calls MKW_GUEST_FUNC({pal})");
}
}
[Fact]
public void ResolveUsesAMatchingIndexAndKeepsWorkingWithoutSources()
{