From f555de201fa4418295491ec59d1df110cd2b3e46 Mon Sep 17 00:00:00 2001 From: Toby Fox <78795456+112cxyz@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:12:53 +0100 Subject: [PATCH] feature: iOS Support (#116) * build: add an iOS arm64 target Selects iOS before macOS, since APPLE is true for both and the macOS branch would otherwise claim an iOS configure and pick a memory backend that cannot compile there. Third-party libraries are pinned static on Apple targets. aurora picks shared ones whenever BUILD_SHARED_LIBS is merely undefined, and its extern/ tree unsets the cache entry while working around xxhash, so passing it on the command line only survives one configure. On iOS that was fatal rather than untidy: libpng linked as a dylib with an @rpath into the build tree, which dyld cannot resolve inside a bundle. User state lives in the container's Documents rather than a name-scoped Application Support directory, so Config.toml sits beside the game data a relative dvd_root resolves against, and both are reachable over file sharing. (cherry picked from commit ad7231fca3d55c5d7d1807c5accb6874d702111f) (cherry picked from commit 76cc9594e02d5a858c0848b4b4e4e5a585151cc0) (cherry picked from commit 80a6b53abd699942178fde2f16010d54dba9952b) * ios: resolve the flat guest base at runtime No fixed base works on every device. Probing an iPhone 17 Pro and an iPad Pro M5 with the same 24-candidate sweep gave disjoint sets of free 4 GiB windows - 448 GiB only, against 12, 16, 20, 24, 32 and 48 GiB - and the extended-virtual-addressing entitlement changed neither map. So iOS takes whatever the kernel offers and publishes it, while every other target keeps its compile-time constant. The backend is the macOS one adapted twice over: is not in the iOS SDK, so the vm_* calls are used instead, and the backing store is an anonymous mapping rather than a file in /tmp, which the sandbox denies. g_requiresCheckedAccess is set from the real page size. Apple Silicon's 16 KiB host pages are coarser than the 4 KiB guest page, so page protection alone cannot be relied on. (cherry picked from commit 0677b83ed45e8b59d779d37cad172c737daf08eb) (cherry picked from commit 9a8a80ffd4c3f87652e37208fc88afd0a94e6cf8) (cherry picked from commit 2b7707e6226ee20a6c0564c0f9bbcdd06b511061) * ios: pick a display explicitly and survive a second scene A bare SDL_WINDOWPOS_CENTERED does not say which display it means. With an external screen attached that left the window sized for one display and the surface for another, which played audio over a black screen. The primary display is now named outright and its bounds used. SDL calls SDL_main from scene:willConnectToSession:, once per connecting scene, and iOS creates a second scene when a display is attached. That re-entered main while the first call was still inside the game loop: the data sections reloaded, no static constructors ran the second time, and aurora then failed to create a window. Declaring UIApplicationSupportsMultipleScenes=false does not prevent the extra scene, so the guard belongs here. (cherry picked from commit d8b984cfbb8521f06776ae765131452c4380e473) (cherry picked from commit d20895a98739c11f97de1222834859e2a55e3516) (cherry picked from commit 492f57ef5122305b0ab63258a5a4efe960f5abc9) * ios: add on-screen touch controls A touch device has no pad and no F10, so without these it boots to a screen nothing can drive. The overlay polls SDL's finger list rather than using ImGui widgets, whose SDL backend collapses touch to one emulated mouse - steering, accelerating and drifting all happen at once. Input merges into PAD__Read_HLE only when port 0 has no real pad, so a controller always wins, and the overlay hides itself while one is attached. Positions are held as a distance from a screen edge in units of screen height. A fraction of width lands somewhere different on a 1.45:1 iPad and a 2.17:1 iPhone. The d-pad is one cross with the direction taken from the dominant axis, and the stick keeps the finger that grabbed it until that finger lifts, so sliding past the gate does not drop steering mid-corner. Buttons use Zacksly's GameCube icons, CC BY 3.0, with attribution beside them in the bundle. The dark backing is each glyph's own silhouette tinted black, so the d-pad and the triggers are not given a disc they do not have. They are decoded up front: doing it lazily made the frame the controls appear on pay for every decode at once. Tapping the FPS readout opens the settings bar, which is otherwise unreachable - and the readout's tap target is cleared before the early return when FPS display is off, or a stale rectangle keeps swallowing taps meant for the fallback menu button. PADIsInputBlocked comes back to aurora; it went with the WUP-028 revert and the overlay needs it to avoid driving the guest while the bar is up. (cherry picked from commit 79d3de3c0b9c883ff2ec34a5964498a475e91240) (cherry picked from commit cf7a8edb3405d4f7d7fd189d19cdb2f746b190f9) (cherry picked from commit 360261208c95616ec81d8f8c3d69efc3c776c54b) * ios: build an unsigned ipa CMake bundles iOS targets with its own default Info.plist, whose CFBundleIdentifier is empty and which carries none of the iOS keys, so what came out of the build could not be installed. It now gets a real plist and is packaged as an .ipa. Unsigned on purpose: AltStore, SideStore and LiveContainer sign on the device with the user's own Apple ID, so anything applied here would only be replaced. WiiCompiled.entitlements is a template for signing by hand, which those tools ignore. Packaging runs after the asset copies rather than before, or the archive gets the binary and nothing else. The touch directory is cleared before it is copied, since copy_directory merges and a removed icon would otherwise be shipped. (cherry picked from commit 5ce43949dbf27abff6782bc41e3be0ede3ac135b) (cherry picked from commit 633e3e0aa2f7ea116741f27ec46e9c743be9cf9a) (cherry picked from commit a4ae6f2e8bdf11596a89dca9607aba92713184be) * docs: document the iOS build Covers building the .ipa, sideloading it, the one entitlement the app needs, and where the game data goes. The configure line is the one that works: without CMAKE_SYSTEM_PROCESSOR the Dawn package URL comes out as dawn-ios-.tar.gz and the build fails on a 404. (cherry picked from commit 08eefad3fd1cb5e8f79a144ac119bc65852e33e6) (cherry picked from commit b7cd38e07eefc8af4c1c60ca411c009eec4e212a) (cherry picked from commit 5a1650ef194f9da4ae8a68f1a9779b16ee118ca8) * ios: stub out Discord presence It connects over a Unix socket to a local Discord client, which iOS does not have, so it can only ever fail and back off. Stubbed rather than left retrying. (cherry picked from commit 69aa5e5c297abe65188d391748b8fb11c57337d2) * build: keep the macOS host tests off iOS and give the audit target libpng The macOS test executables were gated on MKW_PLATFORM_MACOS, which iOS also sets, so a full iOS build tried to compile guest_flat_memory_macos.cpp against an SDK with no . The products-off audit compile picked up the touch overlay sources without the PNG include path. * ios: tidy the touch decode and pad merge after review The row-pointer buffer now lives in PngReader so a libpng longjmp cannot skip its destructor. PAD__Read_HLE no longer uses the port 0 error code to decide whether touch applies; keyboard bindings report PAD_ERR_NONE with no controller attached, and TouchPad::Read already checks for a physical pad. * docs: tag the iOS build fence as sh * ios: pin the CPU baseline instead of tuning for the build host -mcpu=native tuned the phone binary for whichever Mac compiled it, and upstream clang rejects the flag outright when cross-compiling. iOS 17 implies the A12 and later. * build: cross-compile the iOS products from Linux * use online sdk instead of mac sdk to build on linux * build: ios platform identifier * build: build the ios ipa on windows too * fix: strap cover ends too early on slower devices * ci: compile the ios runtime on a linux runner * docs: note the entitlement needed on devices with less than 4gb ram --- .github/workflows/build.yml | 43 +++ README.md | 86 +++++ aurora-main/include/dolphin/pad.h | 1 + aurora-main/lib/dolphin/pad/pad.cpp | 2 + aurora-main/lib/window.cpp | 20 +- runtime/CMakeLists.txt | 45 ++- runtime/assets/touch/ATTRIBUTION.txt | 8 + runtime/assets/touch/a.png | Bin 0 -> 5093 bytes runtime/assets/touch/a_fill.png | Bin 0 -> 2917 bytes runtime/assets/touch/b.png | Bin 0 -> 5515 bytes runtime/assets/touch/b_fill.png | Bin 0 -> 3104 bytes runtime/assets/touch/control_stick.png | Bin 0 -> 7176 bytes runtime/assets/touch/control_stick_fill.png | Bin 0 -> 5985 bytes runtime/assets/touch/d-pad.png | Bin 0 -> 3533 bytes runtime/assets/touch/d-pad_fill.png | Bin 0 -> 2657 bytes runtime/assets/touch/l_analog.png | Bin 0 -> 3312 bytes runtime/assets/touch/l_analog_fill.png | Bin 0 -> 2222 bytes runtime/assets/touch/r_analog.png | Bin 0 -> 4882 bytes runtime/assets/touch/r_analog_fill.png | Bin 0 -> 2781 bytes runtime/assets/touch/right_bumper.png | Bin 0 -> 4325 bytes runtime/assets/touch/right_bumper_fill.png | Bin 0 -> 2458 bytes runtime/assets/touch/start_pause.png | Bin 0 -> 6902 bytes runtime/assets/touch/start_pause_fill.png | Bin 0 -> 6182 bytes runtime/cmake/PublicProducts.cmake | 71 +++- runtime/cmake/ios-linux-toolchain.cmake | 71 ++++ runtime/cmake/ios/Info.plist.in | 26 ++ runtime/cmake/ios/WiiCompiled.entitlements | 11 + runtime/include/guest_flat_memory.h | 14 + runtime/include/settings_overlay.h | 3 + runtime/include/touch_art.h | 18 + runtime/include/touch_pad.h | 27 ++ runtime/src/discord_presence.cpp | 17 + runtime/src/guest_flat_memory_ios.cpp | 154 ++++++++ runtime/src/hle/input/pad.cpp | 13 + runtime/src/hle/input/touch_art.cpp | 108 ++++++ runtime/src/hle/input/touch_pad.cpp | 408 ++++++++++++++++++++ runtime/src/main.cpp | 26 ++ runtime/src/platform/host_platform.cpp | 12 + runtime/src/platform/ios/compiler_rt_shim.c | 21 + runtime/src/settings_overlay.cpp | 37 +- 40 files changed, 1231 insertions(+), 11 deletions(-) create mode 100644 runtime/assets/touch/ATTRIBUTION.txt create mode 100644 runtime/assets/touch/a.png create mode 100644 runtime/assets/touch/a_fill.png create mode 100644 runtime/assets/touch/b.png create mode 100644 runtime/assets/touch/b_fill.png create mode 100644 runtime/assets/touch/control_stick.png create mode 100644 runtime/assets/touch/control_stick_fill.png create mode 100644 runtime/assets/touch/d-pad.png create mode 100644 runtime/assets/touch/d-pad_fill.png create mode 100644 runtime/assets/touch/l_analog.png create mode 100644 runtime/assets/touch/l_analog_fill.png create mode 100644 runtime/assets/touch/r_analog.png create mode 100644 runtime/assets/touch/r_analog_fill.png create mode 100644 runtime/assets/touch/right_bumper.png create mode 100644 runtime/assets/touch/right_bumper_fill.png create mode 100644 runtime/assets/touch/start_pause.png create mode 100644 runtime/assets/touch/start_pause_fill.png create mode 100644 runtime/cmake/ios-linux-toolchain.cmake create mode 100644 runtime/cmake/ios/Info.plist.in create mode 100644 runtime/cmake/ios/WiiCompiled.entitlements create mode 100644 runtime/include/touch_art.h create mode 100644 runtime/include/touch_pad.h create mode 100644 runtime/src/guest_flat_memory_ios.cpp create mode 100644 runtime/src/hle/input/touch_art.cpp create mode 100644 runtime/src/hle/input/touch_pad.cpp create mode 100644 runtime/src/platform/ios/compiler_rt_shim.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f258e14..feb7070 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,3 +34,46 @@ jobs: - name: Test run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal + + ios-crosscompile: + name: iOS arm64 (cross-compile, no Xcode) + runs-on: ubuntu-24.04 + env: + IOS_SDK_DIR: ${{ github.workspace }}/iOS-SDKs/iPhoneOS26.5.sdk + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # The runner images carry clang up to 18; the iOS toolchain file is tested + # against upstream 19, so take it from apt.llvm.org rather than the archive. + - name: Install clang 19, lld and ninja + run: | + wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh + chmod +x /tmp/llvm.sh + sudo /tmp/llvm.sh 19 + sudo apt-get install -y lld-19 ninja-build + + # The only Apple piece in the build. Sparse-cloned so the checkout is one + # SDK (~50 MB) rather than every SDK the repository carries. + - name: Fetch the iPhoneOS SDK + run: | + git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/xybp888/iOS-SDKs.git "${{ github.workspace }}/iOS-SDKs" + git -C "${{ github.workspace }}/iOS-SDKs" sparse-checkout set --no-cone iPhoneOS26.5.sdk + + # Compile-only: linking a product needs the translated shards, which come + # from the user's own Mario Kart Wii dump and are not in this repository. + # This proves the runtime's own sources still build for iOS arm64. + - name: Configure + run: | + cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/runtime/cmake/ios-linux-toolchain.cmake" \ + -DIOS_SDK="$IOS_SDK_DIR" \ + -DMKW_IOS_LLVM_BIN=/usr/lib/llvm-19/bin \ + -DMKW_BUILD_PRODUCTS=OFF \ + -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \ + -DAURORA_DAWN_PROVIDER=package + + - name: Compile the runtime sources for iOS + run: cmake --build build-ios --target mkw_macos_native_compile diff --git a/README.md b/README.md index 3201692..2e06657 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,92 @@ Wheel Wizard downloads the setup tool from this repo and walks you through insta launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. +### iOS + +Requires macOS with Xcode. Build the `WiiCompiled` target for iOS arm64; the build writes +`WiiCompiled-unsigned.ipa` next to the app bundle. + +```sh +cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_SYSTEM_PROCESSOR=arm64 \ + -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphoneos \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \ + -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ + -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \ + -DAURORA_DAWN_PROVIDER=package +cmake --build build-ios --target WiiCompiled +``` + +#### iOS from Linux + +The same .ipa builds on a Linux host with upstream clang and lld; no Theos, xtool or Xcode. Tested +on Debian 13 with `clang-19 lld-19 llvm-19 cmake ninja-build git`. The only Apple piece is the +iPhoneOS SDK, and [xybp888/iOS-SDKs](https://github.com/xybp888/iOS-SDKs) carries current ones: + +```sh +git clone --depth 1 --filter=blob:none --sparse https://github.com/xybp888/iOS-SDKs.git /opt/iOS-SDKs +git -C /opt/iOS-SDKs sparse-checkout set --no-cone iPhoneOS26.5.sdk +``` + +Pass that directory as `IOS_SDK` (or copy `iPhoneOS.sdk` out of a Mac's Xcode to `/opt/iPhoneOS.sdk`, +the default). The translated shard manifest under `generated/` records absolute paths from the +machine that ran the translator, so either translate on the Linux host or symlink that path to your +checkout. + +```sh +cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake -DIOS_SDK=/opt/iOS-SDKs/iPhoneOS26.5.sdk \ + -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package +cmake --build build-ios --target WiiCompiled +``` + +#### iOS from Windows + +The same toolchain file works on Windows with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw), +which is what the Windows build already uses. Three things differ from a Linux host: + +- llvm-mingw does not ship `ld64.lld.exe` or `llvm-install-name-tool.exe`; LLVM tools dispatch on + their file name, so copy `ld.lld.exe` to `ld64.lld.exe` and `llvm-objcopy.exe` to + `llvm-install-name-tool.exe` in its `bin` directory. +- Git for Windows checks the SDK repo's symlinks out as small text files unless `core.symlinks` + is on (Developer Mode). Either enable that before cloning, or replace each placeholder with a + copy of its target; `libSystem.tbd` is one of them and the link fails without it. +- Pass the toolchain file as an absolute path, and `MKW_IOS_LLVM_BIN` as the llvm-mingw `bin` + directory with forward slashes. + +```powershell +cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_TOOLCHAIN_FILE=C:/src/Wiicompiled/runtime/cmake/ios-linux-toolchain.cmake ` + -DIOS_SDK=C:/iOS-SDKs/iPhoneOS26.5.sdk -DMKW_IOS_LLVM_BIN=C:/llvm-mingw/bin ` + -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package +cmake --build build-ios --target WiiCompiled +``` + +Install it with AltStore or SideStore, which sign with your own Apple ID. The app needs the +`com.apple.developer.kernel.increased-memory-limit` entitlement or it exits at startup; +[GetMoreRam](https://github.com/hugeBlack/GetMoreRam) grants it with a free Apple ID. +GetMoreRam grants `com.apple.developer.kernel.extended-virtual-addressing` at the same time, which +matters on devices with less than 4 GB of RAM: the runtime reserves a 4 GiB guest address space at +startup and that is right on the limit iOS allows without it. It makes no difference on the 8 GB +devices this was developed on. + +`runtime/cmake/ios/WiiCompiled.entitlements` is a template for signing by hand, with placeholders to +replace; the sideloaders build their own and ignore it. It carries both entitlements, so the +provisioning profile you sign with needs both capabilities enabled. + +Copy `Config.toml` and an extracted `DATA` directory into the app's Documents folder, using Files on +the device or the Finder with it connected. Keep `dvd_root` relative: + +```toml +[paths] +dvd_root = "DATA" +``` + +Touch controls appear when no gamepad is attached; tap the FPS readout for settings. + + > [!CAUTION] > Only take builds from this repository's > [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an diff --git a/aurora-main/include/dolphin/pad.h b/aurora-main/include/dolphin/pad.h index 6f821c3..4c96538 100644 --- a/aurora-main/include/dolphin/pad.h +++ b/aurora-main/include/dolphin/pad.h @@ -229,6 +229,7 @@ s32 PADGetNativeButtonPressed(u32 port); PADSignedNativeAxis PADGetNativeAxisPulled(u32 port); void PADRestoreDefaultMapping(u32 port); void PADBlockInput(bool block); +bool PADIsInputBlocked(void); /** * Set the default controller mapping used. diff --git a/aurora-main/lib/dolphin/pad/pad.cpp b/aurora-main/lib/dolphin/pad/pad.cpp index bb82c24..fce76d1 100644 --- a/aurora-main/lib/dolphin/pad/pad.cpp +++ b/aurora-main/lib/dolphin/pad/pad.cpp @@ -1627,6 +1627,8 @@ void PADBlockInput(const bool block) { } } +bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); } + SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) { const auto* ctrl = __PADGetControllerForIndex(index); diff --git a/aurora-main/lib/window.cpp b/aurora-main/lib/window.cpp index 86649c9..c187a18 100644 --- a/aurora-main/lib/window.cpp +++ b/aurora-main/lib/window.cpp @@ -11,6 +11,9 @@ #include #include #include +#if defined(__APPLE__) +#include +#endif #include #include #include @@ -408,8 +411,21 @@ bool create_window(AuroraBackend backend) { height = 480; } - const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED; - const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED; + Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED; + Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED; +#if defined(__APPLE__) && TARGET_OS_IPHONE + // Without this the window falls back to the 1280x960 default on any device + // with no saved size. + if (const SDL_DisplayID primary = SDL_GetPrimaryDisplay(); primary != 0) { + SDL_Rect bounds{}; + if (SDL_GetDisplayBounds(primary, &bounds) && bounds.w > 0 && bounds.h > 0) { + width = bounds.w; + height = bounds.h; + } + posX = SDL_WINDOWPOS_CENTERED_DISPLAY(primary); + posY = SDL_WINDOWPOS_CENTERED_DISPLAY(primary); + } +#endif const auto props = SDL_CreateProperties(); TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}", diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 6fd79e2..15ecdc4 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -7,6 +7,13 @@ endif() if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") set(MKW_PLATFORM_WINDOWS TRUE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") + # Checked before macOS: APPLE is true for iOS too, so the macOS branch would + # otherwise claim an iOS build and select a backend that cannot compile + # there ( is absent from the iOS SDK) or run there (/tmp is + # outside the sandbox). + set(MKW_PLATFORM_IOS TRUE) + set(MKW_PLATFORM_MACOS TRUE) # shares the Apple arm64 substrate elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$") # The first native macOS target is Apple Silicon. Intel and universal # binaries remain future compatibility work; do not silently claim them. @@ -15,13 +22,16 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^( set(MKW_PLATFORM_LINUX TRUE) else() message(FATAL_ERROR - "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64") + "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux " + "x86_64/aarch64, or Apple Clang on macOS arm64 or iOS arm64") endif() if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") message(FATAL_ERROR "WiiCompiled only supports Release builds") endif() option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) +set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING + "Reverse-DNS prefix for the iOS bundle identifier") # Preprocessor definitions that belong to this project's own code (the runtime, # the translated shards and the product glue) and to nothing else. They are @@ -30,6 +40,11 @@ option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) # libraries and the products in cmake/PublicProducts.cmake) inherits them while # aurora-main and its third-party tree stay unaffected. set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX) +if(MKW_PLATFORM_IOS) + # One project-wide answer to "is this iOS", so sources do not each pull in + # TargetConditionals.h and spell out the __APPLE__ && TARGET_OS_IPHONE dance. + list(APPEND MKW_PROJECT_COMPILE_DEFINITIONS MKW_PLATFORM_IOS=1) +endif() set(CMAKE_CXX_STANDARD 17) @@ -136,6 +151,16 @@ else() if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt) message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") endif() + # aurora picks shared third-party libraries whenever BUILD_SHARED_LIBS is + # merely undefined, and its extern/ tree calls unset(BUILD_SHARED_LIBS CACHE) + # while working around xxhash, which deletes any -D the user passed. Defining + # it here, before aurora is added, is what actually sticks across + # reconfigures. Apple builds ship a self-contained bundle, so a dylib + # resolved from the build tree or from Homebrew cannot be allowed to leak + # into the link. + if(APPLE) + set(BUILD_SHARED_LIBS OFF) + endif() set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) if(MKW_PLATFORM_WINDOWS) set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) @@ -249,8 +274,18 @@ if(MKW_PLATFORM_MACOS) # mkw_co_init/mkw_co_switch at link time. enable_language(ASM) list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") + set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S" + PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON SKIP_PRECOMPILE_HEADERS ON) + # iOS cannot use the macOS backend: is absent from that SDK + # and /tmp is outside the sandbox. + if(MKW_PLATFORM_IOS) + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") + else() + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp") + endif() else() list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp") endif() set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp") set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp") @@ -292,9 +327,11 @@ if(MKW_PLATFORM_LINUX) add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests) endif() -if(MKW_PLATFORM_MACOS) +if(MKW_PLATFORM_MACOS AND NOT MKW_PLATFORM_IOS) # Exercise the Apple Silicon context ABI and the public host-memory - # contracts separately from translated products. + # contracts separately from translated products. Host-only: the flat memory + # test pulls in guest_flat_memory_macos.cpp, whose does not + # exist in the iOS SDK, and none of these can run on a device anyway. enable_language(ASM) add_executable(mkw_macos_context_abi_tests "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" @@ -364,7 +401,7 @@ else() target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC) target_link_libraries(mkw_macos_native_compile PRIVATE aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx - mkw::pugixml mkw::toml11 mkw::cryptopp) + PNG::PNG mkw::pugixml mkw::toml11 mkw::cryptopp) set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF) endif() add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform) diff --git a/runtime/assets/touch/ATTRIBUTION.txt b/runtime/assets/touch/ATTRIBUTION.txt new file mode 100644 index 0000000..f32db97 --- /dev/null +++ b/runtime/assets/touch/ATTRIBUTION.txt @@ -0,0 +1,8 @@ +GameCube Button Icons and Controls - Zacksly +Licensed under CC BY 3.0 - https://zacksly.itch.io + +The PNG files in this directory are taken unmodified from the "Buttons Outline / +White" set of that pack, at 256px. They are drawn tinted and partly transparent +at runtime, which is a rendering choice and not an edit to the artwork. + +Full licence: http://creativecommons.org/licenses/by/3.0/ diff --git a/runtime/assets/touch/a.png b/runtime/assets/touch/a.png new file mode 100644 index 0000000000000000000000000000000000000000..96c75a01c36cf050cdebc01d6d1fbbb9ac2d8bca GIT binary patch literal 5093 zcmd5=_g520*Pes~BB4YCB25Ac(u;J^5Co))0@4JGfCADXDkvp@NLz{^$Vw3@vMjwQ zy(vv;p(sd=f`+1ornH27+4KDq?+m!4+|+WC@gcrU=< z;?6<;x{Z7BG+Zz+hb9&G+}bQ|{F69SqN#G|{XJY4KyqfB(_2**L#e|#8pw?I1`yzG z)g8=+Q$PU#V}Ssi(*Lwz{qEiFW64fO)3Qs;jKsz3A89YcOC}+V*1_u25sEb3fZW*k z?$I7Ar+j}+%KuufM(-g1I3T$8UQ@O#G7+#Y&Y356SM$PMn94sG#5XqRmCK$si{#6U zS4*9@p2gzX{Pg#U;AvdV#QmpM>-k50`X!V6Jq71+AhD!7@2ILj51Vj>a+tNfIY|r=3F)30}dc4f{tQpGuOAj+KT$ zjN>G8{xv-NbEh=ucahd1o8S>4X5z1F?iPB4O1&|KVTD%P0f`zI6mNMoBI1S7TO~H# zQMmTYlaIW4X5jRQ9yYlskUilVtUD^*K5v{t?-uT-n8?<-h*_Ggq|Hay&2yh6f^5B_ z5&xjQT&!hmtm3oj__nPYu3z1PBsOS)QHG9dR#C?wGyAq4a+)qnZIo6N7#d~Gp*g6L zm5?KpYTr_9saVN{ZVhK zVV4soT8E0(=NBwKah6zE@V=ob_|^P=*-~du(Tkmt>b+Bhy?me2u%CB3BD_=F455&C zj~JsPRXRPKK>OPu7O&g8UbR&}H$c|W;f9gVO(GV}YPYQq*5uU<$2O~94eqY-FSe@M zfAg4;-lQtP5M71?=x~h^@86+*EeUgXPyg9lm|uGMH!D@#jx3IVV%=nWag$hcx{cS? zwn@48mU=~0FdmYdVM{4}vL=#!SazZXPAzuloa-C^%3LU(+JBOf&+S#!jq*Qc+z7_k zXtQ%lWBc_KVr7)-D~{gl&~!K^dHKXSBdW2eAv-yqx}V4E6g`{H5@Y2|yv#m!giSR# z`8%m3(`uwoB;ga$05VP1x+#?A>ZmYo`Z>aEMqJ!W0VfO3Tr{L8iWt$%@5y&gXEik3 zG~vg^lxFyt8*W~8W2@o(@kfh_d(>rC2Yl!Tgb0ll`w>)y>4&+e;$T;vu1|<)tl^8g z>!-4Cx*J}@dGK<(HK|gE9Dap#AL6;Cnpd`(Izq*&i<43(V>fsf;pDEjJBz)7LH^UV zQr99@wugwL%0!E#k;wQbi?Z4@a|`=jEo+h;lE{0g|1Q|jg5}uTdxCp>qV?GD*T6g@ z{J?l?GfT-~fbf&}%T6s@DLxtl%w6TeMlqZ zglxcYoR{8|Jec^Qpg1X1oLI)UO8E58I)B&Wo4n|4I$khCy>{*3X~8-_G2sq%yFrwR z6!6jg-k`Qx2geKU_pflLD|rcB9z`_C0ZFa-Sp+RgfWT>%@OPD>l(3yMn5gJ{h5=;u zf!s*!u2GRO+yAMQUqkcn`Nilx7EFLX(69eNvmKuJb-9KNyEmC`Z)YCa+_d!Jc6bCrtU zsT-S13oAN{2Y+1W7O@O!6j{n&w!h;^dkb*_6HiSgQMLgaLy z$~jakVzZZAx51jcWs=MA{gRv>_W4|ZyArXY^{L>Gbda5Gdy;-SH^nkCBomO7;bE`O zxs5;04Pr8{CHMw4HN?| zBKnr>JoXu4%#E`22nz^A>g0S*^ZEewLoCy6y;bEWnapxg4H5nRaV;0x-VVvv8b#zC zs~nGRSWIfDlCQ29dZ;9tgd-agW}o`y$ivXEE&~%WaC|K(sq=Y1gZri1YHnCZo=SE@ z+4yyc0)qIoN@;5(pT3C=U44e>im03E{*Zh(P?Si`+YqT@aK$4dI zNy4%D?vkA-8#D|+a7cbMIx^HZRN@|lw22SUKFQgcawwFiPs!Tx;ETN<)m4WiCPWu* zSw4F2zBTdf(LUpgr=~#irP?t)?YNO)%607+lUbS9eB>OkBL>g{sr`z5!{EwBwQXg% zZKbVlLNLpUu(k%3dOfIxH429y!dvus_ZY`RASBIS{*$X^F#Fe5`-f6mTuOa!3(QvD zC^I3P!`8|NM*lJANdt(75|=Q0>jA1WChV~BWe+yupmm-3Kwhq6TSYZML;1?AN zXZuX^tbXPoiR@iN6;MpfmyM8r#Y8z+HwB`kP(jD4XR`gFV1&-NRAS|9ECYD{>Mzxp zdTts1Imdgr2y?RE@AzF?OtF=7f%n&i12$Nf<;>vV=8-P^P$qM&^VMSME;FPsc{z?P z6oMFcML9LIBxIW*H{Do(&5npX_cu-61gtnpp}w9)0q&RcktZk&%S#@5ovXW+{iQnZ z#x~CU%FV2g)!WozXK9QAAp@&>JF!hrOi#wrlAhCcV`?RfcA`wo zr6!gDVH>EAUKtin|9W&evdHag*GP8+6teM`0~|ME3v6XXCFrgP*T>&tcoCZ!b=7e2 zd`Jdd)a$?t(^fW1U-Z4IXVu%V(3TvDF7Q4T{#%0mQ3xxnOQmmVGr5?Dyx0_xr>$YJ z+I#uLne>hG+jS0cWF#&1q>m8A|b1^AvtK z#j;Vw^)QR+EuEEF;K%Q=d?h76pv2w_j%U!ogoPKOTsY}F-RJ5qr9x zJAkEn#{RP#a2}pnbA=NV@5w1BNMc8ip53tG>}swujOwCP_4glpmlso%lRo%Q=2Tr7}*9R`?qOMji|R1Tv`LGOjqA+&~+u736$lTor4UQY{ z$(r|WVZvF%vq(@eix{38D1dC6O_D!7Gi94jVZ;?(y>>P<&X!g3BP(XF{)__0mS43g z`W0kXEfqbb^la^SaxoO+1f|vfQd`aK5sAsl8pT>Oaqka3}{JgVLuL}Yi=shDMLf?X3!dye&q=qAj(Vn4is1NN6StS#Z((Rs{+Gb zpabG7c{wUP1GKAC-~lUE;?KR+AdRZ2aYDdHNttnX*7If4Z3xNp$aq`0lvm&>yK_=T z$op7BoLH3uaQ;Qou0T4sa{cfWA{^;I)9wsvklPE73Dy<}<0flJ`t}w3@+W#L5WjW} zp;;+fx1L8`R1Q>2pg1EnTRkBp&INT%f0Y<2l-5&R>C>t?yIcq_nAUx`NIq{5A zF|I2pB;UL1U2`D@Ul7d)zIPQT3%59;yT7$VET9UFB~($w?eS&x!ldBtPL!Q95p|9- zr8*i|c!fm@Hvd+|VDy__@9+62i=o$3VgU2_jUL6Wsx`CfI*@Fh`Ed6m>)&`+3+>KN zJ$I4h0TRPeABQrSR$ZYb&gM@bhn*V1Me~oXv0i$cs#=sA#eAzRaFpx0=hZE5f{^ZS zvzW#@?9)2z!3PpMW+5l4Xa7NCV+ zWM6%aHNLXC0vNZ=L(M0-eYzT38dXMgsm9>2htmuV%hA7A!J52EUKgQ24Xt}zXbAIKNP{DqmM{6C-?apB@`2M3OP~NM5Z!vxRZ^aq7R$vd9$q> z;FYRt9hM3)s7+v#syawK57DUkNproIk$wienZU7&lVNZ2ZcM7yz|ffG_>3^~nCL{I zPh77MW+pK&l6@)uW*;C=+#!#y*z8^y^cFVtmXbJ|OKA+;jBh-%N56W;ftd!8YVYEf zH-GGhw$u5W*Urw4L>f6#>@LD_wJ$uMOP}Pq%^@y%MOb8zqciQHmB)iKE#RsT%O$bd zHgjGDJ*;ZTEf>ep%N6lvXpmWm5DwCRYx>s5Yg46OZU5M2y*9HmkrH_~|G3n_BY3hr z`nbfBpJ_KXb|jKZj_6T*>zLF+#L%JYxG=LvQvTe0mWvR<%HO4;Ee&JUck!?TIjPh2 z?|08tjzn_jF$8LWf`0PSJa6?>l0kVlts!($L;81qk4VLO69R_G=S=#y)}%{5JGEgr zyjLJXBw?Fo2SH=<^LWP}m%e1B#|vqwOSYR&Z;F=8?LVZ+jqSS;;Yv9TR_067w;8rA zhNR`gG+Us~ZuO=b@OSy7ge-i1m5<|TJk77Cv$S~i(#(_Nx=AAp;tLUN1_vH8WXT^G zB^8%D|7Iz$$af>WE$7L#5;Aoi0wEQ>=aen)D{q2O4P3A_$3t&P6eJoFSo}WHa9{}Y z;%Jb z;@XF*t#U0JzvZJqu^InrIa$EC{Ib9@qyJLC+1KTqKoMO%>rc8rVWEo8&aHk~ zq;S9r?6(Gz6=iuoVV|Bt;6gp?P7==Z$LOD0y{{B^g9`(`o#BmXo48YtF~4!CR8b5C zG(3%5yle;GN7IcToC`ZhXMqG{C^0&EKV7_9STkC5py%!k4L~4>y5^K08mC{gS#0ed zKHsK3W1%2K>-n8!w*0=4geGf?0;NHOSD>JBw{LR{|MyHGym;?iQ#2~s*CvO F{{#AV?dAXg literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/a_fill.png b/runtime/assets/touch/a_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..8b9ba48590be841a8a02c20f87c50a69ff2b2e07 GIT binary patch literal 2917 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/b.png b/runtime/assets/touch/b.png new file mode 100644 index 0000000000000000000000000000000000000000..56078711e13fdfcb0683c42abf975750999cd7d7 GIT binary patch literal 5515 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/b_fill.png b/runtime/assets/touch/b_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..7992bd86c4a6a972f4fcf8a8152334c75f3a1368 GIT binary patch literal 3104 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/control_stick.png b/runtime/assets/touch/control_stick.png new file mode 100644 index 0000000000000000000000000000000000000000..51e2f9e5bd486d1dfdf17502b09d9dd412824c44 GIT binary patch literal 7176 zcmd5>^;6W*yZ-F5G{{PKh|(aSbS;f^N(-`-gmj3+(jlp|bc!fQm#~ZAg3={OcXzJT ze)--X?!R#FnK|>GIdkHjcb++Op67|x)m9}Xq9+0Xfb^M~l0E={?uHeJqnID^J*(GAj>w^6tg<*q(4T;^g{Z1n3xhj;dKJerfD{A0;fT(Y%IsSHk(1cqVaKQRZUPza zz(D;k>{1)fumBVQ=<&b+tP2JJjEVEYf|slS@RS4sAfiG6U|8_~V9~l22J^wD zFm*ds-z(sg#7$FC$oyN`T|I7}f)F|GQs2`9smlY`oyP$@-&-sMz9PfvO_M!_HuEP} z1WvqioTSqf=&bhkpKrhHu2m!i``6dTn~FdZIpIK9V|I)gNyK1nM2~(L_Gz3YnSi zk`jLpb%8$VC5EuFv^HVKf8(egL#kO>pun4|k*jE;qKnca^#f9acVRGKb@+Agp5ArM zUxqFm6bS^8k1H}IM2dB39=0cIJ)wnv%!-K1Q%y*9v90Blk5okwKoChn7B|I@7<2-5 zXNO=p4)^SB9Pg%Ca}xuu89UqCQ(hYDr{`vUZSGyYkxo%Xn1(}GfmdHD#NK-*40LB@ zk2*E5@h-Wm5m!k^bT+oi7=Q zfv{n$7D3nH%m?N4STXgq$fJ{*LcdG?qxk$y20vz`E7%_h8B3YT68`B|GLKu;!ADFU zk-GFZZ*VNo9meoRfv{+_BnV-cJF*fg`vri#r$AF~%MdnsdLGMOEh;xMU3FxrWBy;;1483 z+IoNuk^jD`I*|2Y=_+P|S7dgyru_M1>=&q$SW4nSz-R%ks?Y8w>N zawIi3BS$IT?)#2zt|`g*Fd_Kk6+`!GlqL_epa_V-=uGGs8AN23Q&IjV2i06x{319= zkpOLC#`J++Hob}KN9rH2w9=rLA3I3G~B)iKKQsu$BU%cE&bupM7u7w6ffscJM!K55tonYL5=YZ=Z07FdvDJeRIzGV*|W44u&-dK z@LmSH?#i)>Ctb*)^-%4o5E!E6d6#!iRrSEIVk1>23H zt2gQMH?uigqqBw^d{6JWj(HtKMjMvHs*gI*+wvj-09!kfYmA9DnkG?73W<#^R{Xpmh8b)#A=VAHyqRFq5+uuf&e+QcDP7@W(etJyAg7QmvD?J#xS6VL(KE%ey>?vJnr`?WnMhzIJ-vb2q0V+`+kR+e zAt@cUO3-iNB9j!N@i_j$BDsA9%tFnX`iZWk-Rv(`A ztt%mnB|me0pM_B|G{?15`3P6hsTW5ax1{;@Ve<@l-Ug zspLITX;Y%j z1M(NB*bXF_U+DpdVK{(!kV4d2%U3S*r1^FSv@+DLBQFp|eI97zo-vv3g`|o@N7pwL z;TD{-A*!z)uQ@UCfsgT{ZXdfBTw!3{} zF#G#;!G&WdCT)pu%3u3wdQbcdgxkk9-ZzofzrccxS8iU<44SqBd}F3JRN}4biXHa7 zGBEr)_d?TS&^k7>y2ntIA5&w*J3Fs2176}5-~1E-xMA~yjmRNX}-?maSm)frU6~58_K^M$ax_ZxUWzI%*BiUj~*sFxJ+?HsN zap>m_BfQZdz1Wb9wWI*tWbVLF-OgdV!p~?Btj`cS$OSv2A(c?zz08~)lejJNQuoKj zkM=v|F8qe>tZ|Qi%f~!=9>+(n{~5p}W}Jh6$%$r2+ogA|U3zg=czb`vLJY}>mMC(s z1EHdpquU0=sg+dZ<%Xh5A5d^u0)2e_&v%szb7yP18maZl}^i`;zqf@0J- zed<()`LgBTFznG#WBKgcRt^X2fwaMo%eYe=OijEmE(8lusEEpS^TdM0ufH{24$)LG z%Ouxx0md`kl99Yg`X)I((3XWPezhLc_Gh@yE0?S2vI0{#L72p}MDPkWDP8W}8Q`M$ zfkx%R%Q12;CE!@u=GsPa1G8)5S-HvFEFBu&MI6JlzIF&`;~bRfcz7VPabZRK!7P)$ z6S7nBVZ4W8w$}ky9)s600||R=63VFaME$`zHb5yByx0cBm;){>oXWtZ`{`}fkCM1q z?+PAqj-*8C;GZVRW9N)HV3cc2hpSM}e1d!`2f+5bKUWFf=KH00ik>%Gqr+F}3TZ3A zTI=BtzR(=2ZSJ*6N`S3BEO}=eLeeiU5WBi*TR+A?^DkYDz z0&ZoK5;IZNBy{2U8ev+$Yo~9UzQeWciC(=^{z}ND1q8|+4l{~O`oM9cq%vUek7OIO zA6W5>U4yG^DuQg@>dZ8)GUH(ye-$kQ4K5_3oT`A}qYI>t(_yt&$wgHpIJhGR?-*n7 zR&sDLch5YL3ZE;^Oa{T&9jrTOU4;JAha`F(%k%>+WTAehDIV_E=q8cQdymW-SV(%$ zjw_D7OWLkGkmeo9%O_>;KjY2pN5UOPL;T#>NhihvrQb5QG_=@*b(bl2E{W$N41x>7wvm7T|Nr2ny%M0p{I)xQ> zcqL}6z<0HHQ!nL7gI^-%q0In$0})KC>g9I(A9BKs&9CN{gT=bpKzu_Vk#Y8?h??m^ z(`DbW#<%Z#!&c2ovx>_iNE3g)bh*P0yv5JrRZ-)Wj?biPWxvyX#JJJNMn9SvmCFV1 zFD43linQIpY&UIH1pXpVl8F7I0$JWdVMhyQSgg>82cITzdTY|9%0{L{V;4=X*&62a zh${kcZ~SQ5=;HUx*FSN;#9>AOkF<{5(I9=?K6}jr;`^a#a3^6SK)mwlOottJ*MMGy z4*`k^vU&_yY^YNqC|sxhs@hcULnT$UP@H%;D__1kUZqQ%r*Y-|7bwmxaJf&0!-MvY zZmLxz^&WhfrB6itbcX#Yx~nrBEz!)+#-)i-70sM|^|~(TIqgr|gZih)~kC`MN(UuIjJD zQ2jH)jX`yCqwK4QdS!n=jMh0E_YS>ZYdlX!%R!3Ryk8V1QlgwPW9=TNUm7=JsDL{t zzpD|ge^#ahqIE7e8eE*KTZ1F*vO8u;T?YR#Seic&oEm9yl|eozUK9Kh`KZd>o*LMH z;s1r{>xWR_y6cZkeWOs-O+sb)6#Lf~V4t+!r*wfqHoW)lb3_UFAa#++X#lSD9;%MU4GNtfd zi&zw{h1&PZ4|c(~Om%E8ly_)`7CK&j7a%UR2VXw#SUU%rY35y?_=Dm|JeGPX+>bi^ zFvn?CKlcV(1F$A58D<6JJx!$4rUNa8AcDVkky7`ponyFH9L=B4wxd#W=RaTNvb&0> z-94n?Fy@=-rS=kA%IOZerbA!Q3*(g1{yQ{5Cx-6h`{y@{qSAUE*Jz%>60n@5;~Cvx z%D0j|I^No`Lc1>3GV<#C$brfwW^8nKbeuXcno5t;%(G*ahYTX`4Qop*mlJy7pdy19 zcr=irBFSEuGbU80RF!1AC?(mL74}?8b^1Vh>2h}A@v5XiFi%b1%L;G-BWah4dh%c- zKdTQ%SL{uUvnn~MA8Q?l1*rZp;eLSXj}aR4eN?3L z1Cq0m`vjMz-i@ZcMNr4QRk_-nqkcKvq57_95B=mYRc=)8ZHxOU^& zt!iO5VTY@L9bKJxsWF6z4o}S=)VO&nDHMP#6?&cDhNlE$Ko0oazTYexVqEUm%PAT1 zB-%%2Cj(q;Aon+*iiGVEArW!A`?SKhoOoh4Lhu7Q!V4=ZS>l4DgnFLBiu9eH+qvTn zM!mJdR}c54uY^n`(AROV0tVAmuImt>C`~<$e%%^o*i&k3nZn5~27Ral7 z5kT+HEU;oFJ~;HK{|0{dPcD^Y|5IFsx7PQ3lUXtv%CvdlDZP=X=l(G9XTl5Y4=!7Y z4pr_-?x)Z~6wZo>4axuEf6eCt|LF3U5X zZ)$2Q>P`~GhJ7PDYe5;g`>!rv^)&Y~%fM&cR$ss&9|BD|k^IzQj} z?lc63J5H!e_vfV*!)0~4T*hvSO(cw-yUSQC9$Tr~fh;RIM{6g{gh$)<%n?^NgwV2; zhiE)FOpJ?-Zx8&{W=ZAQn8!VLyS#haV7 z4~m<%0tS<}C2g-q;jYGZY%~*Rx1Vi51WXyvGUm2F6;3O0XAn59s|>$p{^p$Uu2_%V z{o_9g3pBEP;{)Ycasc*oV-2$GIY>7&#WZaRXO2AdOs(?DTX%?h^GOXZfgd|nt%XGT z#b-@4%XFt9)jzQcQ$Rs(G=;w1wu_D&l*VQ)f@=|Kl=cfFMcx4&s`x!`Wke4dZ{FT( z^Z(e1(H@6cwxOP6M zHobJiBhe`;Lni1}3*YH{so64sol7*8LKU;W$T#B!b0^27gts_M?cs8G@mOb$m4gTN zA`cs%^{MYMBRFi$boCz5$I^$1p8C!RJT5hITYO#}Qg-pa^7G)CBnwWk)mpt({d$L@U)ic(Pgr6Rzwbm?5i0ZfveI4oj6z+bl{?UTCMWpCA+@7py< z4EJRQjebBTKy};F>Xw!l47P42Ah%XNm`nc z<@BZyN=vDy6nl0Nk&?vl5xtmU=BeQV1UA9~|%6QSUC*GDg#5ABG19ncYWBNmv=lKmN9fyXKh+nK}OqNmKuFl$63C zO!Soip-vf^O&?KpUR()md2b|pw9`GFqMrywe}+=3^Dancr9)uKWx-D8tAv`u^wMnW z<0n0|SlWuE;?Zp2lhS?hv1Vwb^lznV*7%!7EjncOuoDXEds+zJ-dY$D{$rsL8!G2~ zDJpmin4JPox$_W3Tt}bNYHT{aSC4!8k3Xd+ScwP#aqCa0@}FjEb|KNz2Sj2~+(kW` z3v}_=C-GR-MNo5=L4<^lpQW<%DGQY4&|dz4x9l92&pt5D8a@V=2E#Tm0z=AZ*n=W0ea-Zxv(K;;FOSnCBrS}^8AZEyF_ z5w5feGsV65Q3u8@K2mB|TmVGi;E27X%p3?bpf7FhZwDRFV z;5+S0I!vg~@51@=RiM+QPkRtK+=)p2TRAgJ)DQs#mY6yhgpy0GtsH(B3iLif4_gzV zCWe4Jhr{cbz!O63ydE16sJUeafRkA&NSd=>Nw9kwvH~kb?ciT6$ND1@_cr%bAzBTQF%DQ));@jid)VKI$$4}k?HCOSq(GZG z-R1jiS5)02;`;`^X&_K0wWRY<|NIzLTpMDkdw#P;C;f{8?WGdpzn&BqA>RKx)9QbX dG^)~ZEKV2id_9dUXBlwUo+)c9RVi49{Rd7bR>S}R literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/control_stick_fill.png b/runtime/assets/touch/control_stick_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..4f777861a409b9030b198f56c5ef3e8a1a13ff70 GIT binary patch literal 5985 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/d-pad.png b/runtime/assets/touch/d-pad.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc27364c54cac53d2c1bb6de9b79a0aec43d660 GIT binary patch literal 3533 zcmb7{cTiK?x5rNssx$%V67XUWMM@As0-;I?(iKpGfPey0EeM7dycbbAkt!WQREYGT zK_GyDf^=!p0!ko&^d^Di7vKD5-rP6y-u=Bl_UygR-e=aH^F3>=&w6TQVa#(x{0IO5 zJSdZkHUI!(EoU(cL<=HUp`MZGJ***_6A9Xetq{a5cXY|0-m z4fJPsSPZd$)SWg{edp{7w3FqC2c=cP?`*XZqZ^ z-es+5s80AD0FKK5KnDjvf`fs8xc}2!R+b7c-G*%=oE}#e;f0U0@8|(F zm-@*{CgLUzU&?%BPzIihunhbG3~(W7$)U_umGkyV___1iwAEL%N6Fbho1sByGUt3hqDT{Zmt4b5H ziRQQn6xkAJ`^w+_7~zW7GP_(jl8%Cs?3#X*mLt$$P2Q^vX96#rodOJ;LBIDnPB&`w z99>NdT`u_8F)PeGIM%tn&sq{L-hT9-boiS}q&ij_q=W-#>Ot*SA8O1usC<~6CwbNW zntvN~GN*>oBu~8p=^*t+N*M8xO44D-bJiOk%d2E=5P7f{CYdSsi=U=>!2sC1Jn`y# zahU0$KoDg+8dxhCO`|Pn7i%xNqwkuz2q@##EbdOGZ*74XJGukw;)zix(&K)}+T6F4 zb|qS!_muo+K@8wk`;&XOpvc2Riz0*S9lEcz1&A}{C1s0MnC+J?u(uYAp0b_O%EboO4te13avEELiW#4H*l8QR0~X z%=2bsBQ|x)Q$kq#e*40P=EU~x-7Ifz$v5K%ei4sVy2G8h6!d`~@)VesBKxhyirpih zM)_b3FzR58VQ8llmzb-7vV!dUmoNU`^861irE|jHvU&r7nH-=bqxC4sw9q;0ks~73 zV^w^dk*bSRf&qF1RYj3u`KNA&Gt|T{?_?&RFm=t}&3ugq(pV=IP~(9OcdjF`H{~ag zs1aC${&$AiSgwA8R(jX~r?=n;KAs!jeKH*Zu-RqQ!4!t9^LdGeW0f7a+RTF50(ToW zk`PKl_29J{erbLvZ3whe=kB{;`eq+hsSwTunK2rDgp7338&36Ue-kA*ucUsi`*UvuINDZ7N2ej{_c#QMvn zLU;&Js!F#R_B@~wbuIH#486UrKQ4ll6S8~c z_E-1HEN^f1;pT$nS@EU*`U9LK`l>xD4v-`Ise|)2`-pH=?%+C!`{_PyZcl`=G&9k0 z00)vbUDJYb0&^`7RX=n6bkTFpNp}cE>f1h$A9l-Mhd8Se2$cd>N~hj9@xKxWPE?P1 z+t$ltAxn|=24}8Yzd^9bg-q&n{>4f*87l}Z4g}eT05?5uJ;UE*Q?VsvK8)s~`4{B})Y07pV(y$0MmV$N2CTi-zsNm6mNTPoWUh=&#HHAAf z=BV@r`xZqonSnmT^OL@bCfqo^u@!=1W~4LJ#zTtsBA!5O_Xf7g3jD{@DD~QO;u>%JAJj}fV;GB4C0kf9Utl4Th+ey8S-pGE z*T*Yo^x&3jtH&vvL@2PN))-OD+5vV>7}sjfQB;y%!T=R_`OGDP9{w~Lf#fgj>-aR^ zA(a^8=;L!)QxA}U9bRPZ`6Tw(tJYIEx-2zor=(iF>GKVN04;u&wuT{fbb#5XK?thp z*;e-XgI%Ii=fp6mY}_`o8s|qK2PZ|lN|G#nXZj z=T-T(I3m7?99BOBi-*&l>W+$z&#yp1fIngHqfW=me5@M_Ko$i77mNTPfgkwaOgm%X zT_XcwrME%23C3OYLF=GLKItEteJh`uM@+rJBiNPC{(2`!2-3lh*+EwPt`m4xN^kS!o8$&TpiKjF z<8q+$a)j6Q{A+3+&yR!U@=L2qN1trs>bgZD#V`*jI#O2FsnNr5Mabio;-GL>smAvm zin_AlJM3d{Xo7fBXd<_f;8dsKnt_J^UELPEM`c?~jWctSsPgE+Q@<0rqSL>zj zUavuc`hlW&2&A+GwkiZ~9TS}{zA6ujg
    a(M4hmGtBGr)E2Rg6F{?z73Jg%pc2V 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/l_analog_fill.png b/runtime/assets/touch/l_analog_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7cad97eb0b83e7edc506e9814e30fd2fea7e14 GIT binary patch literal 2222 zcmdUx`#03<7ssE^+#0#lk#R6ZODQ2lp_!3NSIRXb%*o2+J~9}D57SBI9O}rWNY0`g zg=5@uo3W_*p2l_6prdignh1w+8RIgS?|j$yxAPyIANG3h^*qmd*52f_=6P^G-$`k@vj8d-Lcp0h`720Y4H~`Rh{})iy^(UJEU@2!#xOg*4{uT)0 zbsEQx6EwwbMhkqzH*p`yy#SvTtO*OJ**O}x4`OJZv7u(Y%||YJNdf}^fE$uaB^>zq}@OGLi3H}i@D&_9|Si0N`cUMvUi~~YNIVk zQ{nEo7i0a2GZX4eJnVyFPK``0}>LiM0+0+ZH3s^n%csG$iAcMazgt))jxt4(z^YyoUYd& z-pUWCrV3KXCaDE-7w22x=J>be(oqIC*4vWnXtb^o0mc)^eG(%vR~&FOK5k{;SP$rk zP7PEfGSeCTAJqDMGE<76FN+Q43OXS_9_rVPJcL}Y%3qO%8Rg>yE32AUi-9!Ec3rdK z1tFDjEmq{=4iTKpvr|op-Epm57z1wT(R62jed3Q=a6eO#ocXS!_)QIBtMsFdk-Nm~ z7~=a2z*0*~>q**d4EYnXUiWV_sXZ&T^F&fatiPSb%ap4?Ep1_C&k}vtL4YBS>ol3& z-vO9Uv+mp@odr4SX$e}o8MnY|=RvOK5szj!${L7_UTk1$62J22wTq4cKMaxhJ^Y*7 zU8)8re@pP0S^h|ZD>)42!bWS1+BCk=yoVk-fu$t&x>W*Wz9q#LJ@Fx$;JutsaI(`MK?@zsy=R}D*;Tw7aK9Hj4s_??hLE9H zPQ+_SJUyh%04hNRo#=D+w zs3q;Uz6#s}m&;w_ja*q8DIy=njoWknYXx77zBCuaI1TT0NY z`tbCEgz70U8c9}z%ZaSnXuW{|U?_VO=(nwT4555B-|`r}x&!udOFvGJE1NS1Ir54l zb3$B35Q6r#h3L|fbgHZn&}AHyA!%psc8M13@bAk=@rG+F%)O@ zsdqm)SSQj56v|Hz)Aa@%!E;4LwmSTdxbBRMZv2_S18N&ll{C;hq=e#M5%zjyG{wi4 zl+mAvvA;{I0!KExf|aOridq^9Q2vLPSO3X}^`PsXj`}3=qu6XvB=3?{;thL28J;ic zerW`UNaS4(KF3Hy8p`f&7XKfuKEqHrh9Z>WjVu0=fy2mw`3G+LJr5ykSytAo#tWBw z0bRoX&%mqjZRa3Ze*P#@)Jqgu^YwTsTM3j2)_xXDhH3$}^_31BEWKnBXvJHe;lxNS zWv<39Z*VZ0G;$$71ww(%`pTtB3&in%;@tNDz)SZspR`pi#qwKpF9MCliZ7E~L&6Ya zX9MD*u=keJa<|aC($Ees5csrS8*QHy(X!Hfr!)`mgvR}(f!1}!hdUGRT>V^UXQ$li#hh302|ua{lt{| XvOl9U^>eIp0Rd-Dx}T^%PRaZ)27IeW literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/r_analog.png b/runtime/assets/touch/r_analog.png new file mode 100644 index 0000000000000000000000000000000000000000..487790f77d3fdf78a36c7f5bd6d5baabb744917f GIT binary patch literal 4882 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 literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/r_analog_fill.png b/runtime/assets/touch/r_analog_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..585344d61705d99b0c3388a9fcb063d95e8bef13 GIT binary patch literal 2781 zcmds(*H@G27Kh(2ArL|dEvS(yq8`c+h8Y2cA)=0=fDs4?QX(**bO8lK$j5*IF$@-@ zh>}D=dap`J6el1vbPOG&DAJoWLmAFm=jQwg=UnXd+iUN=)_yLZ`y}&oVi-II03c>^ z{)`0x&|V7xwBTOY`)9lDg|Odw+W-Ji_y1jx_w^Uw0l>tYoH=C`{CsX`h&3~h8= zW8`8E$V8~)t`ve>ppfH_qKS7$uzvr9LEr?`inBS{h*I;~3JvKf_Y8fV&@Z8kit5F< z4(!Sc!`&hUMa7fgS@?LUP@p7$2?!wZD**4%fVh7TMFoIs0ziolK-B;GGPEUWq&%hW z_5E$HgFz0u71W|x_kq(rx2o0_uJU#9E7MC$j~2>(q&=Hb8THrL@?YhLY$fb{?{O=d zg@z6xn7@=2FzVmN%hg66%#$jdi}qki3Bc5C>2RO8o$ALucdKxMHsX%h+e+Z>G-Ec4 zw-~QR)$$aHP=Ci!L~!C;o`iQ0(?{+R{Al2qug@jET{GWmzNWCIn31I;(G&%axp>9Z zf6fY{Ulc*O$^57Smho59o%9aWt-oTJUFkGziUv*T3nPqM4nZ#$-K49l+GbvU4;qt$ z2pVY{(XXotr5qw7BtV0H`smRB7sk<35BEb4hX=oxh|XsQ-C#9eXgJd=mfZS2jYRF( z5DIX%q&@g`#ObybxIvnq400bwRt-B|F+;-*rrsIf&02h4`cKModXWzB_9``Sw|(OQ z-<8bi-kb@%k5L+aFPq!GxXr@HoIy#rcTHw2@xOf#bY`T5N6DjMg3HVd)s5L!S0&pS zu!=n-04;i^P8lXH@myTHN&#NkG@`z~%fM-`quevlQAYpENtMR91E9Y}eQ08o_hoPASPB8R-GFd-O3*fV5}*&FAl}N?Is0 zWq{`TX0$jg_LMK?YD`oGM=H;je}vffP_cGZ$4a6QN!6IB434XB+5PffB?tr_Dm%7t zm2t!?3{PS$CHZ?sM$OPh#wU4sFb_#F8CUEsAg0uMh1@Cr`~z|K&oIBm6aL3KP`$kG zM{+Cfahl(Hn&f^2dKJrNXRm|Vei3#lW5BTJOIn}xK;lpz-)q{HC5t59yzhG*MBVN4 z%u7prM4iQCsZwvu6?gEsfSbQy)O)?{uj7g4BbP2T7Q9exw?i@SFwZq(C|6DU28t>+ z@SD3m(+=wA9#mLC8u~3qU+YThF0Hws*Ku`Kr|m2JH_xj>gnjs>Hchu@mp;E5?nsNw zoG+IE(yOQC1#in}%Q6GW>#4F_q#YXO^<;z+MDB`t1g%nIH@xg~4MFSIb$7}B5pr$V zuimhK6Qw|BPrYjMLGP( zUR<^&;F@l}Et!6Us6cUk?_{IrdDyD_9CeZ8|K_pzLq{R)tJy}yvJ zqd0-1L5d!m8Xm&1rR@Uk&pslt+5$rG3~|_iO73r>2HGymP6`gF4;tI72Afc zz>OFA&8^2K30?<$H4DKQRmN2s`htMBo}4C-~0uwpMbSwUg!RagRTAb(PKgd!3U7Z zof~C(vBAqf-4ZF(@mWI$3xl46NAS}9#WvQt&ij9O0~8Px)0`BtDURcoKPnfZN9;Zg ziXUjO+trwgNw05A#htX=@*QbVQnI8*54h{J*KpG7lV2RL?)cZ+P8^5MbrL1R*t2$PYV9H`VprYRDBj4m59_VK~o8SmB`PimGh|@JXR@`_W(01k`<~LloRe*%bD-0!`h(d$+W9TKC1n>?P-7*7o0nwtWF*V0X^`Lz-0% z_`2`JU5#%wkgsf}Z1YnCEak(!#~dp5opYH>CRSjo?U|9*79O@3PEwH3`Wd|kYvDIB z`X@lvR7E>2eO_|>L!hQ~vn}AD1Y}oU$zIAys?&BNz_y49*|80c1g)=)Si%ztnLVEPv Qp05Ivv*u?CjVZtX160kFwEzGB literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/right_bumper.png b/runtime/assets/touch/right_bumper.png new file mode 100644 index 0000000000000000000000000000000000000000..5be5a70a87d72b8febb381a4feaa5b3ee3040ac0 GIT binary patch literal 4325 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 Ib^*8l0^(>eJpcdz literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/start_pause.png b/runtime/assets/touch/start_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..d09c8d05e63d5304832de2cd4084bf8021d8da34 GIT binary patch literal 6902 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�rK^pwtL%smb*qG$dy%)40iH*2jS zJ!qT)uDLpUJ6wm~MX|cQe;E;uOGl^C9TVRwk1RP9u6)S}+GO>jg1R5LdR)ODpQ$b# z=##slat^PE3EC{m{H4+*$%; zoJKA;>57zU@i5*07_PK&g!Cwft<18=n`W*kHM?b3Iev vll{+qMX=oe=PQi=;~SIzN92BYFkJ;2$*vrPgWrn-xJOk{Q=#IOWyt>kxElm= literal 0 HcmV?d00001 diff --git a/runtime/assets/touch/start_pause_fill.png b/runtime/assets/touch/start_pause_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..4129e7b3590084466f6525abfc49948b4205d9db GIT binary patch literal 6182 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 literal 0 HcmV?d00001 diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index ad2a535..09d3c15 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -77,10 +77,17 @@ 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 - SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) +if(NOT MKW_PLATFORM_IOS) + # iOS is the one target where SDL must own main(): its entry point is what + # drives UIApplicationMain. Defining this makes a no-op, + # so the app links a bare main() that UIKit never calls. + target_compile_definitions(mkw_runtime_common PRIVATE SDL_MAIN_HANDLED) +endif() target_link_libraries(mkw_runtime_common PRIVATE aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) +# The touch overlay decodes its button artwork at startup. +target_link_libraries(mkw_runtime_common PRIVATE PNG::PNG) target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp) if(MKW_PLATFORM_WINDOWS) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) @@ -192,7 +199,10 @@ function(mkw_configure_product target) "${MKW_RUNTIME_SOURCE_DIR}/.." "${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include") target_compile_definitions(${target} PRIVATE - SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC) + _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC) + if(NOT MKW_PLATFORM_IOS) + target_compile_definitions(${target} PRIVATE SDL_MAIN_HANDLED) + endif() 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 @@ -221,6 +231,26 @@ function(mkw_configure_product target) $ $) endif() + if(MKW_PLATFORM_IOS AND NOT CMAKE_HOST_APPLE) + set(shim "${MKW_RUNTIME_SOURCE_DIR}/src/platform/ios/compiler_rt_shim.c") + target_sources(${target} PRIVATE "${shim}") + set_source_files_properties("${shim}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON SKIP_UNITY_BUILD_INCLUSION ON) + endif() + + if(MKW_PLATFORM_IOS) + # CMake bundles iOS targets with its own default Info.plist, whose + # CFBundleIdentifier is empty and which carries none of the iOS keys, so + # iOS refuses to install the result. Supply a real one. + set_target_properties(${target} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist.in" + MACOSX_BUNDLE_EXECUTABLE_NAME "${target}" + MACOSX_BUNDLE_BUNDLE_NAME "${target}" + MACOSX_BUNDLE_GUI_IDENTIFIER "${MKW_IOS_BUNDLE_ID_PREFIX}.wiicompiled" + MACOSX_BUNDLE_BUNDLE_VERSION "1" + MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0") + endif() + if(MKW_PLATFORM_WINDOWS) target_link_libraries(${target} PRIVATE dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) @@ -263,6 +293,19 @@ function(mkw_configure_product target) add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$/wii_bootstrap") + # Touch control artwork. Only the touch build reads these, but they are copied + # everywhere the other assets are so the bundle layout stays uniform. + set(MKW_TOUCH_ASSET_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/touch") + if(NOT EXISTS "${MKW_TOUCH_ASSET_DIR}/a.png") + message(FATAL_ERROR "Missing touch control artwork: ${MKW_TOUCH_ASSET_DIR}") + endif() + # Cleared first: copy_directory merges, so a removed icon would otherwise + # linger in the bundle and get shipped. + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rm -rf "$/touch" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${MKW_TOUCH_ASSET_DIR}" "$/touch") + set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin") if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}") message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}") @@ -286,6 +329,25 @@ function(mkw_configure_product target) add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MKW_INITIAL_PIPELINE_CACHE}" "$/initial_pipeline_cache.db") + + if(MKW_PLATFORM_IOS) + # An .ipa is a zip with the bundle under Payload/. Left unsigned: the + # sideloaders people actually install with (AltStore, SideStore) sign on + # the device with the user's own Apple ID, and a signature applied here + # would only be replaced. WiiCompiled.entitlements beside this file names + # the one entitlement the runtime needs, for whatever does the signing. + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rm -rf "$/../ipa" + COMMAND ${CMAKE_COMMAND} -E make_directory "$/../ipa/Payload" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "$" + "$/../ipa/Payload/${target}.app" + COMMAND ${CMAKE_COMMAND} -E chdir "$/../ipa" + ${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${target}-unsigned.ipa" --format=zip Payload + COMMAND ${CMAKE_COMMAND} -E rm -rf "$/../ipa" + COMMENT "Packaging ${target}-unsigned.ipa") + endif() + endfunction() add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES}) @@ -319,6 +381,11 @@ endif() # 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-v3) +elseif(MKW_PLATFORM_IOS) + # iOS 17 runs on the A12 and later, and the build host is never the device; + # -mcpu=native there would tune for whatever Mac (or Linux box) did the + # compile, and upstream clang rejects it when cross-compiling. + set(MKW_BASELINE_ARCH_FLAG -mcpu=apple-a12) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") set(MKW_BASELINE_ARCH_FLAG -mcpu=native) else() diff --git a/runtime/cmake/ios-linux-toolchain.cmake b/runtime/cmake/ios-linux-toolchain.cmake new file mode 100644 index 0000000..4dcf51c --- /dev/null +++ b/runtime/cmake/ios-linux-toolchain.cmake @@ -0,0 +1,71 @@ +# Cross-compile the iOS arm64 products from a Linux host. +# +# Needs upstream clang and lld (Debian 13: clang-19 lld-19 llvm-19) and an +# iPhoneOS SDK, either copied out of Xcode or sparse-cloned from +# github.com/xybp888/iOS-SDKs. Nothing from Theos or xtool. See README.md, +# "iOS from Linux". +# +# cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \ +# -DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake \ +# -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package + +set(IOS_SDK "/opt/iPhoneOS.sdk" CACHE PATH "iPhoneOS SDK copied from Xcode") +set(MKW_IOS_LLVM_BIN "/usr/lib/llvm-19/bin" CACHE PATH "Directory holding clang, ld64.lld and llvm-ar") +set(MKW_IOS_CLANG_RT "" CACHE FILEPATH + "Optional: Xcode's libclang_rt.ios.a. Left empty, src/platform/ios/compiler_rt_shim.c covers what the runtime needs") +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES IOS_SDK MKW_IOS_LLVM_BIN MKW_IOS_CLANG_RT) + +set(CMAKE_SYSTEM_NAME iOS) +set(CMAKE_SYSTEM_PROCESSOR arm64) +set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "") +set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0 CACHE STRING "") +set(CMAKE_OSX_SYSROOT "${IOS_SDK}" CACHE PATH "") +set(CMAKE_SYSROOT "${IOS_SDK}") + +set(x "${CMAKE_HOST_EXECUTABLE_SUFFIX}") +set(CMAKE_C_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}") +set(CMAKE_CXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}") +set(CMAKE_OBJC_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}") +set(CMAKE_OBJCXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}") +set(CMAKE_ASM_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}") +set(CMAKE_AR "${MKW_IOS_LLVM_BIN}/llvm-ar${x}" CACHE FILEPATH "") +set(CMAKE_RANLIB "${MKW_IOS_LLVM_BIN}/llvm-ranlib${x}" CACHE FILEPATH "") +set(CMAKE_STRIP "${MKW_IOS_LLVM_BIN}/llvm-strip${x}" CACHE FILEPATH "") +# Not every LLVM distribution ships these (llvm-mingw does not); nothing in +# this build needs them, but CMake's Darwin support likes to know where they are. +foreach(tool INSTALL_NAME_TOOL:install-name-tool LIPO:lipo OTOOL:otool) + string(REPLACE ":" ";" tool "${tool}") + list(GET tool 0 var) + list(GET tool 1 exe) + if(EXISTS "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}") + set(CMAKE_${var} "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}" CACHE FILEPATH "") + endif() +endforeach() +set(CMAKE_LINKER "${MKW_IOS_LLVM_BIN}/ld64.lld${x}" CACHE FILEPATH "") + +foreach(lang C CXX OBJC OBJCXX ASM) + set(CMAKE_${lang}_COMPILER_TARGET arm64-apple-ios17.0) +endforeach() + +# Apple's clang searches the SDK's SubFrameworks implicitly and UIKit's own +# headers import from there. +foreach(lang C CXX OBJC OBJCXX) + set(CMAKE_${lang}_FLAGS_INIT "-iframework ${IOS_SDK}/System/Library/SubFrameworks") +endforeach() +# The SDK carries its own libc++ headers and Apple's clang uses those. Upstream +# clang prefers a libc++ shipped beside itself when there is one (llvm-mingw +# does), which pairs the wrong C++ library with Darwin's C headers. +foreach(lang CXX OBJCXX) + string(APPEND CMAKE_${lang}_FLAGS_INIT " -nostdinc++ -isystem ${IOS_SDK}/usr/include/c++/v1") +endforeach() + +set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}") +set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}") +string(STRIP "${CMAKE_EXE_LINKER_FLAGS_INIT}" CMAKE_EXE_LINKER_FLAGS_INIT) + +set(CMAKE_FIND_ROOT_PATH "${IOS_SDK}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) diff --git a/runtime/cmake/ios/Info.plist.in b/runtime/cmake/ios/Info.plist.in new file mode 100644 index 0000000..1920699 --- /dev/null +++ b/runtime/cmake/ios/Info.plist.in @@ -0,0 +1,26 @@ + + + + + CFBundleExecutable${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleName${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleDisplayName${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleVersion${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundlePackageTypeAPPL + LSRequiresIPhoneOS + MinimumOSVersion${CMAKE_OSX_DEPLOYMENT_TARGET} + UIDeviceFamily12 + UILaunchScreen + UIRequiresFullScreen + CADisableMinimumFrameDuration + UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/runtime/cmake/ios/WiiCompiled.entitlements b/runtime/cmake/ios/WiiCompiled.entitlements new file mode 100644 index 0000000..2c7e5ee --- /dev/null +++ b/runtime/cmake/ios/WiiCompiled.entitlements @@ -0,0 +1,11 @@ + + + + + application-identifierCHANGEME.com.example.wiicompiled + com.apple.developer.team-identifierCHANGEME + get-task-allow + com.apple.developer.kernel.increased-memory-limit + com.apple.developer.kernel.extended-virtual-addressing + + diff --git a/runtime/include/guest_flat_memory.h b/runtime/include/guest_flat_memory.h index 6c0bdd3..e7f1f4f 100644 --- a/runtime/include/guest_flat_memory.h +++ b/runtime/include/guest_flat_memory.h @@ -15,6 +15,17 @@ namespace GuestFlat { // of a global. inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; inline constexpr size_t kGuestPageSize = 0x1000; +#ifdef MKW_PLATFORM_IOS +// No fixed base works on every device: probing an iPhone 17 Pro and an iPad +// Pro M5 gave disjoint sets of free 4 GiB windows (448 GiB only, versus 12-48 +// GiB), and the extended-virtual-addressing entitlement changes neither. So the +// base is whatever the kernel hands out at Initialize(). +// +// Note that memory_access.h routes every flat access to the checked path on +// Apple, so this has no reader yet. It is groundwork, not a hot path. +extern uint8_t* g_flatGuestBase; +#define MKW_FLAT_GUEST_BASE (GuestFlat::g_flatGuestBase) +#else #if defined(__x86_64__) // 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap // placement. @@ -40,7 +51,10 @@ inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull; #error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture" #endif +// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load +// of a global. #define MKW_FLAT_GUEST_BASE (reinterpret_cast(GuestFlat::kFixedFlatGuestBase)) +#endif enum class Backing { Owned, diff --git a/runtime/include/settings_overlay.h b/runtime/include/settings_overlay.h index b706989..6499a4f 100644 --- a/runtime/include/settings_overlay.h +++ b/runtime/include/settings_overlay.h @@ -9,6 +9,9 @@ void InitializeRuntimeSettings() noexcept; // Draw the F10 settings bar before each Aurora present. void HandleEvents(const AuroraEvent* events) noexcept; void Draw() noexcept; +void ToggleTopBar() noexcept; +bool TopBarVisible() noexcept; +bool FpsOverlayBounds(float* minX, float* minY, float* maxX, float* maxY) noexcept; bool StartupScreenVisible() noexcept; void NotifyStrapInputAccepted() noexcept; void AdvancePresentedFrame() noexcept; diff --git a/runtime/include/touch_art.h b/runtime/include/touch_art.h new file mode 100644 index 0000000..cef365e --- /dev/null +++ b/runtime/include/touch_art.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +// Icons are CC BY 3.0 by Zacksly - see runtime/assets/touch/ATTRIBUTION.txt. +namespace TouchArt { + +// By file stem, e.g. "a" or "d-pad". Null when the file is missing; callers +// fall back to drawing the shape. +ImTextureID Get(const char* name); + +// Call once, after aurora is up. +void Preload(); + +} // namespace TouchArt diff --git a/runtime/include/touch_pad.h b/runtime/include/touch_pad.h new file mode 100644 index 0000000..7374aed --- /dev/null +++ b/runtime/include/touch_pad.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +struct PADStatus; + +// On-screen touch controls. Read() fills a status array the caller merges over +// aurora's PADRead result. +// +// Polls SDL's finger list directly rather than using ImGui widgets: steering, +// accelerating and firing happen at once, and ImGui's SDL backend collapses +// touch to a single emulated mouse. +namespace TouchPad { + +// True when the host has a touch screen and no physical pad is driving port 0. +bool IsActive(); + +// Port 0 only. Returns false when touch is not driving input, leaving the +// caller's existing statuses untouched. +bool Read(std::array& statuses); + +// Draws the control overlay. Must be called inside a live ImGui frame. +void Draw(); + + +} // namespace TouchPad diff --git a/runtime/src/discord_presence.cpp b/runtime/src/discord_presence.cpp index a7b9caa..15db8ac 100644 --- a/runtime/src/discord_presence.cpp +++ b/runtime/src/discord_presence.cpp @@ -1,5 +1,20 @@ #include "discord_presence.h" + +#ifdef MKW_PLATFORM_IOS + +// iOS has no Discord client for the IPC socket to reach, so the whole thing is +// stubbed out rather than retrying a connection that cannot succeed. +namespace DiscordPresence { +void Initialize(const std::string&, const std::string&) {} +void SetClient(const std::string&) {} +void SetActivity(Activity) {} +void Reset() {} +void Shutdown() {} +} // namespace DiscordPresence + +#else + #include "runtime_log.h" #include @@ -464,3 +479,5 @@ void Shutdown() { } } // namespace DiscordPresence + +#endif diff --git a/runtime/src/guest_flat_memory_ios.cpp b/runtime/src/guest_flat_memory_ios.cpp new file mode 100644 index 0000000..df0784f --- /dev/null +++ b/runtime/src/guest_flat_memory_ios.cpp @@ -0,0 +1,154 @@ +#include "guest_flat_memory.h" + +// iOS variant of the macOS backend. is not in the iOS SDK, so +// the vm_* calls from are used instead, and the backing store is +// an anonymous mapping rather than a file in /tmp, which the sandbox denies. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace GuestFlat { + +uint8_t* g_flatGuestBase = nullptr; + +bool g_requiresCheckedAccess = false; + +namespace { +struct Mapping { uint32_t base; uint64_t size; uint8_t* host; }; +std::mutex g_mutex; +std::vector g_mappings; +std::vector g_layout; +uint8_t* g_base = nullptr; +bool g_active = false; + +uint64_t Offset(const RegionRequest& r) { + if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu; + if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u; + return 0; +} +bool Same(const std::vector& a, const std::vector& b) { + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), + [](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; }); +} +} // namespace + +bool IsActive() { return g_active; } + +void Initialize(const std::vector& regions) { + std::lock_guard lock(g_mutex); + g_requiresCheckedAccess = static_cast(getpagesize()) > kGuestPageSize; + if (g_active) { + if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); + return; + } + + // The free window differs per device, so let the kernel place it. + vm_address_t address = 0; + if (vm_allocate(mach_task_self(), &address, static_cast(kGuestSpaceSize), + VM_FLAGS_ANYWHERE) != KERN_SUCCESS) { + throw std::runtime_error("unable to reserve a 4 GiB iOS guest address space; a device with\n" + "less than 4 GB of RAM may need the extended-virtual-addressing entitlement"); + } + g_base = reinterpret_cast(address); + + struct Store { Backing kind; uint32_t owned; uint64_t size; uint8_t* mem; }; + std::vector stores; + + // Initialize() is called inside a try that reports a dialog rather than + // aborting, so a throw here has to give the reservation back. + struct Rollback { + vm_address_t reservation; + std::vector* stores; + bool armed = true; + ~Rollback() { + if (!armed) return; + if (stores != nullptr) { + for (const auto& s : *stores) { + if (s.mem != nullptr) munmap(s.mem, static_cast(s.size)); + } + } + if (reservation != 0) { + vm_deallocate(mach_task_self(), reservation, + static_cast(kGuestSpaceSize)); + } + } + } rollback{address, &stores}; + for (const auto& r : regions) { + if (!r.size) continue; + const uint32_t owned = r.backing == Backing::Owned ? r.base : 0; + auto it = std::find_if(stores.begin(), stores.end(), + [&](const Store& s) { return s.kind == r.backing && s.owned == owned; }); + const uint64_t need = Offset(r) + r.size; + if (it == stores.end()) stores.push_back({r.backing, owned, need, nullptr}); + else it->size = std::max(it->size, need); + } + + for (auto& s : stores) { + void* mem = mmap(nullptr, static_cast(s.size), PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (mem == MAP_FAILED) throw std::runtime_error("unable to create iOS guest backing store"); + s.mem = static_cast(mem); + } + + for (const auto& r : regions) { + if (!r.size) continue; + const uint32_t owned = r.backing == Backing::Owned ? r.base : 0; + const auto& s = *std::find_if(stores.begin(), stores.end(), + [&](const Store& x) { return x.kind == r.backing && x.owned == owned; }); + uint8_t* host = s.mem + Offset(r); + + // Alias the same physical pages into the reservation. VM_FLAGS_OVERWRITE + // replaces the placeholder in place; copy=false is what makes this a + // shared alias rather than a copy-on-write duplicate. + vm_address_t target = reinterpret_cast(g_base + r.base); + vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE; + const kern_return_t kr = vm_remap( + mach_task_self(), &target, static_cast(r.size), 0, + VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(), + reinterpret_cast(host), FALSE, &cur, &max, VM_INHERIT_SHARE); + if (kr != KERN_SUCCESS) { + throw std::runtime_error(std::string("vm_remap failed for the iOS guest alias: ") + + mach_error_string(kr)); + } + if (target != reinterpret_cast(g_base + r.base)) { + throw std::runtime_error("vm_remap placed the iOS guest alias at the wrong address"); + } + if (mprotect(g_base + r.base, static_cast(r.size), PROT_READ | PROT_WRITE) != 0) { + throw std::runtime_error(std::string("mprotect failed on the iOS guest alias: ") + + std::strerror(errno)); + } + g_mappings.push_back({r.base, r.size, host}); + } + + g_layout = regions; + // Publish the base only now. Until the aliases are mapped the reservation is + // an unbacked placeholder, and the header advertises this pointer as the base + // for every translated access - so a caller that trusted it early would fault. + rollback.armed = false; + g_flatGuestBase = g_base; + g_active = true; +} + +uint8_t* HostPointer(uint32_t a) { + for (const auto& m : g_mappings) + if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); + return nullptr; +} +void ProtectDeferredRange(uint32_t, size_t) {} +void UnprotectDeferredRange(uint32_t, size_t) {} +void RegisterExecutableRange(uint32_t, uint32_t) {} +FaultCounters Counters() { return {}; } +void LogFaultSummary() noexcept {} +bool HandleAccessViolation(void*, bool) noexcept { return false; } +} // namespace GuestFlat diff --git a/runtime/src/hle/input/pad.cpp b/runtime/src/hle/input/pad.cpp index 1939a84..7e0beee 100644 --- a/runtime/src/hle/input/pad.cpp +++ b/runtime/src/hle/input/pad.cpp @@ -1,4 +1,7 @@ #include "hle_stubs.h" +#include "touch_pad.h" + +#include #include "memory.h" #include "hle/controller_status_contract.h" #include "wii_remote_input.h" @@ -54,6 +57,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) // between "connected" and "no controller" every time the overlay toggles. WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX); + // On-screen controls drive port 0 unless a physical pad is connected. + // TouchPad::Read makes that call itself; the err field is no proxy for it, + // since keyboard bindings report PAD_ERR_NONE with no controller attached. + std::array touchStatuses{}; +#ifdef MKW_PLATFORM_IOS + if (!PADIsInputBlocked() && TouchPad::Read(touchStatuses)) { + statuses[0] = touchStatuses[0]; + } +#endif + try { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { WritePadStatus(statusPtr + static_cast(i * PadStatusContract::kGuestStatusSize), diff --git a/runtime/src/hle/input/touch_art.cpp b/runtime/src/hle/input/touch_art.cpp new file mode 100644 index 0000000..468c957 --- /dev/null +++ b/runtime/src/hle/input/touch_art.cpp @@ -0,0 +1,108 @@ +#include "touch_art.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "runtime_config.h" + +// Icons are CC BY 3.0 by Zacksly; see runtime/assets/touch/ATTRIBUTION.txt. +// Textures are owned by aurora once handed over and live for the process. +namespace TouchArt { +namespace { + +std::unordered_map g_cache; + +// libpng's error path longjmps, so everything it can touch must be released +// through this struct rather than by falling off the end of the function. +struct PngReader { + std::FILE* file = nullptr; + png_structp png = nullptr; + png_infop info = nullptr; + std::vector rows; + ~PngReader() { + if (png != nullptr) png_destroy_read_struct(&png, info != nullptr ? &info : nullptr, nullptr); + if (file != nullptr) std::fclose(file); + } +}; + +bool DecodeRgba8(const std::filesystem::path& path, std::vector& out, uint32_t& width, + uint32_t& height) { + PngReader r; + r.file = std::fopen(path.string().c_str(), "rb"); + if (r.file == nullptr) { + return false; + } + r.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (r.png == nullptr) return false; + r.info = png_create_info_struct(r.png); + if (r.info == nullptr) return false; + if (setjmp(png_jmpbuf(r.png))) return false; + + png_init_io(r.png, r.file); + png_read_info(r.png, r.info); + + png_set_expand(r.png); + png_set_strip_16(r.png); + png_set_gray_to_rgb(r.png); + png_set_filler(r.png, 0xFF, PNG_FILLER_AFTER); + png_read_update_info(r.png, r.info); + + width = png_get_image_width(r.png, r.info); + height = png_get_image_height(r.png, r.info); + if (width == 0 || height == 0) return false; + + out.assign(static_cast(width) * height * 4u, 0); + r.rows.resize(height); + for (uint32_t y = 0; y < height; ++y) { + r.rows[y] = out.data() + static_cast(y) * width * 4u; + } + png_read_image(r.png, r.rows.data()); + return true; +} + +} // namespace + +ImTextureID Get(const char* name) { + if (name == nullptr || *name == '\0') { + return ImTextureID{}; + } + if (const auto it = g_cache.find(name); it != g_cache.end()) { + return it->second; + } + + ImTextureID id{}; + if (const auto dir = RuntimeConfigFile::ExecutableDirectory()) { + const std::filesystem::path path = *dir / "touch" / (std::string(name) + ".png"); + std::vector rgba; + uint32_t w = 0; + uint32_t h = 0; + if (DecodeRgba8(path, rgba, w, h)) { + id = aurora_imgui_add_texture(w, h, rgba.data()); + } else { + // Cached null so this reports once rather than every frame. + std::cout << "[touch] missing button art: " << path << std::endl; + } + } + g_cache.emplace(name, id); + return id; +} + +void Preload() { + // Must match the names used in touch_pad.cpp; a miss costs a hitch, not a + // failure. + static constexpr const char* kNames[] = { + "a", "b", "right_bumper", "l_analog", "r_analog", "start_pause", "d-pad", "control_stick", + }; + for (const char* name : kNames) { + Get(name); + Get((std::string(name) + "_fill").c_str()); + } +} + +} // namespace TouchArt diff --git a/runtime/src/hle/input/touch_pad.cpp b/runtime/src/hle/input/touch_pad.cpp new file mode 100644 index 0000000..1464c87 --- /dev/null +++ b/runtime/src/hle/input/touch_pad.cpp @@ -0,0 +1,408 @@ +#include "touch_pad.h" +#include "touch_art.h" + +#include "hle/controller_status_contract.h" +#include "settings_overlay.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace TouchPad { +namespace { + + +struct Circle { + float x, y, r; + bool Contains(float px, float py, float aspect) const { + const float dx = (px - x) * aspect; + const float dy = py - y; + return (dx * dx + dy * dy) <= (r * r); + } +}; + +// Positions are held as a distance from one screen edge in units of screen +// height. A fraction of width would land somewhere different on a 1.45:1 iPad +// and a 2.17:1 iPhone. +struct Anchor { + enum Edge { Left, Right, Centre }; + Edge edge; + float dx; // distance from that edge, in units of screen height + float y; // fraction of screen height + float r; // radius, in units of screen height + + Circle Resolve(float aspect) const { + const float offset = dx / aspect; + switch (edge) { + case Left: return Circle{offset, y, r}; + case Right: return Circle{1.0f - offset, y, r}; + case Centre: return Circle{0.5f + offset, y, r}; + } + return Circle{0.5f, y, r}; + } +}; + +constexpr Anchor kStickA{Anchor::Left, 0.215f, 0.400f, 0.130f}; +constexpr Anchor kShoulderLA{Anchor::Left, 0.130f, 0.090f, 0.070f}; +constexpr Anchor kShoulderRA{Anchor::Right, 0.130f, 0.090f, 0.070f}; +constexpr Anchor kButtonAA{Anchor::Right, 0.210f, 0.640f, 0.105f}; +constexpr Anchor kButtonBA{Anchor::Right, 0.430f, 0.790f, 0.072f}; +constexpr Anchor kButtonItemA{Anchor::Right, 0.165f, 0.310f, 0.070f}; +constexpr Anchor kButtonStartA{Anchor::Centre, 0.100f, 0.070f, 0.042f}; +// There is no F10 on a touch device, so this is the only way into the settings +// bar. It is drawn whenever a touch screen exists - including when a pad has +// hidden the game controls - or connecting a pad would lock the user out. +constexpr Anchor kButtonMenuA{Anchor::Centre, -0.100f, 0.070f, 0.040f}; + +// r is the half-extent of the whole cross, not of one arm. +constexpr Anchor kDpadA{Anchor::Left, 0.215f, 0.760f, 0.125f}; +constexpr float kDpadDeadzone = 0.30f; + +struct Layout { + Circle stick, shoulderL, shoulderR, a, b, item, start, menu; + Circle dpad; // the whole cross; direction is derived, not four zones +}; + +Layout LayoutFor(float aspect) { + Layout l{}; + l.stick = kStickA.Resolve(aspect); + l.shoulderL = kShoulderLA.Resolve(aspect); + l.shoulderR = kShoulderRA.Resolve(aspect); + l.a = kButtonAA.Resolve(aspect); + l.b = kButtonBA.Resolve(aspect); + l.item = kButtonItemA.Resolve(aspect); + l.start = kButtonStartA.Resolve(aspect); + l.menu = kButtonMenuA.Resolve(aspect); + + l.dpad = kDpadA.Resolve(aspect); + return l; +} + +struct Frame { + bool stickHeld = false; + float stickDx = 0.0f; // -1..1 after deadzone and clamping + float stickDy = 0.0f; + bool a = false, b = false, item = false, start = false; + bool l = false, r = false; + bool up = false, down = false, left = false, right = false; +}; + +// A finger inside the stick circle steers; its offset from the circle centre is +// the analog deflection. Every other finger is hit-tested against the buttons, +// so any number of them can be held at once. +// Owning finger, or 0. Single-threaded: guest fibers and the event pump share +// the host main thread. +SDL_FingerID g_stickFinger = 0; + +Frame SampleFingers(float aspect) { + const Layout L = LayoutFor(aspect); + Frame frame; + bool stickFingerSeen = false; + int deviceCount = 0; + SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount); + if (devices == nullptr) { + return frame; + } + + for (int d = 0; d < deviceCount; ++d) { + int fingerCount = 0; + SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount); + if (fingers == nullptr) { + continue; + } + for (int f = 0; f < fingerCount; ++f) { + const float px = fingers[f]->x; + const float py = fingers[f]->y; + + // Holding the grab lets the thumb slide past the gate without + // dropping steering. + const bool ownsStick = (g_stickFinger != 0 && fingers[f]->id == g_stickFinger); + if (ownsStick || (!frame.stickHeld && g_stickFinger == 0 && + L.stick.Contains(px, py, aspect))) { + g_stickFinger = fingers[f]->id; + stickFingerSeen = true; + frame.stickHeld = true; + const float dx = (px - L.stick.x) * aspect / L.stick.r; + const float dy = (py - L.stick.y) / L.stick.r; + const float len = std::sqrt(dx * dx + dy * dy); + // Clamp to the circle so a finger dragged outside still reads as + // full deflection rather than overshooting the guest's range. + const float scale = (len > 1.0f) ? (1.0f / len) : 1.0f; + frame.stickDx = dx * scale; + frame.stickDy = dy * scale; + continue; + } + if (L.a.Contains(px, py, aspect)) frame.a = true; + if (L.b.Contains(px, py, aspect)) frame.b = true; + if (L.item.Contains(px, py, aspect)) frame.item = true; + if (L.start.Contains(px, py, aspect)) frame.start = true; + if (L.shoulderL.Contains(px, py, aspect)) frame.l = true; + if (L.shoulderR.Contains(px, py, aspect)) frame.r = true; + { + const float ddx = (px - L.dpad.x) * aspect / L.dpad.r; + const float ddy = (py - L.dpad.y) / L.dpad.r; + if (std::fabs(ddx) <= 1.0f && std::fabs(ddy) <= 1.0f && + (ddx * ddx + ddy * ddy) > (kDpadDeadzone * kDpadDeadzone)) { + if (std::fabs(ddx) > std::fabs(ddy)) { + if (ddx < 0.0f) frame.left = true; else frame.right = true; + } else { + if (ddy < 0.0f) frame.up = true; else frame.down = true; + } + } + } + } + SDL_free(fingers); + } + SDL_free(devices); + if (!stickFingerSeen) { + g_stickFinger = 0; + } + + return frame; +} + +float CurrentAspect() { + const ImGuiIO& io = ImGui::GetIO(); + if (io.DisplaySize.y <= 0.0f) { + return 1.0f; + } + return io.DisplaySize.x / io.DisplaySize.y; +} + +// GameCube's own button colours. White-on-white is unreadable over bright track +// surfaces, so every control also gets a dark halo and a dark label shadow, +// which keeps it legible on light and dark backgrounds alike. +constexpr ImU32 kColA = IM_COL32(67, 176, 42, 255); // green +constexpr ImU32 kColB = IM_COL32(228, 0, 43, 255); // red +constexpr ImU32 kColZ = IM_COL32(104, 82, 214, 255); // purple +constexpr ImU32 kColGrey = IM_COL32(196, 198, 206, 255); +constexpr ImU32 kColStick = IM_COL32(174, 176, 184, 255); + +ImU32 WithAlpha(ImU32 colour, int alpha) { + return (colour & 0x00FFFFFFu) | (static_cast(alpha) << IM_COL32_A_SHIFT); +} + +void DrawCircle(ImDrawList* list, const Circle& c, const ImVec2& size, const char* label, + bool pressed, ImU32 colour, const char* art) { + const ImVec2 centre{c.x * size.x, c.y * size.y}; + const float radius = c.r * size.y; + const ImVec2 lo{centre.x - radius, centre.y - radius}; + const ImVec2 hi{centre.x + radius, centre.y + radius}; + + // The solid silhouette tinted black darkens the glyph's own shape; a plain + // circle behind it would put a disc around the cross and the triggers. + if (art != nullptr) { + const ImTextureID fill = TouchArt::Get((std::string(art) + "_fill").c_str()); + if (fill != ImTextureID{}) { + list->AddImage(fill, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + IM_COL32(0, 0, 0, pressed ? 165 : 120)); + } + const ImTextureID line = TouchArt::Get(art); + if (line != ImTextureID{}) { + list->AddImage(line, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + IM_COL32(255, 255, 255, pressed ? 250 : 175)); + return; + } + } + + // Vector fallback, used when the artwork is missing. + list->AddCircleFilled(centre, radius + 3.0f, IM_COL32(0, 0, 0, pressed ? 150 : 110), 48); + list->AddCircleFilled(centre, radius, WithAlpha(colour, pressed ? 235 : 130), 48); + list->AddCircle(centre, radius, WithAlpha(IM_COL32(255, 255, 255, 255), pressed ? 255 : 170), + 48, 2.5f); + if (label != nullptr && *label != '\0') { + const ImVec2 text = ImGui::CalcTextSize(label); + const ImVec2 at{centre.x - text.x * 0.5f, centre.y - text.y * 0.5f}; + list->AddText(ImVec2{at.x + 1.0f, at.y + 1.0f}, IM_COL32(0, 0, 0, 190), label); + list->AddText(at, IM_COL32(255, 255, 255, 240), label); + } +} + +} // namespace + +bool HasTouchScreen() { + int deviceCount = 0; + SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount); + if (devices != nullptr) { + SDL_free(devices); + } + return deviceCount > 0; +} + +// Edge-triggered: the settings bar must toggle once per tap, not once per frame +// for as long as a finger rests on the button. +void PollMenuButton() { + static bool wasDown = false; + bool down = false; + const float aspect = CurrentAspect(); + const Layout L = LayoutFor(aspect); + int deviceCount = 0; + SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount); + if (devices != nullptr) { + for (int d = 0; d < deviceCount && !down; ++d) { + int fingerCount = 0; + SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount); + if (fingers == nullptr) { + continue; + } + for (int f = 0; f < fingerCount; ++f) { + if (L.menu.Contains(fingers[f]->x, fingers[f]->y, aspect)) { + down = true; + break; + } + } + SDL_free(fingers); + } + SDL_free(devices); + } + if (down && !wasDown) { + settings_overlay::ToggleTopBar(); + } + wasDown = down; +} + +// Tapping the FPS readout opens the settings bar. Preferred over a dedicated +// button because it reuses something already on screen. +bool PollFpsTap(const ImVec2& size) { + float minX = 0.0f, minY = 0.0f, maxX = 0.0f, maxY = 0.0f; + if (!settings_overlay::FpsOverlayBounds(&minX, &minY, &maxX, &maxY)) { + return false; // readout hidden - caller falls back to its own button + } + + static bool wasDown = false; + bool down = false; + int deviceCount = 0; + SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount); + if (devices != nullptr) { + for (int d = 0; d < deviceCount && !down; ++d) { + int fingerCount = 0; + SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount); + if (fingers == nullptr) { + continue; + } + for (int f = 0; f < fingerCount; ++f) { + const float px = fingers[f]->x * size.x; + const float py = fingers[f]->y * size.y; + if (px >= minX && px <= maxX && py >= minY && py <= maxY) { + down = true; + break; + } + } + SDL_free(fingers); + } + SDL_free(devices); + } + if (down && !wasDown) { + settings_overlay::ToggleTopBar(); + } + wasDown = down; + return true; +} + +bool IsActive() { + + // A connected pad wins: PAD__Read_HLE already prefers it for input, and the + // overlay should get out of the way visually too rather than sit on top of + // the game doing nothing. + int gamepadCount = 0; + SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepadCount); + if (gamepads != nullptr) { + SDL_free(gamepads); + } + if (gamepadCount > 0) { + return false; + } + + int deviceCount = 0; + SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount); + if (devices != nullptr) { + SDL_free(devices); + } + return deviceCount > 0; +} + + +bool Read(std::array& statuses) { + if (!IsActive()) { + return false; + } + const Frame frame = SampleFingers(CurrentAspect()); + + PADStatus& pad = statuses[0]; + pad = PADStatus{}; + pad.err = PAD_ERR_NONE; + + uint16_t buttons = 0; + if (frame.a) buttons |= PAD_BUTTON_A; + if (frame.b) buttons |= PAD_BUTTON_B; + if (frame.item) buttons |= PAD_TRIGGER_Z; + if (frame.start) buttons |= PAD_BUTTON_START; + if (frame.l) buttons |= PAD_TRIGGER_L; + if (frame.r) buttons |= PAD_TRIGGER_R; + if (frame.up) buttons |= PAD_BUTTON_UP; + if (frame.down) buttons |= PAD_BUTTON_DOWN; + if (frame.left) buttons |= PAD_BUTTON_LEFT; + if (frame.right) buttons |= PAD_BUTTON_RIGHT; + pad.button = buttons; + + // Screen y grows downward, the guest stick grows upward. + pad.stickX = static_cast(std::clamp(frame.stickDx * 127.0f, -127.0f, 127.0f)); + pad.stickY = static_cast(std::clamp(-frame.stickDy * 127.0f, -127.0f, 127.0f)); + pad.triggerLeft = frame.l ? 255 : 0; + pad.triggerRight = frame.r ? 255 : 0; + return true; +} + +void Draw() { + const ImGuiIO& io = ImGui::GetIO(); + const ImVec2 size = io.DisplaySize; + if (size.x <= 0.0f || size.y <= 0.0f || !HasTouchScreen()) { + return; + } + const Layout L = LayoutFor(size.x / size.y); + + // The way into settings exists whenever there is a screen to tap, even when + // a pad has hidden everything else. + ImDrawList* menuList = ImGui::GetBackgroundDrawList(); + if (!PollFpsTap(size)) { + // No FPS readout to tap, so fall back to an explicit button - otherwise a + // touch host with show_fps off has no way into the settings bar at all. + PollMenuButton(); + DrawCircle(menuList, L.menu, size, settings_overlay::TopBarVisible() ? "X" : "=", + settings_overlay::TopBarVisible(), kColGrey, nullptr); + } + + if (!IsActive()) { + return; + } + const Frame frame = SampleFingers(size.x / size.y); + ImDrawList* list = ImGui::GetBackgroundDrawList(); + + // Stick: outer ring plus a thumb dot showing current deflection. + // The glyph is the knob; nothing is drawn behind it. + const ImVec2 stickCentre{L.stick.x * size.x, L.stick.y * size.y}; + const float stickRadius = L.stick.r * size.y; + const float knobRadius = stickRadius * 0.80f; + const float travel = stickRadius * 0.38f; + const Circle knob{(stickCentre.x + frame.stickDx * travel) / size.x, + (stickCentre.y + frame.stickDy * travel) / size.y, + knobRadius / size.y}; + DrawCircle(list, knob, size, "", frame.stickHeld, kColStick, "control_stick"); + + DrawCircle(list, L.a, size, "A", frame.a, kColA, "a"); + DrawCircle(list, L.b, size, "B", frame.b, kColB, "b"); + DrawCircle(list, L.item, size, "Z", frame.item, kColZ, "right_bumper"); + DrawCircle(list, L.start, size, "START", frame.start, kColGrey, "start_pause"); + DrawCircle(list, L.shoulderL, size, "L", frame.l, kColGrey, "l_analog"); + DrawCircle(list, L.shoulderR, size, "R", frame.r, kColGrey, "r_analog"); + const bool dpadHeld = frame.up || frame.down || frame.left || frame.right; + DrawCircle(list, L.dpad, size, "", dpadHeld, kColGrey, "d-pad"); +} + +} // namespace TouchPad diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index d06a0ce..ba06e76 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -60,6 +60,7 @@ #include "aurora_events.h" #include "wii_remote_input.h" #include "discord_presence.h" +#include "touch_art.h" #include "fiber_manager.h" #include "hle_stubs.h" #include "runtime_config.h" @@ -1431,6 +1432,10 @@ int RuntimeMain(int argc, char** argv) { } aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait); GxGuestWrite::InstallAuroraHooks(); +#ifdef MKW_PLATFORM_IOS + // Up front, or the frame the controls appear on pays for every decode. + TouchArt::Preload(); +#endif UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width, auroraInfo.windowSize.native_fb_height); settings_overlay::InitializeRuntimeSettings(); @@ -1509,7 +1514,28 @@ int RuntimeMain(int argc, char** argv) { } } +// SDL must own main() on iOS: it supplies the entry point that starts +// UIApplicationMain and hands control back here once the app is running. +#ifdef MKW_PLATFORM_IOS +#include +#endif + int main(int argc, char** argv) { +#ifdef MKW_PLATFORM_IOS + // SDL's iOS backend calls SDL_main from scene:willConnectToSession:, once + // for every scene that connects (SDL_uikitappdelegate.m, postFinishLaunch). + // Attaching an external display makes iOS create a second scene, so this + // runs again while the first call is still inside the game loop, pumping + // events. Re-entering would boot the guest a second time in a live process: + // the observed result was data sections reloaded, 0 static constructors on + // the second pass, and aurora dying on "Only one window allowed per + // display". Declaring UIApplicationSupportsMultipleScenes=false does not + // prevent the extra scene, so refuse the re-entry here instead. + static std::atomic_flag alreadyRunning = ATOMIC_FLAG_INIT; + if (alreadyRunning.test_and_set(std::memory_order_acq_rel)) { + return 0; + } +#endif return RuntimeMain(argc, argv); } extern "C" bool g_dynamicAspectRatioEnabled = false; diff --git a/runtime/src/platform/host_platform.cpp b/runtime/src/platform/host_platform.cpp index c0caa95..4b658f4 100644 --- a/runtime/src/platform/host_platform.cpp +++ b/runtime/src/platform/host_platform.cpp @@ -59,6 +59,18 @@ std::filesystem::path ApplicationDataDirectory(std::string_view applicationName) CoTaskMemFree(rawPath); return directory; } +#elif defined(MKW_PLATFORM_IOS) + // The whole container is already private to this app, so user state lives + // in Documents rather than a name-scoped Application Support subdirectory. + // That puts Config.toml beside the game data the user copied in, which is + // what makes a relative dvd_root resolve, and UIFileSharingEnabled exposes + // the directory in Files.app so the user can manage it. + if (const char* home = std::getenv("HOME"); home && *home) { + return std::filesystem::path(home) / "Documents"; + } + if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) { + return std::filesystem::path(user->pw_dir) / "Documents"; + } #elif defined(__APPLE__) if (const char* home = std::getenv("HOME"); home && *home) { return std::filesystem::path(home) / "Library" / "Application Support" / applicationName; diff --git a/runtime/src/platform/ios/compiler_rt_shim.c b/runtime/src/platform/ios/compiler_rt_shim.c new file mode 100644 index 0000000..1274c38 --- /dev/null +++ b/runtime/src/platform/ios/compiler_rt_shim.c @@ -0,0 +1,21 @@ +// Apple's clang links libclang_rt.ios.a implicitly; upstream clang on a Linux +// host has no iOS compiler-rt, and the only builtins this program needs from +// it are the two @available() checks. They are thin wrappers over dyld, so +// provide them here rather than asking people to extract a file from Xcode. +#include + +typedef struct { + uint32_t platform; + uint32_t version; +} dyld_build_version_t; + +extern int _availability_version_check(uint32_t count, dyld_build_version_t versions[]); + +int32_t __isPlatformVersionAtLeast(uint32_t platform, uint32_t major, uint32_t minor, uint32_t subminor) { + dyld_build_version_t wanted = {platform, (major << 16) | (minor << 8) | subminor}; + return _availability_version_check(1, &wanted); +} + +int32_t __isOSVersionAtLeast(int32_t major, int32_t minor, int32_t subminor) { + return __isPlatformVersionAtLeast(2 /* PLATFORM_IOS */, (uint32_t)major, (uint32_t)minor, (uint32_t)subminor); +} diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index f2e753f..ca1809d 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1,4 +1,5 @@ #include "settings_overlay.h" +#include "touch_pad.h" #include "audio_backend.h" #include "controller_mapping_wizard.h" #include "game_graphics_options.h" @@ -65,6 +66,8 @@ const char* GraphicsApiDisplayName() { } bool g_topBarVisible = false; +float g_fpsBounds[4] = {0.0f, 0.0f, 0.0f, 0.0f}; +bool g_fpsBoundsValid = false; int g_controllerPort = 0; float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f); int g_audioVolumePercent = static_cast(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f)); @@ -196,7 +199,12 @@ constexpr std::array kDisplayModeConfigNames = { uint64_t g_presentedFrame = 0; std::atomic_bool g_strapInputAccepted = false; std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX; -constexpr uint64_t kStrapTransitionCoverFrames = 60; +// The strap screen is accepted the instant it becomes eligible, so the frames +// it would have spent waiting for A are still rendered underneath this cover. +// One second was not enough on an Apple TV 4K or an iPhone loading from the +// app container: the scene change landed after the cover lifted and the strap +// warning flashed through. Three seconds covers the slowest device measured. +constexpr uint64_t kStrapTransitionCoverFrames = 180; constexpr std::array kResolutions = {{ {"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f}, @@ -798,6 +806,9 @@ void DrawGraphicsSettings() { void DrawFpsOverlay() { AuroraPresentTiming presentTiming{}; aurora_get_present_timing(&presentTiming); + // Before the early return: a stale rectangle would keep swallowing taps + // meant for the menu button, leaving no way into settings. + g_fpsBoundsValid = false; if (!g_showFps) { return; } @@ -815,6 +826,13 @@ void DrawFpsOverlay() { ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings; if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) { + const ImVec2 wpos = ImGui::GetWindowPos(); + const ImVec2 wsize = ImGui::GetWindowSize(); + constexpr float kTouchPad = 14.0f; + g_fpsBounds[0] = wpos.x - kTouchPad; g_fpsBounds[1] = wpos.y - kTouchPad; + g_fpsBounds[2] = wpos.x + wsize.x + kTouchPad; + g_fpsBounds[3] = wpos.y + wsize.y + kTouchPad; + g_fpsBoundsValid = true; if (presentTiming.sampleCount == 0) { ImGui::TextUnformatted("FPS: --"); } else { @@ -1037,6 +1055,14 @@ void HandleEvents(const AuroraEvent* events) noexcept { } } +void ToggleTopBar() noexcept { SetTopBarVisible(!g_topBarVisible); } +bool TopBarVisible() noexcept { return g_topBarVisible; } +bool FpsOverlayBounds(float* a, float* b, float* c, float* d) noexcept { + if (!g_fpsBoundsValid) return false; + *a = g_fpsBounds[0]; *b = g_fpsBounds[1]; *c = g_fpsBounds[2]; *d = g_fpsBounds[3]; + return true; +} + void Draw() noexcept { // Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists // and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands. @@ -1055,8 +1081,13 @@ void Draw() noexcept { DrawFpsOverlay(); DrawTopBar(); controller_mapping_wizard::Draw(); - // The wizard captures raw presses; keep them out of the game. - PADBlockInput(controller_mapping_wizard::IsActive()); +#ifdef MKW_PLATFORM_IOS + TouchPad::Draw(); +#endif + // The wizard captures raw presses; keep them out of the game even when the + // top bar is hidden mid-setup. The top bar matters for touch too: while it + // is open, taps belong to the bar and must not drive the guest. + PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive()); DrawStartupScreen(); }