Merge upstream main into openxr-work

This commit is contained in:
iChris4 committed 2026-09-03 15:08:50 +02:00
commit 60316f0115
69 files changed
+5581 -315

No files matched your search

+160 -47
View File
@@ -1,18 +1,28 @@
cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled)
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64 or aarch64")
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
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.
set(MKW_PLATFORM_MACOS TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
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")
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)
# 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
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
@@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
# untouched, so this target is never built there.
if(NOT WIN32)
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
if(MKW_PLATFORM_LINUX)
add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -110,7 +117,7 @@ set(MKW_CPPWINRT_INCLUDE_DIR "" CACHE PATH "Optional self-contained C++/WinRT in
# available to explicit developer builds without making a nonfunctional SDK
# fetch part of every normal build.
set(MKW_ENABLE_OPENXR_DEFAULT OFF)
if(WIN32)
if(MKW_PLATFORM_WINDOWS)
set(MKW_ENABLE_OPENXR_DEFAULT ON)
endif()
option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ${MKW_ENABLE_OPENXR_DEFAULT})
@@ -143,31 +150,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
if(WIN32)
if(MKW_PLATFORM_WINDOWS)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
else()
# Non-Windows (Linux): mirrors aurora-main's own
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
elseif(MKW_PLATFORM_MACOS)
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
else()
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif()
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
if(MKW_PLATFORM_WINDOWS)
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
endif()
endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -196,7 +207,9 @@ else()
if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
if(MKW_PLATFORM_WINDOWS)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
endif()
# Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here.
@@ -237,7 +250,7 @@ endif()
set(MKW_OPENXR_TARGET "")
if(MKW_ENABLE_OPENXR)
if(NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
if(NOT MKW_PLATFORM_WINDOWS AND NOT MKW_PLATFORM_LINUX)
message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR")
set(MKW_ENABLE_OPENXR OFF)
else()
@@ -286,6 +299,18 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# 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")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.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")
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
# The host ISA guard is the one translation unit that must not receive the
@@ -293,25 +318,113 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
${MKW_CPU_BASELINE_SOURCE})
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
if(MKW_BUILD_PRODUCTS)
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
# The prebuilt export only needs the aurora/third-party closure configured above, so a
# packaging machine without a translation stops here instead of failing.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
return()
endif()
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
else()
if(MKW_PLATFORM_MACOS)
# Compile-only audit of native runtime sources. It deliberately avoids
# translated products until their host dependencies are portable.
# These sources depend on generated/RuntimeConfig.h, which is emitted for
# a particular game by the translator and is intentionally unavailable
# in this platform-only configuration.
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
target_include_directories(mkw_macos_native_compile PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
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)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
endif()
+10
View File
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
endif()
endforeach()
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
# every platform this package format targets, not just the one that first hit the failure.
find_package(Threads REQUIRED)
# Dawn is a prebuilt package on both sides; resolve the same install tree the
# package was built against so its imported target (and therefore
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
+34 -26
View File
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
endif()
list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
@@ -76,13 +81,13 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_ENABLE_OPENXR)
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
endif()
if(WIN32)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
else()
elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
@@ -130,16 +135,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
# -march=x86-64-v3; this object library deliberately is not, which
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
# check from a C initializer so an unsupported machine gets a readable error
# instead of an illegal-instruction crash. Excluded from the unity build and the
# precompiled header because both are produced with the owning target's flags.
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
target_compile_options(mkw_cpu_baseline PRIVATE -w)
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# this object deliberately keeps the plain baseline ISA and checks the CPU
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# floor to probe: NEON/FMA are architectural requirements.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
target_compile_options(mkw_cpu_baseline PRIVATE -w)
endif()
if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base shards")
@@ -179,7 +184,9 @@ function(mkw_configure_product target)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the
# plain baseline ISA while everything around it is built for x86-64-v3.
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
endif()
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
@@ -195,7 +202,7 @@ function(mkw_configure_product target)
# include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
@@ -204,6 +211,10 @@ function(mkw_configure_product target)
# loader dependency must also be present on each final product link.
target_link_libraries(${target} PRIVATE ${MKW_OPENXR_TARGET})
endif()
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target})
@@ -218,23 +229,23 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
if(WIN32)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
else()
elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
# those objects, so the actual executable link needs mkw::libco directly, same as it
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
# needs it independently of the platform branch above. ${CMAKE_DL_LIBS} is
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
# objects (empty string on glibc >= 2.34, where dl* is in libc).
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(WIN32)
if(MKW_PLATFORM_WINDOWS)
foreach(runtime_dll libc++.dll libunwind.dll)
execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
@@ -310,13 +321,10 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif()
# x86-64-v3 (SSE3/SSSE3/SSE4.1/FMA/AVX2/BMI2) is the baseline runtime/src/host_cpu_baseline.cpp
# guards against - a fixed, portable floor since an x86_64 build may run on a different machine
# than the one that built it. AArch64 has no such redistribution path here: every build this
# project produces runs only on the machine that built it (local-build.sh, and the AppImage which
# wraps it, always build from source on the target), so -mcpu=native is safe and strictly better -
# real per-core tuning (scheduling, whatever NEON/atomic extensions that exact CPU actually has)
# instead of the generic armv8-a baseline Clang would otherwise assume.
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
# object above checks. AArch64 builds are compiled locally for the host that
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
# 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(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <string>
// Host implementation of Dolphin's /dev/dolphin Discord contract. The guest
// sends only strings and big-endian integer fields; the IPC wire protocol is
// owned here so translated game code never needs host SDK headers.
namespace DiscordPresence {
struct Activity {
std::string details;
std::string state;
std::string largeImageKey;
std::string largeImageText;
std::string smallImageKey;
std::string smallImageText;
int64_t startTimestamp = 0;
int64_t endTimestamp = 0;
uint32_t partySize = 0;
uint32_t partyMax = 0;
};
void Initialize(const std::string& basicClientId, const std::string& basicTitle);
void SetClient(const std::string& clientId);
void SetActivity(Activity activity);
void Reset();
void Shutdown();
} // namespace DiscordPresence
+5 -10
View File
@@ -6,6 +6,7 @@
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#if defined(_WIN32)
#ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations
struct CpuContext;
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber
struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle
void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -102,10 +103,6 @@ private:
static void CALLBACK FiberProc(void* param);
#else
static void FiberProc(void* param);
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
static void FiberProcTrampoline();
#endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
@@ -117,9 +114,8 @@ private:
static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
// whichever native call stack first called GuestFiberManager::Initialize() - both are
// plain void* handles, so one field serves both platforms.
// The scheduler's own host context. Its opaque handle is supplied by the
// active HostContext backend, so one field serves every supported host.
static void* s_schedulerFiber;
static uint32_t s_currentGuestThread;
static bool s_initialized;
@@ -135,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber
+26
View File
@@ -14,10 +14,17 @@ namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
#elif defined(__aarch64__) && defined(__APPLE__)
// Keep this well above the low address ranges that Darwin's ASLR may use for
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
// Linux AArch64 target retains its 64 GiB placement below.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
#elif defined(__aarch64__)
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
@@ -58,6 +65,25 @@ struct FaultCounters {
// True once the reservation exists and translated code may use the flat path.
bool IsActive();
// True when a host VM page covers more than one 4 KiB Wii page. In that
// configuration, guest-view page protection cannot safely represent per-Wii-
// page MMIO, deferred-read, or executable-write state, so general translated
// accesses must use the checked Memory::* path.
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
// fold this to a compile-time false: it appears in every flat access and must
// not become a hot-path load. Only AArch64, where the page size is a kernel
// configuration (4/16/64 KiB), has to probe it at runtime.
#if defined(_WIN32) || defined(__x86_64__)
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
#endif
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
#else
extern bool g_requiresCheckedAccess;
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
#endif
// Reserves the 4 GiB space (once per process) and maps every requested region
// into both views. Throws std::runtime_error with a precise diagnosis when the
// reservation, the section objects or a view cannot be created - a silent
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers and
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
// backend does not save. Its handles are only valid on the thread that
// initialized the scheduler.
namespace HostContext {
using Handle = void*;
using Entry = void (*)(void*);
bool InitializeScheduler(Handle* scheduler);
void ShutdownScheduler(Handle scheduler);
Handle Create(std::size_t stackSize, Entry entry, void* argument);
void Destroy(Handle context);
bool IsCurrent(Handle context);
void Switch(Handle target);
} // namespace HostContext
+4
View File
@@ -264,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
{
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
return MKW_FLAT_GUEST_BASE + addr;
}
@@ -274,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
// executable, and unmapped pages still trap.
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
{
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
if (addr > UINT32_MAX - 7u) [[unlikely]]
return nullptr;
if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
+34 -5
View File
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
(void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
if (GuestFlat::RequiresCheckedAccess()) {
// A host page can cover multiple independently-special Wii pages.
// Returning null keeps resolved accesses on the checked Memory::*
// path, which materializes deferred reads and applies write policy.
(void)needsWrite;
return nullptr;
}
if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr;
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
// written value can't be recovered from a fault record.
//
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
// FlatWrite* helpers then use the checked page-table path, which materializes
// deferred reads and applies executable-write/MMIO policy before touching RAM.
// FlatWriteRam* remains direct because the translator emits it only for
// addresses it has proven are ordinary RAM.
template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
}
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
return FlatLoad<uint8_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
return FlatLoad<uint16_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
return FlatLoad<uint32_t>(address);
}
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
const uint32_t bits = FlatLoad<uint32_t>(address);
const uint32_t bits = FlatRead32(address);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
const uint64_t bits = FlatLoad<uint64_t>(address);
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
// Small host-services boundary for functionality that must not leak Win32
// assumptions into runtime or game code. Guest execution, virtual memory, and
// cooperative contexts remain outside this layer until dedicated macOS
// prototypes establish a safe abstraction.
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept;
// Returns the platform's conventional per-user application-data directory.
// It does not create the directory, leaving that policy to the caller.
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName);
// The root for per-run diagnostics. Keeping this here ensures log placement
// follows the same host convention as configuration and other user data.
std::filesystem::path LogDirectory(std::string_view applicationName);
uint64_t CurrentProcessId() noexcept;
} // namespace RuntimePlatform
+124
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
@@ -17,6 +18,7 @@
#include <utility>
#include <vector>
#include <toml.hpp>
#include "platform/host_platform.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
@@ -59,7 +61,31 @@ struct RuntimeUserConfig {
std::optional<bool> audioMuted;
std::optional<bool> audioMixWorker;
std::optional<bool> attenuateMusicWhenMediaPlays;
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
std::optional<bool> wiiRemotes;
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
// after launch shows up without restarting.
std::optional<bool> wiiContinuousScan;
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
// SDL's sensor frame (x right, y out of the button face, z towards the user).
// SDL's Wii driver falls back to a nominal zero point when its read of the
// remote's calibration block times out (common over Bluetooth), so this is
// measured in the overlay with the remote at rest.
std::optional<double> wiiAccelOffsetX;
std::optional<double> wiiAccelOffsetY;
std::optional<double> wiiAccelOffsetZ;
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
std::optional<bool> wiiAccelTrace;
std::optional<bool> networkEnabled;
std::optional<bool> discordPresenceEnabled;
// The application ID of the WiiCompiled Discord application. This is only
// used by the base product; Retro Rewind supplies its own ID through the
// standard Dolphin /dev/dolphin interface.
std::optional<std::string> discordClientId;
std::optional<std::string> nandRoot;
std::optional<std::string> dvdRoot;
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
@@ -143,7 +169,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
// Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) {
#if defined(__APPLE__)
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
#elif defined(_WIN32)
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
#endif
return std::find(values.begin(), values.end(), value) != values.end();
}
@@ -174,6 +207,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
return RuntimePlatform::ExecutableDirectory();
#else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
@@ -231,6 +266,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
// non-empty, otherwise its default of $HOME/.local/share.
@@ -308,6 +345,12 @@ inline void EnsureConfigFile() {
"mix_worker = true\n\n"
"[network]\n"
"enabled = true\n\n"
"[discord]\n"
"# Rich Presence talks only to a locally-running Discord client.\n"
"# Retro Rewind supplies its official app ID automatically. Set this\n"
"# to WiiCompiled's Discord application ID for basic base-game presence.\n"
"enabled = true\n"
"# client_id = \"123456789012345678\"\n\n"
"[paths]\n"
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
"# nand_root = \"D:\\\\WiiNand\"\n"
@@ -374,6 +417,7 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
}
}
// Reads every supported setting out of a parsed Config.toml document.
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config;
@@ -456,7 +500,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
config.attenuateMusicWhenMediaPlays =
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
@@ -788,14 +840,75 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
return Get().audioMixWorker.value_or(fallback);
}
// Whether background music should duck automatically for other media playback.
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
}
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
// subsystem comes up, so a change only takes effect on the next launch.
inline bool WiiRemotesEnabled(bool fallback = true) {
return Get().wiiRemotes.value_or(fallback);
}
// Persists the Bluetooth Wii Remote driver switch.
inline bool SetWiiRemotesEnabled(bool value) {
Mutable().wiiRemotes = value;
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
}
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
inline bool WiiContinuousScanEnabled(bool fallback = true) {
return Get().wiiContinuousScan.value_or(fallback);
}
// Persists the continuous scanning switch.
inline bool SetWiiContinuousScanEnabled(bool value) {
Mutable().wiiContinuousScan = value;
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
}
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
// when the remote has not been calibrated.
inline std::array<double, 3> WiiAccelOffset() {
const RuntimeUserConfig& config = Get();
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
config.wiiAccelOffsetZ.value_or(0.0)};
}
// Whether to write the per-frame accelerometer trace (off unless asked for).
inline bool WiiAccelTraceEnabled(bool fallback = false) {
return Get().wiiAccelTrace.value_or(fallback);
}
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
inline bool HasWiiAccelOffset() {
const std::array<double, 3> offset = WiiAccelOffset();
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
}
// Persists the accelerometer correction measured by the overlay's calibration.
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsetX = offset[0];
Mutable().wiiAccelOffsetY = offset[1];
Mutable().wiiAccelOffsetZ = offset[2];
bool ok = true;
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
for (size_t i = 0; i < 3; ++i) {
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << offset[i];
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
}
return ok;
}
// Target frame rate for frame interpolation, or 0 to disable it.
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
return Get().frameInterpolationFps.value_or(fallback);
}
// Whether to skip draws whose graphics pipeline has not finished compiling yet.
inline bool SkipUnreadyPipelines(bool fallback = true) {
return Get().skipUnreadyPipelines.value_or(fallback);
}
@@ -897,6 +1010,14 @@ inline std::string RetroRewindRoot(std::string fallback = "") {
return Get().retroRewindRoot.value_or(std::move(fallback));
}
inline bool DiscordPresenceEnabled(bool fallback = true) {
return Get().discordPresenceEnabled.value_or(fallback);
}
inline std::string DiscordClientId(std::string fallback = "1543984562369990706") {
return Get().discordClientId.value_or(std::move(fallback));
}
inline const std::vector<std::string>& OverlayRoots() {
return Get().overlayRoots;
}
@@ -959,6 +1080,9 @@ inline void LogLoadedConfig() {
if (config.networkEnabled) {
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
}
if (config.discordPresenceEnabled) {
std::cout << " discord_enabled=" << (*config.discordPresenceEnabled ? "true" : "false");
}
if (config.nandRoot) {
std::cout << " nand_root=" << *config.nandRoot;
}
+1
View File
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code);
void MarkFatalErrorReported();
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact
+110
View File
@@ -0,0 +1,110 @@
#pragma once
#include <cstdint>
struct PADStatus;
// Real Wii Remotes paired over Bluetooth.
//
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
// extension in or out mid-game switches control scheme like on the console. Only
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
// aurora's PAD layer as a GameCube pad.
namespace WiiRemoteInput {
enum class Kind : uint8_t {
NotWii,
Remote, // Wii Remote with no extension
RemoteWithNunchuk,
RemoteWithClassic,
WiiUPro,
};
// Must run before SDL's joystick subsystem is initialized (aurora does that
// inside aurora_initialize); SDL only consults the hint on its first device scan.
void ConfigureSdlHints(bool enabled);
// Classifies a gamepad by the name SDL's Wii driver reports for it.
Kind KindForName(const char* gamepadName);
// Classification of the SDL gamepad currently assigned to a game port.
Kind KindForPort(uint32_t port);
const char* KindLabel(Kind kind);
// One frame of a Wii Remote in the units KPAD uses.
struct KpadSample {
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
bool hasNunchuk = false;
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
float nunchukAcc[3] = {};
bool hasClassic = false;
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
float clRStick[2] = {};
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
int16_t clRStickRaw[2] = {};
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
uint8_t clTriggerR = 0;
};
// What the game should see on `chan`: the controller SDL has there right now,
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
// destroys and re-creates the joystick when an extension is plugged in or out,
// and the console never disconnects for that, so the gap is papered over with
// neutral input instead of a "communications interrupted" prompt.
Kind EffectiveKind(uint32_t chan);
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
// Nunchuk or with a Classic Controller (live or within the swap grace period).
bool IsRemoteChannel(uint32_t chan);
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
// is false. During the swap grace period the sample is neutral.
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// Remote accelerometer for the overlay readout: the SDL sample in g after the
// zero-point correction (x right across the face, y out of the button face, z
// towards the user) and the KPAD vector built from it. False when the port has
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
// remote's factory calibration block, but over Bluetooth that read often times
// out ("Using fallback accelerometer calibration" in console.log) and it falls
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
// offset.
// StartAccelCalibration expects the remote lying still with the buttons up;
// Poll() then collects samples for about a second and stores the difference to
// the ideal (0, 1, 0) g in Config.toml.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
bool IsAccelCalibrating();
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Outcome of the last calibration attempt for the overlay, or nullptr.
const char* AccelCalibrationMessage();
// Reports "no controller" on the GameCube side for every port served through
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
// a failed Bluetooth read or an extension change and never re-adds it on its
// own. Poll() runs once per PADRead and, while no Wii controller is present,
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
void Poll();
// Forces one re-enumeration right now (settings overlay "Rescan now").
void RescanNow();
// True while Poll() is actively rescanning (no Wii controller connected).
bool IsScanning();
// Rescans issued since a Wii controller was last seen.
uint32_t ScanCount();
} // namespace WiiRemoteInput
+466
View File
@@ -0,0 +1,466 @@
#include "discord_presence.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <optional>
#include <sstream>
#include <string_view>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
namespace DiscordPresence {
namespace {
constexpr uint32_t kHandshakeOpcode = 0;
constexpr uint32_t kFrameOpcode = 1;
constexpr size_t kMaxClientIdLength = 32;
constexpr auto kConnectionRetryCooldown = std::chrono::seconds(1);
bool IsClientId(std::string_view value) {
return !value.empty() && value.size() <= kMaxClientIdLength &&
std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; });
}
std::string EscapeJson(std::string_view value) {
std::string escaped;
escaped.reserve(value.size());
for (const unsigned char ch : value) {
switch (ch) {
case '\\': escaped += "\\\\"; break;
case '\"': escaped += "\\\""; break;
case '\b': escaped += "\\b"; break;
case '\f': escaped += "\\f"; break;
case '\n': escaped += "\\n"; break;
case '\r': escaped += "\\r"; break;
case '\t': escaped += "\\t"; break;
default:
if (ch < 0x20) {
static constexpr char kHex[] = "0123456789abcdef";
escaped += "\\u00";
escaped += kHex[ch >> 4];
escaped += kHex[ch & 0x0f];
} else {
escaped += static_cast<char>(ch);
}
}
}
return escaped;
}
void AppendJsonString(std::ostringstream& output, bool& hasValue, std::string_view key, std::string_view value) {
if (value.empty()) {
return;
}
if (hasValue) {
output << ',';
}
output << '\"' << key << "\":\"" << EscapeJson(value) << '\"';
hasValue = true;
}
std::string BuildActivityPayload(const Activity& activity) {
std::ostringstream json;
json << "{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"wiicompiled\",\"args\":{\"pid\":";
#if defined(_WIN32)
json << static_cast<unsigned long>(::GetCurrentProcessId());
#else
json << static_cast<long>(::getpid());
#endif
json << ",\"activity\":{";
bool hasActivityField = false;
AppendJsonString(json, hasActivityField, "details", activity.details);
AppendJsonString(json, hasActivityField, "state", activity.state);
const auto appendSection = [&](std::string_view name, const auto& append) {
if (hasActivityField) {
json << ',';
}
json << '\"' << name << "\":{";
append();
json << '}';
hasActivityField = true;
};
const bool hasAssets = !activity.largeImageKey.empty() || !activity.largeImageText.empty() ||
!activity.smallImageKey.empty() || !activity.smallImageText.empty();
if (hasAssets) {
appendSection("assets", [&] {
bool hasAsset = false;
AppendJsonString(json, hasAsset, "large_image", activity.largeImageKey);
AppendJsonString(json, hasAsset, "large_text", activity.largeImageText);
AppendJsonString(json, hasAsset, "small_image", activity.smallImageKey);
AppendJsonString(json, hasAsset, "small_text", activity.smallImageText);
});
}
if (activity.startTimestamp > 0 || activity.endTimestamp > 0) {
appendSection("timestamps", [&] {
if (activity.startTimestamp > 0) {
json << "\"start\":" << activity.startTimestamp;
}
if (activity.endTimestamp > 0) {
if (activity.startTimestamp > 0) {
json << ',';
}
json << "\"end\":" << activity.endTimestamp;
}
});
}
if (activity.partySize > 0 && activity.partyMax > 0) {
appendSection("party", [&] {
json << "\"size\":[" << activity.partySize << ',' << activity.partyMax << ']';
});
}
if (hasActivityField) {
json << ',';
}
json << "\"instance\":false}}}";
return json.str();
}
class Client {
public:
void Initialize(const std::string& clientId, const std::string& title) {
std::lock_guard<std::mutex> lock(mutex_);
basicClientId_ = IsClientId(clientId) ? clientId : std::string{};
basicActivity_ = {};
basicActivity_.details = title;
basicActivity_.startTimestamp = UnixSeconds();
customClient_ = false;
activity_ = basicActivity_;
ReconnectAndSendLocked();
}
void SetClient(const std::string& clientId) {
std::lock_guard<std::mutex> lock(mutex_);
if (!IsClientId(clientId)) {
RT_LOG(RT_TAG_RUNTIME) << "Ignoring invalid Discord client ID from /dev/dolphin" << std::endl;
return;
}
if (clientId_ == clientId && connected_) {
return;
}
customClient_ = true;
clientId_ = clientId;
CloseLocked();
ReconnectAndSendLocked();
}
void SetActivity(Activity activity) {
std::lock_guard<std::mutex> lock(mutex_);
if (!customClient_) {
return;
}
activity_ = std::move(activity);
SendActivityLocked();
}
void Reset() {
std::lock_guard<std::mutex> lock(mutex_);
customClient_ = false;
clientId_.clear();
activity_ = basicActivity_;
CloseLocked();
ReconnectAndSendLocked();
}
void Shutdown() {
std::lock_guard<std::mutex> lock(mutex_);
CloseLocked();
}
private:
static int64_t UnixSeconds() {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
std::string ActiveClientIdLocked() const {
return customClient_ ? clientId_ : basicClientId_;
}
bool WriteFrameLocked(uint32_t opcode, std::string_view payload) {
std::array<uint8_t, 8> header{};
const uint32_t size = static_cast<uint32_t>(payload.size());
for (size_t i = 0; i < 4; ++i) {
header[i] = static_cast<uint8_t>(opcode >> (i * 8));
header[4 + i] = static_cast<uint8_t>(size >> (i * 8));
}
return WriteAllLocked(header.data(), header.size()) &&
WriteAllLocked(reinterpret_cast<const uint8_t*>(payload.data()), payload.size());
}
void ReconnectAndSendLocked() {
const std::string clientId = ActiveClientIdLocked();
if (clientId.empty() || !ConnectLocked()) {
return;
}
const std::string handshake = "{\"v\":1,\"client_id\":\"" + clientId + "\"}";
if (!WriteFrameLocked(kHandshakeOpcode, handshake)) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
if (!ReadReadyLocked()) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
SendActivityLocked();
}
void SendActivityLocked() {
if (!connected_) {
ReconnectAndSendLocked();
return;
}
if (ActiveClientIdLocked().empty()) {
return;
}
if (!WriteFrameLocked(kFrameOpcode, BuildActivityPayload(activity_))) {
RecordFailedConnectionLocked();
CloseLocked();
}
}
bool ConnectionRetryAllowedLocked() const {
return !lastFailedConnectionAttempt_ ||
std::chrono::steady_clock::now() - *lastFailedConnectionAttempt_ >= kConnectionRetryCooldown;
}
void RecordFailedConnectionLocked() {
lastFailedConnectionAttempt_ = std::chrono::steady_clock::now();
}
#if defined(_WIN32)
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
for (unsigned int index = 0; index < 10; ++index) {
const std::string name = "\\\\.\\pipe\\discord-ipc-" + std::to_string(index);
handle_ = ::CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (handle_ != INVALID_HANDLE_VALUE) {
DWORD mode = PIPE_READMODE_BYTE;
::SetNamedPipeHandleState(handle_, &mode, nullptr, nullptr);
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
DWORD written = 0;
if (!::WriteFile(handle_, data, static_cast<DWORD>(size), &written, nullptr) || written == 0) {
return false;
}
data += written;
size -= written;
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
while (size != 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
DWORD available = 0;
if (!::PeekNamedPipe(handle_, nullptr, 0, nullptr, &available, nullptr)) {
return false;
}
if (available == 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
::Sleep(10);
continue;
}
DWORD read = 0;
const DWORD requested = static_cast<DWORD>(std::min<size_t>(size, available));
if (!::ReadFile(handle_, data, requested, &read, nullptr) || read == 0) {
return false;
}
data += read;
size -= read;
}
return true;
}
void CloseLocked() {
if (handle_ != INVALID_HANDLE_VALUE) {
::CloseHandle(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
connected_ = false;
}
HANDLE handle_ = INVALID_HANDLE_VALUE;
#else
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
std::array<std::string, 5> roots{};
size_t rootCount = 0;
if (const char* runtimeDir = std::getenv("XDG_RUNTIME_DIR"); runtimeDir && *runtimeDir) {
roots[rootCount++] = runtimeDir;
}
if (const char* tempDir = std::getenv("TMPDIR"); tempDir && *tempDir) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TEMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
roots[rootCount++] = "/tmp";
for (size_t rootIndex = 0; rootIndex < rootCount; ++rootIndex) {
for (unsigned int index = 0; index < 10; ++index) {
const std::string path = roots[rootIndex] + "/discord-ipc-" + std::to_string(index);
if (path.size() >= sizeof(sockaddr_un::sun_path)) {
continue;
}
const int socketFd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFd < 0) {
continue;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
if (::connect(socketFd, reinterpret_cast<const sockaddr*>(&address), sizeof(address)) == 0) {
timeval timeout{};
timeout.tv_sec = 1;
::setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
fd_ = socketFd;
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
::close(socketFd);
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t written = ::send(fd_, data, size, MSG_NOSIGNAL);
if (written <= 0) {
return false;
}
data += written;
size -= static_cast<size_t>(written);
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t read = ::recv(fd_, data, size, 0);
if (read <= 0) {
return false;
}
data += read;
size -= static_cast<size_t>(read);
}
return true;
}
void CloseLocked() {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
connected_ = false;
}
int fd_ = -1;
#endif
bool ReadReadyLocked() {
std::array<uint8_t, 8> header{};
if (!ReadAllLocked(header.data(), header.size())) {
return false;
}
uint32_t opcode = 0;
uint32_t size = 0;
for (size_t i = 0; i < 4; ++i) {
opcode |= static_cast<uint32_t>(header[i]) << (i * 8);
size |= static_cast<uint32_t>(header[4 + i]) << (i * 8);
}
if (opcode != kFrameOpcode || size > 1024 * 1024) {
return false;
}
std::vector<uint8_t> payload(size);
if (!ReadAllLocked(payload.data(), payload.size())) {
return false;
}
const std::string_view json(reinterpret_cast<const char*>(payload.data()), payload.size());
return json.find("\"evt\":\"READY\"") != std::string_view::npos;
}
std::mutex mutex_;
bool connected_ = false;
bool customClient_ = false;
std::optional<std::chrono::steady_clock::time_point> lastFailedConnectionAttempt_;
std::string basicClientId_;
std::string clientId_;
Activity basicActivity_;
Activity activity_;
};
Client g_client;
} // namespace
void Initialize(const std::string& basicClientId, const std::string& basicTitle) {
g_client.Initialize(basicClientId, basicTitle);
}
void SetClient(const std::string& clientId) {
g_client.SetClient(clientId);
}
void SetActivity(Activity activity) {
g_client.SetActivity(std::move(activity));
}
void Reset() {
g_client.Reset();
}
void Shutdown() {
g_client.Shutdown();
}
} // namespace DiscordPresence
+24 -139
View File
@@ -2,6 +2,7 @@
#include "memory.h"
#include "abi_bridge.h"
#include "hle_stubs.h"
#include "host_context.h"
#include "runtime_log.h"
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
@@ -13,24 +14,8 @@
#include <iomanip>
#include <sstream>
#if !defined(_WIN32)
#include "libco.h"
#endif
namespace Fiber {
#if !defined(_WIN32)
namespace {
// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param).
// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch
// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the
// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single
// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging
// write and the trampoline's read of it.
thread_local uint32_t s_pendingFiberArg = 0;
} // namespace
#endif
std::mutex GuestFiberManager::s_mutex;
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
std::lock_guard<std::mutex> lock(s_mutex);
toDelete.swap(s_fibersPendingDelete);
}
#if defined(_WIN32)
const void* current = GetCurrentFiber();
for (void* f : toDelete) {
if (f && f != current) {
DeleteFiber(f);
if (f && !HostContext::IsCurrent(f)) {
HostContext::Destroy(f);
}
}
#else
const void* current = co_active();
for (void* f : toDelete) {
if (f && f != current) {
co_delete(static_cast<cothread_t>(f));
}
}
#endif
}
// Global VI retrace counter
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
return;
}
#if defined(_WIN32)
// Convert the main thread to a fiber (the scheduler fiber)
s_schedulerFiber = ConvertThreadToFiber(nullptr);
if (!s_schedulerFiber) {
// May already be a fiber
s_schedulerFiber = GetCurrentFiber();
if (!s_schedulerFiber) {
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler fiber!" << std::endl;
ShowRuntimeFatalPopup("guest scheduler initialization failed",
"Windows could not create the scheduler fiber required to run guest threads.");
std::abort();
}
if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
ShowRuntimeFatalPopup("guest scheduler initialization failed",
"The host could not create the scheduler context required to run guest threads.");
std::abort();
}
#else
// co_active() returns a handle for whichever native stack is currently running, creating one
// on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts
// this call's own stack into a switchable target without altering control flow.
s_schedulerFiber = co_active();
#endif
s_currentGuestThread = 0;
s_initialized = true;
}
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
void GuestFiberManager::Shutdown() {
std::lock_guard<std::mutex> lock(s_mutex);
#if defined(_WIN32)
for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) {
DeleteFiber(fiber.fiber);
HostContext::Destroy(fiber.fiber);
fiber.fiber = nullptr;
}
}
s_fibers.clear();
// Convert scheduler fiber back to thread
if (s_schedulerFiber) {
ConvertFiberToThread();
HostContext::ShutdownScheduler(s_schedulerFiber);
s_schedulerFiber = nullptr;
}
#else
for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) {
co_delete(static_cast<cothread_t>(fiber.fiber));
fiber.fiber = nullptr;
}
}
s_fibers.clear();
// Unlike ConvertFiberToThread, libco has no "undo" for co_active(): the scheduler's own
// stack was never separately allocated, so there is nothing to release here.
s_schedulerFiber = nullptr;
#endif
s_initialized = false;
}
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
if (existingIt != s_fibers.end()) {
// Delete the old fiber if it exists and is not the scheduler fiber
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
#if defined(_WIN32)
DeleteFiber(existingIt->second.fiber);
#else
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
#endif
HostContext::Destroy(existingIt->second.fiber);
}
s_fibers.erase(existingIt);
}
@@ -312,31 +255,17 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
gf.cpuContext.pc = entryPoint;
gf.cpuContext.srr0 = entryPoint;
#if defined(_WIN32)
// Create Windows fiber with reasonable stack size
// Use host stack size (64KB should be plenty for translated code)
// The host stack models only translated host calls; the guest stack starts
// at stackBase in the CPU context above.
constexpr size_t kHostStackSize = 64 * 1024;
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
if (!gf.fiber) {
DWORD err = GetLastError();
RT_LOG(RT_TAG_OS) << "CreateFiber failed for thread 0x"
<< std::hex << guestThreadAddr
<< " error=" << std::dec << err << std::endl;
return false;
}
#else
// libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr
// into s_pendingFiberArg immediately before the co_switch that first activates this handle.
constexpr unsigned int kHostStackSize = 64 * 1024;
gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline);
if (!gf.fiber) {
RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x"
RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
<< std::hex << guestThreadAddr << std::dec << std::endl;
return false;
}
#endif
s_fibers[guestThreadAddr] = gf;
@@ -390,24 +319,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
}
if (it->second.fiber && !it->second.isSchedulerFiber) {
#if defined(_WIN32)
const void* current = GetCurrentFiber();
if (it->second.fiber == current) {
if (HostContext::IsCurrent(it->second.fiber)) {
s_fibersPendingDelete.push_back(it->second.fiber);
} else {
DeleteFiber(it->second.fiber);
HostContext::Destroy(it->second.fiber);
}
#else
const void* current = co_active();
if (it->second.fiber == current) {
// Deleting the coroutine we're currently executing on would free the very stack
// this call is running on; defer it (PurgePendingFibers) until some other fiber is
// active, exactly like the Windows branch above.
s_fibersPendingDelete.push_back(it->second.fiber);
} else {
co_delete(static_cast<cothread_t>(it->second.fiber));
}
#endif
it->second.fiber = nullptr;
}
}
@@ -474,12 +390,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
// Check if we're already on the target fiber (e.g., switching to main thread
// when we're already on the scheduler fiber)
#if defined(_WIN32)
void* currentFiber = GetCurrentFiber();
#else
void* currentFiber = co_active();
#endif
if (currentFiber == fiberHandle) {
if (HostContext::IsCurrent(fiberHandle)) {
// Already executing on the target host fiber. This is common for the
// default guest thread, which also owns the scheduler fiber. Keep the
// live CPU context instead of restoring a possibly stale saved copy
@@ -496,15 +407,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
}
// Switch to the target fiber (the target fiber will load its own context)
#if defined(_WIN32)
SwitchToFiber(fiberHandle);
#else
// Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has
// already started, since resuming it re-enters mid-function rather than through the
// trampoline's entry point.
s_pendingFiberArg = guestThreadAddr;
co_switch(static_cast<cothread_t>(fiberHandle));
#endif
HostContext::Switch(fiberHandle);
// When we return here, the fiber that issued SwitchToThread has resumed.
// That does not automatically mean the previous guest thread became runnable
@@ -618,14 +521,6 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) {
}
}
void GuestFiberManager::SwitchToScheduler() {
#if defined(_WIN32)
SwitchToFiber(s_schedulerFiber);
#else
co_switch(static_cast<cothread_t>(s_schedulerFiber));
#endif
}
#if defined(_WIN32)
void CALLBACK GuestFiberManager::FiberProc(void* param)
#else
@@ -634,6 +529,7 @@ void GuestFiberManager::FiberProc(void* param)
{
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
// Get our fiber info
GuestFiber* fiber = nullptr;
uint32_t entryPoint = 0;
@@ -644,7 +540,7 @@ void GuestFiberManager::FiberProc(void* param)
auto it = s_fibers.find(guestThreadAddr);
if (it == s_fibers.end()) {
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
SwitchToScheduler();
HostContext::Switch(s_schedulerFiber);
return;
}
fiber = &it->second;
@@ -707,7 +603,7 @@ void GuestFiberManager::FiberProc(void* param)
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
break;
}
SwitchToScheduler();
HostContext::Switch(s_schedulerFiber);
}
// The deferral loop above yields to the scheduler and therefore can resume
@@ -764,18 +660,7 @@ void GuestFiberManager::FiberProc(void* param)
}
// Return to scheduler
SwitchToScheduler();
HostContext::Switch(s_schedulerFiber);
}
#if !defined(_WIN32)
void GuestFiberManager::FiberProcTrampoline() {
const uint32_t guestThreadAddr = s_pendingFiberArg;
FiberProc(reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
// FiberProc always calls SwitchToScheduler() on every exit path and never falls off its own
// end; this is only a safety net in case that ever changes; falling off co_create's entry
// function is otherwise undefined behavior (libco's own crash() fallback aborts instead).
SwitchToScheduler();
}
#endif
} // namespace Fiber
+21 -4
View File
@@ -36,6 +36,9 @@
#endif
namespace GuestFlat {
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
bool g_requiresCheckedAccess = false;
#endif
namespace {
#if defined(_WIN32)
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
constexpr size_t kHostPageSize = 0x1000;
// Only hosts that can expose a page larger than 4 KiB need to discover their
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
size_t HostPageSize()
{
const long size = sysconf(_SC_PAGESIZE);
return size > 0 ? static_cast<size_t>(size) : kGuestPageSize;
}
#endif
// Named, platform-neutral protection modes so every fault-interception call site below (the
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
@@ -349,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); }
bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
void ApplyExecutableProtectionLocked() {
if (g_base == nullptr) return;
if (g_base == nullptr || RequiresCheckedAccess()) return;
auto& protectedPages = ExecutableProtectedPages();
for (const auto& range : ExecutableRanges()) {
// Only pages fully inside the range are protected: edge pages often share a page with data
@@ -497,6 +510,10 @@ bool IsActive() {
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard<std::mutex> lock(StateMutex());
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
#endif
if (g_initialized) {
if (!SameLayout(g_activeRegions, regions)) {
throw std::runtime_error(
@@ -615,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
}
void ProtectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return;
if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
const uint64_t end = static_cast<uint64_t>(address) + length;
if (end > kGuestSpaceSize) return;
std::lock_guard<std::mutex> lock(StateMutex());
@@ -630,7 +647,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
}
void UnprotectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return;
if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = DeferredRanges();
const uint64_t end = static_cast<uint64_t>(address) + length;
@@ -645,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
}
void RegisterExecutableRange(uint32_t start, uint32_t end) {
if (end <= start) return;
if (RequiresCheckedAccess() || end <= start) return;
std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = ExecutableRanges();
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
+80
View File
@@ -0,0 +1,80 @@
#include "guest_flat_memory.h"
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <vector>
namespace GuestFlat {
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; });
}
int BackingFile(size_t size) {
char name[] = "/tmp/wiicompiled-guest-XXXXXX";
const int fd = mkstemp(name);
if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast<off_t>(size)) != 0) { close(fd); return -1; } }
return fd;
}
} // 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; }
mach_vm_address_t address = kFixedFlatGuestBase;
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase)
throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> 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, -1}); else it->size = std::max(it->size, need);
}
for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); }
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; });
auto* host = static_cast<uint8_t*>(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r)));
auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r));
if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias");
g_mappings.push_back({r.base, r.size, host});
}
for (auto& s : stores) close(s.fd);
g_layout = regions; 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
View File
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
// that happens on another key: unordered_map keeps references valid across a
// rehash, an open-addressed table would not.
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
+9
View File
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
void* GuestToHostPtr(uint32_t addr, size_t len) {
if (addr == 0) return nullptr;
#if defined(__APPLE__)
// The macOS flat guest map exposes separate host aliases for cached,
// uncached, and physical MEM1/MEM2 addresses. GX resources are identified
// by their host pointer, so all aliases of one guest allocation must use
// the same physical mapping before they reach Aurora. Kept macOS-only:
// changing which alias the other hosts hand out would re-key their existing
// GX resource identity.
addr = CanonicalizeGxMainRamAddress(addr);
#endif
try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; }
}
+234 -8
View File
@@ -1,23 +1,249 @@
#include "hle_stubs.h"
#include "memory.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
// WPADCLStatus (buttons, sticks and triggers of the extension).
namespace {
constexpr uint32_t kKpadStatusSize = 0x84;
// KPADStatus field offsets (RVL SDK).
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
kFsAccSpeed = 0x78;
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
kClLTrigger = 0x7C, kClRTrigger = 0x80;
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
constexpr uint32_t kUnifiedSize = 0x38;
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
constexpr uint8_t kDevCore = 0;
constexpr uint8_t kDevFreestyle = 1;
constexpr uint8_t kDevClassic = 2;
constexpr uint8_t kFmtCoreAccDpd = 2;
constexpr uint8_t kFmtFreestyleAccDpd = 5;
constexpr uint8_t kFmtClassicAccDpd = 8;
constexpr int8_t kWpadErrNone = 0;
constexpr int8_t kWpadErrNoController = -1;
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
constexpr float kRawAccZero = 512.0f;
constexpr float kRawAccPerG = 100.0f;
struct ChannelState {
uint32_t prevHold = 0;
uint32_t prevClHold = 0;
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
};
std::array<ChannelState, 4> g_channels{};
// Euclidean length of a 3-vector.
float Length(const float* v) {
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
// Euclidean distance between two 3-vectors.
float Distance(const float* a, const float* b) {
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
return Length(d);
}
// Writes three big-endian floats to guest memory.
void WriteVec3(uint32_t addr, const float* v) {
Memory::WriteFloat32(addr, v[0]);
Memory::WriteFloat32(addr + 4, v[1]);
Memory::WriteFloat32(addr + 8, v[2]);
}
// Zeroes `count` consecutive floats in guest memory.
void WriteZeroFloats(uint32_t addr, uint32_t count) {
for (uint32_t i = 0; i < count; ++i) {
Memory::WriteFloat32(addr + i * 4, 0.0f);
}
}
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
// or writes an "unplugged" status and returns 0.
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
ChannelState& state = g_channels[chan];
if (sample == nullptr) {
state = {};
Memory::Write32(addr + kHold, 0);
Memory::Write32(addr + kTrig, 0);
Memory::Write32(addr + kRelease, 0);
Memory::Write8(addr + kDevType, kDevCore);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kDpdValidFg, 0);
return 0;
}
const uint32_t hold = sample->hold;
Memory::Write32(addr + kHold, hold);
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
state.prevHold = hold;
WriteVec3(addr + kAcc, sample->acc);
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
// the game treats as "pointing away from the screen".
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
Memory::Write8(addr + kDpdValidFg, 0);
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
const uint8_t dataFormat =
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
Memory::Write8(addr + kDevType, devType);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
Memory::Write8(addr + kDataFormat, dataFormat);
if (sample->hasClassic) {
const uint32_t clHold = sample->clHold;
Memory::Write32(addr + kClHold, clHold);
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
state.prevClHold = clHold;
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
} else if (sample->hasNunchuk) {
state.prevClHold = 0;
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
} else {
state.prevClHold = 0;
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
}
return 1;
}
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
uint16_t RawAcc(float g) {
const float raw = kRawAccZero + g * kRawAccPerG;
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
}
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
// Controller tail, then the data format.
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
Memory::Write32(addr + offset, 0);
}
if (sample == nullptr) {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
return;
}
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
for (uint32_t i = 0; i < 4; ++i) {
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
}
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
if (sample->hasClassic) {
Memory::Write8(addr + kUDev, kDevClassic);
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
} else if (sample->hasNunchuk) {
Memory::Write8(addr + kUDev, kDevFreestyle);
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
Memory::Write8(addr + kUFsStickX,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
Memory::Write8(addr + kUFsStickY,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
} else {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
}
}
} // namespace
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{
(void)chan;
(void)statusPtr;
(void)count;
return 0;
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
return 0;
}
WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
} catch (const Memory::AccessViolation&) {
return 0;
}
}
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
(chan, statusPtr, count));
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
// reads the Classic Controller's buttons, sticks and triggers from here. The
// SDK fills `count` entries with the channel's recent samples (the game asks for
// as many as it asked KPADRead for and looks at entry 0); with one sample per
// frame here, every entry gets the current one.
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{
(void)chan;
(void)statusPtr;
(void)count;
return 0;
constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
return 0;
}
WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
const uint32_t entries = std::min(count, kMaxEntries);
for (uint32_t i = 0; i < entries; ++i) {
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
}
} catch (const Memory::AccessViolation&) {
return 0;
}
return have ? 1 : 0;
}
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
+8
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h"
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <cstdio>
@@ -37,6 +38,7 @@ extern "C" uint32_t PAD__Init_HLE()
}
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
// PADRead: gathers every GameCube pad source for the frame and writes the statuses to guest memory.
extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
{
if (statusPtr == 0) {
@@ -44,7 +46,13 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
}
PADStatus statuses[PAD_CHANMAX]{};
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
WiiRemoteInput::Poll();
uint32_t rumbleMask = PADRead(statuses);
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
// applies while input is blocked (overlay open) so the port does not flip
// between "connected" and "no controller" every time the overlay toggles.
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
+24
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h"
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <cstdint>
@@ -98,18 +99,41 @@ extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
}
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
// WPADSetDataFormat: records the per-channel data format the game asked for.
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
{
return g_state.contract.SetDataFormat(chan, format);
}
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
{
if (chan >= WpadContract::kChannelCount) {
return WpadContract::kErrorBadChannel;
}
// Drive the rescan state machine here too: a reconnect probe can arrive
// before the next PADRead, and only Poll() brings a dropped remote back.
WiiRemoteInput::Poll();
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
// Controller. The game reads the type from here (not from
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
// changes, which is what makes an extension swap mid-game work like on the
// console. EffectiveKind keeps the last type through SDL's re-creation of
// the joystick after a swap.
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
if (WiiRemoteInput::IsRemoteChannel(chan)) {
if (typePtr != 0) {
uint32_t type = WpadContract::kExtensionCore;
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
Memory::Write32(typePtr, type);
}
return kStatusOk;
}
if (typePtr != 0) {
Memory::Write32(typePtr, WpadContract::kExtensionCore);
}
+18 -6
View File
@@ -270,7 +270,13 @@ static int32_t EnsureSslCredentials(SslSession& ssl) {
SCHANNEL_CRED cred{};
cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_MANUAL_CRED_VALIDATION;
// Leave certificate validation to Schannel. SCH_CRED_MANUAL_CRED_VALIDATION
// suppresses that validation and requires an explicit CertGetCertificateChain
// implementation, which this HLE does not provide. The target hostname passed
// to InitializeSecurityContextA below is therefore checked together with the
// server certificate chain.
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS |
SCH_CRED_AUTO_CRED_VALIDATION;
TimeStamp expiry{};
const SECURITY_STATUS status = AcquireCredentialsHandleA(
@@ -289,6 +295,13 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
return SSL_OK;
}
// Schannel can authenticate a certificate chain without authenticating a
// server identity when no target name is supplied. Refuse that ambiguous
// mode rather than accepting a certificate for an unrelated endpoint.
if (ssl.hostname.empty()) {
return SSL_ERR_VCOMMONNAME;
}
const int32_t credRet = EnsureSslCredentials(ssl);
if (credRet != SSL_OK) {
return credRet;
@@ -331,7 +344,7 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
const SECURITY_STATUS status = InitializeSecurityContextA(
&ssl.cred, ssl.haveContext ? &ssl.context : nullptr,
ssl.hostname.empty() ? nullptr : const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP,
const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP,
inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry);
if (status != SEC_E_INVALID_HANDLE) {
ssl.haveContext = true;
@@ -567,10 +580,9 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
switch (cmd) {
case IOCTLV_NET_SSL_NEW: {
// out[0] carries the verify option. Its value is not consulted - every
// certificate check is delegated to Schannel with
// SCH_CRED_MANUAL_CRED_VALIDATION - but it is still read so that a
// request pointing outside guest memory faults here as it always has.
// out[0] carries the guest verify option. Host TLS verification is
// always enforced by Schannel, but still read this value so a request
// pointing outside guest memory faults here as it always has.
if (!out.empty() && out[0].address && out[0].size >= 4) {
(void)Memory::Read32(out[0].address);
}
+63 -2
View File
@@ -4,6 +4,7 @@
#include "nand_internal.h"
#include "discord_presence.h"
#include "runtime_log.h"
extern "C" void OSSleepThread_HLE_801aa9b8(CpuContext* ctx);
@@ -201,9 +202,69 @@ static int32_t HandleDolphinIoctlv(uint32_t cmd, uint32_t numIn, uint32_t numOut
}
case DOLPHIN_IOCTL_SET_SPEED_LIMIT:
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT:
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE:
return ISFS_OK;
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT: {
if (numIn != 1 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
const IosVector client = ReadIosVector(vectorPtr, 0);
if (!IsValidGuestRange(client.address, client.size)) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::SetClient(ReadGuestCString(client.address, client.size));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE: {
if (numIn != 10 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
std::array<IosVector, 10> values{};
for (uint32_t index = 0; index < values.size(); ++index) {
values[index] = ReadIosVector(vectorPtr, index);
if (!IsValidGuestRange(values[index].address, values[index].size)) {
return ISFS_EINVAL;
}
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Activity activity;
activity.details = ReadGuestCString(values[0].address, values[0].size);
activity.state = ReadGuestCString(values[1].address, values[1].size);
activity.largeImageKey = ReadGuestCString(values[2].address, values[2].size);
activity.largeImageText = ReadGuestCString(values[3].address, values[3].size);
activity.smallImageKey = ReadGuestCString(values[4].address, values[4].size);
activity.smallImageText = ReadGuestCString(values[5].address, values[5].size);
if (values[6].size >= 8 && Memory::Contains(values[6].address, 8)) {
activity.startTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[6].address)) << 32) |
Memory::Read32(values[6].address + 4));
}
if (values[7].size >= 8 && Memory::Contains(values[7].address, 8)) {
activity.endTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[7].address)) << 32) |
Memory::Read32(values[7].address + 4));
}
if (values[8].size >= 4) {
activity.partySize = Memory::Read32(values[8].address);
}
if (values[9].size >= 4) {
activity.partyMax = Memory::Read32(values[9].address);
}
DiscordPresence::SetActivity(std::move(activity));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_RESET:
if (numIn != 0 || numOut != 0) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Reset();
}
return ISFS_OK;
case DOLPHIN_IOCTL_GET_SYSTEM_TIME: {
+3 -5
View File
@@ -6,6 +6,7 @@
#include "aurora_events.h"
#include "settings_overlay.h"
#include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h"
#include "vr/mkw_vr_policy.h"
#include "vr/openxr_integration.h"
@@ -22,18 +23,15 @@
#include <mutex>
#include <thread>
#include <aurora/aurora.h>
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
#include <aurora/aurora.h>
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
+270
View File
@@ -0,0 +1,270 @@
#include "host_context.h"
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__APPLE__) && defined(__aarch64__)
#include <sys/mman.h>
#include <unistd.h>
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__)
#include <libco.h>
#include <cstdlib>
#include <unordered_map>
#else
#error "HostContext needs a supported cooperative-context backend"
#endif
namespace HostContext {
#if defined(_WIN32)
namespace {
thread_local bool g_convertedScheduler = false;
}
bool InitializeScheduler(Handle* scheduler)
{
void* context = ConvertThreadToFiber(nullptr);
g_convertedScheduler = context != nullptr;
if (!context) {
context = GetCurrentFiber();
}
*scheduler = context;
return context != nullptr;
}
void ShutdownScheduler(Handle scheduler)
{
if (scheduler && g_convertedScheduler) {
ConvertFiberToThread();
}
g_convertedScheduler = false;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
return CreateFiber(stackSize, entry, argument);
}
void Destroy(Handle context)
{
if (context) {
DeleteFiber(context);
}
}
bool IsCurrent(Handle context)
{
return context != nullptr && GetCurrentFiber() == context;
}
void Switch(Handle target)
{
SwitchToFiber(target);
}
#elif defined(__APPLE__) && defined(__aarch64__)
namespace {
struct Context {
void* savedStackPointer = nullptr;
void* stack = nullptr;
std::size_t stackSize = 0;
};
// Guest scheduling is confined to the initialized main host thread. Keeping
// this as ordinary process state also avoids relying on Darwin TLS internals
// while executing on a manually managed stack.
Context* g_current = nullptr;
}
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
g_current = context;
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
const std::size_t guardSize = static_cast<std::size_t>(getpagesize());
const std::size_t totalSize = stackSize + guardSize;
context->stack = mmap(nullptr, totalSize, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (context->stack == MAP_FAILED) {
delete context;
return nullptr;
}
// Fault on stack overflow instead of corrupting the preceding mapping.
if (mprotect(context->stack, guardSize, PROT_NONE) != 0) {
munmap(context->stack, totalSize);
delete context;
return nullptr;
}
context->stackSize = totalSize;
auto* stackTop = static_cast<char*>(context->stack) + totalSize;
context->savedStackPointer = mkw_co_init(stackTop, entry, argument);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
if (nativeContext->stack) {
munmap(nativeContext->stack, nativeContext->stackSize);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
mkw_co_switch(&destination->savedStackPointer, &source->savedStackPointer);
g_current = source;
}
#elif defined(__linux__)
namespace {
struct Context {
cothread_t native = nullptr;
Entry entry = nullptr;
void* argument = nullptr;
bool ownsNative = false;
};
thread_local Context* g_current = nullptr;
thread_local std::unordered_map<cothread_t, Context*> g_contexts;
void ContextEntry()
{
const auto found = g_contexts.find(co_active());
if (found == g_contexts.end() || !found->second || !found->second->entry) {
std::abort();
}
Context* context = found->second;
g_current = context;
context->entry(context->argument);
// A guest fiber must return through FiberProc's scheduler handoff. There
// is no valid native caller to return to from libco's entry trampoline.
std::abort();
}
} // namespace
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
context->native = co_active();
if (!context->native) {
delete context;
return false;
}
g_current = context;
g_contexts.emplace(context->native, context);
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (!context) {
return;
}
g_contexts.erase(context->native);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
context->entry = entry;
context->argument = argument;
context->native = co_create(static_cast<unsigned int>(stackSize), ContextEntry);
context->ownsNative = context->native != nullptr;
if (!context->native) {
delete context;
return nullptr;
}
g_contexts.emplace(context->native, context);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
g_contexts.erase(nativeContext->native);
if (nativeContext->ownsNative) {
co_delete(nativeContext->native);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
co_switch(destination->native);
g_current = source;
}
#endif
} // namespace HostContext
+60 -2
View File
@@ -23,6 +23,10 @@
#include <unordered_map>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
@@ -38,7 +42,12 @@
#include <dbghelp.h>
#else
#include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
// include the header, while the arm64 handler does not use it at all.
#include <ucontext.h>
#endif
#include <unistd.h>
#endif
@@ -49,6 +58,8 @@
#include "system_bridge.h"
#include "ppc_runtime.h"
#include "aurora_events.h"
#include "wii_remote_input.h"
#include "discord_presence.h"
#include "fiber_manager.h"
#include "hle_stubs.h"
#include "runtime_config.h"
@@ -922,6 +933,7 @@ constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
// AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it.
// We must let ASan's handler run so it can print file/line information.
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
void ReportStructuredException(EXCEPTION_POINTERS* info) {
if (!info || !info->ExceptionRecord) {
@@ -1023,13 +1035,34 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
return EXCEPTION_CONTINUE_SEARCH;
}
// Software-raised exceptions (customer bit set) are used for internal control flow by
// system DLLs (e.g. msxml6 while mscms parses a display colour profile) and are caught
// by their own frame handlers. Only hardware faults are fatal at first chance; anything
// else that truly goes unhandled reaches UnhandledSehFilter.
if ((info->ExceptionRecord->ExceptionCode & 0x20000000u) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG WINAPI UnhandledSehFilter(EXCEPTION_POINTERS* info) {
if (info == nullptr || info->ExceptionRecord == nullptr) {
return EXCEPTION_CONTINUE_SEARCH;
}
const DWORD code = info->ExceptionRecord->ExceptionCode;
if (code == kCppExceptionCodeGcc || code == kCppExceptionCodeMsvc || code == kAsanFatalAppExit) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
// Guard against re-entrancy: if we crash while reporting, don't recurse
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
if (s_inCrashHandler.test_and_set()) {
std::_Exit(EXIT_FAILURE);
}
// Report the structured exception with detailed information
ReportStructuredException(info);
const auto* record = info->ExceptionRecord;
@@ -1064,6 +1097,7 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
void InstallSehLogger() {
if (!g_vectoredSehHandle) {
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
SetUnhandledExceptionFilter(UnhandledSehFilter);
}
}
#else
@@ -1269,6 +1303,7 @@ static void TerminateHandler() {
std::_Exit(EXIT_FAILURE);
}
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
int RuntimeMain(int argc, char** argv) {
// Must run before the transcript duplicates stdout/stderr: it decides what
// those descriptors are mirrored to now that the products are GUI-subsystem.
@@ -1293,6 +1328,9 @@ int RuntimeMain(int argc, char** argv) {
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
}
RuntimeConfigFile::LogLoadedConfig();
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
}
SystemBridge::Initialize();
TranslatedFunctionRegistry::Finalize();
@@ -1344,9 +1382,21 @@ int RuntimeMain(int argc, char** argv) {
const char* configName;
AuroraBackend backend;
};
#if defined(__APPLE__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL},
}};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN},
}};
#elif defined(_WIN32)
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
}};
#endif
const auto backendDisplayName = [](AuroraBackend value) -> const char* {
for (const auto& entry : kGraphicsBackends) {
if (entry.backend == value) {
@@ -1375,6 +1425,11 @@ int RuntimeMain(int argc, char** argv) {
}
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
// visible on that first scan.
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
@@ -1441,6 +1496,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
DiscordPresence::Shutdown();
SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript();
return 0;
@@ -1459,6 +1515,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript();
return 1;
} catch (const std::exception& ex) {
@@ -1471,6 +1528,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript();
return 1;
}
+85
View File
@@ -0,0 +1,85 @@
#include "platform/host_platform.h"
#include <cstdlib>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#else
#include <unistd.h>
#endif
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#include <pwd.h>
#endif
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
#if defined(_WIN32)
std::wstring buffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return std::nullopt;
}
if (length < buffer.size() - 1) {
buffer.resize(length);
return std::filesystem::path(buffer).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
uint32_t size = 0;
if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) {
return std::nullopt;
}
std::string path(size, '\0');
if (_NSGetExecutablePath(path.data(), &size) != 0) {
return std::nullopt;
}
path.resize(std::char_traits<char>::length(path.c_str()));
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#else
return std::nullopt;
#endif
}
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName) {
#if defined(_WIN32)
PWSTR rawPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &rawPath)) && rawPath) {
const std::filesystem::path directory = std::filesystem::path(rawPath) / applicationName;
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
if (const char* home = std::getenv("HOME"); home && *home) {
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
}
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
return std::filesystem::path(user->pw_dir) / "Library" / "Application Support" / applicationName;
}
#endif
return std::filesystem::current_path() / applicationName;
}
std::filesystem::path LogDirectory(std::string_view applicationName) {
return ApplicationDataDirectory(applicationName) / "Logs";
}
uint64_t CurrentProcessId() noexcept {
#if defined(_WIN32)
return static_cast<uint64_t>(::GetCurrentProcessId());
#else
return static_cast<uint64_t>(::getpid());
#endif
}
} // namespace RuntimePlatform
+59
View File
@@ -0,0 +1,59 @@
.text
.align 2
// AArch64 Darwin cooperative context frame (240 bytes): x18-x30, then v8-v15.
// x18 is platform-reserved on Darwin and is needed by code that accesses TLS.
// x0 = address holding the target frame pointer; x1 = address to receive the
// current frame pointer. This is intentionally leaf-only: it never calls C++.
.globl _mkw_co_switch
_mkw_co_switch:
sub sp, sp, #240
str x18, [sp, #0]
stp x19, x20, [sp, #16]
stp x21, x22, [sp, #32]
stp x23, x24, [sp, #48]
stp x25, x26, [sp, #64]
stp x27, x28, [sp, #80]
stp x29, x30, [sp, #96]
stp q8, q9, [sp, #112]
stp q10, q11, [sp, #144]
stp q12, q13, [sp, #176]
stp q14, q15, [sp, #208]
mov x2, sp
str x2, [x1]
ldr x2, [x0]
mov sp, x2
ldr x18, [sp, #0]
ldp x19, x20, [sp, #16]
ldp x21, x22, [sp, #32]
ldp x23, x24, [sp, #48]
ldp x25, x26, [sp, #64]
ldp x27, x28, [sp, #80]
ldp x29, x30, [sp, #96]
ldp q8, q9, [sp, #112]
ldp q10, q11, [sp, #144]
ldp q12, q13, [sp, #176]
ldp q14, q15, [sp, #208]
add sp, sp, #240
ret
// Creates a frame compatible with mkw_co_switch and returns its saved SP.
// x0 = one-past-end stack pointer, x1 = entry(void*), x2 = entry argument.
.globl _mkw_co_init
_mkw_co_init:
bic x0, x0, #0xf
sub x0, x0, #240
str x18, [x0, #0] // Darwin platform register / TLS base
str x1, [x0, #16] // x19: entry
str x2, [x0, #24] // x20: argument
str xzr, [x0, #96] // x29
adrp x3, _mkw_co_entry_trampoline@PAGE
add x3, x3, _mkw_co_entry_trampoline@PAGEOFF
str x3, [x0, #104] // x30
ret
_mkw_co_entry_trampoline:
mov x0, x20
blr x19
brk #0
+151
View File
@@ -5,6 +5,7 @@
#include "music_attenuation.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "wii_remote_input.h"
#include <imgui.h>
#include <SDL3/SDL_events.h>
@@ -282,6 +283,15 @@ void ApplyConfiguredMappings() {
if (controllerIndex < 0) {
continue;
}
// The [controller] bindings are positional and shared by every port, so
// they describe whatever pad the user set them up with (usually an Xbox
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
// (A on the east position) that aurora already maps by name; applying
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
// extension never reach the PAD layer: the game reads them through KPAD.)
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
continue;
}
uint32_t count = 0;
if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) {
@@ -313,6 +323,141 @@ void ApplyConfiguredMappings() {
}
}
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(true);
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
void DrawWiiRemoteAccelerometer(uint32_t port) {
ImGui::SeparatorText("Accelerometer");
float sdlG[3] = {};
float kpad[3] = {};
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
} else {
ImGui::TextDisabled("No accelerometer data yet.");
}
// SDL's read of the remote's calibration block often times out over Bluetooth
// and it falls back to a nominal zero point, leaving a small per-axis bias;
// measured here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating()) {
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
WiiRemoteInput::StartAccelCalibration(port);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
"for about two seconds. Corrects the steering offset of a remote held sideways.");
}
ImGui::SameLine();
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
if (ImGui::Button("Clear calibration")) {
WiiRemoteInput::ClearAccelCalibration();
}
ImGui::EndDisabled();
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
ImGui::TextWrapped("%s", message);
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
}
}
// Wii Remotes (Bluetooth) menu: driver switch, pairing help, continuous scanning and the port's controller kind.
void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
if (!ImGui::BeginMenu("Wii Remotes (Bluetooth)")) {
return;
}
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
}
ImGui::TextDisabled("Pairing: Windows Settings > Bluetooth > Add device, then press 1+2");
ImGui::TextDisabled("(or the red SYNC button) on the remote. Leave the PIN empty.");
ImGui::TextDisabled("A remote that was paired before also needs to be turned on with 1+2/SYNC.");
if (ImGui::Checkbox("Keep scanning for Wii Remotes (like Dolphin's Continuous Scanning)",
&g_wiiContinuousScan)) {
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
"remote that dropped out (\"Communications with the controller have been\n"
"interrupted\") or was turned on after launch comes back by itself.");
}
// The driver hint is only read at launch, so a rescan after the user turned
// the setting off would still re-enumerate Wii devices in this session.
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
if (ImGui::Button("Rescan now")) {
WiiRemoteInput::RescanNow();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (WiiRemoteInput::IsScanning()) {
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
} else {
ImGui::TextDisabled("Not scanning");
}
ImGui::Separator();
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(selectedGamePort);
ImGui::Text("Port %u: %s", static_cast<unsigned>(selectedGamePort + 1), WiiRemoteInput::KindLabel(kind));
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
WiiRemoteInput::KpadSample sample;
if (WiiRemoteInput::ReadKpadSample(selectedGamePort, sample)) {
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
ImGui::TextDisabled("Capitals = held. The game reads this Classic Controller through KPAD, as on the");
ImGui::TextDisabled("console: its buttons mean what the game says they mean, no mapping applies.");
}
}
if (kind == WiiRemoteInput::Kind::WiiUPro) {
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(selectedGamePort))) {
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
// expects a hat, so SDL_GetGamepadButton never sees them; read the
// joystick directly, like the fallback in aurora's PADRead does.
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
const auto rawButton = [&](int index) {
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
};
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
ImGui::TextDisabled("Capitals = held. If a button never turns to capitals while physically held,");
ImGui::TextDisabled("that press is not reaching SDL at all (a driver-level issue, not a mapping one).");
ImGui::TextDisabled("This pad uses Nintendo's own layout (a/b/x/y as labelled); the shared");
ImGui::TextDisabled("button mapping above does not apply to it.");
}
}
if (kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
kind == WiiRemoteInput::Kind::RemoteWithClassic) {
DrawWiiRemoteAccelerometer(selectedGamePort);
}
ImGui::EndMenu();
}
// Controller settings menu: port selection, controller assignment and button mapping.
void DrawControllerSettings() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1);
@@ -332,6 +477,7 @@ void DrawControllerSettings() {
}
ImGui::Separator();
controller_mapping_wizard::DrawSetupList();
DrawWiiRemoteSettings(selectedGamePort);
const uint32_t controllerCount = PADCount();
if (controllerCount == 0) {
ImGui::TextDisabled("No controller connected");
@@ -931,6 +1077,11 @@ 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.
aurora_wait_for_frame_worker();
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
// runs once per presented frame whatever the game is doing (e.g. sitting in
// its "communications interrupted" prompt without polling pads). Same guest
// thread as PADRead, so no concurrent access to the scanner's state.
WiiRemoteInput::Poll();
ApplyConfiguredMappings();
PersistDisplayModeIfChanged();
UpdateCursorAutoHide();
+741
View File
@@ -0,0 +1,741 @@
#include "wii_remote_input.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <dolphin/pad.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_sensor.h>
#include <SDL3/SDL_timer.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace WiiRemoteInput {
namespace {
// Dolphin's continuous scanning polls Bluetooth about once a second; SDL's
// enumeration walks every HID device on the main thread, so stay a bit lazier.
constexpr uint64_t kScanIntervalMs = 2000;
// Right after a remote drops it is almost certainly still there, but the first
// re-open attempts tend to fail on timed-out reads, so retry quickly for a while.
constexpr uint64_t kFastScanIntervalMs = 500;
constexpr uint64_t kFastScanWindowMs = 15000;
// How long the Wii driver hint stays at "0" during a rescan; SDL applies hint
// changes on its next joystick update, once per pumped frame.
constexpr uint64_t kRescanDriverOffMs = 100;
constexpr float kStandardGravity = 9.80665f;
// SDL's Wii driver posts the remote's own buttons as raw joystick buttons
// starting at SDL_GAMEPAD_BUTTON_MISC1, in this order (SDL_hidapi_wii.c,
// EWiiButtons), whatever the extension.
enum RawWiiButton : int {
kRawA = SDL_GAMEPAD_BUTTON_MISC1,
kRawB,
kRawOne,
kRawTwo,
kRawPlus,
kRawMinus,
kRawHome,
kRawDpadUp,
kRawDpadDown,
kRawDpadLeft,
kRawDpadRight,
};
// WPAD_BUTTON_* bits as the game reads them from KPADStatus.hold.
constexpr uint32_t kWpadLeft = 0x0001, kWpadRight = 0x0002, kWpadDown = 0x0004, kWpadUp = 0x0008,
kWpadPlus = 0x0010, kWpadTwo = 0x0100, kWpadOne = 0x0200, kWpadB = 0x0400, kWpadA = 0x0800,
kWpadMinus = 0x1000, kWpadZ = 0x2000, kWpadC = 0x4000, kWpadHome = 0x8000;
// WPAD_CL_BUTTON_* bits (WPADCLStatus.clButton / KPADStatus.ex_status.cl.hold):
// the Classic Controller's two button bytes, inverted, high byte first.
constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x0008, kClA = 0x0010, kClY = 0x0020,
kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800,
kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000;
// How long a vanished remote keeps its channel alive with neutral input. SDL's
// in-place reconnect after an extension change takes well under a second; a
// remote that is really gone shows up as disconnected after this.
constexpr uint64_t kExtensionSwapGraceMs = 3000;
// Rescanning closes and re-opens the Bluetooth HID handle, which some Windows
// stacks answer by dropping the link; leave SDL's own reconnect this long first.
constexpr uint64_t kScanStartDelayMs = 3000;
// Per-port memory of the last Wii controller seen there, for EffectiveKind.
struct PortMemory {
Kind lastKind = Kind::NotWii;
uint64_t lastSeenMs = 0;
};
std::array<PortMemory, PAD_MAX_CONTROLLERS> g_ports{};
bool g_wiiDriverEnabled = false;
uint64_t g_lastScanMs = 0;
uint32_t g_scanCount = 0;
bool g_scanning = false;
// Non-zero while a rescan has the Wii driver hint switched off (see RescanNow).
uint64_t g_driverOffSinceMs = 0;
// When the current scan started (last Wii controller seen).
uint64_t g_lostAtMs = 0;
// Instance ids whose accelerometers have been switched on. SDL keeps sensors
// off until asked and forgets that when the gamepad is closed, so a re-paired
// remote gets a fresh id and is enabled again.
std::array<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
// Zero-point correction subtracted from the remote's accelerometer, in g and in
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
// calibration run. The Nunchuk accelerometer is left uncorrected.
std::array<float, 3> g_accelOffset{};
bool g_accelOffsetLoaded = false;
// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a
// sample may stray from the first one before the run is declared "moved".
constexpr int kCalibrationSamples = 90;
constexpr float kCalibrationMaxDeviationG = 0.15f;
// |mean| outside this range means the remote was not at rest or the
// accelerometer is far off its nominal scale; either way the offset is useless.
constexpr float kCalibrationMinGravityG = 0.8f;
constexpr float kCalibrationMaxGravityG = 1.2f;
struct AccelCalibration {
bool active = false;
uint32_t chan = 0;
int count = 0;
double sum[3] = {};
float first[3] = {};
};
AccelCalibration g_calibration;
char g_calibrationMessage[160] = {};
// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on
// frames that bring no sample or a glitched one, so the game never sees a jump.
struct LastAcc {
bool valid = false;
float acc[3] = {0.0f, -1.0f, 0.0f};
};
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastAcc{};
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastNunchukAcc{};
// Any axis beyond this is not a reading the remote's +-3 g sensor can produce.
constexpr float kMaxPlausibleRemoteG = 4.0f;
// Case-sensitive substring test that tolerates a null name.
bool NameContains(const char* name, const char* needle) {
return name != nullptr && std::strstr(name, needle) != nullptr;
}
// Turns on the remote (and Nunchuk) accelerometers once per gamepad instance.
void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) {
const SDL_JoystickID id = SDL_GetGamepadID(gamepad);
if (g_sensorsEnabledFor[port] == id) {
return;
}
bool allEnabled = true;
for (SDL_SensorType sensor : {SDL_SENSOR_ACCEL, SDL_SENSOR_ACCEL_L}) {
if (SDL_GamepadHasSensor(gamepad, sensor) && !SDL_SetGamepadSensorEnabled(gamepad, sensor, true)) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1)
<< ": could not enable an accelerometer: " << SDL_GetError() << std::endl;
allEnabled = false;
}
}
// Only remember the instance once every sensor is on, so a failed attempt
// is retried on the next sample instead of leaving the accelerometer off.
if (allEnabled) {
g_sensorsEnabledFor[port] = id;
}
}
// Loads the stored zero-point correction once.
const std::array<float, 3>& AccelOffset() {
if (!g_accelOffsetLoaded) {
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
g_accelOffsetLoaded = true;
}
return g_accelOffset;
}
// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet.
bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) {
return SDL_GamepadSensorEnabled(gamepad, sensor) && SDL_GetGamepadSensorData(gamepad, sensor, sdl, 3) &&
std::isfinite(sdl[0]) && std::isfinite(sdl[1]) && std::isfinite(sdl[2]);
}
// True for a sample that cannot have come from the sensor. Over Bluetooth on
// Windows the remote delivers, a few times a minute, a report whose
// accelerometer bytes are all zero; SDL decodes that as -0x200 on every axis,
// i.e. (+5.12, -5.12, -5.12) g for the remote (100 units/g) and
// (+2.56, -2.56, -2.56) g for the Nunchuk (200 units/g). Handed to the game as
// is, one such frame is a full-lock steer plus a 9 g "shake". `g` is the
// uncorrected SDL sample.
bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
// An exact zero vector is SDL's sensor buffer before the first report, not
// a reading (the remote never delivers 0 g on all three axes at once).
if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) {
return true;
}
const float zero = sensor == SDL_SENSOR_ACCEL ? 5.12f : 2.56f;
if (std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f) {
return true;
}
if (sensor == SDL_SENSOR_ACCEL) {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i]) > kMaxPlausibleRemoteG) return true;
}
}
return false;
}
// One accelerometer sample in g, SDL's sensor frame. The remote's own axes are
// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin);
// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of
// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip.
// The remote's sample gets the zero-point correction; the Nunchuk's does not.
// False when there is no sample yet or the sample is a glitch (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, sensor, sdl)) {
return false;
}
for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity;
if (IsGlitchedSample(sensor, g)) {
return false;
}
if (sensor == SDL_SENSOR_ACCEL) {
const std::array<float, 3>& offset = AccelOffset();
for (int i = 0; i < 3; ++i) g[i] -= offset[i];
}
return true;
}
// KPAD's acc is the remote reading as (-wiiX, -wiiZ, wiiY): x right across the
// face, y through the back of the remote (rest: -1 with the buttons up), z
// towards the user. That is SDL's frame with y negated. Held sideways as a
// wheel the rest vector is (1, 0, 0) and a turn shows up as z = sin(angle), so
// the sign of z is the direction of the turn; Cemu does the same conversion for
// real remotes on the Wii U.
void AccelGToKpad(const float* g, float* kpad) {
kpad[0] = g[0];
kpad[1] = -g[1];
kpad[2] = g[2];
}
// Sensor -> KPAD acc for ReadKpadSample.
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* kpad) {
float g[3] = {};
if (!ReadAccelG(gamepad, sensor, g)) {
return false;
}
AccelGToKpad(g, kpad);
return true;
}
// Debug trace of every remote sample (controller.wii_accel_trace = true):
// milliseconds, port, WPAD hold bits, the uncorrected SDL sample in g and the
// KPAD acc handed to the game. One CSV per run, truncated at startup.
void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, bool haveRaw, bool accepted) {
static std::ofstream trace;
static bool opened = false;
if (!opened) {
opened = true;
const std::filesystem::path path = RuntimeConfigFile::ApplicationDataDirectory() / "wii_accel_trace.csv";
trace.open(path, std::ios::trunc);
if (trace) {
trace << "ms,port,hold,raw_x,raw_y,raw_z,ok,kpad_x,kpad_y,kpad_z\n";
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: " << path.string() << std::endl;
} else {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: could not open " << path.string() << std::endl;
}
}
if (!trace) {
return;
}
char line[192];
if (haveRaw) {
std::snprintf(line, sizeof(line), "%llu,%u,%04x,%.4f,%.4f,%.4f,%d,%.4f,%.4f,%.4f\n",
static_cast<unsigned long long>(SDL_GetTicks()), chan + 1, sample.hold, rawG[0], rawG[1],
rawG[2], accepted ? 1 : 0, sample.acc[0], sample.acc[1], sample.acc[2]);
} else {
std::snprintf(line, sizeof(line), "%llu,%u,%04x,,,,0,%.4f,%.4f,%.4f\n",
static_cast<unsigned long long>(SDL_GetTicks()), chan + 1, sample.hold, sample.acc[0],
sample.acc[1], sample.acc[2]);
}
trace << line;
// A frame per line; flush so a crash or a killed process keeps the tail.
trace.flush();
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(const char* message) {
g_calibration.active = false;
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message);
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl;
}
// One frame of a calibration run: accumulates the uncorrected sample and, once
// enough frames are in, stores the mean minus the ideal rest vector (0, 1, 0).
void StepAccelCalibration() {
if (!g_calibration.active) {
return;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
FinishAccelCalibration("Cancelled: the Wii Remote went away.");
return;
}
EnsureSensors(gamepad, g_calibration.chan);
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, sdl)) {
return; // no sample this frame; keep waiting
}
float g[3];
for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity;
if (IsGlitchedSample(SDL_SENSOR_ACCEL, g)) {
return; // a zeroed report, not a movement
}
if (g_calibration.count == 0) {
for (int i = 0; i < 3; ++i) g_calibration.first[i] = g[i];
} else {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) {
FinishAccelCalibration("Failed: the remote moved. Put it down, buttons up, and try again.");
return;
}
}
}
for (int i = 0; i < 3; ++i) g_calibration.sum[i] += g[i];
if (++g_calibration.count < kCalibrationSamples) {
return;
}
std::array<double, 3> mean{};
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
return;
}
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
g_accelOffsetLoaded = true;
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(offset);
char message[160];
std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g",
saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1],
offset[2]);
FinishAccelCalibration(message);
}
// True when any controller aurora knows about is a Wii device.
bool AnyWiiControllerConnected() {
const uint32_t count = PADCount();
for (uint32_t index = 0; index < count; ++index) {
if (KindForName(PADGetNameForControllerIndex(index)) != Kind::NotWii) {
return true;
}
}
return false;
}
// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's
// extension/status messages) into console.log, minus the periodic chatter.
void SDLCALL LogSdlMessage(void*, int category, SDL_LogPriority priority, const char* message) {
if (message == nullptr) {
return;
}
if (category == SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN &&
(std::strstr(message, "Motion Plus") != nullptr || std::strstr(message, "Resetting report mode") != nullptr)) {
return;
}
if (category == SDL_LOG_CATEGORY_INPUT || priority >= SDL_LOG_PRIORITY_WARN) {
RT_LOG("sdl") << message << std::endl;
}
}
// Second half of a rescan: re-enables the Wii driver once SDL has seen it off.
void FinishRescan(uint64_t now) {
if (g_driverOffSinceMs == 0 || now - g_driverOffSinceMs < kRescanDriverOffMs) {
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1");
g_driverOffSinceMs = 0;
g_lastScanMs = now;
++g_scanCount;
// The first few and then every tenth, so a remote that never comes back
// leaves a trail in console.log without flooding it.
if (g_scanCount <= 3 || g_scanCount % 10 == 0) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << g_scanCount << ": HIDAPI Wii driver re-enabled ("
<< PADCount() << " controller(s) known to aurora)" << std::endl;
}
}
} // namespace
// Enables SDL's HIDAPI Wii driver and player LEDs, and routes SDL's input log.
void ConfigureSdlHints(bool enabled) {
// A rescan may be mid-flight; drop its bookkeeping so Poll() is not left
// waiting for a FinishRescan() that can no longer happen.
g_driverOffSinceMs = 0;
if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "1" : "0")) {
RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII << ": " << SDL_GetError()
<< std::endl;
}
// Light the player LED that matches the SDL player index, like the console does.
if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED, "1")) {
RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED << ": "
<< SDL_GetError() << std::endl;
}
RT_LOG(RT_TAG_CONFIG) << "Bluetooth Wii Remote support " << (enabled ? "enabled" : "disabled") << std::endl;
g_wiiDriverEnabled = enabled;
if (enabled) {
SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG);
SDL_SetLogOutputFunction(LogSdlMessage, nullptr);
}
}
// Starts a rescan by disabling the Wii driver hint; Poll() finishes it.
void RescanNow() {
if (!g_wiiDriverEnabled || g_driverOffSinceMs != 0) {
return;
}
// SDL only closes the HID handle of a remote it dropped while the Wii driver
// is disabled, and only re-opens it when the driver is enabled again; both
// must happen on separate joystick updates, so the hint stays at "0" until
// FinishRescan() a few frames later. Flipping 1->0->1 within one frame does
// nothing: SDL only ever sees the final "1".
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "0");
g_driverOffSinceMs = SDL_GetTicks();
if (g_scanCount < 3) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << (g_scanCount + 1) << ": HIDAPI Wii driver disabled"
<< std::endl;
}
}
// Per-frame scanning state machine: rescans while no Wii controller is present.
// Also advances an accelerometer calibration run, which needs a sample per frame
// whether or not the game is reading KPAD at that moment.
void Poll() {
StepAccelCalibration();
// Remember what each port had, so EffectiveKind can bridge a swap.
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
(void)EffectiveKind(port);
}
if (!g_wiiDriverEnabled) {
return;
}
// Always complete a rescan in progress so the driver is never left disabled.
FinishRescan(SDL_GetTicks());
if (g_driverOffSinceMs != 0) {
return;
}
if (AnyWiiControllerConnected()) {
if (g_scanning) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote found after " << g_scanCount << " rescan(s)" << std::endl;
}
g_scanning = false;
g_scanCount = 0;
g_lastScanMs = SDL_GetTicks();
return;
}
if (!RuntimeConfigFile::WiiContinuousScanEnabled(true)) {
g_scanning = false;
return;
}
const uint64_t now = SDL_GetTicks();
if (!g_scanning) {
RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; scanning for one (press 1+2 on the remote)"
<< std::endl;
g_scanning = true;
g_lostAtMs = now;
}
if (now - g_lostAtMs < kScanStartDelayMs) {
return;
}
const uint64_t interval = now - g_lostAtMs < kFastScanWindowMs ? kFastScanIntervalMs : kScanIntervalMs;
if (now - g_lastScanMs < interval) {
return;
}
RescanNow();
}
// True while Poll() is looking for a remote.
bool IsScanning() {
return g_scanning;
}
// Number of rescans since a Wii controller was last seen.
uint32_t ScanCount() {
return g_scanCount;
}
// Maps the gamepad name SDL's Wii driver reports to a Kind.
Kind KindForName(const char* name) {
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
// Controller" and "Nintendo Wii U Pro Controller".
if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro;
if (!NameContains(name, "Wii Remote")) return Kind::NotWii;
if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk;
if (NameContains(name, "Classic Controller")) return Kind::RemoteWithClassic;
return Kind::Remote;
}
// Kind of the SDL gamepad assigned to a game port, NotWii when empty.
Kind KindForPort(uint32_t port) {
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
if (gamepad == nullptr) return Kind::NotWii;
return KindForName(SDL_GetGamepadName(gamepad));
}
// Human-readable name of a Kind for the settings overlay.
const char* KindLabel(Kind kind) {
switch (kind) {
case Kind::Remote: return "Wii Remote";
case Kind::RemoteWithNunchuk: return "Wii Remote + Nunchuk";
case Kind::RemoteWithClassic: return "Wii Remote + Classic Controller";
case Kind::WiiUPro: return "Wii U Pro Controller";
default: return "Not a Wii controller";
}
}
// True for the kinds the game reads through KPAD.
static bool IsKpadKind(Kind kind) {
return kind == Kind::Remote || kind == Kind::RemoteWithNunchuk || kind == Kind::RemoteWithClassic;
}
// Live kind of the port, or the remembered one while a swap is in flight.
// Called from the guest thread only (PADRead, KPADRead, WPADProbe and the
// overlay's Draw all run there), so the port memory needs no locking.
Kind EffectiveKind(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
PortMemory& memory = g_ports[chan];
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const uint64_t now = SDL_GetTicks();
if (live != Kind::NotWii) {
memory.lastKind = live;
memory.lastSeenMs = now;
return live;
}
if (gamepad != nullptr) {
// Another controller took the port: the remote is not coming back here.
memory.lastKind = Kind::NotWii;
return Kind::NotWii;
}
if (IsKpadKind(memory.lastKind) && memory.lastSeenMs != 0 && now - memory.lastSeenMs < kExtensionSwapGraceMs) {
return memory.lastKind;
}
return Kind::NotWii;
}
// True when the game reads the port through KPAD (live or bridging a swap).
bool IsRemoteChannel(uint32_t chan) {
return IsKpadKind(EffectiveKind(chan));
}
// Marks KPAD-served ports as "no controller" in the GameCube pad statuses.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count) {
for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) {
if (IsRemoteChannel(port)) {
statuses[port] = {};
statuses[port].err = PAD_ERR_NO_CONTROLLER;
}
}
}
// Neutral sample of the remembered kind, for the frames of an extension swap.
void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) {
sample = {};
for (int i = 0; i < 3; ++i) sample.acc[i] = g_lastAcc[chan].acc[i];
sample.hasNunchuk = kind == Kind::RemoteWithNunchuk;
sample.hasClassic = kind == Kind::RemoteWithClassic;
}
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
// normalises every Classic Controller stick, whatever the report's resolution,
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
// WPAD.h; wut's WPADStatusClassic documents the same range).
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
float value = static_cast<float>(axis) / 32767.0f;
if (invert) value = -value;
value = std::clamp(value, -1.0f, 1.0f);
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
}
// Samples buttons, accelerometers and the extension of the remote on a port.
bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
if (chan >= PAD_MAX_CONTROLLERS) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
if (!IsKpadKind(kind)) {
const Kind remembered = EffectiveKind(chan);
if (!IsKpadKind(remembered)) {
return false;
}
FillGraceSample(chan, remembered, sample);
return true;
}
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
if (joystick == nullptr) {
return false;
}
EnsureSensors(gamepad, chan);
sample = {};
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
float rawG[3] = {};
const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG);
for (float& v : rawG) v /= kStandardGravity;
LastAcc& last = g_lastAcc[chan];
const bool accepted = ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL, sample.acc);
if (accepted) {
last.valid = true;
for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i];
} else {
// No sample this frame or a glitched one: repeat the last good reading
// (rest pose, buttons up, until there is one).
for (int i = 0; i < 3; ++i) sample.acc[i] = last.acc[i];
}
if (RuntimeConfigFile::WiiAccelTraceEnabled(false)) {
TraceSample(chan, sample, rawG, haveRaw, accepted);
}
if (kind == Kind::RemoteWithClassic) {
sample.hasClassic = true;
// The driver posts the extension's buttons as joystick buttons numbered
// by SDL_GAMEPAD_BUTTON_*: a/b/x/y by position (a on the east), +/-,
// Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never
// through SDL's gamepad mapping, which expects a hat), ZL/ZR as the
// trigger axes.
const auto cl = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.clHold |= bit;
};
cl(SDL_GAMEPAD_BUTTON_EAST, kClA);
cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB);
cl(SDL_GAMEPAD_BUTTON_NORTH, kClX);
cl(SDL_GAMEPAD_BUTTON_WEST, kClY);
cl(SDL_GAMEPAD_BUTTON_START, kClPlus);
cl(SDL_GAMEPAD_BUTTON_BACK, kClMinus);
cl(SDL_GAMEPAD_BUTTON_GUIDE, kClHome);
cl(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, kClL);
cl(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, kClR);
cl(SDL_GAMEPAD_BUTTON_DPAD_UP, kClUp);
cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown);
cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft);
cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight);
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
// Only the full-press click of L/R reaches SDL; report it as a full pull.
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
}
if (kind == Kind::RemoteWithNunchuk) {
sample.hasNunchuk = true;
if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) sample.hold |= kWpadC;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.hold |= kWpadZ;
sample.stick[0] = static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX)) / 32767.0f;
// SDL's y grows downwards; KPAD's stick y is up-positive.
sample.stick[1] = -static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f;
for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f);
LastAcc& lastNunchuk = g_lastNunchukAcc[chan];
if (ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
lastNunchuk.valid = true;
for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i];
} else {
for (int i = 0; i < 3; ++i) sample.nunchukAcc[i] = lastNunchuk.acc[i];
}
}
return true;
}
// Corrected SDL sample and KPAD vector of the remote on a port, for the overlay.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
if (!IsRemoteChannel(chan)) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
if (gamepad == nullptr) {
return false;
}
EnsureSensors(gamepad, chan);
if (!ReadAccelG(gamepad, SDL_SENSOR_ACCEL, sdlG)) {
return false;
}
AccelGToKpad(sdlG, kpadAcc);
return true;
}
// Begins collecting rest samples from the remote on `chan`.
void StartAccelCalibration(uint32_t chan) {
if (!IsRemoteChannel(chan)) {
FinishAccelCalibration("No Wii Remote on this port.");
return;
}
g_calibration = {};
g_calibration.active = true;
g_calibration.chan = chan;
g_calibrationMessage[0] = '\0';
}
// Drops the stored correction and goes back to SDL's raw reading.
void ClearAccelCalibration() {
g_calibration.active = false;
g_accelOffset = {};
g_accelOffsetLoaded = true;
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
}
// True while a calibration run is collecting samples.
bool IsAccelCalibrating() {
return g_calibration.active;
}
// Share of the calibration samples collected so far, 0..1; 0 when idle.
float AccelCalibrationProgress() {
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
}
// Outcome of the last calibration run for the overlay, or nullptr before any.
const char* AccelCalibrationMessage() {
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
}
} // namespace WiiRemoteInput
+49
View File
@@ -0,0 +1,49 @@
#include "host_context.h"
#include <cstdlib>
namespace {
// The worker yields twice; each return to the scheduler must preserve both
// context identities and the worker's continuation point.
HostContext::Handle g_scheduler = nullptr;
HostContext::Handle g_worker = nullptr;
int g_steps = 0;
void Worker(void*)
{
if (!HostContext::IsCurrent(g_worker)) {
std::abort();
}
++g_steps;
HostContext::Switch(g_scheduler);
if (!HostContext::IsCurrent(g_worker)) {
std::abort();
}
++g_steps;
HostContext::Switch(g_scheduler);
}
} // namespace
int main()
{
if (!HostContext::InitializeScheduler(&g_scheduler) ||
!HostContext::IsCurrent(g_scheduler)) {
return 1;
}
g_worker = HostContext::Create(64 * 1024, Worker, nullptr);
if (!g_worker) {
return 1;
}
HostContext::Switch(g_worker);
if (g_steps != 1 || !HostContext::IsCurrent(g_scheduler)) {
return 1;
}
HostContext::Switch(g_worker);
if (g_steps != 2 || !HostContext::IsCurrent(g_scheduler)) {
return 1;
}
HostContext::Destroy(g_worker);
HostContext::ShutdownScheduler(g_scheduler);
}
+46
View File
@@ -0,0 +1,46 @@
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
namespace {
// Exercise the raw AArch64 context ABI independently of HostContext so a
// callee-saved-register or stack-frame regression is localized to this layer.
std::array<std::byte, 64 * 1024> g_workerStack{};
void* g_schedulerSp = nullptr;
void* g_workerSp = nullptr;
std::vector<int> g_events;
void Worker(void*) {
g_events.push_back(1);
mkw_co_switch(&g_schedulerSp, &g_workerSp);
g_events.push_back(2);
mkw_co_switch(&g_schedulerSp, &g_workerSp);
std::abort();
}
} // namespace
int main() {
g_workerSp = mkw_co_init(g_workerStack.data() + g_workerStack.size(), Worker, nullptr);
if (!g_workerSp) {
std::cerr << "failed to create AArch64 context frame\n";
return 1;
}
mkw_co_switch(&g_workerSp, &g_schedulerSp);
if (g_events != std::vector<int>{1}) {
std::cerr << "worker did not yield to scheduler\n";
return 1;
}
mkw_co_switch(&g_workerSp, &g_schedulerSp);
if (g_events != std::vector<int>{1, 2}) {
std::cerr << "worker did not resume from saved context\n";
return 1;
}
return 0;
}
@@ -0,0 +1,66 @@
#include "guest_flat_memory.h"
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <unistd.h>
int main() {
GuestFlat::Initialize({
{0x00000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x80000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x10000000u, 0x4000u, GuestFlat::Backing::Mem2},
{0x90000000u, 0x4000u, GuestFlat::Backing::Mem2},
});
if (!GuestFlat::IsActive()) {
std::cerr << "guest address space did not become active\n";
return 1;
}
if (GuestFlat::RequiresCheckedAccess() !=
(static_cast<size_t>(getpagesize()) > GuestFlat::kGuestPageSize)) {
std::cerr << "guest access mode does not reflect the host page size\n";
return 1;
}
auto* mem1Physical = GuestFlat::HostPointer(0x00000000u);
auto* mem1Cached = GuestFlat::HostPointer(0x80000000u);
auto* mem2Physical = GuestFlat::HostPointer(0x10000000u);
auto* mem2Cached = GuestFlat::HostPointer(0x90000000u);
if (!mem1Physical || !mem1Cached || !mem2Physical || !mem2Cached) {
std::cerr << "missing host alias\n";
return 1;
}
if (GuestFlat::HostPointer(0x4000u) != nullptr ||
GuestFlat::HostPointer(0xa0000000u) != nullptr) {
std::cerr << "unmapped guest address resolved to host memory\n";
return 1;
}
mem1Physical[7] = 0x5a;
mem2Cached[9] = 0xa5;
const auto* guest = reinterpret_cast<const uint8_t*>(GuestFlat::kFixedFlatGuestBase);
if (mem1Cached[7] != 0x5a || guest[0x80000007u] != 0x5a ||
mem2Physical[9] != 0xa5 || guest[0x10000009u] != 0xa5) {
std::cerr << "guest aliases are not coherent\n";
return 1;
}
auto* guestWritable = reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase);
guestWritable[0x80000008u] = 0x3c;
guestWritable[0x1000000au] = 0xc3;
if (mem1Physical[8] != 0x3c || mem2Cached[10] != 0xc3) {
std::cerr << "guest writes were not visible through host aliases\n";
return 1;
}
GuestFlat::Initialize({
{0x00000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x80000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x10000000u, 0x4000u, GuestFlat::Backing::Mem2},
{0x90000000u, 0x4000u, GuestFlat::Backing::Mem2},
});
try {
GuestFlat::Initialize({{0x00000000u, 0x8000u, GuestFlat::Backing::Mem1}});
std::cerr << "guest address space accepted a different layout\n";
return 1;
} catch (const std::runtime_error&) {
}
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
#include "platform/host_platform.h"
#include <iostream>
int main() {
if (!RuntimePlatform::ExecutableDirectory()) {
std::cerr << "unable to resolve the current executable directory\n";
return 1;
}
const auto userData = RuntimePlatform::ApplicationDataDirectory("WiiCompiledPlatformPathsTest");
if (userData.filename() != "WiiCompiledPlatformPathsTest") {
std::cerr << "application-data directory lost its application name: " << userData << '\n';
return 1;
}
if (RuntimePlatform::LogDirectory("WiiCompiledPlatformPathsTest") != userData / "Logs") {
std::cerr << "log directory is not derived from application data\n";
return 1;
}
#if defined(__APPLE__)
if (userData.parent_path().filename() != "Application Support" ||
userData.parent_path().parent_path().filename() != "Library") {
std::cerr << "macOS application-data directory is not under ~/Library/Application Support: "
<< userData << '\n';
return 1;
}
#endif
return 0;
}