mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-18 21:01:52 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b4de9fbdb | |||
| 16bfe7b095 | |||
| efedf42035 |
@@ -1,4 +1,5 @@
|
||||
/generated/
|
||||
/runtime/native-index.json
|
||||
/Assets/
|
||||
/assets/
|
||||
/PulsarPacks/
|
||||
|
||||
@@ -112,6 +112,20 @@ Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# back to compiling aurora from source, which is what this package exists to
|
||||
# avoid - and a mismatched one would be an ABI hazard.
|
||||
$portableClangHash = Get-MkwFileSha256 (Join-Path $portableTools 'llvm-mingw\bin\clang-22.exe')
|
||||
# The identity the runtime build compares against the package's runtime archive.
|
||||
function Get-RuntimeSourceFingerprint([string]$RuntimeDirectory, [string]$DependenciesDirectory) {
|
||||
$arguments = @("-DMKW_RUNTIME_DIR=$(ConvertTo-MkwCMakePath $RuntimeDirectory)")
|
||||
$cppWinRt = Join-Path $DependenciesDirectory 'cppwinrt'
|
||||
if (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) {
|
||||
$arguments += "-DMKW_CPPWINRT_INCLUDE_DIR=$(ConvertTo-MkwCMakePath $cppWinRt)"
|
||||
}
|
||||
$arguments += @('-P', (ConvertTo-MkwCMakePath (Join-Path $RuntimeDirectory 'cmake\RuntimeFingerprint.cmake')))
|
||||
$output = @(& (Join-Path $portableTools 'CMake\bin\cmake.exe') @arguments)
|
||||
if ($LASTEXITCODE -ne 0) { throw "Runtime fingerprinting failed with exit code $LASTEXITCODE." }
|
||||
$line = $output | Where-Object { "$_" -match '^runtime_fingerprint=[0-9a-f]{64}$' } | Select-Object -First 1
|
||||
if (-not $line) { throw "RuntimeFingerprint.cmake printed no fingerprint: $($output -join ' ')" }
|
||||
return "$line".Substring('runtime_fingerprint='.Length)
|
||||
}
|
||||
function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
$provenancePath = Join-Path $PackageDirectory 'provenance.json'
|
||||
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) { return $false }
|
||||
@@ -129,6 +143,9 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
# release ships runtime/third_party sources that first-party TUs compile
|
||||
# against, so archives harvested before a vendored change are stale.
|
||||
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
|
||||
# The precompiled runtime archive must be from these runtime sources; a package without one
|
||||
# would make every installation compile the runtime itself.
|
||||
$recordedRuntime = $provenance.PSObject.Properties['RuntimeSourceFingerprint']
|
||||
return ($provenance.CompilerSha256 -eq $portableClangHash) -and
|
||||
($provenance.FlagFingerprint -eq (Get-MkwNativeFlagFingerprint)) -and
|
||||
($null -ne $recordedDawn) -and
|
||||
@@ -136,11 +153,13 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
($null -ne $recordedAurora) -and
|
||||
($recordedAurora.Value -eq (Get-MkwAuroraSourceFingerprint (Join-Path $repoRoot 'aurora-main'))) -and
|
||||
($null -ne $recordedThirdParty) -and
|
||||
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party')))
|
||||
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party'))) -and
|
||||
($null -ne $recordedRuntime) -and
|
||||
($recordedRuntime.Value -eq (Get-RuntimeSourceFingerprint (Join-Path $repoRoot 'runtime') $dependencySources))
|
||||
}
|
||||
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
|
||||
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
|
||||
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
|
||||
Write-Host '[0/6] Building the precompiled aurora, third-party and runtime package...'
|
||||
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
|
||||
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
|
||||
@@ -222,6 +241,22 @@ Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -Destina
|
||||
[IO.Directory]::CreateDirectory((Join-Path $workspace 'Dependencies')) | Out-Null
|
||||
foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependencySources $name) (Join-Path $workspace "Dependencies\$name") }
|
||||
|
||||
# The translation identity covers the runtime's native registrations through this index (see
|
||||
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
|
||||
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
|
||||
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
|
||||
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --out $nativeIndex
|
||||
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $nativeIndex 'Runtime native index'
|
||||
# A staged runtime that fingerprints differently from the shipped archive would make every
|
||||
# installation silently compile the runtime from source.
|
||||
$stagedRuntimeFingerprint = Get-RuntimeSourceFingerprint (Join-Path $workspace 'runtime') (Join-Path $workspace 'Dependencies')
|
||||
$packagedRuntimeFingerprint = (Get-Content -LiteralPath (Join-Path $workspace 'Dependencies\native_prebuilt\provenance.json') -Raw |
|
||||
ConvertFrom-Json).RuntimeSourceFingerprint
|
||||
if ($stagedRuntimeFingerprint -ne $packagedRuntimeFingerprint) {
|
||||
throw "The staged runtime ($stagedRuntimeFingerprint) does not match the precompiled runtime archive ($packagedRuntimeFingerprint)."
|
||||
}
|
||||
|
||||
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
|
||||
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
|
||||
|
||||
|
||||
@@ -208,13 +208,14 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
||||
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
||||
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
||||
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
||||
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
||||
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
||||
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
|
||||
Returns '' when absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
$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 {
|
||||
@@ -245,13 +246,13 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
||||
(Join-Path $root 'extern') + '\',
|
||||
(Join-Path $root 'build') + '\'
|
||||
)
|
||||
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
|
||||
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
|
||||
# hashing them would make the two sides disagree by construction.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
# 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) })
|
||||
-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()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Builds the redistributable precompiled aurora + third-party package: aurora (~43% of local build
|
||||
# CPU time) and vendored Crypto++ are identical for every user, so this configures runtime/ like
|
||||
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure, and harvests the
|
||||
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
|
||||
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure plus the
|
||||
# game-independent runtime archive, and harvests the archives and a generated CMake description
|
||||
# into launcher/artifacts/dependencies/native_prebuilt.
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
|
||||
@@ -87,24 +88,29 @@ try {
|
||||
-AdditionalArguments @("-DMKW_NATIVE_PREBUILT_EXPORT_DIR=$(ConvertTo-MkwCMakePath $exportDirectory)")
|
||||
Invoke-Checked $cmake $configure 'Configuring the aurora/third-party staging build' -LogPrefix 'MKWNP'
|
||||
|
||||
$metaPath = Join-Path $exportDirectory 'meta.txt'
|
||||
Assert-File $metaPath 'Native prebuilt export metadata'
|
||||
$meta = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $metaPath) {
|
||||
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
# The game-independent runtime ships precompiled too; it is outside the probe closure.
|
||||
if (-not $meta.ContainsKey('runtime_fingerprint')) {
|
||||
throw 'The staging configure exported no runtime_fingerprint; the x86_64 package must carry the runtime archive.'
|
||||
}
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products
|
||||
# link, so building it compiles the whole redistributable closure and nothing
|
||||
# else - and a successful link proves the harvested archives are complete.
|
||||
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
|
||||
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
|
||||
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--target', 'mkw_runtime',
|
||||
'--parallel', "$Parallel") 'Building the aurora/third-party closure and the runtime' -LogPrefix 'MKWNP'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# Read the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
$metaPath = Join-Path $exportDirectory 'meta.txt'
|
||||
Assert-File $metaPath 'Native prebuilt export metadata'
|
||||
$meta = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $metaPath) {
|
||||
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
|
||||
$buildNinja = Join-Path $stage 'build.ninja'
|
||||
Assert-File $buildNinja 'Generated build.ninja'
|
||||
@@ -234,6 +240,12 @@ foreach ($name in ($targets.Keys | Sort-Object)) {
|
||||
}
|
||||
if ($sharedImports.Count -eq 0) { throw 'No shared libraries were harvested; the SDL/zlib/libpng DLLs would be missing.' }
|
||||
if ($dawnLinkerFile) { $linkerFileToReference[$dawnLinkerFile] = 'dawn::webgpu_dawn' }
|
||||
$runtimeTarget = $targets['mkw_runtime']
|
||||
if ($null -eq $runtimeTarget -or $runtimeTarget.Type -ne 'STATIC_LIBRARY' -or
|
||||
-not $linkerFileToReference.ContainsKey($runtimeTarget.LinkerFile)) {
|
||||
throw 'The runtime archive (mkw_runtime) was not built and harvested.'
|
||||
}
|
||||
$runtimeArchiveReference = $linkerFileToReference[$runtimeTarget.LinkerFile]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
@@ -297,6 +309,9 @@ $packageLinkItems = @($linkItems | ForEach-Object {
|
||||
if (StartsWithPath $absolute $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $absolute $dependencyRoot)" }
|
||||
throw "Link item is neither a system library nor a harvested archive: $item"
|
||||
})
|
||||
if ($packageLinkItems -contains $runtimeArchiveReference) {
|
||||
throw 'The runtime archive leaked into the aurora link interface.'
|
||||
}
|
||||
|
||||
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile
|
||||
# line, so re-scope it to C++. The runtime also assembles generated .S blobs
|
||||
@@ -333,6 +348,11 @@ $generated.Add('')
|
||||
$generated.Add("set(MKW_NP_DAWN_CONFIG_DIR `"$(ConvertTo-Token $meta['dawn_config_dir'] 'dawn')`")")
|
||||
$generated.Add("set(MKW_NP_DAWN_VERSION `"$($meta['aurora_dawn_version'])`")")
|
||||
$generated.Add('')
|
||||
$generated.Add('# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.')
|
||||
$generated.Add("set(MKW_NP_RUNTIME_ARCHIVE `"$runtimeArchiveReference`")")
|
||||
$generated.Add("set(MKW_NP_RUNTIME_FINGERPRINT `"$($meta['runtime_fingerprint'])`")")
|
||||
$generated.Add("set(MKW_NP_RUNTIME_CPU_PROFILE `"$($meta['runtime_cpu_profile'])`")")
|
||||
$generated.Add('')
|
||||
$generated.Add('# Shared third-party libraries keep imported SHARED targets so that')
|
||||
$generated.Add('# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.')
|
||||
foreach ($import in $sharedImports) {
|
||||
@@ -393,6 +413,9 @@ $provenance = [ordered]@{
|
||||
# release build and the local build can refuse a package aurora has outgrown.
|
||||
AuroraSourceFingerprint = $auroraSourceFingerprint
|
||||
ThirdPartySourceFingerprint = $thirdPartySourceFingerprint
|
||||
# runtime/cmake/RuntimeFingerprint.cmake over the runtime sources the archive was built from.
|
||||
RuntimeSourceFingerprint = $meta['runtime_fingerprint']
|
||||
RuntimeCpuProfile = $meta['runtime_cpu_profile']
|
||||
Sdl3Target = $meta['aurora_sdl3_target']
|
||||
HarvestedLibraryCount = @($linkerFileToReference.Keys).Count
|
||||
Contents = $contents
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
|
||||
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1), Crypto++ and mbed TLS are identical
|
||||
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
|
||||
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
|
||||
# generated CMake description into an output package - the Linux counterpart to
|
||||
# runtime/ against that toolchain, builds just that closure (plus, on x86_64, the game-independent
|
||||
# runtime archive), and harvests the archives plus a generated CMake description into an output
|
||||
# package - the Linux counterpart to
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
@@ -191,12 +192,23 @@ if [[ "${cxx_sha256}" != "${compiler_sha256}" ]]; then
|
||||
compiler_sha256="${compiler_sha256}:${cxx_sha256}"
|
||||
fi
|
||||
|
||||
# The game-independent runtime ships precompiled on x86_64 only; aarch64 compiles it locally.
|
||||
runtime_fingerprint=""
|
||||
if [[ "$arch" == x86_64 ]]; then
|
||||
runtime_fingerprint=$("$cmake_bin" -DMKW_RUNTIME_DIR="$runtime_source" \
|
||||
-P "$runtime_source/cmake/RuntimeFingerprint.cmake" | sed -n 's/^runtime_fingerprint=\([0-9a-f]\{64\}\)$/\1/p')
|
||||
[[ -n "$runtime_fingerprint" ]] || fail "RuntimeFingerprint.cmake printed no runtime fingerprint"
|
||||
fi
|
||||
|
||||
if [[ "$print_fingerprint_only" -eq 1 ]]; then
|
||||
printf 'compiler_sha256=%s\n' "$compiler_sha256"
|
||||
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
|
||||
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
|
||||
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
|
||||
printf 'mbedtls_fingerprint=%s\n' "$mbedtls_fingerprint"
|
||||
if [[ -n "$runtime_fingerprint" ]]; then
|
||||
printf 'runtime_fingerprint=%s\n' "$runtime_fingerprint"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -220,20 +232,30 @@ echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging buil
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
|
||||
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete.
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
meta_path="$export_dir/meta.txt"
|
||||
assert_file "$meta_path" "Native prebuilt export metadata"
|
||||
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete. The runtime archive is outside that closure and built alongside.
|
||||
build_targets=(--target mkw_np_probe)
|
||||
meta_runtime_fingerprint=$(get_meta runtime_fingerprint)
|
||||
if [[ -n "$meta_runtime_fingerprint" ]]; then
|
||||
if [[ -n "$runtime_fingerprint" && "$meta_runtime_fingerprint" != "$runtime_fingerprint" ]]; then
|
||||
fail "The configure's runtime fingerprint ($meta_runtime_fingerprint) differs from RuntimeFingerprint.cmake's ($runtime_fingerprint)"
|
||||
fi
|
||||
build_targets+=(--target mkw_runtime)
|
||||
elif [[ -n "$runtime_fingerprint" ]]; then
|
||||
fail "The staging configure exported no runtime_fingerprint; the $arch package must carry the runtime archive"
|
||||
fi
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" "${build_targets[@]}" --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
build_ninja="$stage_dir/build.ninja"
|
||||
assert_file "$build_ninja" "Generated build.ninja"
|
||||
|
||||
@@ -397,6 +419,13 @@ while IFS='|' read -r name type file linkerfile; do
|
||||
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
|
||||
fi
|
||||
done < "$targets_txt"
|
||||
runtime_archive_ref=""
|
||||
if [[ -n "$meta_runtime_fingerprint" ]]; then
|
||||
runtime_linker=$(awk -F'|' '$1 == "mkw_runtime" && $2 == "STATIC_LIBRARY" { print $4; exit }' "$targets_txt")
|
||||
[[ -n "$runtime_linker" && -f "$runtime_linker" ]] || fail "The runtime archive (mkw_runtime) was not built"
|
||||
runtime_archive_ref=${linker_file_to_reference["$(normalize "$runtime_linker")"]:-}
|
||||
[[ "$runtime_archive_ref" == "@PKG@/lib/"* ]] || fail "The runtime archive (mkw_runtime) was not harvested"
|
||||
fi
|
||||
mbedtls_refs=()
|
||||
mbedtls_txt="$export_dir/mbedtls.txt"
|
||||
assert_file "$mbedtls_txt" "Mbed TLS export targets list"
|
||||
@@ -498,6 +527,11 @@ for item in "${link_items[@]}"; do
|
||||
esac
|
||||
fail "Link item is neither a system library nor a harvested archive: $item"
|
||||
done
|
||||
if [[ -n "$runtime_archive_ref" ]]; then
|
||||
for item in "${package_link_items[@]}"; do
|
||||
[[ "$item" != "$runtime_archive_ref" ]] || fail "The runtime archive leaked into the aurora link interface"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
@@ -562,6 +596,12 @@ generated_cmake="$output_dir/native_prebuilt.cmake"
|
||||
format_cmake_block MKW_NP_MBEDTLS_LIBRARIES "${mbedtls_refs[@]}"
|
||||
echo 'set(MKW_NP_MBEDTLS_INCLUDE_DIR "@PKG@/include/mbedtls")'
|
||||
printf 'set(MKW_NP_MBEDTLS_FINGERPRINT "%s")\n' "$mbedtls_fingerprint"
|
||||
if [[ -n "$runtime_archive_ref" ]]; then
|
||||
# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.
|
||||
printf 'set(MKW_NP_RUNTIME_ARCHIVE "%s")\n' "$runtime_archive_ref"
|
||||
printf 'set(MKW_NP_RUNTIME_FINGERPRINT "%s")\n' "$meta_runtime_fingerprint"
|
||||
printf 'set(MKW_NP_RUNTIME_CPU_PROFILE "%s")\n' "$(get_meta runtime_cpu_profile)"
|
||||
fi
|
||||
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
|
||||
echo ""
|
||||
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
|
||||
@@ -597,12 +637,13 @@ harvested_count=${#linker_file_to_reference[@]}
|
||||
|
||||
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
|
||||
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
|
||||
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" <<'PY'
|
||||
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" \
|
||||
"${runtime_archive_ref:+$meta_runtime_fingerprint}" "${runtime_archive_ref:+$(get_meta runtime_cpu_profile)}" <<'PY'
|
||||
import hashlib, json, os, sys, datetime
|
||||
|
||||
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
|
||||
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
|
||||
harvested_count, mbedtls_fingerprint) = sys.argv[1:]
|
||||
harvested_count, mbedtls_fingerprint, runtime_fingerprint, runtime_cpu_profile) = sys.argv[1:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
@@ -644,6 +685,9 @@ provenance = {
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
if runtime_fingerprint:
|
||||
provenance["RuntimeSourceFingerprint"] = runtime_fingerprint
|
||||
provenance["RuntimeCpuProfile"] = runtime_cpu_profile
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
@@ -127,7 +127,7 @@ foreach ($mandatory in ([regex]::Matches($parameterBlock,
|
||||
}
|
||||
|
||||
# --- The build steps the installer's progress bar acts on.
|
||||
$installProgress = Read-SourceFile (Join-Path $setup 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$installProgress = Read-SourceFile (Join-Path $common 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$knownSteps = Get-QuotedList $installProgress '(?s)class BuildStepIds\s*\{(.*?)\n\}' `
|
||||
'const string \w+ = "([^"]+)";' 'BuildStepIds'
|
||||
$emittedSteps = @([regex]::Matches($localBuild, "(?:-StepId|Write-MkwBuildStep)\s+'([^']+)'") |
|
||||
|
||||
+39
-39
@@ -1,35 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
|
||||
/// existing identifier never changes meaning.
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractToolkit = "extract-toolkit";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string PrepareRetro = "prepare-retro";
|
||||
public const string BuildBase = "build-base";
|
||||
public const string BuildRetro = "build-retro";
|
||||
public const string Publish = "publish";
|
||||
}
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
public interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>Reports to a caller-supplied sink; used by console and maintenance commands.</summary>
|
||||
internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
public sealed class DelegatingInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string> _sink;
|
||||
|
||||
@@ -43,15 +27,20 @@ internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
public sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly string _version;
|
||||
private readonly Action<string>? _log;
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public NdjsonInstallReporter(Action<string>? log = null) => _log = log;
|
||||
public NdjsonInstallReporter(string version, Action<string>? log = null)
|
||||
{
|
||||
_version = version;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
@@ -78,7 +67,7 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
WriteLine(new { type = "result", success = true, version = _version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +95,8 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
public sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string>? _log;
|
||||
|
||||
@@ -126,10 +115,10 @@ internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// <summary>Build step identifiers from the build scripts' <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// id is the contract; LocalBuild.ps1 emits these exactly and Test-PinnedFacts.ps1 fails the release if
|
||||
/// the two sets disagree, so progress never depends on matching reworded English prose.</summary>
|
||||
internal static class BuildStepIds
|
||||
public static class BuildStepIds
|
||||
{
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
@@ -143,16 +132,16 @@ internal static class BuildStepIds
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The bundled
|
||||
/// build script announces every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can
|
||||
/// advance on real events instead of on a timer.
|
||||
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The build
|
||||
/// scripts announce every step they start with an <c>MKWCBUILD:</c> prefix, so the slice can advance
|
||||
/// on real events instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
public sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
/// <summary>The fraction the compile step reaches; beyond it, Ninja's counter drives progress.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
@@ -164,7 +153,7 @@ internal sealed class BuildProgressWindow
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the bundled compiler"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
@@ -209,19 +198,30 @@ internal sealed class BuildProgressWindow
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - an unknown step identifier from a newer script, a plain MKWCBUILD note, or
|
||||
// raw tool output - stays a diagnostic and only feeds the heartbeat below.
|
||||
// Anything else - an unknown step identifier, a plain MKWCBUILD note, or raw tool output -
|
||||
// stays a diagnostic.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
// Ninja prefixes every edge with [done/total]; how many edges there are depends on how much
|
||||
// of the build is already up to date, so follow the counter rather than counting lines.
|
||||
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNinjaProgress(string line, out int done, out int total)
|
||||
{
|
||||
done = total = 0;
|
||||
if (!line.StartsWith('[')) return false;
|
||||
var close = line.IndexOf(']');
|
||||
var slash = line.IndexOf('/');
|
||||
return close > 0 && slash > 0 && slash < close
|
||||
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
|
||||
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
|
||||
&& total > 0 && done <= total;
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
@@ -15,6 +15,12 @@ internal static class DiscTool
|
||||
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
|
||||
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsAlreadyExtracted(manifest, assetsDirectory))
|
||||
{
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Reusing the verified extracted disc", 6);
|
||||
return;
|
||||
}
|
||||
|
||||
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
|
||||
|
||||
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
|
||||
@@ -35,6 +41,8 @@ internal static class DiscTool
|
||||
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Delete(Path.Combine(assetsDirectory, ExtractionMarker));
|
||||
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
|
||||
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
|
||||
|
||||
@@ -61,6 +69,7 @@ internal static class DiscTool
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
|
||||
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
|
||||
File.WriteAllText(Path.Combine(assetsDirectory, ExtractionMarker), ExtractionRecord(dolSha, dataDir));
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
|
||||
}
|
||||
|
||||
@@ -86,6 +95,36 @@ internal static class DiscTool
|
||||
return stdout;
|
||||
}
|
||||
|
||||
private const string ExtractionMarker = ".disc-extracted";
|
||||
|
||||
// Only one disc revision is accepted, so a completed, verified extraction is identical to a fresh one.
|
||||
private static bool IsAlreadyExtracted(ProjectManifest manifest, string assetsDirectory)
|
||||
{
|
||||
var marker = Path.Combine(assetsDirectory, ExtractionMarker);
|
||||
var dol = Path.Combine(assetsDirectory, "main.dol");
|
||||
var rel = Path.Combine(assetsDirectory, "StaticR.rel");
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
if (!File.Exists(marker) || !File.Exists(dol) || !File.Exists(rel) ||
|
||||
!Directory.Exists(Path.Combine(dataDir, "files")))
|
||||
return false;
|
||||
return string.Equals(File.ReadAllText(marker), ExtractionRecord(manifest.DolSha256, dataDir),
|
||||
StringComparison.Ordinal) &&
|
||||
string.Equals(Sha256Of(dol), manifest.DolSha256, StringComparison.Ordinal) &&
|
||||
string.Equals(Sha256Of(rel), manifest.RelSha256, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// The DOL hash plus every extracted file's path and size, so a later deletion or truncation inside
|
||||
// DATA forces a fresh extraction.
|
||||
private static string ExtractionRecord(string dolSha, string dataDir)
|
||||
{
|
||||
var record = new System.Text.StringBuilder(dolSha).Append('\n');
|
||||
foreach (var path in Directory.EnumerateFiles(dataDir, "*", SearchOption.AllDirectories)
|
||||
.Select(path => Path.GetRelativePath(dataDir, path).Replace('\\', '/'))
|
||||
.Order(StringComparer.Ordinal))
|
||||
record.Append(new FileInfo(Path.Combine(dataDir, path)).Length).Append(' ').Append(path).Append('\n');
|
||||
return record.ToString();
|
||||
}
|
||||
|
||||
private static string Sha256Of(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
|
||||
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
|
||||
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
|
||||
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
|
||||
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
|
||||
/// scoping decision).
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
|
||||
// because a later stage happened to estimate a lower number.
|
||||
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
|
||||
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
|
||||
}
|
||||
}
|
||||
|
||||
public void Diagnostic(string line) => Console.Error.WriteLine(line);
|
||||
|
||||
public void Success(string installDirectory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
public void Failure(string error)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = false, error });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
|
||||
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
|
||||
/// </summary>
|
||||
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
|
||||
|
||||
private static void WriteLine(object value)
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
public void Progress(string stage, string message, int percent) =>
|
||||
Console.Out.WriteLine($"[{percent,3}%] {message}");
|
||||
|
||||
public void Diagnostic(string line) => Console.Out.WriteLine(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:<id></c> lines - the id is
|
||||
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
|
||||
/// </summary>
|
||||
internal static class BuildStepIds
|
||||
{
|
||||
public const string BuildTranslator = "build-translator";
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
public const string TranslateBase = "translate-base";
|
||||
public const string EmitBaseManifest = "emit-base-manifest";
|
||||
public const string TranslateMod = "translate-mod";
|
||||
public const string GenerateDataInit = "generate-data-init";
|
||||
public const string EmitBuildShards = "emit-build-shards";
|
||||
public const string ConfigureNative = "configure-native";
|
||||
public const string Compile = "compile";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
|
||||
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
|
||||
/// instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
[
|
||||
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
|
||||
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
|
||||
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
|
||||
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
|
||||
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
private readonly IInstallReporter _reporter;
|
||||
private readonly string _stage;
|
||||
private readonly int _start;
|
||||
private readonly int _end;
|
||||
private double _fraction;
|
||||
private string _message = "Preparing the local build";
|
||||
private int _reportedPercent = -1;
|
||||
|
||||
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_stage = stage;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public void Observe(string line)
|
||||
{
|
||||
var index = line.IndexOf(Marker, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
var text = line[(index + Marker.Length)..].Trim();
|
||||
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
|
||||
{
|
||||
var identifier = text[StepMarker.Length..];
|
||||
var end = identifier.IndexOf(' ');
|
||||
if (end >= 0) identifier = identifier[..end];
|
||||
foreach (var (id, fraction, message) in Steps)
|
||||
{
|
||||
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
|
||||
if (fraction > _fraction)
|
||||
{
|
||||
_fraction = fraction;
|
||||
_message = message;
|
||||
Emit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
|
||||
// feeds the compile progress below.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then ninja prefixes every edge with [done/total].
|
||||
// Follow that counter rather than counting lines: how many lines a build prints depends on
|
||||
// whether aurora and its dependencies are prebuilt (AppImage) or compiled here (macOS), so a
|
||||
// fixed per-line step reached the top long before the game itself compiled.
|
||||
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
|
||||
{
|
||||
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNinjaProgress(string line, out int done, out int total)
|
||||
{
|
||||
done = total = 0;
|
||||
if (!line.StartsWith('[')) return false;
|
||||
var close = line.IndexOf(']');
|
||||
var slash = line.IndexOf('/');
|
||||
return close > 0 && slash > 0 && slash < close
|
||||
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
|
||||
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
|
||||
&& total > 0 && done <= total;
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
if (percent == _reportedPercent) return;
|
||||
_reportedPercent = percent;
|
||||
_reporter.Progress(_stage, _message, percent);
|
||||
}
|
||||
|
||||
private int Interpolate(double fraction) =>
|
||||
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>Stable stage identifiers reported by <c>--progress-json</c>.</summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
var (command, flags) = ParseArgs(args);
|
||||
if (command is null) { PrintUsage(); return 1; }
|
||||
var progressJson = flags.ContainsKey("progress-json");
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) : new ConsoleInstallReporter();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
<Using Include="WiiCompiled.Setup.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -57,7 +57,7 @@ internal static class ConsoleCommands
|
||||
|
||||
public static int VerifyInputs(CommandLine command)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
|
||||
var temp = Path.Combine(Path.GetTempPath(), "mkwc-verify-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
try
|
||||
@@ -107,7 +107,7 @@ internal static class ConsoleCommands
|
||||
}
|
||||
File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}");
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
|
||||
IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log);
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
try
|
||||
@@ -145,7 +145,7 @@ internal static class ConsoleCommands
|
||||
public static int CheckProducts(CommandLine command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
|
||||
var installation = new Installation(string.IsNullOrWhiteSpace(command.InstallDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: Path.GetFullPath(command.InstallDirectory));
|
||||
@@ -198,7 +198,7 @@ internal static class ConsoleCommands
|
||||
}
|
||||
}
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
|
||||
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
var installation = new Installation(installDirectory);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
|
||||
/// existing identifier never changes meaning.
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractToolkit = "extract-toolkit";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string PrepareRetro = "prepare-retro";
|
||||
public const string BuildBase = "build-base";
|
||||
public const string BuildRetro = "build-retro";
|
||||
public const string Publish = "publish";
|
||||
}
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
{
|
||||
// A command line was supplied, so a caller is driving this process. Rejecting the
|
||||
// command with a modal dialog would hang that caller forever.
|
||||
if (progressJson) new NdjsonInstallReporter().Failure(ex.Message);
|
||||
if (progressJson) new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
else Console.Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ internal static class Program
|
||||
// errors raised outside it.
|
||||
if (progressJson)
|
||||
{
|
||||
new NdjsonInstallReporter().Failure(ex.Message);
|
||||
new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1139,10 +1139,10 @@ internal static class SelfTests
|
||||
if (topologyChangedRuntimeAssets == changedRuntimeAssets)
|
||||
throw new Exception("An empty copied runtime-asset directory was omitted from its identity.");
|
||||
|
||||
// Cache-reuse subsets: translator or runtime/src changes (the translator regex-scans it
|
||||
// for native overrides and effect contracts) invalidate translation but not toolchain,
|
||||
// since CMake/Ninja rebuild exactly the affected objects; a compiler change invalidates
|
||||
// only toolchain; a native_prebuilt provenance rewrite invalidates neither.
|
||||
// Cache-reuse subsets: translator or native registration changes invalidate translation
|
||||
// but not toolchain, since CMake/Ninja rebuild exactly the affected objects; a compiler
|
||||
// change invalidates only toolchain; a native_prebuilt provenance rewrite invalidates
|
||||
// neither. Without a current native index, runtime/src itself decides translation.
|
||||
var components = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (components.Compile != afterPatch)
|
||||
throw new Exception("ComputeComponents disagrees with the compile identity.");
|
||||
@@ -1153,15 +1153,36 @@ internal static class SelfTests
|
||||
throw new Exception("A new translator did not change the translation identity.");
|
||||
if (afterNewTranslator.NativeToolchain != components.NativeToolchain)
|
||||
throw new Exception("A new translator needlessly changed the native toolchain identity.");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"),
|
||||
"int main3();");
|
||||
var runtimeMain = Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp");
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
var afterUnindexedRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterUnindexedRuntime.Translation == afterNewTranslator.Translation)
|
||||
throw new Exception("A runtime source change without a native index did not change the translation identity.");
|
||||
|
||||
File.WriteAllText(runtimeMain, "int main();");
|
||||
if (WriteFakeNativeIndex(root, "\"a\"") !=
|
||||
"50b9779591f4b205751361c8899accd362f96ea5dfd32febed1bedca534c523a")
|
||||
throw new Exception("The native index source digest no longer matches the translator's.");
|
||||
var indexed = ToolkitFingerprint.ComputeComponents(root);
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
WriteFakeNativeIndex(root, "\"a\"");
|
||||
var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterNewRuntime.Translation == afterNewTranslator.Translation)
|
||||
throw new Exception("A runtime source change did not change the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain)
|
||||
if (afterNewRuntime.Translation != indexed.Translation)
|
||||
throw new Exception("A runtime source change with unchanged registrations changed the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
|
||||
throw new Exception("A runtime source change needlessly changed the native toolchain identity.");
|
||||
if (afterNewRuntime.Compile == afterNewTranslator.Compile)
|
||||
if (afterNewRuntime.Compile == indexed.Compile)
|
||||
throw new Exception("A runtime source change did not change the compile identity.");
|
||||
File.WriteAllText(runtimeMain, "int main4();");
|
||||
if (ToolkitFingerprint.ComputeComponents(root).Translation == afterNewRuntime.Translation)
|
||||
throw new Exception("A stale native index was trusted for the translation identity.");
|
||||
File.WriteAllText(runtimeMain, "int main3();");
|
||||
WriteFakeNativeIndex(root, "\"a\", \"b\"");
|
||||
afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterNewRuntime.Translation == indexed.Translation)
|
||||
throw new Exception("A native registration change did not change the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
|
||||
throw new Exception("A native registration change needlessly changed the native toolchain identity.");
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", "clang-22.exe"),
|
||||
"clang-v2");
|
||||
var afterNewCompiler = ToolkitFingerprint.ComputeComponents(root);
|
||||
@@ -1214,6 +1235,26 @@ internal static class SelfTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes a native index current for the fake runtime/src; returns its source digest.</summary>
|
||||
private static string WriteFakeNativeIndex(string root, string registrations)
|
||||
{
|
||||
var source = Path.Combine(root, "BuildWorkspace", "runtime", "src");
|
||||
var manifest = new System.Text.StringBuilder();
|
||||
foreach (var (relative, path) in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)
|
||||
.Where(path => path.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".h", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(path => (Path.GetRelativePath(source, path).Replace('\\', '/'), path))
|
||||
.OrderBy(item => item.Item1, StringComparer.Ordinal))
|
||||
manifest.Append(InputValidation.Sha256File(path)).Append(" ").Append(relative).Append('\n');
|
||||
var digest = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(manifest.ToString())))
|
||||
.ToLowerInvariant();
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "native-index.json"),
|
||||
"{\n \"format\": \"wiicompiled-runtime-native-index\",\n \"sourceDigest\": \"" + digest +
|
||||
"\",\n \"registrations\": [" + registrations + "]\n}\n");
|
||||
return digest;
|
||||
}
|
||||
|
||||
private static void CreateFakeToolkit(string root)
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Translator"));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
@@ -12,9 +14,10 @@ namespace WiiCompiled.Setup.Windows;
|
||||
internal static class ToolkitFingerprint
|
||||
{
|
||||
// v6/native-toolchain v2 forces one clean rebuild past a Ninja stale-object bug.
|
||||
// translation v2 added runtime/src, which the translator regex-scans for native overrides.
|
||||
// translation v2 added runtime/src, which the translator regex-scans for native overrides;
|
||||
// v3 replaced it with the native index extracted from it, whenever that index is current.
|
||||
private const string Version = "mkwc-toolkit-compile-v6";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v3";
|
||||
private const string NativeToolchainVersion = "mkwc-toolkit-native-toolchain-v2";
|
||||
private const string PackageVersion = "mkwc-toolkit-package-v1";
|
||||
private const string RuntimeAssetsVersion = "mkwc-product-runtime-assets-v3";
|
||||
@@ -25,20 +28,25 @@ internal static class ToolkitFingerprint
|
||||
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that decide translator output: translator, translation project, runtime sources it
|
||||
/// scans for native override registrations/effect contracts, and the build script's translator
|
||||
/// command lines. Unchanged means the completed base translation can be reused instead of re-run.
|
||||
/// Inputs that decide translator output: translator, translation project, the native override
|
||||
/// registrations/effect contracts it extracts from runtime/src (see
|
||||
/// <see cref="AddNativeRegistrations"/>), and the build script's translator command lines.
|
||||
/// Unchanged means the completed base translation can be reused instead of re-run.
|
||||
/// </summary>
|
||||
private static readonly string[] TranslationPrefixes =
|
||||
[
|
||||
InstalledLayout.ToolkitEntryPrefix + "Translator/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/"
|
||||
];
|
||||
|
||||
private static readonly string[] TranslationFiles =
|
||||
[InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"];
|
||||
|
||||
private const string NativeSourcePrefix = InstalledLayout.WorkspaceEntryPrefix + "runtime/src/";
|
||||
private const string NativeIndexEntry = InstalledLayout.WorkspaceEntryPrefix + "runtime/native-index.json";
|
||||
// The file set the translator's native index source digest covers.
|
||||
private static readonly string[] NativeIndexSourceExtensions = [".cpp", ".h", ".hpp"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that make compiled objects unsafe to reuse: the compiler/CMake/Ninja toolchain, the
|
||||
/// shipped runtime DLLs, and the scripts owning configure flags. Source/dependency changes are
|
||||
@@ -108,14 +116,16 @@ internal static class ToolkitFingerprint
|
||||
if (entries.Count == 0)
|
||||
throw new InvalidDataException($"No toolkit files were found under {root}; the installation is incomplete.");
|
||||
|
||||
var translation = SelectSubset(entries, TranslationPrefixes, TranslationFiles);
|
||||
AddNativeRegistrations(translation, entries, root);
|
||||
return new ToolkitFingerprintComponents(
|
||||
BuildIdentity(Version, entries),
|
||||
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
|
||||
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
|
||||
NativeToolchainFiles));
|
||||
BuildIdentity(TranslationVersion, translation),
|
||||
BuildIdentity(NativeToolchainVersion,
|
||||
SelectSubset(entries, NativeToolchainPrefixes, NativeToolchainFiles)));
|
||||
}
|
||||
|
||||
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
|
||||
private static SortedDictionary<string, string> SelectSubset(SortedDictionary<string, string> entries,
|
||||
string[] prefixes, string[] files)
|
||||
{
|
||||
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
@@ -125,9 +135,61 @@ internal static class ToolkitFingerprint
|
||||
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
|
||||
subset[relative] = hash;
|
||||
}
|
||||
return BuildIdentity(version, subset);
|
||||
return subset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A native-index.json extracted from exactly the shipped runtime/src stands in for those
|
||||
/// sources, minus its source digest, so a runtime change that leaves the registrations alone
|
||||
/// keeps the base translation. A missing, unreadable or stale index keeps the sources instead.
|
||||
/// </summary>
|
||||
private static void AddNativeRegistrations(SortedDictionary<string, string> subset,
|
||||
SortedDictionary<string, string> entries, string root)
|
||||
{
|
||||
var sources = entries.Where(entry => entry.Key.StartsWith(NativeSourcePrefix, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
var registrations = TryHashCurrentNativeIndex(
|
||||
Path.Combine(InstalledLayout.Workspace(root), "runtime", "native-index.json"), sources);
|
||||
if (registrations is not null)
|
||||
{
|
||||
subset[NativeIndexEntry] = registrations;
|
||||
return;
|
||||
}
|
||||
foreach (var (relative, hash) in sources) subset[relative] = hash;
|
||||
}
|
||||
|
||||
private static string? TryHashCurrentNativeIndex(string path, List<KeyValuePair<string, string>> sources)
|
||||
{
|
||||
if (!File.Exists(path)) return null;
|
||||
JsonObject? document;
|
||||
try { document = JsonNode.Parse(File.ReadAllText(path)) as JsonObject; }
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (document?["sourceDigest"] is not JsonValue recorded || !recorded.TryGetValue<string>(out var digest))
|
||||
return null;
|
||||
|
||||
// Mirrors the translator's RuntimeNativeIndexFile.ComputeSourceDigest.
|
||||
var manifest = new StringBuilder();
|
||||
foreach (var (relative, hash) in sources)
|
||||
{
|
||||
if (!NativeIndexSourceExtensions.Any(extension =>
|
||||
relative.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
continue;
|
||||
manifest.Append(hash).Append(" ").Append(relative, NativeSourcePrefix.Length,
|
||||
relative.Length - NativeSourcePrefix.Length).Append('\n');
|
||||
}
|
||||
if (!digest.Equals(Sha256Hex(manifest.ToString()), StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
document.Remove("sourceDigest");
|
||||
return Sha256Hex(document.ToJsonString());
|
||||
}
|
||||
|
||||
private static string Sha256Hex(string text) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of the source-owned files copied verbatim beside every product. This is
|
||||
/// intentionally independent of <see cref="Compute"/>: an asset-only release is published
|
||||
@@ -273,8 +335,7 @@ internal static class ToolkitFingerprint
|
||||
var builder = new StringBuilder(version).Append('\n');
|
||||
foreach (var (relative, hash) in entries)
|
||||
builder.Append(relative).Append('|').Append(hash).Append('\n');
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
|
||||
.ToLowerInvariant();
|
||||
return Sha256Hex(builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
<Using Include="WiiCompiled.Setup.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+32
-11
@@ -127,10 +127,10 @@ mkdir -p "$appdir/usr/toolchain"
|
||||
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
|
||||
|
||||
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
|
||||
# recompiles the aurora/Crypto++/mbed TLS closure with the toolchain above, so this is skipped unless
|
||||
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
|
||||
# the current compiler, flags, source trees or mbed TLS pin.
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time), nor on x86_64
|
||||
# the game-independent runtime. Re-harvesting recompiles that closure with the toolchain above, so
|
||||
# this is skipped unless --print-fingerprint-only (a fast, build-free check) says the existing
|
||||
# package no longer matches the current compiler, flags, source trees or mbed TLS pin.
|
||||
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
|
||||
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
|
||||
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
|
||||
@@ -149,6 +149,8 @@ fields = {
|
||||
"aurora_fingerprint": "AuroraSourceFingerprint",
|
||||
"third_party_fingerprint": "ThirdPartySourceFingerprint",
|
||||
"mbedtls_fingerprint": "MbedTlsFingerprint",
|
||||
# Only printed on x86_64, so a package without the runtime archive is stale there.
|
||||
"runtime_fingerprint": "RuntimeSourceFingerprint",
|
||||
}
|
||||
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
|
||||
PY
|
||||
@@ -174,7 +176,29 @@ done
|
||||
# is a plain developer build directory.
|
||||
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$appdir/workspace/Launcher/"
|
||||
|
||||
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
|
||||
# index, so it is extracted from exactly the bundled runtime sources.
|
||||
echo "Extracting the runtime native index..."
|
||||
"$appdir/usr/bin/translator-cli" emit-native-index \
|
||||
--native-source-dir "$appdir/workspace/runtime/src" \
|
||||
--out "$appdir/workspace/runtime/native-index.json"
|
||||
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
|
||||
|
||||
# A bundled runtime that fingerprints differently from the shipped archive would make every
|
||||
# installation silently compile the runtime from source.
|
||||
if [[ "$appimagetool_arch" == x86_64 ]]; then
|
||||
staged_runtime_fingerprint=$("$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch/bin/cmake" \
|
||||
-DMKW_RUNTIME_DIR="$appdir/workspace/runtime" -P "$appdir/workspace/runtime/cmake/RuntimeFingerprint.cmake" |
|
||||
sed -n 's/^runtime_fingerprint=//p')
|
||||
packaged_runtime_fingerprint=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("RuntimeSourceFingerprint", ""))' \
|
||||
"$appdir/native-prebuilt/provenance.json")
|
||||
if [[ -z "$staged_runtime_fingerprint" || "$staged_runtime_fingerprint" != "$packaged_runtime_fingerprint" ]]; then
|
||||
echo "build-appimage.sh: the bundled runtime ($staged_runtime_fingerprint) does not match the precompiled runtime archive ($packaged_runtime_fingerprint)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
|
||||
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
|
||||
@@ -204,12 +228,9 @@ CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
|
||||
mkdir -p "$CACHE"
|
||||
if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
|
||||
mkdir -p "$CACHE/Launcher"
|
||||
for dir in runtime aurora-main projects; do
|
||||
rm -rf "$CACHE/$dir"
|
||||
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
|
||||
done
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
# Unchanged files keep their timestamps so the incremental build survives the update.
|
||||
source "$HERE/workspace/Launcher/build-cache.sh"
|
||||
mkw_sync_tree "$HERE/workspace" "$CACHE" runtime aurora-main projects Launcher >&2
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Shared incremental-build logic for the Unix setups (Linux AppImage and macOS), sourced by
|
||||
# local-build.sh, local-build-macos.command, the AppImage's AppRun and macos/setup.command.
|
||||
# Mirrors the Windows installer's translation/toolchain identities and timestamp preservation.
|
||||
# Must stay compatible with macOS's bash 3.2.
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
mkw_sha256_cmd=(sha256sum)
|
||||
else
|
||||
mkw_sha256_cmd=(shasum -a 256)
|
||||
fi
|
||||
|
||||
mkw_sha256() {
|
||||
"${mkw_sha256_cmd[@]}" "$@" | awk '{print $1}'
|
||||
}
|
||||
|
||||
# Prints "<sha> <relative path>" for every file under each directory, relative to $1.
|
||||
mkw_digest_tree() {
|
||||
local root=$1; shift
|
||||
(
|
||||
cd "$root" || exit 1
|
||||
local dir
|
||||
for dir in "$@"; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
printf 'tree %s\n' "$dir"
|
||||
find "$dir" -type f -print0 | LC_ALL=C sort -z | xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
|
||||
else
|
||||
printf 'absent %s\n' "$dir"
|
||||
fi
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
# Content-only digest of a file or directory, independent of where it lives (the AppImage mount
|
||||
# path changes every launch).
|
||||
mkw_digest_path() {
|
||||
if [[ -d "$1" ]]; then
|
||||
mkw_digest_tree "$1" . | mkw_sha256
|
||||
elif [[ -f "$1" ]]; then
|
||||
mkw_sha256 < "$1"
|
||||
else
|
||||
printf 'absent\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# Same digest as the translator's RuntimeNativeIndexFile.ComputeSourceDigest. $1 = runtime/src.
|
||||
mkw_native_source_digest() {
|
||||
(
|
||||
cd "$1" || exit 1
|
||||
find . -type f \( -iname '*.cpp' -o -iname '*.h' -o -iname '*.hpp' \) -print0 | LC_ALL=C sort -z |
|
||||
xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
|
||||
) | grep -v ' /dev/null$' | sed 's| \./| |' | mkw_sha256
|
||||
}
|
||||
|
||||
# The native registrations the translator reads from runtime/src. A current native-index.json
|
||||
# stands in for the sources (minus its source digest), so runtime edits that leave the
|
||||
# registrations alone keep the translation; a missing or stale index falls back to the sources.
|
||||
# $1 = workspace.
|
||||
mkw_native_index_identity() {
|
||||
local index="$1/runtime/native-index.json" recorded
|
||||
recorded=$(mkw_read_identity "$index" sourceDigest)
|
||||
if [[ -n "$recorded" && -d "$1/runtime/src" && "$recorded" == "$(mkw_native_source_digest "$1/runtime/src")" ]]; then
|
||||
printf 'native-index %s\n' "$(grep -v '^[[:space:]]*"sourceDigest"[[:space:]]*:' "$index" | mkw_sha256)"
|
||||
else
|
||||
mkw_digest_tree "$1" runtime/src
|
||||
fi
|
||||
}
|
||||
|
||||
# Everything the base translation depends on: game inputs, project, native registrations, the
|
||||
# translator itself and the build script that drives it.
|
||||
# $1 = workspace, $2 = translator file or directory, $3 = build script.
|
||||
mkw_translation_identity() {
|
||||
local workspace=$1 translator=$2 script=$3
|
||||
{
|
||||
printf 'mkw-translation-v2\n'
|
||||
printf 'dol %s\n' "$(mkw_digest_path "$workspace/Assets/main.dol")"
|
||||
printf 'rel %s\n' "$(mkw_digest_path "$workspace/Assets/StaticR.rel")"
|
||||
printf 'translator %s\n' "$(mkw_digest_path "$translator")"
|
||||
printf 'script %s\n' "$(mkw_digest_path "$script")"
|
||||
mkw_digest_tree "$workspace" projects
|
||||
mkw_native_index_identity "$workspace"
|
||||
} | mkw_sha256
|
||||
}
|
||||
|
||||
# Inputs that make compiled objects unsafe to reuse: compiler/CMake/Ninja versions and the
|
||||
# from-source dependency pins. Source changes are excluded; Ninja rebuilds exactly what they touch.
|
||||
# $1 = workspace, remaining = tools (C compiler, C++ compiler, cmake, ninja).
|
||||
mkw_toolchain_identity() {
|
||||
local workspace=$1; shift
|
||||
local tool
|
||||
{
|
||||
printf 'mkw-toolchain-v2\n'
|
||||
for tool in "$@"; do
|
||||
# First line only: clang appends its InstalledDir, which moves with the AppImage mount.
|
||||
printf '%s\n' "$("$tool" --version 2>/dev/null | head -n 1)"
|
||||
done
|
||||
mkw_digest_tree "$workspace" aurora-main/extern aurora-main/cmake
|
||||
printf 'aurora %s\n' "$(mkw_digest_path "$workspace/aurora-main/CMakeLists.txt")"
|
||||
printf 'mbedtls %s\n' "$(mkw_digest_path "$workspace/runtime/cmake/MbedTLSPin.cmake")"
|
||||
} | mkw_sha256
|
||||
}
|
||||
|
||||
# $1 = JSON file, $2 = key. Prints the recorded string value, or nothing.
|
||||
mkw_read_identity() {
|
||||
[[ -f "$1" ]] || return 0
|
||||
grep -o "\"$2\" *: *\"[^\"]*\"" "$1" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true
|
||||
}
|
||||
|
||||
mkw_write_identity() {
|
||||
printf '{"SchemaVersion":1,"%s":"%s"}\n' "$2" "$3" > "$1"
|
||||
}
|
||||
|
||||
# Mirrors each named directory from $1 into $2. Unchanged files are left untouched so their
|
||||
# timestamps (which clang's PCH validation and Ninja compare) survive; changed and new files are
|
||||
# copied fresh and files no longer shipped are removed.
|
||||
mkw_sync_tree() {
|
||||
local source=$1 destination=$2; shift 2
|
||||
local dir relative changed=0 unchanged=0 removed=0
|
||||
for dir in "$@"; do
|
||||
if [[ ! -d "$source/$dir" ]]; then
|
||||
rm -rf "$destination/$dir"
|
||||
continue
|
||||
fi
|
||||
if [[ -e "$destination/$dir" && ! -d "$destination/$dir" ]]; then
|
||||
rm -f "$destination/$dir"
|
||||
fi
|
||||
mkdir -p "$destination/$dir"
|
||||
while IFS= read -r -d '' relative; do
|
||||
relative=${relative#./}
|
||||
if [[ -d "$source/$dir/$relative" ]]; then
|
||||
[[ -d "$destination/$dir/$relative" ]] || { rm -f "$destination/$dir/$relative"; mkdir -p "$destination/$dir/$relative"; }
|
||||
elif [[ -f "$destination/$dir/$relative" ]] && cmp -s "$source/$dir/$relative" "$destination/$dir/$relative"; then
|
||||
unchanged=$((unchanged + 1))
|
||||
else
|
||||
rm -rf "$destination/$dir/$relative"
|
||||
cp "$source/$dir/$relative" "$destination/$dir/$relative"
|
||||
changed=$((changed + 1))
|
||||
fi
|
||||
done < <(cd "$source/$dir" && find . -mindepth 1 -print0)
|
||||
while IFS= read -r -d '' relative; do
|
||||
relative=${relative#./}
|
||||
if [[ ! -e "$source/$dir/$relative" && ( -e "$destination/$dir/$relative" || -L "$destination/$dir/$relative" ) ]]; then
|
||||
[[ -d "$destination/$dir/$relative" ]] || removed=$((removed + 1))
|
||||
rm -rf "$destination/$dir/$relative"
|
||||
fi
|
||||
done < <(cd "$destination/$dir" && find . -mindepth 1 -depth -print0)
|
||||
done
|
||||
printf 'Workspace refreshed: %d changed, %d unchanged, %d removed file(s).\n' "$changed" "$unchanged" "$removed"
|
||||
}
|
||||
@@ -4,11 +4,12 @@ set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
default_workspace=$(cd "$script_dir/.." && pwd)
|
||||
# shellcheck source=build-cache.sh
|
||||
source "$script_dir/build-cache.sh"
|
||||
|
||||
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -126,11 +127,30 @@ if (( builds_retro )); then
|
||||
fi
|
||||
fi
|
||||
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
translation_provenance="$generated/translation-provenance.json"
|
||||
translation_fingerprint=$(mkw_translation_identity "$workspace" "${translator_bin:-$(dirname "${translator_dll:-}")}" "$script_dir/local-build-macos.command")
|
||||
reuse_base=0
|
||||
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" && -f "$metadata" && -f "$manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
if (( reuse_base && builds_retro )); then
|
||||
# A base tree that never saw this Code.pul would bake vanilla code into the modded product.
|
||||
pul_sha=$(mkw_sha256 "$retro_root/Binaries/Code.pul")
|
||||
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$metadata" && \
|
||||
! translator check-base-mod-awareness --project "$project" --profile retro-rewind --translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul"; then
|
||||
reuse_base=0
|
||||
fi
|
||||
fi
|
||||
if (( reuse_base )); then
|
||||
step reuse-base-translation 'Reusing the completed base translation'
|
||||
else
|
||||
rm -f "$translation_provenance"; mkdir -p "$functions" "$manifest_dir"
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
|
||||
fi
|
||||
if (( builds_retro )); then
|
||||
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
|
||||
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
|
||||
@@ -141,10 +161,17 @@ args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-
|
||||
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
|
||||
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
|
||||
|
||||
toolchain_provenance="$native_build/toolchain-provenance.json"
|
||||
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" clang clang++ "$cmake_bin" "$ninja_bin")
|
||||
if [[ -d "$native_build" && "$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
|
||||
printf 'MKWCBUILD: The toolchain or a pinned dependency changed; rebuilding the native build from scratch\n'
|
||||
rm -rf "$native_build"
|
||||
fi
|
||||
step configure-native 'Configuring the native toolchain'
|
||||
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
|
||||
# Aurora's required API even when find_package() succeeds.
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
|
||||
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
|
||||
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
|
||||
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
|
||||
|
||||
+20
-13
@@ -39,7 +39,7 @@ require_command() {
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
mkw_sha256 "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -47,6 +47,8 @@ sha256_of() {
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck source=build-cache.sh
|
||||
source "$script_dir/build-cache.sh"
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
profile=base
|
||||
output_dir=""
|
||||
@@ -275,10 +277,9 @@ else
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
|
||||
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
|
||||
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
|
||||
# a match plus every expected output file present means the previous translation is still good.
|
||||
# Translation cache: this script is the only owner of the reuse decision. The identity (see
|
||||
# build-cache.sh) covers the game inputs, project, native registrations, translator and this script,
|
||||
# matching the Windows installer's translation identity.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (( force_clean_build )); then
|
||||
@@ -286,13 +287,12 @@ if (( force_clean_build )); then
|
||||
rm -rf "$generated" "$base_manifest_dir" "$build"
|
||||
fi
|
||||
|
||||
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
|
||||
translator_identity_input=${translator_bin:-$(dirname "$translator_dll")}
|
||||
translation_fingerprint=$(mkw_translation_identity "$workspace" "$translator_identity_input" "$script_dir/local-build.sh")
|
||||
reuse_base=0
|
||||
if [[ -f "$translation_provenance" ]]; then
|
||||
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
|
||||
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" \
|
||||
&& -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -340,8 +340,7 @@ else
|
||||
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
|
||||
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
|
||||
|
||||
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
|
||||
> "$translation_provenance"
|
||||
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -416,6 +415,13 @@ if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
|
||||
fi
|
||||
fi
|
||||
# The AppImage keeps tool paths stable across updates, so a changed compiler is only visible here.
|
||||
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin")
|
||||
if [[ "$keep_native_build" -eq 1 && \
|
||||
"$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
|
||||
keep_native_build=0
|
||||
stale_reason=" (the toolchain or a pinned dependency changed)"
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
|
||||
rm -rf "$build"
|
||||
@@ -465,6 +471,7 @@ fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
|
||||
@@ -150,9 +150,16 @@ for source in aurora-main projects runtime translator; do
|
||||
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
|
||||
copy_clean "$workspace/$source" "$resources/workspace/$source"
|
||||
done
|
||||
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
|
||||
# index, so it is extracted from exactly the packaged runtime sources.
|
||||
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
|
||||
"$host_translator" emit-native-index --native-source-dir "$resources/workspace/runtime/src" \
|
||||
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
|
||||
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
copy_clean "$workspace/Launcher/Directory.Build.props" "$resources/workspace/Launcher/Directory.Build.props"
|
||||
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
|
||||
copy_clean "$workspace/Launcher/build-cache.sh" "$resources/workspace/Launcher/build-cache.sh"
|
||||
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
|
||||
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
|
||||
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
|
||||
|
||||
@@ -38,6 +38,16 @@ done
|
||||
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
|
||||
|
||||
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
|
||||
|
||||
# Sorted "<size> <path>" of every extracted file; recorded at extraction, compared on reuse.
|
||||
data_manifest() { (cd "$1" && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 stat -f '%z %N'); }
|
||||
|
||||
if [[ -d "$assets_dir/DATA/files" && -f "$assets_dir/.data-manifest" && -f "$assets_dir/main.dol" && -f "$assets_dir/StaticR.rel" ]] && \
|
||||
[[ $(sha256 "$assets_dir/main.dol") == "$EXPECTED_DOL_SHA256" && $(sha256 "$assets_dir/StaticR.rel") == "$EXPECTED_REL_SHA256" ]] && \
|
||||
[[ "$(data_manifest "$assets_dir/DATA")" == "$(cat "$assets_dir/.data-manifest")" ]]; then
|
||||
printf 'MKWCBUILD:STEP:disc-ready Reusing the verified extracted game assets\n'
|
||||
exit 0
|
||||
fi
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
|
||||
cleanup() { rm -rf "$scratch"; }
|
||||
trap cleanup EXIT
|
||||
@@ -66,15 +76,17 @@ mkdir -p "$stage"
|
||||
ditto "$data_root" "$stage/DATA"
|
||||
ditto "$dol" "$stage/main.dol"
|
||||
ditto "$rel" "$stage/StaticR.rel"
|
||||
data_manifest "$stage/DATA" > "$stage/.data-manifest"
|
||||
|
||||
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/.data-manifest" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
for item in DATA .data-manifest main.dol StaticR.rel; do
|
||||
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
|
||||
done
|
||||
fi
|
||||
mv "$stage/DATA" "$assets_dir/DATA"
|
||||
mv "$stage/.data-manifest" "$assets_dir/.data-manifest"
|
||||
mv "$stage/main.dol" "$assets_dir/main.dol"
|
||||
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
|
||||
rmdir "$stage"
|
||||
|
||||
@@ -91,15 +91,12 @@ if (( needs_workspace_refresh )); then
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
else
|
||||
# Refresh only packaged source inputs. Assets and the staged Retro
|
||||
# Rewind package belong to the user and stay in place.
|
||||
for source in aurora-main projects runtime translator Launcher; do
|
||||
rm -rf "$workspace/$source"
|
||||
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
|
||||
done
|
||||
# Rewind package belong to the user and stay in place; unchanged files
|
||||
# keep their timestamps so the incremental native build survives.
|
||||
# shellcheck source=../build-cache.sh
|
||||
source "$workspace_source/Launcher/build-cache.sh"
|
||||
mkw_sync_tree "$workspace_source" "$workspace" aurora-main projects runtime translator Launcher
|
||||
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
|
||||
# A dependency provider can be cached in this directory, so make the
|
||||
# refreshed sources configure from a clean native build tree.
|
||||
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+25
-20
@@ -94,6 +94,8 @@ set(MKW_NATIVE_PREBUILT_DIR "" CACHE PATH
|
||||
# package can be harvested from it.
|
||||
set(MKW_NATIVE_PREBUILT_EXPORT_DIR "" CACHE PATH
|
||||
"Maintainer-only: write a description of the configured aurora build here")
|
||||
option(MKW_USE_PREBUILT_RUNTIME
|
||||
"Link the native prebuilt package's runtime archive when it matches this workspace" ON)
|
||||
|
||||
# The two Crypto++ configuration macros are load-bearing for ABI: they shape the
|
||||
# structs and inline code the headers generate, so a first-party TU including
|
||||
@@ -305,20 +307,16 @@ else()
|
||||
TARGET_DIRECTORY aurora_gx
|
||||
PROPERTIES COMPILE_OPTIONS "-fno-finite-math-only")
|
||||
endif()
|
||||
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Everything from here down is this project's own code, so this is where the
|
||||
# project definitions enter the directory scope. A directory-scoped definition
|
||||
# seeds the COMPILE_DEFINITIONS of targets as they are created, which means:
|
||||
# * aurora-main and its third-party tree were added above -> unaffected;
|
||||
# * mkw_runtime_common, the translated shard libraries and the products in
|
||||
# cmake/PublicProducts.cmake are all created below (that file is include()d,
|
||||
# not add_subdirectory()d, so it shares this directory scope) -> they pick
|
||||
# the definitions up exactly as before.
|
||||
# * the runtime library, the translated shard libraries and the products in
|
||||
# cmake/RuntimeLibrary.cmake and cmake/PublicProducts.cmake are all created
|
||||
# below (those files are include()d, not add_subdirectory()d, so they share
|
||||
# this directory scope) -> they pick the definitions up exactly as before.
|
||||
if(MKW_PROJECT_COMPILE_DEFINITIONS)
|
||||
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
|
||||
endif()
|
||||
@@ -347,13 +345,27 @@ set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
# x86-64-v3 flags, so it gets its own object library instead of riding along in
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
# the runtime library. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/RuntimeLibrary.cmake")
|
||||
|
||||
# The prebuilt export also carries the game-independent runtime as a static
|
||||
# archive (x86_64 only; AArch64 compiles it locally with -mcpu=native). It is
|
||||
# defined here, after the project definitions, so its flags match the products'.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
mkw_add_runtime_library(mkw_runtime STATIC ${SOURCES})
|
||||
set_target_properties(mkw_runtime PROPERTIES EXCLUDE_FROM_ALL ON)
|
||||
endif()
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
@@ -361,8 +373,8 @@ if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
# The prebuilt export only needs the aurora/third-party closure and the runtime
|
||||
# configured above, so a packaging machine without a translation stops here.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
@@ -379,20 +391,13 @@ if(MKW_BUILD_PRODUCTS)
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
# runtime/src is game-independent; per-game glue lives in runtime/game.
|
||||
add_library(mkw_macos_native_compile OBJECT ${SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#
|
||||
# Produced by launcher/Prepare-NativePrebuilt.ps1 with the same toolchain and the
|
||||
# same canonical flag set the consuming build uses, which is what makes a single
|
||||
# package valid for every user. Nothing about this project's own code changes:
|
||||
# the runtime, the translated shards and the products still compile locally and
|
||||
# still link targets named aurora::gx / aurora::pad / aurora::si / aurora::vi /
|
||||
# aurora::mtx, so cmake/PublicProducts.cmake needs no prebuilt-specific branch.
|
||||
# package valid for every user. The translated shards and the products still
|
||||
# compile locally and still link targets named aurora::gx / aurora::pad /
|
||||
# aurora::si / aurora::vi / aurora::mtx. The package may also carry the native
|
||||
# runtime archive; cmake/RuntimeLibrary.cmake decides whether it can be used.
|
||||
#
|
||||
# The aurora and dependency source trees are still present in the workspace - the
|
||||
# runtime includes aurora headers directly - so the package records include
|
||||
@@ -42,6 +42,10 @@ endfunction()
|
||||
mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES})
|
||||
mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES})
|
||||
mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR})
|
||||
set(MKW_NP_RUNTIME_ARCHIVE_PATH "")
|
||||
if(MKW_NP_RUNTIME_ARCHIVE)
|
||||
mkw_np_resolve(MKW_NP_RUNTIME_ARCHIVE_PATH "${MKW_NP_RUNTIME_ARCHIVE}")
|
||||
endif()
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
if(NOT MKW_NP_MBEDTLS_LIBRARIES OR NOT MKW_NP_MBEDTLS_INCLUDE_DIR)
|
||||
message(FATAL_ERROR
|
||||
|
||||
@@ -87,6 +87,11 @@ mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets)
|
||||
# the scan above cannot see it; it is appended explicitly. The type and closure
|
||||
# checks below still apply to it.
|
||||
list(APPEND _mkw_np_all_targets "mkw_cryptopp")
|
||||
# The game-independent runtime archive (RuntimeLibrary.cmake). It is outside the
|
||||
# probe closure, so it ships only when the harvester builds it explicitly.
|
||||
if(TARGET mkw_runtime)
|
||||
list(APPEND _mkw_np_all_targets "mkw_runtime")
|
||||
endif()
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
set(_mkw_np_mbedtls_targets mbedtls mbedx509 mbedcrypto)
|
||||
# mbedcrypto links these third-party archives when upstream defines them.
|
||||
@@ -168,5 +173,12 @@ file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
|
||||
"cxx_compiler=${CMAKE_CXX_COMPILER}\n"
|
||||
"cxx_compiler_version=${CMAKE_CXX_COMPILER_VERSION}\n"
|
||||
"library_targets=${_mkw_np_lib_targets_csv}\n")
|
||||
if(TARGET mkw_runtime)
|
||||
mkw_runtime_fingerprint(_mkw_np_runtime_fingerprint "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
file(APPEND "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
|
||||
"runtime_fingerprint=${_mkw_np_runtime_fingerprint}\n"
|
||||
"runtime_cpu_profile=${MKW_X86_CPU_PROFILE}\n")
|
||||
endif()
|
||||
|
||||
message(STATUS "Native prebuilt export: ${MKW_NATIVE_PREBUILT_EXPORT_DIR}")
|
||||
|
||||
@@ -4,55 +4,42 @@
|
||||
# functions are compiled once into mkw_base_shared; only callers whose direct
|
||||
# ABI differs between profiles receive small base/RR variants.
|
||||
|
||||
# Per-game data the runtime reads but does not own. Kept out of the runtime
|
||||
# library so that library stays game-independent (and can be precompiled).
|
||||
set(MKW_GAME_DATA_SOURCES "")
|
||||
set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp")
|
||||
set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S")
|
||||
if(EXISTS "${DATA_INIT_FILE}")
|
||||
list(APPEND SOURCES "${DATA_INIT_FILE}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_FILE}")
|
||||
endif()
|
||||
# Crash-report symbolization table emitted by generate-data-init. The stub
|
||||
# (deliberately outside the globbed src/ tree so it is never picked up twice)
|
||||
# keeps link succeeding when the generated table has not been produced yet.
|
||||
set(GUEST_SYMBOL_TABLE_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/guest_symbol_table.cpp")
|
||||
if(EXISTS "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
else()
|
||||
list(APPEND SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
|
||||
endif()
|
||||
if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
enable_language(ASM)
|
||||
set_source_files_properties("${DATA_INIT_BLOB_ASM}" PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
list(APPEND SOURCES "${DATA_INIT_BLOB_ASM}")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
# Runtime glue that depends on generated/ headers (e.g. RuntimeConfig.h).
|
||||
file(GLOB MKW_GAME_GLUE_SOURCES CONFIGURE_DEPENDS "${MKW_RUNTIME_SOURCE_DIR}/game/*.cpp")
|
||||
list(APPEND MKW_GAME_DATA_SOURCES ${MKW_GAME_GLUE_SOURCES})
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_apply_translated_compile_options target)
|
||||
target_compile_options(${target} PRIVATE
|
||||
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_configure_object_target target)
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
TARGET_PC)
|
||||
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
|
||||
endfunction()
|
||||
|
||||
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
|
||||
# comparatively small. A dedicated Ninja job pool caps how many of them run at once so the
|
||||
# global parallelism can use every core for the cheap TUs without the memory ceiling being a
|
||||
@@ -74,75 +61,35 @@ function(mkw_configure_translated_target target)
|
||||
mkw_bound_translated_compiles(${target})
|
||||
endfunction()
|
||||
|
||||
add_library(mkw_runtime_common OBJECT ${SOURCES})
|
||||
mkw_configure_object_target(mkw_runtime_common)
|
||||
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
mkw_select_runtime_provider()
|
||||
if(MKW_RUNTIME_USE_PREBUILT)
|
||||
mkw_import_prebuilt_runtime(mkw_runtime_prebuilt "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
|
||||
# A replacement archive can carry an older mtime than the last link, so relink on fingerprint change.
|
||||
set(MKW_RUNTIME_PREBUILT_STAMP "${CMAKE_BINARY_DIR}/mkw_runtime_prebuilt.stamp")
|
||||
file(CONFIGURE OUTPUT "${MKW_RUNTIME_PREBUILT_STAMP}" CONTENT "${MKW_NP_RUNTIME_FINGERPRINT}\n")
|
||||
else()
|
||||
mkw_add_runtime_library(mkw_runtime_common OBJECT ${SOURCES})
|
||||
endif()
|
||||
|
||||
add_library(mkw_game_data OBJECT ${MKW_GAME_DATA_SOURCES})
|
||||
mkw_configure_object_target(mkw_game_data)
|
||||
target_compile_features(mkw_game_data PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_game_data PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
if(MKW_SETUP_VERSION)
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE MKW_SETUP_VERSION="${MKW_SETUP_VERSION}")
|
||||
set_property(SOURCE "${MKW_RUNTIME_SOURCE_DIR}/game/setup_version.cpp" APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "MKW_SETUP_VERSION=\"${MKW_SETUP_VERSION}\"")
|
||||
endif()
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# CoreAudio framework is required for automatic music muting on macOS.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE "${MKW_COREAUDIO_FRAMEWORK}")
|
||||
if(MKW_PLATFORM_MACOS_X86_64)
|
||||
# libco is used by Intel macOS. Apple Silicon uses the local
|
||||
# x18-safe assembly backend and therefore does not define mkw::libco.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
target_include_directories(mkw_runtime_common PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
# Keep runtime unity units small and semantically related. The old generated-TU
|
||||
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
|
||||
foreach(source IN LISTS SOURCES)
|
||||
get_filename_component(source_name "${source}" NAME_WE)
|
||||
string(REPLACE "\\" "/" source_normalized "${source}")
|
||||
if(source_normalized MATCHES "/hle/gx/")
|
||||
set(runtime_group "gx_bridge")
|
||||
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
|
||||
set(runtime_group "network_ios")
|
||||
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
|
||||
set(runtime_group "guest_system")
|
||||
elseif(source_name MATCHES "debug|trace|prof")
|
||||
set(runtime_group "diagnostics")
|
||||
else()
|
||||
string(SHA256 source_hash "${source_name}")
|
||||
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
|
||||
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
|
||||
set(runtime_group "runtime_${runtime_bucket}")
|
||||
endif()
|
||||
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
|
||||
endforeach()
|
||||
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
|
||||
set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
mkw_runtime_link_dependencies(mkw_game_data PRIVATE)
|
||||
set_target_properties(mkw_game_data PROPERTIES UNITY_BUILD OFF)
|
||||
mkw_apply_common_compile_options(mkw_game_data)
|
||||
|
||||
# Host ISA guard deliberately keeps the plain baseline ISA and checks the
|
||||
# selected x86 profile before optional instructions can execute. AArch64 has
|
||||
# no equivalent optional ISA floor to probe: NEON/FMA are architectural
|
||||
# requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
@@ -156,9 +103,22 @@ if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
endif()
|
||||
|
||||
# Translated code compiles against the guest ABI surface only, so runtime
|
||||
# implementation edits do not invalidate its PCH.
|
||||
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/guest_abi/guest_abi_pch.h")
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_PCH}")
|
||||
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
endif()
|
||||
|
||||
add_library(mkw_base_shared STATIC ${MKW_BASE_COMMON_SHARDS})
|
||||
mkw_configure_translated_target(mkw_base_shared)
|
||||
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_TRANSLATED_PCH}")
|
||||
|
||||
# Registration and indirect-dispatch tables are translator output too; they
|
||||
# compile with the translated configuration rather than inside the products.
|
||||
add_library(mkw_base_registration OBJECT ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
mkw_configure_translated_target(mkw_base_registration)
|
||||
target_precompile_headers(mkw_base_registration REUSE_FROM mkw_base_shared)
|
||||
|
||||
if(MKW_BASE_PORTABLE_SENSITIVE_SHARDS)
|
||||
add_library(mkw_base_sensitive OBJECT ${MKW_BASE_PORTABLE_SENSITIVE_SHARDS})
|
||||
@@ -181,16 +141,31 @@ if(MKW_HAVE_RETRO_REWIND)
|
||||
set_source_files_properties("${source}" PROPERTIES LANGUAGE ASM SKIP_PRECOMPILE_HEADERS ON)
|
||||
endif()
|
||||
endforeach()
|
||||
# mod_data_patches (MKW_RETRO_EXTRA_SOURCES) calls runtime APIs directly; it
|
||||
# keeps the full runtime include path this target already has.
|
||||
add_library(mkw_retro_rewind_functions OBJECT ${MKW_RETRO_TRANSLATED_SOURCES})
|
||||
mkw_configure_translated_target(mkw_retro_rewind_functions)
|
||||
target_precompile_headers(mkw_retro_rewind_functions REUSE_FROM mkw_base_shared)
|
||||
|
||||
add_library(mkw_retro_registration OBJECT ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
mkw_configure_translated_target(mkw_retro_registration)
|
||||
target_precompile_headers(mkw_retro_registration REUSE_FROM mkw_base_shared)
|
||||
endif()
|
||||
|
||||
function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
if(TARGET mkw_runtime_common)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
else()
|
||||
# Natives self-register from static constructors, so every archive
|
||||
# member must be kept. Listed before mkw_base_shared so single-pass
|
||||
# linkers resolve the runtime's references to translated functions.
|
||||
target_link_libraries(${target} PRIVATE "$<LINK_LIBRARY:WHOLE_ARCHIVE,mkw_runtime_prebuilt>")
|
||||
set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${MKW_RUNTIME_PREBUILT_STAMP}")
|
||||
endif()
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_game_data>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
@@ -204,9 +179,6 @@ function(mkw_configure_product target)
|
||||
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||
mkw_apply_common_compile_options(${target})
|
||||
# The dispatch-table and registration shards compile inside the product target itself and
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
|
||||
@@ -314,8 +286,9 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}")
|
||||
mkw_configure_product(WiiCompiled)
|
||||
target_sources(WiiCompiled PRIVATE $<TARGET_OBJECTS:mkw_base_registration>)
|
||||
target_precompile_headers(WiiCompiled PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
if(TARGET mkw_base_sensitive)
|
||||
@@ -323,9 +296,10 @@ if(TARGET mkw_base_sensitive)
|
||||
endif()
|
||||
|
||||
if(MKW_HAVE_RETRO_REWIND)
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
|
||||
mkw_configure_product(RetroRewind)
|
||||
target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled)
|
||||
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_registration>)
|
||||
if(TARGET mkw_retro_sensitive)
|
||||
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_sensitive>)
|
||||
endif()
|
||||
@@ -339,26 +313,12 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
|
||||
# builds are compiled locally for the host that will run them, so both Linux
|
||||
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
|
||||
# target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
# mkw_runtime_common gets the same flags from mkw_add_runtime_library.
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind)
|
||||
mkw_game_data mkw_base_shared mkw_base_registration mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions mkw_retro_registration WiiCompiled RetroRewind)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
if(TARGET ${target})
|
||||
mkw_apply_baseline_arch(${target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Content identity of the game-independent runtime and the rule deciding whether
|
||||
# a precompiled runtime archive may replace compiling it. Script mode prints
|
||||
# exactly one stdout line, runtime_fingerprint=<sha256 hex>:
|
||||
# cmake -DMKW_RUNTIME_DIR=<abs runtime dir> [-DMKW_CPPWINRT_INCLUDE_DIR=<dir>]
|
||||
# [-DMKW_X86_CPU_PROFILE=v2|v3] [-DCMAKE_OSX_DEPLOYMENT_TARGET=<ver>]
|
||||
# -P runtime/cmake/RuntimeFingerprint.cmake
|
||||
#
|
||||
# Inputs: src/, include/, cmake/ and CMakeLists.txt (sorted relative paths and
|
||||
# content hashes), winrt/base.h, the CPU profile and the macOS deployment target.
|
||||
# third_party/ has its own package fingerprint; game/ is per-user. Dot-files
|
||||
# differ between machines.
|
||||
|
||||
function(mkw_runtime_fingerprint_inputs out_var runtime_dir)
|
||||
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
|
||||
file(GLOB_RECURSE _files LIST_DIRECTORIES false RELATIVE "${_root}"
|
||||
"${_root}/src/*" "${_root}/include/*" "${_root}/cmake/*")
|
||||
list(FILTER _files EXCLUDE REGEX "(^|/)\\.")
|
||||
list(APPEND _files "CMakeLists.txt")
|
||||
list(SORT _files COMPARE STRING CASE SENSITIVE)
|
||||
set(${out_var} "${_files}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(mkw_runtime_fingerprint out_var runtime_dir cppwinrt_dir cpu_profile)
|
||||
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
|
||||
mkw_runtime_fingerprint_inputs(_files "${_root}")
|
||||
set(_manifest "")
|
||||
foreach(_file IN LISTS _files)
|
||||
if(NOT EXISTS "${_root}/${_file}")
|
||||
message(FATAL_ERROR "Runtime fingerprint input is missing: ${_root}/${_file}")
|
||||
endif()
|
||||
file(SHA256 "${_root}/${_file}" _hash)
|
||||
string(APPEND _manifest "${_file} ${_hash}\n")
|
||||
endforeach()
|
||||
if(cppwinrt_dir)
|
||||
if(NOT EXISTS "${cppwinrt_dir}/winrt/base.h")
|
||||
message(FATAL_ERROR "MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${cppwinrt_dir}")
|
||||
endif()
|
||||
file(SHA256 "${cppwinrt_dir}/winrt/base.h" _hash)
|
||||
string(APPEND _manifest "cppwinrt:winrt/base.h ${_hash}\n")
|
||||
endif()
|
||||
string(APPEND _manifest "x86_cpu_profile ${cpu_profile}\n")
|
||||
string(APPEND _manifest "osx_deployment_target ${CMAKE_OSX_DEPLOYMENT_TARGET}\n")
|
||||
string(SHA256 _fingerprint "${_manifest}")
|
||||
set(${out_var} "${_fingerprint}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Sets out_use to TRUE/FALSE and out_reason to a one-line explanation. Pure, so
|
||||
# it can be exercised in script mode without a configured project.
|
||||
cmake_policy(PUSH)
|
||||
if(POLICY CMP0174)
|
||||
cmake_policy(SET CMP0174 NEW)
|
||||
endif()
|
||||
function(mkw_runtime_prebuilt_decision out_use out_reason)
|
||||
cmake_parse_arguments(PARSE_ARGV 2 _d ""
|
||||
"ARCHIVE;ENABLED;X86_64;ARCHIVE_EXISTS;PACKAGE_PROFILE;PROFILE;PACKAGE_FINGERPRINT;FINGERPRINT" "")
|
||||
set(_use FALSE)
|
||||
if(NOT _d_ARCHIVE)
|
||||
set(_reason "the native prebuilt package has no runtime archive")
|
||||
elseif(NOT _d_ENABLED)
|
||||
set(_reason "MKW_USE_PREBUILT_RUNTIME is OFF")
|
||||
elseif(NOT _d_X86_64)
|
||||
set(_reason "the precompiled runtime is x86_64-only")
|
||||
elseif(NOT _d_ARCHIVE_EXISTS)
|
||||
set(_reason "the runtime archive is missing: ${_d_ARCHIVE}")
|
||||
elseif(NOT _d_PACKAGE_PROFILE STREQUAL _d_PROFILE)
|
||||
set(_reason "the package was built for x86-64-${_d_PACKAGE_PROFILE}, this build targets x86-64-${_d_PROFILE}")
|
||||
elseif(NOT _d_PACKAGE_FINGERPRINT)
|
||||
set(_reason "the package records no runtime fingerprint")
|
||||
elseif(NOT _d_FINGERPRINT OR NOT _d_PACKAGE_FINGERPRINT STREQUAL _d_FINGERPRINT)
|
||||
set(_reason "runtime sources differ from the package (package ${_d_PACKAGE_FINGERPRINT}, workspace ${_d_FINGERPRINT})")
|
||||
else()
|
||||
set(_use TRUE)
|
||||
set(_reason "fingerprint ${_d_FINGERPRINT} matches")
|
||||
endif()
|
||||
set(${out_use} ${_use} PARENT_SCOPE)
|
||||
set(${out_reason} "${_reason}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
cmake_policy(POP)
|
||||
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
get_filename_component(_mkw_script "${CMAKE_SCRIPT_MODE_FILE}" REALPATH)
|
||||
get_filename_component(_mkw_self "${CMAKE_CURRENT_LIST_FILE}" REALPATH)
|
||||
endif()
|
||||
if(CMAKE_SCRIPT_MODE_FILE AND _mkw_script STREQUAL _mkw_self)
|
||||
if(NOT MKW_RUNTIME_DIR OR NOT IS_DIRECTORY "${MKW_RUNTIME_DIR}")
|
||||
message(FATAL_ERROR "Pass -DMKW_RUNTIME_DIR=<runtime directory>")
|
||||
endif()
|
||||
if(NOT MKW_X86_CPU_PROFILE)
|
||||
set(MKW_X86_CPU_PROFILE "v3")
|
||||
endif()
|
||||
# Same default runtime/CMakeLists.txt forces on every platform before project().
|
||||
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
|
||||
endif()
|
||||
mkw_runtime_fingerprint(_mkw_fingerprint "${MKW_RUNTIME_DIR}" "${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
# message() writes to stderr; the echo child inherits stdout.
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "runtime_fingerprint=${_mkw_fingerprint}")
|
||||
endif()
|
||||
@@ -0,0 +1,180 @@
|
||||
# The game-independent native runtime (runtime/src). One definition serves
|
||||
# three uses: the OBJECT library the products link from source, the STATIC
|
||||
# archive the native prebuilt export ships (mkw_runtime), and that archive
|
||||
# imported in place of compiling it (mkw_runtime_prebuilt).
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/RuntimeFingerprint.cmake")
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_HOST_IS_X86_64 TRUE)
|
||||
else()
|
||||
set(MKW_HOST_IS_X86_64 FALSE)
|
||||
endif()
|
||||
|
||||
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
|
||||
# builds are compiled locally for the host that will run them, so both Linux
|
||||
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
|
||||
# target-specific performance on the table.
|
||||
if(MKW_HOST_IS_X86_64)
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
|
||||
endif()
|
||||
|
||||
# CMake decides WHOLE_ARCHIVE's form by probing ld.lld in ELF mode, which
|
||||
# accepts --push-state; the MinGW lld driver rejects it at link time.
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(CMAKE_CXX_LINK_LIBRARY_USING_WHOLE_ARCHIVE
|
||||
"LINKER:--whole-archive" "<LINK_ITEM>" "LINKER:--no-whole-archive")
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
|
||||
function(mkw_apply_baseline_arch target)
|
||||
if(MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(mkw_configure_object_target target)
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
TARGET_PC)
|
||||
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
|
||||
endfunction()
|
||||
|
||||
# scope is PRIVATE for a compiled target and INTERFACE for the imported archive.
|
||||
function(mkw_runtime_link_dependencies target scope)
|
||||
target_link_libraries(${target} ${scope}
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
target_link_libraries(${target} ${scope} mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# CoreAudio framework is required for automatic music muting on macOS.
|
||||
target_link_libraries(${target} ${scope} "${MKW_COREAUDIO_FRAMEWORK}")
|
||||
if(MKW_PLATFORM_MACOS_X86_64)
|
||||
# libco is used by Intel macOS. Apple Silicon uses the local
|
||||
# x18-safe assembly backend and therefore does not define mkw::libco.
|
||||
target_link_libraries(${target} ${scope} mkw::libco)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Compiles the runtime sources as an OBJECT or STATIC library.
|
||||
function(mkw_add_runtime_library target type)
|
||||
set(_sources ${ARGN})
|
||||
add_library(${target} ${type} ${_sources})
|
||||
mkw_configure_object_target(${target})
|
||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
mkw_runtime_link_dependencies(${target} PRIVATE)
|
||||
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
# Keep runtime unity units small and semantically related. The old generated-TU
|
||||
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
|
||||
foreach(source IN LISTS _sources)
|
||||
get_filename_component(source_name "${source}" NAME_WE)
|
||||
string(REPLACE "\\" "/" source_normalized "${source}")
|
||||
if(source_normalized MATCHES "/hle/gx/")
|
||||
set(runtime_group "gx_bridge")
|
||||
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
|
||||
set(runtime_group "network_ios")
|
||||
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
|
||||
set(runtime_group "guest_system")
|
||||
elseif(source_name MATCHES "debug|trace|prof")
|
||||
set(runtime_group "diagnostics")
|
||||
else()
|
||||
string(SHA256 source_hash "${source_name}")
|
||||
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
|
||||
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
|
||||
set(runtime_group "runtime_${runtime_bucket}")
|
||||
endif()
|
||||
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
|
||||
endforeach()
|
||||
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
|
||||
set_target_properties(${target} PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
|
||||
target_precompile_headers(${target} PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(${target})
|
||||
mkw_apply_baseline_arch(${target})
|
||||
endfunction()
|
||||
|
||||
function(mkw_import_prebuilt_runtime target archive)
|
||||
add_library(${target} STATIC IMPORTED GLOBAL)
|
||||
set_target_properties(${target} PROPERTIES IMPORTED_LOCATION "${archive}")
|
||||
mkw_runtime_link_dependencies(${target} INTERFACE)
|
||||
endfunction()
|
||||
|
||||
# Sets MKW_RUNTIME_USE_PREBUILT in the caller. The archive replaces the
|
||||
# runtime's objects only when it was built from exactly this workspace's
|
||||
# runtime inputs for the same x86-64 profile.
|
||||
function(mkw_select_runtime_provider)
|
||||
set(_archive "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
|
||||
set(_exists FALSE)
|
||||
if(_archive AND EXISTS "${_archive}")
|
||||
set(_exists TRUE)
|
||||
endif()
|
||||
set(_fingerprint "")
|
||||
if(_exists AND MKW_USE_PREBUILT_RUNTIME AND MKW_HOST_IS_X86_64 AND
|
||||
MKW_NP_RUNTIME_CPU_PROFILE STREQUAL MKW_X86_CPU_PROFILE)
|
||||
mkw_runtime_fingerprint(_fingerprint "${MKW_RUNTIME_SOURCE_DIR}"
|
||||
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
|
||||
endif()
|
||||
mkw_runtime_prebuilt_decision(_use _reason
|
||||
ARCHIVE "${_archive}"
|
||||
ENABLED "${MKW_USE_PREBUILT_RUNTIME}"
|
||||
X86_64 "${MKW_HOST_IS_X86_64}"
|
||||
ARCHIVE_EXISTS "${_exists}"
|
||||
PACKAGE_PROFILE "${MKW_NP_RUNTIME_CPU_PROFILE}"
|
||||
PROFILE "${MKW_X86_CPU_PROFILE}"
|
||||
PACKAGE_FINGERPRINT "${MKW_NP_RUNTIME_FINGERPRINT}"
|
||||
FINGERPRINT "${_fingerprint}")
|
||||
if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
set(_reason "no native prebuilt package is configured")
|
||||
endif()
|
||||
|
||||
if(_use)
|
||||
message(STATUS "Native runtime: precompiled archive ${_archive} (${_reason})")
|
||||
# Any runtime edit must re-run configure so the mismatch is noticed.
|
||||
mkw_runtime_fingerprint_inputs(_inputs "${MKW_RUNTIME_SOURCE_DIR}")
|
||||
list(TRANSFORM _inputs PREPEND "${MKW_RUNTIME_SOURCE_DIR}/")
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
list(APPEND _inputs "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
endif()
|
||||
set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_inputs})
|
||||
else()
|
||||
message(STATUS "Native runtime: compiled from source (${_reason})")
|
||||
endif()
|
||||
set(MKW_RUNTIME_USE_PREBUILT ${_use} PARENT_SCOPE)
|
||||
endfunction()
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "guest_small_data.h"
|
||||
|
||||
#include "generated/RuntimeConfig.h"
|
||||
|
||||
namespace GuestSmallData {
|
||||
extern const uint32_t kSda1Base = RuntimeConfig::SDA1_BASE;
|
||||
extern const uint32_t kSda2Base = RuntimeConfig::SDA2_BASE;
|
||||
} // namespace GuestSmallData
|
||||
@@ -0,0 +1,7 @@
|
||||
// Compiled per user rather than into the shared runtime archive.
|
||||
extern const char* const kMkwSetupVersion =
|
||||
#if defined(MKW_SETUP_VERSION)
|
||||
MKW_SETUP_VERSION;
|
||||
#else
|
||||
"";
|
||||
#endif
|
||||
@@ -18,638 +18,14 @@
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
|
||||
#include "guest_abi/dispatch.h"
|
||||
|
||||
// Some game-facing runtime options alter arguments at well-defined ABI
|
||||
// boundaries. Keep this independent of the dispatch mechanism: generated
|
||||
// static calls deliberately bypass InvokeDirectCpu for performance.
|
||||
// (used for the path mask filtering in ScnRenderer::createPath: depth of
|
||||
// field is always removed, bloom when the user disabled it)
|
||||
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
|
||||
if (target == 0x8023BD38u) {
|
||||
// ScnRenderer::createPath receives the post-processing path mask in r4.
|
||||
ctx->gpr[4] = RuntimeGameGraphicsOptions::FilterScnRendererPathMask(ctx->gpr[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent per-thread CPU context used across translated function calls.
|
||||
CpuContext& GetPersistentCpuContext();
|
||||
void InitializePersistentCpuContext();
|
||||
|
||||
enum class FunctionKind : uint8_t {
|
||||
BaseTranslated = 0,
|
||||
ModTranslated = 1,
|
||||
Native = 2,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
|
||||
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
|
||||
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
|
||||
inline constexpr uint32_t kNativeFunctionPriority = 10000;
|
||||
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
|
||||
struct TranslatedFunctionInfo {
|
||||
uint32_t address = 0;
|
||||
const char* name = "";
|
||||
uint64_t moduleId = 0;
|
||||
uint32_t priority = 0;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
|
||||
void (*rawCpuInvoker)(CpuContext*) = nullptr;
|
||||
bool mustRemainDynamicallyDispatchable = true;
|
||||
FunctionKind kind = FunctionKind::BaseTranslated;
|
||||
};
|
||||
|
||||
struct RawDispatchRecord {
|
||||
uint32_t address = 0;
|
||||
void (*entry)(CpuContext*) = nullptr;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
bool preserveNonvolatileGprs = false;
|
||||
};
|
||||
|
||||
// Translator-owned indirect dispatch is split by the high address byte and
|
||||
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
|
||||
// page range that contains verified function entry points. The final search is
|
||||
// bounded to at most the 1024 aligned instruction addresses in one guest page.
|
||||
struct StaticIndirectDispatchPage {
|
||||
uint32_t firstEntry = 0;
|
||||
uint16_t entryCount = 0;
|
||||
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
|
||||
// literal third initializer, so it is part of the generated-data contract.
|
||||
uint16_t reserved = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchSegment {
|
||||
const StaticIndirectDispatchPage* pages = nullptr;
|
||||
uint16_t firstPage = 0;
|
||||
uint16_t pageCount = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchTable {
|
||||
const char* profileName = nullptr;
|
||||
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
|
||||
const RawDispatchRecord* entries = nullptr;
|
||||
size_t entryCount = 0;
|
||||
};
|
||||
|
||||
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
|
||||
const StaticIndirectDispatchTable* table,
|
||||
uint32_t address) noexcept {
|
||||
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& segment = table->segments[address >> 24];
|
||||
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
|
||||
if (!segment.pages || pageNumber < segment.firstPage) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t relativePage = pageNumber - segment.firstPage;
|
||||
if (relativePage >= segment.pageCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& page = segment.pages[relativePage];
|
||||
const size_t first = page.firstEntry;
|
||||
const size_t count = page.entryCount;
|
||||
if (first > table->entryCount || count > table->entryCount - first) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t lower = first;
|
||||
size_t upper = first + count;
|
||||
while (lower < upper) {
|
||||
const size_t middle = lower + (upper - lower) / 2;
|
||||
const uint32_t candidate = table->entries[middle].address;
|
||||
if (candidate < address) {
|
||||
lower = middle + 1;
|
||||
} else {
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
return lower < first + count && table->entries[lower].address == address
|
||||
? &table->entries[lower]
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
|
||||
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
|
||||
|
||||
class StaticIndirectDispatchTableRegistrar {
|
||||
public:
|
||||
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
|
||||
RegisterStaticIndirectDispatchTable(table);
|
||||
}
|
||||
};
|
||||
|
||||
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
|
||||
return static_cast<unsigned>(__builtin_ctz(value));
|
||||
}
|
||||
|
||||
class PpcNonvolatileFprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
|
||||
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
// Real masks are a single contiguous register run - f14..f31 for the
|
||||
// conservative default and f23..f31 or similar for a measured writer -
|
||||
// so resolve the run once and copy it as one straight-line block instead
|
||||
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
|
||||
// saves and restores exactly the same registers.
|
||||
const uint32_t lowest = mask_ & (0u - mask_);
|
||||
const uint32_t aboveRun = mask_ + lowest;
|
||||
if ((aboveRun & (aboveRun - 1u)) == 0u) {
|
||||
const unsigned first = PpcLowestSetBitIndex(mask_);
|
||||
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
|
||||
firstSlot_ = static_cast<uint8_t>(first - 14u);
|
||||
slotCount_ = static_cast<uint8_t>(end - first);
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
saved_[i] = cpu_->fpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileFprGuard() noexcept {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (slotCount_ != 0) {
|
||||
const unsigned first = 14u + firstSlot_;
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
cpu_->fpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
|
||||
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
uint32_t mask_ = 0;
|
||||
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
|
||||
// a single run, so the destructor mirrors the constructor's per-bit loop.
|
||||
uint8_t firstSlot_ = 0;
|
||||
uint8_t slotCount_ = 0;
|
||||
// Every element selected by mask_ is written before it is read. Leaving the
|
||||
// remaining slots uninitialized avoids zero-filling all 18 values around a
|
||||
// call that may preserve only one or two architectural FPRs.
|
||||
std::array<PPC_FPR, 18> saved_;
|
||||
};
|
||||
|
||||
class PpcNonvolatileGprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
|
||||
: cpu_(enabled ? cpu : nullptr) {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
saved_[i] = cpu_->gpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileGprGuard() noexcept {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
cpu_->gpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
|
||||
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
|
||||
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
|
||||
std::array<uint32_t, 18> saved_;
|
||||
};
|
||||
|
||||
// Guest address of OSLoadContext, which models rfi-style context restoration
|
||||
// and intentionally replaces the full guest register file instead of returning
|
||||
// like a normal ABI call. It can never be guarded.
|
||||
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
|
||||
|
||||
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
|
||||
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
|
||||
}
|
||||
|
||||
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
|
||||
if (!info || info->nonvolatileFprWriteMask == 0) {
|
||||
return 0;
|
||||
}
|
||||
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTranslatedCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = true;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
// Translator-emitted trait specializations, one macro shared by every generated shard.
|
||||
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
|
||||
// publish their own name). A specialization only exists for a single statically bound winner,
|
||||
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
|
||||
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
|
||||
extern "C" void winner(CpuContext* ctx); \
|
||||
template <> \
|
||||
struct KnownTranslatedCpuCall<0x##addr##u> { \
|
||||
static constexpr bool kAvailable = true; \
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
|
||||
static constexpr void (*Entry)(CpuContext*) = &winner; \
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTypedNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
};
|
||||
|
||||
#include "native_cpu_calls.inc"
|
||||
|
||||
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
|
||||
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
|
||||
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
|
||||
// power of two, the index masks with (size - 1).
|
||||
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
|
||||
|
||||
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
|
||||
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in ppc_runtime.h).
|
||||
struct IndirectResolvedDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const TranslatedFunctionInfo* info;
|
||||
};
|
||||
inline thread_local IndirectResolvedDispatchMemoEntry
|
||||
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
struct IndirectRawDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const RawDispatchRecord* record;
|
||||
};
|
||||
inline thread_local IndirectRawDispatchMemoEntry
|
||||
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
class TranslatedFunctionRegistry {
|
||||
public:
|
||||
static void Register(TranslatedFunctionInfo info);
|
||||
static void Finalize();
|
||||
static inline bool IsLookupPublished() noexcept {
|
||||
return lookupPublished_.load(std::memory_order_acquire);
|
||||
}
|
||||
static std::optional<TranslatedFunctionInfo> FindNearestByAddress(uint32_t address);
|
||||
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return FindByAddressPtrSlow(address);
|
||||
}
|
||||
|
||||
auto& cached = g_indirectResolvedDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.info;
|
||||
}
|
||||
|
||||
const auto* info = FindByAddressPtrSlow(address);
|
||||
cached = {true, address, info};
|
||||
return info;
|
||||
}
|
||||
|
||||
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
|
||||
// generated-table binary search alongside it pushed the whole function past
|
||||
// the inliner's budget and out-of-lined the probe itself, which is the part
|
||||
// every bctrl executes.
|
||||
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& cached = g_indirectRawDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.record;
|
||||
}
|
||||
|
||||
return FindRawByAddressPtrMiss(address, cached);
|
||||
}
|
||||
// Look up a registered function by host (native) address.
|
||||
// This enables stack trace symbolication for translated functions.
|
||||
static std::optional<TranslatedFunctionInfo> FindByHostAddress(uintptr_t hostAddr);
|
||||
|
||||
private:
|
||||
// Memo miss: resolve against the header-inline generated table first; only dynamically
|
||||
// registered records or a not-yet-published table fall through to the slow path.
|
||||
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
|
||||
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
|
||||
const RawDispatchRecord* record = nullptr;
|
||||
if (const auto* table =
|
||||
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
|
||||
record = FindStaticIndirectDispatchEntry(table, address);
|
||||
}
|
||||
if (record == nullptr) {
|
||||
record = FindRawByAddressPtrSlow(address);
|
||||
}
|
||||
cached = {true, address, record};
|
||||
return record;
|
||||
}
|
||||
|
||||
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
|
||||
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
|
||||
|
||||
// Finalize publishes immutable registry, generated-table, and dynamic raw
|
||||
// dispatch storage. Header-inlined cache hits are enabled only after that
|
||||
// release publication, so a pre-finalization miss can never poison them.
|
||||
inline static std::atomic_bool lookupPublished_{false};
|
||||
};
|
||||
|
||||
// Cold, profile-owned registration data is emitted in compact table shards.
|
||||
// Keeping it out of generated function bodies avoids one static constructor
|
||||
// per translated function.
|
||||
struct BulkTranslatedFunctionRecord {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
void (*entry)(CpuContext*);
|
||||
FunctionKind kind;
|
||||
bool preservesNonvolatileFprs;
|
||||
uint32_t nonvolatileFprWriteMask;
|
||||
uint32_t priority;
|
||||
uint64_t moduleId;
|
||||
bool mustRemainDynamicallyDispatchable;
|
||||
};
|
||||
|
||||
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
|
||||
|
||||
class BulkTranslatedFunctionRegistrar {
|
||||
public:
|
||||
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
|
||||
RegisterBulkTranslatedFunctions(records, count);
|
||||
}
|
||||
};
|
||||
|
||||
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
|
||||
if (!info || !info->rawCpuInvoker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
|
||||
if (!record || !record->entry) {
|
||||
return false;
|
||||
}
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
record->entry(cpu);
|
||||
} else {
|
||||
record->entry(cpu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
|
||||
const auto invokeKnownTranslated = [&]() {
|
||||
KnownTranslatedCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
|
||||
// A statically resolved, non-overridable translated call already running
|
||||
// in this CpuContext needs no registry, context, or diagnostic target
|
||||
// transition. This is the normal generated-to-generated hot path.
|
||||
if (TryGetCpuContext() == cpu) {
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(
|
||||
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CpuContextScope contextScope(cpu);
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
|
||||
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
|
||||
// in the run's log folder, popup, exit.
|
||||
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
|
||||
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
|
||||
// Only latch a result once the registry is published (immutable for the process lifetime);
|
||||
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
|
||||
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
|
||||
static std::atomic<uintptr_t> cachedValue{0};
|
||||
constexpr uintptr_t kNegativeCache = 1;
|
||||
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kNegativeCache
|
||||
? nullptr
|
||||
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
|
||||
return ResolveDirectCpuTargetInfo<Target>();
|
||||
}
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
cachedValue.store(info != nullptr
|
||||
? reinterpret_cast<uintptr_t>(info)
|
||||
: kNegativeCache,
|
||||
std::memory_order_release);
|
||||
return info;
|
||||
}
|
||||
|
||||
// Caches the "is the published winner still base translation?" verdict for state-free fast
|
||||
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
|
||||
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
|
||||
template <uint32_t Target>
|
||||
inline bool IsBaseTranslatedCpuTargetActive() {
|
||||
static std::atomic<uint8_t> cachedVerdict{0};
|
||||
constexpr uint8_t kNegativeVerdict = 1;
|
||||
constexpr uint8_t kPositiveVerdict = 2;
|
||||
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kPositiveVerdict;
|
||||
}
|
||||
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
|
||||
if (publishable) {
|
||||
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
|
||||
std::memory_order_release);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void InvokeDirectCpu(CpuContext* ctx) {
|
||||
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
ApplyRuntimeCallOptions(Target, cpu);
|
||||
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
|
||||
const auto invokeKnownNative = [&]() {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
return;
|
||||
}
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownNative();
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownNative();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
} else {
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
|
||||
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
|
||||
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportMissingCpuTarget(Target, cpu);
|
||||
}
|
||||
|
||||
// Indirect jump (bctr without link) - used for switch tables.
|
||||
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
|
||||
// Use the caller's context, not the persistent global context!
|
||||
// This is essential for tail calls via bctr where the 'this' pointer (r3)
|
||||
// must be passed correctly to the target function.
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cold fallback for dynamically registered or signature-only targets.
|
||||
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
|
||||
if (info) {
|
||||
if (TryDispatchResolvedCpuTarget(info, cpu)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If not registered, this is likely a switch table jump to an intra-function label.
|
||||
// The translator should have recognized this pattern and generated proper switch code.
|
||||
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
|
||||
<< std::hex << target << std::dec
|
||||
<< " is not a registered function.\n"
|
||||
<< "This is likely a switch statement jump table that the translator\n"
|
||||
<< "failed to recognize. The function containing this bctr needs\n"
|
||||
<< "proper switch pattern recognition." << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
|
||||
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
|
||||
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
|
||||
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
|
||||
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
|
||||
DumpHostStackTraceForRuntimeHelper();
|
||||
std::fflush(stderr);
|
||||
std::ostringstream message;
|
||||
message << "The game stopped because an indirect jump targeted guest address 0x"
|
||||
<< std::hex << target
|
||||
<< ", but that address is not a registered translated function.\n\n"
|
||||
<< "The translator may have missed a switch/jump-table target.";
|
||||
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
|
||||
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
|
||||
// atexit reporter, so the crash log has to be written here.
|
||||
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
|
||||
SetRuntimeExitCode(EXIT_FAILURE);
|
||||
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
|
||||
MarkFatalErrorReported();
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (target == 0) {
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
ApplyRuntimeCallOptions(target, cpu);
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address);
|
||||
// Look up a registered function by host (native) address.
|
||||
// This enables stack trace symbolication for translated functions.
|
||||
std::optional<TranslatedFunctionInfo> FindTranslatedFunctionByHostAddress(uintptr_t hostAddr);
|
||||
|
||||
template <typename Signature>
|
||||
class AbiTrampoline;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
// Bump whenever guest_abi/, isa/ or ppc_isa_memory.h changes what translated code compiles against.
|
||||
#define MKW_GUEST_ABI_VERSION 1
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "isa/ppc_isa_fpenv.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
union PPC_FPR {
|
||||
uint64_t raw;
|
||||
double d;
|
||||
struct {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
float ps1; // Low word (Least Significant)
|
||||
float ps0; // High word (Most Significant)
|
||||
#else
|
||||
float ps0; // High word
|
||||
float ps1; // Low word
|
||||
#endif
|
||||
} paired;
|
||||
};
|
||||
|
||||
// PowerPC CPU Context
|
||||
struct CpuContext {
|
||||
// Standard GPRs
|
||||
uint32_t gpr[32];
|
||||
|
||||
// Special Purpose Registers defined by standard PPC
|
||||
uint32_t cr; // Condition Register
|
||||
uint32_t lr; // Link Register
|
||||
uint32_t ctr; // Count Register
|
||||
uint32_t xer; // Integer Exception Register
|
||||
uint32_t fpscr; // Floating-Point Status and Control Register
|
||||
|
||||
// Program State
|
||||
uint32_t pc; // Program Counter (not used alot tho)
|
||||
|
||||
// Floating Point Registers (Modified for Paired Single support)
|
||||
PPC_FPR fpr[32];
|
||||
|
||||
// Broadway Specific Extensions
|
||||
uint32_t gqr[8]; // Graphics Quantization Registers
|
||||
uint32_t hid0; // HID0
|
||||
uint32_t hid1; // HID1
|
||||
uint32_t hid2; // HID2
|
||||
|
||||
uint32_t srr0; // Save/Restore Register 0
|
||||
uint32_t srr1; // Save/Restore Register 1
|
||||
uint32_t msr; // Machine State Register
|
||||
};
|
||||
|
||||
inline thread_local CpuContext* g_currentCpuContext = nullptr;
|
||||
|
||||
class CpuContextScope {
|
||||
public:
|
||||
explicit CpuContextScope(CpuContext* ctx)
|
||||
: previous_(g_currentCpuContext)
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
~CpuContextScope()
|
||||
{
|
||||
g_currentCpuContext = previous_;
|
||||
if (previous_ != nullptr)
|
||||
MkwApplyHostNiMode(previous_->fpscr);
|
||||
else
|
||||
MkwRestoreHostMxcsr(savedMxcsr_);
|
||||
}
|
||||
|
||||
CpuContextScope(const CpuContextScope&) = delete;
|
||||
CpuContextScope& operator=(const CpuContextScope&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* previous_ = nullptr;
|
||||
uint32_t savedMxcsr_ = 0;
|
||||
};
|
||||
|
||||
inline CpuContext* TryGetCpuContext() noexcept
|
||||
{
|
||||
return g_currentCpuContext;
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
#pragma once
|
||||
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "isa/ppc_isa_config.h"
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RuntimeCrash {
|
||||
[[noreturn]] void FatalMissingGuestTarget(uint32_t target, struct CpuContext* cpu) noexcept;
|
||||
} // namespace RuntimeCrash
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
|
||||
|
||||
uint32_t FilterScnRendererPathMaskOption(uint32_t pathMask) noexcept;
|
||||
|
||||
// Some game-facing runtime options alter arguments at well-defined ABI
|
||||
// boundaries. Keep this independent of the dispatch mechanism: generated
|
||||
// static calls deliberately bypass InvokeDirectCpu for performance.
|
||||
// (used for the path mask filtering in ScnRenderer::createPath: depth of
|
||||
// field is always removed, bloom when the user disabled it)
|
||||
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
|
||||
if (target == 0x8023BD38u) {
|
||||
// ScnRenderer::createPath receives the post-processing path mask in r4.
|
||||
ctx->gpr[4] = FilterScnRendererPathMaskOption(ctx->gpr[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent per-thread CPU context used across translated function calls.
|
||||
CpuContext& GetPersistentCpuContext();
|
||||
|
||||
enum class FunctionKind : uint8_t {
|
||||
BaseTranslated = 0,
|
||||
ModTranslated = 1,
|
||||
Native = 2,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
|
||||
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
|
||||
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
|
||||
inline constexpr uint32_t kNativeFunctionPriority = 10000;
|
||||
|
||||
struct TranslatedFunctionInfo {
|
||||
uint32_t address = 0;
|
||||
const char* name = "";
|
||||
uint64_t moduleId = 0;
|
||||
uint32_t priority = 0;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
|
||||
void (*rawCpuInvoker)(CpuContext*) = nullptr;
|
||||
bool mustRemainDynamicallyDispatchable = true;
|
||||
FunctionKind kind = FunctionKind::BaseTranslated;
|
||||
};
|
||||
|
||||
struct RawDispatchRecord {
|
||||
uint32_t address = 0;
|
||||
void (*entry)(CpuContext*) = nullptr;
|
||||
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
bool preserveNonvolatileGprs = false;
|
||||
};
|
||||
|
||||
// Translator-owned indirect dispatch is split by the high address byte and
|
||||
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
|
||||
// page range that contains verified function entry points. The final search is
|
||||
// bounded to at most the 1024 aligned instruction addresses in one guest page.
|
||||
struct StaticIndirectDispatchPage {
|
||||
uint32_t firstEntry = 0;
|
||||
uint16_t entryCount = 0;
|
||||
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
|
||||
// literal third initializer, so it is part of the generated-data contract.
|
||||
uint16_t reserved = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchSegment {
|
||||
const StaticIndirectDispatchPage* pages = nullptr;
|
||||
uint16_t firstPage = 0;
|
||||
uint16_t pageCount = 0;
|
||||
};
|
||||
|
||||
struct StaticIndirectDispatchTable {
|
||||
const char* profileName = nullptr;
|
||||
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
|
||||
const RawDispatchRecord* entries = nullptr;
|
||||
size_t entryCount = 0;
|
||||
};
|
||||
|
||||
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
|
||||
const StaticIndirectDispatchTable* table,
|
||||
uint32_t address) noexcept {
|
||||
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& segment = table->segments[address >> 24];
|
||||
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
|
||||
if (!segment.pages || pageNumber < segment.firstPage) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t relativePage = pageNumber - segment.firstPage;
|
||||
if (relativePage >= segment.pageCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& page = segment.pages[relativePage];
|
||||
const size_t first = page.firstEntry;
|
||||
const size_t count = page.entryCount;
|
||||
if (first > table->entryCount || count > table->entryCount - first) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t lower = first;
|
||||
size_t upper = first + count;
|
||||
while (lower < upper) {
|
||||
const size_t middle = lower + (upper - lower) / 2;
|
||||
const uint32_t candidate = table->entries[middle].address;
|
||||
if (candidate < address) {
|
||||
lower = middle + 1;
|
||||
} else {
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
return lower < first + count && table->entries[lower].address == address
|
||||
? &table->entries[lower]
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
|
||||
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
|
||||
|
||||
class StaticIndirectDispatchTableRegistrar {
|
||||
public:
|
||||
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
|
||||
RegisterStaticIndirectDispatchTable(table);
|
||||
}
|
||||
};
|
||||
|
||||
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
|
||||
return static_cast<unsigned>(__builtin_ctz(value));
|
||||
}
|
||||
|
||||
class PpcNonvolatileFprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
|
||||
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
// Real masks are a single contiguous register run - f14..f31 for the
|
||||
// conservative default and f23..f31 or similar for a measured writer -
|
||||
// so resolve the run once and copy it as one straight-line block instead
|
||||
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
|
||||
// saves and restores exactly the same registers.
|
||||
const uint32_t lowest = mask_ & (0u - mask_);
|
||||
const uint32_t aboveRun = mask_ + lowest;
|
||||
if ((aboveRun & (aboveRun - 1u)) == 0u) {
|
||||
const unsigned first = PpcLowestSetBitIndex(mask_);
|
||||
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
|
||||
firstSlot_ = static_cast<uint8_t>(first - 14u);
|
||||
slotCount_ = static_cast<uint8_t>(end - first);
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
saved_[i] = cpu_->fpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileFprGuard() noexcept {
|
||||
if (!cpu_ || mask_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (slotCount_ != 0) {
|
||||
const unsigned first = 14u + firstSlot_;
|
||||
const size_t count = slotCount_;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
if ((mask_ & (1u << (14 + i))) == 0) {
|
||||
continue;
|
||||
}
|
||||
cpu_->fpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
|
||||
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
uint32_t mask_ = 0;
|
||||
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
|
||||
// a single run, so the destructor mirrors the constructor's per-bit loop.
|
||||
uint8_t firstSlot_ = 0;
|
||||
uint8_t slotCount_ = 0;
|
||||
// Every element selected by mask_ is written before it is read. Leaving the
|
||||
// remaining slots uninitialized avoids zero-filling all 18 values around a
|
||||
// call that may preserve only one or two architectural FPRs.
|
||||
std::array<PPC_FPR, 18> saved_;
|
||||
};
|
||||
|
||||
class PpcNonvolatileGprGuard {
|
||||
public:
|
||||
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
|
||||
: cpu_(enabled ? cpu : nullptr) {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
saved_[i] = cpu_->gpr[14 + i];
|
||||
}
|
||||
}
|
||||
|
||||
~PpcNonvolatileGprGuard() noexcept {
|
||||
if (!cpu_) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < saved_.size(); ++i) {
|
||||
cpu_->gpr[14 + i] = saved_[i];
|
||||
}
|
||||
}
|
||||
|
||||
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
|
||||
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* cpu_ = nullptr;
|
||||
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
|
||||
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
|
||||
std::array<uint32_t, 18> saved_;
|
||||
};
|
||||
|
||||
// Guest address of OSLoadContext, which models rfi-style context restoration
|
||||
// and intentionally replaces the full guest register file instead of returning
|
||||
// like a normal ABI call. It can never be guarded.
|
||||
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
|
||||
|
||||
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
|
||||
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
|
||||
}
|
||||
|
||||
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
|
||||
if (!info || info->nonvolatileFprWriteMask == 0) {
|
||||
return 0;
|
||||
}
|
||||
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTranslatedCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = true;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
// Translator-emitted trait specializations, one macro shared by every generated shard.
|
||||
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
|
||||
// publish their own name). A specialization only exists for a single statically bound winner,
|
||||
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
|
||||
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
|
||||
extern "C" void winner(CpuContext* ctx); \
|
||||
template <> \
|
||||
struct KnownTranslatedCpuCall<0x##addr##u> { \
|
||||
static constexpr bool kAvailable = true; \
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
|
||||
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
|
||||
static constexpr void (*Entry)(CpuContext*) = &winner; \
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
|
||||
static constexpr void (*Entry)(CpuContext*) = nullptr;
|
||||
};
|
||||
|
||||
template <uint32_t Target>
|
||||
struct KnownTypedNativeCpuCall {
|
||||
static constexpr bool kAvailable = false;
|
||||
};
|
||||
|
||||
#include "guest_abi/native_cpu_calls.inc"
|
||||
|
||||
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
|
||||
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
|
||||
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
|
||||
// power of two, the index masks with (size - 1).
|
||||
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
|
||||
|
||||
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
|
||||
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in guest_abi/cpu_context.h).
|
||||
struct IndirectResolvedDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const TranslatedFunctionInfo* info;
|
||||
};
|
||||
inline thread_local IndirectResolvedDispatchMemoEntry
|
||||
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
struct IndirectRawDispatchMemoEntry {
|
||||
bool valid;
|
||||
uint32_t address;
|
||||
const RawDispatchRecord* record;
|
||||
};
|
||||
inline thread_local IndirectRawDispatchMemoEntry
|
||||
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
|
||||
|
||||
class TranslatedFunctionRegistry {
|
||||
public:
|
||||
static void Register(TranslatedFunctionInfo info);
|
||||
static void Finalize();
|
||||
static inline bool IsLookupPublished() noexcept {
|
||||
return lookupPublished_.load(std::memory_order_acquire);
|
||||
}
|
||||
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return FindByAddressPtrSlow(address);
|
||||
}
|
||||
|
||||
auto& cached = g_indirectResolvedDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.info;
|
||||
}
|
||||
|
||||
const auto* info = FindByAddressPtrSlow(address);
|
||||
cached = {true, address, info};
|
||||
return info;
|
||||
}
|
||||
|
||||
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
|
||||
// generated-table binary search alongside it pushed the whole function past
|
||||
// the inliner's budget and out-of-lined the probe itself, which is the part
|
||||
// every bctrl executes.
|
||||
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
|
||||
if (!lookupPublished_.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& cached = g_indirectRawDispatchMemo[
|
||||
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
|
||||
if (cached.valid && cached.address == address) {
|
||||
return cached.record;
|
||||
}
|
||||
|
||||
return FindRawByAddressPtrMiss(address, cached);
|
||||
}
|
||||
|
||||
private:
|
||||
// Memo miss: resolve against the header-inline generated table first; only dynamically
|
||||
// registered records or a not-yet-published table fall through to the slow path.
|
||||
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
|
||||
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
|
||||
const RawDispatchRecord* record = nullptr;
|
||||
if (const auto* table =
|
||||
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
|
||||
record = FindStaticIndirectDispatchEntry(table, address);
|
||||
}
|
||||
if (record == nullptr) {
|
||||
record = FindRawByAddressPtrSlow(address);
|
||||
}
|
||||
cached = {true, address, record};
|
||||
return record;
|
||||
}
|
||||
|
||||
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
|
||||
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
|
||||
|
||||
// Finalize publishes immutable registry, generated-table, and dynamic raw
|
||||
// dispatch storage. Header-inlined cache hits are enabled only after that
|
||||
// release publication, so a pre-finalization miss can never poison them.
|
||||
inline static std::atomic_bool lookupPublished_{false};
|
||||
};
|
||||
|
||||
// Cold, profile-owned registration data is emitted in compact table shards.
|
||||
// Keeping it out of generated function bodies avoids one static constructor
|
||||
// per translated function.
|
||||
struct BulkTranslatedFunctionRecord {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
void (*entry)(CpuContext*);
|
||||
FunctionKind kind;
|
||||
bool preservesNonvolatileFprs;
|
||||
uint32_t nonvolatileFprWriteMask;
|
||||
uint32_t priority;
|
||||
uint64_t moduleId;
|
||||
bool mustRemainDynamicallyDispatchable;
|
||||
};
|
||||
|
||||
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
|
||||
|
||||
class BulkTranslatedFunctionRegistrar {
|
||||
public:
|
||||
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
|
||||
RegisterBulkTranslatedFunctions(records, count);
|
||||
}
|
||||
};
|
||||
|
||||
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
|
||||
if (!info || !info->rawCpuInvoker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
info->rawCpuInvoker(cpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
|
||||
if (!record || !record->entry) {
|
||||
return false;
|
||||
}
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
|
||||
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
record->entry(cpu);
|
||||
} else {
|
||||
record->entry(cpu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
|
||||
const auto invokeKnownTranslated = [&]() {
|
||||
KnownTranslatedCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
|
||||
// A statically resolved, non-overridable translated call already running
|
||||
// in this CpuContext needs no registry, context, or diagnostic target
|
||||
// transition. This is the normal generated-to-generated hot path.
|
||||
if (TryGetCpuContext() == cpu) {
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(
|
||||
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CpuContextScope contextScope(cpu);
|
||||
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownTranslated();
|
||||
return;
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownTranslated();
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
|
||||
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
|
||||
// in the run's log folder, popup, exit.
|
||||
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
|
||||
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
|
||||
// Only latch a result once the registry is published (immutable for the process lifetime);
|
||||
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
|
||||
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
|
||||
static std::atomic<uintptr_t> cachedValue{0};
|
||||
constexpr uintptr_t kNegativeCache = 1;
|
||||
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kNegativeCache
|
||||
? nullptr
|
||||
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
|
||||
return ResolveDirectCpuTargetInfo<Target>();
|
||||
}
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
cachedValue.store(info != nullptr
|
||||
? reinterpret_cast<uintptr_t>(info)
|
||||
: kNegativeCache,
|
||||
std::memory_order_release);
|
||||
return info;
|
||||
}
|
||||
|
||||
// Caches the "is the published winner still base translation?" verdict for state-free fast
|
||||
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
|
||||
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
|
||||
template <uint32_t Target>
|
||||
inline bool IsBaseTranslatedCpuTargetActive() {
|
||||
static std::atomic<uint8_t> cachedVerdict{0};
|
||||
constexpr uint8_t kNegativeVerdict = 1;
|
||||
constexpr uint8_t kPositiveVerdict = 2;
|
||||
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
|
||||
if (cached != 0) {
|
||||
return cached == kPositiveVerdict;
|
||||
}
|
||||
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
|
||||
const auto* info = ResolveDirectCpuTargetInfo<Target>();
|
||||
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
|
||||
if (publishable) {
|
||||
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
|
||||
std::memory_order_release);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
template <uint32_t Target>
|
||||
inline void InvokeDirectCpu(CpuContext* ctx) {
|
||||
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
ApplyRuntimeCallOptions(Target, cpu);
|
||||
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
|
||||
const auto invokeKnownNative = [&]() {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
return;
|
||||
}
|
||||
KnownNativeCpuCall<Target>::Entry(cpu);
|
||||
};
|
||||
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
|
||||
invokeKnownNative();
|
||||
} else {
|
||||
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
|
||||
invokeKnownNative();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
|
||||
PpcNonvolatileGprGuard gprGuard(cpu);
|
||||
if (TryGetCpuContext() != cpu) {
|
||||
CpuContextScope scope(cpu);
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
} else {
|
||||
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
|
||||
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
|
||||
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportMissingCpuTarget(Target, cpu);
|
||||
}
|
||||
|
||||
// Cold tail of InvokeIndirectJump: a bctr target that is not a registered function.
|
||||
[[noreturn]] MKW_PPC_NO_INLINE MKW_PPC_COLD void ReportMissingIndirectJumpTarget(uint32_t target, CpuContext* cpu);
|
||||
|
||||
// Indirect jump (bctr without link) - used for switch tables.
|
||||
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
|
||||
// Use the caller's context, not the persistent global context!
|
||||
// This is essential for tail calls via bctr where the 'this' pointer (r3)
|
||||
// must be passed correctly to the target function.
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cold fallback for dynamically registered or signature-only targets.
|
||||
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
|
||||
if (info) {
|
||||
if (TryDispatchResolvedCpuTarget(info, cpu)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ReportMissingIndirectJumpTarget(target, cpu);
|
||||
}
|
||||
|
||||
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
|
||||
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
|
||||
if (target == 0) {
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
ApplyRuntimeCallOptions(target, cpu);
|
||||
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
|
||||
return;
|
||||
}
|
||||
ReportMissingCpuTarget(target, cpu);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RecompMod {
|
||||
|
||||
// Diagnostic-only, read by fatal reporters (unmapped access, executable-write, CPU dumps).
|
||||
// Defined here as `inline thread_local` with a constant initializer, not `extern thread_local`
|
||||
// in the .cpp: every indirect dispatch scopes this, so an out-of-line ctor/dtor would cost two
|
||||
// un-inlinable calls plus a register spill each for three instructions of work.
|
||||
inline thread_local uint32_t g_currentTranslatedExecutionAddress = 0;
|
||||
|
||||
class ScopedTranslatedExecutionAddress {
|
||||
public:
|
||||
explicit ScopedTranslatedExecutionAddress(uint32_t address) noexcept
|
||||
: previous_(g_currentTranslatedExecutionAddress) {
|
||||
// Address 0 means "nothing new to report" - the enclosing scope's value
|
||||
// stays visible, and the unconditional restore below keeps nesting exact.
|
||||
if (address != 0) {
|
||||
g_currentTranslatedExecutionAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedTranslatedExecutionAddress() noexcept {
|
||||
g_currentTranslatedExecutionAddress = previous_;
|
||||
}
|
||||
|
||||
ScopedTranslatedExecutionAddress(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
ScopedTranslatedExecutionAddress& operator=(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
|
||||
private:
|
||||
uint32_t previous_ = 0;
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageShift = 12;
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageCount = 1u << (32 - kExecutableWriteGuardPageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageShift = 20;
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageCount = 1u << (32 - kExecutableWriteGuardCoarsePageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageShift = 16;
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageCount = 1u << (32 - kExecutableWriteGuardMidPageShift);
|
||||
|
||||
extern std::atomic<bool> g_executableWriteGuardEnabled;
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount];
|
||||
|
||||
} // namespace RecompMod
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// Flat 4 GiB guest address space constants used by translated accesses; see guest_flat_memory.h.
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_abi/flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
} // namespace GuestFlat
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// The only header translated code includes. Everything reachable from here is the
|
||||
// guest ABI; runtime-only headers must never be pulled in.
|
||||
#include "guest_abi/abi_version.h"
|
||||
|
||||
#include "isa/ppc_isa_config.h"
|
||||
#include "isa/big_endian.h"
|
||||
#include "isa/ppc_isa_fpenv.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "isa/ppc_isa_int.h"
|
||||
#include "isa/ppc_isa_float.h"
|
||||
#include "guest_abi/flat_memory.h"
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "guest_abi/memory.h"
|
||||
#include "isa/ppc_isa_quantized.h"
|
||||
#include "isa/ppc_isa_cr.h"
|
||||
#include "guest_abi/guest_cr.h"
|
||||
#include "guest_abi/dispatch.h"
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#include "guest_abi/guest_abi.h"
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// Condition Register Fields
|
||||
#define CR_LT 0
|
||||
#define CR_GT 1
|
||||
#define CR_EQ 2
|
||||
#define CR_SO 3
|
||||
|
||||
[[noreturn]] void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details);
|
||||
|
||||
// Used by generated code when the translator encounters privileged/unmodeled PPC instructions
|
||||
// (e.g. rfi). This is intentionally loud so missing HLE hooks are easy to find.
|
||||
#define UNDEFINED(pc, rawInstruction, details) PPC_Undefined((pc), (rawInstruction), (details))
|
||||
|
||||
// Helper to set CR bits (Signed)
|
||||
inline void SetCR(CpuContext* cpu, int field, int32_t a, int32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Unsigned)
|
||||
inline void SetCR(CpuContext* cpu, int field, uint32_t a, uint32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Floating-point)
|
||||
inline void SetCRFloat(CpuContext* cpu, int field, double a, double b) {
|
||||
uint32_t crField = 0;
|
||||
if (std::isnan(a) || std::isnan(b)) {
|
||||
// Unordered: LT/GT/EQ clear, SO set.
|
||||
crField = 0x1;
|
||||
} else {
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
}
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to get CR bit
|
||||
inline bool GetCRBit(CpuContext* cpu, int field, int bit) {
|
||||
int shift = (7 - field) * 4 + (3 - bit);
|
||||
return (cpu->cr >> shift) & 1;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Memory {
|
||||
public:
|
||||
using DeferredReadCallback = bool (*)(void* user);
|
||||
|
||||
static constexpr size_t kMem1Size = 24u * 1024u * 1024u;
|
||||
static constexpr size_t kMem2Size = 128u * 1024u * 1024u;
|
||||
static constexpr uint32_t kMem1PhysicalBase = 0x00000000u;
|
||||
static constexpr uint32_t kMem1CachedBase = 0x80000000u;
|
||||
static constexpr uint32_t kMem1UncachedBase = 0xC0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalBase = 0x10000000u;
|
||||
static constexpr uint32_t kMem2CachedBase = 0x90000000u;
|
||||
static constexpr uint32_t kMem2UncachedBase = 0xD0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalEnd =
|
||||
kMem2PhysicalBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2CachedEnd =
|
||||
kMem2CachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2UncachedEnd =
|
||||
kMem2UncachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
|
||||
struct RegionConfig {
|
||||
std::string name;
|
||||
uint32_t baseAddress = 0;
|
||||
size_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<RegionConfig> regions;
|
||||
static Config WiiDefaults();
|
||||
};
|
||||
|
||||
class AccessViolation : public std::runtime_error {
|
||||
public:
|
||||
AccessViolation(uint32_t address, size_t length, std::string_view reason);
|
||||
|
||||
uint32_t address() const noexcept { return address_; }
|
||||
size_t length() const noexcept { return length_; }
|
||||
std::string_view reason() const noexcept { return reason_; }
|
||||
|
||||
private:
|
||||
uint32_t address_ = 0;
|
||||
size_t length_ = 0;
|
||||
std::string reason_;
|
||||
};
|
||||
|
||||
static void Init(const Config& config);
|
||||
// Single flat MEM1 region. Not used by the shipped runtime, but the
|
||||
// translator integration-test harnesses emit calls to it.
|
||||
static void Init(size_t mem1Size);
|
||||
static void Reset();
|
||||
// Executable ranges are normally registered during startup. Rebuild the
|
||||
// writable fast-path classification after each registration so a page
|
||||
// previously classified as ordinary data cannot retain a stale direct
|
||||
// write bias.
|
||||
static void RefreshWritableFastPathsForExecutableRanges();
|
||||
|
||||
static uint8_t Read8(uint32_t addr);
|
||||
static uint16_t Read16(uint32_t addr);
|
||||
static uint32_t Read32(uint32_t addr);
|
||||
static uint64_t Read64(uint32_t addr);
|
||||
static float ReadFloat32(uint32_t addr);
|
||||
static double ReadFloat64(uint32_t addr);
|
||||
static void Write8(uint32_t addr, uint8_t val);
|
||||
static void Write16(uint32_t addr, uint16_t val);
|
||||
static void Write32(uint32_t addr, uint32_t val);
|
||||
static void Write64(uint32_t addr, uint64_t val);
|
||||
static void WriteFloat32(uint32_t addr, double val);
|
||||
static void WriteFloat64(uint32_t addr, double val);
|
||||
|
||||
// Exception-safe scalar access for HLE code. These keep Read32/Write32's
|
||||
// full mapping behavior and only convert an unmapped address into a failure
|
||||
// result; they are deliberately not MemoryInline::Try*GuestScalar, which is
|
||||
// the translated-code fast path over the page table.
|
||||
static bool TryRead32(uint32_t addr, uint32_t& value) noexcept {
|
||||
try {
|
||||
value = Read32(addr);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryWrite32(uint32_t addr, uint32_t value) noexcept {
|
||||
try {
|
||||
Write32(addr, value);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t* GetPointer(uint32_t addr);
|
||||
static uint8_t* GetPointer(uint32_t addr, size_t length);
|
||||
static bool Contains(uint32_t addr, size_t length = 1);
|
||||
|
||||
static uint64_t RegisterDeferredRead(uint32_t addr, size_t length,
|
||||
DeferredReadCallback callback, void* user);
|
||||
static void ClearDeferredReads();
|
||||
|
||||
// sizeBytes reports the storage actually allocated, which is not always the
|
||||
// configured size (aliased MEM1/MEM2 windows are clamped to what is behind
|
||||
// them). Used by the crash dump.
|
||||
static std::vector<RegionConfig> DescribeRegions();
|
||||
};
|
||||
|
||||
#include "guest_abi/memory_access.h"
|
||||
@@ -0,0 +1,732 @@
|
||||
#pragma once
|
||||
|
||||
#include "guest_abi/exec_guard.h"
|
||||
#include "guest_abi/flat_memory.h"
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
// Hooks for GX HLE FIFO handling
|
||||
extern "C" {
|
||||
void GX_HLE_FIFO_WriteFloat(float val);
|
||||
void GX_HLE_FIFO_Write32(uint32_t val);
|
||||
void GX_HLE_FIFO_Write16(uint16_t val);
|
||||
void GX_HLE_FIFO_Write8(uint8_t val);
|
||||
void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
}
|
||||
|
||||
namespace MemoryInline {
|
||||
#if defined(_WIN32)
|
||||
#define MKW_MEMORY_FORCE_INLINE __forceinline
|
||||
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
// See runtime/include/isa/ppc_isa_config.h for why non-Windows Clang needs the GNU-attribute
|
||||
// spellings instead of the MS-extension keywords.
|
||||
#define MKW_MEMORY_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_MEMORY_NO_INLINE __attribute__((noinline))
|
||||
#endif
|
||||
#define MKW_MEMORY_COLD __attribute__((cold))
|
||||
inline constexpr uint32_t kPageShift = 20;
|
||||
inline constexpr uint32_t kPageSize = 1u << kPageShift;
|
||||
inline constexpr uint32_t kPageMask = kPageSize - 1u;
|
||||
inline constexpr uint32_t kPageCount = 1u << (32 - kPageShift);
|
||||
inline constexpr uint32_t kMaxFastScalarSize = 8;
|
||||
inline constexpr uint32_t kWritableSubPageShift = RecompMod::kExecutableWriteGuardPageShift;
|
||||
inline constexpr uint32_t kWritableSubPageSize = 1u << kWritableSubPageShift;
|
||||
inline constexpr uint32_t kWritableSubPagesPerPage = kPageSize / kWritableSubPageSize;
|
||||
|
||||
struct PageEntry {
|
||||
uint8_t* base = nullptr;
|
||||
uint32_t limit = 0;
|
||||
};
|
||||
|
||||
extern PageEntry g_pageTable[kPageCount];
|
||||
// Encoded (host page base - guest page base) + 1 for full pages whose next
|
||||
// page is contiguous. This permits any native access up to 8 bytes without a
|
||||
// per-access mask/limit check. Zero retains the general PageEntry fallback.
|
||||
extern uintptr_t g_fullPageBias[kPageCount];
|
||||
// Runtime-active readable biases. Deferred-read pages clear these entries once
|
||||
// instead of paying a mode branch on every translated read.
|
||||
extern uintptr_t g_fullReadablePageBias[kPageCount];
|
||||
// Same encoding, but only for pages proven not to contain executable bytes.
|
||||
// Executable-range registration invalidates entries before guest execution.
|
||||
extern uintptr_t g_fullWritablePageBias[kPageCount];
|
||||
|
||||
// Allocated only for mapped 1 MiB pages that contain both executable and data
|
||||
// 4 KiB pages. Entries use the same encoded host bias as the coarse table.
|
||||
// Exact executable bits remain authoritative and are checked at lookup time,
|
||||
// including both sides of a cross-4-KiB access.
|
||||
struct SparseWritablePageTable {
|
||||
uintptr_t encodedBias[kWritableSubPagesPerPage]{};
|
||||
};
|
||||
extern const SparseWritablePageTable* g_sparseWritablePageTables[kPageCount];
|
||||
|
||||
// Nonzero while any registered deferred read overlaps the page. Small
|
||||
// mappings (e.g. the 16 KiB locked cache) have no coarse bias entry, so a
|
||||
// zero readable bias alone cannot distinguish "deferred content pending"
|
||||
// from "small but plain memory"; range resolution needs the explicit flag.
|
||||
extern uint8_t g_deferredReadCoveredPages[kPageCount];
|
||||
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint8_t Read8Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint16_t Read16Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint32_t Read32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint64_t Read64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadFloat32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadFloat64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write8Slow(uint32_t addr, uint8_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write16Slow(uint32_t addr, uint16_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write32Slow(uint32_t addr, uint32_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write64Slow(uint32_t addr, uint64_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat32Slow(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat64Slow(uint32_t addr, double val);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD T ReadResolvedFallback(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadResolvedFallbackFloat32(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadResolvedFallbackFloat64(uint32_t addr);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallback(uint32_t addr, T value);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat32(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat64(uint32_t addr, double val);
|
||||
bool ResolveDeferredReads(uint32_t addr, size_t length);
|
||||
|
||||
constexpr bool IsMmioAddress(uint32_t addr) {
|
||||
return addr >= 0xCC000000u && addr < 0xCE000000u;
|
||||
}
|
||||
|
||||
constexpr bool IsGpuFifoAddress(uint32_t addr) {
|
||||
return addr >= 0xCC008000u && addr < 0xCC008100u;
|
||||
}
|
||||
|
||||
// Page protections can't cover this: an MMIO write must reach GX HLE with its value or be
|
||||
// reported, and a fault record can't carry the value, so this mask/compare sits in front of
|
||||
// every flat store instead.
|
||||
MKW_MEMORY_FORCE_INLINE bool FlatWriteNeedsPolicy(uint32_t address) {
|
||||
return (address & 0xFE000000u) == 0xCC000000u; // 0xCC000000..0xCDFFFFFF
|
||||
}
|
||||
|
||||
// Gekko stfs conversion is a bit-level narrowing operation. In particular it
|
||||
// does not behave like a host double-to-float cast for values which were left
|
||||
// in double precision, and it has hardware-tested handling for tiny values.
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ConvertPpcDoubleToSingleBits(double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const uint32_t exponent = static_cast<uint32_t>((bits >> 52) & 0x7FFu);
|
||||
// The subnormal-single arm applies to exactly the exponents 874..896; every
|
||||
// other exponent takes the plain sign/exponent/fraction narrowing below.
|
||||
// Inside that window the exponent field is nonzero, so the magnitude cannot
|
||||
// be zero and needs no separate test - the zero case (exponent 0) reaches
|
||||
// the narrowing exactly as it did when the two arms shared that test.
|
||||
if (exponent - 874u <= 22u) [[unlikely]]
|
||||
{
|
||||
uint32_t narrowed = static_cast<uint32_t>(
|
||||
0x80000000ULL | ((bits & 0x000FFFFFFFFFFFFFULL) >> 21));
|
||||
narrowed >>= (905u - exponent);
|
||||
narrowed |= static_cast<uint32_t>((bits >> 32) & 0x80000000ULL);
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
// Results below the documented conversion range are architecturally
|
||||
// undefined; this is the behavior measured on Gekko/Broadway hardware.
|
||||
return static_cast<uint32_t>(
|
||||
((bits >> 32) & 0xC0000000ULL) | ((bits >> 29) & 0x3FFFFFFFULL));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float PpcSingleBitsToFloat(uint32_t bits) {
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetPointerFast(uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
const uint32_t page = address >> kPageShift;
|
||||
if (length <= 8) {
|
||||
const uintptr_t encodedBias = g_fullPageBias[page];
|
||||
if (encodedBias != 0) {
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[page];
|
||||
if (!entry.base || offset + length > entry.limit) {
|
||||
pointer = nullptr;
|
||||
return false;
|
||||
}
|
||||
pointer = entry.base + offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint8_t* GetPointerFast(uint32_t address, size_t length) {
|
||||
uint8_t* pointer = nullptr;
|
||||
return TryGetPointerFast(address, length, pointer) ? pointer : nullptr;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetWritablePointerFast(
|
||||
uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
// The sparse-table hit below distinguishes itself from the small-mapping
|
||||
// hit through `pointer`, so the out-parameter must start null regardless
|
||||
// of what the caller passed in. (A caller handing in an uninitialized
|
||||
// pointer used to turn every sparse hit into a write through stack
|
||||
// garbage - random host memory corruption.)
|
||||
pointer = nullptr;
|
||||
if (length == 0 || length > 8 || address > UINT32_MAX - (length - 1))
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(length - 1);
|
||||
const uint32_t firstExactPage = address >> kWritableSubPageShift;
|
||||
const uint32_t lastExactPage = endAddress >> kWritableSubPageShift;
|
||||
if (encodedBias != 0 && (endAddress >> kPageShift) != coarsePage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (encodedBias == 0) {
|
||||
const auto* subTable = g_sparseWritablePageTables[coarsePage];
|
||||
if (subTable != nullptr) {
|
||||
encodedBias = subTable->encodedBias[
|
||||
(address & kPageMask) >> kWritableSubPageShift];
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
} else {
|
||||
// Small mappings such as Broadway's 16 KiB locked cache cannot
|
||||
// populate the full-1-MiB bias table. Keep them native by proving
|
||||
// the exact access against the ordinary page entry, then applying
|
||||
// the same executable-write policy as a sparse-table hit.
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[coarsePage];
|
||||
if (!entry.base || offset + length > entry.limit)
|
||||
return false;
|
||||
pointer = entry.base + offset;
|
||||
}
|
||||
|
||||
// The exact 4 KiB guard is the final authority. Checking it on every
|
||||
// checked/sparse hit also makes later executable-range registration
|
||||
// safe when a prebuilt table still contains the old mapped bias.
|
||||
if (RecompMod::g_executableWriteGuardPages[firstExactPage].load(
|
||||
std::memory_order_relaxed) != 0 ||
|
||||
(lastExactPage != firstExactPage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointer != nullptr)
|
||||
return true;
|
||||
}
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flat form: guest_flat_memory.h's page protections already answer mapped/non-deferred/non-executable, so resolving is pure address arithmetic.
|
||||
// Two checks stay inline: a wrapped guest address can't survive 64-bit `host + rangeOffset`, and an MMIO write's value isn't recoverable from a
|
||||
// fault record, so a write touching that window must resolve null and fall back to Memory::Write*. Checking both range endpoints is a complete
|
||||
// proof since length <= kPageSize (1 MiB) can't straddle the 32 MiB MMIO window.
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffset, uint32_t length,
|
||||
bool needsRead, bool needsWrite) {
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + guestStart;
|
||||
}
|
||||
|
||||
// Guest-address byte order. Distinct from isa/big_endian.h, which is the
|
||||
// host-pointer codec; do not "unify" them.
|
||||
inline uint16_t ByteSwap16(uint16_t value) {
|
||||
return __builtin_bswap16(value);
|
||||
}
|
||||
|
||||
inline uint32_t ByteSwap32(uint32_t value) {
|
||||
return __builtin_bswap32(value);
|
||||
}
|
||||
|
||||
inline uint64_t ByteSwap64(uint64_t value) {
|
||||
return __builtin_bswap64(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T MaybeByteSwap(T value) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
return value;
|
||||
} else if constexpr (sizeof(T) == 2) {
|
||||
return static_cast<T>(ByteSwap16(static_cast<uint16_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
return static_cast<T>(ByteSwap32(static_cast<uint32_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 8) {
|
||||
return static_cast<T>(ByteSwap64(static_cast<uint64_t>(value)));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool ReadResolvedScalar(uint8_t* host, uint32_t rangeOffset, T& outValue) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = host[rangeOffset];
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, host + rangeOffset, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ResolvedLoadPair {
|
||||
uint32_t first = 0;
|
||||
uint32_t second = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset) {
|
||||
uint32_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {packed >> 16, packed & 0xFFFFu, true};
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair32(uint8_t* host, uint32_t rangeOffset) {
|
||||
uint64_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {static_cast<uint32_t>(packed >> 32), static_cast<uint32_t>(packed), true};
|
||||
}
|
||||
|
||||
template <typename Packed>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPairFast(
|
||||
uint8_t* host, uint32_t rangeOffset, Packed packed) {
|
||||
if (!host) return false;
|
||||
const Packed swapped = MaybeByteSwap(packed);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(swapped));
|
||||
return true;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset, uint32_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair32(
|
||||
uint8_t* host, uint32_t rangeOffset, uint64_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedScalar(uint8_t* host, uint32_t rangeOffset, T value) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
host[rangeOffset] = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool TryReadMappedScalar(uint32_t address, T& outValue) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryReadGuestScalar(uint32_t address, T& outValue) {
|
||||
const uintptr_t encodedBias = g_fullReadablePageBias[address >> kPageShift];
|
||||
if (encodedBias == 0) [[unlikely]]
|
||||
return false;
|
||||
auto* ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryWriteGuestScalar(uint32_t address, T value) {
|
||||
static_assert(sizeof(T) >= 1 && sizeof(T) <= kMaxFastScalarSize);
|
||||
// A mixed executable/data 1 MiB page zeroes the coarse writable bias even though most of
|
||||
// its 4 KiB sub-pages are plain data; MKW's THP buffers share such a page with .text, which
|
||||
// used to force ~15% of total CPU through the cold path. The sparse sub-page tier below
|
||||
// keeps those stores native while the exact 4 KiB executable guards stay authoritative.
|
||||
if (address > UINT32_MAX - static_cast<uint32_t>(sizeof(T) - 1u)) [[unlikely]]
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(sizeof(T) - 1u);
|
||||
if ((endAddress >> kPageShift) != coarsePage) [[unlikely]]
|
||||
return false;
|
||||
const uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
uint8_t* ptr = nullptr;
|
||||
if (encodedBias != 0) {
|
||||
ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
} else if (!TryGetWritablePointerFast(address, sizeof(T), ptr)) [[unlikely]] {
|
||||
return false;
|
||||
}
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool WriteStackScalarFast(uint32_t address, T value) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline uint8_t ReadStack8(uint32_t address) {
|
||||
uint8_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read8(address);
|
||||
}
|
||||
|
||||
inline uint16_t ReadStack16(uint32_t address) {
|
||||
uint16_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read16(address);
|
||||
}
|
||||
|
||||
inline uint32_t ReadStack32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read32(address);
|
||||
}
|
||||
|
||||
inline uint64_t ReadStack64(uint32_t address) {
|
||||
uint64_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read64(address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline void WriteStack8(uint32_t address, uint8_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write8(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack16(uint32_t address, uint16_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write16(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack32(uint32_t address, uint32_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack64(uint32_t address, uint64_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat32(uint32_t address, double value) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename SlowRead>
|
||||
MKW_MEMORY_FORCE_INLINE T ReadResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address,
|
||||
SlowRead slow) {
|
||||
T value = 0;
|
||||
if (ReadResolvedScalar(host, rangeOffset, value)) {
|
||||
return value;
|
||||
}
|
||||
[[unlikely]] return slow(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t ReadResolved8(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint8_t>(r, o, a, ReadResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t ReadResolved16(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint16_t>(r, o, a, ReadResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ReadResolved32(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint32_t>(r, o, a, ReadResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h (the psq resolved tier packs two lanes into
|
||||
// one 64-bit access); generated code never names it directly.
|
||||
MKW_MEMORY_FORCE_INLINE uint64_t ReadResolved64(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint64_t>(r, o, a, ReadResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE float ReadResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint32_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat32(a);
|
||||
float value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE double ReadResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint64_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat64(a);
|
||||
double value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
|
||||
template <typename T, typename SlowWrite>
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address, T value,
|
||||
SlowWrite slow) {
|
||||
if (WriteResolvedScalar(host, rangeOffset, value)) {
|
||||
return;
|
||||
}
|
||||
[[unlikely]] slow(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved8(uint8_t* r, uint32_t o, uint32_t a, uint8_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved16(uint8_t* r, uint32_t o, uint32_t a, uint16_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved32(uint8_t* r, uint32_t o, uint32_t a, uint32_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h, as ReadResolved64 above.
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved64(uint8_t* r, uint32_t o, uint32_t a, uint64_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(v);
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat32(a, v);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
uint64_t bits; std::memcpy(&bits, &v, sizeof(bits));
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat64(a, v);
|
||||
}
|
||||
|
||||
// Flat guest memory (audit item T-MEM): the 4 GiB reservation makes a guest access a byte swap
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
T value{};
|
||||
std::memcpy(&value, MKW_FLAT_GUEST_BASE + address, sizeof(T));
|
||||
return MaybeByteSwap(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
// Check-free stores: emitted ONLY for addresses the translator proved at translate time are ordinary guest RAM (r1-relative stack slots, ~45%
|
||||
// of flat stores), skipping the MMIO mask/compare that's pure overhead there. Still safe if that proof were ever wrong: the flat view maps
|
||||
// 0xCC000000..0xCDFFFFFF PAGE_NOACCESS, so a stray MMIO store faults into the same handler and diagnostic as the checked path, just reported
|
||||
// instead of dispatched inline. Never use these for an address the translator hasn't proven.
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam8(uint32_t address, uint8_t value) {
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam16(uint32_t address, uint16_t value) {
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam32(uint32_t address, uint32_t value) {
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat32(uint32_t address, double value) {
|
||||
FlatStore<uint32_t>(address, ConvertPpcDoubleToSingleBits(value));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
} // namespace MemoryInline
|
||||
|
||||
#undef MKW_MEMORY_FORCE_INLINE
|
||||
#undef MKW_MEMORY_NO_INLINE
|
||||
#undef MKW_MEMORY_COLD
|
||||
|
||||
inline uint8_t Memory::Read8(uint32_t addr) {
|
||||
uint8_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read8Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint16_t Memory::Read16(uint32_t addr) {
|
||||
uint16_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read16Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint32_t Memory::Read32(uint32_t addr) {
|
||||
uint32_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read32Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint64_t Memory::Read64(uint32_t addr) {
|
||||
uint64_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read64Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float Memory::ReadFloat32(uint32_t addr) {
|
||||
uint32_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat32Slow(addr);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline double Memory::ReadFloat64(uint32_t addr) {
|
||||
uint64_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat64Slow(addr);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void Memory::Write8(uint32_t addr, uint8_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write8Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write16(uint32_t addr, uint16_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write16Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write32(uint32_t addr, uint32_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write64(uint32_t addr, uint64_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write64Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat32(uint32_t addr, double val) {
|
||||
{
|
||||
const uint32_t bits = MemoryInline::ConvertPpcDoubleToSingleBits(val);
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat64(uint32_t addr, double val) {
|
||||
{
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &val, sizeof(bits));
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat64Slow(addr, val);
|
||||
}
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
// Native call catalog used by InvokeDirectCpu. Keep project-specific HLE
|
||||
// address mappings here; abi_bridge.h contains only the generic ABI machinery.
|
||||
// address mappings here; dispatch.h contains only the generic ABI machinery.
|
||||
// Part of the guest ABI: translated shards bind these calls at compile time.
|
||||
//
|
||||
// This is a .inc, not a header: it has no includes of its own and references
|
||||
// CpuContext, KnownTypedNativeCpuCall<> and KnownNativeCpuCall<>, all of which
|
||||
// are declared above its single mid-file include site in abi_bridge.h. It
|
||||
// are declared above its single mid-file include site in dispatch.h. It
|
||||
// cannot compile standalone and must never be included anywhere else.
|
||||
|
||||
extern "C" void nw4r__lyt__detail__DrawQuad_800847c0(CpuContext* ctx);
|
||||
@@ -9,39 +9,10 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "guest_abi/flat_memory.h"
|
||||
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
enum class Backing {
|
||||
Owned,
|
||||
Mem1,
|
||||
@@ -65,25 +36,6 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// PowerPC EABI small-data base pointers of the translated game. Defined per game
|
||||
// in runtime/game/runtime_config_values.cpp so the runtime itself stays game-independent.
|
||||
namespace GuestSmallData {
|
||||
extern const uint32_t kSda1Base; // _SDA_BASE_, loaded into r13
|
||||
extern const uint32_t kSda2Base; // _SDA2_BASE_, loaded into r2
|
||||
} // namespace GuestSmallData
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "ppc_isa_fpenv.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -10,85 +11,6 @@
|
||||
|
||||
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept;
|
||||
|
||||
union PPC_FPR {
|
||||
uint64_t raw;
|
||||
double d;
|
||||
struct {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
float ps1; // Low word (Least Significant)
|
||||
float ps0; // High word (Most Significant)
|
||||
#else
|
||||
float ps0; // High word
|
||||
float ps1; // Low word
|
||||
#endif
|
||||
} paired;
|
||||
};
|
||||
|
||||
// PowerPC CPU Context
|
||||
struct CpuContext {
|
||||
// Standard GPRs
|
||||
uint32_t gpr[32];
|
||||
|
||||
// Special Purpose Registers defined by standard PPC
|
||||
uint32_t cr; // Condition Register
|
||||
uint32_t lr; // Link Register
|
||||
uint32_t ctr; // Count Register
|
||||
uint32_t xer; // Integer Exception Register
|
||||
uint32_t fpscr; // Floating-Point Status and Control Register
|
||||
|
||||
// Program State
|
||||
uint32_t pc; // Program Counter (not used alot tho)
|
||||
|
||||
// Floating Point Registers (Modified for Paired Single support)
|
||||
PPC_FPR fpr[32];
|
||||
|
||||
// Broadway Specific Extensions
|
||||
uint32_t gqr[8]; // Graphics Quantization Registers
|
||||
uint32_t hid0; // HID0
|
||||
uint32_t hid1; // HID1
|
||||
uint32_t hid2; // HID2
|
||||
|
||||
uint32_t srr0; // Save/Restore Register 0
|
||||
uint32_t srr1; // Save/Restore Register 1
|
||||
uint32_t msr; // Machine State Register
|
||||
};
|
||||
|
||||
inline thread_local CpuContext* g_currentCpuContext = nullptr;
|
||||
|
||||
class CpuContextScope {
|
||||
public:
|
||||
explicit CpuContextScope(CpuContext* ctx)
|
||||
: previous_(g_currentCpuContext)
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
~CpuContextScope()
|
||||
{
|
||||
g_currentCpuContext = previous_;
|
||||
if (previous_ != nullptr)
|
||||
MkwApplyHostNiMode(previous_->fpscr);
|
||||
else
|
||||
MkwRestoreHostMxcsr(savedMxcsr_);
|
||||
}
|
||||
|
||||
CpuContextScope(const CpuContextScope&) = delete;
|
||||
CpuContextScope& operator=(const CpuContextScope&) = delete;
|
||||
|
||||
private:
|
||||
CpuContext* previous_ = nullptr;
|
||||
uint32_t savedMxcsr_ = 0;
|
||||
};
|
||||
|
||||
inline CpuContext* TryGetCpuContext() noexcept
|
||||
{
|
||||
return g_currentCpuContext;
|
||||
}
|
||||
|
||||
inline CpuContext* CurrentCpuContext()
|
||||
{
|
||||
CpuContext* cpu = TryGetCpuContext();
|
||||
@@ -110,79 +32,7 @@ inline CpuContext* CurrentCpuContext()
|
||||
return cpu;
|
||||
}
|
||||
|
||||
// Condition Register Fields
|
||||
#define CR_LT 0
|
||||
#define CR_GT 1
|
||||
#define CR_EQ 2
|
||||
#define CR_SO 3
|
||||
#include "guest_abi/guest_cr.h"
|
||||
|
||||
extern "C" void DumpHostStackTraceForRuntimeHelper();
|
||||
void MarkFatalErrorReported();
|
||||
|
||||
[[noreturn]] inline void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"[runtime] UNDEFINED guest instruction: pc=0x%08X raw=0x%08X %s\n",
|
||||
pc,
|
||||
rawInstruction,
|
||||
details ? details : "");
|
||||
char message[256]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"The game stopped because it reached an unsupported guest instruction at PC 0x%08X (instruction 0x%08X).\n\n%s",
|
||||
pc, rawInstruction, details ? details : "No additional details were provided.");
|
||||
ShowRuntimeFatalPopup("Unsupported guest instruction", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Used by generated code when the translator encounters privileged/unmodeled PPC instructions
|
||||
// (e.g. rfi). This is intentionally loud so missing HLE hooks are easy to find.
|
||||
#define UNDEFINED(pc, rawInstruction, details) PPC_Undefined((pc), (rawInstruction), (details))
|
||||
|
||||
// Helper to set CR bits (Signed)
|
||||
inline void SetCR(CpuContext* cpu, int field, int32_t a, int32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Unsigned)
|
||||
inline void SetCR(CpuContext* cpu, int field, uint32_t a, uint32_t b) {
|
||||
uint32_t crField = 0;
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
crField |= (cpu->xer >> 31) & 1u; // SO
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to set CR bits (Floating-point)
|
||||
inline void SetCRFloat(CpuContext* cpu, int field, double a, double b) {
|
||||
uint32_t crField = 0;
|
||||
if (std::isnan(a) || std::isnan(b)) {
|
||||
// Unordered: LT/GT/EQ clear, SO set.
|
||||
crField = 0x1;
|
||||
} else {
|
||||
if (a < b) crField |= 0x8; // LT
|
||||
if (a > b) crField |= 0x4; // GT
|
||||
if (a == b) crField |= 0x2; // EQ
|
||||
}
|
||||
|
||||
int shift = (7 - field) * 4;
|
||||
uint32_t mask = 0xF << shift;
|
||||
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
|
||||
}
|
||||
|
||||
// Helper to get CR bit
|
||||
inline bool GetCRBit(CpuContext* cpu, int field, int bit) {
|
||||
int shift = (7 - field) * 4 + (3 - bit);
|
||||
return (cpu->cr >> shift) & 1;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
// the psq_l/psq_st tier that does lives in ppc_isa_quantized.h.
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
#include "ppc_isa_context.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
@@ -34,7 +34,7 @@ inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
|
||||
inline thread_local double g_mkwNiFlushThreshold = 0.0;
|
||||
|
||||
// Host FP control register access (MXCSR on x86_64, FPCR on AArch64), abstracted so
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (ppc_isa_context.h) don't each need
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (guest_abi/cpu_context.h) don't each need
|
||||
// their own per-arch branch.
|
||||
inline uint32_t MkwGetHostFpControl() noexcept
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
#include "ppc_isa_context.h"
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "ppc_isa_float.h"
|
||||
#include "big_endian.h"
|
||||
|
||||
|
||||
+1
-103
@@ -8,109 +8,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class Memory {
|
||||
public:
|
||||
using DeferredReadCallback = bool (*)(void* user);
|
||||
|
||||
static constexpr size_t kMem1Size = 24u * 1024u * 1024u;
|
||||
static constexpr size_t kMem2Size = 128u * 1024u * 1024u;
|
||||
static constexpr uint32_t kMem1PhysicalBase = 0x00000000u;
|
||||
static constexpr uint32_t kMem1CachedBase = 0x80000000u;
|
||||
static constexpr uint32_t kMem1UncachedBase = 0xC0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalBase = 0x10000000u;
|
||||
static constexpr uint32_t kMem2CachedBase = 0x90000000u;
|
||||
static constexpr uint32_t kMem2UncachedBase = 0xD0000000u;
|
||||
static constexpr uint32_t kMem2PhysicalEnd =
|
||||
kMem2PhysicalBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2CachedEnd =
|
||||
kMem2CachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
static constexpr uint32_t kMem2UncachedEnd =
|
||||
kMem2UncachedBase + static_cast<uint32_t>(kMem2Size);
|
||||
|
||||
struct RegionConfig {
|
||||
std::string name;
|
||||
uint32_t baseAddress = 0;
|
||||
size_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<RegionConfig> regions;
|
||||
static Config WiiDefaults();
|
||||
};
|
||||
|
||||
class AccessViolation : public std::runtime_error {
|
||||
public:
|
||||
AccessViolation(uint32_t address, size_t length, std::string_view reason);
|
||||
|
||||
uint32_t address() const noexcept { return address_; }
|
||||
size_t length() const noexcept { return length_; }
|
||||
std::string_view reason() const noexcept { return reason_; }
|
||||
|
||||
private:
|
||||
uint32_t address_ = 0;
|
||||
size_t length_ = 0;
|
||||
std::string reason_;
|
||||
};
|
||||
|
||||
static void Init(const Config& config);
|
||||
// Single flat MEM1 region. Not used by the shipped runtime, but the
|
||||
// translator integration-test harnesses emit calls to it.
|
||||
static void Init(size_t mem1Size);
|
||||
static void Reset();
|
||||
// Executable ranges are normally registered during startup. Rebuild the
|
||||
// writable fast-path classification after each registration so a page
|
||||
// previously classified as ordinary data cannot retain a stale direct
|
||||
// write bias.
|
||||
static void RefreshWritableFastPathsForExecutableRanges();
|
||||
|
||||
static uint8_t Read8(uint32_t addr);
|
||||
static uint16_t Read16(uint32_t addr);
|
||||
static uint32_t Read32(uint32_t addr);
|
||||
static uint64_t Read64(uint32_t addr);
|
||||
static float ReadFloat32(uint32_t addr);
|
||||
static double ReadFloat64(uint32_t addr);
|
||||
static void Write8(uint32_t addr, uint8_t val);
|
||||
static void Write16(uint32_t addr, uint16_t val);
|
||||
static void Write32(uint32_t addr, uint32_t val);
|
||||
static void Write64(uint32_t addr, uint64_t val);
|
||||
static void WriteFloat32(uint32_t addr, double val);
|
||||
static void WriteFloat64(uint32_t addr, double val);
|
||||
|
||||
// Exception-safe scalar access for HLE code. These keep Read32/Write32's
|
||||
// full mapping behavior and only convert an unmapped address into a failure
|
||||
// result; they are deliberately not MemoryInline::Try*GuestScalar, which is
|
||||
// the translated-code fast path over the page table.
|
||||
static bool TryRead32(uint32_t addr, uint32_t& value) noexcept {
|
||||
try {
|
||||
value = Read32(addr);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryWrite32(uint32_t addr, uint32_t value) noexcept {
|
||||
try {
|
||||
Write32(addr, value);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t* GetPointer(uint32_t addr);
|
||||
static uint8_t* GetPointer(uint32_t addr, size_t length);
|
||||
static bool Contains(uint32_t addr, size_t length = 1);
|
||||
|
||||
static uint64_t RegisterDeferredRead(uint32_t addr, size_t length,
|
||||
DeferredReadCallback callback, void* user);
|
||||
static void ClearDeferredReads();
|
||||
|
||||
// sizeBytes reports the storage actually allocated, which is not always the
|
||||
// configured size (aliased MEM1/MEM2 windows are clamped to what is behind
|
||||
// them). Used by the crash dump.
|
||||
static std::vector<RegionConfig> DescribeRegions();
|
||||
};
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
// The translated-code access layer is kept separately from the public memory API.
|
||||
#include "memory_access.h"
|
||||
|
||||
@@ -4,729 +4,4 @@
|
||||
#include "memory.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
// Hooks for GX HLE FIFO handling
|
||||
extern "C" {
|
||||
void GX_HLE_FIFO_WriteFloat(float val);
|
||||
void GX_HLE_FIFO_Write32(uint32_t val);
|
||||
void GX_HLE_FIFO_Write16(uint16_t val);
|
||||
void GX_HLE_FIFO_Write8(uint8_t val);
|
||||
void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
}
|
||||
|
||||
namespace MemoryInline {
|
||||
#if defined(_WIN32)
|
||||
#define MKW_MEMORY_FORCE_INLINE __forceinline
|
||||
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
// See runtime/include/isa/ppc_isa_config.h for why non-Windows Clang needs the GNU-attribute
|
||||
// spellings instead of the MS-extension keywords.
|
||||
#define MKW_MEMORY_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_MEMORY_NO_INLINE __attribute__((noinline))
|
||||
#endif
|
||||
#define MKW_MEMORY_COLD __attribute__((cold))
|
||||
inline constexpr uint32_t kPageShift = 20;
|
||||
inline constexpr uint32_t kPageSize = 1u << kPageShift;
|
||||
inline constexpr uint32_t kPageMask = kPageSize - 1u;
|
||||
inline constexpr uint32_t kPageCount = 1u << (32 - kPageShift);
|
||||
inline constexpr uint32_t kMaxFastScalarSize = 8;
|
||||
inline constexpr uint32_t kWritableSubPageShift = RecompMod::kExecutableWriteGuardPageShift;
|
||||
inline constexpr uint32_t kWritableSubPageSize = 1u << kWritableSubPageShift;
|
||||
inline constexpr uint32_t kWritableSubPagesPerPage = kPageSize / kWritableSubPageSize;
|
||||
|
||||
struct PageEntry {
|
||||
uint8_t* base = nullptr;
|
||||
uint32_t limit = 0;
|
||||
};
|
||||
|
||||
extern PageEntry g_pageTable[kPageCount];
|
||||
// Encoded (host page base - guest page base) + 1 for full pages whose next
|
||||
// page is contiguous. This permits any native access up to 8 bytes without a
|
||||
// per-access mask/limit check. Zero retains the general PageEntry fallback.
|
||||
extern uintptr_t g_fullPageBias[kPageCount];
|
||||
// Runtime-active readable biases. Deferred-read pages clear these entries once
|
||||
// instead of paying a mode branch on every translated read.
|
||||
extern uintptr_t g_fullReadablePageBias[kPageCount];
|
||||
// Same encoding, but only for pages proven not to contain executable bytes.
|
||||
// Executable-range registration invalidates entries before guest execution.
|
||||
extern uintptr_t g_fullWritablePageBias[kPageCount];
|
||||
|
||||
// Allocated only for mapped 1 MiB pages that contain both executable and data
|
||||
// 4 KiB pages. Entries use the same encoded host bias as the coarse table.
|
||||
// Exact executable bits remain authoritative and are checked at lookup time,
|
||||
// including both sides of a cross-4-KiB access.
|
||||
struct SparseWritablePageTable {
|
||||
uintptr_t encodedBias[kWritableSubPagesPerPage]{};
|
||||
};
|
||||
extern const SparseWritablePageTable* g_sparseWritablePageTables[kPageCount];
|
||||
|
||||
// Nonzero while any registered deferred read overlaps the page. Small
|
||||
// mappings (e.g. the 16 KiB locked cache) have no coarse bias entry, so a
|
||||
// zero readable bias alone cannot distinguish "deferred content pending"
|
||||
// from "small but plain memory"; range resolution needs the explicit flag.
|
||||
extern uint8_t g_deferredReadCoveredPages[kPageCount];
|
||||
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint8_t Read8Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint16_t Read16Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint32_t Read32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint64_t Read64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadFloat32Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadFloat64Slow(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write8Slow(uint32_t addr, uint8_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write16Slow(uint32_t addr, uint16_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write32Slow(uint32_t addr, uint32_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write64Slow(uint32_t addr, uint64_t val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat32Slow(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat64Slow(uint32_t addr, double val);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD T ReadResolvedFallback(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadResolvedFallbackFloat32(uint32_t addr);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadResolvedFallbackFloat64(uint32_t addr);
|
||||
template <typename T>
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallback(uint32_t addr, T value);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat32(uint32_t addr, double val);
|
||||
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat64(uint32_t addr, double val);
|
||||
bool ResolveDeferredReads(uint32_t addr, size_t length);
|
||||
|
||||
constexpr bool IsMmioAddress(uint32_t addr) {
|
||||
return addr >= 0xCC000000u && addr < 0xCE000000u;
|
||||
}
|
||||
|
||||
constexpr bool IsGpuFifoAddress(uint32_t addr) {
|
||||
return addr >= 0xCC008000u && addr < 0xCC008100u;
|
||||
}
|
||||
|
||||
// Page protections can't cover this: an MMIO write must reach GX HLE with its value or be
|
||||
// reported, and a fault record can't carry the value, so this mask/compare sits in front of
|
||||
// every flat store instead.
|
||||
MKW_MEMORY_FORCE_INLINE bool FlatWriteNeedsPolicy(uint32_t address) {
|
||||
return (address & 0xFE000000u) == 0xCC000000u; // 0xCC000000..0xCDFFFFFF
|
||||
}
|
||||
|
||||
// Gekko stfs conversion is a bit-level narrowing operation. In particular it
|
||||
// does not behave like a host double-to-float cast for values which were left
|
||||
// in double precision, and it has hardware-tested handling for tiny values.
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ConvertPpcDoubleToSingleBits(double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const uint32_t exponent = static_cast<uint32_t>((bits >> 52) & 0x7FFu);
|
||||
// The subnormal-single arm applies to exactly the exponents 874..896; every
|
||||
// other exponent takes the plain sign/exponent/fraction narrowing below.
|
||||
// Inside that window the exponent field is nonzero, so the magnitude cannot
|
||||
// be zero and needs no separate test - the zero case (exponent 0) reaches
|
||||
// the narrowing exactly as it did when the two arms shared that test.
|
||||
if (exponent - 874u <= 22u) [[unlikely]]
|
||||
{
|
||||
uint32_t narrowed = static_cast<uint32_t>(
|
||||
0x80000000ULL | ((bits & 0x000FFFFFFFFFFFFFULL) >> 21));
|
||||
narrowed >>= (905u - exponent);
|
||||
narrowed |= static_cast<uint32_t>((bits >> 32) & 0x80000000ULL);
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
// Results below the documented conversion range are architecturally
|
||||
// undefined; this is the behavior measured on Gekko/Broadway hardware.
|
||||
return static_cast<uint32_t>(
|
||||
((bits >> 32) & 0xC0000000ULL) | ((bits >> 29) & 0x3FFFFFFFULL));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float PpcSingleBitsToFloat(uint32_t bits) {
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetPointerFast(uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
const uint32_t page = address >> kPageShift;
|
||||
if (length <= 8) {
|
||||
const uintptr_t encodedBias = g_fullPageBias[page];
|
||||
if (encodedBias != 0) {
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[page];
|
||||
if (!entry.base || offset + length > entry.limit) {
|
||||
pointer = nullptr;
|
||||
return false;
|
||||
}
|
||||
pointer = entry.base + offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint8_t* GetPointerFast(uint32_t address, size_t length) {
|
||||
uint8_t* pointer = nullptr;
|
||||
return TryGetPointerFast(address, length, pointer) ? pointer : nullptr;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool TryGetWritablePointerFast(
|
||||
uint32_t address, size_t length, uint8_t*& pointer) {
|
||||
// The sparse-table hit below distinguishes itself from the small-mapping
|
||||
// hit through `pointer`, so the out-parameter must start null regardless
|
||||
// of what the caller passed in. (A caller handing in an uninitialized
|
||||
// pointer used to turn every sparse hit into a write through stack
|
||||
// garbage - random host memory corruption.)
|
||||
pointer = nullptr;
|
||||
if (length == 0 || length > 8 || address > UINT32_MAX - (length - 1))
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(length - 1);
|
||||
const uint32_t firstExactPage = address >> kWritableSubPageShift;
|
||||
const uint32_t lastExactPage = endAddress >> kWritableSubPageShift;
|
||||
if (encodedBias != 0 && (endAddress >> kPageShift) != coarsePage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (encodedBias == 0) {
|
||||
const auto* subTable = g_sparseWritablePageTables[coarsePage];
|
||||
if (subTable != nullptr) {
|
||||
encodedBias = subTable->encodedBias[
|
||||
(address & kPageMask) >> kWritableSubPageShift];
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
} else {
|
||||
// Small mappings such as Broadway's 16 KiB locked cache cannot
|
||||
// populate the full-1-MiB bias table. Keep them native by proving
|
||||
// the exact access against the ordinary page entry, then applying
|
||||
// the same executable-write policy as a sparse-table hit.
|
||||
const uint32_t offset = address & kPageMask;
|
||||
const auto& entry = g_pageTable[coarsePage];
|
||||
if (!entry.base || offset + length > entry.limit)
|
||||
return false;
|
||||
pointer = entry.base + offset;
|
||||
}
|
||||
|
||||
// The exact 4 KiB guard is the final authority. Checking it on every
|
||||
// checked/sparse hit also makes later executable-range registration
|
||||
// safe when a prebuilt table still contains the old mapped bias.
|
||||
if (RecompMod::g_executableWriteGuardPages[firstExactPage].load(
|
||||
std::memory_order_relaxed) != 0 ||
|
||||
(lastExactPage != firstExactPage &&
|
||||
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
|
||||
std::memory_order_relaxed) != 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointer != nullptr)
|
||||
return true;
|
||||
}
|
||||
if (encodedBias == 0)
|
||||
return false;
|
||||
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flat form: guest_flat_memory.h's page protections already answer mapped/non-deferred/non-executable, so resolving is pure address arithmetic.
|
||||
// Two checks stay inline: a wrapped guest address can't survive 64-bit `host + rangeOffset`, and an MMIO write's value isn't recoverable from a
|
||||
// fault record, so a write touching that window must resolve null and fall back to Memory::Write*. Checking both range endpoints is a complete
|
||||
// proof since length <= kPageSize (1 MiB) can't straddle the 32 MiB MMIO window.
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffset, uint32_t length,
|
||||
bool needsRead, bool needsWrite) {
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + guestStart;
|
||||
}
|
||||
|
||||
// Guest-address byte order. Distinct from isa/big_endian.h, which is the
|
||||
// host-pointer codec; do not "unify" them.
|
||||
inline uint16_t ByteSwap16(uint16_t value) {
|
||||
return __builtin_bswap16(value);
|
||||
}
|
||||
|
||||
inline uint32_t ByteSwap32(uint32_t value) {
|
||||
return __builtin_bswap32(value);
|
||||
}
|
||||
|
||||
inline uint64_t ByteSwap64(uint64_t value) {
|
||||
return __builtin_bswap64(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T MaybeByteSwap(T value) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
return value;
|
||||
} else if constexpr (sizeof(T) == 2) {
|
||||
return static_cast<T>(ByteSwap16(static_cast<uint16_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
return static_cast<T>(ByteSwap32(static_cast<uint32_t>(value)));
|
||||
} else if constexpr (sizeof(T) == 8) {
|
||||
return static_cast<T>(ByteSwap64(static_cast<uint64_t>(value)));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool ReadResolvedScalar(uint8_t* host, uint32_t rangeOffset, T& outValue) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = host[rangeOffset];
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, host + rangeOffset, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ResolvedLoadPair {
|
||||
uint32_t first = 0;
|
||||
uint32_t second = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset) {
|
||||
uint32_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {packed >> 16, packed & 0xFFFFu, true};
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair32(uint8_t* host, uint32_t rangeOffset) {
|
||||
uint64_t packed = 0;
|
||||
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
|
||||
return {static_cast<uint32_t>(packed >> 32), static_cast<uint32_t>(packed), true};
|
||||
}
|
||||
|
||||
template <typename Packed>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPairFast(
|
||||
uint8_t* host, uint32_t rangeOffset, Packed packed) {
|
||||
if (!host) return false;
|
||||
const Packed swapped = MaybeByteSwap(packed);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(swapped));
|
||||
return true;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair16(
|
||||
uint8_t* host, uint32_t rangeOffset, uint32_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair32(
|
||||
uint8_t* host, uint32_t rangeOffset, uint64_t packed) {
|
||||
return WriteResolvedPairFast(host, rangeOffset, packed);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool WriteResolvedScalar(uint8_t* host, uint32_t rangeOffset, T value) {
|
||||
if (!host) return false;
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
host[rangeOffset] = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(host + rangeOffset, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool TryReadMappedScalar(uint32_t address, T& outValue) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryReadGuestScalar(uint32_t address, T& outValue) {
|
||||
const uintptr_t encodedBias = g_fullReadablePageBias[address >> kPageShift];
|
||||
if (encodedBias == 0) [[unlikely]]
|
||||
return false;
|
||||
auto* ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
outValue = *ptr;
|
||||
} else {
|
||||
T value = 0;
|
||||
std::memcpy(&value, ptr, sizeof(T));
|
||||
outValue = MaybeByteSwap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE bool TryWriteGuestScalar(uint32_t address, T value) {
|
||||
static_assert(sizeof(T) >= 1 && sizeof(T) <= kMaxFastScalarSize);
|
||||
// A mixed executable/data 1 MiB page zeroes the coarse writable bias even though most of
|
||||
// its 4 KiB sub-pages are plain data; MKW's THP buffers share such a page with .text, which
|
||||
// used to force ~15% of total CPU through the cold path. The sparse sub-page tier below
|
||||
// keeps those stores native while the exact 4 KiB executable guards stay authoritative.
|
||||
if (address > UINT32_MAX - static_cast<uint32_t>(sizeof(T) - 1u)) [[unlikely]]
|
||||
return false;
|
||||
const uint32_t coarsePage = address >> kPageShift;
|
||||
const uint32_t endAddress = address + static_cast<uint32_t>(sizeof(T) - 1u);
|
||||
if ((endAddress >> kPageShift) != coarsePage) [[unlikely]]
|
||||
return false;
|
||||
const uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
|
||||
uint8_t* ptr = nullptr;
|
||||
if (encodedBias != 0) {
|
||||
ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
|
||||
} else if (!TryGetWritablePointerFast(address, sizeof(T), ptr)) [[unlikely]] {
|
||||
return false;
|
||||
}
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool WriteStackScalarFast(uint32_t address, T value) {
|
||||
uint8_t* ptr = nullptr;
|
||||
if (TryGetPointerFast(address, sizeof(T), ptr)) {
|
||||
if constexpr (sizeof(T) == 1) {
|
||||
*ptr = static_cast<uint8_t>(value);
|
||||
} else {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(ptr, &swapped, sizeof(T));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline uint8_t ReadStack8(uint32_t address) {
|
||||
uint8_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read8(address);
|
||||
}
|
||||
|
||||
inline uint16_t ReadStack16(uint32_t address) {
|
||||
uint16_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read16(address);
|
||||
}
|
||||
|
||||
inline uint32_t ReadStack32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read32(address);
|
||||
}
|
||||
|
||||
inline uint64_t ReadStack64(uint32_t address) {
|
||||
uint64_t value = 0;
|
||||
return TryReadMappedScalar(address, value) ? value : Memory::Read64(address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline void WriteStack8(uint32_t address, uint8_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write8(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack16(uint32_t address, uint16_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write16(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack32(uint32_t address, uint32_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStack64(uint32_t address, uint64_t value) {
|
||||
if (!WriteStackScalarFast(address, value)) {
|
||||
Memory::Write64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat32(uint32_t address, double value) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat32(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteStackFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (!WriteStackScalarFast(address, bits)) {
|
||||
Memory::WriteFloat64(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename SlowRead>
|
||||
MKW_MEMORY_FORCE_INLINE T ReadResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address,
|
||||
SlowRead slow) {
|
||||
T value = 0;
|
||||
if (ReadResolvedScalar(host, rangeOffset, value)) {
|
||||
return value;
|
||||
}
|
||||
[[unlikely]] return slow(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t ReadResolved8(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint8_t>(r, o, a, ReadResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t ReadResolved16(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint16_t>(r, o, a, ReadResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t ReadResolved32(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint32_t>(r, o, a, ReadResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h (the psq resolved tier packs two lanes into
|
||||
// one 64-bit access); generated code never names it directly.
|
||||
MKW_MEMORY_FORCE_INLINE uint64_t ReadResolved64(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint64_t>(r, o, a, ReadResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE float ReadResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint32_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat32(a);
|
||||
float value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE double ReadResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a) {
|
||||
uint64_t bits = 0;
|
||||
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat64(a);
|
||||
double value; std::memcpy(&value, &bits, sizeof(value)); return value;
|
||||
}
|
||||
|
||||
template <typename T, typename SlowWrite>
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address, T value,
|
||||
SlowWrite slow) {
|
||||
if (WriteResolvedScalar(host, rangeOffset, value)) {
|
||||
return;
|
||||
}
|
||||
[[unlikely]] slow(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved8(uint8_t* r, uint32_t o, uint32_t a, uint8_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint8_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved16(uint8_t* r, uint32_t o, uint32_t a, uint16_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint16_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved32(uint8_t* r, uint32_t o, uint32_t a, uint32_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint32_t>); }
|
||||
// Live via isa/ppc_isa_quantized.h, as ReadResolved64 above.
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolved64(uint8_t* r, uint32_t o, uint32_t a, uint64_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint64_t>); }
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(v);
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat32(a, v);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a, double v) {
|
||||
uint64_t bits; std::memcpy(&bits, &v, sizeof(bits));
|
||||
if (WriteResolvedScalar(r, o, bits)) return;
|
||||
[[unlikely]] WriteResolvedFallbackFloat64(a, v);
|
||||
}
|
||||
|
||||
// Flat guest memory (audit item T-MEM): the 4 GiB reservation makes a guest access a byte swap
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
T value{};
|
||||
std::memcpy(&value, MKW_FLAT_GUEST_BASE + address, sizeof(T));
|
||||
return MaybeByteSwap(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
const T swapped = MaybeByteSwap(value);
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
// Check-free stores: emitted ONLY for addresses the translator proved at translate time are ordinary guest RAM (r1-relative stack slots, ~45%
|
||||
// of flat stores), skipping the MMIO mask/compare that's pure overhead there. Still safe if that proof were ever wrong: the flat view maps
|
||||
// 0xCC000000..0xCDFFFFFF PAGE_NOACCESS, so a stray MMIO store faults into the same handler and diagnostic as the checked path, just reported
|
||||
// instead of dispatched inline. Never use these for an address the translator hasn't proven.
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam8(uint32_t address, uint8_t value) {
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam16(uint32_t address, uint16_t value) {
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRam32(uint32_t address, uint32_t value) {
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat32(uint32_t address, double value) {
|
||||
FlatStore<uint32_t>(address, ConvertPpcDoubleToSingleBits(value));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat64(uint32_t address, double value) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
FlatStore<uint64_t>(address, bits);
|
||||
}
|
||||
|
||||
} // namespace MemoryInline
|
||||
|
||||
#undef MKW_MEMORY_FORCE_INLINE
|
||||
#undef MKW_MEMORY_NO_INLINE
|
||||
#undef MKW_MEMORY_COLD
|
||||
|
||||
inline uint8_t Memory::Read8(uint32_t addr) {
|
||||
uint8_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read8Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint16_t Memory::Read16(uint32_t addr) {
|
||||
uint16_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read16Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint32_t Memory::Read32(uint32_t addr) {
|
||||
uint32_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read32Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint64_t Memory::Read64(uint32_t addr) {
|
||||
uint64_t value = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
|
||||
return MemoryInline::Read64Slow(addr);
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float Memory::ReadFloat32(uint32_t addr) {
|
||||
uint32_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat32Slow(addr);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline double Memory::ReadFloat64(uint32_t addr) {
|
||||
uint64_t bits = 0;
|
||||
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
|
||||
return MemoryInline::ReadFloat64Slow(addr);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void Memory::Write8(uint32_t addr, uint8_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write8Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write16(uint32_t addr, uint16_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write16Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write32(uint32_t addr, uint32_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::Write64(uint32_t addr, uint64_t val) {
|
||||
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
|
||||
MemoryInline::Write64Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat32(uint32_t addr, double val) {
|
||||
{
|
||||
const uint32_t bits = MemoryInline::ConvertPpcDoubleToSingleBits(val);
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat32Slow(addr, val);
|
||||
}
|
||||
|
||||
inline void Memory::WriteFloat64(uint32_t addr, double val) {
|
||||
{
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &val, sizeof(bits));
|
||||
if (MemoryInline::TryWriteGuestScalar(addr, bits))
|
||||
return;
|
||||
}
|
||||
[[unlikely]] MemoryInline::WriteFloat64Slow(addr, val);
|
||||
}
|
||||
#include "guest_abi/memory_access.h"
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
// Host memory seam for isa/ppc_isa_quantized.h. That header includes
|
||||
// "ppc_isa_memory.h" by plain quoted name and expects whatever it resolves to
|
||||
// to supply the Memory / MemoryInline APIs and the flat-guest-base macros its
|
||||
// psq fast paths use. This file is this project's implementation of that
|
||||
// contract; memory.h chains memory_access.h, which defines MemoryInline.
|
||||
// psq fast paths use. Part of the guest ABI, so it resolves to guest_abi/ only.
|
||||
|
||||
#include "memory.h" // chains memory_access.h, which supplies MemoryInline and
|
||||
// includes guest_flat_memory.h for the flat-base macros
|
||||
#include "guest_abi/memory.h"
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
#include "isa/ppc_isa_int.h"
|
||||
#include "isa/ppc_isa_float.h"
|
||||
#include "isa/ppc_isa_quantized.h"
|
||||
#include "memory.h"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "guest_abi/exec_guard.h"
|
||||
|
||||
struct CpuContext;
|
||||
|
||||
namespace RecompMod {
|
||||
@@ -20,46 +22,6 @@ struct MemoryReservation {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Diagnostic-only, read by fatal reporters (unmapped access, executable-write, CPU dumps).
|
||||
// Defined here as `inline thread_local` with a constant initializer, not `extern thread_local`
|
||||
// in the .cpp: every indirect dispatch scopes this, so an out-of-line ctor/dtor would cost two
|
||||
// un-inlinable calls plus a register spill each for three instructions of work.
|
||||
inline thread_local uint32_t g_currentTranslatedExecutionAddress = 0;
|
||||
|
||||
class ScopedTranslatedExecutionAddress {
|
||||
public:
|
||||
explicit ScopedTranslatedExecutionAddress(uint32_t address) noexcept
|
||||
: previous_(g_currentTranslatedExecutionAddress) {
|
||||
// Address 0 means "nothing new to report" - the enclosing scope's value
|
||||
// stays visible, and the unconditional restore below keeps nesting exact.
|
||||
if (address != 0) {
|
||||
g_currentTranslatedExecutionAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedTranslatedExecutionAddress() noexcept {
|
||||
g_currentTranslatedExecutionAddress = previous_;
|
||||
}
|
||||
|
||||
ScopedTranslatedExecutionAddress(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
ScopedTranslatedExecutionAddress& operator=(const ScopedTranslatedExecutionAddress&) = delete;
|
||||
|
||||
private:
|
||||
uint32_t previous_ = 0;
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageShift = 12;
|
||||
inline constexpr uint32_t kExecutableWriteGuardPageCount = 1u << (32 - kExecutableWriteGuardPageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageShift = 20;
|
||||
inline constexpr uint32_t kExecutableWriteGuardCoarsePageCount = 1u << (32 - kExecutableWriteGuardCoarsePageShift);
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageShift = 16;
|
||||
inline constexpr uint32_t kExecutableWriteGuardMidPageCount = 1u << (32 - kExecutableWriteGuardMidPageShift);
|
||||
|
||||
extern std::atomic<bool> g_executableWriteGuardEnabled;
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount];
|
||||
extern std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount];
|
||||
|
||||
// Two initializer phases, both emitted by the translator's mod data-patch
|
||||
// writer. (A third, plain RegisterInitializer/RunInitializers pair existed with
|
||||
// no registrant on either side and was removed.)
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "generated/RuntimeConfig.h"
|
||||
#include "guest_small_data.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
// Nothing reads CpuContext by offset; these asserts just canary accidental field reordering
|
||||
@@ -253,6 +255,41 @@ std::string ValidateGeneratedIndirectDispatchLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t FilterScnRendererPathMaskOption(uint32_t pathMask) noexcept {
|
||||
return RuntimeGameGraphicsOptions::FilterScnRendererPathMask(pathMask);
|
||||
}
|
||||
|
||||
void ReportMissingIndirectJumpTarget(uint32_t target, CpuContext* cpu) {
|
||||
// If not registered, this is likely a switch table jump to an intra-function label.
|
||||
// The translator should have recognized this pattern and generated proper switch code.
|
||||
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
|
||||
<< std::hex << target << std::dec
|
||||
<< " is not a registered function.\n"
|
||||
<< "This is likely a switch statement jump table that the translator\n"
|
||||
<< "failed to recognize. The function containing this bctr needs\n"
|
||||
<< "proper switch pattern recognition." << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
|
||||
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
|
||||
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
|
||||
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
|
||||
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
|
||||
DumpHostStackTraceForRuntimeHelper();
|
||||
std::fflush(stderr);
|
||||
std::ostringstream message;
|
||||
message << "The game stopped because an indirect jump targeted guest address 0x"
|
||||
<< std::hex << target
|
||||
<< ", but that address is not a registered translated function.\n\n"
|
||||
<< "The translator may have missed a switch/jump-table target.";
|
||||
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
|
||||
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
|
||||
// atexit reporter, so the crash log has to be written here.
|
||||
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
|
||||
SetRuntimeExitCode(EXIT_FAILURE);
|
||||
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
|
||||
MarkFatalErrorReported();
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table) {
|
||||
std::lock_guard<std::mutex> lock(RegistryMutex());
|
||||
if (RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
@@ -403,7 +440,7 @@ const RawDispatchRecord* TranslatedFunctionRegistry::FindRawByAddressPtrSlow(uin
|
||||
return it != entries.end() && it->address == address ? &*it : nullptr;
|
||||
}
|
||||
|
||||
std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindByHostAddress(uintptr_t hostAddr) {
|
||||
std::optional<TranslatedFunctionInfo> FindTranslatedFunctionByHostAddress(uintptr_t hostAddr) {
|
||||
const bool frozen = RegistryFrozen().load(std::memory_order_acquire);
|
||||
if (!frozen) {
|
||||
std::lock_guard<std::mutex> lock(RegistryMutex());
|
||||
@@ -439,7 +476,7 @@ std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindByHostAddr
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<TranslatedFunctionInfo> TranslatedFunctionRegistry::FindNearestByAddress(uint32_t address) {
|
||||
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address) {
|
||||
std::optional<TranslatedFunctionInfo> best;
|
||||
uint32_t bestDelta = UINT32_MAX;
|
||||
const auto scan = [&] {
|
||||
@@ -478,9 +515,24 @@ void InitializePersistentCpuContext() {
|
||||
// seed the PowerPC ABI environmental registers if no one has yet:
|
||||
// r2 = _SDA2_BASE_ (read-only small data), r13 = _SDA_BASE_.
|
||||
if (g_persistentCpu.gpr[2] == 0) {
|
||||
g_persistentCpu.gpr[2] = RuntimeConfig::SDA2_BASE;
|
||||
g_persistentCpu.gpr[2] = GuestSmallData::kSda2Base;
|
||||
}
|
||||
if (g_persistentCpu.gpr[13] == 0) {
|
||||
g_persistentCpu.gpr[13] = RuntimeConfig::SDA1_BASE;
|
||||
g_persistentCpu.gpr[13] = GuestSmallData::kSda1Base;
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"[runtime] UNDEFINED guest instruction: pc=0x%08X raw=0x%08X %s\n",
|
||||
pc,
|
||||
rawInstruction,
|
||||
details ? details : "");
|
||||
char message[256]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"The game stopped because it reached an unsupported guest instruction at PC 0x%08X (instruction 0x%08X).\n\n%s",
|
||||
pc, rawInstruction, details ? details : "No additional details were provided.");
|
||||
ShowRuntimeFatalPopup("Unsupported guest instruction", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
@@ -93,12 +93,20 @@ extern "C" int32_t WUDGetStatus_HLE()
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
// WPADGetDataFormat reads the per-channel format set by WPADSetDataFormat. Can't reuse the
|
||||
// translated SDK implementation: it dereferences Bluetooth control blocks that HLE'd WPADInit
|
||||
// never constructs.
|
||||
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
|
||||
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
|
||||
// Bluetooth control blocks that HLE'd WPADInit never constructs.
|
||||
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
|
||||
{
|
||||
return g_state.contract.GetDataFormat(chan);
|
||||
const int32_t status = g_state.contract.GetDataFormat(chan);
|
||||
if (status < 0) {
|
||||
return status;
|
||||
}
|
||||
switch (WiiRemoteInput::EffectiveKind(chan)) {
|
||||
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
|
||||
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "ppc_runtime.h"
|
||||
#include "runtime_log.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "generated/RuntimeConfig.h"
|
||||
#include "guest_small_data.h"
|
||||
#include "os_internal.h"
|
||||
|
||||
extern "C" void func_801AADE0(CpuContext* ctx);
|
||||
@@ -41,7 +41,7 @@ struct AlarmProcessScope {
|
||||
// the other call sites have never logged.
|
||||
void EnsureSda1Base(CpuContext* cpu, bool logOnce = false)
|
||||
{
|
||||
if (cpu->gpr[13] == RuntimeConfig::SDA1_BASE) {
|
||||
if (cpu->gpr[13] == GuestSmallData::kSda1Base) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ void EnsureSda1Base(CpuContext* cpu, bool logOnce = false)
|
||||
RT_LOG(RT_TAG_OS) << "OS_HLE_ProcessAlarms: fixing r13 to SDA1 base" << std::endl;
|
||||
});
|
||||
}
|
||||
cpu->gpr[13] = RuntimeConfig::SDA1_BASE;
|
||||
cpu->gpr[13] = GuestSmallData::kSda1Base;
|
||||
}
|
||||
|
||||
void IncrementSchedulerDisableCount()
|
||||
|
||||
@@ -152,6 +152,8 @@ private:
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
extern const char* const kMkwSetupVersion;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string g_lastEntryLabel;
|
||||
@@ -382,11 +384,9 @@ void AttachParentConsoleForDiagnostics() {}
|
||||
// to build-fingerprint.json beside the executable. Surfaced at the top of the transcript so every
|
||||
// attached log self-identifies.
|
||||
std::string ReadInstalledSetupVersion() {
|
||||
#if defined(MKW_SETUP_VERSION)
|
||||
if (constexpr std::string_view builtIn = MKW_SETUP_VERSION; !builtIn.empty()) {
|
||||
if (const std::string_view builtIn = kMkwSetupVersion; !builtIn.empty()) {
|
||||
return std::string(builtIn);
|
||||
}
|
||||
#endif
|
||||
const auto directory = RuntimeConfigFile::ExecutableDirectory();
|
||||
if (!directory) {
|
||||
return {};
|
||||
@@ -630,7 +630,7 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
|
||||
symbol->MaxNameLen = MAX_SYM_NAME;
|
||||
if (symbolsReady && SymFromAddr(process, addr, &symbolDisp, symbol)) {
|
||||
symbolName = symbol->Name;
|
||||
} else if (auto info = TranslatedFunctionRegistry::FindByHostAddress(static_cast<uintptr_t>(addr))) {
|
||||
} else if (auto info = FindTranslatedFunctionByHostAddress(static_cast<uintptr_t>(addr))) {
|
||||
// DbgHelp could not name it; the translated-function registry can.
|
||||
translatedFuncName = info->name;
|
||||
ppcAddress = info->address;
|
||||
|
||||
@@ -57,8 +57,8 @@ std::optional<std::string> PrintableU32(uint32_t value) {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address) {
|
||||
auto best = TranslatedFunctionRegistry::FindNearestByAddress(address);
|
||||
std::optional<TranslatedFunctionInfo> FindNearbyTranslatedFunction(uint32_t address) {
|
||||
auto best = FindNearestTranslatedFunction(address);
|
||||
|
||||
// Crash-only heuristic: large gaps are usually data/FourCC values, not code offsets.
|
||||
if (best && address - best->address < 0x10000u) {
|
||||
@@ -161,7 +161,7 @@ void PrintGuestValue(std::ostream& os, std::string_view label, uint32_t value) {
|
||||
} else {
|
||||
os << "<" << exact->name << ">";
|
||||
}
|
||||
} else if (auto nearest = FindNearestTranslatedFunction(value)) {
|
||||
} else if (auto nearest = FindNearbyTranslatedFunction(value)) {
|
||||
if (mapName && mapStart >= nearest->address) {
|
||||
os << "<" << mapName;
|
||||
if (value != mapStart) {
|
||||
@@ -255,7 +255,7 @@ void DumpGuestStackHints(std::ostream& os, const CpuContext* cpu) {
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::FindByAddressPtr(*value) && !FindNearestTranslatedFunction(*value)) {
|
||||
if (!TranslatedFunctionRegistry::FindByAddressPtr(*value) && !FindNearbyTranslatedFunction(*value)) {
|
||||
continue;
|
||||
}
|
||||
if (printed == 0) {
|
||||
|
||||
@@ -66,6 +66,7 @@ Relative paths resolve from `workspace_root`, which itself resolves from the man
|
||||
- `generate-data-init --project path`
|
||||
- `emit-base-manifest --project path`
|
||||
- `emit-build-shards --project path`
|
||||
- `emit-native-index --project path --out path [--native-source-dir path]` - writes the serialized runtime native index
|
||||
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
|
||||
|
||||
Any command prints its own option list with `--help`.
|
||||
|
||||
@@ -68,8 +68,8 @@ var canonicalIrStore = new CanonicalIrStore();
|
||||
// recursive ABI cache in one stable pass, exactly like translate-mod does.
|
||||
var inferredGuestAbi = new Lazy<InferredGuestFunctionAbiProvider>(() =>
|
||||
new InferredGuestFunctionAbiProvider(image.Value, canonicalIrStore));
|
||||
var runtimeNativeIndex = new Lazy<RuntimeNativeIndex>(() =>
|
||||
RuntimeNativeIndexBuilder.Build(RequireProject().Runtime.NativeRegistrationRoot));
|
||||
var runtimeNativeIndex = new Lazy<RuntimeNativeIndex>(() => RuntimeNativeIndexBuilder.Resolve(
|
||||
RequireProject().Runtime.NativeRegistrationRoot, RequireProject().Runtime.NativeIndex, Console.Error));
|
||||
// Void-stub signatures are only trusted as declared guest ABIs when the stub
|
||||
// lives under runtime.native_abi_directories (the vetted set); everywhere else
|
||||
// the C++ signature is documentation and the inferred ABI stays authoritative.
|
||||
@@ -164,6 +164,7 @@ return command switch
|
||||
"translate-recursive" => RunTranslateRecursive(tail),
|
||||
"translate-mod" => RunTranslateMod(tail),
|
||||
"emit-build-shards" => RunEmitBuildShards(tail),
|
||||
"emit-native-index" => RunEmitNativeIndex(tail),
|
||||
"emit-base-manifest" => RunEmitBaseManifest(tail),
|
||||
"check-base-mod-awareness" => RunCheckBaseModAwareness(tail),
|
||||
"validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail),
|
||||
@@ -222,13 +223,21 @@ int RunEmitBuildShards(string[] argsTail)
|
||||
var baseFunctions = OptionValue(argsTail, "--base-functions-dir") ?? Path.Combine(root, "generated", "functions");
|
||||
var output = OptionValue(argsTail, "--out") ?? Path.Combine(root, "generated", "build_shards");
|
||||
var nativeSources = OptionValue(argsTail, "--native-source-dir") ?? Path.Combine(root, "runtime", "src");
|
||||
var nativeIndexOption = OptionValue(argsTail, "--native-index");
|
||||
var resolvedProfile = OptionValue(argsTail, "--resolved-profile");
|
||||
var retroCpp = OptionValue(argsTail, "--retro-cpp-dir");
|
||||
|
||||
try
|
||||
{
|
||||
var nativeSourcePath = Path.GetFullPath(nativeSources);
|
||||
var nativeIndex = RuntimeNativeIndexBuilder.Build(nativeSourcePath);
|
||||
var nativeIndexPath = string.IsNullOrWhiteSpace(nativeIndexOption)
|
||||
? Path.Combine(
|
||||
Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(nativeSourcePath))!,
|
||||
RuntimeNativeIndexFile.DefaultFileName)
|
||||
: Path.GetFullPath(nativeIndexOption);
|
||||
if (!Directory.Exists(nativeSourcePath) && !File.Exists(nativeIndexPath))
|
||||
throw new DirectoryNotFoundException(nativeSourcePath);
|
||||
var nativeIndex = RuntimeNativeIndexBuilder.Resolve(nativeSourcePath, nativeIndexPath, Console.Error);
|
||||
var result = TranslatedBuildShardEmitter.Emit(new TranslatedBuildShardOptions(
|
||||
Path.GetFullPath(baseMetadata),
|
||||
Path.GetFullPath(baseFunctions),
|
||||
@@ -269,6 +278,33 @@ int RunEmitBuildShards(string[] argsTail)
|
||||
}
|
||||
}
|
||||
|
||||
int RunEmitNativeIndex(string[] argsTail)
|
||||
{
|
||||
var output = OptionValue(argsTail, "--out");
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
Console.Error.WriteLine("[translator] emit-native-index needs --out.");
|
||||
return 1;
|
||||
}
|
||||
var nativeSourcePath = Path.GetFullPath(
|
||||
OptionValue(argsTail, "--native-source-dir") ?? RequireProject().Runtime.NativeRegistrationRoot);
|
||||
if (!Directory.Exists(nativeSourcePath))
|
||||
{
|
||||
Console.Error.WriteLine($"[translator] native source directory not found: {nativeSourcePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Digest first: sources edited mid-scan leave a stale digest, which only forces a rescan.
|
||||
var digest = RuntimeNativeIndexFile.ComputeSourceDigest(nativeSourcePath);
|
||||
var index = RuntimeNativeIndexBuilder.Build(nativeSourcePath);
|
||||
var outputPath = Path.GetFullPath(output);
|
||||
var changed = RuntimeNativeIndexFile.Write(outputPath, index, digest);
|
||||
Console.WriteLine($"[translator] native index {(changed ? "written" : "unchanged")}: {outputPath}");
|
||||
Console.WriteLine($" registrations/void-stub ABIs/effects: {index.Registrations.Length:N0}/{index.VoidStubAbis.Length:N0}/{index.Effects.Length:N0}");
|
||||
Console.WriteLine($" source digest: {digest}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] LoadBinaryInput(string spec, string outDir, string downloadFileName)
|
||||
{
|
||||
if (Uri.TryCreate(spec, UriKind.Absolute, out var uri) &&
|
||||
@@ -2940,7 +2976,7 @@ int EmitGuestSymbolTable(FunctionMap? map, string outputPath)
|
||||
sb.Append("\",\n");
|
||||
}
|
||||
sb.Append("};\n\n} // extern \"C\"\n");
|
||||
File.WriteAllText(outputPath, sb.ToString());
|
||||
FileOutput.WriteTextIfChanged(outputPath, sb.ToString());
|
||||
return entries.Count;
|
||||
}
|
||||
|
||||
@@ -3573,6 +3609,7 @@ static string[] KnownCommands() => new[]
|
||||
"translate-mod",
|
||||
"emit-base-manifest",
|
||||
"emit-build-shards",
|
||||
"emit-native-index",
|
||||
"check-base-mod-awareness",
|
||||
"validate-retro-wfc-payload"
|
||||
};
|
||||
@@ -3633,10 +3670,16 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
|
||||
new("--base-metadata", "path"),
|
||||
new("--base-functions-dir", "path"),
|
||||
new("--native-source-dir", "path"),
|
||||
new("--native-index", "path"),
|
||||
new("--resolved-profile", "path"),
|
||||
new("--retro-cpp-dir", "path"),
|
||||
new("--out", "generated/build_shards")
|
||||
}),
|
||||
"emit-native-index" => (null, new CommandOption[]
|
||||
{
|
||||
new("--native-source-dir", "path"),
|
||||
new("--out", "path", Required: true)
|
||||
}),
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
@@ -104,7 +104,8 @@ internal sealed class TranslationProjectConfig
|
||||
.ToArray();
|
||||
var runtime = new ProjectRuntime(
|
||||
abiDirectories,
|
||||
ResolvePath(workspaceRoot, dto.Runtime?.NativeRegistrationRoot ?? "runtime/src"));
|
||||
ResolvePath(workspaceRoot, dto.Runtime?.NativeRegistrationRoot ?? "runtime/src"),
|
||||
ResolvePath(workspaceRoot, dto.Runtime?.NativeIndex ?? "runtime/native-index.json"));
|
||||
var outputRoot = ResolvePath(workspaceRoot, dto.Output?.Root ?? "generated");
|
||||
var output = new ProjectOutput(
|
||||
outputRoot,
|
||||
@@ -416,6 +417,7 @@ internal sealed class TranslationProjectConfig
|
||||
{
|
||||
public List<string>? NativeAbiDirectories { get; init; }
|
||||
public string? NativeRegistrationRoot { get; init; }
|
||||
public string? NativeIndex { get; init; }
|
||||
}
|
||||
|
||||
private sealed class OutputDto
|
||||
@@ -475,7 +477,8 @@ internal sealed record ProjectTranslation(
|
||||
bool AllowUnsupportedInstructions);
|
||||
internal sealed record ProjectRuntime(
|
||||
IReadOnlyList<string> NativeAbiDirectories,
|
||||
string NativeRegistrationRoot);
|
||||
string NativeRegistrationRoot,
|
||||
string NativeIndex);
|
||||
internal sealed record ProjectOutput(
|
||||
string Root,
|
||||
string Functions,
|
||||
|
||||
@@ -288,10 +288,10 @@ public static partial class TranslatedBuildShardEmitter
|
||||
{
|
||||
throw new FileNotFoundException("Required shard input is missing.", options.BaseMetadataPath);
|
||||
}
|
||||
foreach (var path in new[] { options.BaseFunctionsDirectory, options.NativeSourceDirectory })
|
||||
{
|
||||
if (!Directory.Exists(path)) throw new DirectoryNotFoundException(path);
|
||||
}
|
||||
if (!Directory.Exists(options.BaseFunctionsDirectory))
|
||||
throw new DirectoryNotFoundException(options.BaseFunctionsDirectory);
|
||||
if (options.NativeIndex is null && !Directory.Exists(options.NativeSourceDirectory))
|
||||
throw new DirectoryNotFoundException(options.NativeSourceDirectory);
|
||||
if (options.BaseShardCount < 1 || options.ModShardCount < 1 || options.RegistrationShardCount < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(options), "Shard counts must be positive.");
|
||||
@@ -629,7 +629,7 @@ public static partial class TranslatedBuildShardEmitter
|
||||
var traitsPath = Path.Combine(directory, $"shard_{hash[..24]}_traits.h");
|
||||
var source = new StringBuilder();
|
||||
source.AppendLine("// Translator-owned content-addressed aggregate shard; do not edit.");
|
||||
source.AppendLine("#include \"abi_bridge.h\"");
|
||||
source.AppendLine("#include \"guest_abi/guest_abi.h\"");
|
||||
// Quoted include resolves the sibling traits header on every toolchain and keeps
|
||||
// the shard text free of the emitting machine's absolute paths.
|
||||
source.AppendLine($"#include \"{Path.GetFileName(traitsPath)}\"");
|
||||
@@ -909,7 +909,7 @@ public static partial class TranslatedBuildShardEmitter
|
||||
var path = Path.Combine(directory, $"registration_{index:D2}_{hash[..16]}.cpp");
|
||||
var source = new StringBuilder();
|
||||
source.AppendLine("// Translator-owned bulk registration; do not edit.");
|
||||
source.AppendLine("#include \"abi_bridge.h\"");
|
||||
source.AppendLine("#include \"guest_abi/guest_abi.h\"");
|
||||
source.AppendLine("#include <cstddef>");
|
||||
foreach (var symbol in group.Select(static record => record.Symbol).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
|
||||
{
|
||||
@@ -955,7 +955,7 @@ public static partial class TranslatedBuildShardEmitter
|
||||
var path = Path.Combine(directory, $"indirect_dispatch_{hash[..16]}.cpp");
|
||||
var source = new StringBuilder();
|
||||
source.AppendLine("// Translator-owned immutable indirect dispatch table; do not edit.");
|
||||
source.AppendLine("#include \"abi_bridge.h\"");
|
||||
source.AppendLine("#include \"guest_abi/guest_abi.h\"");
|
||||
source.AppendLine("#include <iterator>");
|
||||
foreach (var symbol in entries.Select(static pair => pair.Value.WinnerSymbol!)
|
||||
.Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
|
||||
|
||||
@@ -176,10 +176,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
: LeafRegisterCacheInitializers.Empty;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("#include <cstdint>");
|
||||
sb.AppendLine("#include \"ppc_runtime.h\"");
|
||||
sb.AppendLine("#include \"abi_bridge.h\"");
|
||||
sb.AppendLine("#include \"memory.h\"");
|
||||
sb.AppendLine("#include \"recomp_mod_loader.h\"");
|
||||
sb.AppendLine("#include \"guest_abi/guest_abi.h\"");
|
||||
sb.AppendLine();
|
||||
|
||||
var forwardDecls = CollectCallTargets(func)
|
||||
|
||||
@@ -12,21 +12,30 @@ internal static class NativeSourceParsing
|
||||
public static IReadOnlyList<NativeSourceFile> ReadDirectory(string directory)
|
||||
{
|
||||
var root = Path.GetFullPath(directory);
|
||||
return EnumerateSourcePaths(root)
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => new NativeSourceFile(
|
||||
path,
|
||||
RelativePath(root, path),
|
||||
StripCommentsAndLiterals(File.ReadAllText(path))))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Every file the scanner reads; the native index digest covers exactly this set.</summary>
|
||||
public static IEnumerable<string> EnumerateSourcePaths(string root)
|
||||
{
|
||||
if (!Directory.Exists(root))
|
||||
return [];
|
||||
|
||||
return Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)
|
||||
.Where(static path => path.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".h", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase))
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => new NativeSourceFile(
|
||||
path,
|
||||
Path.GetRelativePath(root, path).Replace('\\', '/'),
|
||||
StripCommentsAndLiterals(File.ReadAllText(path))))
|
||||
.ToArray();
|
||||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static string RelativePath(string root, string path) =>
|
||||
Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
|
||||
public static IEnumerable<string> SplitArguments(string arguments)
|
||||
{
|
||||
var start = 0;
|
||||
|
||||
@@ -26,7 +26,8 @@ public sealed record RuntimeNativeEffectEntry(
|
||||
/// <summary>
|
||||
/// One process-local view of the runtime's native registrations and their guest
|
||||
/// ABI contracts. C++ remains the only source of truth: the translator builds
|
||||
/// this index once, shares it across all consumers, and writes nothing to disk.
|
||||
/// this index once and shares it across all consumers. Only emit-native-index
|
||||
/// persists it (<see cref="RuntimeNativeIndexFile"/>).
|
||||
/// </summary>
|
||||
public sealed record RuntimeNativeIndex(
|
||||
RuntimeNativeRegistration[] Registrations,
|
||||
@@ -69,6 +70,31 @@ public static class RuntimeNativeIndexBuilder
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The index for <paramref name="nativeSourceDirectory"/>: the serialized index at
|
||||
/// <paramref name="indexPath"/> when it matches the sources (or the sources are absent),
|
||||
/// otherwise a fresh scan.
|
||||
/// </summary>
|
||||
public static RuntimeNativeIndex Resolve(
|
||||
string nativeSourceDirectory, string? indexPath, TextWriter? diagnostics = null)
|
||||
{
|
||||
var sourceRoot = Path.GetFullPath(nativeSourceDirectory);
|
||||
if (string.IsNullOrWhiteSpace(indexPath) || !File.Exists(indexPath))
|
||||
return Build(sourceRoot);
|
||||
|
||||
var expectedDigest = Directory.Exists(sourceRoot)
|
||||
? RuntimeNativeIndexFile.ComputeSourceDigest(sourceRoot)
|
||||
: null;
|
||||
if (RuntimeNativeIndexFile.TryLoad(indexPath, expectedDigest, out var index, out var reason))
|
||||
return index;
|
||||
if (expectedDigest is null)
|
||||
throw new InvalidOperationException(
|
||||
$"The native index {indexPath} is unusable ({reason}) and the native sources it describes are missing: {sourceRoot}");
|
||||
|
||||
diagnostics?.WriteLine($"[translator] ignoring native index {indexPath} ({reason}); scanning {sourceRoot}");
|
||||
return Build(sourceRoot);
|
||||
}
|
||||
|
||||
private static IEnumerable<RuntimeNativeRegistration> ScanRegistrations(
|
||||
IReadOnlyList<NativeSourceFile> sources)
|
||||
{
|
||||
@@ -95,7 +121,9 @@ public static class RuntimeNativeIndexBuilder
|
||||
.Distinct()
|
||||
.OrderBy(static registration => registration.Address)
|
||||
.ThenBy(static registration => registration.Symbol, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal);
|
||||
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.IsTranslatedOverride)
|
||||
.ThenBy(static registration => registration.ExcludesBaseTranslation);
|
||||
}
|
||||
|
||||
private static uint ParseAddress(string value) => GuestTargetParser.ParseHexAddress(value);
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.IO;
|
||||
|
||||
namespace Translator.Core;
|
||||
|
||||
/// <summary>
|
||||
/// On-disk form of <see cref="RuntimeNativeIndex"/>. It is only trusted while its
|
||||
/// source digest matches the native sources it was extracted from.
|
||||
/// </summary>
|
||||
public static class RuntimeNativeIndexFile
|
||||
{
|
||||
public const string Format = "wiicompiled-runtime-native-index";
|
||||
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 = 1;
|
||||
public const string DefaultFileName = "native-index.json";
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Lowercase hex SHA-256 over one <c>"<file sha256> <relative path>\n"</c> line per
|
||||
/// scanned file, ordered by relative path (ordinal, '/' separators).
|
||||
/// </summary>
|
||||
public static string ComputeSourceDigest(string nativeSourceDirectory)
|
||||
{
|
||||
var root = Path.GetFullPath(nativeSourceDirectory);
|
||||
var manifest = new StringBuilder();
|
||||
foreach (var (relative, path) in NativeSourceParsing.EnumerateSourcePaths(root)
|
||||
.Select(path => (Relative: NativeSourceParsing.RelativePath(root, path), Path: path))
|
||||
.OrderBy(static item => item.Relative, StringComparer.Ordinal))
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
manifest.Append(Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant())
|
||||
.Append(" ").Append(relative).Append('\n');
|
||||
}
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(manifest.ToString()))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static byte[] Serialize(RuntimeNativeIndex index, string sourceDigest)
|
||||
{
|
||||
var document = new IndexDocument
|
||||
{
|
||||
Format = Format,
|
||||
FormatVersion = FormatVersion,
|
||||
ExtractorVersion = ExtractorVersion,
|
||||
SourceDigest = sourceDigest,
|
||||
Registrations = index.Registrations
|
||||
.OrderBy(static registration => registration.Address)
|
||||
.ThenBy(static registration => registration.Symbol, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.IsTranslatedOverride)
|
||||
.ThenBy(static registration => registration.ExcludesBaseTranslation)
|
||||
.Select(static registration => new RegistrationDto
|
||||
{
|
||||
Address = FormatAddress(registration.Address),
|
||||
Symbol = registration.Symbol,
|
||||
SourceFile = registration.SourceFile,
|
||||
IsTranslatedOverride = registration.IsTranslatedOverride,
|
||||
ExcludesBaseTranslation = registration.ExcludesBaseTranslation
|
||||
})
|
||||
.ToList(),
|
||||
VoidStubAbis = index.VoidStubAbis
|
||||
.OrderBy(static entry => entry.Address)
|
||||
.Select(static entry => new AbiDto
|
||||
{
|
||||
Address = FormatAddress(entry.Address),
|
||||
ArgumentRegisters = entry.ArgumentRegisters.ToList(),
|
||||
ScalarFloatArgumentRegisters = entry.ScalarFloatArgumentRegisters.ToList()
|
||||
})
|
||||
.ToList(),
|
||||
Effects = index.Effects
|
||||
.OrderBy(static entry => entry.Address)
|
||||
.Select(static entry => new EffectDto
|
||||
{
|
||||
Address = FormatAddress(entry.Address),
|
||||
IsPrecise = entry.IsPrecise,
|
||||
Contract = ToDto(entry.Address, entry.Contract)
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
// Indented output uses the platform newline on net8; pin it so the bytes are identical everywhere.
|
||||
var json = JsonSerializer.Serialize(document, Options).Replace("\r\n", "\n", StringComparison.Ordinal);
|
||||
return Encoding.UTF8.GetBytes(json + "\n");
|
||||
}
|
||||
|
||||
public static bool Write(string path, RuntimeNativeIndex index, string sourceDigest) =>
|
||||
FileOutput.WriteBytesIfChanged(path, Serialize(index, sourceDigest));
|
||||
|
||||
/// <summary>
|
||||
/// Loads the index at <paramref name="path"/> when it was produced by this extractor and, unless
|
||||
/// <paramref name="expectedSourceDigest"/> is null, from exactly those sources.
|
||||
/// </summary>
|
||||
public static bool TryLoad(
|
||||
string path,
|
||||
string? expectedSourceDigest,
|
||||
out RuntimeNativeIndex index,
|
||||
out string reason)
|
||||
{
|
||||
index = new RuntimeNativeIndex([], [], []);
|
||||
IndexDocument? document;
|
||||
try
|
||||
{
|
||||
using var parsed = JsonDocument.Parse(File.ReadAllBytes(path));
|
||||
var root = parsed.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object ||
|
||||
!root.TryGetProperty("format", out var format) || format.ValueKind != JsonValueKind.String ||
|
||||
format.GetString() != Format)
|
||||
{
|
||||
reason = "not a runtime native index";
|
||||
return false;
|
||||
}
|
||||
if (!root.TryGetProperty("formatVersion", out var formatVersion) ||
|
||||
formatVersion.ValueKind != JsonValueKind.Number || formatVersion.GetInt32() != FormatVersion)
|
||||
{
|
||||
reason = "format version differs";
|
||||
return false;
|
||||
}
|
||||
if (!root.TryGetProperty("extractorVersion", out var extractorVersion) ||
|
||||
extractorVersion.ValueKind != JsonValueKind.Number || extractorVersion.GetInt32() != ExtractorVersion)
|
||||
{
|
||||
reason = "extractor version differs";
|
||||
return false;
|
||||
}
|
||||
document = root.Deserialize<IndexDocument>(Options);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException or FormatException)
|
||||
{
|
||||
reason = $"unreadable: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document is null)
|
||||
{
|
||||
reason = "empty";
|
||||
return false;
|
||||
}
|
||||
if (expectedSourceDigest is not null &&
|
||||
!string.Equals(document.SourceDigest, expectedSourceDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reason = "native sources changed";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
index = new RuntimeNativeIndex(
|
||||
Present(document.Registrations, "registrations")
|
||||
.Select(static registration => new RuntimeNativeRegistration(
|
||||
ParseAddress(registration.Address),
|
||||
Present(registration.Symbol, "symbol"),
|
||||
Present(registration.SourceFile, "sourceFile"),
|
||||
registration.IsTranslatedOverride,
|
||||
registration.ExcludesBaseTranslation))
|
||||
.ToArray(),
|
||||
Present(document.VoidStubAbis, "voidStubAbis")
|
||||
.Select(static entry => new RuntimeNativeAbiEntry(
|
||||
ParseAddress(entry.Address),
|
||||
Present(entry.ArgumentRegisters, "argumentRegisters").Select(static r => Present(r, "register")).ToArray(),
|
||||
Present(entry.ScalarFloatArgumentRegisters, "scalarFloatArgumentRegisters").Select(static r => Present(r, "register")).ToArray()))
|
||||
.ToArray(),
|
||||
Present(document.Effects, "effects")
|
||||
.Select(static entry => new RuntimeNativeEffectEntry(
|
||||
ParseAddress(entry.Address), FromDto(Present(entry.Contract, "contract")), entry.IsPrecise))
|
||||
.ToArray());
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
reason = $"unreadable: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatAddress(uint address) => $"0x{address:X8}";
|
||||
|
||||
private static uint ParseAddress(string text) => GuestTargetParser.ParseHexAddress(text);
|
||||
|
||||
private static T Present<T>(T? value, string name) where T : class =>
|
||||
value ?? throw new FormatException($"'{name}' is null.");
|
||||
|
||||
private static ContractDto ToDto(uint address, GuestAbiContract contract)
|
||||
{
|
||||
var dto = new ContractDto
|
||||
{
|
||||
GprReadBeforeWriteMask = contract.GprReadBeforeWriteMask,
|
||||
GprPossibleWriteMask = contract.GprPossibleWriteMask,
|
||||
GprReturnMask = contract.GprReturnMask,
|
||||
FprReadBeforeWriteMask = contract.FprReadBeforeWriteMask,
|
||||
FprPossibleWriteMask = contract.FprPossibleWriteMask,
|
||||
FprReturnMask = contract.FprReturnMask,
|
||||
CrReadBeforeWriteMask = contract.CrReadBeforeWriteMask,
|
||||
CrPossibleWriteMask = contract.CrPossibleWriteMask,
|
||||
ReadsXerBeforeWrite = contract.ReadsXerBeforeWrite,
|
||||
MayWriteXer = contract.MayWriteXer,
|
||||
ReadsCtrBeforeWrite = contract.ReadsCtrBeforeWrite,
|
||||
MayWriteCtr = contract.MayWriteCtr,
|
||||
ReadsLrBeforeWrite = contract.ReadsLrBeforeWrite,
|
||||
MayWriteLr = contract.MayWriteLr,
|
||||
BoundaryFlags = (int)contract.BoundaryFlags
|
||||
};
|
||||
// Native contracts only ever set the positional masks; anything else would be lost on load.
|
||||
if (!FromDto(dto).Equals(contract))
|
||||
throw new InvalidOperationException(
|
||||
$"Native contract for 0x{address:X8} uses fields the native index does not serialize.");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static GuestAbiContract FromDto(ContractDto dto) =>
|
||||
new(dto.GprReadBeforeWriteMask, dto.GprPossibleWriteMask, dto.GprReturnMask,
|
||||
dto.FprReadBeforeWriteMask, dto.FprPossibleWriteMask, dto.FprReturnMask,
|
||||
dto.CrReadBeforeWriteMask, dto.CrPossibleWriteMask,
|
||||
dto.ReadsXerBeforeWrite, dto.MayWriteXer,
|
||||
dto.ReadsCtrBeforeWrite, dto.MayWriteCtr,
|
||||
dto.ReadsLrBeforeWrite, dto.MayWriteLr,
|
||||
(GuestCallBoundaryFlags)dto.BoundaryFlags, Array.Empty<uint>());
|
||||
|
||||
private sealed class IndexDocument
|
||||
{
|
||||
public required string Format { get; init; }
|
||||
public required int FormatVersion { get; init; }
|
||||
public required int ExtractorVersion { get; init; }
|
||||
public required string SourceDigest { get; init; }
|
||||
public required List<RegistrationDto> Registrations { get; init; }
|
||||
public required List<AbiDto> VoidStubAbis { get; init; }
|
||||
public required List<EffectDto> Effects { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RegistrationDto
|
||||
{
|
||||
public required string Address { get; init; }
|
||||
public required string Symbol { get; init; }
|
||||
public required string SourceFile { get; init; }
|
||||
public required bool IsTranslatedOverride { get; init; }
|
||||
public required bool ExcludesBaseTranslation { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AbiDto
|
||||
{
|
||||
public required string Address { get; init; }
|
||||
public required List<string> ArgumentRegisters { get; init; }
|
||||
public required List<string> ScalarFloatArgumentRegisters { get; init; }
|
||||
}
|
||||
|
||||
private sealed class EffectDto
|
||||
{
|
||||
public required string Address { get; init; }
|
||||
public required bool IsPrecise { get; init; }
|
||||
public required ContractDto Contract { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ContractDto
|
||||
{
|
||||
public required uint GprReadBeforeWriteMask { get; init; }
|
||||
public required uint GprPossibleWriteMask { get; init; }
|
||||
public required uint GprReturnMask { get; init; }
|
||||
public required uint FprReadBeforeWriteMask { get; init; }
|
||||
public required uint FprPossibleWriteMask { get; init; }
|
||||
public required uint FprReturnMask { get; init; }
|
||||
public required byte CrReadBeforeWriteMask { get; init; }
|
||||
public required byte CrPossibleWriteMask { get; init; }
|
||||
public required bool ReadsXerBeforeWrite { get; init; }
|
||||
public required bool MayWriteXer { get; init; }
|
||||
public required bool ReadsCtrBeforeWrite { get; init; }
|
||||
public required bool MayWriteCtr { get; init; }
|
||||
public required bool ReadsLrBeforeWrite { get; init; }
|
||||
public required bool MayWriteLr { get; init; }
|
||||
public required int BoundaryFlags { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Translator.Core;
|
||||
using Translator.Core.CodeGen;
|
||||
using Xunit;
|
||||
@@ -6,6 +8,24 @@ namespace Translator.Tests;
|
||||
|
||||
public sealed class RuntimeNativeIndexTests
|
||||
{
|
||||
// The Windows setup self-test and Launcher/build-cache.sh pin the same fixture digest.
|
||||
[Fact]
|
||||
public void SourceDigestMatchesInstallerImplementations()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"mkw-native-digest-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(directory, "main.cpp"), "int main();");
|
||||
Assert.Equal("50b9779591f4b205751361c8899accd362f96ea5dfd32febed1bedca534c523a",
|
||||
RuntimeNativeIndexFile.ComputeSourceDigest(directory));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCapturesEveryRuntimeRegistrationKind()
|
||||
{
|
||||
@@ -72,6 +92,7 @@ public sealed class RuntimeNativeIndexTests
|
||||
var outOfScope = RuntimeNativeFunctionAbiProvider.FromIndex(index, new HashSet<uint>());
|
||||
Assert.False(outOfScope.TryGetGuestFunctionAbi("func_80001234", out _));
|
||||
|
||||
RuntimeNativeIndexBuilder.Resolve(directory, Path.Combine(directory, "missing-index.json"));
|
||||
Assert.Equal([sourcePath], Directory.GetFiles(directory));
|
||||
}
|
||||
finally
|
||||
@@ -80,4 +101,177 @@ public sealed class RuntimeNativeIndexTests
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializedRepositoryIndexRoundTripsExactly()
|
||||
{
|
||||
var sources = Path.Combine(ProjectPathsForTests.FindRepositoryRoot(), "runtime", "src");
|
||||
var built = RuntimeNativeIndexBuilder.Build(sources);
|
||||
Assert.NotEmpty(built.Registrations);
|
||||
Assert.NotEmpty(built.VoidStubAbis);
|
||||
Assert.NotEmpty(built.Effects);
|
||||
|
||||
var path = Path.Combine(Path.GetTempPath(), $"mkw-native-index-{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
var bytes = RuntimeNativeIndexFile.Serialize(built, "digest");
|
||||
File.WriteAllBytes(path, bytes);
|
||||
Assert.True(RuntimeNativeIndexFile.TryLoad(path, "digest", out var loaded, out var reason), reason);
|
||||
AssertIndexEqual(built, loaded);
|
||||
Assert.Equal(bytes, RuntimeNativeIndexFile.Serialize(loaded, "digest"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveUsesAMatchingIndexAndKeepsWorkingWithoutSources()
|
||||
{
|
||||
var root = CreateFixture();
|
||||
try
|
||||
{
|
||||
var sources = Path.Combine(root, "src");
|
||||
var indexPath = Path.Combine(root, RuntimeNativeIndexFile.DefaultFileName);
|
||||
var built = RuntimeNativeIndexBuilder.Build(sources);
|
||||
Assert.True(RuntimeNativeIndexFile.Write(
|
||||
indexPath, built, RuntimeNativeIndexFile.ComputeSourceDigest(sources)));
|
||||
|
||||
var diagnostics = new StringWriter();
|
||||
AssertIndexEqual(built, RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics));
|
||||
Assert.Equal("", diagnostics.ToString());
|
||||
Assert.Contains(built.Registrations, static entry => entry.SourceFile == "hle/gx/stubs.cpp");
|
||||
Assert.Contains(built.Effects, static entry => entry.IsPrecise);
|
||||
Assert.Contains(built.Effects, static entry => !entry.IsPrecise);
|
||||
|
||||
Directory.Delete(sources, recursive: true);
|
||||
AssertIndexEqual(built, RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics));
|
||||
Assert.Equal("", diagnostics.ToString());
|
||||
Assert.Empty(RuntimeNativeIndexBuilder.Build(sources).Registrations);
|
||||
|
||||
File.WriteAllText(indexPath, "{}");
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveRescansWhenTheIndexIsStale()
|
||||
{
|
||||
var root = CreateFixture();
|
||||
try
|
||||
{
|
||||
var sources = Path.Combine(root, "src");
|
||||
var indexPath = Path.Combine(root, RuntimeNativeIndexFile.DefaultFileName);
|
||||
RuntimeNativeIndexFile.Write(
|
||||
indexPath, RuntimeNativeIndexBuilder.Build(sources), RuntimeNativeIndexFile.ComputeSourceDigest(sources));
|
||||
|
||||
File.AppendAllText(Path.Combine(sources, "natives.cpp"), "\nREGISTER_NATIVE_FUNCTION(0x80009990, Added);\n");
|
||||
var diagnostics = new StringWriter();
|
||||
var resolved = RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics);
|
||||
Assert.Contains(resolved.Registrations, static entry => entry.Address == 0x80009990u);
|
||||
var current = RuntimeNativeIndexBuilder.Build(sources);
|
||||
AssertIndexEqual(current, resolved);
|
||||
Assert.Contains("native sources changed", diagnostics.ToString());
|
||||
|
||||
var json = Encoding.UTF8.GetString(
|
||||
RuntimeNativeIndexFile.Serialize(current, RuntimeNativeIndexFile.ComputeSourceDigest(sources)))
|
||||
.Replace($"\"extractorVersion\": {RuntimeNativeIndexFile.ExtractorVersion}", "\"extractorVersion\": 0");
|
||||
File.WriteAllText(indexPath, json);
|
||||
diagnostics = new StringWriter();
|
||||
AssertIndexEqual(current, RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics));
|
||||
Assert.Contains("extractor version differs", diagnostics.ToString());
|
||||
|
||||
File.WriteAllText(indexPath, "{ not json");
|
||||
diagnostics = new StringWriter();
|
||||
AssertIndexEqual(current, RuntimeNativeIndexBuilder.Resolve(sources, indexPath, diagnostics));
|
||||
Assert.Contains("unreadable", diagnostics.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializationAndDigestAreDeterministicAcrossLocations()
|
||||
{
|
||||
var first = CreateFixture();
|
||||
var second = CreateFixture();
|
||||
try
|
||||
{
|
||||
var firstSources = Path.Combine(first, "src");
|
||||
var secondSources = Path.Combine(second, "src");
|
||||
var digest = RuntimeNativeIndexFile.ComputeSourceDigest(firstSources);
|
||||
Assert.Equal(digest, RuntimeNativeIndexFile.ComputeSourceDigest(secondSources));
|
||||
|
||||
var bytes = RuntimeNativeIndexFile.Serialize(RuntimeNativeIndexBuilder.Build(firstSources), digest);
|
||||
Assert.Equal(bytes, RuntimeNativeIndexFile.Serialize(RuntimeNativeIndexBuilder.Build(firstSources), digest));
|
||||
Assert.Equal(bytes, RuntimeNativeIndexFile.Serialize(RuntimeNativeIndexBuilder.Build(secondSources), digest));
|
||||
Assert.DoesNotContain((byte)'\r', bytes);
|
||||
Assert.Equal((byte)'\n', bytes[^1]);
|
||||
|
||||
var expected = new StringBuilder();
|
||||
foreach (var relative in new[] { "hle/gx/stubs.cpp", "include/decl.h", "natives.cpp" })
|
||||
{
|
||||
var fileHash = SHA256.HashData(File.ReadAllBytes(Path.Combine(firstSources, relative)));
|
||||
expected.Append(Convert.ToHexString(fileHash).ToLowerInvariant()).Append(" ").Append(relative).Append('\n');
|
||||
}
|
||||
Assert.Equal(
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(expected.ToString()))).ToLowerInvariant(),
|
||||
digest);
|
||||
|
||||
File.WriteAllText(Path.Combine(firstSources, "notes.txt"), "not scanned");
|
||||
Assert.Equal(digest, RuntimeNativeIndexFile.ComputeSourceDigest(firstSources));
|
||||
File.AppendAllText(Path.Combine(firstSources, "include", "decl.h"), "\n");
|
||||
Assert.NotEqual(digest, RuntimeNativeIndexFile.ComputeSourceDigest(firstSources));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(first, recursive: true);
|
||||
Directory.Delete(second, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string CreateFixture()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"mkw-native-fixture-{Guid.NewGuid():N}");
|
||||
var sources = Path.Combine(root, "src");
|
||||
Directory.CreateDirectory(Path.Combine(sources, "hle", "gx"));
|
||||
Directory.CreateDirectory(Path.Combine(sources, "include"));
|
||||
File.WriteAllText(Path.Combine(sources, "natives.cpp"), """
|
||||
extern "C" void Precise(CpuContext* ctx) { ctx->gpr[3] = ctx->gpr[4]; ctx->lr = 0; }
|
||||
extern "C" void Complete(CpuContext* ctx) { Helper(ctx); }
|
||||
REGISTER_NATIVE_FUNCTION(0x80000010, Precise);
|
||||
REGISTER_NATIVE_FUNCTION_AS(0x80000020, Complete, "alias");
|
||||
REGISTER_TRANSLATED_FUNCTION(0x80000030, Translated);
|
||||
GX_FATAL_STUB(80000050, "Fatal")
|
||||
""");
|
||||
File.WriteAllText(Path.Combine(sources, "hle", "gx", "stubs.cpp"), """
|
||||
extern "C" void Typed(float value, uint32_t count) { (void)value; (void)count; }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80001234, Typed, (float value, uint32_t count), (value, count));
|
||||
""");
|
||||
File.WriteAllText(Path.Combine(sources, "include", "decl.h"), """
|
||||
REGISTER_NATIVE_FUNCTION(0x80000060, Declared);
|
||||
""");
|
||||
return root;
|
||||
}
|
||||
|
||||
internal static void AssertIndexEqual(RuntimeNativeIndex expected, RuntimeNativeIndex actual)
|
||||
{
|
||||
Assert.Equal(expected.Registrations, actual.Registrations);
|
||||
Assert.Equal(expected.Effects, actual.Effects);
|
||||
Assert.Equal(expected.VoidStubAbis.Length, actual.VoidStubAbis.Length);
|
||||
foreach (var (left, right) in expected.VoidStubAbis.Zip(actual.VoidStubAbis))
|
||||
{
|
||||
Assert.Equal(left.Address, right.Address);
|
||||
Assert.Equal(left.ArgumentRegisters, right.ArgumentRegisters);
|
||||
Assert.Equal(left.ScalarFloatArgumentRegisters, right.ScalarFloatArgumentRegisters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Translator.Core;
|
||||
using Translator.Core.Build;
|
||||
using Translator.Core.Mods;
|
||||
using Xunit;
|
||||
@@ -359,6 +360,62 @@ public sealed class TranslatedBuildShardEmitterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadedNativeIndexEmitsTheSameGraphAsAScan()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"translator-native-index-shards-{Guid.NewGuid():N}");
|
||||
var functions = Path.Combine(root, "functions");
|
||||
var native = Path.Combine(root, "runtime", "src");
|
||||
var output = Path.Combine(root, "out");
|
||||
Directory.CreateDirectory(functions);
|
||||
try
|
||||
{
|
||||
var sources = new Dictionary<uint, string>();
|
||||
foreach (var address in new[] { 0x80001000u, 0x80003000u, 0x80004000u })
|
||||
{
|
||||
sources[address] = $$"""
|
||||
#include "abi_bridge.h"
|
||||
extern "C" void func_{{address:X8}}(CpuContext*) {}
|
||||
// RECOMP_REGISTRATION base 0x{{address:X8}} func_{{address:X8}} preserves=true fpr_mask=0x00000000
|
||||
""";
|
||||
Write(Path.Combine(functions, $"func_{address:X8}.cpp"), sources[address]);
|
||||
}
|
||||
Write(Path.Combine(native, "hle", "override.cpp"),
|
||||
"REGISTER_TRANSLATED_FUNCTION(0x80003000, RuntimeOverride_80003000);\n" +
|
||||
"REGISTER_NATIVE_FUNCTION(0x80004000, NativeOverride_80004000);\n");
|
||||
var metadataPath = Path.Combine(root, "base_output.json");
|
||||
BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(metadataPath,
|
||||
BaseTranslationOutputMetadata.Create(
|
||||
sources.Select(item => Metadata($"func_{item.Key:X8}.cpp", item.Key, item.Value)).ToList(),
|
||||
TranslationQualityMetadata.Clean, "identity"));
|
||||
|
||||
var indexPath = Path.Combine(root, "runtime", RuntimeNativeIndexFile.DefaultFileName);
|
||||
var scanned = RuntimeNativeIndexBuilder.Resolve(native, indexPath);
|
||||
TranslatedBuildShardEmitter.Emit(new TranslatedBuildShardOptions(
|
||||
metadataPath, functions, output, native, BaseShardCount: 2, ModShardCount: 1,
|
||||
RegistrationShardCount: 1, NativeIndex: scanned));
|
||||
var expected = Snapshot(output);
|
||||
Assert.Contains(expected.Values, static text => text.Contains("RuntimeOverride_80003000"));
|
||||
|
||||
RuntimeNativeIndexFile.Write(indexPath, scanned, RuntimeNativeIndexFile.ComputeSourceDigest(native));
|
||||
Directory.Delete(native, recursive: true);
|
||||
Directory.Delete(output, recursive: true);
|
||||
var loaded = RuntimeNativeIndexBuilder.Resolve(native, indexPath);
|
||||
TranslatedBuildShardEmitter.Emit(new TranslatedBuildShardOptions(
|
||||
metadataPath, functions, output, native, BaseShardCount: 2, ModShardCount: 1,
|
||||
RegistrationShardCount: 1, NativeIndex: loaded));
|
||||
Assert.Equal(expected, Snapshot(output));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
|
||||
static SortedDictionary<string, string> Snapshot(string directory) =>
|
||||
new(Directory.GetFiles(directory, "*", SearchOption.AllDirectories)
|
||||
.ToDictionary(path => Path.GetRelativePath(directory, path), File.ReadAllText), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static BaseTranslationFunctionMetadata Metadata(string path, uint address, string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
Reference in New Issue
Block a user