From 6a1fa79bf60ad1a64452d6fd55f03ca7bb46ef04 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:47:08 +0200 Subject: [PATCH] Check CPU support before building (#331) * check cpu support before building * build for x86-64-v2 on windows and intel macs --- Launcher/LocalBuild.ps1 | 8 +- Launcher/Test-Recompilation.ps1 | 5 +- .../HostCpuRequirement.cs | 99 +++++++++++++++++++ Launcher/WiiCompiled.Setup.Linux/Program.cs | 2 + .../LocalBuildService.cs | 3 +- Launcher/WiiCompiled.Setup.Windows/Program.cs | 2 + .../WiiCompiled.Setup.Windows/SelfTests.cs | 52 ++++++++++ Launcher/local-build-macos.command | 13 ++- Launcher/macos/setup.command | 22 +++++ 9 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 Launcher/WiiCompiled.Setup.Common/HostCpuRequirement.cs diff --git a/Launcher/LocalBuild.ps1 b/Launcher/LocalBuild.ps1 index 15eb89f..e319eb6 100644 --- a/Launcher/LocalBuild.ps1 +++ b/Launcher/LocalBuild.ps1 @@ -21,7 +21,8 @@ param( # Discards every cache first. Used when a product was reported broken/blocked, so a possibly # corrupted translation or build directory can never contribute to the repaired product. [switch]$ForceCleanBuild, - [int]$Parallel = 0 + [int]$Parallel = 0, + [ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3' ) $ErrorActionPreference = 'Stop' @@ -397,11 +398,14 @@ try { $dependencies = Join-Path $Workspace 'Dependencies' $nativePrebuilt = Resolve-NativePrebuiltDirectory $dependencies $toolchainBin ` (Join-Path $Workspace 'aurora-main') (Join-Path $Workspace 'runtime\third_party') + if ($X86CpuProfile -eq 'v2') { + Write-Host 'MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2' + } $configure = Get-MkwNativeConfigureArguments ` -SourceDirectory (Join-Path $Workspace 'runtime') -BuildDirectory $build ` -Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres ` -DependenciesDirectory $dependencies -NativePrebuiltDirectory $nativePrebuilt ` - -AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs") + -AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs", "-DMKW_X86_CPU_PROFILE=$X86CpuProfile") Invoke-Checked $cmake $configure 'Configuring the bundled native toolchain' -StepId 'configure-native' if (-not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint)) { [ordered]@{ SchemaVersion = 1; NativeToolchainFingerprint = $NativeToolchainFingerprint } | diff --git a/Launcher/Test-Recompilation.ps1 b/Launcher/Test-Recompilation.ps1 index cc2799e..6d06cf8 100644 --- a/Launcher/Test-Recompilation.ps1 +++ b/Launcher/Test-Recompilation.ps1 @@ -5,7 +5,8 @@ param( [string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools', [string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies', [string]$StageDirectory = 'build/recomp-test', - [ValidateRange(1, 64)] [int]$Parallel = 3 + [ValidateRange(1, 64)] [int]$Parallel = 3, + [ValidateSet('v2', 'v3')] [string]$X86CpuProfile = 'v3' ) $ErrorActionPreference = 'Stop' @@ -136,7 +137,7 @@ try { -Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') ` -CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') ` -ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') ` - -DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON') + -DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON', "-DMKW_X86_CPU_PROFILE=$X86CpuProfile") Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' ` -WaitForProcessTree $false Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") ` diff --git a/Launcher/WiiCompiled.Setup.Common/HostCpuRequirement.cs b/Launcher/WiiCompiled.Setup.Common/HostCpuRequirement.cs new file mode 100644 index 0000000..fc8c45c --- /dev/null +++ b/Launcher/WiiCompiled.Setup.Common/HostCpuRequirement.cs @@ -0,0 +1,99 @@ +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; + +namespace WiiCompiled.Setup.Common; + +public enum X86CpuProfile +{ + V2, + V3 +} + +// Setup-time copy of runtime/src/host_cpu_baseline.cpp, checked before anything is built. +public static class HostCpuRequirement +{ + private readonly record struct Feature(string Name, uint Leaf, int Register, int Bit, X86CpuProfile Profile); + + // Same table and order as the runtime: register 0..3 = eax, ebx, ecx, edx. + private static readonly Feature[] Features = + [ + new("SSE3", 1, 2, 0, X86CpuProfile.V2), + new("SSSE3", 1, 2, 9, X86CpuProfile.V2), + new("CMPXCHG16B", 1, 2, 13, X86CpuProfile.V2), + new("SSE4.1", 1, 2, 19, X86CpuProfile.V2), + new("SSE4.2", 1, 2, 20, X86CpuProfile.V2), + new("POPCNT", 1, 2, 23, X86CpuProfile.V2), + new("LAHF-SAHF", 0x80000001u, 2, 0, X86CpuProfile.V2), + new("FMA", 1, 2, 12, X86CpuProfile.V3), + new("MOVBE", 1, 2, 22, X86CpuProfile.V3), + new("OSXSAVE", 1, 2, 27, X86CpuProfile.V3), + new("AVX", 1, 2, 28, X86CpuProfile.V3), + new("F16C", 1, 2, 29, X86CpuProfile.V3), + new("BMI1", 7, 1, 3, X86CpuProfile.V3), + new("AVX2", 7, 1, 5, X86CpuProfile.V3), + new("BMI2", 7, 1, 8, X86CpuProfile.V3), + new("LZCNT", 0x80000001u, 2, 5, X86CpuProfile.V3), + ]; + + public static void EnsureSupported(X86CpuProfile profile) + { + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + return; + + // The JIT only reports AVX when the OS also saves the YMM state (XCR0), which is the + // runtime's last check; there is no managed XGETBV. + var missing = FindMissingFeatures(profile, CpuId, Avx.IsSupported); + if (missing.Count > 0) + throw new PlatformNotSupportedException(Describe(profile, missing)); + } + + // The highest profile this machine runs; builds fall back to v2 when v3 is missing. + public static X86CpuProfile SelectBuildProfile() => + RuntimeInformation.ProcessArchitecture != Architecture.X64 + ? X86CpuProfile.V3 + : SelectBuildProfile(CpuId, Avx.IsSupported); + + public static X86CpuProfile SelectBuildProfile(Func cpuid, bool osSavesAvxState) => + FindMissingFeatures(X86CpuProfile.V3, cpuid, osSavesAvxState).Count == 0 + ? X86CpuProfile.V3 + : X86CpuProfile.V2; + + public static IReadOnlyList FindMissingFeatures(X86CpuProfile profile, + Func cpuid, bool osSavesAvxState) + { + var maxBasic = cpuid(0)[0]; + var maxExtended = cpuid(0x80000000u)[0]; + var missing = new List(); + foreach (var feature in Features) + { + if (feature.Profile > profile) + continue; + var leafAvailable = (feature.Leaf & 0x80000000u) != 0 + ? feature.Leaf <= maxExtended + : feature.Leaf <= maxBasic; + if (!leafAvailable || (cpuid(feature.Leaf)[feature.Register] & (1u << feature.Bit)) == 0) + missing.Add(feature.Name); + } + + if (missing.Count == 0 && profile >= X86CpuProfile.V3 && !osSavesAvxState) + missing.Add("operating system support for AVX register state (XCR0 YMM bits)"); + return missing; + } + + public static string Describe(X86CpuProfile profile, IReadOnlyList missing) => + profile == X86CpuProfile.V2 + ? "This build needs a processor that supports the x86-64-v2 instruction set.\n\n" + + "Missing on this machine: " + string.Join(", ", missing) + "\n\n" + + "x86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors " + + "from Jaguar (2013) onward." + : "This build needs a processor that supports the x86-64-v3 instruction set.\n\n" + + "Missing on this machine: " + string.Join(", ", missing) + "\n\n" + + "x86-64-v3 covers Intel Core processors from Haswell (4th generation, 2013) onward " + + "and AMD processors from Excavator (2015) onward."; + + private static uint[] CpuId(uint leaf) + { + var (eax, ebx, ecx, edx) = X86Base.CpuId(unchecked((int)leaf), 0); + return [unchecked((uint)eax), unchecked((uint)ebx), unchecked((uint)ecx), unchecked((uint)edx)]; + } +} diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs index 8bbe64f..75cc9de 100644 --- a/Launcher/WiiCompiled.Setup.Linux/Program.cs +++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs @@ -74,6 +74,8 @@ internal static class Program private static async Task InstallAsync(Dictionary flags, IInstallReporter reporter, CancellationToken token) { + // local-build.sh falls back to an x86-64-v2 build when this CPU lacks v3. + HostCpuRequirement.EnsureSupported(X86CpuProfile.V2); var retroDir = flags.GetValueOrDefault("retro-dir"); var installsRetro = !string.IsNullOrEmpty(retroDir); var downloadPayload = flags.ContainsKey("download-retro-wfc-payload"); diff --git a/Launcher/WiiCompiled.Setup.Windows/LocalBuildService.cs b/Launcher/WiiCompiled.Setup.Windows/LocalBuildService.cs index e265544..4a390ea 100644 --- a/Launcher/WiiCompiled.Setup.Windows/LocalBuildService.cs +++ b/Launcher/WiiCompiled.Setup.Windows/LocalBuildService.cs @@ -60,7 +60,8 @@ internal sealed class LocalBuildService BuildProfile.RetroRewind => "retro-rewind", _ => "both" }, - "-OutputDirectory", outputDirectory + "-OutputDirectory", outputDirectory, + "-X86CpuProfile", HostCpuRequirement.SelectBuildProfile() == X86CpuProfile.V2 ? "v2" : "v3" }; if (baseOutputDirectory is not null) { diff --git a/Launcher/WiiCompiled.Setup.Windows/Program.cs b/Launcher/WiiCompiled.Setup.Windows/Program.cs index 97e5125..761f39a 100644 --- a/Launcher/WiiCompiled.Setup.Windows/Program.cs +++ b/Launcher/WiiCompiled.Setup.Windows/Program.cs @@ -64,6 +64,8 @@ internal static class Program if (command.Mode is AppMode.SilentInstall or AppMode.RepairProducts) { + // CPUs without x86-64-v3 get an x86-64-v2 build. + HostCpuRequirement.EnsureSupported(X86CpuProfile.V2); using var cancellationSignal = CancellationSignal.ObserveEnvironment(); return command.Mode == AppMode.SilentInstall ? ConsoleCommands.Install(command, cancellationSignal.Token).GetAwaiter().GetResult() diff --git a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs index 9536687..f688a8a 100644 --- a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs +++ b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs @@ -36,6 +36,7 @@ internal static class SelfTests Test("Toolkit accompanies the workspace", TestToolkitRefreshDecision, failures); Test("Retro-WFC payload snapshot reconciliation", TestRetroWfcPayloadSnapshotReconciliation, failures); Test("Retro-WFC transient retry policy", TestRetroWfcTransientRetryPolicy, failures); + Test("Host CPU requirement", TestHostCpuRequirement, failures); Test("Shared runtime configuration", () => TestPathSettingRoundTrip("dvd_root", RuntimeConfiguration.SetDvdRoot, RuntimeConfiguration.RemoveDvdRootIfOwned), failures); Test("Canonical Retro Rewind configuration", () => TestPathSettingRoundTrip("retro_rewind_root", @@ -994,6 +995,57 @@ internal static class SelfTests throw new Exception("A non-I/O failure would be retried."); } + private static void TestHostCpuRequirement() + { + // Leaf 1 ecx, leaf 7 ebx, leaf 0x80000001 ecx of a v2-only and a v3 processor. + const uint v2Leaf1 = (1u << 0) | (1u << 9) | (1u << 13) | (1u << 19) | (1u << 20) | (1u << 23); + const uint v3Leaf1 = v2Leaf1 | (1u << 12) | (1u << 22) | (1u << 27) | (1u << 28) | (1u << 29); + const uint v3Leaf7 = (1u << 3) | (1u << 5) | (1u << 8); + Func Cpu(uint maxBasic, uint leaf1, uint leaf7, uint extended) => leaf => leaf switch + { + 0 => [maxBasic, 0, 0, 0], + 1 => [0, 0, leaf1, 0], + 7 => [0, leaf7, 0, 0], + 0x80000000u => [0x80000001u, 0, 0, 0], + 0x80000001u => [0, 0, extended, 0], + _ => [0, 0, 0, 0] + }; + var nehalem = Cpu(11, v2Leaf1, 0, 1u); + var haswell = Cpu(13, v3Leaf1, v3Leaf7, 1u | (1u << 5)); + + void Expect(IReadOnlyList actual, string expected, string what) + { + if (string.Join(", ", actual) != expected) + throw new Exception($"{what}: expected [{expected}], got [{string.Join(", ", actual)}]."); + } + + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, nehalem, false), "", + "A v2 processor was rejected for the v2 build"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, nehalem, false), + "FMA, MOVBE, OSXSAVE, AVX, F16C, BMI1, AVX2, BMI2, LZCNT", "A v2 processor for the v3 build"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, haswell, true), "", + "A v3 processor was rejected"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, haswell, false), "", + "A v3 processor was rejected for the v2 build"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, haswell, false), + "operating system support for AVX register state (XCR0 YMM bits)", "Disabled AVX state"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V3, Cpu(6, v3Leaf1, v3Leaf7, 1u | (1u << 5)), true), + "BMI1, AVX2, BMI2", "Unavailable leaf 7"); + Expect(HostCpuRequirement.FindMissingFeatures(X86CpuProfile.V2, Cpu(11, v2Leaf1, 0, 0), false), + "LAHF-SAHF", "Missing LAHF/SAHF"); + + if (HostCpuRequirement.SelectBuildProfile(nehalem, false) != X86CpuProfile.V2) + throw new Exception("A v2 processor was not given the v2 build."); + if (HostCpuRequirement.SelectBuildProfile(haswell, true) != X86CpuProfile.V3) + throw new Exception("A v3 processor was not given the v3 build."); + if (HostCpuRequirement.SelectBuildProfile(haswell, false) != X86CpuProfile.V2) + throw new Exception("A v3 processor without AVX state was given the v3 build."); + + var message = HostCpuRequirement.Describe(X86CpuProfile.V3, ["AVX2"]); + if (!message.Contains("x86-64-v3") || !message.Contains("Missing on this machine: AVX2")) + throw new Exception("The unsupported-processor message lost its detail."); + } + private static void TestRetroWfcTransientRetryPolicy() { if (!InputValidation.IsTransientRetroWfcDownloadFailure( diff --git a/Launcher/local-build-macos.command b/Launcher/local-build-macos.command index a4a26f2..a775b31 100755 --- a/Launcher/local-build-macos.command +++ b/Launcher/local-build-macos.command @@ -86,6 +86,17 @@ assets="$workspace/Assets"; generated="$workspace/generated" functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base" manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards" native_build="$workspace/native-build-macos-$macos_arch" +x86_cpu_profile=v3 +if [[ "$macos_arch" == x86_64 ]]; then + native_macros=$(clang -march=native -dM -E -x c /dev/null 2>/dev/null || true) + for macro in __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__; do + if ! grep -q "^#define $macro " <<<"$native_macros"; then + x86_cpu_profile=v2 + printf 'MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2\n' + break + fi + done +fi if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --workspace "$workspace" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel' # The translation project is the one pinned to the extracted main.dol, which decides the region. @@ -172,7 +183,7 @@ fi step configure-native 'Configuring the native toolchain' # Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than # Aurora's required API even when find_package() succeeds. -"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version" +"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=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" -DMKW_X86_CPU_PROFILE="$x86_cpu_profile" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version" mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint" targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind) step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs" diff --git a/Launcher/macos/setup.command b/Launcher/macos/setup.command index 28c2e33..a839ad3 100755 --- a/Launcher/macos/setup.command +++ b/Launcher/macos/setup.command @@ -41,6 +41,28 @@ done host_arch=$(uname -m) case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac +# Same x86-64-v2 check as runtime/src/host_cpu_baseline.cpp, before anything is built; +# local-build-macos.command builds for v2 when the CPU lacks v3. +if [[ "$host_arch" == x86_64 ]]; then + cpu_features=$(/usr/sbin/sysctl -n machdep.cpu.features machdep.cpu.extfeatures 2>/dev/null | tr '\n' ' ' || true) + if [[ -n "${cpu_features// /}" ]]; then + missing_features="" + for feature in SSE3=SSE3 SSSE3=SSSE3 CX16=CMPXCHG16B SSE4.1=SSE4.1 SSE4.2=SSE4.2 POPCNT=POPCNT \ + LAHF=LAHF-SAHF; do + [[ " $cpu_features " == *" ${feature%%=*} "* ]] || missing_features+="${missing_features:+, }${feature#*=}" + done + if [[ -n "$missing_features" ]]; then + message="This build needs a processor that supports the x86-64-v2 instruction set. + +Missing on this machine: $missing_features + +x86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors from Jaguar (2013) onward." + printf 'WiiCompiled Setup: %s\n' "$message" >&2 + notice "$message" + exit 1 + fi + fi +fi host_tools="$resources/tools/$host_arch" nodtool="$host_tools/nodtool" translator="$host_tools/Translator.Cli"