Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f555de201f |
@@ -34,3 +34,46 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
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.
|
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]
|
> [!CAUTION]
|
||||||
> Only take builds from this repository's
|
> Only take builds from this repository's
|
||||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
> [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);
|
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
||||||
void PADRestoreDefaultMapping(u32 port);
|
void PADRestoreDefaultMapping(u32 port);
|
||||||
void PADBlockInput(bool block);
|
void PADBlockInput(bool block);
|
||||||
|
bool PADIsInputBlocked(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the default controller mapping used.
|
* 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) {
|
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
||||||
const auto* ctrl = __PADGetControllerForIndex(index);
|
const auto* ctrl = __PADGetControllerForIndex(index);
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <aurora/event.h>
|
#include <aurora/event.h>
|
||||||
#include <aurora/gfx.h>
|
#include <aurora/gfx.h>
|
||||||
#include <aurora/render_size_limits.hpp>
|
#include <aurora/render_size_limits.hpp>
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
#include <TargetConditionals.h>
|
||||||
|
#endif
|
||||||
#include <SDL3/SDL_error.h>
|
#include <SDL3/SDL_error.h>
|
||||||
#include <SDL3/SDL_events.h>
|
#include <SDL3/SDL_events.h>
|
||||||
#include <SDL3/SDL_keyboard.h>
|
#include <SDL3/SDL_keyboard.h>
|
||||||
@@ -408,8 +411,21 @@ bool create_window(AuroraBackend backend) {
|
|||||||
height = 480;
|
height = 480;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
|
Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
|
||||||
const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : 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();
|
const auto props = SDL_CreateProperties();
|
||||||
TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}",
|
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)$")
|
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
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)$")
|
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||||
# The first native macOS target is Apple Silicon. Intel and universal
|
# The first native macOS target is Apple Silicon. Intel and universal
|
||||||
# binaries remain future compatibility work; do not silently claim them.
|
# 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)
|
set(MKW_PLATFORM_LINUX TRUE)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR
|
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()
|
endif()
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
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,
|
# 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
|
# 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
|
# libraries and the products in cmake/PublicProducts.cmake) inherits them while
|
||||||
# aurora-main and its third-party tree stay unaffected.
|
# aurora-main and its third-party tree stay unaffected.
|
||||||
set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX)
|
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)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
@@ -136,6 +151,16 @@ else()
|
|||||||
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
|
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
|
||||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||||
endif()
|
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)
|
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||||
if(MKW_PLATFORM_WINDOWS)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
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.
|
# mkw_co_init/mkw_co_switch at link time.
|
||||||
enable_language(ASM)
|
enable_language(ASM)
|
||||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
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()
|
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_macos.cpp")
|
||||||
|
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp")
|
||||||
endif()
|
endif()
|
||||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
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")
|
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)
|
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||||
endif()
|
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
|
# 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)
|
enable_language(ASM)
|
||||||
add_executable(mkw_macos_context_abi_tests
|
add_executable(mkw_macos_context_abi_tests
|
||||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
"${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_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
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)
|
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||||
endif()
|
endif()
|
||||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
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)
|
mkw_configure_object_target(mkw_runtime_common)
|
||||||
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||||
SDL_MAIN_HANDLED
|
|
||||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
_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
|
target_link_libraries(mkw_runtime_common PRIVATE
|
||||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
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)
|
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||||
if(MKW_PLATFORM_WINDOWS)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
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}/.."
|
||||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||||
target_compile_definitions(${target} PRIVATE
|
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)
|
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||||
mkw_apply_common_compile_options(${target})
|
mkw_apply_common_compile_options(${target})
|
||||||
# The dispatch-table and registration shards compile inside the product target itself and
|
# 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}>)
|
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||||
endif()
|
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)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
target_link_libraries(${target} PRIVATE
|
target_link_libraries(${target} PRIVATE
|
||||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
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
|
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
|
"${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")
|
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
|
||||||
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
||||||
message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${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
|
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
"$<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()
|
endfunction()
|
||||||
|
|
||||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
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.
|
# tuning rather than leaving target-specific performance on the table.
|
||||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
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)$")
|
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||||
else()
|
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.
|
// of a global.
|
||||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
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__)
|
#if defined(__x86_64__)
|
||||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||||
// placement.
|
// 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"
|
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||||
#endif
|
#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))
|
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||||
|
#endif
|
||||||
|
|
||||||
enum class Backing {
|
enum class Backing {
|
||||||
Owned,
|
Owned,
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ void InitializeRuntimeSettings() noexcept;
|
|||||||
// Draw the F10 settings bar before each Aurora present.
|
// Draw the F10 settings bar before each Aurora present.
|
||||||
void HandleEvents(const AuroraEvent* events) noexcept;
|
void HandleEvents(const AuroraEvent* events) noexcept;
|
||||||
void Draw() noexcept;
|
void Draw() noexcept;
|
||||||
|
void ToggleTopBar() noexcept;
|
||||||
|
bool TopBarVisible() noexcept;
|
||||||
|
bool FpsOverlayBounds(float* minX, float* minY, float* maxX, float* maxY) noexcept;
|
||||||
bool StartupScreenVisible() noexcept;
|
bool StartupScreenVisible() noexcept;
|
||||||
void NotifyStrapInputAccepted() noexcept;
|
void NotifyStrapInputAccepted() noexcept;
|
||||||
void AdvancePresentedFrame() 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"
|
#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 "runtime_log.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -464,3 +479,5 @@ void Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace DiscordPresence
|
} // 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 "hle_stubs.h"
|
||||||
|
#include "touch_pad.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include "memory.h"
|
#include "memory.h"
|
||||||
#include "hle/controller_status_contract.h"
|
#include "hle/controller_status_contract.h"
|
||||||
#include "wii_remote_input.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.
|
// between "connected" and "no controller" every time the overlay toggles.
|
||||||
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
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 {
|
try {
|
||||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||||
WritePadStatus(statusPtr + static_cast<uint32_t>(i * PadStatusContract::kGuestStatusSize),
|
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 "aurora_events.h"
|
||||||
#include "wii_remote_input.h"
|
#include "wii_remote_input.h"
|
||||||
#include "discord_presence.h"
|
#include "discord_presence.h"
|
||||||
|
#include "touch_art.h"
|
||||||
#include "fiber_manager.h"
|
#include "fiber_manager.h"
|
||||||
#include "hle_stubs.h"
|
#include "hle_stubs.h"
|
||||||
#include "runtime_config.h"
|
#include "runtime_config.h"
|
||||||
@@ -1431,6 +1432,10 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||||
GxGuestWrite::InstallAuroraHooks();
|
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,
|
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||||
auroraInfo.windowSize.native_fb_height);
|
auroraInfo.windowSize.native_fb_height);
|
||||||
settings_overlay::InitializeRuntimeSettings();
|
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) {
|
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);
|
return RuntimeMain(argc, argv);
|
||||||
}
|
}
|
||||||
extern "C" bool g_dynamicAspectRatioEnabled = false;
|
extern "C" bool g_dynamicAspectRatioEnabled = false;
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ std::filesystem::path ApplicationDataDirectory(std::string_view applicationName)
|
|||||||
CoTaskMemFree(rawPath);
|
CoTaskMemFree(rawPath);
|
||||||
return directory;
|
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__)
|
#elif defined(__APPLE__)
|
||||||
if (const char* home = std::getenv("HOME"); home && *home) {
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
||||||
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
|
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 "settings_overlay.h"
|
||||||
|
#include "touch_pad.h"
|
||||||
#include "audio_backend.h"
|
#include "audio_backend.h"
|
||||||
#include "controller_mapping_wizard.h"
|
#include "controller_mapping_wizard.h"
|
||||||
#include "game_graphics_options.h"
|
#include "game_graphics_options.h"
|
||||||
@@ -65,6 +66,8 @@ const char* GraphicsApiDisplayName() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool g_topBarVisible = false;
|
bool g_topBarVisible = false;
|
||||||
|
float g_fpsBounds[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
bool g_fpsBoundsValid = false;
|
||||||
int g_controllerPort = 0;
|
int g_controllerPort = 0;
|
||||||
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
||||||
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.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;
|
uint64_t g_presentedFrame = 0;
|
||||||
std::atomic_bool g_strapInputAccepted = false;
|
std::atomic_bool g_strapInputAccepted = false;
|
||||||
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
|
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 = {{
|
constexpr std::array<ResolutionItem, 8> kResolutions = {{
|
||||||
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
|
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
|
||||||
@@ -798,6 +806,9 @@ void DrawGraphicsSettings() {
|
|||||||
void DrawFpsOverlay() {
|
void DrawFpsOverlay() {
|
||||||
AuroraPresentTiming presentTiming{};
|
AuroraPresentTiming presentTiming{};
|
||||||
aurora_get_present_timing(&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) {
|
if (!g_showFps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -815,6 +826,13 @@ void DrawFpsOverlay() {
|
|||||||
ImGuiWindowFlags_NoNav |
|
ImGuiWindowFlags_NoNav |
|
||||||
ImGuiWindowFlags_NoSavedSettings;
|
ImGuiWindowFlags_NoSavedSettings;
|
||||||
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
|
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) {
|
if (presentTiming.sampleCount == 0) {
|
||||||
ImGui::TextUnformatted("FPS: --");
|
ImGui::TextUnformatted("FPS: --");
|
||||||
} else {
|
} 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 {
|
void Draw() noexcept {
|
||||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
// 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.
|
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||||
@@ -1055,8 +1081,13 @@ void Draw() noexcept {
|
|||||||
DrawFpsOverlay();
|
DrawFpsOverlay();
|
||||||
DrawTopBar();
|
DrawTopBar();
|
||||||
controller_mapping_wizard::Draw();
|
controller_mapping_wizard::Draw();
|
||||||
// The wizard captures raw presses; keep them out of the game.
|
#ifdef MKW_PLATFORM_IOS
|
||||||
PADBlockInput(controller_mapping_wizard::IsActive());
|
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();
|
DrawStartupScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||