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: <mach/mach_vm.h> 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 <mach/mach_vm.h>. 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <aurora/event.h>
|
||||
#include <aurora/gfx.h>
|
||||
#include <aurora/render_size_limits.hpp>
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
@@ -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 {}: {}",
|
||||
|
||||
@@ -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 (<mach/mach_vm.h> 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: <mach/mach_vm.h> 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 <mach/mach_vm.h> 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)
|
||||
|
||||
@@ -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/
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -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 <SDL3/SDL_main.h> 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)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${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}" "$<TARGET_FILE_DIR:${target}>/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 "$<TARGET_FILE_DIR:${target}>/touch"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${MKW_TOUCH_ASSET_DIR}" "$<TARGET_FILE_DIR:${target}>/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}"
|
||||
"$<TARGET_FILE_DIR:${target}>/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 "$<TARGET_FILE_DIR:${target}>/../ipa"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${target}>/../ipa/Payload"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"$<TARGET_BUNDLE_DIR:${target}>"
|
||||
"$<TARGET_FILE_DIR:${target}>/../ipa/Payload/${target}.app"
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir "$<TARGET_FILE_DIR:${target}>/../ipa"
|
||||
${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${target}-unsigned.ipa" --format=zip Payload
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../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()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key><string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key><string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
<key>CFBundleName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
<key>CFBundleDisplayName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
<key>CFBundleVersion</key><string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key><string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>LSRequiresIPhoneOS</key><true/>
|
||||
<key>MinimumOSVersion</key><string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
|
||||
<key>UILaunchScreen</key><dict/>
|
||||
<key>UIRequiresFullScreen</key><true/>
|
||||
<key>CADisableMinimumFrameDuration</key><true/>
|
||||
<key>UIFileSharingEnabled</key><true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key><true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key><string>CHANGEME.com.example.wiicompiled</string>
|
||||
<key>com.apple.developer.team-identifier</key><string>CHANGEME</string>
|
||||
<key>get-task-allow</key><true/>
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key><true/>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
#endif
|
||||
|
||||
enum class Backing {
|
||||
Owned,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
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<PADStatus, 4>& statuses);
|
||||
|
||||
// Draws the control overlay. Must be called inside a live ImGui frame.
|
||||
void Draw();
|
||||
|
||||
|
||||
} // namespace TouchPad
|
||||
@@ -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 <algorithm>
|
||||
@@ -464,3 +479,5 @@ void Shutdown() {
|
||||
}
|
||||
|
||||
} // namespace DiscordPresence
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "guest_flat_memory.h"
|
||||
|
||||
// iOS variant of the macOS backend. <mach/mach_vm.h> is not in the iOS SDK, so
|
||||
// the vm_* calls from <mach/mach.h> are used instead, and the backing store is
|
||||
// an anonymous mapping rather than a file in /tmp, which the sandbox denies.
|
||||
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_error.h>
|
||||
#include <mach/vm_map.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
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<Mapping> g_mappings;
|
||||
std::vector<RegionRequest> 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<RegionRequest>& a, const std::vector<RegionRequest>& 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<RegionRequest>& regions) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_requiresCheckedAccess = static_cast<size_t>(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<vm_size_t>(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<uint8_t*>(address);
|
||||
|
||||
struct Store { Backing kind; uint32_t owned; uint64_t size; uint8_t* mem; };
|
||||
std::vector<Store> 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<Store>* 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<size_t>(s.size));
|
||||
}
|
||||
}
|
||||
if (reservation != 0) {
|
||||
vm_deallocate(mach_task_self(), reservation,
|
||||
static_cast<vm_size_t>(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<size_t>(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<uint8_t*>(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<vm_address_t>(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<vm_size_t>(r.size), 0,
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(),
|
||||
reinterpret_cast<vm_address_t>(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<vm_address_t>(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<size_t>(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
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "touch_pad.h"
|
||||
|
||||
#include <array>
|
||||
#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<PADStatus, PAD_CHANMAX> 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<uint32_t>(i * PadStatusContract::kGuestStatusSize),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "touch_art.h"
|
||||
|
||||
#include <aurora/imgui.h>
|
||||
#include <png.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string, ImTextureID> 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<png_bytep> 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<uint8_t>& 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<size_t>(width) * height * 4u, 0);
|
||||
r.rows.resize(height);
|
||||
for (uint32_t y = 0; y < height; ++y) {
|
||||
r.rows[y] = out.data() + static_cast<size_t>(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<uint8_t> 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
|
||||
@@ -0,0 +1,408 @@
|
||||
#include "touch_pad.h"
|
||||
#include "touch_art.h"
|
||||
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "settings_overlay.h"
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_touch.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
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<ImU32>(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<PADStatus, 4>& 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<int8_t>(std::clamp(frame.stickDx * 127.0f, -127.0f, 127.0f));
|
||||
pad.stickY = static_cast<int8_t>(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
|
||||
@@ -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 <SDL3/SDL_main.h>
|
||||
#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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
|
||||
@@ -196,7 +199,12 @@ constexpr std::array<std::string_view, 3> 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<ResolutionItem, 8> 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();
|
||||
}
|
||||
|
||||
|
||||