From a673236d5f7e10b37713d025dc5af92648a6d027 Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 02:15:17 -0300 Subject: [PATCH 1/5] Remove switch sf_exe port --- src/SF_EXE/cmake/platforms/Switch.cmake | 20 - .../platform/switch/application_loop.cpp | 13 - .../presentation/switch_surface_presenter.cpp | 159 ------ thirdparty/lal/lal_switch.c | 182 ------- thirdparty/lwl/lwl_switch.c | 469 ------------------ 5 files changed, 843 deletions(-) delete mode 100644 src/SF_EXE/cmake/platforms/Switch.cmake delete mode 100644 src/SF_EXE/runtime/platform/switch/application_loop.cpp delete mode 100644 src/SF_EXE/runtime/presentation/switch_surface_presenter.cpp delete mode 100644 thirdparty/lal/lal_switch.c delete mode 100644 thirdparty/lwl/lwl_switch.c diff --git a/src/SF_EXE/cmake/platforms/Switch.cmake b/src/SF_EXE/cmake/platforms/Switch.cmake deleted file mode 100644 index bea303e9..00000000 --- a/src/SF_EXE/cmake/platforms/Switch.cmake +++ /dev/null @@ -1,20 +0,0 @@ -function(osf_configure_switch_platform target) - target_sources( - ${target} - PRIVATE - runtime/platform/switch/application_loop.cpp - ) - target_compile_definitions(${target} PRIVATE OSF_PLATFORM_SWITCH) - - if(COMMAND nx_create_nro) - nx_create_nro( - ${target} - ICON "${PROJECT_SOURCE_DIR}/readme/sf-logo-sm.jpg" - ) - else() - message( - WARNING - "nx_create_nro is unavailable: no .nro will be produced. " - "Configure with -DCMAKE_TOOLCHAIN_FILE=$ENV{DEVKITPRO}/cmake/Switch.cmake.") - endif() -endfunction() diff --git a/src/SF_EXE/runtime/platform/switch/application_loop.cpp b/src/SF_EXE/runtime/platform/switch/application_loop.cpp deleted file mode 100644 index 4a1b66c5..00000000 --- a/src/SF_EXE/runtime/platform/switch/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 (appletMainLoop() && application->frame()) { - } - return 0; -} - -} diff --git a/src/SF_EXE/runtime/presentation/switch_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/switch_surface_presenter.cpp deleted file mode 100644 index efbc9ae5..00000000 --- a/src/SF_EXE/runtime/presentation/switch_surface_presenter.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include "runtime/presentation/surface_presenter.hpp" - -#include "lwl.h" - -#include - -#include -#include -#include -#include - -namespace { - -constexpr std::int32_t kScreenWidth = 1280; -constexpr std::int32_t kScreenHeight = 720; - -class SwitchSurfacePresenter final - : public osf::runtime::SurfacePresenter { -public: - ~SwitchSurfacePresenter() 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(); - - Framebuffer framebuffer_{}; - bool initialized_ = false; - bool frame_prepared_ = false; -}; - -bool SwitchSurfacePresenter::initialize( - LwlWindow* window, - std::string* error) { - (void) window; - shutdown(); - - if (R_FAILED(framebufferCreate( - &framebuffer_, - nwindowGetDefault(), - kScreenWidth, - kScreenHeight, - PIXEL_FORMAT_RGBA_8888, - 2)) || - R_FAILED(framebufferMakeLinear(&framebuffer_))) { - framebufferClose(&framebuffer_); - std::memset(&framebuffer_, 0, sizeof(framebuffer_)); - if (error) { - *error = "Could not create the Switch framebuffer."; - } - return false; - } - - initialized_ = true; - if (error) { - error->clear(); - } - return true; -} - -void SwitchSurfacePresenter::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 (!initialized_ || !surface.pixels || - surface.width <= 0 || surface.height <= 0) { - return; - } - - std::uint32_t stride = 0; - auto* const output = static_cast( - framebufferBegin(&framebuffer_, &stride)); - if (!output || stride < - static_cast(kScreenWidth * sizeof(*output))) { - if (output) { - framebufferEnd(&framebuffer_); - } - return; - } - - const osf::gapi::Viewport viewport = osf::gapi::fitViewport( - surface.width, - surface.height, - kScreenWidth, - kScreenHeight); - const std::uint32_t outputStride = stride / sizeof(*output); - for (std::int32_t y = 0; y < kScreenHeight; ++y) { - std::uint32_t* const row = output + y * outputStride; - for (std::int32_t x = 0; x < kScreenWidth; ++x) { - row[x] = RGBA8(0, 0, 0, 255); - } - } - - for (std::int32_t y = 0; y < viewport.height; ++y) { - const std::int32_t sourceY = - y * surface.height / viewport.height; - std::uint32_t* const row = output + - (viewport.y + y) * outputStride + viewport.x; - for (std::int32_t x = 0; x < viewport.width; ++x) { - const std::int32_t sourceX = - x * surface.width / viewport.width; - const osf::gapi::Color color = surface.pixels[ - sourceY * surface.width + sourceX]; - row[x] = RGBA8( - color.red, color.green, color.blue, color.alpha); - } - } - frame_prepared_ = true; -} - -void SwitchSurfacePresenter::displayFrame() { - if (!initialized_ || !frame_prepared_) { - return; - } - framebufferEnd(&framebuffer_); - frame_prepared_ = false; -} - -#if OSF_ENABLE_DEBUG_TOOLS -std::optional -SwitchSurfacePresenter::videoMemoryUsageBytes() const { - if (!initialized_) { - return std::nullopt; - } - constexpr std::uint64_t kBufferCount = 2; - return static_cast(kScreenWidth) * - static_cast(kScreenHeight) * - sizeof(std::uint32_t) * kBufferCount; -} -#endif - -void SwitchSurfacePresenter::shutdown() { - if (initialized_ && frame_prepared_) { - framebufferEnd(&framebuffer_); - } - frame_prepared_ = false; - if (initialized_) { - framebufferClose(&framebuffer_); - initialized_ = false; - } - std::memset(&framebuffer_, 0, sizeof(framebuffer_)); -} - -} - -std::unique_ptr -osf::runtime::createSurfacePresenter() { - return std::make_unique(); -} diff --git a/thirdparty/lal/lal_switch.c b/thirdparty/lal/lal_switch.c deleted file mode 100644 index 543963cd..00000000 --- a/thirdparty/lal/lal_switch.c +++ /dev/null @@ -1,182 +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 - -enum { - LAL_SWITCH_BUFFER_COUNT = 3, - LAL_SWITCH_OUTPUT_FRAMES = 960, - LAL_SWITCH_INPUT_FRAMES = 882 -}; - -static AudioOutBuffer g_buffers[LAL_SWITCH_BUFFER_COUNT]; -static int16_t g_samples[LAL_SWITCH_BUFFER_COUNT] - [0x1000 / sizeof(int16_t)] - __attribute__((aligned(0x1000))); -static Thread g_thread; -static Mutex g_mutex; -static bool g_mutex_ready; -static bool g_running; -static bool g_thread_started; -static bool g_audio_open; - -void lal_platform_shutdown(void); - -static bool lal_switch_is_running(void) { - bool running; - - mutexLock(&g_mutex); - running = g_running; - mutexUnlock(&g_mutex); - return running; -} - -static void lal_switch_fill(AudioOutBuffer *buffer) { - int16_t source[LAL_SWITCH_INPUT_FRAMES * LAL_OUTPUT_CHANNELS]; - int16_t *output = (int16_t *) buffer->buffer; - size_t output_index; - - mutexLock(&g_mutex); - lal_mix_frames(source, LAL_SWITCH_INPUT_FRAMES); - mutexUnlock(&g_mutex); - - /* audout is fixed at 48 kHz. 960 output frames consume exactly 882 - * 44.1-kHz mixer frames, avoiding cumulative timing drift. */ - for (output_index = 0; - output_index < LAL_SWITCH_OUTPUT_FRAMES; - ++output_index) { - const size_t source_index = output_index * 147 / 160; - output[output_index * LAL_OUTPUT_CHANNELS] = - source[source_index * LAL_OUTPUT_CHANNELS]; - output[output_index * LAL_OUTPUT_CHANNELS + 1] = - source[source_index * LAL_OUTPUT_CHANNELS + 1]; - } - buffer->data_size = - LAL_SWITCH_OUTPUT_FRAMES * LAL_OUTPUT_CHANNELS * sizeof(*output); -} - -static void lal_switch_thread(void *unused) { - (void) unused; - while (lal_switch_is_running()) { - AudioOutBuffer *released = NULL; - u32 released_count = 0; - const Result result = audoutWaitPlayFinish( - &released, &released_count, UINT64_MAX); - if (R_FAILED(result) || !lal_switch_is_running()) { - break; - } - while (released) { - AudioOutBuffer *const next = released->next; - lal_switch_fill(released); - if (R_FAILED(audoutAppendAudioOutBuffer(released))) { - mutexLock(&g_mutex); - g_running = false; - mutexUnlock(&g_mutex); - return; - } - released = next; - } - } -} - -bool lal_platform_init(void) { - int index; - - mutexInit(&g_mutex); - g_mutex_ready = true; - if (R_FAILED(audoutInitialize())) { - lal_set_error("Could not initialize Switch audio output."); - g_mutex_ready = false; - return false; - } - if (R_FAILED(audoutStartAudioOut())) { - lal_set_error("Could not start Switch audio output."); - audoutExit(); - g_mutex_ready = false; - return false; - } - g_audio_open = true; - - memset(g_buffers, 0, sizeof(g_buffers)); - for (index = 0; index < LAL_SWITCH_BUFFER_COUNT; ++index) { - g_buffers[index].buffer = g_samples[index]; - g_buffers[index].buffer_size = sizeof(g_samples[index]); - lal_switch_fill(&g_buffers[index]); - if (R_FAILED(audoutAppendAudioOutBuffer(&g_buffers[index]))) { - lal_set_error("Could not queue a Switch audio buffer."); - lal_platform_shutdown(); - return false; - } - } - - mutexLock(&g_mutex); - g_running = true; - mutexUnlock(&g_mutex); - if (R_FAILED(threadCreate( - &g_thread, - lal_switch_thread, - NULL, - NULL, - 0x4000, - 0x2B, - -2))) { - lal_set_error("Could not create the Switch audio thread."); - lal_platform_shutdown(); - return false; - } - if (R_FAILED(threadStart(&g_thread))) { - lal_set_error("Could not start the Switch audio thread."); - threadClose(&g_thread); - lal_platform_shutdown(); - return false; - } - g_thread_started = true; - return true; -} - -void lal_platform_shutdown(void) { - if (g_mutex_ready) { - mutexLock(&g_mutex); - g_running = false; - mutexUnlock(&g_mutex); - } - if (g_audio_open) { - audoutStopAudioOut(); - } - if (g_thread_started) { - threadWaitForExit(&g_thread); - threadClose(&g_thread); - g_thread_started = false; - } - if (g_audio_open) { - audoutExit(); - g_audio_open = false; - } - g_mutex_ready = false; -} - -void lal_platform_lock(void) { - if (g_mutex_ready) { - mutexLock(&g_mutex); - } -} - -void lal_platform_unlock(void) { - if (g_mutex_ready) { - mutexUnlock(&g_mutex); - } -} diff --git a/thirdparty/lwl/lwl_switch.c b/thirdparty/lwl/lwl_switch.c deleted file mode 100644 index d91db227..00000000 --- a/thirdparty/lwl/lwl_switch.c +++ /dev/null @@ -1,469 +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. - * - * LWL 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 - * LWL. If not, see . - */ - -/* - * Nintendo Switch backend. The console has no windowing: the "window" is a fixed - * 1280x720 surface (the handheld touchscreen resolution; the system upscales it - * when docked). The nxfb presenter owns the libnx framebuffer; this file provides - * input and timing. - * - * Input maps the touchscreen to the pointer (touch = move + primary click) and - * the gamepad to keys, so the game's mouse/keyboard-driven UI works unchanged. - * ZL opens the system keyboard and submits its text to the focused text field. - * Pointer coordinates are reported in the 1280x720 surface space; the game's - * InputAdapter maps them onto the 640x480 virtual surface with the same - * fitViewport the presenter uses. - * - * Compiled only for Switch (selected in CMakeLists.txt), so it is not wrapped in - * a platform #if. - */ - -#include "lwl.h" - -#include - -#include -#include -#include -#include - -#define LWL_SWITCH_WIDTH 1280 -#define LWL_SWITCH_HEIGHT 720 - -struct LwlWindow { - int width; - int height; -}; - -static struct LwlWindow g_window; - -/* --- Input: touchscreen -> pointer, gamepad -> keys --- */ - -enum { LWL_SWITCH_EVENT_CAPACITY = 32 }; - -static LwlEvent g_events[LWL_SWITCH_EVENT_CAPACITY]; -static int g_event_count; -static int g_event_read; -static bool g_polled_this_frame; - -static PadState g_pad; -static bool g_input_ready; -static bool g_was_touching; -static int g_pointer_x = LWL_SWITCH_WIDTH / 2; -static int g_pointer_y = LWL_SWITCH_HEIGHT / 2; - -static int lwl_switch_clamp(int value, int low, int high) { - if (value < low) { - return low; - } - if (value > high) { - return high; - } - return value; -} - -static LwlEvent *lwl_switch_push(LwlEventType type) { - LwlEvent *event; - if (g_event_count >= LWL_SWITCH_EVENT_CAPACITY) { - return NULL; - } - event = &g_events[g_event_count++]; - memset(event, 0, sizeof(*event)); - event->type = type; - return event; -} - -static void lwl_switch_push_pointer(LwlEventType type, int button) { - LwlEvent *event = lwl_switch_push(type); - if (event) { - event->x = g_pointer_x; - event->y = g_pointer_y; - event->button = button; - event->clicks = 1; - } -} - -static void lwl_switch_push_key(LwlEventType type, const char *key) { - LwlEvent *event = lwl_switch_push(type); - if (event) { - strncpy(event->key, key, sizeof(event->key) - 1); - } -} - -static void lwl_switch_show_keyboard(void) { - SwkbdConfig keyboard; - char text[64] = {0}; - - if (R_FAILED(swkbdCreate(&keyboard, 0))) { - return; - } - swkbdConfigMakePresetUserName(&keyboard); - swkbdConfigSetHeaderText(&keyboard, "Character name"); - swkbdConfigSetGuideText(&keyboard, "Enter a name for your character."); - swkbdConfigSetStringLenMax(&keyboard, 15); - - if (R_SUCCEEDED(swkbdShow(&keyboard, text, sizeof(text))) && - text[0] != '\0') { - LwlEvent *event = lwl_switch_push(LWL_EVENT_TEXT_INPUT); - if (event) { - strncpy(event->text, text, sizeof(event->text) - 1); - } - } - swkbdClose(&keyboard); -} - -struct LwlSwitchKeyMapping { - u64 mask; - const char *key; -}; - -static const struct LwlSwitchKeyMapping g_key_mappings[] = { - {HidNpadButton_Up, "up"}, - {HidNpadButton_Down, "down"}, - {HidNpadButton_Left, "left"}, - {HidNpadButton_Right, "right"}, - {HidNpadButton_A, "return"}, - {HidNpadButton_Plus, "return"}, - {HidNpadButton_B, "escape"}, - {HidNpadButton_Minus, "escape"}, - {HidNpadButton_X, "i"}, - {HidNpadButton_Y, "n"}, - {HidNpadButton_L, "q"}, - {HidNpadButton_R, "r"}, -}; - -static void lwl_switch_sample_input(void) { - HidTouchScreenState touch; - u64 pressed; - u64 released; - size_t index; - - g_event_count = 0; - g_event_read = 0; - if (!g_input_ready) { - return; - } - - padUpdate(&g_pad); - pressed = padGetButtonsDown(&g_pad); - released = padGetButtonsUp(&g_pad); - - /* ZL is otherwise unused by the game's native controller mapping. */ - if (pressed & HidNpadButton_ZL) { - lwl_switch_show_keyboard(); - } - - /* Touchscreen -> pointer + primary button. */ - memset(&touch, 0, sizeof(touch)); - if (hidGetTouchScreenStates(&touch, 1) && touch.count > 0) { - const int x = lwl_switch_clamp( - (int) touch.touches[0].x, 0, LWL_SWITCH_WIDTH - 1); - const int y = lwl_switch_clamp( - (int) touch.touches[0].y, 0, LWL_SWITCH_HEIGHT - 1); - if (x != g_pointer_x || y != g_pointer_y || !g_was_touching) { - LwlEvent *move = lwl_switch_push(LWL_EVENT_MOUSE_MOVE); - if (move) { - move->dx = x - g_pointer_x; - move->dy = y - g_pointer_y; - g_pointer_x = x; - g_pointer_y = y; - move->x = g_pointer_x; - move->y = g_pointer_y; - } else { - g_pointer_x = x; - g_pointer_y = y; - } - } - if (!g_was_touching) { - lwl_switch_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); - g_was_touching = true; - } - } else if (g_was_touching) { - lwl_switch_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 (pressed & g_key_mappings[index].mask) { - lwl_switch_push_key(LWL_EVENT_KEY_DOWN, g_key_mappings[index].key); - } - if (released & g_key_mappings[index].mask) { - lwl_switch_push_key(LWL_EVENT_KEY_UP, g_key_mappings[index].key); - } - } -} - -bool lwl_init(void) { - padConfigureInput(1, HidNpadStyleSet_NpadStandard); - padInitializeDefault(&g_pad); - hidInitializeTouchScreen(); - g_input_ready = true; - g_was_touching = false; - g_pointer_x = LWL_SWITCH_WIDTH / 2; - g_pointer_y = LWL_SWITCH_HEIGHT / 2; - g_event_count = 0; - g_event_read = 0; - g_polled_this_frame = false; - return true; -} - -void lwl_shutdown(void) { -} - -LwlWindow *lwl_window_create(const char *title, int width, int height) { - (void) title; - (void) width; - (void) height; - g_window.width = LWL_SWITCH_WIDTH; - g_window.height = LWL_SWITCH_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_SWITCH_WIDTH; - } - if (height) { - *height = window ? window->height : LWL_SWITCH_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_switch_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) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double) ts.tv_sec + (double) ts.tv_nsec / 1000000000.0; -} - -void lwl_sleep_seconds(double seconds) { - if (seconds <= 0.0) { - return; - } - svcSleepThread((s64) (seconds * 1000000000.0)); -} - -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 "switch"; -} - -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 = 3; - config.double_buffer = true; - return config; -} - -/* The Switch presents through the libnx framebuffer; no LWL GL context exists. */ -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; -} From 5ec44687b84cbc6093ad03c27aee5f3af54d577f Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 05:37:05 -0300 Subject: [PATCH 2/5] Fix switch sf_exe build revert --- .github/workflows/switch.yml | 20 ++++-- CMakeLists.txt | 17 +++-- documentation/switch-port.md | 69 ++++++------------- shadowflare/CMakeLists.txt | 12 ++++ shadowflare/assets/retail_paths.c | 3 + src/SF_EXE/cmake/configure_platform.cmake | 3 - src/SF_EXE/cmake/configure_presentation.cmake | 6 -- thirdparty/lal/CMakeLists.txt | 2 - thirdparty/lwl/CMakeLists.txt | 2 - tools/switch/build-nro.sh | 19 +++-- 10 files changed, 71 insertions(+), 82 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index afbc0f75..00846827 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -2,24 +2,32 @@ name: Build Nintendo Switch NRO on: push: + branches: + - master paths: - '.github/workflows/switch.yml' - 'CMakeLists.txt' - - 'src/**' - - 'thirdparty/**' + - 'shadowflare/**' + - 'thirdparty/twl/**' + - 'thirdparty/tal/**' - 'tools/switch/**' pull_request: paths: - '.github/workflows/switch.yml' - 'CMakeLists.txt' - - 'src/**' - - 'thirdparty/**' + - 'shadowflare/**' + - 'thirdparty/twl/**' + - 'thirdparty/tal/**' - 'tools/switch/**' workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -34,11 +42,11 @@ jobs: run: sh tools/switch/build-nro.sh - name: Verify the NRO - run: test -s build/switch/release/src/SF_EXE/ShadowFlare_rebuilt.nro + run: test -s build/switch/release/OpenShadowFlare.nro - name: Upload the NRO uses: actions/upload-artifact@v4 with: name: openshadowflare-switch-release - path: build/switch/release/src/SF_EXE/ShadowFlare_rebuilt.nro + path: build/switch/release/OpenShadowFlare.nro if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index fa0f2a4b..10855535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,11 +24,7 @@ option( ON ) -if(NINTENDO_SWITCH) - set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "nxfb") -else() - set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "lgl") -endif() +set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "lgl") set( OPENSHADOWFLARE_PRESENTATION_BACKEND "${OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND}" @@ -37,14 +33,17 @@ set( ) set_property( CACHE OPENSHADOWFLARE_PRESENTATION_BACKEND - PROPERTY STRINGS lgl nxfb + PROPERTY STRINGS lgl ) -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/switch-port.md b/documentation/switch-port.md index bca154cb..a79b25a5 100644 --- a/documentation/switch-port.md +++ b/documentation/switch-port.md @@ -1,70 +1,45 @@ # Nintendo Switch port -OpenShadowFlare builds as a Nintendo Switch homebrew application (`.nro`) via -libnx. The NRO does not include the original game data. +The Switch build compiles the small C99 ShadowFlare runtime and packages it as a +libnx homebrew application (`.nro`). It does not include retail game data. -## Build locally +## Install the toolchain -Install devkitPro's Switch environment (`devkitA64`, `libnx`, and -`switch-tools`). In the devkitPro MSYS2 shell or a POSIX shell on Linux, run from the repository root: +Install devkitPro's Switch toolchain and Ninja: ```bash -sh tools/switch/build-nro.sh +sudo dkp-pacman -S --needed switch-dev ninja ``` -The script uses devkitPro's Switch CMake toolchain and Ninja, configures a -release build, disables host-only tests, and writes the result to: +Set `DEVKITPRO` if devkitPro is not installed at `/opt/devkitpro`. -```text -build/switch/release/src/SF_EXE/ShadowFlare_rebuilt.nro -``` +## Build -If `build/switch/release` was previously configured with another CMake -generator, remove it once before running the script: +From the repository root: ```bash -rm -rf build/switch/release +sh tools/switch/build-nro.sh ``` -## GitHub Actions - -`Build Nintendo Switch NRO` runs the same script in the pinned devkitPro -container and uploads `openshadowflare-switch-release` as a workflow artifact. -Download that artifact to test a CI build; no local devkitPro or Android Studio -installation is required. - -## Add game data - -On Switch hardware, place the NRO beside a `ShadowFlare` directory: +The application is written to: ```text -sdmc:/switch/OpenShadowFlare/ - ShadowFlare_rebuilt.nro - ShadowFlare/ - SFlare.Cfg - System/ - ... +build/switch/release/OpenShadowFlare.nro ``` +## Move the game files + +The retail data is loaded from the SD card and is never bundled. Copy your +ShadowFlare `System` folder and `SFlare.Cfg` into a `ShadowFlare` folder at the +SD root: + ```text -/sdcard/switch/ShadowFlare/ +sdmc:/ShadowFlare/ SFlare.Cfg System/ - ... + ... ``` -Load the NRO with **File > Load Application** - -## Controls - -| Switch input | Game action | -| --- | --- | -| Touchscreen | Pointer movement 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 | Open the character-name keyboard | +On an emulator this is the `sdcard/ShadowFlare/` folder inside its data +directory. Copy `OpenShadowFlare.nro` to `sdmc:/switch/` and launch it from the +homebrew menu. diff --git a/shadowflare/CMakeLists.txt b/shadowflare/CMakeLists.txt index 8dd2de94..a0de1b90 100644 --- a/shadowflare/CMakeLists.txt +++ b/shadowflare/CMakeLists.txt @@ -179,3 +179,15 @@ if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) Twl::Twl ) endif() + +if(NINTENDO_SWITCH) + target_compile_definitions( + shadowflare_core PRIVATE SF_RETAIL_ROOT_FALLBACK="sdmc:/ShadowFlare") + + if(NOT COMMAND nx_create_nro) + message(FATAL_ERROR + "nx_create_nro is unavailable. Configure the Switch build with " + "-DCMAKE_TOOLCHAIN_FILE=$ENV{DEVKITPRO}/cmake/Switch.cmake.") + endif() + nx_create_nro(osf ICON "${PROJECT_SOURCE_DIR}/readme/sf-logo-sm.jpg") +endif() diff --git a/shadowflare/assets/retail_paths.c b/shadowflare/assets/retail_paths.c index 87e28bb9..ce05e7b4 100644 --- a/shadowflare/assets/retail_paths.c +++ b/shadowflare/assets/retail_paths.c @@ -151,6 +151,9 @@ 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 + 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 456288fc..7ca33e6c 100644 --- a/src/SF_EXE/cmake/configure_platform.cmake +++ b/src/SF_EXE/cmake/configure_platform.cmake @@ -2,9 +2,6 @@ function(osf_configure_platform target) if(EMSCRIPTEN) include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Emscripten.cmake") osf_configure_emscripten_platform(${target}) - elseif(NINTENDO_SWITCH) - include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Switch.cmake") - osf_configure_switch_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..1a77d284 100644 --- a/src/SF_EXE/cmake/configure_presentation.cmake +++ b/src/SF_EXE/cmake/configure_presentation.cmake @@ -6,12 +6,6 @@ function(osf_configure_presentation target) runtime/presentation/lgl_surface_presenter.cpp ) target_link_libraries(${target} PRIVATE Lgl::Lgl) - elseif(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "nxfb") - target_sources( - ${target} - PRIVATE - runtime/presentation/switch_surface_presenter.cpp - ) else() message(FATAL_ERROR "Unsupported presentation backend: " diff --git a/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index 6d19e0ec..7a99cc14 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -87,8 +87,6 @@ elseif(APPLE) "${AUDIOTOOLBOX_FRAMEWORK}" Threads::Threads ) -elseif(NINTENDO_SWITCH) - target_sources(lal PRIVATE lal_switch.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lal PRIVATE lal_alsa.c) find_package(ALSA REQUIRED) diff --git a/thirdparty/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index f27651fc..65026b23 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -62,8 +62,6 @@ elseif(APPLE) "${COREGRAPHICS_FRAMEWORK}" "${OPENGL_FRAMEWORK}" ) -elseif(NINTENDO_SWITCH) - target_sources(lwl PRIVATE lwl_switch.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lwl PRIVATE lwl_x11.c) find_package(OpenGL REQUIRED) diff --git a/tools/switch/build-nro.sh b/tools/switch/build-nro.sh index 28e3b85c..e411e08b 100644 --- a/tools/switch/build-nro.sh +++ b/tools/switch/build-nro.sh @@ -6,30 +6,35 @@ ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) DEVKITPRO=${DEVKITPRO:-/opt/devkitpro} TOOLCHAIN_FILE="$DEVKITPRO/cmake/Switch.cmake" BUILD_DIR="$ROOT_DIR/build/switch/release" -NRO="$BUILD_DIR/src/SF_EXE/ShadowFlare_rebuilt.nro" +OUTPUT_NRO="$BUILD_DIR/OpenShadowFlare.nro" if [ ! -f "$TOOLCHAIN_FILE" ]; then echo "Nintendo Switch devkitPro tools were not found under: $DEVKITPRO" >&2 exit 1 fi -export DEVKITPRO - if ! command -v ninja >/dev/null 2>&1; then echo "Ninja is required to build the Switch port." >&2 exit 1 fi +export DEVKITPRO + cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \ -G Ninja \ -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \ -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=OFF + -DBUILD_TESTING=OFF \ + -DOPENSHADOWFLARE_BUILD_EXE=OFF cmake --build "$BUILD_DIR" --parallel -if [ ! -s "$NRO" ]; then - echo "CMake did not produce the expected NRO: $NRO" >&2 +NRO=$(find "$BUILD_DIR" -type f -name '*.nro' ! -path "$OUTPUT_NRO" -print -quit) +if [ -z "$NRO" ]; then + echo "CMake did not produce an NRO under: $BUILD_DIR" >&2 exit 1 fi -echo "NRO: $NRO" +if [ "$NRO" != "$OUTPUT_NRO" ]; then + cp "$NRO" "$OUTPUT_NRO" +fi +echo "NRO: $OUTPUT_NRO" From 859ac879e6a7cf9e9c485f1355b3cb062f016e45 Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 05:57:28 -0300 Subject: [PATCH 3/5] Add switch twl/tal backends --- thirdparty/tal/CMakeLists.txt | 2 + thirdparty/tal/tal_switch.c | 226 ++++++++++++++++++ thirdparty/twl/CMakeLists.txt | 8 + thirdparty/twl/backends/switch/backend.h | 48 ++++ thirdparty/twl/backends/switch/input.c | 150 ++++++++++++ thirdparty/twl/backends/switch/presentation.c | 88 +++++++ thirdparty/twl/backends/switch/window.c | 100 ++++++++ 7 files changed, 622 insertions(+) create mode 100644 thirdparty/tal/tal_switch.c create mode 100644 thirdparty/twl/backends/switch/backend.h create mode 100644 thirdparty/twl/backends/switch/input.c create mode 100644 thirdparty/twl/backends/switch/presentation.c create mode 100644 thirdparty/twl/backends/switch/window.c diff --git a/thirdparty/tal/CMakeLists.txt b/thirdparty/tal/CMakeLists.txt index 65935042..5ff8909b 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(NINTENDO_SWITCH) + target_sources(tal PRIVATE tal_switch.c) else() target_sources(tal PRIVATE tal_null.c) endif() diff --git a/thirdparty/tal/tal_switch.c b/thirdparty/tal/tal_switch.c new file mode 100644 index 00000000..43a5853a --- /dev/null +++ b/thirdparty/tal/tal_switch.c @@ -0,0 +1,226 @@ +/* + * 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 + +#define TAL_SWITCH_BUFFER_COUNT 3u +#define TAL_SWITCH_OUTPUT_FRAMES 640u +#define TAL_SWITCH_OUTPUT_CHANNELS 2u +#define TAL_SWITCH_BUFFER_BYTES 0x1000u +#define TAL_SWITCH_MAX_INPUT_FRAMES 512u +#define TAL_SWITCH_THREAD_STACK 0x4000u +#define TAL_SWITCH_THREAD_PRIORITY 0x2B +#define TAL_SWITCH_THREAD_CORE (-2) + +typedef struct { + Tal *tal; + AudioOutBuffer buffers[TAL_SWITCH_BUFFER_COUNT]; + int16_t *samples[TAL_SWITCH_BUFFER_COUNT]; + Thread thread; + Mutex mutex; + uint32_t out_rate; + uint32_t input_frames; + bool mutex_ready; + bool audio_open; + bool thread_started; + bool running; +} TalSwitch; + +static size_t tal_switch_header_size(void) { + return (sizeof(TalSwitch) + (TAL_SWITCH_BUFFER_BYTES - 1u)) & + ~(size_t) (TAL_SWITCH_BUFFER_BYTES - 1u); +} + +size_t tal_backend_memory_alignment(void) { + return TAL_SWITCH_BUFFER_BYTES; +} + +size_t tal_backend_memory_required(const TalConfig *config) { + (void) config; + return tal_switch_header_size() + + (size_t) TAL_SWITCH_BUFFER_COUNT * TAL_SWITCH_BUFFER_BYTES; +} + +void tal_backend_lock(Tal *tal) { + TalSwitch *sw = tal ? (TalSwitch *) tal->backend : NULL; + if (sw && sw->mutex_ready) mutexLock(&sw->mutex); +} + +void tal_backend_unlock(Tal *tal) { + TalSwitch *sw = tal ? (TalSwitch *) tal->backend : NULL; + if (sw && sw->mutex_ready) mutexUnlock(&sw->mutex); +} + +static bool tal_switch_is_running(TalSwitch *sw) { + bool running; + mutexLock(&sw->mutex); + running = sw->running; + mutexUnlock(&sw->mutex); + return running; +} + +static void tal_switch_fill(TalSwitch *sw, AudioOutBuffer *buffer) { + int16_t source[TAL_SWITCH_MAX_INPUT_FRAMES]; + int16_t *output = (int16_t *) buffer->buffer; + const uint32_t sample_rate = sw->tal->config.sample_rate; + uint32_t frame; + + mutexLock(&sw->mutex); + if (tal_internal_render(sw->tal, source, sw->input_frames) != TAL_RESULT_OK) { + tal_internal_zero(source, (size_t) sw->input_frames * sizeof(int16_t)); + } + mutexUnlock(&sw->mutex); + + for (frame = 0u; frame < TAL_SWITCH_OUTPUT_FRAMES; ++frame) { + uint32_t index = + (uint32_t) ((uint64_t) frame * sample_rate / sw->out_rate); + if (index >= sw->input_frames) index = sw->input_frames - 1u; + output[frame * TAL_SWITCH_OUTPUT_CHANNELS] = source[index]; + output[frame * TAL_SWITCH_OUTPUT_CHANNELS + 1u] = source[index]; + } + + buffer->data_size = + TAL_SWITCH_OUTPUT_FRAMES * TAL_SWITCH_OUTPUT_CHANNELS * sizeof(int16_t); + armDCacheFlush(output, buffer->data_size); +} + +static void tal_switch_thread(void *arg) { + TalSwitch *sw = (TalSwitch *) arg; + while (tal_switch_is_running(sw)) { + AudioOutBuffer *released = NULL; + uint32_t released_count = 0u; + if (R_FAILED(audoutWaitPlayFinish( + &released, &released_count, UINT64_MAX)) || + !tal_switch_is_running(sw)) { + break; + } + while (released) { + AudioOutBuffer *const next = released->next; + tal_switch_fill(sw, released); + if (R_FAILED(audoutAppendAudioOutBuffer(released))) { + mutexLock(&sw->mutex); + sw->running = false; + mutexUnlock(&sw->mutex); + return; + } + released = next; + } + } +} + +TalResult tal_backend_init( + Tal *tal, void *memory, size_t memory_size, const TalConfig *config) { + TalSwitch *sw; + size_t header; + uint32_t index; + 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; + } + + sw = (TalSwitch *) memory; + sw->tal = tal; + header = tal_switch_header_size(); + for (index = 0u; index < TAL_SWITCH_BUFFER_COUNT; ++index) { + sw->samples[index] = (int16_t *) + ((uint8_t *) memory + header + (size_t) index * TAL_SWITCH_BUFFER_BYTES); + } + + mutexInit(&sw->mutex); + sw->mutex_ready = true; + + if (R_FAILED(audoutInitialize())) { + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + if (R_FAILED(audoutStartAudioOut())) { + audoutExit(); + return TAL_RESULT_BACKEND_FAILURE; + } + sw->audio_open = true; + + sw->out_rate = audoutGetSampleRate(); + if (sw->out_rate == 0u) sw->out_rate = 48000u; + sw->input_frames = (uint32_t) ( + ((uint64_t) TAL_SWITCH_OUTPUT_FRAMES * config->sample_rate + + sw->out_rate - 1u) / sw->out_rate); + if (sw->input_frames == 0u) sw->input_frames = 1u; + if (sw->input_frames > TAL_SWITCH_MAX_INPUT_FRAMES) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + + tal_internal_zero(sw->buffers, sizeof(sw->buffers)); + for (index = 0u; index < TAL_SWITCH_BUFFER_COUNT; ++index) { + sw->buffers[index].buffer = sw->samples[index]; + sw->buffers[index].buffer_size = TAL_SWITCH_BUFFER_BYTES; + tal_switch_fill(sw, &sw->buffers[index]); + if (R_FAILED(audoutAppendAudioOutBuffer(&sw->buffers[index]))) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_FAILURE; + } + } + + mutexLock(&sw->mutex); + sw->running = true; + mutexUnlock(&sw->mutex); + if (R_FAILED(threadCreate( + &sw->thread, tal_switch_thread, sw, NULL, + TAL_SWITCH_THREAD_STACK, TAL_SWITCH_THREAD_PRIORITY, + TAL_SWITCH_THREAD_CORE)) || + R_FAILED(threadStart(&sw->thread))) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_FAILURE; + } + sw->thread_started = true; + return TAL_RESULT_OK; +} + +void tal_backend_shutdown(Tal *tal) { + TalSwitch *sw = tal ? (TalSwitch *) tal->backend : NULL; + if (!sw) return; + if (sw->mutex_ready) { + mutexLock(&sw->mutex); + sw->running = false; + mutexUnlock(&sw->mutex); + } + + if (sw->audio_open) { + audoutStopAudioOut(); + } + if (sw->thread_started) { + threadWaitForExit(&sw->thread); + threadClose(&sw->thread); + sw->thread_started = false; + } + if (sw->audio_open) { + audoutExit(); + sw->audio_open = false; + } + sw->mutex_ready = false; +} + +TalResult tal_backend_update(Tal *tal) { + (void) tal; + return TAL_RESULT_OK; +} diff --git a/thirdparty/twl/CMakeLists.txt b/thirdparty/twl/CMakeLists.txt index 5e4388ae..a3076652 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(NINTENDO_SWITCH) + target_sources( + twl + PRIVATE + backends/switch/window.c + backends/switch/input.c + backends/switch/presentation.c + ) else() target_sources(twl PRIVATE backends/null/backend.c) endif() diff --git a/thirdparty/twl/backends/switch/backend.h b/thirdparty/twl/backends/switch/backend.h new file mode 100644 index 00000000..23981537 --- /dev/null +++ b/thirdparty/twl/backends/switch/backend.h @@ -0,0 +1,48 @@ +/* + * 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_SWITCH_BACKEND_H +#define TWL_SWITCH_BACKEND_H + +#include "twl_internal.h" +#include + +/* The default nwindow framebuffer is 1280x720; the system scales it to the + * handheld screen or the docked TV output. */ +#define TWL_SWITCH_SCREEN_WIDTH 1280 +#define TWL_SWITCH_SCREEN_HEIGHT 720 + +typedef struct { + Framebuffer framebuffer; + PadState pad; + uint32_t frame_width; + uint32_t frame_height; + int32_t view_x; + int32_t view_y; + int32_t view_w; + int32_t view_h; + int32_t pointer_x; + int32_t pointer_y; + bool touching; + bool fb_ready; + bool pad_ready; + bool frame_prepared; + bool quit_pushed; +} TwlSwitch; + +#endif diff --git a/thirdparty/twl/backends/switch/input.c b/thirdparty/twl/backends/switch/input.c new file mode 100644 index 00000000..4e4c33a3 --- /dev/null +++ b/thirdparty/twl/backends/switch/input.c @@ -0,0 +1,150 @@ +/* + * 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 + +static const struct { + uint64_t mask; + TwlKey key; +} twl_switch_button_keys[] = { + {HidNpadButton_Up, TWL_KEY_UP}, + {HidNpadButton_Down, TWL_KEY_DOWN}, + {HidNpadButton_Left, TWL_KEY_LEFT}, + {HidNpadButton_Right, TWL_KEY_RIGHT}, + {HidNpadButton_A, TWL_KEY_RETURN}, + {HidNpadButton_Plus, TWL_KEY_RETURN}, + {HidNpadButton_B, TWL_KEY_ESCAPE}, + {HidNpadButton_Minus, TWL_KEY_ESCAPE}, + {HidNpadButton_X, TWL_KEY_I}, + {HidNpadButton_R, TWL_KEY_R}, +}; + +static int32_t twl_switch_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_switch_push(Twl *twl, TwlEvent *event) { + event->timestamp_us = twl_backend_time_microseconds(twl); + twl_internal_push_event(twl, event); +} + +static void twl_switch_push_key(Twl *twl, TwlEventType type, TwlKey key) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = type; + event.key = key; + twl_switch_push(twl, &event); +} + +static void twl_switch_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_switch_push(twl, &event); +} + +static void twl_switch_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_switch_push(twl, &event); + } +} + +static void twl_switch_read_touch(Twl *twl, TwlSwitch *sw) { + HidTouchScreenState touch; + memset(&touch, 0, sizeof(touch)); + if (hidGetTouchScreenStates(&touch, 1) && touch.count > 0) { + const int32_t x = twl_switch_clamp( + ((int32_t) touch.touches[0].x - sw->view_x) * + (int32_t) sw->frame_width / sw->view_w, + 0, (int32_t) sw->frame_width - 1); + const int32_t y = twl_switch_clamp( + ((int32_t) touch.touches[0].y - sw->view_y) * + (int32_t) sw->frame_height / sw->view_h, + 0, (int32_t) sw->frame_height - 1); + if (!sw->touching || x != sw->pointer_x || y != sw->pointer_y) { + twl_switch_push_pointer(twl, TWL_EVENT_POINTER_MOVE, x, y); + } + if (!sw->touching) { + twl_switch_push_pointer(twl, TWL_EVENT_POINTER_DOWN, x, y); + } + sw->pointer_x = x; + sw->pointer_y = y; + sw->touching = true; + } else if (sw->touching) { + twl_switch_push_pointer( + twl, TWL_EVENT_POINTER_UP, sw->pointer_x, sw->pointer_y); + sw->touching = false; + } +} + +void twl_backend_pump_events(Twl *twl) { + TwlSwitch *sw = twl ? (TwlSwitch *) twl->backend : NULL; + uint64_t pressed; + uint64_t released; + size_t index; + if (!sw) { + return; + } + if (!appletMainLoop()) { + if (!sw->quit_pushed) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = TWL_EVENT_QUIT; + twl_switch_push(twl, &event); + sw->quit_pushed = true; + } + return; + } + if (!sw->pad_ready) { + return; + } + + padUpdate(&sw->pad); + pressed = padGetButtonsDown(&sw->pad); + released = padGetButtonsUp(&sw->pad); + + twl_switch_read_touch(twl, sw); + + for (index = 0; + index < sizeof(twl_switch_button_keys) / + sizeof(twl_switch_button_keys[0]); + ++index) { + const uint64_t mask = twl_switch_button_keys[index].mask; + if (pressed & mask) { + twl_switch_push_key( + twl, TWL_EVENT_KEY_DOWN, twl_switch_button_keys[index].key); + } + if (released & mask) { + twl_switch_push_key( + twl, TWL_EVENT_KEY_UP, twl_switch_button_keys[index].key); + } + } + + if (pressed & HidNpadButton_ZL) { + twl_switch_push_text(twl, "Player"); + } +} diff --git a/thirdparty/twl/backends/switch/presentation.c b/thirdparty/twl/backends/switch/presentation.c new file mode 100644 index 00000000..43220032 --- /dev/null +++ b/thirdparty/twl/backends/switch/presentation.c @@ -0,0 +1,88 @@ +/* + * 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 + +TwlResult twl_backend_prepare_frame(Twl *twl, const TwlSurface *surface) { + TwlSwitch *sw = twl ? (TwlSwitch *) twl->backend : NULL; + uint32_t stride; + uint32_t *output; + uint32_t output_stride; + const uint16_t *src; + size_t src_stride; + int32_t y; + int32_t x; + if (!sw || !sw->fb_ready || !surface || !surface->pixels) { + return TWL_RESULT_INVALID_ARGUMENT; + } + if (surface->format != TWL_PIXEL_RGB555 || + surface->width != sw->frame_width || + surface->height != sw->frame_height) { + return TWL_RESULT_INVALID_ARGUMENT; + } + + output = (uint32_t *) framebufferBegin(&sw->framebuffer, &stride); + if (!output || + stride < (uint32_t) (TWL_SWITCH_SCREEN_WIDTH * sizeof(uint32_t))) { + if (output) framebufferEnd(&sw->framebuffer); + return TWL_RESULT_BACKEND_FAILURE; + } + output_stride = stride / (uint32_t) sizeof(uint32_t); + src = (const uint16_t *) surface->pixels; + src_stride = surface->stride_bytes / sizeof(uint16_t); + + for (y = 0; y < TWL_SWITCH_SCREEN_HEIGHT; ++y) { + uint32_t *row = output + (uint32_t) y * output_stride; + for (x = 0; x < TWL_SWITCH_SCREEN_WIDTH; ++x) { + row[x] = RGBA8(0, 0, 0, 255); + } + } + + for (y = 0; y < sw->view_h; ++y) { + const int32_t source_y = y * (int32_t) sw->frame_height / sw->view_h; + uint32_t *row = + output + (uint32_t) (sw->view_y + y) * output_stride + + (uint32_t) sw->view_x; + const uint16_t *source_row = src + (size_t) source_y * src_stride; + for (x = 0; x < sw->view_w; ++x) { + const int32_t source_x = x * (int32_t) sw->frame_width / sw->view_w; + const uint16_t pixel = source_row[source_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); + row[x] = RGBA8( + (uint8_t) ((r5 << 3) | (r5 >> 2)), + (uint8_t) ((g5 << 3) | (g5 >> 2)), + (uint8_t) ((b5 << 3) | (b5 >> 2)), + 255); + } + } + + sw->frame_prepared = true; + return TWL_RESULT_OK; +} + +TwlResult twl_backend_display_frame(Twl *twl) { + TwlSwitch *sw = twl ? (TwlSwitch *) twl->backend : NULL; + if (!sw || !sw->fb_ready) { + return TWL_RESULT_INVALID_ARGUMENT; + } + if (sw->frame_prepared) { + framebufferEnd(&sw->framebuffer); + sw->frame_prepared = false; + } + return TWL_RESULT_OK; +} diff --git a/thirdparty/twl/backends/switch/window.c b/thirdparty/twl/backends/switch/window.c new file mode 100644 index 00000000..51f6a0e2 --- /dev/null +++ b/thirdparty/twl/backends/switch/window.c @@ -0,0 +1,100 @@ +/* + * 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. + */ + +/* + * Switch window/lifecycle backend. libnx presents through a linear nwindow + * framebuffer that the CPU writes directly, so there is no shader or GPU setup; + * the finished RGB555 frame is scaled into an aspect-fit rectangle in + * presentation.c. HID is set up here and read in input.c. + */ + +#include "backend.h" + +static void twl_switch_fit_viewport(TwlSwitch *sw) { + const int32_t content_w = (int32_t) sw->frame_width; + const int32_t content_h = (int32_t) sw->frame_height; + if ((int64_t) content_w * TWL_SWITCH_SCREEN_HEIGHT >= + (int64_t) content_h * TWL_SWITCH_SCREEN_WIDTH) { + sw->view_w = TWL_SWITCH_SCREEN_WIDTH; + sw->view_h = content_h * TWL_SWITCH_SCREEN_WIDTH / content_w; + } else { + sw->view_h = TWL_SWITCH_SCREEN_HEIGHT; + sw->view_w = content_w * TWL_SWITCH_SCREEN_HEIGHT / content_h; + } + sw->view_x = (TWL_SWITCH_SCREEN_WIDTH - sw->view_w) / 2; + sw->view_y = (TWL_SWITCH_SCREEN_HEIGHT - sw->view_h) / 2; +} + +size_t twl_backend_memory_alignment(void) { + return _Alignof(TwlSwitch); +} + +size_t twl_backend_memory_required(const TwlConfig *config) { + (void) config; + return sizeof(TwlSwitch); +} + +TwlResult twl_backend_init( + Twl *twl, void *memory, size_t memory_size, const TwlConfig *config) { + TwlSwitch *sw; + if (!twl || !memory || memory_size < sizeof(TwlSwitch) || !config) { + return TWL_RESULT_INVALID_ARGUMENT; + } + sw = (TwlSwitch *) memory; + sw->frame_width = config->width ? config->width : 640u; + sw->frame_height = config->height ? config->height : 480u; + + if (R_FAILED(framebufferCreate( + &sw->framebuffer, nwindowGetDefault(), + TWL_SWITCH_SCREEN_WIDTH, TWL_SWITCH_SCREEN_HEIGHT, + PIXEL_FORMAT_RGBA_8888, 2)) || + R_FAILED(framebufferMakeLinear(&sw->framebuffer))) { + framebufferClose(&sw->framebuffer); + return TWL_RESULT_BACKEND_FAILURE; + } + sw->fb_ready = true; + + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + padInitializeDefault(&sw->pad); + hidInitializeTouchScreen(); + sw->pad_ready = true; + sw->pointer_x = (int32_t) (sw->frame_width / 2u); + sw->pointer_y = (int32_t) (sw->frame_height / 2u); + + twl_switch_fit_viewport(sw); + twl_internal_set_display_size(twl, sw->frame_width, sw->frame_height); + return TWL_RESULT_OK; +} + +void twl_backend_shutdown(Twl *twl) { + TwlSwitch *sw = twl ? (TwlSwitch *) twl->backend : NULL; + if (!sw) { + return; + } + if (sw->fb_ready) { + framebufferClose(&sw->framebuffer); + sw->fb_ready = false; + } +} + +uint64_t twl_backend_time_microseconds(const Twl *twl) { + (void) twl; + return armTicksToNs(armGetSystemTick()) / UINT64_C(1000); +} + +void twl_backend_sleep_microseconds(Twl *twl, uint64_t duration) { + (void) twl; + svcSleepThread((int64_t) (duration * UINT64_C(1000))); +} From dd2d8d43f12b6dc56417c8f95967d0eb603a562f Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 06:14:23 -0300 Subject: [PATCH 4/5] Fix clearing screen in every frame --- thirdparty/twl/backends/switch/backend.h | 3 +++ thirdparty/twl/backends/switch/presentation.c | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/thirdparty/twl/backends/switch/backend.h b/thirdparty/twl/backends/switch/backend.h index 23981537..b524318b 100644 --- a/thirdparty/twl/backends/switch/backend.h +++ b/thirdparty/twl/backends/switch/backend.h @@ -26,6 +26,7 @@ * handheld screen or the docked TV output. */ #define TWL_SWITCH_SCREEN_WIDTH 1280 #define TWL_SWITCH_SCREEN_HEIGHT 720 +#define TWL_SWITCH_MAX_BUFFERS 4 typedef struct { Framebuffer framebuffer; @@ -38,6 +39,8 @@ typedef struct { int32_t view_h; int32_t pointer_x; int32_t pointer_y; + void *cleared_buffers[TWL_SWITCH_MAX_BUFFERS]; + uint32_t cleared_count; bool touching; bool fb_ready; bool pad_ready; diff --git a/thirdparty/twl/backends/switch/presentation.c b/thirdparty/twl/backends/switch/presentation.c index 43220032..3790e85d 100644 --- a/thirdparty/twl/backends/switch/presentation.c +++ b/thirdparty/twl/backends/switch/presentation.c @@ -16,6 +16,14 @@ #include "backend.h" #include +static bool twl_switch_buffer_cleared(const TwlSwitch *sw, const void *buffer) { + uint32_t index; + for (index = 0u; index < sw->cleared_count; ++index) { + if (sw->cleared_buffers[index] == buffer) return true; + } + return false; +} + TwlResult twl_backend_prepare_frame(Twl *twl, const TwlSurface *surface) { TwlSwitch *sw = twl ? (TwlSwitch *) twl->backend : NULL; uint32_t stride; @@ -44,10 +52,15 @@ TwlResult twl_backend_prepare_frame(Twl *twl, const TwlSurface *surface) { src = (const uint16_t *) surface->pixels; src_stride = surface->stride_bytes / sizeof(uint16_t); - for (y = 0; y < TWL_SWITCH_SCREEN_HEIGHT; ++y) { - uint32_t *row = output + (uint32_t) y * output_stride; - for (x = 0; x < TWL_SWITCH_SCREEN_WIDTH; ++x) { - row[x] = RGBA8(0, 0, 0, 255); + if (!twl_switch_buffer_cleared(sw, output)) { + for (y = 0; y < TWL_SWITCH_SCREEN_HEIGHT; ++y) { + uint32_t *row = output + (uint32_t) y * output_stride; + for (x = 0; x < TWL_SWITCH_SCREEN_WIDTH; ++x) { + row[x] = RGBA8(0, 0, 0, 255); + } + } + if (sw->cleared_count < TWL_SWITCH_MAX_BUFFERS) { + sw->cleared_buffers[sw->cleared_count++] = output; } } From 19b3f76eff509cff0be704a4090e416ad3bc1627 Mon Sep 17 00:00:00 2001 From: Trulio Date: Tue, 4 Aug 2026 06:19:28 -0300 Subject: [PATCH 5/5] Build presentation to 30hz --- tools/switch/build-nro.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/switch/build-nro.sh b/tools/switch/build-nro.sh index e411e08b..948aa68f 100644 --- a/tools/switch/build-nro.sh +++ b/tools/switch/build-nro.sh @@ -25,7 +25,8 @@ cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \ -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ - -DOPENSHADOWFLARE_BUILD_EXE=OFF + -DOPENSHADOWFLARE_BUILD_EXE=OFF \ + -DOPENSHADOWFLARE_C99_PRESENTATION_HZ=30 cmake --build "$BUILD_DIR" --parallel NRO=$(find "$BUILD_DIR" -type f -name '*.nro' ! -path "$OUTPUT_NRO" -print -quit)