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 0000000..96c75a0 Binary files /dev/null and b/runtime/assets/touch/a.png differ diff --git a/runtime/assets/touch/a_fill.png b/runtime/assets/touch/a_fill.png new file mode 100644 index 0000000..8b9ba48 Binary files /dev/null and b/runtime/assets/touch/a_fill.png differ diff --git a/runtime/assets/touch/b.png b/runtime/assets/touch/b.png new file mode 100644 index 0000000..5607871 Binary files /dev/null and b/runtime/assets/touch/b.png differ diff --git a/runtime/assets/touch/b_fill.png b/runtime/assets/touch/b_fill.png new file mode 100644 index 0000000..7992bd8 Binary files /dev/null and b/runtime/assets/touch/b_fill.png differ diff --git a/runtime/assets/touch/control_stick.png b/runtime/assets/touch/control_stick.png new file mode 100644 index 0000000..51e2f9e Binary files /dev/null and b/runtime/assets/touch/control_stick.png differ diff --git a/runtime/assets/touch/control_stick_fill.png b/runtime/assets/touch/control_stick_fill.png new file mode 100644 index 0000000..4f77786 Binary files /dev/null and b/runtime/assets/touch/control_stick_fill.png differ diff --git a/runtime/assets/touch/d-pad.png b/runtime/assets/touch/d-pad.png new file mode 100644 index 0000000..cfc2736 Binary files /dev/null and b/runtime/assets/touch/d-pad.png differ diff --git a/runtime/assets/touch/d-pad_fill.png b/runtime/assets/touch/d-pad_fill.png new file mode 100644 index 0000000..58eb468 Binary files /dev/null and b/runtime/assets/touch/d-pad_fill.png differ diff --git a/runtime/assets/touch/l_analog.png b/runtime/assets/touch/l_analog.png new file mode 100644 index 0000000..e4764e8 Binary files /dev/null and b/runtime/assets/touch/l_analog.png differ diff --git a/runtime/assets/touch/l_analog_fill.png b/runtime/assets/touch/l_analog_fill.png new file mode 100644 index 0000000..ad7cad9 Binary files /dev/null and b/runtime/assets/touch/l_analog_fill.png differ diff --git a/runtime/assets/touch/r_analog.png b/runtime/assets/touch/r_analog.png new file mode 100644 index 0000000..487790f Binary files /dev/null and b/runtime/assets/touch/r_analog.png differ diff --git a/runtime/assets/touch/r_analog_fill.png b/runtime/assets/touch/r_analog_fill.png new file mode 100644 index 0000000..585344d Binary files /dev/null and b/runtime/assets/touch/r_analog_fill.png differ diff --git a/runtime/assets/touch/right_bumper.png b/runtime/assets/touch/right_bumper.png new file mode 100644 index 0000000..5be5a70 Binary files /dev/null and b/runtime/assets/touch/right_bumper.png differ diff --git a/runtime/assets/touch/right_bumper_fill.png b/runtime/assets/touch/right_bumper_fill.png new file mode 100644 index 0000000..7e32ed4 Binary files /dev/null and b/runtime/assets/touch/right_bumper_fill.png differ diff --git a/runtime/assets/touch/start_pause.png b/runtime/assets/touch/start_pause.png new file mode 100644 index 0000000..d09c8d0 Binary files /dev/null and b/runtime/assets/touch/start_pause.png differ diff --git a/runtime/assets/touch/start_pause_fill.png b/runtime/assets/touch/start_pause_fill.png new file mode 100644 index 0000000..4129e7b Binary files /dev/null and b/runtime/assets/touch/start_pause_fill.png differ 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(); }