diff --git a/Launcher/Test-NativeDependencies.ps1 b/Launcher/Test-NativeDependencies.ps1 index a8bbe8f..34377fc 100644 --- a/Launcher/Test-NativeDependencies.ps1 +++ b/Launcher/Test-NativeDependencies.ps1 @@ -18,7 +18,7 @@ $llvmReadobj = [System.IO.Path]::GetFullPath($LlvmReadobjPath) $systemDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) @( - 'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll', + 'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'bcryptprimitives.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll', 'd3d11.dll', 'd3d12.dll', 'dbghelp.dll', 'dcomp.dll', 'dwrite.dll', 'dwmapi.dll', 'dxgi.dll', 'gdi32.dll', 'imm32.dll', 'iphlpapi.dll', 'kernel32.dll', 'mf.dll', 'mfplat.dll', 'mfreadwrite.dll', 'mfuuid.dll', diff --git a/Launcher/Test-PinnedFacts.ps1 b/Launcher/Test-PinnedFacts.ps1 index aa16cc8..bd65c84 100644 --- a/Launcher/Test-PinnedFacts.ps1 +++ b/Launcher/Test-PinnedFacts.ps1 @@ -16,6 +16,7 @@ if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { $repoRoot = [IO.Path]::GetFullPath($RepositoryRoot) $launcher = Join-Path $repoRoot 'Launcher' $setup = Join-Path $launcher 'WiiCompiled.Setup.Windows' +$common = Join-Path $launcher 'WiiCompiled.Setup.Common' $failures = [Collections.Generic.List[string]]::new() function Add-Failure([string]$Message) { $failures.Add($Message) } @@ -48,9 +49,11 @@ function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedNa $pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml') # --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a -# --- redirected or rewritten endpoint cannot be fetched from. -$inputValidation = Read-SourceFile (Join-Path $setup 'InputValidation.cs') 'InputValidation.cs' -$hostUri = Get-CapturedValue $inputValidation 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' ` +# --- redirected or rewritten endpoint cannot be fetched from. The literal lives in +# --- WiiCompiled.Setup.Common (shared with WiiCompiled.Setup.Linux) - InputValidation.cs only +# --- re-exports it as `= RetroWfcPayload.CurrentRetroWfcPayloadUri;`, no literal to capture there. +$retroWfcPayload = Read-SourceFile (Join-Path $common 'RetroWfcPayload.cs') 'RetroWfcPayload.cs' +$hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' ` 'The host Retro-WFC endpoint constant' if ($hostUri -cne $pins.RetroWfcPayloadUri) { Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'." diff --git a/Launcher/build-appimage.sh b/Launcher/build-appimage.sh old mode 100644 new mode 100755 index fe56749..0fb3311 --- a/Launcher/build-appimage.sh +++ b/Launcher/build-appimage.sh @@ -21,6 +21,42 @@ set -euo pipefail script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) workspace=$(cd "$script_dir/.." && pwd) +# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64 +# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64 +# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool) +# will actually run, so this reads the ELF header of this script's own running bash interpreter - +# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe: +# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this +# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386, +# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every +# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no +# Linux distro ships a big-endian x86 or ARM userland. +elf_exe=$(readlink -f "/proc/$$/exe") +elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ') +elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ') +elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ') +elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo )) + +# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation. +# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the +# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself: +# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively +# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check). +case "$elf_class:$elf_machine" in + 2:62) + dotnet_rid=linux-x64 + appimagetool_arch=x86_64 + ;; + 2:183) + dotnet_rid=linux-arm64 + appimagetool_arch=aarch64 + ;; + *) + echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2 + exit 1 + ;; +esac + output_dir="$workspace/Launcher/dist" appimagetool_override="" @@ -40,10 +76,10 @@ appdir="$workspace/Launcher/artifacts/appimage-build/AppDir" rm -rf "$appdir" mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher" -echo "Publishing the installer (self-contained linux-x64)..." +echo "Publishing the installer (self-contained $dotnet_rid)..." publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish" rm -rf "$publish_tmp" -dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \ +dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \ --self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \ -o "$publish_tmp" cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup" @@ -52,10 +88,10 @@ chmod +x "$appdir/usr/bin/wiicompiled-setup" # Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on # PATH at all - local-build.sh is told about it via --translator-bin and skips its own # dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch). -echo "Publishing the translator (self-contained linux-x64)..." +echo "Publishing the translator (self-contained $dotnet_rid)..." translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator" rm -rf "$translator_publish_tmp" -dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \ +dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \ --self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \ -o "$translator_publish_tmp" cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli" @@ -155,11 +191,13 @@ PY echo "Resolving appimagetool..." appimagetool="$appimagetool_override" if [[ -z "$appimagetool" ]]; then - appimagetool="$workspace/Launcher/artifacts/appimagetool" + # Cache path is arch-tagged so a workspace shared or synced across an x86_64 and an aarch64 + # machine never picks up the wrong architecture's cached binary. + appimagetool="$workspace/Launcher/artifacts/appimagetool-$appimagetool_arch" if [[ ! -x "$appimagetool" ]]; then - echo "Downloading appimagetool..." + echo "Downloading appimagetool ($appimagetool_arch)..." mkdir -p "$(dirname "$appimagetool")" - curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \ + curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \ -o "$appimagetool" chmod +x "$appimagetool" fi @@ -169,5 +207,6 @@ mkdir -p "$output_dir" echo "Packaging..." # appimagetool detects the target architecture from the first ELF executable it finds in the # AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly. -ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage" -echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage" +output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage" +ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name" +echo "Built: $output_dir/$output_name" diff --git a/aurora-main/lib/input.cpp b/aurora-main/lib/input.cpp index 2b3651e..e22e764 100644 --- a/aurora-main/lib/input.cpp +++ b/aurora-main/lib/input.cpp @@ -329,12 +329,52 @@ void apply_port_preferences() noexcept { } } +#if defined(_WIN32) // Ports are explicit assignments. SDL may choose a player index at connection // time, but accepting it would make a newly connected controller silently take -// over a game port before the user assigns it in the controller menu. +// over a game port before the user assigns it in the controller menu - which +// matters here because a manually-assigned WUP-028 adapter port (see +// wup028_adapter.cpp, Windows-only) could otherwise collide with one SDL +// auto-claimed. Elsewhere, with no WUP-028 port to collide with, the original +// auto-claim behavior below is restored instead. void ensure_player_index(GameController& controller) noexcept { assign_player_index(controller, -1); } +#else +// SDL only hands out a player index when the device already had a gamepad mapping +// at connect time, so anything mapped later (the setup wizard) stays at -1. +void ensure_player_index(GameController& controller) noexcept { + const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller); + if (player >= 0) { + controller.m_playerIndex = player; + return; + } + if (controller.m_playerIndex >= 0) { + return; + } + ensure_port_preferences_loaded(); + const auto claim = [&](bool skipConfiguredPorts) { + for (int32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) { + if (skipConfiguredPorts && g_portPreferences[port].state != PortPreferenceState::Unset) { + continue; + } + const bool taken = std::any_of(g_GameControllers.begin(), g_GameControllers.end(), [&](const auto& entry) { + return entry.second.m_controller != controller.m_controller && effective_player_index(entry.second) == port; + }); + if (!taken) { + assign_player_index(controller, port); + return true; + } + } + return false; + }; + // Explicitly configured ports are only used as a last resort so a hot-plugged + // controller cannot steal the port its preferred device will claim. + if (!claim(true)) { + claim(false); + } +} +#endif } // namespace GameController* get_controller_for_player(uint32_t player) noexcept { diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 642de5f..ba948ee 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -237,6 +237,15 @@ 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}) +# WUP-028 (official GameCube adapter) support talks to the adapter over WinUSB, which only +# exists on Windows - SDL3 already exposes the same hardware as a normal joystick on Linux/macOS, +# so this file has nothing to do there. Every call site into it is separately gated behind +# #if defined(_WIN32) (see wup028_adapter.cpp's own header comment), so it's safe to simply not +# compile it at all on other platforms rather than build a stub implementation. +if(NOT WIN32) + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/wup028_adapter.cpp") +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. diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h index f8a3bae..0a0debf 100644 --- a/runtime/include/runtime_config.h +++ b/runtime/include/runtime_config.h @@ -63,9 +63,11 @@ struct RuntimeUserConfig { // comma-separated SDL-style physical button names ("south", or // "dpad_up,left_shoulder") as values; pressing either bound button counts. std::array, 12> controllerButtons; +#ifdef _WIN32 // One-based physical WUP-028 adapter port assigned to each game port. // Zero or a missing value means the adapter does not own that game port. std::array gameCubeAdapterPorts{}; +#endif }; namespace RuntimeConfigFile { @@ -365,12 +367,14 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) { config.controllerButtons[index] = FindConfigValue(document, "controller", buttonKeys[index]); } +#ifdef _WIN32 for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) { const std::string key = "adapter_port_" + std::to_string(index + 1); if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) { config.gameCubeAdapterPorts[index] = *value; } } +#endif config.widescreen = FindConfigValue(document, "video", "widescreen"); config.windowPosX = FindConfigInt(document, "video", "window_x"); @@ -634,6 +638,7 @@ inline bool SetControllerButton(size_t index, std::string value) { return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value)); } +#ifdef _WIN32 inline int GameCubeAdapterPort(size_t gamePort) { if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1; const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort]; @@ -646,6 +651,7 @@ inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) { Mutable().gameCubeAdapterPorts[gamePort] = storedPort; return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort)); } +#endif inline bool SetAudioVolume(float value) { value = std::clamp(value, 0.0f, 1.0f); diff --git a/runtime/src/hle/input/pad.cpp b/runtime/src/hle/input/pad.cpp index 68645d7..2a71351 100644 --- a/runtime/src/hle/input/pad.cpp +++ b/runtime/src/hle/input/pad.cpp @@ -1,7 +1,9 @@ #include "hle_stubs.h" #include "memory.h" #include "hle/controller_status_contract.h" +#ifdef _WIN32 #include "wup028_adapter.h" +#endif #include #include @@ -34,7 +36,9 @@ void WritePadStatus(uint32_t base, const PADStatus& status) { extern "C" uint32_t PAD__Init_HLE() { +#if defined(_WIN32) Wup028Adapter::Initialize(); +#endif return PADInit() ? 1u : 0u; } PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ()); @@ -46,8 +50,9 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) } PADStatus statuses[PAD_CHANMAX]{}; - std::array adapterStatuses{}; uint32_t rumbleMask = PADRead(statuses); +#if defined(_WIN32) + std::array adapterStatuses{}; if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) { for (uint32_t port = 0; port < PAD_CHANMAX; ++port) { if (adapterStatuses[port].err == PAD_ERR_NONE) { @@ -56,6 +61,7 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) } } } +#endif try { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { @@ -84,8 +90,12 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), ( extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command) { +#if defined(_WIN32) if (!Wup028Adapter::SetRumble(static_cast(chan), command == PAD_MOTOR_RUMBLE)) { PADControlMotor(chan, command); } +#else + PADControlMotor(chan, command); +#endif } PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command)); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index edc6b98..84d7a44 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1377,7 +1377,9 @@ int RuntimeMain(int argc, char** argv) { } aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait); GxGuestWrite::InstallAuroraHooks(); +#if defined(_WIN32) Wup028Adapter::Initialize(); +#endif UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width, auroraInfo.windowSize.native_fb_height); settings_overlay::InitializeRuntimeSettings(); @@ -1419,7 +1421,9 @@ int RuntimeMain(int argc, char** argv) { // Shutdown fiber system Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); +#if defined(_WIN32) Wup028Adapter::Shutdown(); +#endif aurora_shutdown(); SetRuntimeExitCodeImpl(0); ShutdownProcessTranscript(); @@ -1437,7 +1441,9 @@ int RuntimeMain(int argc, char** argv) { SetRuntimeExitCodeImpl(1); Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); +#if defined(_WIN32) Wup028Adapter::Shutdown(); +#endif aurora_shutdown(); ShutdownProcessTranscript(); return 1; @@ -1449,7 +1455,9 @@ int RuntimeMain(int argc, char** argv) { SetRuntimeExitCodeImpl(1); Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); +#if defined(_WIN32) Wup028Adapter::Shutdown(); +#endif aurora_shutdown(); ShutdownProcessTranscript(); return 1; diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index fcf3389..cb2497c 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1,5 +1,7 @@ #include "settings_overlay.h" +#ifdef _WIN32 #include "wup028_adapter.h" +#endif #include "audio_backend.h" #include "controller_mapping_wizard.h" #include "game_graphics_options.h" @@ -313,6 +315,12 @@ void ApplyConfiguredMappings() { } void DrawGameCubeAdapterInfo() { + // The official GameCube adapter is Windows-only (see wup028_adapter.cpp); on Linux/macOS, + // Wup028Adapter is a permanently-disconnected stub, and SDL3 already exposes the same + // hardware as a normal joystick, so this menu would only ever show "Searching" and four + // perpetually-empty adapter ports - confusing clutter for a feature that can't do anything + // on this platform. Skip it entirely rather than render a menu that never has content. +#if defined(_WIN32) ImGui::Separator(); if (!ImGui::BeginMenu("GameCube adapter info")) return; @@ -338,6 +346,7 @@ void DrawGameCubeAdapterInfo() { } } ImGui::EndMenu(); +#endif } void DrawControllerSettings() { @@ -351,13 +360,20 @@ void DrawControllerSettings() { ImGui::Separator(); const uint32_t selectedGamePort = static_cast(g_controllerPort); +#if defined(_WIN32) const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort); if (adapterAssignment >= 0) { ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1); - } else { + } else +#endif + { const char* currentName = PADGetName(selectedGamePort); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); } +#if defined(_WIN32) + // Windows-only, same reasoning as DrawGameCubeAdapterInfo() above: on other platforms + // adapterAssignment is always -1 and every port would always read "(empty)", so this submenu + // would never have anything real to offer. if (ImGui::BeginMenu("Assign GameCube adapter port")) { if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) { Wup028Adapter::SetPortAssignment(selectedGamePort, -1); @@ -381,10 +397,13 @@ void DrawControllerSettings() { } ImGui::EndMenu(); } +#endif if (ImGui::MenuItem("Unassign controller")) { PADClearPort(selectedGamePort); +#if defined(_WIN32) Wup028Adapter::SetPortAssignment(selectedGamePort, -1); RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1); +#endif g_configuredControllerIndices.fill(std::numeric_limits::min()); } ImGui::Separator(); @@ -392,7 +411,9 @@ void DrawControllerSettings() { const uint32_t controllerCount = PADCount(); if (controllerCount == 0) { ImGui::TextDisabled("No controller connected"); +#if defined(_WIN32) DrawGameCubeAdapterInfo(); +#endif return; } @@ -401,8 +422,10 @@ void DrawControllerSettings() { const char* name = PADGetNameForControllerIndex(index); ImGui::PushID(static_cast(index)); if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) { +#if defined(_WIN32) Wup028Adapter::SetPortAssignment(selectedGamePort, -1); RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1); +#endif PADSetPortForIndex(index, selectedGamePort); g_configuredControllerIndices.fill(std::numeric_limits::min()); ApplyConfiguredMappings(); @@ -416,7 +439,9 @@ void DrawControllerSettings() { PADButtonMapping* mappings = PADGetButtonMappings(static_cast(g_controllerPort), &mappingCount); if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) { ImGui::TextDisabled("Assign a controller to edit its buttons"); +#if defined(_WIN32) DrawGameCubeAdapterInfo(); +#endif return; } @@ -556,7 +581,9 @@ void DrawControllerSettings() { ImGui::TextUnformatted(kControllerButtons[i].label); ImGui::PopID(); } +#if defined(_WIN32) DrawGameCubeAdapterInfo(); +#endif } void DrawAudioSettings() {