From 40f62275634774f9b080189d95c9ecee03983c69 Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 12:03:19 -0300 Subject: [PATCH 1/7] Add Wii U port --- .github/workflows/wiiu.yml | 52 +++ CMakeLists.txt | 6 +- assets/wiiu/icon.png | Bin 0 -> 24122 bytes documentation/wiiu-port.md | 56 +++ src/SF_EXE/cmake/configure_platform.cmake | 3 + src/SF_EXE/cmake/configure_presentation.cmake | 6 + src/SF_EXE/cmake/platforms/WiiU.cmake | 49 +++ src/SF_EXE/runtime/game_runtime.cpp | 2 +- src/SF_EXE/runtime/main.cpp | 12 + .../platform/wiiu/application_loop.cpp | 13 + .../presentation/wiiu_surface_presenter.cpp | 127 ++++++ thirdparty/lal/CMakeLists.txt | 19 + thirdparty/lal/lal_wiiu.c | 95 +++++ thirdparty/lwl/CMakeLists.txt | 2 + thirdparty/lwl/lwl_wiiu.c | 390 ++++++++++++++++++ tools/wiiu/build-wuhb.sh | 39 ++ 16 files changed, 868 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/wiiu.yml create mode 100644 assets/wiiu/icon.png create mode 100644 documentation/wiiu-port.md create mode 100644 src/SF_EXE/cmake/platforms/WiiU.cmake create mode 100644 src/SF_EXE/runtime/platform/wiiu/application_loop.cpp create mode 100644 src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp create mode 100644 thirdparty/lal/lal_wiiu.c create mode 100644 thirdparty/lwl/lwl_wiiu.c create mode 100644 tools/wiiu/build-wuhb.sh diff --git a/.github/workflows/wiiu.yml b/.github/workflows/wiiu.yml new file mode 100644 index 00000000..3aceb374 --- /dev/null +++ b/.github/workflows/wiiu.yml @@ -0,0 +1,52 @@ +name: Build Wii U WUHB + +on: + push: + paths: + - '.github/workflows/wiiu.yml' + - 'CMakeLists.txt' + - 'assets/wiiu/**' + - 'src/**' + - 'thirdparty/**' + - 'tools/wiiu/**' + pull_request: + paths: + - '.github/workflows/wiiu.yml' + - 'CMakeLists.txt' + - 'assets/wiiu/**' + - 'src/**' + - 'thirdparty/**' + - 'tools/wiiu/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + container: + image: devkitpro/devkitppc:20260203 + + steps: + - name: Check out the repository + uses: actions/checkout@v6 + + - name: Install Wii U dependencies + run: | + apt-get update + apt-get install -y ninja-build + dkp-pacman -Syu --needed --noconfirm wiiu-dev wiiu-sdl2 + + - name: Build the release WUHB + run: sh tools/wiiu/build-wuhb.sh + + - name: Verify the WUHB + run: test -s build/wiiu/release/OpenShadowFlare.wuhb + + - name: Upload the WUHB + uses: actions/upload-artifact@v4 + with: + name: openshadowflare-wiiu-release + path: build/wiiu/release/OpenShadowFlare.wuhb + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index cd2a878d..840a81b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ project( LANGUAGES C CXX ) -if(NINTENDO_SWITCH) +if(NINTENDO_SWITCH OR WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") set(BUILD_TESTING OFF CACHE BOOL "" FORCE) endif() @@ -16,6 +16,8 @@ option(OPENSHADOWFLARE_BUILD_EXE "Build the portable ShadowFlare executable" ON) if(NINTENDO_SWITCH) set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "nxfb") +elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") + set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "wiiu") else() set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "lgl") endif() @@ -27,7 +29,7 @@ set( ) set_property( CACHE OPENSHADOWFLARE_PRESENTATION_BACKEND - PROPERTY STRINGS lgl nxfb + PROPERTY STRINGS lgl nxfb wiiu ) add_subdirectory(thirdparty/lwl) diff --git a/assets/wiiu/icon.png b/assets/wiiu/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd9310aac35f9584b9a546a9c2c079b704dcf35 GIT binary patch literal 24122 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 JPDHLkV1l)qR15$B literal 0 HcmV?d00001 diff --git a/documentation/wiiu-port.md b/documentation/wiiu-port.md new file mode 100644 index 00000000..bbadb2ef --- /dev/null +++ b/documentation/wiiu-port.md @@ -0,0 +1,56 @@ +# Wii U port + +The Wii U build produces an Aroma-compatible Homebrew Bundle (`.wuhb`). It +does not include retail game data. + +## Build + +Install devkitPro's Wii U toolchain, SDL2 port, and Ninja: + +```bash +sudo dkp-pacman -S --needed wiiu-dev wiiu-sdl2 ninja +``` + +From the repository root, build the bundle: + +```bash +sh tools/wiiu/build-wuhb.sh +``` + +Output: + +```text +build/wiiu/release/OpenShadowFlare.wuhb +``` + +```bash +rm -rf build/wiiu/release +``` + +## Test in Emulator + +```text +sdcard/wiiu/OpenShadowFlare/ShadowFlare/ + SFlare.Cfg + System/ + ... +``` + +Load `build/wiiu/release/OpenShadowFlare.wuhb`. Configure Controller as a **Wii U GamePad**; +a Pro Controller profile does not provide the VPAD input used by this port. + +## Controls + +| GamePad control | Game action | +| --- | --- | +| Touchscreen | Pointer and primary click | +| D-pad | Menu navigation and movement | +| A / Plus | Confirm | +| B / Minus | Back / cancel | +| X | Inventory | +| Y | Map | +| L | Mission list | +| R | Toggle walk/run | +| ZL | Enter `Player` in the name field | + +The current SDL2 presenter mirrors the game to the TV and GamePad. diff --git a/src/SF_EXE/cmake/configure_platform.cmake b/src/SF_EXE/cmake/configure_platform.cmake index 456288fc..3ce48e6f 100644 --- a/src/SF_EXE/cmake/configure_platform.cmake +++ b/src/SF_EXE/cmake/configure_platform.cmake @@ -5,6 +5,9 @@ function(osf_configure_platform target) elseif(NINTENDO_SWITCH) include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Switch.cmake") osf_configure_switch_platform(${target}) + elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/WiiU.cmake") + osf_configure_wiiu_platform(${target}) elseif( WIN32 OR APPLE diff --git a/src/SF_EXE/cmake/configure_presentation.cmake b/src/SF_EXE/cmake/configure_presentation.cmake index 8d324e16..fddfd426 100644 --- a/src/SF_EXE/cmake/configure_presentation.cmake +++ b/src/SF_EXE/cmake/configure_presentation.cmake @@ -12,6 +12,12 @@ function(osf_configure_presentation target) PRIVATE runtime/presentation/switch_surface_presenter.cpp ) + elseif(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "wiiu") + target_sources( + ${target} + PRIVATE + runtime/presentation/wiiu_surface_presenter.cpp + ) else() message(FATAL_ERROR "Unsupported presentation backend: " diff --git a/src/SF_EXE/cmake/platforms/WiiU.cmake b/src/SF_EXE/cmake/platforms/WiiU.cmake new file mode 100644 index 00000000..4f1c6baa --- /dev/null +++ b/src/SF_EXE/cmake/platforms/WiiU.cmake @@ -0,0 +1,49 @@ +function(osf_configure_wiiu_platform target) + target_sources( + ${target} + PRIVATE + runtime/platform/wiiu/application_loop.cpp + ) + target_compile_definitions(${target} PRIVATE OSF_PLATFORM_WIIU) + + set(_osf_wiiu_portlibs "$ENV{DEVKITPRO}/portlibs/wiiu") + find_path( + OSF_WIIU_SDL2_INCLUDE_DIR + NAMES SDL.h + PATHS "${_osf_wiiu_portlibs}/include/SDL2" + NO_DEFAULT_PATH + REQUIRED + ) + find_library( + OSF_WIIU_SDL2_LIBRARY + NAMES SDL2 + PATHS "${_osf_wiiu_portlibs}/lib" + NO_DEFAULT_PATH + REQUIRED + ) + target_include_directories(${target} PRIVATE "${OSF_WIIU_SDL2_INCLUDE_DIR}") + target_link_libraries(${target} PRIVATE "${OSF_WIIU_SDL2_LIBRARY}") + + # The devkitPro Wii U CMake wrapper supplies WUT's Cafe OS import libraries + # at the end of the link line. Do not add gx2/vpad/coreinit manually. + + if(COMMAND wut_create_rpx) + wut_create_rpx(${target}) + else() + message(FATAL_ERROR + "wut_create_rpx is unavailable. Configure with " + "$ENV{DEVKITPRO}/portlibs/wiiu/bin/powerpc-eabi-cmake.") + endif() + + if(COMMAND wut_create_wuhb) + wut_create_wuhb( + ${target} + NAME "OpenShadowFlare" + AUTHOR "OpenShadowFlare contributors" + ICON "${PROJECT_SOURCE_DIR}/assets/wiiu/icon.png" + ) + else() + message(WARNING + "wut_create_wuhb is unavailable: only OpenShadowFlare.rpx will be produced.") + endif() +endfunction() diff --git a/src/SF_EXE/runtime/game_runtime.cpp b/src/SF_EXE/runtime/game_runtime.cpp index e84051ae..a0b66997 100644 --- a/src/SF_EXE/runtime/game_runtime.cpp +++ b/src/SF_EXE/runtime/game_runtime.cpp @@ -517,7 +517,7 @@ class Runtime final : public osf::runtime::FrameApplication { int osf::runtime::runGame( const std::filesystem::path& data_root, const GameConfig& game_config, - bool smoke_test) { +bool smoke_test) { auto runtime = std::make_unique(data_root); if (!runtime->initialize(game_config)) { return 1; diff --git a/src/SF_EXE/runtime/main.cpp b/src/SF_EXE/runtime/main.cpp index 947875c5..acaf815e 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -55,6 +55,18 @@ std::filesystem::path findDataRoot() { return std::filesystem::path{}; }; +#ifdef OSF_PLATFORM_WIIU + const std::filesystem::path wiiuSdDataPaths[] = { + "/vol/external01/wiiu/OpenShadowFlare/ShadowFlare", + "/vol/external01/wiiu/apps/OpenShadowFlare/ShadowFlare", + }; + for (const auto& wiiuSdData : wiiuSdDataPaths) { + if (isDataRoot(wiiuSdData)) { + return wiiuSdData; + } + } +#endif + std::error_code error; const std::filesystem::path fromWorkingDirectory = searchParents(std::filesystem::current_path(error)); diff --git a/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp b/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp new file mode 100644 index 00000000..87b27e17 --- /dev/null +++ b/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp @@ -0,0 +1,13 @@ +#include "runtime/application_loop.hpp" + +#include + +namespace osf::runtime { + +int runApplicationLoop(std::unique_ptr application) { + while (WHBProcIsRunning() && application->frame()) { + } + return 0; +} + +} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp new file mode 100644 index 00000000..9a751f80 --- /dev/null +++ b/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp @@ -0,0 +1,127 @@ +#include "runtime/presentation/surface_presenter.hpp" + +#include + +#include +#include +#include + +namespace { + +constexpr std::int32_t kFrameWidth = 640; +constexpr std::int32_t kFrameHeight = 480; + +class WiiUSurfacePresenter final : public osf::runtime::SurfacePresenter { +public: + ~WiiUSurfacePresenter() override { + shutdown(); + } + + bool initialize(LwlWindow* window, std::string* error) override; + void present(osf::gapi::SurfaceView surface) override; + +private: + void shutdown(); + + SDL_Window* window_ = nullptr; + SDL_Renderer* renderer_ = nullptr; + SDL_Texture* texture_ = nullptr; + bool videoInitialized_ = false; +}; + +bool WiiUSurfacePresenter::initialize(LwlWindow* window, std::string* error) { + (void) window; + shutdown(); + + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + if (error) { + *error = std::string("Could not initialize Wii U video: ") + + SDL_GetError(); + } + return false; + } + videoInitialized_ = true; + + window_ = SDL_CreateWindow( + "OpenShadowFlare", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + kFrameWidth, kFrameHeight, SDL_WINDOW_SHOWN); + if (!window_) { + if (error) { + *error = std::string("Could not create the Wii U video surface: ") + + SDL_GetError(); + } + shutdown(); + return false; + } + + renderer_ = SDL_CreateRenderer( + window_, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer_) { + if (error) { + *error = std::string("Could not create the Wii U GPU renderer: ") + + SDL_GetError(); + } + shutdown(); + return false; + } + + texture_ = SDL_CreateTexture( + renderer_, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, + kFrameWidth, kFrameHeight); + if (!texture_) { + if (error) { + *error = std::string("Could not create the Wii U frame texture: ") + + SDL_GetError(); + } + shutdown(); + return false; + } + + if (error) { + error->clear(); + } + return true; +} + +void WiiUSurfacePresenter::present(osf::gapi::SurfaceView surface) { + static_assert( + sizeof(osf::gapi::Color) == sizeof(std::uint32_t), + "GAPI colors must be tightly packed RGBA bytes."); + if (!texture_ || !renderer_ || !surface.pixels || surface.width != kFrameWidth || + surface.height != kFrameHeight) { + return; + } + + SDL_UpdateTexture( + texture_, nullptr, surface.pixels, + kFrameWidth * static_cast(sizeof(*surface.pixels))); + SDL_RenderClear(renderer_); + SDL_RenderCopy(renderer_, texture_, nullptr, nullptr); + SDL_RenderPresent(renderer_); +} + +void WiiUSurfacePresenter::shutdown() { + if (texture_) { + SDL_DestroyTexture(texture_); + texture_ = nullptr; + } + if (renderer_) { + SDL_DestroyRenderer(renderer_); + renderer_ = nullptr; + } + if (window_) { + SDL_DestroyWindow(window_); + window_ = nullptr; + } + if (videoInitialized_) { + SDL_QuitSubSystem(SDL_INIT_VIDEO); + videoInitialized_ = false; + } +} + +} // namespace + +std::unique_ptr +osf::runtime::createSurfacePresenter() { + return std::make_unique(); +} diff --git a/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index 1f9ff88e..8233f335 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -54,6 +54,25 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lal PRIVATE lal_switch.c) +elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") + target_sources(lal PRIVATE lal_wiiu.c) + set(_lal_wiiu_portlibs "$ENV{DEVKITPRO}/portlibs/wiiu") + find_path( + LAL_WIIU_SDL2_INCLUDE_DIR + NAMES SDL.h + PATHS "${_lal_wiiu_portlibs}/include/SDL2" + NO_DEFAULT_PATH + REQUIRED + ) + find_library( + LAL_WIIU_SDL2_LIBRARY + NAMES SDL2 + PATHS "${_lal_wiiu_portlibs}/lib" + NO_DEFAULT_PATH + REQUIRED + ) + target_include_directories(lal PRIVATE "${LAL_WIIU_SDL2_INCLUDE_DIR}") + target_link_libraries(lal PRIVATE "${LAL_WIIU_SDL2_LIBRARY}") elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lal PRIVATE lal_alsa.c) find_package(ALSA REQUIRED) diff --git a/thirdparty/lal/lal_wiiu.c b/thirdparty/lal/lal_wiiu.c new file mode 100644 index 00000000..12c571bc --- /dev/null +++ b/thirdparty/lal/lal_wiiu.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of LAL. + * + * LAL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "lal_internal.h" + +#include + +#include +#include +#include + +static SDL_AudioDeviceID g_device; +static SDL_mutex *g_mutex; +static bool g_audio_initialized; + +static void lal_wiiu_audio_callback( + void *user_data, Uint8 *stream, int byte_count) { + const size_t frame_count = + (size_t) byte_count / (LAL_OUTPUT_CHANNELS * sizeof(int16_t)); + + (void) user_data; + SDL_LockMutex(g_mutex); + lal_mix_frames((int16_t *) stream, frame_count); + SDL_UnlockMutex(g_mutex); +} + +bool lal_platform_init(void) { + SDL_AudioSpec requested; + + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { + lal_set_error("Could not initialize Wii U audio output."); + return false; + } + g_audio_initialized = true; + + g_mutex = SDL_CreateMutex(); + if (!g_mutex) { + lal_set_error("Could not create the Wii U audio mixer mutex."); + lal_platform_shutdown(); + return false; + } + + memset(&requested, 0, sizeof(requested)); + requested.freq = LAL_OUTPUT_SAMPLE_RATE; + requested.format = AUDIO_S16SYS; + requested.channels = LAL_OUTPUT_CHANNELS; + requested.samples = LAL_BUFFER_FRAMES; + requested.callback = lal_wiiu_audio_callback; + + g_device = SDL_OpenAudioDevice( + NULL, 0, &requested, NULL, 0); + if (g_device == 0) { + lal_set_error("Could not open the Wii U audio playback device."); + lal_platform_shutdown(); + return false; + } + + SDL_PauseAudioDevice(g_device, 0); + return true; +} + +void lal_platform_shutdown(void) { + if (g_device != 0) { + SDL_CloseAudioDevice(g_device); + g_device = 0; + } + if (g_mutex) { + SDL_DestroyMutex(g_mutex); + g_mutex = NULL; + } + if (g_audio_initialized) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + g_audio_initialized = false; + } +} + +void lal_platform_lock(void) { + if (g_mutex) { + SDL_LockMutex(g_mutex); + } +} + +void lal_platform_unlock(void) { + if (g_mutex) { + SDL_UnlockMutex(g_mutex); + } +} diff --git a/thirdparty/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index f27651fc..3e107bea 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -64,6 +64,8 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lwl PRIVATE lwl_switch.c) +elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") + target_sources(lwl PRIVATE lwl_wiiu.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lwl PRIVATE lwl_x11.c) find_package(OpenGL REQUIRED) diff --git a/thirdparty/lwl/lwl_wiiu.c b/thirdparty/lwl/lwl_wiiu.c new file mode 100644 index 00000000..04f993f8 --- /dev/null +++ b/thirdparty/lwl/lwl_wiiu.c @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of LWL. + * + * LWL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +/* Wii U GamePad backend: touchscreen maps directly to the game pointer. */ + +#include "lwl.h" + +#include +#include +#include +#include + +#include +#include +#include + +/* SDL stretches the 640x480 game texture to the 854x480 GamePad output. */ +#define LWL_WIIU_WIDTH 640 +#define LWL_WIIU_HEIGHT 480 +#define LWL_WIIU_GAMEPAD_WIDTH 854 + +struct LwlWindow { + int width; + int height; +}; + +static struct LwlWindow g_window; + +enum { LWL_WIIU_EVENT_CAPACITY = 32 }; + +static LwlEvent g_events[LWL_WIIU_EVENT_CAPACITY]; +static int g_event_count; +static int g_event_read; +static bool g_polled_this_frame; +static bool g_vpad_initialized; +static bool g_was_touching; +static int g_pointer_x = LWL_WIIU_WIDTH / 2; +static int g_pointer_y = LWL_WIIU_HEIGHT / 2; + +static int lwl_wiiu_clamp(int value, int low, int high) { + if (value < low) { + return low; + } + if (value > high) { + return high; + } + return value; +} + +static LwlEvent *lwl_wiiu_push(LwlEventType type) { + LwlEvent *event; + if (g_event_count >= LWL_WIIU_EVENT_CAPACITY) { + return NULL; + } + event = &g_events[g_event_count++]; + memset(event, 0, sizeof(*event)); + event->type = type; + return event; +} + +static void lwl_wiiu_push_pointer(LwlEventType type, int button) { + LwlEvent *event = lwl_wiiu_push(type); + if (event) { + event->x = g_pointer_x; + event->y = g_pointer_y; + event->button = button; + event->clicks = 1; + } +} + +static void lwl_wiiu_push_key(LwlEventType type, const char *key) { + LwlEvent *event = lwl_wiiu_push(type); + if (event) { + strncpy(event->key, key, sizeof(event->key) - 1); + } +} + +static void lwl_wiiu_push_text(const char *text) { + LwlEvent *event = lwl_wiiu_push(LWL_EVENT_TEXT_INPUT); + if (event) { + strncpy(event->text, text, sizeof(event->text) - 1); + } +} + +struct LwlWiiUKeyMapping { + uint32_t mask; + const char *key; +}; + +static const struct LwlWiiUKeyMapping g_key_mappings[] = { + {VPAD_BUTTON_UP, "up"}, + {VPAD_BUTTON_DOWN, "down"}, + {VPAD_BUTTON_LEFT, "left"}, + {VPAD_BUTTON_RIGHT, "right"}, + {VPAD_BUTTON_A, "return"}, + {VPAD_BUTTON_PLUS, "return"}, + {VPAD_BUTTON_B, "escape"}, + {VPAD_BUTTON_MINUS, "escape"}, + {VPAD_BUTTON_X, "i"}, + {VPAD_BUTTON_Y, "n"}, + {VPAD_BUTTON_L, "q"}, + {VPAD_BUTTON_R, "r"}, +}; + +static void lwl_wiiu_sample_input(void) { + VPADStatus status; + VPADReadError error; + VPADTouchData touch; + int32_t count; + size_t index; + + g_event_count = 0; + g_event_read = 0; + memset(&status, 0, sizeof(status)); + count = VPADRead(VPAD_CHAN_0, &status, 1, &error); + if (count <= 0 && error == VPAD_READ_UNINITIALIZED) { + VPADInit(); + g_vpad_initialized = true; + count = VPADRead(VPAD_CHAN_0, &status, 1, &error); + } + if (count <= 0 || error != VPAD_READ_SUCCESS) { + return; + } + + if ((status.trigger & VPAD_BUTTON_ZL) != 0u) { + lwl_wiiu_push_text("Player"); + } + + memset(&touch, 0, sizeof(touch)); + VPADGetTPCalibratedPointEx( + VPAD_CHAN_0, VPAD_TP_854X480, &touch, &status.tpNormal); + if (touch.touched != 0u) { + const int x = + lwl_wiiu_clamp((int) touch.x, 0, LWL_WIIU_GAMEPAD_WIDTH - 1) * + LWL_WIIU_WIDTH / LWL_WIIU_GAMEPAD_WIDTH; + const int y = lwl_wiiu_clamp((int) touch.y, 0, LWL_WIIU_HEIGHT - 1); + if (x != g_pointer_x || y != g_pointer_y || !g_was_touching) { + LwlEvent *move = lwl_wiiu_push(LWL_EVENT_MOUSE_MOVE); + if (move) { + move->dx = x - g_pointer_x; + move->dy = y - g_pointer_y; + move->x = x; + move->y = y; + } + g_pointer_x = x; + g_pointer_y = y; + } + if (!g_was_touching) { + lwl_wiiu_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); + g_was_touching = true; + } + } else if (g_was_touching) { + lwl_wiiu_push_pointer(LWL_EVENT_MOUSE_UP, 1); + g_was_touching = false; + } + + for (index = 0; + index < sizeof(g_key_mappings) / sizeof(g_key_mappings[0]); + ++index) { + if ((status.trigger & g_key_mappings[index].mask) != 0u) { + lwl_wiiu_push_key(LWL_EVENT_KEY_DOWN, g_key_mappings[index].key); + } + if ((status.release & g_key_mappings[index].mask) != 0u) { + lwl_wiiu_push_key(LWL_EVENT_KEY_UP, g_key_mappings[index].key); + } + } +} + +bool lwl_init(void) { + WHBProcInit(); + VPADInit(); + g_event_count = 0; + g_event_read = 0; + g_polled_this_frame = false; + g_vpad_initialized = true; + g_was_touching = false; + g_pointer_x = LWL_WIIU_WIDTH / 2; + g_pointer_y = LWL_WIIU_HEIGHT / 2; + return true; +} + +void lwl_shutdown(void) { + if (g_vpad_initialized) { + VPADShutdown(); + g_vpad_initialized = false; + } + WHBProcShutdown(); +} + +LwlWindow *lwl_window_create(const char *title, int width, int height) { + (void) title; + (void) width; + (void) height; + g_window.width = LWL_WIIU_WIDTH; + g_window.height = LWL_WIIU_HEIGHT; + return &g_window; +} + +LwlWindow *lwl_window_create_with_native_message_handler( + const char *title, int width, int height, + LwlNativeMessageHandler handler, void *user_data) { + (void) handler; + (void) user_data; + return lwl_window_create(title, width, height); +} + +LwlWindow *lwl_window_attach_native( + void *native_window, int width, int height) { + (void) native_window; + return lwl_window_create(NULL, width, height); +} + +void *lwl_window_get_native_handle(LwlWindow *window) { + (void) window; + return NULL; +} + +void lwl_window_destroy(LwlWindow *window) { (void) window; } +void lwl_window_show(LwlWindow *window) { (void) window; } +void lwl_window_set_title(LwlWindow *window, const char *title) { + (void) window; + (void) title; +} +void lwl_window_set_mode(LwlWindow *window, LwlWindowMode mode) { + (void) window; + (void) mode; +} +bool lwl_window_has_focus(LwlWindow *window) { + (void) window; + return true; +} +void lwl_window_set_cursor(LwlWindow *window, LwlCursor cursor) { + (void) window; + (void) cursor; +} +bool lwl_window_set_cursor_image( + LwlWindow *window, const LwlColor *pixels, + int width, int height, int hotspot_x, int hotspot_y) { + (void) window; + (void) pixels; + (void) width; + (void) height; + (void) hotspot_x; + (void) hotspot_y; + return false; +} +void lwl_window_set_cursor_visible(LwlWindow *window, bool visible) { + (void) window; + (void) visible; +} +bool lwl_window_set_size(LwlWindow *window, int width, int height) { + (void) window; + (void) width; + (void) height; + return false; +} +void lwl_window_get_size(LwlWindow *window, int *width, int *height) { + if (width) { + *width = window ? window->width : LWL_WIIU_WIDTH; + } + if (height) { + *height = window ? window->height : LWL_WIIU_HEIGHT; + } +} +LwlColor *lwl_window_get_framebuffer( + LwlWindow *window, int *width, int *height) { + (void) window; + if (width) { + *width = 0; + } + if (height) { + *height = 0; + } + return NULL; +} +bool lwl_window_resize_framebuffer( + LwlWindow *window, int width, int height) { + (void) window; + (void) width; + (void) height; + return false; +} +void lwl_window_update_rects( + LwlWindow *window, const LwlRect *rects, int count) { + (void) window; + (void) rects; + (void) count; +} + +bool lwl_poll_event(LwlWindow *window, LwlEvent *event) { + (void) window; + if (!event) { + return false; + } + if (g_event_read < g_event_count) { + *event = g_events[g_event_read++]; + return true; + } + if (g_polled_this_frame) { + g_polled_this_frame = false; + return false; + } + g_polled_this_frame = true; + lwl_wiiu_sample_input(); + if (g_event_read < g_event_count) { + *event = g_events[g_event_read++]; + return true; + } + g_polled_this_frame = false; + return false; +} + +bool lwl_wait_event(LwlWindow *window, double timeout_seconds) { + (void) window; + (void) timeout_seconds; + return false; +} +char *lwl_clipboard_get(LwlWindow *window) { + (void) window; + return NULL; +} +void lwl_clipboard_set(LwlWindow *window, const char *text) { + (void) window; + (void) text; +} +char *lwl_select_folder(LwlWindow *window, const char *title) { + (void) window; + (void) title; + return NULL; +} +void lwl_free(void *ptr) { free(ptr); } + +double lwl_time_seconds(void) { + return (double) OSGetTime() / (double) OSTimerClockSpeed; +} +void lwl_sleep_seconds(double seconds) { + if (seconds > 0.0) { + OSSleepTicks((OSTime) (seconds * (double) OSTimerClockSpeed)); + } +} +void lwl_sleep_until_seconds(double time_seconds) { + const double now = lwl_time_seconds(); + if (time_seconds > now) { + lwl_sleep_seconds(time_seconds - now); + } +} +const char *lwl_platform_name(void) { return "wiiu"; } +double lwl_display_scale(void) { return 1.0; } +bool lwl_exe_path(char *buf, int size) { + (void) buf; + (void) size; + return false; +} +LwlGlConfig lwl_gl_config_default(void) { + LwlGlConfig config; + memset(&config, 0, sizeof(config)); + config.api = LWL_GL_API_ES; + config.major_version = 2; + config.double_buffer = true; + return config; +} +LwlGlContext *lwl_gl_context_create( + LwlWindow *window, const LwlGlConfig *config) { + (void) window; + (void) config; + return NULL; +} +void lwl_gl_context_destroy(LwlGlContext *context) { (void) context; } +bool lwl_gl_context_make_current(LwlGlContext *context) { + (void) context; + return false; +} +void lwl_gl_context_swap_buffers(LwlGlContext *context) { (void) context; } +bool lwl_gl_context_set_swap_interval(LwlGlContext *context, int interval) { + (void) context; + (void) interval; + return false; +} +void *lwl_gl_get_proc_address(const char *name) { + (void) name; + return NULL; +} diff --git a/tools/wiiu/build-wuhb.sh b/tools/wiiu/build-wuhb.sh new file mode 100644 index 00000000..7f086825 --- /dev/null +++ b/tools/wiiu/build-wuhb.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env sh + +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +DEVKITPRO=${DEVKITPRO:-/opt/devkitpro} +WIIU_CMAKE="$DEVKITPRO/portlibs/wiiu/bin/powerpc-eabi-cmake" +BUILD_DIR="$ROOT_DIR/build/wiiu/release" +OUTPUT_WUHB="$BUILD_DIR/OpenShadowFlare.wuhb" + +if [ ! -x "$WIIU_CMAKE" ]; then + echo "The devkitPro Wii U CMake wrapper was not found: $WIIU_CMAKE" >&2 + echo "Install wiiu-dev and set DEVKITPRO if it is not /opt/devkitpro." >&2 + exit 1 +fi + +if ! command -v ninja >/dev/null 2>&1; then + echo "Ninja is required to build the Wii U port." >&2 + exit 1 +fi + +export DEVKITPRO + +"$WIIU_CMAKE" -S "$ROOT_DIR" -B "$BUILD_DIR" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF +cmake --build "$BUILD_DIR" --parallel + +WUHB=$(find "$BUILD_DIR" -type f -name '*.wuhb' ! -path "$OUTPUT_WUHB" -print -quit) +if [ -z "$WUHB" ]; then + echo "CMake did not produce a WUHB under: $BUILD_DIR" >&2 + exit 1 +fi + +if [ "$WUHB" != "$OUTPUT_WUHB" ]; then + cp "$WUHB" "$OUTPUT_WUHB" +fi +echo "WUHB: $OUTPUT_WUHB" From 1c04114d652582381392e7212349a8e37c262f4e Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 12:13:48 -0300 Subject: [PATCH 2/7] fix wii u workflow --- .github/workflows/switch.yml | 6 ++++++ .github/workflows/wiiu.yml | 23 ++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index afbc0f75..1883b689 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -2,6 +2,8 @@ name: Build Nintendo Switch NRO on: push: + branches: + - master paths: - '.github/workflows/switch.yml' - 'CMakeLists.txt' @@ -20,6 +22,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/wiiu.yml b/.github/workflows/wiiu.yml index 3aceb374..0c1b8092 100644 --- a/.github/workflows/wiiu.yml +++ b/.github/workflows/wiiu.yml @@ -2,6 +2,8 @@ name: Build Wii U WUHB on: push: + branches: + - master paths: - '.github/workflows/wiiu.yml' - 'CMakeLists.txt' @@ -22,6 +24,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -32,11 +38,22 @@ jobs: - name: Check out the repository uses: actions/checkout@v6 - - name: Install Wii U dependencies + - name: Install build tools run: | apt-get update - apt-get install -y ninja-build - dkp-pacman -Syu --needed --noconfirm wiiu-dev wiiu-sdl2 + apt-get install -y ninja-build git + + - name: Build the Wii U SDL2 port + run: | + git clone --depth 1 --branch wiiu-sdl2-devel https://github.com/dkosmari/SDL.git /tmp/wiiu-sdl2 + /opt/devkitpro/portlibs/wiiu/bin/powerpc-eabi-cmake \ + -S /tmp/wiiu-sdl2 \ + -B /tmp/wiiu-sdl2/build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/opt/devkitpro/portlibs/wiiu + cmake --build /tmp/wiiu-sdl2/build --parallel + cmake --install /tmp/wiiu-sdl2/build - name: Build the release WUHB run: sh tools/wiiu/build-wuhb.sh From cae0fe5479d6feb75b79a3ade12a92b2e632b902 Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 10:20:00 -0300 Subject: [PATCH 3/7] Fix wiiu surface presenter --- .../presentation/wiiu_surface_presenter.cpp | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp index 9a751f80..17e77977 100644 --- a/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp +++ b/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp @@ -18,7 +18,12 @@ class WiiUSurfacePresenter final : public osf::runtime::SurfacePresenter { } bool initialize(LwlWindow* window, std::string* error) override; - void present(osf::gapi::SurfaceView surface) override; + void prepareFrame(osf::gapi::SurfaceView surface) override; + void displayFrame() override; +#if OSF_ENABLE_DEBUG_TOOLS + std::optional + videoMemoryUsageBytes() const override; +#endif private: void shutdown(); @@ -27,6 +32,7 @@ class WiiUSurfacePresenter final : public osf::runtime::SurfacePresenter { SDL_Renderer* renderer_ = nullptr; SDL_Texture* texture_ = nullptr; bool videoInitialized_ = false; + bool frame_prepared_ = false; }; bool WiiUSurfacePresenter::initialize(LwlWindow* window, std::string* error) { @@ -83,7 +89,8 @@ bool WiiUSurfacePresenter::initialize(LwlWindow* window, std::string* error) { return true; } -void WiiUSurfacePresenter::present(osf::gapi::SurfaceView surface) { +void WiiUSurfacePresenter::prepareFrame(osf::gapi::SurfaceView surface) { + frame_prepared_ = false; static_assert( sizeof(osf::gapi::Color) == sizeof(std::uint32_t), "GAPI colors must be tightly packed RGBA bytes."); @@ -92,13 +99,36 @@ void WiiUSurfacePresenter::present(osf::gapi::SurfaceView surface) { return; } - SDL_UpdateTexture( - texture_, nullptr, surface.pixels, - kFrameWidth * static_cast(sizeof(*surface.pixels))); - SDL_RenderClear(renderer_); - SDL_RenderCopy(renderer_, texture_, nullptr, nullptr); + if (SDL_UpdateTexture( + texture_, nullptr, surface.pixels, + kFrameWidth * + static_cast(sizeof(*surface.pixels))) != 0 || + SDL_RenderClear(renderer_) != 0 || + SDL_RenderCopy(renderer_, texture_, nullptr, nullptr) != 0) { + return; + } + frame_prepared_ = true; +} + +void WiiUSurfacePresenter::displayFrame() { + if (!renderer_ || !frame_prepared_) { + return; + } SDL_RenderPresent(renderer_); + frame_prepared_ = false; +} + +#if OSF_ENABLE_DEBUG_TOOLS +std::optional +WiiUSurfacePresenter::videoMemoryUsageBytes() const { + if (!texture_) { + return std::nullopt; + } + return static_cast(kFrameWidth) * + static_cast(kFrameHeight) * + sizeof(std::uint32_t); } +#endif void WiiUSurfacePresenter::shutdown() { if (texture_) { @@ -117,6 +147,7 @@ void WiiUSurfacePresenter::shutdown() { SDL_QuitSubSystem(SDL_INIT_VIDEO); videoInitialized_ = false; } + frame_prepared_ = false; } } // namespace From cbd007266a7b8b4523b02478cc36b4c80fa61cbe Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 01:01:59 -0300 Subject: [PATCH 4/7] Refactor wiiu SF_EXE port to C99 --- .github/workflows/wiiu.yml | 22 +- CMakeLists.txt | 16 +- documentation/wiiu-port.md | 44 +- shadowflare/CMakeLists.txt | 24 ++ shadowflare/assets/retail_paths.c | 5 + src/SF_EXE/cmake/configure_platform.cmake | 3 - src/SF_EXE/cmake/configure_presentation.cmake | 6 - src/SF_EXE/cmake/platforms/WiiU.cmake | 49 --- src/SF_EXE/runtime/game_runtime.cpp | 2 +- src/SF_EXE/runtime/main.cpp | 12 - .../platform/wiiu/application_loop.cpp | 13 - .../presentation/wiiu_surface_presenter.cpp | 158 ------- thirdparty/lal/CMakeLists.txt | 19 - thirdparty/lal/lal_wiiu.c | 95 ----- thirdparty/lwl/CMakeLists.txt | 2 - thirdparty/lwl/lwl_wiiu.c | 390 ------------------ thirdparty/twl/CMakeLists.txt | 8 + thirdparty/twl/backends/wiiu/backend.h | 50 +++ .../twl/backends/wiiu/content/README.md | 8 + .../backends/wiiu/content/texture_blit.gsh | Bin 0 -> 1176 bytes thirdparty/twl/backends/wiiu/input.c | 33 ++ thirdparty/twl/backends/wiiu/presentation.c | 124 ++++++ .../twl/backends/wiiu/shaders/README.md | 41 ++ .../backends/wiiu/shaders/texture_blit.psh | 18 + .../backends/wiiu/shaders/texture_blit.vsh | 25 ++ thirdparty/twl/backends/wiiu/window.c | 231 +++++++++++ tools/wiiu/build-wuhb.sh | 3 +- 27 files changed, 598 insertions(+), 803 deletions(-) delete mode 100644 src/SF_EXE/cmake/platforms/WiiU.cmake delete mode 100644 src/SF_EXE/runtime/platform/wiiu/application_loop.cpp delete mode 100644 src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp delete mode 100644 thirdparty/lal/lal_wiiu.c delete mode 100644 thirdparty/lwl/lwl_wiiu.c create mode 100644 thirdparty/twl/backends/wiiu/backend.h create mode 100644 thirdparty/twl/backends/wiiu/content/README.md create mode 100644 thirdparty/twl/backends/wiiu/content/texture_blit.gsh create mode 100644 thirdparty/twl/backends/wiiu/input.c create mode 100644 thirdparty/twl/backends/wiiu/presentation.c create mode 100644 thirdparty/twl/backends/wiiu/shaders/README.md create mode 100644 thirdparty/twl/backends/wiiu/shaders/texture_blit.psh create mode 100644 thirdparty/twl/backends/wiiu/shaders/texture_blit.vsh create mode 100644 thirdparty/twl/backends/wiiu/window.c diff --git a/.github/workflows/wiiu.yml b/.github/workflows/wiiu.yml index 0c1b8092..df462a99 100644 --- a/.github/workflows/wiiu.yml +++ b/.github/workflows/wiiu.yml @@ -8,16 +8,18 @@ on: - '.github/workflows/wiiu.yml' - 'CMakeLists.txt' - 'assets/wiiu/**' - - 'src/**' - - 'thirdparty/**' + - 'shadowflare/**' + - 'thirdparty/twl/**' + - 'thirdparty/tal/**' - 'tools/wiiu/**' pull_request: paths: - '.github/workflows/wiiu.yml' - 'CMakeLists.txt' - 'assets/wiiu/**' - - 'src/**' - - 'thirdparty/**' + - 'shadowflare/**' + - 'thirdparty/twl/**' + - 'thirdparty/tal/**' - 'tools/wiiu/**' workflow_dispatch: @@ -43,18 +45,6 @@ jobs: apt-get update apt-get install -y ninja-build git - - name: Build the Wii U SDL2 port - run: | - git clone --depth 1 --branch wiiu-sdl2-devel https://github.com/dkosmari/SDL.git /tmp/wiiu-sdl2 - /opt/devkitpro/portlibs/wiiu/bin/powerpc-eabi-cmake \ - -S /tmp/wiiu-sdl2 \ - -B /tmp/wiiu-sdl2/build \ - -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/opt/devkitpro/portlibs/wiiu - cmake --build /tmp/wiiu-sdl2/build --parallel - cmake --install /tmp/wiiu-sdl2/build - - name: Build the release WUHB run: sh tools/wiiu/build-wuhb.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index e49d3825..1b8c8c7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ project( LANGUAGES C CXX ) -if(NINTENDO_SWITCH OR WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") +if(NINTENDO_SWITCH OR WIIU) set(BUILD_TESTING OFF CACHE BOOL "" FORCE) endif() @@ -26,8 +26,6 @@ option( if(NINTENDO_SWITCH) set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "nxfb") -elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") - set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "wiiu") else() set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "lgl") endif() @@ -39,14 +37,16 @@ set( ) set_property( CACHE OPENSHADOWFLARE_PRESENTATION_BACKEND - PROPERTY STRINGS lgl nxfb wiiu + PROPERTY STRINGS lgl nxfb ) - -add_subdirectory(thirdparty/lwl) -add_subdirectory(thirdparty/lal) +if(OPENSHADOWFLARE_BUILD_EXE OR BUILD_TESTING) + add_subdirectory(thirdparty/lwl) + add_subdirectory(thirdparty/lal) +endif() add_subdirectory(thirdparty/twl) add_subdirectory(thirdparty/tal) -if(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl" OR BUILD_TESTING) +if((OPENSHADOWFLARE_BUILD_EXE AND + OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl") OR BUILD_TESTING) add_subdirectory(thirdparty/lgl) endif() diff --git a/documentation/wiiu-port.md b/documentation/wiiu-port.md index bbadb2ef..21371a4c 100644 --- a/documentation/wiiu-port.md +++ b/documentation/wiiu-port.md @@ -1,33 +1,36 @@ # Wii U port -The Wii U build produces an Aroma-compatible Homebrew Bundle (`.wuhb`). It -does not include retail game data. +The Wii U build compiles the small C99 ShadowFlare runtime and packages it as an +Aroma-compatible Homebrew Bundle (`.wuhb`). It does not include retail game data. -## Build +## Install the toolchain -Install devkitPro's Wii U toolchain, SDL2 port, and Ninja: +Install devkitPro's Wii U toolchain and Ninja: ```bash -sudo dkp-pacman -S --needed wiiu-dev wiiu-sdl2 ninja +sudo dkp-pacman -S --needed wiiu-dev ninja ``` -From the repository root, build the bundle: +Set `DEVKITPRO` if devkitPro is not installed at `/opt/devkitpro`. + +## Build + +From the repository root: ```bash sh tools/wiiu/build-wuhb.sh ``` -Output: +The bundle is written to: ```text build/wiiu/release/OpenShadowFlare.wuhb ``` -```bash -rm -rf build/wiiu/release -``` +## Move the game files -## Test in Emulator +The retail data is loaded from the SD card and is never bundled. Copy your +ShadowFlare `System` folder and `SFlare.Cfg` to: ```text sdcard/wiiu/OpenShadowFlare/ShadowFlare/ @@ -35,22 +38,3 @@ sdcard/wiiu/OpenShadowFlare/ShadowFlare/ System/ ... ``` - -Load `build/wiiu/release/OpenShadowFlare.wuhb`. Configure Controller as a **Wii U GamePad**; -a Pro Controller profile does not provide the VPAD input used by this port. - -## Controls - -| GamePad control | Game action | -| --- | --- | -| Touchscreen | Pointer and primary click | -| D-pad | Menu navigation and movement | -| A / Plus | Confirm | -| B / Minus | Back / cancel | -| X | Inventory | -| Y | Map | -| L | Mission list | -| R | Toggle walk/run | -| ZL | Enter `Player` in the name field | - -The current SDL2 presenter mirrors the game to the TV and GamePad. diff --git a/shadowflare/CMakeLists.txt b/shadowflare/CMakeLists.txt index 19a878ba..0c009a7e 100644 --- a/shadowflare/CMakeLists.txt +++ b/shadowflare/CMakeLists.txt @@ -140,3 +140,27 @@ if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) Twl::Twl ) endif() +if(WIIU) + target_compile_definitions( + shadowflare_core PRIVATE + SF_RETAIL_ROOT_FALLBACK="/vol/external01/wiiu/OpenShadowFlare/ShadowFlare") + + if(NOT COMMAND wut_create_rpx) + message(FATAL_ERROR + "wut_create_rpx is unavailable. Configure the Wii U build with " + "$ENV{DEVKITPRO}/portlibs/wiiu/bin/powerpc-eabi-cmake.") + endif() + wut_create_rpx(osf) + if(COMMAND wut_create_wuhb) + wut_create_wuhb( + osf + NAME "OpenShadowFlare" + AUTHOR "OpenShadowFlare contributors" + ICON "${PROJECT_SOURCE_DIR}/assets/wiiu/icon.png" + CONTENT "${PROJECT_SOURCE_DIR}/thirdparty/twl/backends/wiiu/content" + ) + else() + message(WARNING + "wut_create_wuhb is unavailable: only osf.rpx will be produced.") + endif() +endif() diff --git a/shadowflare/assets/retail_paths.c b/shadowflare/assets/retail_paths.c index da027d88..da4391b8 100644 --- a/shadowflare/assets/retail_paths.c +++ b/shadowflare/assets/retail_paths.c @@ -139,6 +139,11 @@ bool sf_retail_root_find( root[0] = '\0'; if (requested_root) return sf_retail_try_root(root, capacity, requested_root); +#ifdef SF_RETAIL_ROOT_FALLBACK + /* A build may pin a data root when the platform has no meaningful executable + * path or working directory (e.g. a console loading from a fixed SD path). */ + if (sf_retail_try_root(root, capacity, SF_RETAIL_ROOT_FALLBACK)) return true; +#endif if (sf_retail_executable_directory( executable_directory, sizeof(executable_directory), executable_path)) { if (sf_retail_try_root(root, capacity, executable_directory)) return true; diff --git a/src/SF_EXE/cmake/configure_platform.cmake b/src/SF_EXE/cmake/configure_platform.cmake index 3ce48e6f..456288fc 100644 --- a/src/SF_EXE/cmake/configure_platform.cmake +++ b/src/SF_EXE/cmake/configure_platform.cmake @@ -5,9 +5,6 @@ function(osf_configure_platform target) elseif(NINTENDO_SWITCH) include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Switch.cmake") osf_configure_switch_platform(${target}) - elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") - include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/WiiU.cmake") - osf_configure_wiiu_platform(${target}) elseif( WIN32 OR APPLE diff --git a/src/SF_EXE/cmake/configure_presentation.cmake b/src/SF_EXE/cmake/configure_presentation.cmake index fddfd426..8d324e16 100644 --- a/src/SF_EXE/cmake/configure_presentation.cmake +++ b/src/SF_EXE/cmake/configure_presentation.cmake @@ -12,12 +12,6 @@ function(osf_configure_presentation target) PRIVATE runtime/presentation/switch_surface_presenter.cpp ) - elseif(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "wiiu") - target_sources( - ${target} - PRIVATE - runtime/presentation/wiiu_surface_presenter.cpp - ) else() message(FATAL_ERROR "Unsupported presentation backend: " diff --git a/src/SF_EXE/cmake/platforms/WiiU.cmake b/src/SF_EXE/cmake/platforms/WiiU.cmake deleted file mode 100644 index 4f1c6baa..00000000 --- a/src/SF_EXE/cmake/platforms/WiiU.cmake +++ /dev/null @@ -1,49 +0,0 @@ -function(osf_configure_wiiu_platform target) - target_sources( - ${target} - PRIVATE - runtime/platform/wiiu/application_loop.cpp - ) - target_compile_definitions(${target} PRIVATE OSF_PLATFORM_WIIU) - - set(_osf_wiiu_portlibs "$ENV{DEVKITPRO}/portlibs/wiiu") - find_path( - OSF_WIIU_SDL2_INCLUDE_DIR - NAMES SDL.h - PATHS "${_osf_wiiu_portlibs}/include/SDL2" - NO_DEFAULT_PATH - REQUIRED - ) - find_library( - OSF_WIIU_SDL2_LIBRARY - NAMES SDL2 - PATHS "${_osf_wiiu_portlibs}/lib" - NO_DEFAULT_PATH - REQUIRED - ) - target_include_directories(${target} PRIVATE "${OSF_WIIU_SDL2_INCLUDE_DIR}") - target_link_libraries(${target} PRIVATE "${OSF_WIIU_SDL2_LIBRARY}") - - # The devkitPro Wii U CMake wrapper supplies WUT's Cafe OS import libraries - # at the end of the link line. Do not add gx2/vpad/coreinit manually. - - if(COMMAND wut_create_rpx) - wut_create_rpx(${target}) - else() - message(FATAL_ERROR - "wut_create_rpx is unavailable. Configure with " - "$ENV{DEVKITPRO}/portlibs/wiiu/bin/powerpc-eabi-cmake.") - endif() - - if(COMMAND wut_create_wuhb) - wut_create_wuhb( - ${target} - NAME "OpenShadowFlare" - AUTHOR "OpenShadowFlare contributors" - ICON "${PROJECT_SOURCE_DIR}/assets/wiiu/icon.png" - ) - else() - message(WARNING - "wut_create_wuhb is unavailable: only OpenShadowFlare.rpx will be produced.") - endif() -endfunction() diff --git a/src/SF_EXE/runtime/game_runtime.cpp b/src/SF_EXE/runtime/game_runtime.cpp index 704694e7..087d3689 100644 --- a/src/SF_EXE/runtime/game_runtime.cpp +++ b/src/SF_EXE/runtime/game_runtime.cpp @@ -581,7 +581,7 @@ class Runtime final : public osf::runtime::FrameApplication { int osf::runtime::runGame( const std::filesystem::path& data_root, const GameConfig& game_config, -bool smoke_test) { + bool smoke_test) { auto runtime = std::make_unique(data_root); if (!runtime->initialize(game_config)) { return 1; diff --git a/src/SF_EXE/runtime/main.cpp b/src/SF_EXE/runtime/main.cpp index acaf815e..947875c5 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -55,18 +55,6 @@ std::filesystem::path findDataRoot() { return std::filesystem::path{}; }; -#ifdef OSF_PLATFORM_WIIU - const std::filesystem::path wiiuSdDataPaths[] = { - "/vol/external01/wiiu/OpenShadowFlare/ShadowFlare", - "/vol/external01/wiiu/apps/OpenShadowFlare/ShadowFlare", - }; - for (const auto& wiiuSdData : wiiuSdDataPaths) { - if (isDataRoot(wiiuSdData)) { - return wiiuSdData; - } - } -#endif - std::error_code error; const std::filesystem::path fromWorkingDirectory = searchParents(std::filesystem::current_path(error)); diff --git a/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp b/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp deleted file mode 100644 index 87b27e17..00000000 --- a/src/SF_EXE/runtime/platform/wiiu/application_loop.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "runtime/application_loop.hpp" - -#include - -namespace osf::runtime { - -int runApplicationLoop(std::unique_ptr application) { - while (WHBProcIsRunning() && application->frame()) { - } - return 0; -} - -} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp deleted file mode 100644 index 17e77977..00000000 --- a/src/SF_EXE/runtime/presentation/wiiu_surface_presenter.cpp +++ /dev/null @@ -1,158 +0,0 @@ -#include "runtime/presentation/surface_presenter.hpp" - -#include - -#include -#include -#include - -namespace { - -constexpr std::int32_t kFrameWidth = 640; -constexpr std::int32_t kFrameHeight = 480; - -class WiiUSurfacePresenter final : public osf::runtime::SurfacePresenter { -public: - ~WiiUSurfacePresenter() override { - shutdown(); - } - - bool initialize(LwlWindow* window, std::string* error) override; - void prepareFrame(osf::gapi::SurfaceView surface) override; - void displayFrame() override; -#if OSF_ENABLE_DEBUG_TOOLS - std::optional - videoMemoryUsageBytes() const override; -#endif - -private: - void shutdown(); - - SDL_Window* window_ = nullptr; - SDL_Renderer* renderer_ = nullptr; - SDL_Texture* texture_ = nullptr; - bool videoInitialized_ = false; - bool frame_prepared_ = false; -}; - -bool WiiUSurfacePresenter::initialize(LwlWindow* window, std::string* error) { - (void) window; - shutdown(); - - if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { - if (error) { - *error = std::string("Could not initialize Wii U video: ") + - SDL_GetError(); - } - return false; - } - videoInitialized_ = true; - - window_ = SDL_CreateWindow( - "OpenShadowFlare", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - kFrameWidth, kFrameHeight, SDL_WINDOW_SHOWN); - if (!window_) { - if (error) { - *error = std::string("Could not create the Wii U video surface: ") + - SDL_GetError(); - } - shutdown(); - return false; - } - - renderer_ = SDL_CreateRenderer( - window_, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - if (!renderer_) { - if (error) { - *error = std::string("Could not create the Wii U GPU renderer: ") + - SDL_GetError(); - } - shutdown(); - return false; - } - - texture_ = SDL_CreateTexture( - renderer_, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, - kFrameWidth, kFrameHeight); - if (!texture_) { - if (error) { - *error = std::string("Could not create the Wii U frame texture: ") + - SDL_GetError(); - } - shutdown(); - return false; - } - - if (error) { - error->clear(); - } - return true; -} - -void WiiUSurfacePresenter::prepareFrame(osf::gapi::SurfaceView surface) { - frame_prepared_ = false; - static_assert( - sizeof(osf::gapi::Color) == sizeof(std::uint32_t), - "GAPI colors must be tightly packed RGBA bytes."); - if (!texture_ || !renderer_ || !surface.pixels || surface.width != kFrameWidth || - surface.height != kFrameHeight) { - return; - } - - if (SDL_UpdateTexture( - texture_, nullptr, surface.pixels, - kFrameWidth * - static_cast(sizeof(*surface.pixels))) != 0 || - SDL_RenderClear(renderer_) != 0 || - SDL_RenderCopy(renderer_, texture_, nullptr, nullptr) != 0) { - return; - } - frame_prepared_ = true; -} - -void WiiUSurfacePresenter::displayFrame() { - if (!renderer_ || !frame_prepared_) { - return; - } - SDL_RenderPresent(renderer_); - frame_prepared_ = false; -} - -#if OSF_ENABLE_DEBUG_TOOLS -std::optional -WiiUSurfacePresenter::videoMemoryUsageBytes() const { - if (!texture_) { - return std::nullopt; - } - return static_cast(kFrameWidth) * - static_cast(kFrameHeight) * - sizeof(std::uint32_t); -} -#endif - -void WiiUSurfacePresenter::shutdown() { - if (texture_) { - SDL_DestroyTexture(texture_); - texture_ = nullptr; - } - if (renderer_) { - SDL_DestroyRenderer(renderer_); - renderer_ = nullptr; - } - if (window_) { - SDL_DestroyWindow(window_); - window_ = nullptr; - } - if (videoInitialized_) { - SDL_QuitSubSystem(SDL_INIT_VIDEO); - videoInitialized_ = false; - } - frame_prepared_ = false; -} - -} // namespace - -std::unique_ptr -osf::runtime::createSurfacePresenter() { - return std::make_unique(); -} diff --git a/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index e1497866..6d19e0ec 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -89,25 +89,6 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lal PRIVATE lal_switch.c) -elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") - target_sources(lal PRIVATE lal_wiiu.c) - set(_lal_wiiu_portlibs "$ENV{DEVKITPRO}/portlibs/wiiu") - find_path( - LAL_WIIU_SDL2_INCLUDE_DIR - NAMES SDL.h - PATHS "${_lal_wiiu_portlibs}/include/SDL2" - NO_DEFAULT_PATH - REQUIRED - ) - find_library( - LAL_WIIU_SDL2_LIBRARY - NAMES SDL2 - PATHS "${_lal_wiiu_portlibs}/lib" - NO_DEFAULT_PATH - REQUIRED - ) - target_include_directories(lal PRIVATE "${LAL_WIIU_SDL2_INCLUDE_DIR}") - target_link_libraries(lal PRIVATE "${LAL_WIIU_SDL2_LIBRARY}") elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lal PRIVATE lal_alsa.c) find_package(ALSA REQUIRED) diff --git a/thirdparty/lal/lal_wiiu.c b/thirdparty/lal/lal_wiiu.c deleted file mode 100644 index 12c571bc..00000000 --- a/thirdparty/lal/lal_wiiu.c +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2026 Michael Binder and contributors - * - * This file is part of LAL. - * - * LAL is free software: you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later - * version. - */ - -#include "lal_internal.h" - -#include - -#include -#include -#include - -static SDL_AudioDeviceID g_device; -static SDL_mutex *g_mutex; -static bool g_audio_initialized; - -static void lal_wiiu_audio_callback( - void *user_data, Uint8 *stream, int byte_count) { - const size_t frame_count = - (size_t) byte_count / (LAL_OUTPUT_CHANNELS * sizeof(int16_t)); - - (void) user_data; - SDL_LockMutex(g_mutex); - lal_mix_frames((int16_t *) stream, frame_count); - SDL_UnlockMutex(g_mutex); -} - -bool lal_platform_init(void) { - SDL_AudioSpec requested; - - if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { - lal_set_error("Could not initialize Wii U audio output."); - return false; - } - g_audio_initialized = true; - - g_mutex = SDL_CreateMutex(); - if (!g_mutex) { - lal_set_error("Could not create the Wii U audio mixer mutex."); - lal_platform_shutdown(); - return false; - } - - memset(&requested, 0, sizeof(requested)); - requested.freq = LAL_OUTPUT_SAMPLE_RATE; - requested.format = AUDIO_S16SYS; - requested.channels = LAL_OUTPUT_CHANNELS; - requested.samples = LAL_BUFFER_FRAMES; - requested.callback = lal_wiiu_audio_callback; - - g_device = SDL_OpenAudioDevice( - NULL, 0, &requested, NULL, 0); - if (g_device == 0) { - lal_set_error("Could not open the Wii U audio playback device."); - lal_platform_shutdown(); - return false; - } - - SDL_PauseAudioDevice(g_device, 0); - return true; -} - -void lal_platform_shutdown(void) { - if (g_device != 0) { - SDL_CloseAudioDevice(g_device); - g_device = 0; - } - if (g_mutex) { - SDL_DestroyMutex(g_mutex); - g_mutex = NULL; - } - if (g_audio_initialized) { - SDL_QuitSubSystem(SDL_INIT_AUDIO); - g_audio_initialized = false; - } -} - -void lal_platform_lock(void) { - if (g_mutex) { - SDL_LockMutex(g_mutex); - } -} - -void lal_platform_unlock(void) { - if (g_mutex) { - SDL_UnlockMutex(g_mutex); - } -} diff --git a/thirdparty/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index 3e107bea..f27651fc 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -64,8 +64,6 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lwl PRIVATE lwl_switch.c) -elseif(WIIU OR CMAKE_SYSTEM_NAME STREQUAL "WiiU") - target_sources(lwl PRIVATE lwl_wiiu.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lwl PRIVATE lwl_x11.c) find_package(OpenGL REQUIRED) diff --git a/thirdparty/lwl/lwl_wiiu.c b/thirdparty/lwl/lwl_wiiu.c deleted file mode 100644 index 04f993f8..00000000 --- a/thirdparty/lwl/lwl_wiiu.c +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (C) 2026 Michael Binder and contributors - * - * This file is part of LWL. - * - * LWL is free software: you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later - * version. - */ - -/* Wii U GamePad backend: touchscreen maps directly to the game pointer. */ - -#include "lwl.h" - -#include -#include -#include -#include - -#include -#include -#include - -/* SDL stretches the 640x480 game texture to the 854x480 GamePad output. */ -#define LWL_WIIU_WIDTH 640 -#define LWL_WIIU_HEIGHT 480 -#define LWL_WIIU_GAMEPAD_WIDTH 854 - -struct LwlWindow { - int width; - int height; -}; - -static struct LwlWindow g_window; - -enum { LWL_WIIU_EVENT_CAPACITY = 32 }; - -static LwlEvent g_events[LWL_WIIU_EVENT_CAPACITY]; -static int g_event_count; -static int g_event_read; -static bool g_polled_this_frame; -static bool g_vpad_initialized; -static bool g_was_touching; -static int g_pointer_x = LWL_WIIU_WIDTH / 2; -static int g_pointer_y = LWL_WIIU_HEIGHT / 2; - -static int lwl_wiiu_clamp(int value, int low, int high) { - if (value < low) { - return low; - } - if (value > high) { - return high; - } - return value; -} - -static LwlEvent *lwl_wiiu_push(LwlEventType type) { - LwlEvent *event; - if (g_event_count >= LWL_WIIU_EVENT_CAPACITY) { - return NULL; - } - event = &g_events[g_event_count++]; - memset(event, 0, sizeof(*event)); - event->type = type; - return event; -} - -static void lwl_wiiu_push_pointer(LwlEventType type, int button) { - LwlEvent *event = lwl_wiiu_push(type); - if (event) { - event->x = g_pointer_x; - event->y = g_pointer_y; - event->button = button; - event->clicks = 1; - } -} - -static void lwl_wiiu_push_key(LwlEventType type, const char *key) { - LwlEvent *event = lwl_wiiu_push(type); - if (event) { - strncpy(event->key, key, sizeof(event->key) - 1); - } -} - -static void lwl_wiiu_push_text(const char *text) { - LwlEvent *event = lwl_wiiu_push(LWL_EVENT_TEXT_INPUT); - if (event) { - strncpy(event->text, text, sizeof(event->text) - 1); - } -} - -struct LwlWiiUKeyMapping { - uint32_t mask; - const char *key; -}; - -static const struct LwlWiiUKeyMapping g_key_mappings[] = { - {VPAD_BUTTON_UP, "up"}, - {VPAD_BUTTON_DOWN, "down"}, - {VPAD_BUTTON_LEFT, "left"}, - {VPAD_BUTTON_RIGHT, "right"}, - {VPAD_BUTTON_A, "return"}, - {VPAD_BUTTON_PLUS, "return"}, - {VPAD_BUTTON_B, "escape"}, - {VPAD_BUTTON_MINUS, "escape"}, - {VPAD_BUTTON_X, "i"}, - {VPAD_BUTTON_Y, "n"}, - {VPAD_BUTTON_L, "q"}, - {VPAD_BUTTON_R, "r"}, -}; - -static void lwl_wiiu_sample_input(void) { - VPADStatus status; - VPADReadError error; - VPADTouchData touch; - int32_t count; - size_t index; - - g_event_count = 0; - g_event_read = 0; - memset(&status, 0, sizeof(status)); - count = VPADRead(VPAD_CHAN_0, &status, 1, &error); - if (count <= 0 && error == VPAD_READ_UNINITIALIZED) { - VPADInit(); - g_vpad_initialized = true; - count = VPADRead(VPAD_CHAN_0, &status, 1, &error); - } - if (count <= 0 || error != VPAD_READ_SUCCESS) { - return; - } - - if ((status.trigger & VPAD_BUTTON_ZL) != 0u) { - lwl_wiiu_push_text("Player"); - } - - memset(&touch, 0, sizeof(touch)); - VPADGetTPCalibratedPointEx( - VPAD_CHAN_0, VPAD_TP_854X480, &touch, &status.tpNormal); - if (touch.touched != 0u) { - const int x = - lwl_wiiu_clamp((int) touch.x, 0, LWL_WIIU_GAMEPAD_WIDTH - 1) * - LWL_WIIU_WIDTH / LWL_WIIU_GAMEPAD_WIDTH; - const int y = lwl_wiiu_clamp((int) touch.y, 0, LWL_WIIU_HEIGHT - 1); - if (x != g_pointer_x || y != g_pointer_y || !g_was_touching) { - LwlEvent *move = lwl_wiiu_push(LWL_EVENT_MOUSE_MOVE); - if (move) { - move->dx = x - g_pointer_x; - move->dy = y - g_pointer_y; - move->x = x; - move->y = y; - } - g_pointer_x = x; - g_pointer_y = y; - } - if (!g_was_touching) { - lwl_wiiu_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); - g_was_touching = true; - } - } else if (g_was_touching) { - lwl_wiiu_push_pointer(LWL_EVENT_MOUSE_UP, 1); - g_was_touching = false; - } - - for (index = 0; - index < sizeof(g_key_mappings) / sizeof(g_key_mappings[0]); - ++index) { - if ((status.trigger & g_key_mappings[index].mask) != 0u) { - lwl_wiiu_push_key(LWL_EVENT_KEY_DOWN, g_key_mappings[index].key); - } - if ((status.release & g_key_mappings[index].mask) != 0u) { - lwl_wiiu_push_key(LWL_EVENT_KEY_UP, g_key_mappings[index].key); - } - } -} - -bool lwl_init(void) { - WHBProcInit(); - VPADInit(); - g_event_count = 0; - g_event_read = 0; - g_polled_this_frame = false; - g_vpad_initialized = true; - g_was_touching = false; - g_pointer_x = LWL_WIIU_WIDTH / 2; - g_pointer_y = LWL_WIIU_HEIGHT / 2; - return true; -} - -void lwl_shutdown(void) { - if (g_vpad_initialized) { - VPADShutdown(); - g_vpad_initialized = false; - } - WHBProcShutdown(); -} - -LwlWindow *lwl_window_create(const char *title, int width, int height) { - (void) title; - (void) width; - (void) height; - g_window.width = LWL_WIIU_WIDTH; - g_window.height = LWL_WIIU_HEIGHT; - return &g_window; -} - -LwlWindow *lwl_window_create_with_native_message_handler( - const char *title, int width, int height, - LwlNativeMessageHandler handler, void *user_data) { - (void) handler; - (void) user_data; - return lwl_window_create(title, width, height); -} - -LwlWindow *lwl_window_attach_native( - void *native_window, int width, int height) { - (void) native_window; - return lwl_window_create(NULL, width, height); -} - -void *lwl_window_get_native_handle(LwlWindow *window) { - (void) window; - return NULL; -} - -void lwl_window_destroy(LwlWindow *window) { (void) window; } -void lwl_window_show(LwlWindow *window) { (void) window; } -void lwl_window_set_title(LwlWindow *window, const char *title) { - (void) window; - (void) title; -} -void lwl_window_set_mode(LwlWindow *window, LwlWindowMode mode) { - (void) window; - (void) mode; -} -bool lwl_window_has_focus(LwlWindow *window) { - (void) window; - return true; -} -void lwl_window_set_cursor(LwlWindow *window, LwlCursor cursor) { - (void) window; - (void) cursor; -} -bool lwl_window_set_cursor_image( - LwlWindow *window, const LwlColor *pixels, - int width, int height, int hotspot_x, int hotspot_y) { - (void) window; - (void) pixels; - (void) width; - (void) height; - (void) hotspot_x; - (void) hotspot_y; - return false; -} -void lwl_window_set_cursor_visible(LwlWindow *window, bool visible) { - (void) window; - (void) visible; -} -bool lwl_window_set_size(LwlWindow *window, int width, int height) { - (void) window; - (void) width; - (void) height; - return false; -} -void lwl_window_get_size(LwlWindow *window, int *width, int *height) { - if (width) { - *width = window ? window->width : LWL_WIIU_WIDTH; - } - if (height) { - *height = window ? window->height : LWL_WIIU_HEIGHT; - } -} -LwlColor *lwl_window_get_framebuffer( - LwlWindow *window, int *width, int *height) { - (void) window; - if (width) { - *width = 0; - } - if (height) { - *height = 0; - } - return NULL; -} -bool lwl_window_resize_framebuffer( - LwlWindow *window, int width, int height) { - (void) window; - (void) width; - (void) height; - return false; -} -void lwl_window_update_rects( - LwlWindow *window, const LwlRect *rects, int count) { - (void) window; - (void) rects; - (void) count; -} - -bool lwl_poll_event(LwlWindow *window, LwlEvent *event) { - (void) window; - if (!event) { - return false; - } - if (g_event_read < g_event_count) { - *event = g_events[g_event_read++]; - return true; - } - if (g_polled_this_frame) { - g_polled_this_frame = false; - return false; - } - g_polled_this_frame = true; - lwl_wiiu_sample_input(); - if (g_event_read < g_event_count) { - *event = g_events[g_event_read++]; - return true; - } - g_polled_this_frame = false; - return false; -} - -bool lwl_wait_event(LwlWindow *window, double timeout_seconds) { - (void) window; - (void) timeout_seconds; - return false; -} -char *lwl_clipboard_get(LwlWindow *window) { - (void) window; - return NULL; -} -void lwl_clipboard_set(LwlWindow *window, const char *text) { - (void) window; - (void) text; -} -char *lwl_select_folder(LwlWindow *window, const char *title) { - (void) window; - (void) title; - return NULL; -} -void lwl_free(void *ptr) { free(ptr); } - -double lwl_time_seconds(void) { - return (double) OSGetTime() / (double) OSTimerClockSpeed; -} -void lwl_sleep_seconds(double seconds) { - if (seconds > 0.0) { - OSSleepTicks((OSTime) (seconds * (double) OSTimerClockSpeed)); - } -} -void lwl_sleep_until_seconds(double time_seconds) { - const double now = lwl_time_seconds(); - if (time_seconds > now) { - lwl_sleep_seconds(time_seconds - now); - } -} -const char *lwl_platform_name(void) { return "wiiu"; } -double lwl_display_scale(void) { return 1.0; } -bool lwl_exe_path(char *buf, int size) { - (void) buf; - (void) size; - return false; -} -LwlGlConfig lwl_gl_config_default(void) { - LwlGlConfig config; - memset(&config, 0, sizeof(config)); - config.api = LWL_GL_API_ES; - config.major_version = 2; - config.double_buffer = true; - return config; -} -LwlGlContext *lwl_gl_context_create( - LwlWindow *window, const LwlGlConfig *config) { - (void) window; - (void) config; - return NULL; -} -void lwl_gl_context_destroy(LwlGlContext *context) { (void) context; } -bool lwl_gl_context_make_current(LwlGlContext *context) { - (void) context; - return false; -} -void lwl_gl_context_swap_buffers(LwlGlContext *context) { (void) context; } -bool lwl_gl_context_set_swap_interval(LwlGlContext *context, int interval) { - (void) context; - (void) interval; - return false; -} -void *lwl_gl_get_proc_address(const char *name) { - (void) name; - return NULL; -} diff --git a/thirdparty/twl/CMakeLists.txt b/thirdparty/twl/CMakeLists.txt index 5e4388ae..cea723f5 100644 --- a/thirdparty/twl/CMakeLists.txt +++ b/thirdparty/twl/CMakeLists.txt @@ -74,6 +74,14 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(X11 REQUIRED) find_package(OpenGL REQUIRED) target_link_libraries(twl PRIVATE X11::X11 OpenGL::GL) +elseif(WIIU) + target_sources( + twl + PRIVATE + backends/wiiu/window.c + backends/wiiu/input.c + backends/wiiu/presentation.c + ) else() target_sources(twl PRIVATE backends/null/backend.c) endif() diff --git a/thirdparty/twl/backends/wiiu/backend.h b/thirdparty/twl/backends/wiiu/backend.h new file mode 100644 index 00000000..36ff03f6 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/backend.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of TWL. + * + * TWL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * TWL is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + * + * You should have received a copy of the GNU General Public License along + * with TWL. If not, see . + */ + +#ifndef TWL_WIIU_BACKEND_H +#define TWL_WIIU_BACKEND_H + +#include "twl_internal.h" + +#include +#include +#include +#include + +/* Compiled GFD shader bundled with the .wuhb. See shaders/README.md for how it + * is produced from shaders/texture_blit.{vsh,psh}. */ +#ifndef TWL_WIIU_SHADER_PATH +#define TWL_WIIU_SHADER_PATH "/vol/content/texture_blit.gsh" +#endif + +typedef struct { + WHBGfxShaderGroup shader; + GX2RBuffer position_buffer; + GX2RBuffer texcoord_buffer; + GX2Texture texture; + GX2Sampler sampler; + uint32_t frame_width; + uint32_t frame_height; + bool proc_ready; + bool gfx_ready; + bool shader_ready; + bool texture_ready; + bool quit_pushed; +} TwlWiiU; + +#endif diff --git a/thirdparty/twl/backends/wiiu/content/README.md b/thirdparty/twl/backends/wiiu/content/README.md new file mode 100644 index 00000000..d8e47c59 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/content/README.md @@ -0,0 +1,8 @@ +# Wii U bundle content + +Files in this directory are bundled into `OpenShadowFlare.wuhb` and mounted at +`/vol/content` on the console. + +The Wii U TWL backend loads `texture_blit.gsh` from here at startup. It is the +committed compiled output of `../shaders/texture_blit.{vsh,psh}`; see +`../shaders/README.md` to regenerate it. diff --git a/thirdparty/twl/backends/wiiu/content/texture_blit.gsh b/thirdparty/twl/backends/wiiu/content/texture_blit.gsh new file mode 100644 index 0000000000000000000000000000000000000000..6c20ce8fc12b87bea71435c65a378015ef432f1d GIT binary patch literal 1176 zcmZ=yt1x0Y? z0Hgz`=ns&B*#|QJ{{Ug2wG4bfEC9q3AP$Lu=|Tdd39))k6)=VX)pEns1NjL+HYb$N zm>7^>oLQ2Yp9d633`wnU&d)DO0kVO1Z~)bs0I^FgFk*m08W25 z3}okEi9c4L9LSHb_yjT=IBga%Fm$jnOkr?f1kws3SoE<0wSdA0Ilh=g5as~QLoyPm zZ~y>5&`yveK<)$ruzMj)GD*~MW_SS&XJA|~ynq=AVq*y}V15GI56&M7V8sU-Kol^1 cr+~R5hyX{30PhE2S_3n&gcmIFuz-RE0G`x&1^@s6 literal 0 HcmV?d00001 diff --git a/thirdparty/twl/backends/wiiu/input.c b/thirdparty/twl/backends/wiiu/input.c new file mode 100644 index 00000000..810c1231 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/input.c @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of TWL. + * + * TWL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * TWL is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + */ + +#include "backend.h" + +#include + +void twl_backend_pump_events(Twl *twl) { + TwlWiiU *wiiu = twl ? (TwlWiiU *) twl->backend : NULL; + if (!wiiu) { + return; + } + if (!wiiu->quit_pushed && !WHBProcIsRunning()) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = TWL_EVENT_QUIT; + event.timestamp_us = twl_backend_time_microseconds(twl); + twl_internal_push_event(twl, &event); + wiiu->quit_pushed = true; + } +} diff --git a/thirdparty/twl/backends/wiiu/presentation.c b/thirdparty/twl/backends/wiiu/presentation.c new file mode 100644 index 00000000..e7fb5177 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/presentation.c @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of TWL. + * + * TWL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * TWL is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + */ + +#include "backend.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/* Expand the RGB555 frame (red in the low 5 bits) into the RGBA8 texture. */ +static void twl_wiiu_upload(TwlWiiU *wiiu, const TwlSurface *surface) { + const uint16_t *src = (const uint16_t *) surface->pixels; + uint8_t *dst = (uint8_t *) wiiu->texture.surface.image; + const size_t src_stride = surface->stride_bytes / sizeof(uint16_t); + const uint32_t dst_pitch = wiiu->texture.surface.pitch; + const uint32_t width = wiiu->frame_width; + const uint32_t height = wiiu->frame_height; + uint32_t x; + uint32_t y; + if (!dst) { + return; + } + for (y = 0; y < height; ++y) { + const uint16_t *src_row = src + (size_t) y * src_stride; + uint8_t *dst_row = dst + (size_t) y * dst_pitch * 4u; + for (x = 0; x < width; ++x) { + const uint16_t pixel = src_row[x]; + const uint8_t r5 = (uint8_t) (pixel & 0x1fu); + const uint8_t g5 = (uint8_t) ((pixel >> 5) & 0x1fu); + const uint8_t b5 = (uint8_t) ((pixel >> 10) & 0x1fu); + uint8_t *texel = dst_row + (size_t) x * 4u; + texel[0] = (uint8_t) ((r5 << 3) | (r5 >> 2)); + texel[1] = (uint8_t) ((g5 << 3) | (g5 >> 2)); + texel[2] = (uint8_t) ((b5 << 3) | (b5 >> 2)); + texel[3] = 0xffu; + } + } + GX2Invalidate( + GX2_INVALIDATE_MODE_CPU_TEXTURE, wiiu->texture.surface.image, + wiiu->texture.surface.imageSize); +} + +static void twl_wiiu_draw(TwlWiiU *wiiu) { + GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, FALSE, FALSE); + GX2SetDepthOnlyControl(FALSE, FALSE, GX2_COMPARE_FUNC_ALWAYS); + GX2SetFetchShader(&wiiu->shader.fetchShader); + GX2SetVertexShader(wiiu->shader.vertexShader); + GX2SetPixelShader(wiiu->shader.pixelShader); + GX2SetPixelTexture(&wiiu->texture, 0); + GX2SetPixelSampler(&wiiu->sampler, 0); + GX2RSetAttributeBuffer( + &wiiu->position_buffer, 0, wiiu->position_buffer.elemSize, 0); + GX2RSetAttributeBuffer( + &wiiu->texcoord_buffer, 1, wiiu->texcoord_buffer.elemSize, 0); + GX2DrawEx(GX2_PRIMITIVE_MODE_TRIANGLE_STRIP, 4, 0, 1); +} + +TwlResult twl_backend_prepare_frame(Twl *twl, const TwlSurface *surface) { + static int logged; + TwlWiiU *wiiu = twl ? (TwlWiiU *) twl->backend : NULL; + if (!wiiu || !surface || !surface->pixels) { + return TWL_RESULT_INVALID_ARGUMENT; + } + if (surface->format != TWL_PIXEL_RGB555 || + surface->width != wiiu->frame_width || + surface->height != wiiu->frame_height) { + if (logged < 4) { + WHBLogPrintf( + "[twl-wiiu] prepare_frame rejected surface: fmt=%d %ux%u " + "(expected %ux%u RGB555)", + (int) surface->format, surface->width, surface->height, + wiiu->frame_width, wiiu->frame_height); + ++logged; + } + return TWL_RESULT_INVALID_ARGUMENT; + } + + twl_wiiu_upload(wiiu, surface); + + WHBGfxBeginRender(); + + WHBGfxBeginRenderTV(); + WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); + twl_wiiu_draw(wiiu); + WHBGfxFinishRenderTV(); + + WHBGfxBeginRenderDRC(); + WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); + twl_wiiu_draw(wiiu); + WHBGfxFinishRenderDRC(); + + return TWL_RESULT_OK; +} + +TwlResult twl_backend_display_frame(Twl *twl) { + TwlWiiU *wiiu = twl ? (TwlWiiU *) twl->backend : NULL; + if (!wiiu) { + return TWL_RESULT_INVALID_ARGUMENT; + } + WHBGfxFinishRender(); + return TWL_RESULT_OK; +} diff --git a/thirdparty/twl/backends/wiiu/shaders/README.md b/thirdparty/twl/backends/wiiu/shaders/README.md new file mode 100644 index 00000000..4af4c4e8 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/shaders/README.md @@ -0,0 +1,41 @@ +# Wii U blit shaders + +`texture_blit.vsh` / `texture_blit.psh` are the Latte-assembly source for the +full-screen texture blit used by the Wii U TWL backend. They are compiled once +into `../content/texture_blit.gsh`, which is committed, bundled into the `.wuhb`, +and loaded at runtime with `WHBGfxLoadGFDShaderGroup`. + +The repository build and CI **do not** run the shader compiler; they only bundle +the committed `.gsh`. You only need the tool below if you change a shader. + +## Regenerating texture_blit.gsh + +The compiler is `latte-assembler` from decaf-emu. Only a Windows binary is +published (), so on Linux +run it through Wine: + +```sh +wine latte-assembler.exe assemble \ + --vsh texture_blit.vsh \ + --psh texture_blit.psh \ + ../content/texture_blit.gsh +``` + +Then commit the regenerated `../content/texture_blit.gsh`. + +### Notes for this compiler version + +The published `v0.1` binary is older than the assembly examples shipped with +current wut, so: + +- attribute/uniform types use **GLSL names** (`vec4`, `vec2`, ...), not + `Float4`/`Float2`; +- keep the metadata minimal (attribute vars + `SPI_VS_OUT_ID` / + `SPI_PS_INPUT_CNTL`). The verbose `SQ_PGM_RESOURCES_*`, `SQ_VTX_SEMANTIC_*` + and `VGT_*` fields from the newer examples are rejected by this build. + +## Status + +The shader assembles cleanly and renders the title screen correctly under Cemu. +If a future screen looks vertically flipped, flip the `V` texture coordinates in +the backend quad in `presentation.c`, not the shader. diff --git a/thirdparty/twl/backends/wiiu/shaders/texture_blit.psh b/thirdparty/twl/backends/wiiu/shaders/texture_blit.psh new file mode 100644 index 00000000..c5ef22f4 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/shaders/texture_blit.psh @@ -0,0 +1,18 @@ +; Wii U texture-blit pixel shader (Latte assembly for latte-assembler). +; +; Samples the uploaded framebuffer texture (t0) with sampler s0 using the +; interpolated texture coordinate (semantic 0, arriving in R0) and writes it to +; the render target. The framebuffer is uploaded as RGBA8 by the backend, so no +; channel swizzling is needed here. +; +; Compiled to texture_blit.gsh with latte-assembler; see shaders/README.md. + +; $MODE = "UniformRegister" +; $NUM_SPI_PS_INPUT_CNTL = 1 +; $SPI_PS_INPUT_CNTL[0].SEMANTIC = 0 +; $SPI_PS_INPUT_CNTL[0].DEFAULT_VAL = 1 + +00 TEX: ADDR(32) CNT(1) VALID_PIX + 0 SAMPLE R0, R0.xy0x, t0, s0 +01 EXP_DONE: PIX0, R0.xyzw +END_OF_PROGRAM diff --git a/thirdparty/twl/backends/wiiu/shaders/texture_blit.vsh b/thirdparty/twl/backends/wiiu/shaders/texture_blit.vsh new file mode 100644 index 00000000..a76ceddb --- /dev/null +++ b/thirdparty/twl/backends/wiiu/shaders/texture_blit.vsh @@ -0,0 +1,25 @@ +; Wii U texture-blit vertex shader (Latte assembly for latte-assembler). +; +; Passes a full-screen quad's clip-space position straight through and forwards +; the texture coordinate to the pixel shader. Attribute register mapping follows +; the convention "attribute at location N arrives in register R(N+1)". +; +; aPosition (location 0) -> R1 -> POS0 +; aTexCoord (location 1) -> R2 -> PARAM0 (semantic 0) +; +; Compiled to texture_blit.gsh with latte-assembler; see shaders/README.md. + +; $MODE = "UniformRegister" +; $ATTRIB_VARS[0].name = "aPosition" +; $ATTRIB_VARS[0].type = "vec4" +; $ATTRIB_VARS[0].location = 0 +; $ATTRIB_VARS[1].name = "aTexCoord" +; $ATTRIB_VARS[1].type = "vec2" +; $ATTRIB_VARS[1].location = 1 +; $NUM_SPI_VS_OUT_ID = 1 +; $SPI_VS_OUT_ID[0].SEMANTIC_0 = 0 + +00 CALL_FS NO_BARRIER +01 EXP_DONE: POS0, R1.xyzw +02 EXP_DONE: PARAM0, R2.xyzw NO_BARRIER +END_OF_PROGRAM diff --git a/thirdparty/twl/backends/wiiu/window.c b/thirdparty/twl/backends/wiiu/window.c new file mode 100644 index 00000000..7713a6b4 --- /dev/null +++ b/thirdparty/twl/backends/wiiu/window.c @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of TWL. + * + * TWL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * TWL is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + */ + +#include "backend.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const float twl_wiiu_positions[] = { + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, +}; + +static const float twl_wiiu_texcoords[] = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, +}; + +static bool twl_wiiu_create_buffer( + GX2RBuffer *buffer, uint32_t elem_size, uint32_t elem_count, + const void *data) { + void *mapped; + memset(buffer, 0, sizeof(*buffer)); + buffer->flags = GX2R_RESOURCE_BIND_VERTEX_BUFFER | + GX2R_RESOURCE_USAGE_CPU_READ | + GX2R_RESOURCE_USAGE_CPU_WRITE | + GX2R_RESOURCE_USAGE_GPU_READ; + buffer->elemSize = elem_size; + buffer->elemCount = elem_count; + if (!GX2RCreateBuffer(buffer)) { + return false; + } + mapped = GX2RLockBufferEx(buffer, 0); + if (!mapped) { + return false; + } + memcpy(mapped, data, (size_t) elem_size * elem_count); + GX2RUnlockBufferEx(buffer, 0); + return true; +} + +static bool twl_wiiu_create_texture( + TwlWiiU *wiiu, uint32_t width, uint32_t height) { + GX2Texture *texture = &wiiu->texture; + memset(texture, 0, sizeof(*texture)); + texture->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + texture->surface.width = width; + texture->surface.height = height; + texture->surface.depth = 1; + texture->surface.mipLevels = 1; + texture->surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + texture->surface.aa = GX2_AA_MODE1X; + texture->surface.use = GX2_SURFACE_USE_TEXTURE; + texture->surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED; + texture->surface.swizzle = 0; + texture->viewFirstMip = 0; + texture->viewNumMips = 1; + texture->viewFirstSlice = 0; + texture->viewNumSlices = 1; + texture->compMap = 0x00010203u; + GX2CalcSurfaceSizeAndAlignment(&texture->surface); + GX2InitTextureRegs(texture); + texture->surface.image = + memalign(texture->surface.alignment, texture->surface.imageSize); + if (!texture->surface.image) { + return false; + } + memset(texture->surface.image, 0, texture->surface.imageSize); + GX2Invalidate( + GX2_INVALIDATE_MODE_CPU_TEXTURE, texture->surface.image, + texture->surface.imageSize); + return true; +} + +static void twl_wiiu_teardown(TwlWiiU *wiiu) { + if (!wiiu) { + return; + } + if (wiiu->texture_ready) { + free(wiiu->texture.surface.image); + wiiu->texture.surface.image = NULL; + wiiu->texture_ready = false; + } + if (wiiu->position_buffer.flags) { + GX2RDestroyBufferEx(&wiiu->position_buffer, 0); + } + if (wiiu->texcoord_buffer.flags) { + GX2RDestroyBufferEx(&wiiu->texcoord_buffer, 0); + } + if (wiiu->shader_ready) { + WHBGfxFreeShaderGroup(&wiiu->shader); + wiiu->shader_ready = false; + } + if (wiiu->gfx_ready) { + WHBGfxShutdown(); + wiiu->gfx_ready = false; + } + if (wiiu->proc_ready) { + WHBProcShutdown(); + wiiu->proc_ready = false; + } +} + +size_t twl_backend_memory_alignment(void) { + return _Alignof(TwlWiiU); +} + +size_t twl_backend_memory_required(const TwlConfig *config) { + (void) config; + return sizeof(TwlWiiU); +} + +TwlResult twl_backend_init( + Twl *twl, void *memory, size_t memory_size, const TwlConfig *config) { + TwlWiiU *wiiu; + char *shader_file; + if (!twl || !memory || memory_size < sizeof(TwlWiiU) || !config) { + return TWL_RESULT_INVALID_ARGUMENT; + } + wiiu = (TwlWiiU *) memory; + wiiu->frame_width = config->width ? config->width : 640u; + wiiu->frame_height = config->height ? config->height : 480u; + + WHBProcInit(); + wiiu->proc_ready = true; + WHBLogCafeInit(); + WHBLogUdpInit(); + + if (!WHBGfxInit()) { + WHBLogPrint("[twl-wiiu] ERROR: WHBGfxInit failed"); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_BACKEND_FAILURE; + } + wiiu->gfx_ready = true; + + shader_file = WHBReadWholeFile(TWL_WIIU_SHADER_PATH, NULL); + if (!shader_file) { + WHBLogPrintf( + "[twl-wiiu] ERROR: could not read shader %s", TWL_WIIU_SHADER_PATH); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_BACKEND_FAILURE; + } + if (!WHBGfxLoadGFDShaderGroup(&wiiu->shader, 0, shader_file)) { + WHBLogPrint("[twl-wiiu] ERROR: WHBGfxLoadGFDShaderGroup failed"); + WHBFreeWholeFile(shader_file); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_BACKEND_FAILURE; + } + WHBGfxInitShaderAttribute( + &wiiu->shader, "aPosition", 0, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32_32_32); + WHBGfxInitShaderAttribute( + &wiiu->shader, "aTexCoord", 1, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32); + WHBGfxInitFetchShader(&wiiu->shader); + WHBFreeWholeFile(shader_file); + wiiu->shader_ready = true; + + if (!twl_wiiu_create_buffer( + &wiiu->position_buffer, 4u * sizeof(float), 4u, twl_wiiu_positions) || + !twl_wiiu_create_buffer( + &wiiu->texcoord_buffer, 2u * sizeof(float), 4u, twl_wiiu_texcoords)) { + WHBLogPrint("[twl-wiiu] ERROR: attribute buffer creation failed"); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_INSUFFICIENT_MEMORY; + } + + if (!twl_wiiu_create_texture( + wiiu, wiiu->frame_width, wiiu->frame_height)) { + WHBLogPrint("[twl-wiiu] ERROR: texture creation failed"); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_INSUFFICIENT_MEMORY; + } + wiiu->texture_ready = true; + + GX2InitSampler( + &wiiu->sampler, GX2_TEX_CLAMP_MODE_CLAMP, GX2_TEX_XY_FILTER_MODE_LINEAR); + + twl_internal_set_display_size(twl, wiiu->frame_width, wiiu->frame_height); + return TWL_RESULT_OK; +} + +void twl_backend_shutdown(Twl *twl) { + TwlWiiU *wiiu = twl ? (TwlWiiU *) twl->backend : NULL; + if (!wiiu) { + return; + } + twl_wiiu_teardown(wiiu); + WHBLogCafeDeinit(); + WHBLogUdpDeinit(); +} + +uint64_t twl_backend_time_microseconds(const Twl *twl) { + (void) twl; + return (uint64_t) OSTicksToMicroseconds(OSGetSystemTime()); +} + +void twl_backend_sleep_microseconds(Twl *twl, uint64_t duration) { + (void) twl; + OSSleepTicks(OSMicrosecondsToTicks(duration)); +} diff --git a/tools/wiiu/build-wuhb.sh b/tools/wiiu/build-wuhb.sh index 7f086825..abfa5cf4 100644 --- a/tools/wiiu/build-wuhb.sh +++ b/tools/wiiu/build-wuhb.sh @@ -24,7 +24,8 @@ export DEVKITPRO "$WIIU_CMAKE" -S "$ROOT_DIR" -B "$BUILD_DIR" \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=OFF + -DBUILD_TESTING=OFF \ + -DOPENSHADOWFLARE_BUILD_EXE=OFF cmake --build "$BUILD_DIR" --parallel WUHB=$(find "$BUILD_DIR" -type f -name '*.wuhb' ! -path "$OUTPUT_WUHB" -print -quit) From bcba663fe3406cfafc22e3951369dd31d6ff82b1 Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 01:22:51 -0300 Subject: [PATCH 5/7] Add wiiu input --- thirdparty/twl/backends/wiiu/backend.h | 4 + thirdparty/twl/backends/wiiu/input.c | 148 +++++++++++++++++++- thirdparty/twl/backends/wiiu/presentation.c | 8 +- thirdparty/twl/backends/wiiu/window.c | 27 ++-- 4 files changed, 162 insertions(+), 25 deletions(-) diff --git a/thirdparty/twl/backends/wiiu/backend.h b/thirdparty/twl/backends/wiiu/backend.h index 36ff03f6..9a127d5b 100644 --- a/thirdparty/twl/backends/wiiu/backend.h +++ b/thirdparty/twl/backends/wiiu/backend.h @@ -40,10 +40,14 @@ typedef struct { GX2Sampler sampler; uint32_t frame_width; uint32_t frame_height; + int32_t pointer_x; + int32_t pointer_y; + bool touching; bool proc_ready; bool gfx_ready; bool shader_ready; bool texture_ready; + bool vpad_ready; bool quit_pushed; } TwlWiiU; diff --git a/thirdparty/twl/backends/wiiu/input.c b/thirdparty/twl/backends/wiiu/input.c index 810c1231..5e89edf9 100644 --- a/thirdparty/twl/backends/wiiu/input.c +++ b/thirdparty/twl/backends/wiiu/input.c @@ -13,21 +13,155 @@ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. */ +/* + * Wii U GamePad (VPAD) input. The touchscreen drives the game pointer; the + * buttons map to the key events the runtime already understands, so no game + * code changes are needed. WHBProcIsRunning also services ProcUI and reports + * when the user leaves via the HOME menu. + */ + #include "backend.h" +#include #include +#include + +/* The GamePad's calibrated touch resolution. The game frame is stretched to + * fill this screen, so touch coordinates scale linearly to frame coordinates. */ +#define TWL_WIIU_TP_WIDTH 854 +#define TWL_WIIU_TP_HEIGHT 480 + +static const struct { + uint32_t mask; + TwlKey key; +} twl_wiiu_button_keys[] = { + {VPAD_BUTTON_UP, TWL_KEY_UP}, + {VPAD_BUTTON_DOWN, TWL_KEY_DOWN}, + {VPAD_BUTTON_LEFT, TWL_KEY_LEFT}, + {VPAD_BUTTON_RIGHT, TWL_KEY_RIGHT}, + {VPAD_BUTTON_A, TWL_KEY_RETURN}, + {VPAD_BUTTON_PLUS, TWL_KEY_RETURN}, + {VPAD_BUTTON_B, TWL_KEY_ESCAPE}, + {VPAD_BUTTON_MINUS, TWL_KEY_ESCAPE}, + {VPAD_BUTTON_X, TWL_KEY_I}, + {VPAD_BUTTON_R, TWL_KEY_R}, +}; + +static int32_t twl_wiiu_clamp(int32_t value, int32_t low, int32_t high) { + if (value < low) return low; + if (value > high) return high; + return value; +} + +static void twl_wiiu_push(Twl *twl, TwlEvent *event) { + event->timestamp_us = twl_backend_time_microseconds(twl); + twl_internal_push_event(twl, event); +} + +static void twl_wiiu_push_key(Twl *twl, TwlEventType type, TwlKey key) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = type; + event.key = key; + twl_wiiu_push(twl, &event); +} + +static void twl_wiiu_push_pointer( + Twl *twl, TwlEventType type, int32_t x, int32_t y) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = type; + event.x = x; + event.y = y; + event.button = 1u; + twl_wiiu_push(twl, &event); +} + +/* There is no keyboard, so ZL fills the fixed name "Player" one code point at a + * time, matching the TWL_EVENT_TEXT contract. */ +static void twl_wiiu_push_text(Twl *twl, const char *text) { + const char *cursor; + for (cursor = text; *cursor; ++cursor) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = TWL_EVENT_TEXT; + event.codepoint = (uint32_t) (unsigned char) *cursor; + twl_wiiu_push(twl, &event); + } +} + +static void twl_wiiu_read_touch( + Twl *twl, TwlWiiU *wiiu, VPADStatus *status) { + VPADTouchData touch; + memset(&touch, 0, sizeof(touch)); + VPADGetTPCalibratedPointEx( + VPAD_CHAN_0, VPAD_TP_854X480, &touch, &status->tpNormal); + if (touch.touched) { + const int32_t x = + twl_wiiu_clamp((int32_t) touch.x, 0, TWL_WIIU_TP_WIDTH - 1) * + (int32_t) wiiu->frame_width / TWL_WIIU_TP_WIDTH; + const int32_t y = + twl_wiiu_clamp((int32_t) touch.y, 0, TWL_WIIU_TP_HEIGHT - 1) * + (int32_t) wiiu->frame_height / TWL_WIIU_TP_HEIGHT; + if (!wiiu->touching || x != wiiu->pointer_x || y != wiiu->pointer_y) { + twl_wiiu_push_pointer(twl, TWL_EVENT_POINTER_MOVE, x, y); + } + if (!wiiu->touching) { + twl_wiiu_push_pointer(twl, TWL_EVENT_POINTER_DOWN, x, y); + } + wiiu->pointer_x = x; + wiiu->pointer_y = y; + wiiu->touching = true; + } else if (wiiu->touching) { + twl_wiiu_push_pointer( + twl, TWL_EVENT_POINTER_UP, wiiu->pointer_x, wiiu->pointer_y); + wiiu->touching = false; + } +} + +static void twl_wiiu_read_gamepad(Twl *twl, TwlWiiU *wiiu) { + VPADStatus status; + VPADReadError error; + size_t index; + if (VPADRead(VPAD_CHAN_0, &status, 1, &error) <= 0 || + error != VPAD_READ_SUCCESS) { + return; + } + + twl_wiiu_read_touch(twl, wiiu, &status); + + for (index = 0; + index < sizeof(twl_wiiu_button_keys) / sizeof(twl_wiiu_button_keys[0]); + ++index) { + const uint32_t mask = twl_wiiu_button_keys[index].mask; + if (status.trigger & mask) { + twl_wiiu_push_key(twl, TWL_EVENT_KEY_DOWN, twl_wiiu_button_keys[index].key); + } + if (status.release & mask) { + twl_wiiu_push_key(twl, TWL_EVENT_KEY_UP, twl_wiiu_button_keys[index].key); + } + } + + if (status.trigger & VPAD_BUTTON_ZL) { + twl_wiiu_push_text(twl, "Player"); + } +} + void twl_backend_pump_events(Twl *twl) { TwlWiiU *wiiu = twl ? (TwlWiiU *) twl->backend : NULL; if (!wiiu) { return; } - if (!wiiu->quit_pushed && !WHBProcIsRunning()) { - TwlEvent event; - twl_internal_zero(&event, sizeof(event)); - event.type = TWL_EVENT_QUIT; - event.timestamp_us = twl_backend_time_microseconds(twl); - twl_internal_push_event(twl, &event); - wiiu->quit_pushed = true; + if (!WHBProcIsRunning()) { + if (!wiiu->quit_pushed) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = TWL_EVENT_QUIT; + twl_wiiu_push(twl, &event); + wiiu->quit_pushed = true; + } + return; } + twl_wiiu_read_gamepad(twl, wiiu); } diff --git a/thirdparty/twl/backends/wiiu/presentation.c b/thirdparty/twl/backends/wiiu/presentation.c index e7fb5177..5849b633 100644 --- a/thirdparty/twl/backends/wiiu/presentation.c +++ b/thirdparty/twl/backends/wiiu/presentation.c @@ -17,12 +17,12 @@ #include #include -#include #include #include #include #include #include +#include #include #include @@ -32,7 +32,7 @@ /* Expand the RGB555 frame (red in the low 5 bits) into the RGBA8 texture. */ static void twl_wiiu_upload(TwlWiiU *wiiu, const TwlSurface *surface) { const uint16_t *src = (const uint16_t *) surface->pixels; - uint8_t *dst = (uint8_t *) wiiu->texture.surface.image; + uint8_t *dst = (uint8_t *) GX2RLockSurfaceEx(&wiiu->texture.surface, 0, 0); const size_t src_stride = surface->stride_bytes / sizeof(uint16_t); const uint32_t dst_pitch = wiiu->texture.surface.pitch; const uint32_t width = wiiu->frame_width; @@ -57,9 +57,7 @@ static void twl_wiiu_upload(TwlWiiU *wiiu, const TwlSurface *surface) { texel[3] = 0xffu; } } - GX2Invalidate( - GX2_INVALIDATE_MODE_CPU_TEXTURE, wiiu->texture.surface.image, - wiiu->texture.surface.imageSize); + GX2RUnlockSurfaceEx(&wiiu->texture.surface, 0, 0); } static void twl_wiiu_draw(TwlWiiU *wiiu) { diff --git a/thirdparty/twl/backends/wiiu/window.c b/thirdparty/twl/backends/wiiu/window.c index 7713a6b4..ec147895 100644 --- a/thirdparty/twl/backends/wiiu/window.c +++ b/thirdparty/twl/backends/wiiu/window.c @@ -18,11 +18,12 @@ #include #include #include -#include #include #include #include #include +#include +#include #include #include #include @@ -30,7 +31,6 @@ #include #include -#include #include #include @@ -90,17 +90,13 @@ static bool twl_wiiu_create_texture( texture->viewFirstSlice = 0; texture->viewNumSlices = 1; texture->compMap = 0x00010203u; - GX2CalcSurfaceSizeAndAlignment(&texture->surface); - GX2InitTextureRegs(texture); - texture->surface.image = - memalign(texture->surface.alignment, texture->surface.imageSize); - if (!texture->surface.image) { + if (!GX2RCreateSurface( + &texture->surface, + GX2R_RESOURCE_BIND_TEXTURE | GX2R_RESOURCE_USAGE_CPU_WRITE | + GX2R_RESOURCE_USAGE_GPU_READ)) { return false; } - memset(texture->surface.image, 0, texture->surface.imageSize); - GX2Invalidate( - GX2_INVALIDATE_MODE_CPU_TEXTURE, texture->surface.image, - texture->surface.imageSize); + GX2InitTextureRegs(texture); return true; } @@ -109,8 +105,7 @@ static void twl_wiiu_teardown(TwlWiiU *wiiu) { return; } if (wiiu->texture_ready) { - free(wiiu->texture.surface.image); - wiiu->texture.surface.image = NULL; + GX2RDestroySurfaceEx(&wiiu->texture.surface, 0); wiiu->texture_ready = false; } if (wiiu->position_buffer.flags) { @@ -127,6 +122,10 @@ static void twl_wiiu_teardown(TwlWiiU *wiiu) { WHBGfxShutdown(); wiiu->gfx_ready = false; } + if (wiiu->vpad_ready) { + VPADShutdown(); + wiiu->vpad_ready = false; + } if (wiiu->proc_ready) { WHBProcShutdown(); wiiu->proc_ready = false; @@ -155,6 +154,8 @@ TwlResult twl_backend_init( WHBProcInit(); wiiu->proc_ready = true; + VPADInit(); + wiiu->vpad_ready = true; WHBLogCafeInit(); WHBLogUdpInit(); From e08305aba02c393d1db544d9b36193aa47fd886c Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 01:46:51 -0300 Subject: [PATCH 6/7] Add wiiu tal --- thirdparty/tal/CMakeLists.txt | 2 + thirdparty/tal/tal_wiiu.c | 229 ++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 thirdparty/tal/tal_wiiu.c diff --git a/thirdparty/tal/CMakeLists.txt b/thirdparty/tal/CMakeLists.txt index 65935042..ab6757e6 100644 --- a/thirdparty/tal/CMakeLists.txt +++ b/thirdparty/tal/CMakeLists.txt @@ -48,6 +48,8 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(tal PRIVATE tal_alsa.c) find_package(ALSA REQUIRED) target_link_libraries(tal PRIVATE ALSA::ALSA) +elseif(WIIU) + target_sources(tal PRIVATE tal_wiiu.c) else() target_sources(tal PRIVATE tal_null.c) endif() diff --git a/thirdparty/tal/tal_wiiu.c b/thirdparty/tal/tal_wiiu.c new file mode 100644 index 00000000..71803534 --- /dev/null +++ b/thirdparty/tal/tal_wiiu.c @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2026 Michael Binder and contributors + * + * This file is part of TAL. + * + * TAL is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * TAL is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + * + * You should have received a copy of the GNU General Public License along + * with TAL. If not, see . + */ + +#include "tal_internal.h" + +#include +#include +#include +#include +#include + +#define TAL_WIIU_BUFFER_FRAMES 8192u +#define TAL_WIIU_LEAD_FRAMES 1024u +#define TAL_WIIU_VOICE_PRIORITY 31u +#define TAL_WIIU_VOLUME_FULL 0x8000u + +typedef struct { + Tal *tal; + AXVoice *voice; + int16_t *buffer; + uint32_t buffer_frames; + uint32_t write_offset; + OSMutex mutex; + bool mutex_ready; + bool ax_owned; + bool callback_ready; + bool running; +} TalWiiU; + +static TalWiiU *tal_wiiu_active; + +static size_t tal_wiiu_header_size(void) { + return (sizeof(TalWiiU) + 63u) & ~(size_t) 63u; +} + +size_t tal_backend_memory_alignment(void) { + return 64u; +} + +size_t tal_backend_memory_required(const TalConfig *config) { + (void) config; + return tal_wiiu_header_size() + + (size_t) TAL_WIIU_BUFFER_FRAMES * sizeof(int16_t); +} + +void tal_backend_lock(Tal *tal) { + TalWiiU *wiiu = tal ? (TalWiiU *) tal->backend : NULL; + if (wiiu && wiiu->mutex_ready) OSLockMutex(&wiiu->mutex); +} + +void tal_backend_unlock(Tal *tal) { + TalWiiU *wiiu = tal ? (TalWiiU *) tal->backend : NULL; + if (wiiu && wiiu->mutex_ready) OSUnlockMutex(&wiiu->mutex); +} + +static void tal_wiiu_fill(TalWiiU *wiiu, uint32_t count) { + const uint32_t block_max = wiiu->tal->config.mix_block_frames; + while (count > 0u) { + uint32_t chunk = count; + const uint32_t until_wrap = wiiu->buffer_frames - wiiu->write_offset; + if (chunk > until_wrap) chunk = until_wrap; + if (chunk > block_max) chunk = block_max; + if (tal_internal_render( + wiiu->tal, wiiu->buffer + wiiu->write_offset, chunk) != + TAL_RESULT_OK) { + tal_internal_zero( + wiiu->buffer + wiiu->write_offset, (size_t) chunk * sizeof(int16_t)); + } + DCFlushRange( + wiiu->buffer + wiiu->write_offset, + (uint32_t) ((size_t) chunk * sizeof(int16_t))); + wiiu->write_offset += chunk; + if (wiiu->write_offset >= wiiu->buffer_frames) + wiiu->write_offset -= wiiu->buffer_frames; + count -= chunk; + } +} + +static void tal_wiiu_frame_callback(void) { + TalWiiU *wiiu = tal_wiiu_active; + if (!wiiu || !wiiu->mutex_ready) return; + OSLockMutex(&wiiu->mutex); + if (wiiu->running && wiiu->voice) { + const uint32_t play = + AXGetVoiceCurrentOffsetEx(wiiu->voice, wiiu->buffer); + if (play < wiiu->buffer_frames) { + uint32_t target = play + TAL_WIIU_LEAD_FRAMES; + uint32_t count; + if (target >= wiiu->buffer_frames) target -= wiiu->buffer_frames; + count = (target + wiiu->buffer_frames - wiiu->write_offset) % + wiiu->buffer_frames; + if (count > 0u) tal_wiiu_fill(wiiu, count); + } + } + OSUnlockMutex(&wiiu->mutex); +} + +static bool tal_wiiu_start_voice(TalWiiU *wiiu, const TalConfig *config) { + AXVoiceOffsets offsets; + AXVoiceVeData volume; + AXVoiceDeviceMixData mix[6]; + wiiu->voice = AXAcquireVoice(TAL_WIIU_VOICE_PRIORITY, NULL, NULL); + if (!wiiu->voice) return false; + + AXVoiceBegin(wiiu->voice); + AXSetVoiceType(wiiu->voice, 0); + + tal_internal_zero(&volume, sizeof(volume)); + volume.volume = TAL_WIIU_VOLUME_FULL; + AXSetVoiceVe(wiiu->voice, &volume); + + tal_internal_zero(mix, sizeof(mix)); + mix[0].bus[0].volume = TAL_WIIU_VOLUME_FULL; + mix[1].bus[0].volume = TAL_WIIU_VOLUME_FULL; + AXSetVoiceDeviceMix(wiiu->voice, AX_DEVICE_TYPE_TV, 0, mix); + AXSetVoiceDeviceMix(wiiu->voice, AX_DEVICE_TYPE_DRC, 0, mix); + + AXSetVoiceSrcType(wiiu->voice, AX_VOICE_SRC_TYPE_LINEAR); + AXSetVoiceSrcRatio( + wiiu->voice, + (float) config->sample_rate / (float) AXGetInputSamplesPerSec()); + + tal_internal_zero(&offsets, sizeof(offsets)); + offsets.dataType = AX_VOICE_FORMAT_LPCM16; + offsets.loopingEnabled = AX_VOICE_LOOP_ENABLED; + offsets.loopOffset = 0u; + offsets.endOffset = wiiu->buffer_frames - 1u; + offsets.currentOffset = 0u; + offsets.data = wiiu->buffer; + AXSetVoiceOffsets(wiiu->voice, &offsets); + + AXSetVoiceState(wiiu->voice, AX_VOICE_STATE_PLAYING); + AXVoiceEnd(wiiu->voice); + return true; +} + +TalResult tal_backend_init( + Tal *tal, void *memory, size_t memory_size, const TalConfig *config) { + TalWiiU *wiiu; + if (!tal || !memory || !config || + memory_size < tal_backend_memory_required(config)) { + return TAL_RESULT_INVALID_ARGUMENT; + } + + if (config->channels != 1u) { + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + + wiiu = (TalWiiU *) memory; + wiiu->tal = tal; + wiiu->buffer = (int16_t *) ((uint8_t *) memory + tal_wiiu_header_size()); + wiiu->buffer_frames = TAL_WIIU_BUFFER_FRAMES; + wiiu->write_offset = 0u; + OSInitMutex(&wiiu->mutex); + wiiu->mutex_ready = true; + + DCFlushRange( + wiiu->buffer, + (uint32_t) ((size_t) wiiu->buffer_frames * sizeof(int16_t))); + + if (!AXIsInit()) { + AXInitParams params; + tal_internal_zero(¶ms, sizeof(params)); + params.renderer = AX_INIT_RENDERER_48KHZ; + params.pipeline = AX_INIT_PIPELINE_SINGLE; + AXInitWithParams(¶ms); + wiiu->ax_owned = true; + } + if (!AXIsInit()) { + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + + if (!tal_wiiu_start_voice(wiiu, config)) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_FAILURE; + } + + tal_wiiu_active = wiiu; + wiiu->running = true; + if (AXRegisterAppFrameCallback(tal_wiiu_frame_callback) == 0) + wiiu->callback_ready = true; + return TAL_RESULT_OK; +} + +void tal_backend_shutdown(Tal *tal) { + TalWiiU *wiiu = tal ? (TalWiiU *) tal->backend : NULL; + if (!wiiu) return; + if (wiiu->mutex_ready) { + OSLockMutex(&wiiu->mutex); + wiiu->running = false; + OSUnlockMutex(&wiiu->mutex); + } + if (wiiu->callback_ready) { + AXDeregisterAppFrameCallback(tal_wiiu_frame_callback); + wiiu->callback_ready = false; + } + if (wiiu->voice) { + AXVoiceBegin(wiiu->voice); + AXSetVoiceState(wiiu->voice, AX_VOICE_STATE_STOPPED); + AXVoiceEnd(wiiu->voice); + AXFreeVoice(wiiu->voice); + wiiu->voice = NULL; + } + if (wiiu->ax_owned) { + AXQuit(); + wiiu->ax_owned = false; + } + tal_wiiu_active = NULL; +} + +TalResult tal_backend_update(Tal *tal) { + (void) tal; + return TAL_RESULT_OK; +} From 7e0ba4fef167b44cf3f2ae7c06faefe79ec69817 Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 01:56:00 -0300 Subject: [PATCH 7/7] Fix wiiu aspect ratio --- .github/workflows/switch.yml | 6 -- thirdparty/twl/backends/wiiu/backend.h | 3 +- thirdparty/twl/backends/wiiu/input.c | 26 ++++++-- thirdparty/twl/backends/wiiu/presentation.c | 9 ++- thirdparty/twl/backends/wiiu/window.c | 71 ++++++++++++++++----- 5 files changed, 81 insertions(+), 34 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 1883b689..afbc0f75 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -2,8 +2,6 @@ name: Build Nintendo Switch NRO on: push: - branches: - - master paths: - '.github/workflows/switch.yml' - 'CMakeLists.txt' @@ -22,10 +20,6 @@ on: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - jobs: build: runs-on: ubuntu-latest diff --git a/thirdparty/twl/backends/wiiu/backend.h b/thirdparty/twl/backends/wiiu/backend.h index 9a127d5b..bb33b4e1 100644 --- a/thirdparty/twl/backends/wiiu/backend.h +++ b/thirdparty/twl/backends/wiiu/backend.h @@ -34,7 +34,8 @@ typedef struct { WHBGfxShaderGroup shader; - GX2RBuffer position_buffer; + GX2RBuffer tv_position_buffer; + GX2RBuffer drc_position_buffer; GX2RBuffer texcoord_buffer; GX2Texture texture; GX2Sampler sampler; diff --git a/thirdparty/twl/backends/wiiu/input.c b/thirdparty/twl/backends/wiiu/input.c index 5e89edf9..7176bbca 100644 --- a/thirdparty/twl/backends/wiiu/input.c +++ b/thirdparty/twl/backends/wiiu/input.c @@ -98,12 +98,26 @@ static void twl_wiiu_read_touch( VPADGetTPCalibratedPointEx( VPAD_CHAN_0, VPAD_TP_854X480, &touch, &status->tpNormal); if (touch.touched) { - const int32_t x = - twl_wiiu_clamp((int32_t) touch.x, 0, TWL_WIIU_TP_WIDTH - 1) * - (int32_t) wiiu->frame_width / TWL_WIIU_TP_WIDTH; - const int32_t y = - twl_wiiu_clamp((int32_t) touch.y, 0, TWL_WIIU_TP_HEIGHT - 1) * - (int32_t) wiiu->frame_height / TWL_WIIU_TP_HEIGHT; + const int32_t frame_w = (int32_t) wiiu->frame_width; + const int32_t frame_h = (int32_t) wiiu->frame_height; + int32_t content_w = TWL_WIIU_TP_WIDTH; + int32_t content_h = TWL_WIIU_TP_HEIGHT; + int32_t origin_x = 0; + int32_t origin_y = 0; + int32_t x; + int32_t y; + if ((int64_t) TWL_WIIU_TP_WIDTH * frame_h >= + (int64_t) TWL_WIIU_TP_HEIGHT * frame_w) { + content_w = TWL_WIIU_TP_HEIGHT * frame_w / frame_h; + origin_x = (TWL_WIIU_TP_WIDTH - content_w) / 2; + } else { + content_h = TWL_WIIU_TP_WIDTH * frame_h / frame_w; + origin_y = (TWL_WIIU_TP_HEIGHT - content_h) / 2; + } + x = twl_wiiu_clamp( + ((int32_t) touch.x - origin_x) * frame_w / content_w, 0, frame_w - 1); + y = twl_wiiu_clamp( + ((int32_t) touch.y - origin_y) * frame_h / content_h, 0, frame_h - 1); if (!wiiu->touching || x != wiiu->pointer_x || y != wiiu->pointer_y) { twl_wiiu_push_pointer(twl, TWL_EVENT_POINTER_MOVE, x, y); } diff --git a/thirdparty/twl/backends/wiiu/presentation.c b/thirdparty/twl/backends/wiiu/presentation.c index 5849b633..51e40a17 100644 --- a/thirdparty/twl/backends/wiiu/presentation.c +++ b/thirdparty/twl/backends/wiiu/presentation.c @@ -60,7 +60,7 @@ static void twl_wiiu_upload(TwlWiiU *wiiu, const TwlSurface *surface) { GX2RUnlockSurfaceEx(&wiiu->texture.surface, 0, 0); } -static void twl_wiiu_draw(TwlWiiU *wiiu) { +static void twl_wiiu_draw(TwlWiiU *wiiu, GX2RBuffer *positions) { GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, FALSE, FALSE); GX2SetDepthOnlyControl(FALSE, FALSE, GX2_COMPARE_FUNC_ALWAYS); GX2SetFetchShader(&wiiu->shader.fetchShader); @@ -68,8 +68,7 @@ static void twl_wiiu_draw(TwlWiiU *wiiu) { GX2SetPixelShader(wiiu->shader.pixelShader); GX2SetPixelTexture(&wiiu->texture, 0); GX2SetPixelSampler(&wiiu->sampler, 0); - GX2RSetAttributeBuffer( - &wiiu->position_buffer, 0, wiiu->position_buffer.elemSize, 0); + GX2RSetAttributeBuffer(positions, 0, positions->elemSize, 0); GX2RSetAttributeBuffer( &wiiu->texcoord_buffer, 1, wiiu->texcoord_buffer.elemSize, 0); GX2DrawEx(GX2_PRIMITIVE_MODE_TRIANGLE_STRIP, 4, 0, 1); @@ -101,12 +100,12 @@ TwlResult twl_backend_prepare_frame(Twl *twl, const TwlSurface *surface) { WHBGfxBeginRenderTV(); WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); - twl_wiiu_draw(wiiu); + twl_wiiu_draw(wiiu, &wiiu->tv_position_buffer); WHBGfxFinishRenderTV(); WHBGfxBeginRenderDRC(); WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); - twl_wiiu_draw(wiiu); + twl_wiiu_draw(wiiu, &wiiu->drc_position_buffer); WHBGfxFinishRenderDRC(); return TWL_RESULT_OK; diff --git a/thirdparty/twl/backends/wiiu/window.c b/thirdparty/twl/backends/wiiu/window.c index ec147895..5eedfe8b 100644 --- a/thirdparty/twl/backends/wiiu/window.c +++ b/thirdparty/twl/backends/wiiu/window.c @@ -34,13 +34,6 @@ #include #include -static const float twl_wiiu_positions[] = { - -1.0f, -1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 0.0f, 1.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, -}; - static const float twl_wiiu_texcoords[] = { 0.0f, 1.0f, 1.0f, 1.0f, @@ -48,6 +41,33 @@ static const float twl_wiiu_texcoords[] = { 1.0f, 0.0f, }; +static void twl_wiiu_letterbox_positions( + float *positions, uint32_t content_w, uint32_t content_h, + uint32_t screen_w, uint32_t screen_h) { + float scale_x = 1.0f; + float scale_y = 1.0f; + size_t vertex; + if (content_w > 0u && content_h > 0u && screen_w > 0u && screen_h > 0u) { + const uint64_t content = (uint64_t) content_w * screen_h; + const uint64_t screen = (uint64_t) screen_w * content_h; + if (screen > content) { + scale_x = (float) content / (float) screen; + } else if (content > screen) { + scale_y = (float) screen / (float) content; + } + } + { + const float xs[4] = {-scale_x, scale_x, -scale_x, scale_x}; + const float ys[4] = {-scale_y, -scale_y, scale_y, scale_y}; + for (vertex = 0u; vertex < 4u; ++vertex) { + positions[vertex * 4u + 0u] = xs[vertex]; + positions[vertex * 4u + 1u] = ys[vertex]; + positions[vertex * 4u + 2u] = 0.0f; + positions[vertex * 4u + 3u] = 1.0f; + } + } +} + static bool twl_wiiu_create_buffer( GX2RBuffer *buffer, uint32_t elem_size, uint32_t elem_count, const void *data) { @@ -108,8 +128,11 @@ static void twl_wiiu_teardown(TwlWiiU *wiiu) { GX2RDestroySurfaceEx(&wiiu->texture.surface, 0); wiiu->texture_ready = false; } - if (wiiu->position_buffer.flags) { - GX2RDestroyBufferEx(&wiiu->position_buffer, 0); + if (wiiu->tv_position_buffer.flags) { + GX2RDestroyBufferEx(&wiiu->tv_position_buffer, 0); + } + if (wiiu->drc_position_buffer.flags) { + GX2RDestroyBufferEx(&wiiu->drc_position_buffer, 0); } if (wiiu->texcoord_buffer.flags) { GX2RDestroyBufferEx(&wiiu->texcoord_buffer, 0); @@ -187,13 +210,29 @@ TwlResult twl_backend_init( WHBFreeWholeFile(shader_file); wiiu->shader_ready = true; - if (!twl_wiiu_create_buffer( - &wiiu->position_buffer, 4u * sizeof(float), 4u, twl_wiiu_positions) || - !twl_wiiu_create_buffer( - &wiiu->texcoord_buffer, 2u * sizeof(float), 4u, twl_wiiu_texcoords)) { - WHBLogPrint("[twl-wiiu] ERROR: attribute buffer creation failed"); - twl_wiiu_teardown(wiiu); - return TWL_RESULT_INSUFFICIENT_MEMORY; + { + const GX2ColorBuffer *tv = WHBGfxGetTVColourBuffer(); + const GX2ColorBuffer *drc = WHBGfxGetDRCColourBuffer(); + float tv_positions[16]; + float drc_positions[16]; + twl_wiiu_letterbox_positions( + tv_positions, wiiu->frame_width, wiiu->frame_height, + tv ? tv->surface.width : wiiu->frame_width, + tv ? tv->surface.height : wiiu->frame_height); + twl_wiiu_letterbox_positions( + drc_positions, wiiu->frame_width, wiiu->frame_height, + drc ? drc->surface.width : wiiu->frame_width, + drc ? drc->surface.height : wiiu->frame_height); + if (!twl_wiiu_create_buffer( + &wiiu->tv_position_buffer, 4u * sizeof(float), 4u, tv_positions) || + !twl_wiiu_create_buffer( + &wiiu->drc_position_buffer, 4u * sizeof(float), 4u, drc_positions) || + !twl_wiiu_create_buffer( + &wiiu->texcoord_buffer, 2u * sizeof(float), 4u, twl_wiiu_texcoords)) { + WHBLogPrint("[twl-wiiu] ERROR: attribute buffer creation failed"); + twl_wiiu_teardown(wiiu); + return TWL_RESULT_INSUFFICIENT_MEMORY; + } } if (!twl_wiiu_create_texture(