Compare commits

..

9 Commits

Author SHA1 Message Date
patchzyy 2d7fe7450c replace per-address region table with range map 2026-10-21 20:29:38 +02:00
patchzyy 0ae0d6391c Keyboard input for friend code entry (#327)
* keyboard input for friend code entry

* add new guest imports to synthetic test
2026-10-21 18:54:05 +02:00
patchzyy e9f1fdd77c 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>
2026-10-21 17:37:44 +02:00
patchzyy ad95c5c7da Run GX on its own thread (#325) 2026-10-21 13:35:51 +02:00
patchzyy dfe32f61e1 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
2026-10-20 12:21:46 +02:00
patchzyy e3f5e3a3ec build for x86-64-v2 on older linux cpus (#319) 2026-10-19 17:32:11 +02:00
patchzyy 0321842569 Simplify controller settings menu (#317)
* simplify controller settings menu

* platform pairing hint
2026-10-19 16:02:59 +02:00
patchzyy 35b552782f Update settings_overlay.cpp (#257) 2026-10-19 15:22:54 +02:00
patchzyy 2f30417d61 move aurora to the aurora-wiicompiled submodule (#315) 2026-10-19 14:35:01 +02:00
71 changed files with 661 additions and 2669 deletions
-18
View File
@@ -278,24 +278,6 @@ $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.9.3</Version>
<Version>0.2.35</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+73 -32
View File
@@ -21,8 +21,7 @@ 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,
[ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3'
[int]$Parallel = 0
)
$ErrorActionPreference = 'Stop'
@@ -34,6 +33,65 @@ 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
@@ -101,8 +159,6 @@ 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'
@@ -151,9 +207,8 @@ 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) / 0.5)))
[math]::Floor($memoryGiB / 2)))
$globalJobs = [math]::Max($translatedJobs, [Environment]::ProcessorCount)
}
@@ -230,18 +285,11 @@ 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.
$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) {
& $translator @(
'check-base-mod-awareness', '--project', $project, '--profile', 'retro-rewind',
'--translation-output-metadata', $baseMetadata, '--code-pul', $retroCodePul
) | Write-Host
if ($LASTEXITCODE -ne 0) {
Write-MkwBuildStep 'retranslate-base' 'The base translation is stale; retranslating the base game for the new Code.pul'
$reuseBase = $false
}
@@ -264,14 +312,12 @@ 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' `
-TimeoutSeconds $translatorTimeoutSeconds
) 'Translating the user-owned base game' -StepId 'translate-base'
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' `
-TimeoutSeconds $translatorTimeoutSeconds
) 'Creating the local base translation manifest' -StepId 'emit-base-manifest'
if (-not [string]::IsNullOrWhiteSpace($TranslationFingerprint)) {
[ordered]@{ SchemaVersion = 1; TranslationFingerprint = $TranslationFingerprint; DolSha256 = $pins.DolSha256 } |
@@ -301,12 +347,11 @@ try {
throw 'Retro Rewind requires either -RetroWfcOfflineDirectory or -SkipRetroWfcPayload.'
}
Invoke-Checked $translator $translateModArguments 'Translating the selected Retro Rewind Code.pul' `
-StepId 'translate-mod' -TimeoutSeconds $translatorTimeoutSeconds
-StepId 'translate-mod'
}
Invoke-Checked $translator @('generate-data-init', '--project', $project) `
'Generating local game data initialization' -StepId 'generate-data-init' `
-TimeoutSeconds $translatorTimeoutSeconds
'Generating local game data initialization' -StepId 'generate-data-init'
$shardArgs = @(
'emit-build-shards', '--project', $project, '--base-metadata', $baseMetadata,
@@ -318,8 +363,7 @@ 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' `
-TimeoutSeconds $translatorTimeoutSeconds
Invoke-Checked $translator $shardArgs 'Preparing local native build shards' -StepId 'emit-build-shards'
# 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
@@ -351,16 +395,13 @@ try {
}
}
$dependencies = Join-Path $Workspace 'Dependencies'
$nativePrebuilt = Resolve-MkwNativePrebuiltDirectory $dependencies $toolchainBin `
$nativePrebuilt = Resolve-NativePrebuiltDirectory $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", "-DMKW_X86_CPU_PROFILE=$X86CpuProfile")
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs")
Invoke-Checked $cmake $configure 'Configuring the bundled native toolchain' -StepId 'configure-native'
if (-not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint)) {
[ordered]@{ SchemaVersion = 1; NativeToolchainFingerprint = $NativeToolchainFingerprint } |
+54 -118
View File
@@ -157,8 +157,7 @@ function Resolve-MkwProjectForAssets([string]$Workspace, [string]$AssetsDirector
}
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true,
[int]$TimeoutSeconds = 0) {
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
<#
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
@@ -167,7 +166,6 @@ 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" }
@@ -176,17 +174,7 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
# Backslashes directly before the closing quote would otherwise escape it.
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
})
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) {
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
@@ -248,121 +236,69 @@ function Get-MkwDawnRuntimeSha256([string]$DependenciesDirectory) {
return Get-MkwFileSha256 $runtime
}
function ConvertTo-MkwHex([byte[]]$Bytes) {
return [BitConverter]::ToString($Bytes).Replace('-', '').ToLowerInvariant()
}
function Get-MkwSourceTreeFingerprint([string]$Directory, [string[]]$ExcludedTopDirectories = @()) {
function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
<#
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.
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.
#>
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)
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('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
try {
$builder = [Text.StringBuilder]::new()
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")
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")
}
return ConvertTo-MkwHex $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
} finally { $sha.Dispose() }
}
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
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
}
function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
<#
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.
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.
#>
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 ''
}
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()
try {
$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 ''
$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")
}
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
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
} finally { $sha.Dispose() }
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
}
function ConvertTo-MkwCMakePath([string]$Path) {
@@ -411,9 +347,9 @@ function Get-MkwNativeConfigureArguments {
}
}
# 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)" }
if (-not [string]::IsNullOrWhiteSpace($NativePrebuiltDirectory)) {
$arguments += "-DMKW_NATIVE_PREBUILT_DIR=$(ConvertTo-MkwCMakePath $NativePrebuiltDirectory)"
}
$arguments += $AdditionalArguments
return $arguments
+14 -57
View File
@@ -33,33 +33,28 @@ 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')
@@ -67,50 +62,42 @@ $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' })
}
@@ -119,7 +106,6 @@ $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 = @()
}
@@ -142,20 +128,11 @@ 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) {
$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
}
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
$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..."
@@ -165,21 +142,11 @@ 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
$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"
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
}
function Expand-Package([string]$Archive, [string]$Target) {
@@ -203,34 +170,25 @@ 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
Install-Package $package (Join-Path $Destination $package.Name)
$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)"
}
$cppWinRt = Join-Path $Destination 'cppwinrt'
$cppWinRtCurrent = (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) -and
(Test-Stamp $cppWinRt (Get-Stamp $cppWinRtTool))
if (-not $cppWinRtCurrent) {
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
Install-Package $cppWinRtTool $toolRoot $compiler
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
}
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
@@ -241,7 +199,6 @@ if (-not $cppWinRtCurrent) {
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'
}
+2 -3
View File
@@ -5,8 +5,7 @@ param(
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$StageDirectory = 'build/recomp-test',
[ValidateRange(1, 64)] [int]$Parallel = 3,
[ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3'
[ValidateRange(1, 64)] [int]$Parallel = 3
)
$ErrorActionPreference = 'Stop'
@@ -137,7 +136,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', "-DMKW_X86_CPU_PROFILE=$X86CpuProfile")
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
@@ -1,99 +0,0 @@
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,8 +74,6 @@ 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,8 +60,7 @@ internal sealed class LocalBuildService
BuildProfile.RetroRewind => "retro-rewind",
_ => "both"
},
"-OutputDirectory", outputDirectory,
"-X86CpuProfile", HostCpuRequirement.SelectBuildProfile() == X86CpuProfile.V2 ? "v2" : "v3"
"-OutputDirectory", outputDirectory
};
if (baseOutputDirectory is not null)
{
@@ -64,8 +64,6 @@ 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,7 +36,6 @@ 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",
@@ -995,57 +994,6 @@ 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(
+3 -17
View File
@@ -80,26 +80,12 @@ 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
# 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
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
assets="$workspace/Assets"; generated="$workspace/generated"
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.
@@ -178,7 +164,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=$(printf '%s\n' "$(mkw_toolchain_identity "$workspace" "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin")" "$cc_bin" "$cxx_bin" | mkw_sha256)
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" clang clang++ "$cmake_bin" "$ninja_bin")
if [[ -d "$native_build" && "$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
printf 'MKWCBUILD: The toolchain or a pinned dependency changed; rebuilding the native build from scratch\n'
rm -rf "$native_build"
@@ -186,7 +172,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="$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"
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
-6
View File
@@ -37,10 +37,6 @@ 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"
@@ -55,7 +51,6 @@ 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>
@@ -64,7 +59,6 @@ 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,28 +41,6 @@ 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"
+1 -11
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=12583, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29419
# Verdicts: PROVEN=12178, REFERENCED=12573, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29409
800018a8 0x800018a8
80001d50 0x80001d50
80001d6c 0x80001d6c
@@ -10702,14 +10702,6 @@
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
@@ -20509,7 +20501,6 @@
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
@@ -21399,7 +21390,6 @@
806f1e54 Objects::HwanwanBall::LoadRoute
806f1e58 Objects::HwanwanBall::GetSubFileName
806f1e64 Objects::HwanwanBall::GetPropertiesBitfield
806f1e6c Objects::HwanwanBall::__dt(thunk)
806f1e74 0x806f1e74
806f2024 0x806f2024
806f2194 0x806f2194
+7 -12
View File
@@ -33,14 +33,9 @@
],
[
"0x80021BA8",
"0x80225F14",
"0x80225F1C",
"0x80021B08"
],
[
"0x80225F14",
"0x802261F8",
"0x80243F18"
],
[
"0x802261F8",
"0x802402E0",
@@ -613,13 +608,13 @@
],
[
"0x806CE828",
"0x806D02C4",
"0x806D02BC",
"0x806C229C"
],
[
"0x806D02C4",
"0x806D02BC",
"0x806D28F4",
"0x806EA1A4"
"0x806EA19C"
],
[
"0x806D2908",
@@ -678,13 +673,13 @@
],
[
"0x806E95B0",
"0x806EC7C8",
"0x806EC7C0",
"0x806EEC5C"
],
[
"0x806EC7C8",
"0x806EC7C0",
"0x806ED53C",
"0x806D856C"
"0x806D8564"
],
[
"0x806ED53C",
+1 -15
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 -mllvm -hot-cold-split=true -w -pipe)
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
endfunction()
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
@@ -293,21 +293,8 @@ 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")
@@ -318,7 +305,6 @@ 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)
+1 -7
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)
// Hand-written registration macros, given the PAL address. Generated code never registers
// Three hand-written registration macros, given the PAL address. Generated code never registers
// per function; every translated function reaches the registry via the bulk
// BulkTranslatedFunctionRecord tables in the translator's *_registration TUs.
#define REGISTER_TRANSLATED_FUNCTION(address, fn) \
@@ -168,9 +168,3 @@ 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,8 +22,6 @@ 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;
@@ -45,7 +43,6 @@ 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;
};
+1 -7
View File
@@ -4,7 +4,6 @@
#include "settings_overlay.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include "perf_stats.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -143,12 +142,7 @@ inline bool BeginAuroraFrame() {
if (GxWorker::ShouldDefer()) {
// The window half can touch the surface and render targets the worker draws into.
GxWorker::Sync();
bool begun = false;
{
PerfStats::WaitScope wait;
begun = aurora_begin_frame_window();
}
if (!begun) {
if (!aurora_begin_frame_window()) {
return false;
}
GxWorker::Submit([] { GxWorkerBeginFrame(); });
-11
View File
@@ -1,11 +0,0 @@
#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
+9 -44
View File
@@ -291,11 +291,6 @@ 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;
@@ -559,10 +554,7 @@ inline void InvokeDirectCpu(CpuContext* ctx) {
return;
}
if constexpr (IsPureTypedNativeCpuCall<KnownTypedNativeCpuCall<Target>>::value) {
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
return;
} else if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
@@ -589,20 +581,12 @@ 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);
// 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) {
// Indirect jump (bctr without link) - used for switch tables.
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
// Use the caller's context, not the persistent global context!
// This is essential for tail calls via bctr where the 'this' pointer (r3)
// must be passed correctly to the target function.
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
@@ -617,22 +601,12 @@ MKW_PPC_NO_INLINE inline void InvokeIndirectJumpSlow(uint32_t target, CpuContext
ReportMissingIndirectJumpTarget(target, cpu);
}
// Indirect jump (bctr without link) - used for switch tables.
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
// Use the caller's context, not the persistent global context!
// This is essential for tail calls via bctr where the 'this' pointer (r3)
// must be passed correctly to the target function.
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
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;
}
@@ -641,12 +615,3 @@ MKW_PPC_NO_INLINE inline void InvokeIndirectCpuSlow(uint32_t target, CpuContext*
}
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);
}
+5 -6
View File
@@ -9,8 +9,7 @@ MKW_REGION_RANGE(NtscU, 8000808C, 8000ADBB, -0x40)
MKW_REGION_RANGE(NtscU, 8000AF24, 8000B6B3, -0xA0)
MKW_REGION_RANGE(NtscU, 8000B6B4, 8000C173, +0x15994)
MKW_REGION_RANGE(NtscU, 8000C174, 80021BA7, -0xB60)
MKW_REGION_RANGE(NtscU, 80021BA8, 80225F13, -0xA0)
MKW_REGION_RANGE(NtscU, 80225F14, 802261F7, +0x1E004)
MKW_REGION_RANGE(NtscU, 80021BA8, 80225F1B, -0xA0)
MKW_REGION_RANGE(NtscU, 802261F8, 802402DF, -0x384)
MKW_REGION_RANGE(NtscU, 802402E0, 80240E17, +0x3F1C)
MKW_REGION_RANGE(NtscU, 80240E18, 80244DD3, -0xEBC)
@@ -133,8 +132,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, 806D02C3, -0xC58C)
MKW_REGION_RANGE(NtscU, 806D02C4, 806D28F3, +0x19EE0)
MKW_REGION_RANGE(NtscU, 806CE828, 806D02BB, -0xC58C)
MKW_REGION_RANGE(NtscU, 806D02BC, 806D28F3, +0x19EE0)
MKW_REGION_RANGE(NtscU, 806D2908, 806D5C5B, -0xEBD0)
MKW_REGION_RANGE(NtscU, 806D5C60, 806D5ED7, +0x16B74)
MKW_REGION_RANGE(NtscU, 806D5ED8, 806DA913, -0xEE40)
@@ -146,8 +145,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, 806EC7C7, +0x56AC)
MKW_REGION_RANGE(NtscU, 806EC7C8, 806ED53B, -0x1425C)
MKW_REGION_RANGE(NtscU, 806E95B0, 806EC7BF, +0x56AC)
MKW_REGION_RANGE(NtscU, 806EC7C0, 806ED53B, -0x1425C)
MKW_REGION_RANGE(NtscU, 806ED53C, 806F62FB, -0x14254)
MKW_REGION_RANGE(NtscU, 806F62FC, 806F7697, -0x4488)
MKW_REGION_RANGE(NtscU, 806F77C4, 806F7A53, +0x1C2B4)
@@ -43,9 +43,6 @@ 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.
@@ -93,20 +90,6 @@ 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,8 +3,6 @@
// 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"
@@ -14,10 +12,6 @@ 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) \
@@ -25,9 +19,3 @@ Entry NextTranslation(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
@@ -1,28 +0,0 @@
#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
+1 -17
View File
@@ -44,7 +44,6 @@ 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;
@@ -277,7 +276,6 @@ 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"
@@ -437,7 +435,6 @@ 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");
@@ -643,11 +640,6 @@ 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");
@@ -839,9 +831,8 @@ 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 true;
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
}
inline bool WiiAutoPairEnabled(bool fallback = true) {
@@ -900,10 +891,6 @@ 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);
}
@@ -1016,9 +1003,6 @@ 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");
}
+1 -2
View File
@@ -84,8 +84,7 @@ 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`, MEM2 to `mem1Path + ".mem2"` and the Kamek overlay to
// `mem1Path + ".kamek"`, logging outcomes to `os`.
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, 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.

Before

Width:  |  Height:  |  Size: 50 KiB

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

Before

Width:  |  Height:  |  Size: 108 KiB

-1
View File
@@ -1 +0,0 @@
1 ICON "WiiCompiled.ico"
+14 -41
View File
@@ -426,10 +426,20 @@ const TranslatedFunctionInfo* TranslatedFunctionRegistry::FindByAddressPtrSlow(u
return nullptr;
}
namespace {
[[noreturn]] void ReportMissingGuestFunction(uint32_t address) {
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no translation registered at 0x" << std::hex << address << std::dec
GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
if (!RegistryFrozen().load(std::memory_order_acquire)) {
lock.lock();
}
for (const auto& entry : Registry()) {
if (entry.address == address && entry.kind == FunctionKind::BaseTranslated && entry.rawCpuInvoker) {
return entry.rawCpuInvoker;
}
}
if (lock.owns_lock()) {
lock.unlock();
}
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no base translation registered at 0x" << std::hex << address << std::dec
<< std::endl;
std::ostringstream message;
message << "The runtime calls the translated function at guest address 0x" << std::hex << address
@@ -441,43 +451,6 @@ namespace {
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;
+1 -7
View File
@@ -15,7 +15,7 @@ AudioBackend& AudioBackend::Instance() {
}
float AudioBackend::EffectiveGainLocked() const {
return m_muted || m_silenced ? 0.0f : m_masterVolume;
return m_muted ? 0.0f : m_masterVolume;
}
void AudioBackend::ApplyGainLocked() {
@@ -36,12 +36,6 @@ 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;
+4 -37
View File
@@ -7,7 +7,6 @@
#include "memory.h"
#include "runtime_product.h"
#include <SDL3/SDL_clipboard.h>
#include <SDL3/SDL_events.h>
#include <atomic>
@@ -28,32 +27,11 @@ 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;
@@ -120,27 +98,16 @@ void HandleEvents(const AuroraEvent* events) noexcept {
if (!events || !g_pageFocused.load(std::memory_order_acquire) || InputBindings::InputBlocked()) {
return;
}
std::vector<int> keys;
std::lock_guard lock(g_pendingMutex);
for (const AuroraEvent* ev = events; ev->type != AURORA_NONE; ++ev) {
if (ev->type != AURORA_SDL_EVENT || ev->sdl.type != SDL_EVENT_KEY_DOWN) {
continue;
}
if (IsPaste(ev->sdl.key)) {
AppendClipboardDigits(keys);
} else if (const std::optional<int> key = KeyValue(ev->sdl.key)) {
keys.push_back(*key);
const std::optional<int> key = KeyValue(ev->sdl.key);
if (key && g_pending.size() < kMaxPendingKeys) {
g_pending.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
+10 -11
View File
@@ -503,20 +503,19 @@ int64_t ConsumeAudioPollDeltaMicros()
using Clock = std::chrono::steady_clock;
static thread_local Clock::time_point lastPoll = Clock::now();
const Clock::time_point now = Clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - lastPoll).count();
lastPoll = now;
if (elapsed < 0) {
elapsed = 0;
}
// Cap the catch-up interval after a debugger pause or host stall; backlog drains via
// kMaxBlocksPerTick per pass. Never return early on a zero delta: two scheduler passes
// can land in the same microsecond and the backlog still needs servicing.
constexpr std::chrono::microseconds kMaxPollDelta{100'000};
const Clock::time_point now = Clock::now();
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - lastPoll);
if (elapsed.count() < 0 || elapsed > kMaxPollDelta) {
lastPoll = now;
return elapsed.count() < 0 ? 0 : kMaxPollDelta.count();
}
// Keep the sub-microsecond remainder for the next poll.
lastPoll += elapsed;
return elapsed.count();
constexpr int64_t kMaxPollDeltaMicros = 100'000;
return std::min(elapsed, kMaxPollDeltaMicros);
}
} // namespace
-2
View File
@@ -3,7 +3,6 @@
#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"
@@ -123,7 +122,6 @@ 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
@@ -1,220 +0,0 @@
#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");
+12 -7
View File
@@ -807,17 +807,21 @@ struct DlMayContainDrawVisitor {
bool OnNop(uint8_t) { return true; }
bool OnBpReg(const uint8_t*, uint32_t) { return true; }
// 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; }
// 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); }
bool OnXfReg(const uint8_t*, uint32_t) { return true; }
bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return false; }
bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return true; }
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; }
// 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; }
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);
}
};
static bool DisplayListMayContainDraw(const uint8_t* data, uint32_t nbytes) {
@@ -1138,7 +1142,8 @@ 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 && gxAttr <= GX_VA_TEX7) {
if (!needsPublish && !IsMatrixIndexAttr(gxAttr) && gxAttr <= GX_VA_TEX7 &&
(type == GX_INDEX8 || type == GX_INDEX16)) {
needsPublish = !AuroraVtxDescMatchesLive(gxAttr, type);
}
if (needsPublish) {
+2 -9
View File
@@ -490,16 +490,9 @@ 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;
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 (TryGetRawDirectFifoVertexSize(vtxFmt, rawVertexSize)) {
const uint32_t packetBytes = 3u + static_cast<uint32_t>(vtxCount) * rawVertexSize;
if (g_hleGxState.fifoByteCount < packetBytes) {
break;
}
+2 -5
View File
@@ -703,11 +703,8 @@ 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 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));
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));
// ============================================================================
// 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::Read8(p+12)});
list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu});
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=nv!=0; g_hleGxState.auroraBeginCalled=false;
g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=true; g_hleGxState.auroraBeginCalled=false;
g_hleGxState.fifoReadOffset = 0;
g_hleGxState.fifoByteCount=0; g_hleGxState.ResetVertex();
}
+7 -43
View File
@@ -1,6 +1,5 @@
#include "gx_worker.h"
#include "perf_stats.h"
#include "runtime_config.h"
#include "runtime_log.h"
@@ -85,37 +84,12 @@ 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) {
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();
}
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) {
@@ -129,19 +103,9 @@ 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) {
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);
}
WaitForTail(g_write + bytes - kCapacity, false);
}
}
@@ -270,10 +234,11 @@ 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() {
if (g_write - g_published >= kPublishBytes) {
Publish();
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();
}
}
@@ -368,7 +333,6 @@ 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;
+8 -23
View File
@@ -1031,46 +1031,31 @@ extern "C" int32_t DVDLowInit_80164848() {
}
PPC_NATIVE_OVERRIDE(80164848, DVDLowInit_80164848, int32_t, (), ());
// DVDLowInquiry(DVDDriveInfo* info, DVDLowCallback cb); reports a retail drive.
extern "C" int32_t DVDLowInquiry_80165A30(uint32_t driveInfoPtr, uint32_t callback)
extern "C" int32_t DVDLowInquiry_80165A30(uint32_t cmdBlockPtr, uint32_t callback)
{
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);
// 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);
}
InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete);
CompleteDvdCancelState();
return 1;
return 1; // Return 1 to indicate the command was successfully issued.
}
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 && Memory::Contains(diskIdPtr, 0x20)) {
for (uint32_t i = 0x07; i < 0x20; ++i) {
Memory::Write8(diskIdPtr + i, 0);
}
if (diskIdPtr) {
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_REPLACEMENT_AS(0x801938FC, NAND_IOS_OpenBody_HLE_801938FC, "NAND_IOS_OpenBody_HLE_801938FC");
REGISTER_NATIVE_FUNCTION_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 PathExists(hostPath) ? ISFS_EINVAL : ISFS_ENOENT;
return ISFS_ENOENT;
}
// NAND names are at most 12 characters; longer host names cannot exist on
-3
View File
@@ -8,7 +8,6 @@
#include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h"
#include "perf_stats.h"
#include <dolphin/vi.h>
@@ -162,7 +161,6 @@ 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;
@@ -586,7 +584,6 @@ 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) {
+6 -28
View File
@@ -57,9 +57,6 @@
#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"
@@ -168,7 +165,6 @@ 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};
@@ -700,7 +696,6 @@ 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;
@@ -775,7 +770,7 @@ void MarkFatalErrorReported() {
}
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept {
if (g_headless || g_fatalPopupShown.exchange(true, std::memory_order_acq_rel)) {
if (g_fatalPopupShown.exchange(true, std::memory_order_acq_rel)) {
return;
}
@@ -1347,25 +1342,12 @@ 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 {
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);
if (argc != 1) {
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
}
RuntimeConfigFile::LogLoadedConfig();
if (!g_headless && RuntimeConfigFile::DiscordPresenceEnabled()) {
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
}
SystemBridge::Initialize();
@@ -1399,8 +1381,7 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
auroraConfig.hasWindowPosition = RuntimeConfigFile::WindowPosition(
auroraConfig.windowPosX, auroraConfig.windowPosY);
auroraConfig.allowJoystickBackgroundEvents = !g_headless;
auroraConfig.startHidden = g_headless;
auroraConfig.allowJoystickBackgroundEvents = true;
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.
@@ -1409,7 +1390,7 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.allowTextureReplacements = RuntimeConfigFile::TextureReplacements(false);
auroraConfig.allowTextureDumps = auroraConfig.allowTextureReplacements &&
RuntimeConfigFile::TextureDumps(false);
auroraConfig.vsync = RuntimeConfigFile::Vsync();
// No vsync knob: aurora always configures a non-blocking present mode.
auroraConfig.desiredBackend = BACKEND_AUTO;
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
ConfigureMkwDynamicAspect(configWidescreen, forceAspect169,
@@ -1479,9 +1460,6 @@ 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
@@ -1,67 +0,0 @@
#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
+7 -55
View File
@@ -75,7 +75,6 @@ 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);
@@ -110,7 +109,6 @@ 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();
@@ -300,8 +298,6 @@ 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);
@@ -382,18 +378,10 @@ 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)) {
g_wiiRemotesSaveFailed = !RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
if (g_wiiRemotesSaveFailed) g_wiiRemotesEnabled = !g_wiiRemotesEnabled;
g_restartPromptOpen = g_wiiRemotesEnabled != g_wiiRemotesAtLaunch && !g_wiiRemotesSaveFailed;
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
}
if (ImGui::IsItemHovered()) {
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::SetTooltip("Takes effect on the next launch. Turn this off to use a Wii U Pro Controller as a gamepad.");
}
ImGui::Unindent();
}
@@ -1201,10 +1189,6 @@ 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);
}
@@ -1215,12 +1199,7 @@ void DrawGraphicsSettings() {
void DrawFpsOverlay() {
AuroraPresentTiming presentTiming{};
aurora_get_present_timing(&presentTiming);
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) {
if (!g_showFps) {
return;
}
@@ -1237,9 +1216,9 @@ void DrawFpsOverlay() {
ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings;
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
if (g_showFps && presentTiming.sampleCount == 0) {
if (presentTiming.sampleCount == 0) {
ImGui::TextUnformatted("FPS: --");
} else if (g_showFps) {
} else {
// Present timing includes the additional frames produced by
// interpolation, so this remains the actual displayed FPS.
ImGui::Text("FPS: %.1f", presentTiming.framesPerSecond);
@@ -1252,17 +1231,6 @@ 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();
}
@@ -1368,19 +1336,6 @@ 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;
@@ -1519,7 +1474,7 @@ void PersistDisplayModeIfChanged() {
void ApplyInputBlockState() {
const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active ||
g_exitPromptOpen || g_restartPromptOpen || g_topBarVisible || StartupScreenVisible();
g_exitPromptOpen || g_topBarVisible || StartupScreenVisible();
PADBlockInput(blocked);
InputBindings::SetInputBlocked(blocked);
}
@@ -1585,9 +1540,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
RuntimeConfigFile::SetAudioMuted(g_audioMuted);
}
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) {
if (g_restartPromptOpen) {
g_restartPromptOpen = false;
} else if (g_exitPromptOpen) {
if (g_exitPromptOpen) {
g_exitPromptOpen = false;
} else if (g_topBarVisible) {
SetTopBarVisible(false);
@@ -1636,7 +1589,6 @@ void Draw() noexcept {
DrawFpsOverlay();
DrawTopBar();
DrawExitPrompt();
DrawRestartPrompt();
controller_mapping_wizard::Draw();
ApplyInputBlockState();
DrawStartupScreen();
+10 -17
View File
@@ -413,18 +413,14 @@ 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)) {
writeRegion("MEM1", kMem1Base, kMem1Size, mem1Path);
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;
}
constexpr uint32_t kMem2Base = 0x90000000u;
for (uint32_t mem2Size : {0x08000000u, 0x04000000u}) {
@@ -433,16 +429,13 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesys
}
std::filesystem::path mem2Path = mem1Path;
mem2Path += ".mem2";
writeRegion("MEM2", kMem2Base, mem2Size, mem2Path);
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;
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,
+106 -134
View File
@@ -139,15 +139,6 @@ 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
@@ -481,6 +472,10 @@ 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
@@ -498,6 +493,7 @@ 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.
@@ -569,15 +565,13 @@ 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)>(queue.Count);
var batch = new List<(uint Address, int Depth, string Name)>(batchSize);
var batchAddresses = new HashSet<uint>();
var candidates = QueueBatch.Dequeue(
queue,
queue.Count,
batchSize,
next => ShouldSkip(next) || !batchAddresses.Add(next.addr));
foreach (var next in candidates)
{
@@ -604,19 +598,23 @@ int RunTranslateRecursive(string[] argsTail)
}
}
// Everything here is speculative, so a "call" decoded from an unreached map entry
// isn't proven code; treat failures as soft, not fatal.
// 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.
void ProcessSpeculativeSeeds()
{
while (speculativeSeeds.Count > 0)
{
var batch = new List<(uint Address, int Depth, string Name)>(speculativeSeeds.Count);
var batch = new List<(uint Address, int Depth, string Name)>(batchSize);
var candidates = QueueBatch.Dequeue(
speculativeSeeds,
speculativeSeeds.Count,
batchSize,
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}")));
}
@@ -654,42 +652,22 @@ 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 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));
var results = new FunctionDiscoveryResult?[batch.Count];
IndexedParallel.For(batch.Count, parallelOptions, i => results[i] = Attempt(batch[i], speculative));
// Re-run only translations that swallowed a newly revealed sibling entry, or direct
// 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)
// 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)
.SelectMany(i => discoveryTargets[i].Concat(swallowedSiblingEntries[i]))
.Where(knownBaseFunctionEntryPoints.Add)
.ToHashSet();
@@ -697,39 +675,49 @@ 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 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));
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();
}
});
}
// 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 (metrics[i] is not { } functionMetrics) continue;
CommitResult(batch[i], functionMetrics, discoveryTargets[i], summaries[i], functionEnds[i], flowCoverage[i]);
if (results[i] is not { } result) continue;
CommitResult(batch[i], result, discoveryTargets[i], summaries[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)
@@ -785,18 +773,16 @@ int RunTranslateRecursive(string[] argsTail)
void CommitResult(
(uint Address, int Depth, string Name) work,
TranslationMetrics functionMetrics,
FunctionDiscoveryResult result,
IReadOnlyList<uint> discoveryTargets,
GqrFunctionSummary summary,
uint functionEnd,
uint[] flowCoverage)
GqrFunctionSummary summary)
{
// File writing moved to parallel section above
totals.Add(functionMetrics);
totals.Add(result.Metrics);
count++;
translated.Add(work.Address);
translatedFunctionEnds[work.Address] = functionEnd;
translatedFlowCoverage[work.Address] = flowCoverage;
translatedFunctionEnds[work.Address] = GetFunctionInstructionEnd(result);
translatedFlowCoverage[work.Address] = CoalesceInstructionRanges(result);
foreach (var target in discoveryTargets)
{
@@ -835,7 +821,6 @@ 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)
{
@@ -879,31 +864,26 @@ int RunTranslateRecursive(string[] argsTail)
if (boundaryRepairAddresses.Length != 0)
{
Console.WriteLine($"[translator] Reanalyzing {boundaryRepairAddresses.Length:N0} function(s) affected by later-discovered boundaries...");
var repairedSummaries = new GqrFunctionSummary[boundaryRepairAddresses.Length];
var repairedEnds = new uint[boundaryRepairAddresses.Length];
var repairedCoverage = new uint[boundaryRepairAddresses.Length][];
var repaired = new FunctionDiscoveryResult[boundaryRepairAddresses.Length];
IndexedParallel.For(boundaryRepairAddresses.Length, parallelOptions, i =>
{
var address = boundaryRepairAddresses[i];
var work = baseWork[address];
var repaired = translator.Value.Discover(address, WithProjectFrontEndPolicy(new TranslationOptions(
repaired[i] = 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] = repairedSummaries[i];
translatedFunctionEnds[address] = repairedEnds[i];
translatedFlowCoverage[address] = repairedCoverage[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]);
}
}
@@ -926,12 +906,6 @@ 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);
@@ -1209,7 +1183,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, maxThreads));
ReportAbiPrewarm(inferredGuestAbi.Value.Prewarm(emissionAddresses));
LogPhase("ABI prewarm");
// State-free entries use the transformed leaf's exact live-at-entry
@@ -1430,7 +1404,6 @@ 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;
@@ -1486,8 +1459,6 @@ 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)
@@ -1546,7 +1517,16 @@ int RunTranslateRecursive(string[] argsTail)
: Path.GetRelativePath(
Path.GetDirectoryName(Path.GetFullPath(outputMetadataPath!))!,
Path.GetFullPath(productionSourceBundlePath)).Replace('\\', '/');
var metadataUnsupportedInstructionCount = unsupportedByIndex.Sum();
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));
});
completedOutputMetadata = BaseTranslationOutputMetadata.Create(
outputMetadata,
new TranslationQualityMetadata(
@@ -1721,7 +1701,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 = ReadBaseMetadataCached(baseTranslationOutputMetadataPath);
var guardedBaseMetadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath);
var codePulSha256 = ChecksumUtilities.Sha256HexOfFile(codePul);
var baseKnowsThisPul = guardedBaseMetadata.ModPatchAwareness is { } awareness &&
awareness.Any(entry => string.Equals(entry.CodePulSha256, codePulSha256, StringComparison.OrdinalIgnoreCase));
@@ -1800,8 +1780,6 @@ 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)
@@ -2010,7 +1988,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
baseManifest,
dvdOverlayRoots,
relocatedModuleImage,
sourceKamekCodeSha1,
SHA1.HashData(selected.CodeBlob),
retroWfcLoweringPlan?.StaticPointers,
selected.CodeSize,
selected.BssSize,
@@ -2099,7 +2077,7 @@ void WriteResolvedDispatchProfile(
var metadataPath = baseTranslationOutputMetadataPath;
if (File.Exists(metadataPath))
{
var metadata = ReadBaseMetadataCached(metadataPath);
var metadata = BaseTranslationOutputMetadataFile.Read(metadataPath);
if (!string.IsNullOrWhiteSpace(metadata.SourceBundlePath))
{
var bundlePath = Path.IsPathRooted(metadata.SourceBundlePath)
@@ -2174,7 +2152,7 @@ void WriteResolvedDispatchProfile(
BaseTranslationOutputMetadata? metadata = null;
if (File.Exists(baseTranslationOutputMetadataPath))
{
metadata = ReadBaseMetadataCached(baseTranslationOutputMetadataPath);
metadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath);
}
if (metadata is null || metadata.Functions.Any(static function => function.Build is null))
{
@@ -2278,21 +2256,12 @@ 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 =>
{
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));
})
group.Any(entry => entry.Kind == "rr") &&
group.OrderByDescending(entry => entry.Priority)
.ThenByDescending(entry => KindRank(entry.Kind))
.First().Kind == "native")
.Select(group => new
{
Address = group.Key,
@@ -2355,9 +2324,12 @@ int EmitModCpp(
: new CompositeGuestFunctionAbiProvider(inferredAbi);
var modTranslator = new FunctionTranslator(modImage, abiProvider);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = maxThreads };
// 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)");
// 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}");
var cppRoot = Path.Combine(outDir, "cpp");
if (Directory.Exists(cppRoot))
@@ -2436,7 +2408,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), maxThreads));
ReportAbiPrewarm(inferredAbi.Prewarm(wave.Select(static work => work.Address)));
return ModTranslationWaveRunner.Translate(() => modTranslator, wave, parallelOptions);
}
@@ -2473,7 +2445,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), maxThreads));
ReportAbiPrewarm(inferredAbi.Prewarm(ordered.Select(static work => work.Address)));
// 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
@@ -2618,7 +2590,7 @@ int EmitModCpp(
{
while (discoveredContinuationQueue.Count > 0)
{
var wave = QueueBatch.Dequeue(discoveredContinuationQueue, discoveredContinuationQueue.Count)
var wave = QueueBatch.Dequeue(discoveredContinuationQueue, waveSize)
.Select(CreateContinuationWork)
.ToArray();
CommitWave(TranslateWave(wave), (result, work) =>
@@ -2637,10 +2609,10 @@ int EmitModCpp(
var emittedModuleStarts = new HashSet<uint>();
while (moduleQueue.Count > 0)
{
var wave = new List<ModTranslationWork>(moduleQueue.Count);
var wave = new List<ModTranslationWork>(waveSize);
foreach (var start in QueueBatch.Dequeue(
moduleQueue,
moduleQueue.Count,
waveSize,
start => !emittedModuleStarts.Add(start.Address)))
{
var name = ModuleFunctionName(start.Address);
@@ -8,7 +8,6 @@
<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,12 +253,6 @@ 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);
@@ -460,21 +454,11 @@ 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,7 +102,12 @@ public static class GuestMemoryRangeLowering
.Any(group => group.Count() >= minimumAccesses))
return function;
var graph = BlockGraph.Build(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 plans = new Dictionary<(int Block, int Instruction), Plan>();
var resolves = new Dictionary<int, List<PlacedResolve>>();
@@ -124,35 +129,24 @@ 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 =>
!graph.Dominates(defined.BlockIndex, candidate.BlockIndex) ||
!dominators[candidate.AccessBlockLabel(function)].Contains(definitionBlock) ||
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 => Compatible(anchor, candidate))
var members = remaining.Where(candidate =>
DominatesAccess(function, dominators, anchor, candidate) &&
!HasInvalidatingBoundaryOnPath(
function, reachability, cyclicBlocks,
invalidators, anchor, candidate))
.OrderBy(candidate => candidate.Access.Index)
.ToList();
// Keep a bounded affine window around the real anchor. A
@@ -254,13 +248,15 @@ public static class GuestMemoryRangeLowering
}
private static bool DominatesAccess(
BlockGraph graph,
IrFunction function,
IReadOnlyDictionary<string, HashSet<string>> dominators,
FunctionAccess anchor,
FunctionAccess consumer)
{
if (anchor.BlockIndex == consumer.BlockIndex)
return anchor.Access.Index <= consumer.Access.Index;
return graph.Dominates(anchor.BlockIndex, consumer.BlockIndex);
return dominators[consumer.AccessBlockLabel(function)].Contains(
anchor.AccessBlockLabel(function));
}
private static bool HasOverlappingOppositeAccess(
@@ -277,25 +273,27 @@ public static class GuestMemoryRangeLowering
}
private static bool HasInvalidatingBoundaryOnPath(
BlockGraph graph,
IrFunction function,
IReadOnlyDictionary<string, HashSet<string>> reachability,
IReadOnlySet<string> cyclicBlocks,
IReadOnlyList<InstructionSite> invalidators,
FunctionAccess anchor,
FunctionAccess consumer)
{
var anchorBlock = anchor.BlockIndex;
var consumerBlock = consumer.BlockIndex;
var consumerReentered = anchorBlock != consumerBlock && graph.Cyclic[consumerBlock];
var anchorLabel = anchor.AccessBlockLabel(function);
var consumerLabel = consumer.AccessBlockLabel(function);
foreach (var invalidator in invalidators)
{
var boundaryBlock = invalidator.BlockIndex;
var afterAnchor = boundaryBlock == anchorBlock
var boundaryLabel = function.Blocks[invalidator.BlockIndex].Label;
var afterAnchor = invalidator.BlockIndex == anchor.BlockIndex
? invalidator.InstructionIndex > anchor.Access.Index
: 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;
: 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;
}
return false;
}
@@ -317,117 +315,54 @@ public static class GuestMemoryRangeLowering
return result;
}
/// <summary>Dominator and reachability sets as bitsets over block ordinals.</summary>
private sealed class BlockGraph
private static Dictionary<string, HashSet<string>> ComputeDominators(IrCfg cfg)
{
private readonly ulong[][] _dominators;
private readonly ulong[][] _reach;
private BlockGraph(ulong[][] dominators, ulong[][] reach, bool[] cyclic)
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
{
_dominators = dominators;
_reach = reach;
Cyclic = cyclic;
}
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;
}
/// <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)
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)
{
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 reachable = new HashSet<string>(comparer);
var pending = new Stack<string>();
pending.Push(start);
while (pending.Count != 0)
{
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 current = pending.Pop();
if (!reachable.Add(current)) continue;
foreach (var successor in cfg.Successors(current)) pending.Push(successor);
}
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);
result[start] = reachable;
}
return result;
}
private static IrInstruction ToResolved(IrInstruction instruction, Access access, Plan plan)
@@ -3,7 +3,6 @@ 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;
@@ -73,9 +72,6 @@ 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
@@ -99,21 +95,14 @@ 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))
{
@@ -162,7 +151,6 @@ 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;
@@ -184,7 +172,6 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
finally
{
inProgress.Remove(address);
state.FrameAddresses.RemoveAt(frameIndex);
tainted = frameTaint[frameIndex];
frameTaint.RemoveAt(frameIndex);
if (tainted && frameIndex > 0)
@@ -287,7 +274,6 @@ 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
@@ -432,102 +418,82 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private HashSet<string> InferArgumentRegisters(IrFunction function, IrCfg cfg)
{
var comparer = StringComparer.OrdinalIgnoreCase;
// 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)
{
if (!ids.TryGetValue(name, out var id))
{
id = names.Count;
ids.Add(name, id);
names.Add(name);
}
return id;
}
var blockUses = new Dictionary<string, HashSet<string>>(comparer);
var blockDefs = new Dictionary<string, HashSet<string>>(comparer);
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++)
foreach (var block in function.Blocks)
{
var block = function.Blocks[b];
var uses = new List<int>();
var defs = new List<int>();
var defined = new HashSet<int>();
var uses = new HashSet<string>(comparer);
var defs = new HashSet<string>(comparer);
foreach (var instruction in block.Instructions)
{
foreach (var use in InstructionRegisterUsesForArgumentInference(instruction))
{
var id = Id(use);
if (!defined.Contains(id))
uses.Add(id);
if (!defs.Contains(use))
{
uses.Add(use);
}
}
foreach (var def in InstructionRegisterDefsForArgumentInference(instruction))
{
var id = Id(def);
if (defined.Add(id))
defs.Add(id);
defs.Add(def);
}
}
useLists[b] = uses;
defLists[b] = defs;
blockUses[block.Label] = uses;
blockDefs[block.Label] = defs;
}
for (var b = 0; b < blockCount; b++)
labelIndex.Add(function.Blocks[b].Label, b);
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);
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 b = blockCount - 1; b >= 0; b--)
for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; blockIndex--)
{
var row = b * words;
for (var w = 0; w < words; w++)
var block = function.Blocks[blockIndex];
var newOut = new HashSet<string>(comparer);
foreach (var successor in cfg.Successors(block.Label))
{
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])
if (liveIn.TryGetValue(successor, out var successorLiveIn))
{
liveIn[row + w] = value;
changed = true;
newOut.UnionWith(successorLiveIn);
}
}
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;
}
}
}
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;
return liveIn.TryGetValue(function.EntryLabel, out var entryLive)
? entryLive
.Where(IsGprOrFprRegister)
.ToHashSet(comparer)
: new HashSet<string>(comparer);
}
private IEnumerable<string> InstructionRegisterUsesForArgumentInference(IrInstruction instruction)
@@ -789,74 +755,45 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private HashSet<string> InferScalarFloatArguments(IrFunction function, IrCfg cfg)
{
var scalarArgs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
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 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 changed = true;
while (changed)
{
changed = false;
for (var b = 0; b < blockCount; b++)
foreach (var block in function.Blocks)
{
int[] newIn;
if (isEntry[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)
{
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];
}
}
ProcessScalarArgumentInstruction(ins, newOut, scalarArgs);
}
var newOut = (int[])newIn.Clone();
foreach (var ins in function.Blocks[b].Instructions)
if (!AliasStatesEqual(newIn, inMap[block.Label]))
{
ProcessScalarArgumentInstruction(ins, keys, newOut, scalarArgs);
}
if (!newIn.AsSpan().SequenceEqual(inMap[b]))
{
inMap[b] = newIn;
inMap[block.Label] = newIn;
changed = true;
}
if (!newOut.AsSpan().SequenceEqual(outMap[b]))
if (!AliasStatesEqual(newOut, outMap[block.Label]))
{
outMap[b] = newOut;
outMap[block.Label] = newOut;
changed = true;
}
}
@@ -865,153 +802,63 @@ 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,
AliasKeys keys,
int[] aliases,
Dictionary<string, HashSet<string>> aliases,
HashSet<string> scalarArgs)
{
switch (ins)
{
case IrAssign assign:
PropagateFloatAlias(assign.Destination, assign.Value, keys, aliases);
PropagateFloatAlias(assign.Destination, assign.Value, aliases);
break;
case IrBinary bin:
MarkScalarUse(bin.Left, keys, aliases, scalarArgs);
MarkScalarUse(bin.Right, keys, aliases, scalarArgs);
KillFloatAlias(bin.Destination, keys, aliases);
MarkScalarUse(bin.Left, aliases, scalarArgs);
MarkScalarUse(bin.Right, aliases, scalarArgs);
KillFloatAlias(bin.Destination, aliases);
break;
case IrLoad load:
KillFloatAlias(load.Destination, keys, aliases);
KillFloatAlias(load.Destination, aliases);
break;
case IrStore store:
if (store.SizeBytes is 4 or 8)
{
MarkScalarUse(store.Source, keys, aliases, scalarArgs);
MarkScalarUse(store.Source, aliases, scalarArgs);
}
break;
case IrCall call:
ProcessScalarArgumentCall(call.Target, call.Arguments, keys, aliases, scalarArgs);
KillFloatAlias(call.Destination, keys, aliases);
ProcessScalarArgumentCall(call.Target, call.Arguments, aliases, scalarArgs);
KillFloatAlias(call.Destination, aliases);
break;
case IrIndirectCall icall:
foreach (var arg in icall.Arguments)
{
MarkScalarUse(arg, keys, aliases, scalarArgs);
MarkScalarUse(arg, aliases, scalarArgs);
}
aliases[F1AliasKey] = -1;
KillFloatAlias(icall.Destination, keys, aliases);
aliases.Remove("f1");
KillFloatAlias(icall.Destination, aliases);
break;
case IrSetCrField setCr:
MarkScalarUse(setCr.Left, keys, aliases, scalarArgs);
MarkScalarUse(setCr.Right, keys, aliases, scalarArgs);
MarkScalarUse(setCr.Left, aliases, scalarArgs);
MarkScalarUse(setCr.Right, aliases, scalarArgs);
break;
case IrPhi phi:
KillFloatAlias(phi.Destination, keys, aliases);
KillFloatAlias(phi.Destination, 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,
AliasKeys keys,
int[] aliases,
Dictionary<string, HashSet<string>> aliases,
HashSet<string> scalarArgs)
{
if (IsGuestCallTarget(target))
@@ -1026,14 +873,14 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
arg.RegisterName != null &&
abi.HasScalarFloatArgument(Base(arg.RegisterName)))
{
MarkScalarUse(arg, keys, aliases, scalarArgs);
MarkScalarUse(arg, aliases, scalarArgs);
}
}
}
if (writesFloatReturnRegister)
{
aliases[F1AliasKey] = -1;
aliases.Remove("f1");
}
return;
}
@@ -1045,7 +892,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
foreach (var arg in arguments)
{
MarkScalarUse(arg, keys, aliases, scalarArgs);
MarkScalarUse(arg, aliases, scalarArgs);
}
}
@@ -1236,8 +1083,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
private static void MarkScalarUse(
IrValue value,
AliasKeys keys,
int[] aliases,
IReadOnlyDictionary<string, HashSet<string>> aliases,
HashSet<string> scalarArgs)
{
if (value.Kind != "register" || value.RegisterName == null)
@@ -1246,22 +1092,24 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
}
var baseName = Base(value.RegisterName);
if (!IsFloatRegister(baseName) || !keys.TryGet(baseName, out var key) || aliases[key] < 0)
if (!IsFloatRegister(baseName) || !aliases.TryGetValue(baseName, out var sources))
{
return;
}
for (var sources = aliases[key]; sources != 0; sources &= sources - 1)
foreach (var source in sources)
{
scalarArgs.Add(AbiFloatArgumentRegisters[BitOperations.TrailingZeroCount(sources)]);
if (AbiFloatArgumentSet.Contains(source))
{
scalarArgs.Add(source);
}
}
}
private static void PropagateFloatAlias(
string destination,
IrValue value,
AliasKeys keys,
int[] aliases)
Dictionary<string, HashSet<string>> aliases)
{
var destBase = Base(destination);
if (!IsFloatRegister(destBase))
@@ -1269,27 +1117,24 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider
return;
}
keys.TryGet(destBase, out var destKey);
if (value.Kind == "register" &&
value.RegisterName != null &&
IsFloatRegister(Base(value.RegisterName)) &&
keys.TryGet(Base(value.RegisterName), out var sourceKey) &&
aliases[sourceKey] >= 0)
aliases.TryGetValue(Base(value.RegisterName), out var srcAliases))
{
aliases[destKey] = aliases[sourceKey];
aliases[destBase] = new HashSet<string>(srcAliases, StringComparer.OrdinalIgnoreCase);
}
else
{
aliases[destKey] = -1;
aliases.Remove(destBase);
}
}
private static void KillFloatAlias(string destination, AliasKeys keys, int[] aliases)
private static void KillFloatAlias(string destination, Dictionary<string, HashSet<string>> aliases)
{
if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination)) &&
keys.TryGet(Base(destination), out var key))
if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination)))
{
aliases[key] = -1;
aliases.Remove(Base(destination));
}
}
@@ -1301,6 +1146,70 @@ 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, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount)
if (!TryFindUpperBound(ordered, loadIndex, indexReg.Name, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount)
{
return false;
}
@@ -381,60 +381,47 @@ internal static class JumpTableDetector
return false;
}
private static bool TryFindUpperBound(IReadOnlyList<PpcInstruction> ordered, int startIndex, PpcRegisterOperand register, out int upperBound)
private static bool TryFindUpperBound(IReadOnlyList<PpcInstruction> ordered, int startIndex, string register, out int upperBound)
{
// 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;
// Preferred: compare directly on the same register used for lwzx indexing.
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
{
var ins = ordered[i];
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))
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)
{
continue;
}
if (IsIndexDerivation(ins) &&
ins.Operands[0] is PpcRegisterOperand dest && dest.Number == tracked &&
ins.Operands[1] is PpcRegisterOperand source)
if (ins.Mnemonic is "cmplwi" or "cmpwi")
{
tracked = source.Number;
continue;
upperBound = imm.Value;
return true;
}
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--)
{
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)
var ins = ordered[i];
if (ins.Operands.Count < 2 || ins.Operands[1] is not PpcImmediateOperand imm)
{
continue;
}
upperBound = bound;
if (ins.Mnemonic is not ("cmplwi" or "cmpwi"))
{
continue;
}
if (imm.Value < 0 || imm.Value >= MaxEntryCount)
{
continue;
}
upperBound = imm.Value;
return true;
}
@@ -442,70 +429,6 @@ 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,11 +83,9 @@ 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. <c>REGISTER_NATIVE_WRAPPER_AS</c> and
/// <c>REGISTER_NATIVE_REPLACEMENT_AS</c> are alias forms too, named by the
/// <c>modOverlay</c> group.
/// the <c>as</c> group tells the two apart.
/// </summary>
[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)]
[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)]
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,24 +69,9 @@ 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,19 +11,7 @@ public sealed record RuntimeNativeRegistration(
string Symbol,
string SourceFile,
bool IsTranslatedOverride,
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
}
bool ExcludesBaseTranslation);
public sealed record RuntimeNativeAbiEntry(
uint Address,
@@ -131,17 +119,7 @@ public static class RuntimeNativeIndexBuilder
{
var source = sourceFile.Content;
foreach (Match match in GeneratedMarkers.NativeFunctionRegistrationPattern().Matches(source))
{
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
});
}
Add(match, match.Groups["symbol"].Value, false, !match.Groups["as"].Success);
foreach (Match match in GeneratedMarkers.TranslatedFunctionRegistrationPattern().Matches(source))
Add(match, match.Groups["symbol"].Value, true, true);
foreach (Match match in GeneratedMarkers.NativeOverridePattern().Matches(source))
@@ -149,11 +127,10 @@ 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,
NativeModOverlay modOverlay = NativeModOverlay.Unspecified) =>
void Add(Match match, string symbol, bool translated, bool excludesBase) =>
registrations.Add(new RuntimeNativeRegistration(
ParseAddress(match.Groups["address"].Value), symbol, sourceFile.RelativePath,
translated, excludesBase, modOverlay));
translated, excludesBase));
}
return registrations
@@ -162,8 +139,7 @@ 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.ModOverlay);
.ThenBy(static registration => registration.ExcludesBaseTranslation);
}
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 = 2;
public const int FormatVersion = 1;
// Bump whenever the scanner or the ABI/effect analyzers can produce different output for the same sources.
public const int ExtractorVersion = 2;
public const int ExtractorVersion = 1;
public const string DefaultFileName = "native-index.json";
private static readonly JsonSerializerOptions Options = new()
@@ -59,15 +59,13 @@ 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,
ModOverlay = registration.ModOverlay.ToString()
ExcludesBaseTranslation = registration.ExcludesBaseTranslation
})
.ToList(),
VoidStubAbis = index.VoidStubAbis
@@ -162,8 +160,7 @@ public static class RuntimeNativeIndexFile
Present(registration.Symbol, "symbol"),
Present(registration.SourceFile, "sourceFile"),
registration.IsTranslatedOverride,
registration.ExcludesBaseTranslation,
ParseModOverlay(Present(registration.ModOverlay, "modOverlay"))))
registration.ExcludesBaseTranslation))
.ToArray(),
Present(document.VoidStubAbis, "voidStubAbis")
.Select(static entry => new RuntimeNativeAbiEntry(
@@ -188,11 +185,6 @@ 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 =>
@@ -252,7 +244,6 @@ 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,14 +30,6 @@ 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;
@@ -63,16 +55,12 @@ 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);
var stream = _encodeStream ??= new MemoryStream();
stream.SetLength(0);
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
writer.Write(Magic);
WriteVarUInt(writer, (uint)CanonicalIrStore.FormatVersion);
@@ -97,11 +85,9 @@ internal static class CanonicalIrBinaryCodec
return stream.ToArray();
}
public static IrFunction Decode(ReadOnlySpan<byte> encoded) => Decode(encoded.ToArray());
public static IrFunction Decode(byte[] encoded)
public static IrFunction Decode(ReadOnlySpan<byte> encoded)
{
using var stream = new MemoryStream(encoded, writable: false);
using var stream = new MemoryStream(encoded.ToArray(), writable: false);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (!reader.ReadBytes(Magic.Length).SequenceEqual(Magic) ||
ReadVarUInt(reader) != CanonicalIrStore.FormatVersion)
@@ -497,52 +497,12 @@ 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>();
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);
var queue = new Queue<uint>(roots);
while (queue.TryDequeue(out var address))
{
if (!reachable.Add(address)) continue;
@@ -550,6 +510,7 @@ public static class GqrInterproceduralAnalysis
if (functions.ContainsKey(target) && !reachable.Contains(target))
queue.Enqueue(target);
}
return reachable;
}
private static void TransferCompact(
@@ -538,122 +538,4 @@ 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,31 +167,6 @@ 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|NEXT)\(\s*([0-9A-Fa-f]{8})\s*\)")
.SelectMany(static path => Regex.Matches(File.ReadAllText(path), @"MKW_GUEST_FUNC\(\s*([0-9A-Fa-f]{8})\s*\)")
.Select(static match => Convert.ToUInt32(match.Groups[1].Value, 16)))
.Distinct()
.ToArray();
@@ -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/NEXT({pal:X8}) is not a {region} function entry (0x{address:X8})");
Assert.True(starts.Contains(address), $"MKW_GUEST_FUNC({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: [PointerRelocation(0x40, 0x50)]);
commands: []);
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: [PointerRelocation(0x20, 0x100)]);
commands: []);
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, 0x280, moduleBase + 0x200); // appended pointer table
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
@@ -655,450 +655,6 @@ 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,69 +152,4 @@ 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,6 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Translator.Core;
using Translator.Core.CodeGen;
using Xunit;
@@ -41,24 +40,14 @@ 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(7, registrations.Length);
Assert.Equal(5, registrations.Length);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation &&
entry.ModOverlay == NativeModOverlay.Unspecified);
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation);
Assert.Contains(registrations, static entry =>
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);
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000030u && entry.IsTranslatedOverride);
Assert.Contains(registrations, static entry =>
@@ -119,8 +108,6 @@ 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);
@@ -139,26 +126,6 @@ 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()
{