From 4a8aff0182cdb641c7de8ad26bd7b5e71cc153a4 Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 09:27:20 -0300 Subject: [PATCH 1/8] Add PlayStation Vita port --- .github/workflows/vita.yml | 49 ++ CMakeLists.txt | 6 +- assets/vita/sce_sys/icon0.png | Bin 0 -> 24194 bytes documentation/vita-port.md | 33 ++ src/SF_EXE/cmake/configure_platform.cmake | 3 + src/SF_EXE/cmake/configure_presentation.cmake | 7 + src/SF_EXE/cmake/platforms/Vita.cmake | 35 ++ .../resources/item_inventory_resource.cpp | 4 +- src/SF_EXE/runtime/game_runtime.cpp | 23 + src/SF_EXE/runtime/main.cpp | 8 + .../runtime/platform/platform_text_input.hpp | 14 + .../platform/vita/application_loop.cpp | 11 + .../runtime/platform/vita/text_input.cpp | 104 ++++ .../presentation/vita_surface_presenter.cpp | 139 ++++++ thirdparty/lal/CMakeLists.txt | 2 + thirdparty/lal/lal_vita.c | 27 + thirdparty/lwl/CMakeLists.txt | 9 + thirdparty/lwl/lwl_vita.c | 464 ++++++++++++++++++ tools/vita/build-vpk.sh | 34 ++ 19 files changed, 968 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/vita.yml create mode 100644 assets/vita/sce_sys/icon0.png create mode 100644 documentation/vita-port.md create mode 100644 src/SF_EXE/cmake/platforms/Vita.cmake create mode 100644 src/SF_EXE/runtime/platform/platform_text_input.hpp create mode 100644 src/SF_EXE/runtime/platform/vita/application_loop.cpp create mode 100644 src/SF_EXE/runtime/platform/vita/text_input.cpp create mode 100644 src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp create mode 100644 thirdparty/lal/lal_vita.c create mode 100644 thirdparty/lwl/lwl_vita.c create mode 100644 tools/vita/build-vpk.sh diff --git a/.github/workflows/vita.yml b/.github/workflows/vita.yml new file mode 100644 index 00000000..ec5a5c8e --- /dev/null +++ b/.github/workflows/vita.yml @@ -0,0 +1,49 @@ +name: Build PlayStation Vita VPK + +on: + push: + paths: + - '.github/workflows/vita.yml' + - 'CMakeLists.txt' + - 'assets/vita/**' + - 'src/**' + - 'thirdparty/**' + - 'tools/vita/**' + pull_request: + paths: + - '.github/workflows/vita.yml' + - 'CMakeLists.txt' + - 'assets/vita/**' + - 'src/**' + - 'thirdparty/**' + - 'tools/vita/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + container: + image: vitasdk/vitasdk:latest + + steps: + - name: Check out the repository + uses: actions/checkout@v6 + + - name: Install build requirements + run: apk add --no-cache cmake ninja + + - name: Build the release VPK + run: sh tools/vita/build-vpk.sh + + - name: Verify the VPK + run: test -s build/vita/release/src/SF_EXE/OpenShadowFlare.vpk + + - name: Upload the VPK + uses: actions/upload-artifact@v4 + with: + name: openshadowflare-vita-release + path: build/vita/release/src/SF_EXE/OpenShadowFlare.vpk + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index cd2a878d..daaa55ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ project( LANGUAGES C CXX ) -if(NINTENDO_SWITCH) +if(NINTENDO_SWITCH OR VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") 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(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + set(OPENSHADOWFLARE_DEFAULT_PRESENTATION_BACKEND "vita") 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 vita ) add_subdirectory(thirdparty/lwl) diff --git a/assets/vita/sce_sys/icon0.png b/assets/vita/sce_sys/icon0.png new file mode 100644 index 0000000000000000000000000000000000000000..8865ec3fb531e1ad991ce651c8d03e343d3c087d GIT binary patch literal 24194 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 literal 0 HcmV?d00001 diff --git a/documentation/vita-port.md b/documentation/vita-port.md new file mode 100644 index 00000000..73736259 --- /dev/null +++ b/documentation/vita-port.md @@ -0,0 +1,33 @@ +# PlayStation Vita + +Build with [VitaSDK](https://vitasdk.org/) on Linux or WSL: + +```sh +sh tools/vita/build-vpk.sh +``` + +Output: `build/vita/release/src/SF_EXE/OpenShadowFlare.vpk`. GitHub Actions +uploads the same file as `openshadowflare-vita-release`. + +The VPK does not include retail data. Copy game installation to: + +```text +ux0:data/OpenShadowFlare/ShadowFlare/ +``` + +It must include `SFlare.Cfg` and `System/Title/Pattern/Title.njp`. + +## Controls + +| Vita control | Game input | +| --- | --- | +| Front touchscreen | Pointer and primary click | +| Left stick | Virtual pointer | +| Cross / Start | Confirm; Cross also primary click | +| Circle / Select | Back; Circle also secondary click | +| D-pad | Menu navigation | +| Triangle / Square | Inventory / map | +| L / R | Mission list / run toggle | + +The port uses double-buffered software scaling at 960x544 and the system IME +for character names. Audio is not implemented yet. diff --git a/src/SF_EXE/cmake/configure_platform.cmake b/src/SF_EXE/cmake/configure_platform.cmake index 456288fc..a4356b18 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(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Vita.cmake") + osf_configure_vita_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..658331ce 100644 --- a/src/SF_EXE/cmake/configure_presentation.cmake +++ b/src/SF_EXE/cmake/configure_presentation.cmake @@ -12,6 +12,13 @@ function(osf_configure_presentation target) PRIVATE runtime/presentation/switch_surface_presenter.cpp ) + elseif(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "vita") + target_sources( + ${target} + PRIVATE + runtime/presentation/vita_surface_presenter.cpp + ) + target_link_libraries(${target} PRIVATE SceDisplay_stub) else() message(FATAL_ERROR "Unsupported presentation backend: " diff --git a/src/SF_EXE/cmake/platforms/Vita.cmake b/src/SF_EXE/cmake/platforms/Vita.cmake new file mode 100644 index 00000000..c563fc58 --- /dev/null +++ b/src/SF_EXE/cmake/platforms/Vita.cmake @@ -0,0 +1,35 @@ +function(osf_configure_vita_platform target) + target_sources( + ${target} + PRIVATE + runtime/platform/vita/application_loop.cpp + runtime/platform/vita/text_input.cpp + ) + target_compile_definitions(${target} PRIVATE OSF_PLATFORM_VITA) + target_link_libraries( + ${target} + PRIVATE + SceCommonDialog_stub + SceIme_stub + SceSysmodule_stub + ) + + if(NOT DEFINED ENV{VITASDK}) + message(FATAL_ERROR + "VITASDK is not set. Configure with " + "-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake.") + endif() + + include("$ENV{VITASDK}/share/vita.cmake") + vita_create_self(OpenShadowFlare.self ${target}) + vita_create_vpk( + OpenShadowFlare.vpk + OSFLR0001 + OpenShadowFlare.self + VERSION "01.00" + NAME "OpenShadowFlare" + FILE + "${PROJECT_SOURCE_DIR}/assets/vita/sce_sys/icon0.png" + "sce_sys/icon0.png" + ) +endfunction() diff --git a/src/SF_EXE/resources/item_inventory_resource.cpp b/src/SF_EXE/resources/item_inventory_resource.cpp index 48632d0e..8a510777 100644 --- a/src/SF_EXE/resources/item_inventory_resource.cpp +++ b/src/SF_EXE/resources/item_inventory_resource.cpp @@ -27,8 +27,8 @@ bool ItemInventoryResource::load( std::snprintf( retail_path, sizeof(retail_path), - "System\\Game\\Pattern\\Item%04zu.njp", - index); + "System\\Game\\Pattern\\Item%04lu.njp", + static_cast(index)); std::string resource_error; if (!groups_[index].load( resolveRetailPath( diff --git a/src/SF_EXE/runtime/game_runtime.cpp b/src/SF_EXE/runtime/game_runtime.cpp index e84051ae..8e334640 100644 --- a/src/SF_EXE/runtime/game_runtime.cpp +++ b/src/SF_EXE/runtime/game_runtime.cpp @@ -8,6 +8,9 @@ #include "runtime/frontend_assets.hpp" #include "runtime/gameplay_ui_controller.hpp" #include "runtime/input_adapter.hpp" +#ifdef OSF_PLATFORM_VITA +#include "runtime/platform/platform_text_input.hpp" +#endif #include "runtime/presentation/surface_presenter.hpp" #include "runtime/runtime_renderer.hpp" #include "runtime/state_bindings.hpp" @@ -298,9 +301,26 @@ class Runtime final : public osf::runtime::FrameApplication { case osf::GameState::character_select: { input_.characterSelect().saved_game_count = frontendAssets_.savedGameCount(); +#ifdef OSF_PLATFORM_VITA + if (!pendingCharacterName_.empty()) { + input_.characterSelect().text_input = + pendingCharacterName_; + pendingCharacterName_.clear(); + } +#endif characterFrame_ = characterSelectState_.update( input_.characterSelect()); +#ifdef OSF_PLATFORM_VITA + if (characterFrame_.mode_action == + osf::CharacterSelectModeAction::begin_name_entry) { + pendingCharacterName_ = + osf::runtime::openPlatformTextInput( + "Enter character name", + characterSelectState_.data().character_name, + 15); + } +#endif audio_.playCharacterSelectFrame(characterFrame_); if (characterFrame_.action == osf::CharacterSelectAction::return_to_title) { @@ -491,6 +511,9 @@ class Runtime final : public osf::runtime::FrameApplication { std::uint32_t fpsWindowFrames_ = 0; std::int32_t framesPerSecond_ = 0; osf::GameConfig gameConfig_; +#ifdef OSF_PLATFORM_VITA + std::string pendingCharacterName_; +#endif osf::PlayerLoadRequest gameplayPlayer_; std::filesystem::path dataRoot_; osf::runtime::FrontendAssets frontendAssets_; diff --git a/src/SF_EXE/runtime/main.cpp b/src/SF_EXE/runtime/main.cpp index 947875c5..e3f7dde1 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -55,6 +55,14 @@ std::filesystem::path findDataRoot() { return std::filesystem::path{}; }; +#ifdef OSF_PLATFORM_VITA + const std::filesystem::path vitaDataRoot = + "ux0:data/OpenShadowFlare/ShadowFlare"; + if (isDataRoot(vitaDataRoot)) { + return vitaDataRoot; + } +#endif + std::error_code error; const std::filesystem::path fromWorkingDirectory = searchParents(std::filesystem::current_path(error)); diff --git a/src/SF_EXE/runtime/platform/platform_text_input.hpp b/src/SF_EXE/runtime/platform/platform_text_input.hpp new file mode 100644 index 00000000..5dba7341 --- /dev/null +++ b/src/SF_EXE/runtime/platform/platform_text_input.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace osf::runtime { + +// Opens a platform text-input method for one short field. An empty result +// means that the user cancelled or did not enter any text. +std::string openPlatformTextInput( + const std::string& title, + const std::string& initialText, + int maxLength); + +} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/platform/vita/application_loop.cpp b/src/SF_EXE/runtime/platform/vita/application_loop.cpp new file mode 100644 index 00000000..0e158012 --- /dev/null +++ b/src/SF_EXE/runtime/platform/vita/application_loop.cpp @@ -0,0 +1,11 @@ +#include "runtime/application_loop.hpp" + +namespace osf::runtime { + +int runApplicationLoop(std::unique_ptr application) { + while (application->frame()) { + } + return 0; +} + +} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/platform/vita/text_input.cpp b/src/SF_EXE/runtime/platform/vita/text_input.cpp new file mode 100644 index 00000000..045fd911 --- /dev/null +++ b/src/SF_EXE/runtime/platform/vita/text_input.cpp @@ -0,0 +1,104 @@ +#include "runtime/platform/platform_text_input.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace osf::runtime { +namespace { + +constexpr std::size_t kTextCapacity = 64; + +void copyAsciiToUtf16( + const std::string& source, + SceWChar16* destination, + std::size_t capacity) { + const std::size_t count = std::min( + source.size(), capacity > 0 ? capacity - 1 : 0); + for (std::size_t index = 0; index < count; ++index) { + destination[index] = static_cast( + static_cast(source[index])); + } + if (capacity > 0) { + destination[count] = 0; + } +} + +std::string utf16ToAscii( + const SceWChar16* source, + int maxLength) { + std::string result; + for (int index = 0; index < maxLength && source[index] != 0; ++index) { + const SceWChar16 codePoint = source[index]; + result.push_back(codePoint < 128 + ? static_cast(codePoint) + : '?'); + } + return result; +} + +} // namespace + +std::string openPlatformTextInput( + const std::string& title, + const std::string& initialText, + int maxLength) { + maxLength = std::clamp( + maxLength, 1, static_cast(kTextCapacity - 1)); + + if (sceSysmoduleLoadModule(SCE_SYSMODULE_IME) < 0) { + return {}; + } + + std::array titleUtf16{}; + std::array initialUtf16{}; + std::array resultUtf16{}; + copyAsciiToUtf16(title, titleUtf16.data(), titleUtf16.size()); + copyAsciiToUtf16( + initialText, initialUtf16.data(), initialUtf16.size()); + + SceImeDialogParam parameters{}; + sceImeDialogParamInit(¶meters); + parameters.supportedLanguages = + SCE_IME_LANGUAGE_ENGLISH | SCE_IME_LANGUAGE_PORTUGUESE_BR; + parameters.type = SCE_IME_TYPE_BASIC_LATIN; + parameters.option = SCE_IME_OPTION_NO_AUTO_CAPITALIZATION; + parameters.dialogMode = SCE_IME_DIALOG_DIALOG_MODE_WITH_CANCEL; + parameters.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR; + parameters.title = titleUtf16.data(); + parameters.maxTextLength = static_cast(maxLength); + parameters.initialText = initialUtf16.data(); + parameters.inputTextBuffer = resultUtf16.data(); + + if (sceImeDialogInit(¶meters) < 0) { + sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); + return {}; + } + + for (;;) { + const SceCommonDialogStatus status = sceImeDialogGetStatus(); + if (status == SCE_COMMON_DIALOG_STATUS_FINISHED) { + break; + } + if (status == SCE_COMMON_DIALOG_STATUS_NONE) { + sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); + return {}; + } + sceDisplayWaitVblankStart(); + } + + SceImeDialogResult result{}; + const bool accepted = sceImeDialogGetResult(&result) >= 0 && + result.button == SCE_IME_DIALOG_BUTTON_ENTER; + sceImeDialogTerm(); + sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); + return accepted ? utf16ToAscii(resultUtf16.data(), maxLength) + : std::string{}; +} + +} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp new file mode 100644 index 00000000..ea3e9921 --- /dev/null +++ b/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp @@ -0,0 +1,139 @@ +#include "runtime/presentation/surface_presenter.hpp" + +#include "lwl.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::int32_t kScreenWidth = 960; +constexpr std::int32_t kScreenHeight = 544; +constexpr std::size_t kBufferBytes = + static_cast(kScreenWidth) * kScreenHeight * + sizeof(std::uint32_t); + +class VitaSurfacePresenter final + : public osf::runtime::SurfacePresenter { +public: + ~VitaSurfacePresenter() override { + shutdown(); + } + + bool initialize(LwlWindow* window, std::string* error) override; + void present(osf::gapi::SurfaceView surface) override; + +private: + void shutdown(); + + std::uint32_t* buffers_ = nullptr; + int back_buffer_ = 0; + bool initialized_ = false; +}; + +bool VitaSurfacePresenter::initialize( + LwlWindow* window, + std::string* error) { + (void) window; + shutdown(); + + // The display module requires each framebuffer to be 256 KiB aligned. + buffers_ = static_cast( + memalign(0x40000, kBufferBytes * 2)); + if (!buffers_) { + if (error) { + *error = "Could not allocate Vita display buffers."; + } + return false; + } + std::memset(buffers_, 0, kBufferBytes * 2); + + SceDisplayFrameBuf frameBuffer{}; + frameBuffer.size = sizeof(frameBuffer); + frameBuffer.base = buffers_; + frameBuffer.pitch = kScreenWidth; + frameBuffer.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; + frameBuffer.width = kScreenWidth; + frameBuffer.height = kScreenHeight; + if (sceDisplaySetFrameBuf( + &frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME) < 0) { + shutdown(); + if (error) { + *error = "Could not configure the Vita display framebuffer."; + } + return false; + } + + initialized_ = true; + if (error) { + error->clear(); + } + return true; +} + +void VitaSurfacePresenter::present(osf::gapi::SurfaceView surface) { + 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* const output = buffers_ + + static_cast(back_buffer_) * + kScreenWidth * kScreenHeight; + std::memset(output, 0, kBufferBytes); + + const osf::gapi::Viewport viewport = osf::gapi::fitViewport( + surface.width, surface.height, kScreenWidth, kScreenHeight); + 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) * kScreenWidth + 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]; + // A8B8G8R8 is stored as RGBA bytes on Vita's little-endian CPU. + row[x] = (static_cast(color.alpha) << 24) | + (static_cast(color.blue) << 16) | + (static_cast(color.green) << 8) | + color.red; + } + } + + SceDisplayFrameBuf frameBuffer{}; + frameBuffer.size = sizeof(frameBuffer); + frameBuffer.base = output; + frameBuffer.pitch = kScreenWidth; + frameBuffer.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; + frameBuffer.width = kScreenWidth; + frameBuffer.height = kScreenHeight; + sceDisplaySetFrameBuf(&frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME); + sceDisplayWaitVblankStart(); + back_buffer_ = 1 - back_buffer_; +} + +void VitaSurfacePresenter::shutdown() { + if (buffers_) { + free(buffers_); + buffers_ = nullptr; + } + back_buffer_ = 0; + initialized_ = 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..32839a48 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -54,6 +54,8 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lal PRIVATE lal_switch.c) +elseif(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + target_sources(lal PRIVATE lal_vita.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lal PRIVATE lal_alsa.c) find_package(ALSA REQUIRED) diff --git a/thirdparty/lal/lal_vita.c b/thirdparty/lal/lal_vita.c new file mode 100644 index 00000000..2f63d904 --- /dev/null +++ b/thirdparty/lal/lal_vita.c @@ -0,0 +1,27 @@ +/* + * 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 + +bool lal_platform_init(void) { + return true; +} + +void lal_platform_shutdown(void) { +} + +void lal_platform_lock(void) { +} + +void lal_platform_unlock(void) { +} diff --git a/thirdparty/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index f27651fc..bfcb0e16 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -64,6 +64,15 @@ elseif(APPLE) ) elseif(NINTENDO_SWITCH) target_sources(lwl PRIVATE lwl_switch.c) +elseif(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + target_sources(lwl PRIVATE lwl_vita.c) + target_link_libraries( + lwl + PRIVATE + SceCtrl_stub + SceTouch_stub + SceLibKernel_stub + ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lwl PRIVATE lwl_x11.c) find_package(OpenGL REQUIRED) diff --git a/thirdparty/lwl/lwl_vita.c b/thirdparty/lwl/lwl_vita.c new file mode 100644 index 00000000..46362916 --- /dev/null +++ b/thirdparty/lwl/lwl_vita.c @@ -0,0 +1,464 @@ +/* + * 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. + */ + +#include "lwl.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LWL_VITA_WIDTH 960 +#define LWL_VITA_HEIGHT 544 +#define LWL_VITA_TOUCH_WIDTH 1920 +#define LWL_VITA_TOUCH_HEIGHT 1088 + +struct LwlWindow { + int width; + int height; +}; + +static struct LwlWindow g_window; + +enum { LWL_VITA_EVENT_CAPACITY = 32 }; + +static LwlEvent g_events[LWL_VITA_EVENT_CAPACITY]; +static int g_event_count; +static int g_event_read; +static bool g_polled_this_frame; +static int g_cursor_x = LWL_VITA_WIDTH / 2; +static int g_cursor_y = LWL_VITA_HEIGHT / 2; +static unsigned int g_prev_buttons; +static bool g_was_touching; + +static int lwl_vita_clamp(int value, int low, int high) { + if (value < low) { + return low; + } + if (value > high) { + return high; + } + return value; +} + +static int lwl_vita_axis_delta(unsigned char axis) { + enum { DEAD_ZONE = 24, AXIS_RANGE = 127, MAX_SPEED = 12 }; + const int offset = (int) axis - 128; + const int magnitude = offset < 0 ? -offset : offset; + float normalized; + int speed; + + if (magnitude <= DEAD_ZONE) { + return 0; + } + normalized = (float) (magnitude - DEAD_ZONE) / + (float) (AXIS_RANGE - DEAD_ZONE); + if (normalized > 1.0f) { + normalized = 1.0f; + } + speed = (int) (normalized * normalized * (float) MAX_SPEED + 0.5f); + return offset < 0 ? -speed : speed; +} + +static LwlEvent *lwl_vita_push(LwlEventType type) { + LwlEvent *event; + if (g_event_count >= LWL_VITA_EVENT_CAPACITY) { + return NULL; + } + event = &g_events[g_event_count++]; + memset(event, 0, sizeof(*event)); + event->type = type; + return event; +} + +static void lwl_vita_push_pointer(LwlEventType type, int button) { + LwlEvent *event = lwl_vita_push(type); + if (event) { + event->x = g_cursor_x; + event->y = g_cursor_y; + event->button = button; + event->clicks = 1; + } +} + +static void lwl_vita_push_key(LwlEventType type, const char *key) { + LwlEvent *event = lwl_vita_push(type); + if (event) { + strncpy(event->key, key, sizeof(event->key) - 1); + } +} + +struct LwlVitaKeyMapping { + unsigned int mask; + const char *key; +}; + +static const struct LwlVitaKeyMapping g_key_mappings[] = { + {SCE_CTRL_UP, "up"}, + {SCE_CTRL_DOWN, "down"}, + {SCE_CTRL_LEFT, "left"}, + {SCE_CTRL_RIGHT, "right"}, + {SCE_CTRL_CROSS, "return"}, + {SCE_CTRL_START, "return"}, + {SCE_CTRL_CIRCLE, "escape"}, + {SCE_CTRL_SELECT, "escape"}, + {SCE_CTRL_TRIANGLE, "i"}, + {SCE_CTRL_SQUARE, "n"}, + {SCE_CTRL_LTRIGGER, "q"}, + {SCE_CTRL_RTRIGGER, "r"}, +}; + +static void lwl_vita_sample_input(void) { + SceCtrlData pad; + SceTouchData touch; + unsigned int buttons = 0; + unsigned int pressed; + unsigned int released; + size_t index; + + g_event_count = 0; + g_event_read = 0; + + memset(&pad, 0, sizeof(pad)); + if (sceCtrlPeekBufferPositive(0, &pad, 1) > 0) { + const int delta_x = lwl_vita_axis_delta(pad.lx); + const int delta_y = lwl_vita_axis_delta(pad.ly); + buttons = pad.buttons; + if (delta_x != 0 || delta_y != 0) { + const int old_x = g_cursor_x; + const int old_y = g_cursor_y; + LwlEvent *move; + g_cursor_x = lwl_vita_clamp( + g_cursor_x + delta_x, 0, LWL_VITA_WIDTH - 1); + g_cursor_y = lwl_vita_clamp( + g_cursor_y + delta_y, 0, LWL_VITA_HEIGHT - 1); + move = lwl_vita_push(LWL_EVENT_MOUSE_MOVE); + if (move) { + move->x = g_cursor_x; + move->y = g_cursor_y; + move->dx = g_cursor_x - old_x; + move->dy = g_cursor_y - old_y; + } + } + } + + memset(&touch, 0, sizeof(touch)); + if (sceTouchPeek(SCE_TOUCH_PORT_FRONT, &touch, 1) > 0 && + touch.reportNum > 0) { + const int x = lwl_vita_clamp( + (int) touch.report[0].x * LWL_VITA_WIDTH / LWL_VITA_TOUCH_WIDTH, + 0, LWL_VITA_WIDTH - 1); + const int y = lwl_vita_clamp( + (int) touch.report[0].y * LWL_VITA_HEIGHT / LWL_VITA_TOUCH_HEIGHT, + 0, LWL_VITA_HEIGHT - 1); + if (x != g_cursor_x || y != g_cursor_y || !g_was_touching) { + LwlEvent *move = lwl_vita_push(LWL_EVENT_MOUSE_MOVE); + if (move) { + move->dx = x - g_cursor_x; + move->dy = y - g_cursor_y; + move->x = x; + move->y = y; + } + g_cursor_x = x; + g_cursor_y = y; + } + if (!g_was_touching) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); + g_was_touching = true; + } + } else if (g_was_touching) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 1); + g_was_touching = false; + } + + pressed = buttons & ~g_prev_buttons; + released = ~buttons & g_prev_buttons; + if (pressed & SCE_CTRL_CROSS) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); + } + if (released & SCE_CTRL_CROSS) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 1); + } + if (pressed & SCE_CTRL_CIRCLE) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 3); + } + if (released & SCE_CTRL_CIRCLE) { + lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 3); + } + for (index = 0; + index < sizeof(g_key_mappings) / sizeof(g_key_mappings[0]); + ++index) { + if (pressed & g_key_mappings[index].mask) { + lwl_vita_push_key(LWL_EVENT_KEY_DOWN, g_key_mappings[index].key); + } + if (released & g_key_mappings[index].mask) { + lwl_vita_push_key(LWL_EVENT_KEY_UP, g_key_mappings[index].key); + } + } + g_prev_buttons = buttons; +} + +bool lwl_init(void) { + sceCtrlSetSamplingMode(SCE_CTRL_MODE_ANALOG); + sceTouchSetSamplingState( + SCE_TOUCH_PORT_FRONT, SCE_TOUCH_SAMPLING_STATE_START); + g_cursor_x = LWL_VITA_WIDTH / 2; + g_cursor_y = LWL_VITA_HEIGHT / 2; + g_prev_buttons = 0; + g_was_touching = false; + 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_VITA_WIDTH; + g_window.height = LWL_VITA_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_VITA_WIDTH; + } + if (height) { + *height = window ? window->height : LWL_VITA_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_vita_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) sceKernelGetProcessTimeWide() / 1000000.0; +} + +void lwl_sleep_seconds(double seconds) { + if (seconds > 0.0) { + sceKernelDelayThread((unsigned int) (seconds * 1000000.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 "vita"; +} + +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/vita/build-vpk.sh b/tools/vita/build-vpk.sh new file mode 100644 index 00000000..2a5fa867 --- /dev/null +++ b/tools/vita/build-vpk.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env sh + +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +VITASDK=${VITASDK:-} +BUILD_DIR="$ROOT_DIR/build/vita/release" +VPK="$BUILD_DIR/src/SF_EXE/OpenShadowFlare.vpk" + +if [ -z "$VITASDK" ] || [ ! -f "$VITASDK/share/vita.toolchain.cmake" ]; then + echo "VitaSDK was not found. Set VITASDK to its installation directory." >&2 + exit 1 +fi + +if ! command -v ninja >/dev/null 2>&1; then + echo "Ninja is required to build the Vita port." >&2 + exit 1 +fi + +export VITASDK + +cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \ + -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF +cmake --build "$BUILD_DIR" --parallel + +if [ ! -s "$VPK" ]; then + echo "CMake did not produce the expected VPK: $VPK" >&2 + exit 1 +fi + +echo "VPK: $VPK" From d1b5b348182830479a835ea2d6244ae64dc7a59b Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 09:34:13 -0300 Subject: [PATCH 2/8] fix audio --- thirdparty/lal/CMakeLists.txt | 6 ++ thirdparty/lal/lal_vita.c | 109 ++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index 32839a48..5a16d018 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -56,6 +56,12 @@ elseif(NINTENDO_SWITCH) target_sources(lal PRIVATE lal_switch.c) elseif(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") target_sources(lal PRIVATE lal_vita.c) + target_link_libraries( + lal + PRIVATE + SceAudio_stub + SceKernelThreadMgr_stub + ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_sources(lal PRIVATE lal_alsa.c) find_package(ALSA REQUIRED) diff --git a/thirdparty/lal/lal_vita.c b/thirdparty/lal/lal_vita.c index 2f63d904..afdae6bf 100644 --- a/thirdparty/lal/lal_vita.c +++ b/thirdparty/lal/lal_vita.c @@ -11,17 +11,126 @@ #include "lal_internal.h" +#include +#include + #include +#include + +static int16_t g_samples[LAL_BUFFER_FRAMES * LAL_OUTPUT_CHANNELS] + __attribute__((aligned(64))); +static SceUID g_mutex = -1; +static SceUID g_thread = -1; +static int g_audio_port = -1; +static bool g_running; + +void lal_platform_shutdown(void); + +static bool lal_vita_is_running(void) { + bool running = false; + + if (g_mutex >= 0) { + sceKernelLockMutex(g_mutex, 1, NULL); + running = g_running; + sceKernelUnlockMutex(g_mutex, 1); + } + return running; +} + +static void lal_vita_fill(void) { + if (g_mutex < 0) { + return; + } + sceKernelLockMutex(g_mutex, 1, NULL); + lal_mix_frames(g_samples, LAL_BUFFER_FRAMES); + sceKernelUnlockMutex(g_mutex, 1); +} + +static int lal_vita_audio_thread(SceSize arguments, void* argument) { + (void) arguments; + (void) argument; + + while (lal_vita_is_running()) { + lal_vita_fill(); + if (sceAudioOutOutput(g_audio_port, g_samples) < 0) { + if (g_mutex >= 0) { + sceKernelLockMutex(g_mutex, 1, NULL); + g_running = false; + sceKernelUnlockMutex(g_mutex, 1); + } + break; + } + } + return 0; +} bool lal_platform_init(void) { + g_mutex = sceKernelCreateMutex("osf_audio", 0, 0, NULL); + if (g_mutex < 0) { + lal_set_error("Could not create the Vita audio mutex."); + return false; + } + + g_audio_port = sceAudioOutOpenPort( + SCE_AUDIO_OUT_PORT_TYPE_BGM, + LAL_BUFFER_FRAMES, + LAL_OUTPUT_SAMPLE_RATE, + SCE_AUDIO_OUT_MODE_STEREO); + if (g_audio_port < 0) { + lal_set_error("Could not open the Vita audio output port."); + lal_platform_shutdown(); + return false; + } + + g_running = true; + g_thread = sceKernelCreateThread( + "osf_audio", lal_vita_audio_thread, 0x10000100, 0x10000, + 0, SCE_KERNEL_THREAD_CPU_AFFINITY_MASK_DEFAULT, NULL); + if (g_thread < 0) { + lal_set_error("Could not create the Vita audio thread."); + lal_platform_shutdown(); + return false; + } + if (sceKernelStartThread(g_thread, 0, NULL) < 0) { + lal_set_error("Could not start the Vita audio thread."); + sceKernelDeleteThread(g_thread); + g_thread = -1; + lal_platform_shutdown(); + return false; + } return true; } void lal_platform_shutdown(void) { + if (g_mutex >= 0) { + sceKernelLockMutex(g_mutex, 1, NULL); + g_running = false; + sceKernelUnlockMutex(g_mutex, 1); + } + if (g_thread >= 0) { + sceKernelWaitThreadEnd(g_thread, NULL, NULL); + sceKernelDeleteThread(g_thread); + g_thread = -1; + } + if (g_audio_port >= 0) { + sceAudioOutReleasePort(g_audio_port); + g_audio_port = -1; + } + if (g_mutex >= 0) { + sceKernelDeleteMutex(g_mutex); + g_mutex = -1; + } + g_running = false; } void lal_platform_lock(void) { + if (g_mutex >= 0) { + sceKernelLockMutex(g_mutex, 1, NULL); + } } void lal_platform_unlock(void) { + if (g_mutex >= 0) { + sceKernelUnlockMutex(g_mutex, 1); + } } From 41172f01c99524e1def35c2331aa26c55e403760 Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 09:37:18 -0300 Subject: [PATCH 3/8] fix vita port audio info --- documentation/vita-port.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/vita-port.md b/documentation/vita-port.md index 73736259..b965eb3d 100644 --- a/documentation/vita-port.md +++ b/documentation/vita-port.md @@ -30,4 +30,4 @@ It must include `SFlare.Cfg` and `System/Title/Pattern/Title.njp`. | L / R | Mission list / run toggle | The port uses double-buffered software scaling at 960x544 and the system IME -for character names. Audio is not implemented yet. +for character names. From 5649d6a08c1c17c6db679aa7228a9b2cd65a93ab Mon Sep 17 00:00:00 2001 From: Trulio Date: Sun, 2 Aug 2026 12:17:36 -0300 Subject: [PATCH 4/8] fix vita workflow --- .github/workflows/vita.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/vita.yml b/.github/workflows/vita.yml index ec5a5c8e..32572f75 100644 --- a/.github/workflows/vita.yml +++ b/.github/workflows/vita.yml @@ -2,6 +2,8 @@ name: Build PlayStation Vita VPK on: push: + branches: + - master paths: - '.github/workflows/vita.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 From c3cc9ec6923be32570aa6ce938b927fbc520c60e Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 10:17:27 -0300 Subject: [PATCH 5/8] Fix vita presenter --- .../presentation/vita_surface_presenter.cpp | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp index ea3e9921..ce76c5d3 100644 --- a/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp +++ b/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp @@ -27,7 +27,12 @@ class VitaSurfacePresenter final } 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(); @@ -35,6 +40,7 @@ class VitaSurfacePresenter final std::uint32_t* buffers_ = nullptr; int back_buffer_ = 0; bool initialized_ = false; + bool frame_prepared_ = false; }; bool VitaSurfacePresenter::initialize( @@ -77,7 +83,8 @@ bool VitaSurfacePresenter::initialize( return true; } -void VitaSurfacePresenter::present(osf::gapi::SurfaceView surface) { +void VitaSurfacePresenter::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."); @@ -117,10 +124,31 @@ void VitaSurfacePresenter::present(osf::gapi::SurfaceView surface) { frameBuffer.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; frameBuffer.width = kScreenWidth; frameBuffer.height = kScreenHeight; - sceDisplaySetFrameBuf(&frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME); + if (sceDisplaySetFrameBuf( + &frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME) < 0) { + return; + } + frame_prepared_ = true; +} + +void VitaSurfacePresenter::displayFrame() { + if (!initialized_ || !frame_prepared_) { + return; + } sceDisplayWaitVblankStart(); back_buffer_ = 1 - back_buffer_; + frame_prepared_ = false; +} + +#if OSF_ENABLE_DEBUG_TOOLS +std::optional +VitaSurfacePresenter::videoMemoryUsageBytes() const { + if (!initialized_) { + return std::nullopt; + } + return static_cast(kBufferBytes) * 2; } +#endif void VitaSurfacePresenter::shutdown() { if (buffers_) { @@ -129,6 +157,7 @@ void VitaSurfacePresenter::shutdown() { } back_buffer_ = 0; initialized_ = false; + frame_prepared_ = false; } } // namespace From 3f2cd81f046d9bf732b02ed48afe24fba272e636 Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 10:31:55 -0300 Subject: [PATCH 6/8] Fix item literal --- src/SF_EXE/resources/item_inventory_resource.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SF_EXE/resources/item_inventory_resource.cpp b/src/SF_EXE/resources/item_inventory_resource.cpp index 618ccc1c..c092921a 100644 --- a/src/SF_EXE/resources/item_inventory_resource.cpp +++ b/src/SF_EXE/resources/item_inventory_resource.cpp @@ -128,8 +128,8 @@ bool ItemInventoryResource::preparePatterns( std::snprintf( retail_path, sizeof(retail_path), - "System\\Game\\Pattern\\Item%04zu.njp", - index); + "System\\Game\\Pattern\\Item%04lu.njp", + static_cast(index)); std::string resource_error; if (!groups_[index].loadSelectedPatterns( resolveRetailPath( From 790f72127ccf40a1f5a712be224aa14c1c76d264 Mon Sep 17 00:00:00 2001 From: Trulio Date: Wed, 5 Aug 2026 00:48:06 -0300 Subject: [PATCH 7/8] revert sf_exe build and add vpk twl null backend --- CMakeLists.txt | 2 +- shadowflare/CMakeLists.txt | 24 + src/SF_EXE/cmake/platforms/Vita.cmake | 35 -- .../platform/vita/application_loop.cpp | 11 - .../runtime/platform/vita/text_input.cpp | 104 ---- .../presentation/vita_surface_presenter.cpp | 168 ------- thirdparty/lal/lal_vita.c | 136 ----- thirdparty/lwl/lwl_vita.c | 464 ------------------ tools/vita/build-vpk.sh | 8 +- 9 files changed, 30 insertions(+), 922 deletions(-) delete mode 100644 src/SF_EXE/cmake/platforms/Vita.cmake delete mode 100644 src/SF_EXE/runtime/platform/vita/application_loop.cpp delete mode 100644 src/SF_EXE/runtime/platform/vita/text_input.cpp delete mode 100644 src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp delete mode 100644 thirdparty/lal/lal_vita.c delete mode 100644 thirdparty/lwl/lwl_vita.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 10855535..74964493 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ project( LANGUAGES C CXX ) -if(NINTENDO_SWITCH) +if(NINTENDO_SWITCH OR VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") set(BUILD_TESTING OFF CACHE BOOL "" FORCE) endif() diff --git a/shadowflare/CMakeLists.txt b/shadowflare/CMakeLists.txt index 405fa5b0..0530c805 100644 --- a/shadowflare/CMakeLists.txt +++ b/shadowflare/CMakeLists.txt @@ -257,3 +257,27 @@ if(NINTENDO_SWITCH) endif() nx_create_nro(osf ICON "${PROJECT_SOURCE_DIR}/readme/sf-logo-sm.jpg") endif() + +if(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + target_compile_definitions( + shadowflare_core PRIVATE + SF_RETAIL_ROOT_FALLBACK="ux0:data/OpenShadowFlare/ShadowFlare") + + if(NOT DEFINED ENV{VITASDK}) + message(FATAL_ERROR + "VITASDK is not set. Configure the Vita build with " + "-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake.") + endif() + include("$ENV{VITASDK}/share/vita.cmake") + vita_create_self(OpenShadowFlare.self osf) + vita_create_vpk( + OpenShadowFlare.vpk + OSFLR0001 + OpenShadowFlare.self + VERSION "01.00" + NAME "OpenShadowFlare" + FILE + "${PROJECT_SOURCE_DIR}/assets/vita/sce_sys/icon0.png" + "sce_sys/icon0.png" + ) +endif() diff --git a/src/SF_EXE/cmake/platforms/Vita.cmake b/src/SF_EXE/cmake/platforms/Vita.cmake deleted file mode 100644 index c563fc58..00000000 --- a/src/SF_EXE/cmake/platforms/Vita.cmake +++ /dev/null @@ -1,35 +0,0 @@ -function(osf_configure_vita_platform target) - target_sources( - ${target} - PRIVATE - runtime/platform/vita/application_loop.cpp - runtime/platform/vita/text_input.cpp - ) - target_compile_definitions(${target} PRIVATE OSF_PLATFORM_VITA) - target_link_libraries( - ${target} - PRIVATE - SceCommonDialog_stub - SceIme_stub - SceSysmodule_stub - ) - - if(NOT DEFINED ENV{VITASDK}) - message(FATAL_ERROR - "VITASDK is not set. Configure with " - "-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake.") - endif() - - include("$ENV{VITASDK}/share/vita.cmake") - vita_create_self(OpenShadowFlare.self ${target}) - vita_create_vpk( - OpenShadowFlare.vpk - OSFLR0001 - OpenShadowFlare.self - VERSION "01.00" - NAME "OpenShadowFlare" - FILE - "${PROJECT_SOURCE_DIR}/assets/vita/sce_sys/icon0.png" - "sce_sys/icon0.png" - ) -endfunction() diff --git a/src/SF_EXE/runtime/platform/vita/application_loop.cpp b/src/SF_EXE/runtime/platform/vita/application_loop.cpp deleted file mode 100644 index 0e158012..00000000 --- a/src/SF_EXE/runtime/platform/vita/application_loop.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "runtime/application_loop.hpp" - -namespace osf::runtime { - -int runApplicationLoop(std::unique_ptr application) { - while (application->frame()) { - } - return 0; -} - -} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/platform/vita/text_input.cpp b/src/SF_EXE/runtime/platform/vita/text_input.cpp deleted file mode 100644 index 045fd911..00000000 --- a/src/SF_EXE/runtime/platform/vita/text_input.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "runtime/platform/platform_text_input.hpp" - -#include -#include -#include -#include - -#include -#include -#include - -namespace osf::runtime { -namespace { - -constexpr std::size_t kTextCapacity = 64; - -void copyAsciiToUtf16( - const std::string& source, - SceWChar16* destination, - std::size_t capacity) { - const std::size_t count = std::min( - source.size(), capacity > 0 ? capacity - 1 : 0); - for (std::size_t index = 0; index < count; ++index) { - destination[index] = static_cast( - static_cast(source[index])); - } - if (capacity > 0) { - destination[count] = 0; - } -} - -std::string utf16ToAscii( - const SceWChar16* source, - int maxLength) { - std::string result; - for (int index = 0; index < maxLength && source[index] != 0; ++index) { - const SceWChar16 codePoint = source[index]; - result.push_back(codePoint < 128 - ? static_cast(codePoint) - : '?'); - } - return result; -} - -} // namespace - -std::string openPlatformTextInput( - const std::string& title, - const std::string& initialText, - int maxLength) { - maxLength = std::clamp( - maxLength, 1, static_cast(kTextCapacity - 1)); - - if (sceSysmoduleLoadModule(SCE_SYSMODULE_IME) < 0) { - return {}; - } - - std::array titleUtf16{}; - std::array initialUtf16{}; - std::array resultUtf16{}; - copyAsciiToUtf16(title, titleUtf16.data(), titleUtf16.size()); - copyAsciiToUtf16( - initialText, initialUtf16.data(), initialUtf16.size()); - - SceImeDialogParam parameters{}; - sceImeDialogParamInit(¶meters); - parameters.supportedLanguages = - SCE_IME_LANGUAGE_ENGLISH | SCE_IME_LANGUAGE_PORTUGUESE_BR; - parameters.type = SCE_IME_TYPE_BASIC_LATIN; - parameters.option = SCE_IME_OPTION_NO_AUTO_CAPITALIZATION; - parameters.dialogMode = SCE_IME_DIALOG_DIALOG_MODE_WITH_CANCEL; - parameters.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR; - parameters.title = titleUtf16.data(); - parameters.maxTextLength = static_cast(maxLength); - parameters.initialText = initialUtf16.data(); - parameters.inputTextBuffer = resultUtf16.data(); - - if (sceImeDialogInit(¶meters) < 0) { - sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); - return {}; - } - - for (;;) { - const SceCommonDialogStatus status = sceImeDialogGetStatus(); - if (status == SCE_COMMON_DIALOG_STATUS_FINISHED) { - break; - } - if (status == SCE_COMMON_DIALOG_STATUS_NONE) { - sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); - return {}; - } - sceDisplayWaitVblankStart(); - } - - SceImeDialogResult result{}; - const bool accepted = sceImeDialogGetResult(&result) >= 0 && - result.button == SCE_IME_DIALOG_BUTTON_ENTER; - sceImeDialogTerm(); - sceSysmoduleUnloadModule(SCE_SYSMODULE_IME); - return accepted ? utf16ToAscii(resultUtf16.data(), maxLength) - : std::string{}; -} - -} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp b/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp deleted file mode 100644 index ce76c5d3..00000000 --- a/src/SF_EXE/runtime/presentation/vita_surface_presenter.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "runtime/presentation/surface_presenter.hpp" - -#include "lwl.h" - -#include - -#include -#include -#include -#include -#include -#include - -namespace { - -constexpr std::int32_t kScreenWidth = 960; -constexpr std::int32_t kScreenHeight = 544; -constexpr std::size_t kBufferBytes = - static_cast(kScreenWidth) * kScreenHeight * - sizeof(std::uint32_t); - -class VitaSurfacePresenter final - : public osf::runtime::SurfacePresenter { -public: - ~VitaSurfacePresenter() 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(); - - std::uint32_t* buffers_ = nullptr; - int back_buffer_ = 0; - bool initialized_ = false; - bool frame_prepared_ = false; -}; - -bool VitaSurfacePresenter::initialize( - LwlWindow* window, - std::string* error) { - (void) window; - shutdown(); - - // The display module requires each framebuffer to be 256 KiB aligned. - buffers_ = static_cast( - memalign(0x40000, kBufferBytes * 2)); - if (!buffers_) { - if (error) { - *error = "Could not allocate Vita display buffers."; - } - return false; - } - std::memset(buffers_, 0, kBufferBytes * 2); - - SceDisplayFrameBuf frameBuffer{}; - frameBuffer.size = sizeof(frameBuffer); - frameBuffer.base = buffers_; - frameBuffer.pitch = kScreenWidth; - frameBuffer.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; - frameBuffer.width = kScreenWidth; - frameBuffer.height = kScreenHeight; - if (sceDisplaySetFrameBuf( - &frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME) < 0) { - shutdown(); - if (error) { - *error = "Could not configure the Vita display framebuffer."; - } - return false; - } - - initialized_ = true; - if (error) { - error->clear(); - } - return true; -} - -void VitaSurfacePresenter::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* const output = buffers_ + - static_cast(back_buffer_) * - kScreenWidth * kScreenHeight; - std::memset(output, 0, kBufferBytes); - - const osf::gapi::Viewport viewport = osf::gapi::fitViewport( - surface.width, surface.height, kScreenWidth, kScreenHeight); - 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) * kScreenWidth + 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]; - // A8B8G8R8 is stored as RGBA bytes on Vita's little-endian CPU. - row[x] = (static_cast(color.alpha) << 24) | - (static_cast(color.blue) << 16) | - (static_cast(color.green) << 8) | - color.red; - } - } - - SceDisplayFrameBuf frameBuffer{}; - frameBuffer.size = sizeof(frameBuffer); - frameBuffer.base = output; - frameBuffer.pitch = kScreenWidth; - frameBuffer.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; - frameBuffer.width = kScreenWidth; - frameBuffer.height = kScreenHeight; - if (sceDisplaySetFrameBuf( - &frameBuffer, SCE_DISPLAY_SETBUF_NEXTFRAME) < 0) { - return; - } - frame_prepared_ = true; -} - -void VitaSurfacePresenter::displayFrame() { - if (!initialized_ || !frame_prepared_) { - return; - } - sceDisplayWaitVblankStart(); - back_buffer_ = 1 - back_buffer_; - frame_prepared_ = false; -} - -#if OSF_ENABLE_DEBUG_TOOLS -std::optional -VitaSurfacePresenter::videoMemoryUsageBytes() const { - if (!initialized_) { - return std::nullopt; - } - return static_cast(kBufferBytes) * 2; -} -#endif - -void VitaSurfacePresenter::shutdown() { - if (buffers_) { - free(buffers_); - buffers_ = nullptr; - } - back_buffer_ = 0; - initialized_ = false; - frame_prepared_ = false; -} - -} // namespace - -std::unique_ptr -osf::runtime::createSurfacePresenter() { - return std::make_unique(); -} diff --git a/thirdparty/lal/lal_vita.c b/thirdparty/lal/lal_vita.c deleted file mode 100644 index afdae6bf..00000000 --- a/thirdparty/lal/lal_vita.c +++ /dev/null @@ -1,136 +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 int16_t g_samples[LAL_BUFFER_FRAMES * LAL_OUTPUT_CHANNELS] - __attribute__((aligned(64))); -static SceUID g_mutex = -1; -static SceUID g_thread = -1; -static int g_audio_port = -1; -static bool g_running; - -void lal_platform_shutdown(void); - -static bool lal_vita_is_running(void) { - bool running = false; - - if (g_mutex >= 0) { - sceKernelLockMutex(g_mutex, 1, NULL); - running = g_running; - sceKernelUnlockMutex(g_mutex, 1); - } - return running; -} - -static void lal_vita_fill(void) { - if (g_mutex < 0) { - return; - } - sceKernelLockMutex(g_mutex, 1, NULL); - lal_mix_frames(g_samples, LAL_BUFFER_FRAMES); - sceKernelUnlockMutex(g_mutex, 1); -} - -static int lal_vita_audio_thread(SceSize arguments, void* argument) { - (void) arguments; - (void) argument; - - while (lal_vita_is_running()) { - lal_vita_fill(); - if (sceAudioOutOutput(g_audio_port, g_samples) < 0) { - if (g_mutex >= 0) { - sceKernelLockMutex(g_mutex, 1, NULL); - g_running = false; - sceKernelUnlockMutex(g_mutex, 1); - } - break; - } - } - return 0; -} - -bool lal_platform_init(void) { - g_mutex = sceKernelCreateMutex("osf_audio", 0, 0, NULL); - if (g_mutex < 0) { - lal_set_error("Could not create the Vita audio mutex."); - return false; - } - - g_audio_port = sceAudioOutOpenPort( - SCE_AUDIO_OUT_PORT_TYPE_BGM, - LAL_BUFFER_FRAMES, - LAL_OUTPUT_SAMPLE_RATE, - SCE_AUDIO_OUT_MODE_STEREO); - if (g_audio_port < 0) { - lal_set_error("Could not open the Vita audio output port."); - lal_platform_shutdown(); - return false; - } - - g_running = true; - g_thread = sceKernelCreateThread( - "osf_audio", lal_vita_audio_thread, 0x10000100, 0x10000, - 0, SCE_KERNEL_THREAD_CPU_AFFINITY_MASK_DEFAULT, NULL); - if (g_thread < 0) { - lal_set_error("Could not create the Vita audio thread."); - lal_platform_shutdown(); - return false; - } - if (sceKernelStartThread(g_thread, 0, NULL) < 0) { - lal_set_error("Could not start the Vita audio thread."); - sceKernelDeleteThread(g_thread); - g_thread = -1; - lal_platform_shutdown(); - return false; - } - return true; -} - -void lal_platform_shutdown(void) { - if (g_mutex >= 0) { - sceKernelLockMutex(g_mutex, 1, NULL); - g_running = false; - sceKernelUnlockMutex(g_mutex, 1); - } - if (g_thread >= 0) { - sceKernelWaitThreadEnd(g_thread, NULL, NULL); - sceKernelDeleteThread(g_thread); - g_thread = -1; - } - if (g_audio_port >= 0) { - sceAudioOutReleasePort(g_audio_port); - g_audio_port = -1; - } - if (g_mutex >= 0) { - sceKernelDeleteMutex(g_mutex); - g_mutex = -1; - } - g_running = false; -} - -void lal_platform_lock(void) { - if (g_mutex >= 0) { - sceKernelLockMutex(g_mutex, 1, NULL); - } -} - -void lal_platform_unlock(void) { - if (g_mutex >= 0) { - sceKernelUnlockMutex(g_mutex, 1); - } -} diff --git a/thirdparty/lwl/lwl_vita.c b/thirdparty/lwl/lwl_vita.c deleted file mode 100644 index 46362916..00000000 --- a/thirdparty/lwl/lwl_vita.c +++ /dev/null @@ -1,464 +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. - */ - -#include "lwl.h" - -#include -#include -#include -#include - -#include -#include -#include - -#define LWL_VITA_WIDTH 960 -#define LWL_VITA_HEIGHT 544 -#define LWL_VITA_TOUCH_WIDTH 1920 -#define LWL_VITA_TOUCH_HEIGHT 1088 - -struct LwlWindow { - int width; - int height; -}; - -static struct LwlWindow g_window; - -enum { LWL_VITA_EVENT_CAPACITY = 32 }; - -static LwlEvent g_events[LWL_VITA_EVENT_CAPACITY]; -static int g_event_count; -static int g_event_read; -static bool g_polled_this_frame; -static int g_cursor_x = LWL_VITA_WIDTH / 2; -static int g_cursor_y = LWL_VITA_HEIGHT / 2; -static unsigned int g_prev_buttons; -static bool g_was_touching; - -static int lwl_vita_clamp(int value, int low, int high) { - if (value < low) { - return low; - } - if (value > high) { - return high; - } - return value; -} - -static int lwl_vita_axis_delta(unsigned char axis) { - enum { DEAD_ZONE = 24, AXIS_RANGE = 127, MAX_SPEED = 12 }; - const int offset = (int) axis - 128; - const int magnitude = offset < 0 ? -offset : offset; - float normalized; - int speed; - - if (magnitude <= DEAD_ZONE) { - return 0; - } - normalized = (float) (magnitude - DEAD_ZONE) / - (float) (AXIS_RANGE - DEAD_ZONE); - if (normalized > 1.0f) { - normalized = 1.0f; - } - speed = (int) (normalized * normalized * (float) MAX_SPEED + 0.5f); - return offset < 0 ? -speed : speed; -} - -static LwlEvent *lwl_vita_push(LwlEventType type) { - LwlEvent *event; - if (g_event_count >= LWL_VITA_EVENT_CAPACITY) { - return NULL; - } - event = &g_events[g_event_count++]; - memset(event, 0, sizeof(*event)); - event->type = type; - return event; -} - -static void lwl_vita_push_pointer(LwlEventType type, int button) { - LwlEvent *event = lwl_vita_push(type); - if (event) { - event->x = g_cursor_x; - event->y = g_cursor_y; - event->button = button; - event->clicks = 1; - } -} - -static void lwl_vita_push_key(LwlEventType type, const char *key) { - LwlEvent *event = lwl_vita_push(type); - if (event) { - strncpy(event->key, key, sizeof(event->key) - 1); - } -} - -struct LwlVitaKeyMapping { - unsigned int mask; - const char *key; -}; - -static const struct LwlVitaKeyMapping g_key_mappings[] = { - {SCE_CTRL_UP, "up"}, - {SCE_CTRL_DOWN, "down"}, - {SCE_CTRL_LEFT, "left"}, - {SCE_CTRL_RIGHT, "right"}, - {SCE_CTRL_CROSS, "return"}, - {SCE_CTRL_START, "return"}, - {SCE_CTRL_CIRCLE, "escape"}, - {SCE_CTRL_SELECT, "escape"}, - {SCE_CTRL_TRIANGLE, "i"}, - {SCE_CTRL_SQUARE, "n"}, - {SCE_CTRL_LTRIGGER, "q"}, - {SCE_CTRL_RTRIGGER, "r"}, -}; - -static void lwl_vita_sample_input(void) { - SceCtrlData pad; - SceTouchData touch; - unsigned int buttons = 0; - unsigned int pressed; - unsigned int released; - size_t index; - - g_event_count = 0; - g_event_read = 0; - - memset(&pad, 0, sizeof(pad)); - if (sceCtrlPeekBufferPositive(0, &pad, 1) > 0) { - const int delta_x = lwl_vita_axis_delta(pad.lx); - const int delta_y = lwl_vita_axis_delta(pad.ly); - buttons = pad.buttons; - if (delta_x != 0 || delta_y != 0) { - const int old_x = g_cursor_x; - const int old_y = g_cursor_y; - LwlEvent *move; - g_cursor_x = lwl_vita_clamp( - g_cursor_x + delta_x, 0, LWL_VITA_WIDTH - 1); - g_cursor_y = lwl_vita_clamp( - g_cursor_y + delta_y, 0, LWL_VITA_HEIGHT - 1); - move = lwl_vita_push(LWL_EVENT_MOUSE_MOVE); - if (move) { - move->x = g_cursor_x; - move->y = g_cursor_y; - move->dx = g_cursor_x - old_x; - move->dy = g_cursor_y - old_y; - } - } - } - - memset(&touch, 0, sizeof(touch)); - if (sceTouchPeek(SCE_TOUCH_PORT_FRONT, &touch, 1) > 0 && - touch.reportNum > 0) { - const int x = lwl_vita_clamp( - (int) touch.report[0].x * LWL_VITA_WIDTH / LWL_VITA_TOUCH_WIDTH, - 0, LWL_VITA_WIDTH - 1); - const int y = lwl_vita_clamp( - (int) touch.report[0].y * LWL_VITA_HEIGHT / LWL_VITA_TOUCH_HEIGHT, - 0, LWL_VITA_HEIGHT - 1); - if (x != g_cursor_x || y != g_cursor_y || !g_was_touching) { - LwlEvent *move = lwl_vita_push(LWL_EVENT_MOUSE_MOVE); - if (move) { - move->dx = x - g_cursor_x; - move->dy = y - g_cursor_y; - move->x = x; - move->y = y; - } - g_cursor_x = x; - g_cursor_y = y; - } - if (!g_was_touching) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); - g_was_touching = true; - } - } else if (g_was_touching) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 1); - g_was_touching = false; - } - - pressed = buttons & ~g_prev_buttons; - released = ~buttons & g_prev_buttons; - if (pressed & SCE_CTRL_CROSS) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 1); - } - if (released & SCE_CTRL_CROSS) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 1); - } - if (pressed & SCE_CTRL_CIRCLE) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_DOWN, 3); - } - if (released & SCE_CTRL_CIRCLE) { - lwl_vita_push_pointer(LWL_EVENT_MOUSE_UP, 3); - } - for (index = 0; - index < sizeof(g_key_mappings) / sizeof(g_key_mappings[0]); - ++index) { - if (pressed & g_key_mappings[index].mask) { - lwl_vita_push_key(LWL_EVENT_KEY_DOWN, g_key_mappings[index].key); - } - if (released & g_key_mappings[index].mask) { - lwl_vita_push_key(LWL_EVENT_KEY_UP, g_key_mappings[index].key); - } - } - g_prev_buttons = buttons; -} - -bool lwl_init(void) { - sceCtrlSetSamplingMode(SCE_CTRL_MODE_ANALOG); - sceTouchSetSamplingState( - SCE_TOUCH_PORT_FRONT, SCE_TOUCH_SAMPLING_STATE_START); - g_cursor_x = LWL_VITA_WIDTH / 2; - g_cursor_y = LWL_VITA_HEIGHT / 2; - g_prev_buttons = 0; - g_was_touching = false; - 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_VITA_WIDTH; - g_window.height = LWL_VITA_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_VITA_WIDTH; - } - if (height) { - *height = window ? window->height : LWL_VITA_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_vita_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) sceKernelGetProcessTimeWide() / 1000000.0; -} - -void lwl_sleep_seconds(double seconds) { - if (seconds > 0.0) { - sceKernelDelayThread((unsigned int) (seconds * 1000000.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 "vita"; -} - -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/vita/build-vpk.sh b/tools/vita/build-vpk.sh index 2a5fa867..01bc0402 100644 --- a/tools/vita/build-vpk.sh +++ b/tools/vita/build-vpk.sh @@ -3,9 +3,9 @@ set -eu ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) -VITASDK=${VITASDK:-} +VITASDK=${VITASDK:-/usr/local/vitasdk} BUILD_DIR="$ROOT_DIR/build/vita/release" -VPK="$BUILD_DIR/src/SF_EXE/OpenShadowFlare.vpk" +VPK="$BUILD_DIR/shadowflare/OpenShadowFlare.vpk" if [ -z "$VITASDK" ] || [ ! -f "$VITASDK/share/vita.toolchain.cmake" ]; then echo "VitaSDK was not found. Set VITASDK to its installation directory." >&2 @@ -23,7 +23,9 @@ cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \ -G Ninja \ -DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" \ -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=OFF + -DBUILD_TESTING=OFF \ + -DOPENSHADOWFLARE_BUILD_EXE=OFF \ + -DOPENSHADOWFLARE_C99_PRESENTATION_HZ=30 cmake --build "$BUILD_DIR" --parallel if [ ! -s "$VPK" ]; then From dc80389c606ffb597c386f7e758c3508bec3b47a Mon Sep 17 00:00:00 2001 From: Trulio Date: Wed, 5 Aug 2026 01:55:41 -0300 Subject: [PATCH 8/8] Add twl and tal backends --- thirdparty/tal/CMakeLists.txt | 3 + thirdparty/tal/tal_vita.c | 205 ++++++++++++++++++++ thirdparty/twl/CMakeLists.txt | 23 +++ thirdparty/twl/backends/vita/backend.h | 54 ++++++ thirdparty/twl/backends/vita/input.c | 152 +++++++++++++++ thirdparty/twl/backends/vita/presentation.c | 70 +++++++ thirdparty/twl/backends/vita/window.c | 103 ++++++++++ 7 files changed, 610 insertions(+) create mode 100644 thirdparty/tal/tal_vita.c create mode 100644 thirdparty/twl/backends/vita/backend.h create mode 100644 thirdparty/twl/backends/vita/input.c create mode 100644 thirdparty/twl/backends/vita/presentation.c create mode 100644 thirdparty/twl/backends/vita/window.c diff --git a/thirdparty/tal/CMakeLists.txt b/thirdparty/tal/CMakeLists.txt index 5ff8909b..72107a93 100644 --- a/thirdparty/tal/CMakeLists.txt +++ b/thirdparty/tal/CMakeLists.txt @@ -50,6 +50,9 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(tal PRIVATE ALSA::ALSA) elseif(NINTENDO_SWITCH) target_sources(tal PRIVATE tal_switch.c) +elseif(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + target_sources(tal PRIVATE tal_vita.c) + target_link_libraries(tal PUBLIC SceAudio_stub SceKernelThreadMgr_stub) else() target_sources(tal PRIVATE tal_null.c) endif() diff --git a/thirdparty/tal/tal_vita.c b/thirdparty/tal/tal_vita.c new file mode 100644 index 00000000..05eafbbe --- /dev/null +++ b/thirdparty/tal/tal_vita.c @@ -0,0 +1,205 @@ +/* + * 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 + +#define TAL_VITA_OUT_RATE 48000 +#define TAL_VITA_OUT_FRAMES 1024u +#define TAL_VITA_OUT_CHANNELS 2u +#define TAL_VITA_MAX_INPUT_FRAMES 256u + +typedef struct { + int16_t output[TAL_VITA_OUT_FRAMES * TAL_VITA_OUT_CHANNELS]; + int16_t input[TAL_VITA_MAX_INPUT_FRAMES]; + Tal *tal; + SceUID mutex; + SceUID thread; + int port; + uint32_t in_rate; + uint32_t input_frames; + bool running; + bool mutex_ready; + bool port_ready; + bool thread_ready; +} TalVita; + +size_t tal_backend_memory_alignment(void) { + return 64u; +} + +size_t tal_backend_memory_required(const TalConfig *config) { + (void) config; + return sizeof(TalVita); +} + +void tal_backend_lock(Tal *tal) { + TalVita *vita = tal ? (TalVita *) tal->backend : NULL; + if (vita && vita->mutex_ready) { + sceKernelLockMutex(vita->mutex, 1, NULL); + } +} + +void tal_backend_unlock(Tal *tal) { + TalVita *vita = tal ? (TalVita *) tal->backend : NULL; + if (vita && vita->mutex_ready) { + sceKernelUnlockMutex(vita->mutex, 1); + } +} + +static bool tal_vita_running(TalVita *vita) { + bool running; + sceKernelLockMutex(vita->mutex, 1, NULL); + running = vita->running; + sceKernelUnlockMutex(vita->mutex, 1); + return running; +} + +static void tal_vita_fill(TalVita *vita) { + uint32_t frame; + sceKernelLockMutex(vita->mutex, 1, NULL); + if (tal_internal_render(vita->tal, vita->input, vita->input_frames) != + TAL_RESULT_OK) { + tal_internal_zero(vita->input, (size_t) vita->input_frames * sizeof(int16_t)); + } + sceKernelUnlockMutex(vita->mutex, 1); + + for (frame = 0u; frame < TAL_VITA_OUT_FRAMES; ++frame) { + uint32_t index = + (uint32_t) ((uint64_t) frame * vita->in_rate / TAL_VITA_OUT_RATE); + if (index >= vita->input_frames) { + index = vita->input_frames - 1u; + } + vita->output[frame * TAL_VITA_OUT_CHANNELS] = vita->input[index]; + vita->output[frame * TAL_VITA_OUT_CHANNELS + 1u] = vita->input[index]; + } +} + +static int tal_vita_thread(SceSize args, void *argp) { + TalVita *vita = *(TalVita **) argp; + (void) args; + while (tal_vita_running(vita)) { + tal_vita_fill(vita); + if (sceAudioOutOutput(vita->port, vita->output) < 0) { + sceKernelLockMutex(vita->mutex, 1, NULL); + vita->running = false; + sceKernelUnlockMutex(vita->mutex, 1); + break; + } + } + return 0; +} + +TalResult tal_backend_init( + Tal *tal, void *memory, size_t memory_size, const TalConfig *config) { + TalVita *vita; + TalVita *thread_arg; + if (!tal || !memory || !config || memory_size < sizeof(TalVita)) { + return TAL_RESULT_INVALID_ARGUMENT; + } + if (config->channels != 1u) { + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + vita = (TalVita *) memory; + vita->tal = tal; + vita->mutex = -1; + vita->thread = -1; + vita->port = -1; + vita->in_rate = config->sample_rate ? config->sample_rate : 11025u; + vita->input_frames = (uint32_t) ( + (uint64_t) TAL_VITA_OUT_FRAMES * vita->in_rate / TAL_VITA_OUT_RATE); + if (vita->input_frames == 0u) { + vita->input_frames = 1u; + } + if (vita->input_frames > TAL_VITA_MAX_INPUT_FRAMES) { + vita->input_frames = TAL_VITA_MAX_INPUT_FRAMES; + } + vita->running = false; + vita->mutex_ready = false; + vita->port_ready = false; + vita->thread_ready = false; + + vita->mutex = sceKernelCreateMutex("osf_audio", 0, 0, NULL); + if (vita->mutex < 0) { + return TAL_RESULT_BACKEND_FAILURE; + } + vita->mutex_ready = true; + + vita->port = sceAudioOutOpenPort( + SCE_AUDIO_OUT_PORT_TYPE_MAIN, (int) TAL_VITA_OUT_FRAMES, + TAL_VITA_OUT_RATE, SCE_AUDIO_OUT_MODE_STEREO); + if (vita->port < 0) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_UNAVAILABLE; + } + vita->port_ready = true; + + vita->running = true; + vita->thread = sceKernelCreateThread( + "osf_audio", tal_vita_thread, 0x10000100, 0x10000, 0, + SCE_KERNEL_THREAD_CPU_AFFINITY_MASK_DEFAULT, NULL); + if (vita->thread < 0) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_FAILURE; + } + thread_arg = vita; + if (sceKernelStartThread( + vita->thread, (SceSize) sizeof(thread_arg), &thread_arg) < 0) { + tal_backend_shutdown(tal); + return TAL_RESULT_BACKEND_FAILURE; + } + vita->thread_ready = true; + return TAL_RESULT_OK; +} + +void tal_backend_shutdown(Tal *tal) { + TalVita *vita = tal ? (TalVita *) tal->backend : NULL; + if (!vita) { + return; + } + if (vita->mutex_ready) { + sceKernelLockMutex(vita->mutex, 1, NULL); + vita->running = false; + sceKernelUnlockMutex(vita->mutex, 1); + } + if (vita->thread >= 0) { + if (vita->thread_ready) { + sceKernelWaitThreadEnd(vita->thread, NULL, NULL); + } + sceKernelDeleteThread(vita->thread); + vita->thread = -1; + vita->thread_ready = false; + } + if (vita->port_ready) { + sceAudioOutReleasePort(vita->port); + vita->port = -1; + vita->port_ready = false; + } + if (vita->mutex_ready) { + sceKernelDeleteMutex(vita->mutex); + vita->mutex = -1; + vita->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 a3076652..8eb28043 100644 --- a/thirdparty/twl/CMakeLists.txt +++ b/thirdparty/twl/CMakeLists.txt @@ -82,6 +82,29 @@ elseif(NINTENDO_SWITCH) backends/switch/input.c backends/switch/presentation.c ) +elseif(VITA OR CMAKE_SYSTEM_NAME STREQUAL "Vita") + target_sources( + twl + PRIVATE + backends/vita/window.c + backends/vita/presentation.c + backends/vita/input.c + ) + target_link_libraries( + twl + PUBLIC + vita2d + SceGxm_stub + SceDisplay_stub + SceCommonDialog_stub + SceSysmodule_stub + SceAppMgr_stub + SceCtrl_stub + SceTouch_stub + SceLibKernel_stub + SceKernelThreadMgr_stub + m + ) else() target_sources(twl PRIVATE backends/null/backend.c) endif() diff --git a/thirdparty/twl/backends/vita/backend.h b/thirdparty/twl/backends/vita/backend.h new file mode 100644 index 00000000..a43470b3 --- /dev/null +++ b/thirdparty/twl/backends/vita/backend.h @@ -0,0 +1,54 @@ +/* + * 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_VITA_BACKEND_H +#define TWL_VITA_BACKEND_H + +#include "twl_internal.h" + +#include + +/* The Vita screen is 960x544; the game framebuffer is uploaded to a GPU texture + * and vita2d scales it into an aspect-fit rectangle. The front touch panel + * reports at double the screen resolution. */ +#define TWL_VITA_SCREEN_WIDTH 960 +#define TWL_VITA_SCREEN_HEIGHT 544 +#define TWL_VITA_TOUCH_WIDTH 1920 +#define TWL_VITA_TOUCH_HEIGHT 1088 + +typedef struct { + vita2d_texture *texture; + uint32_t *tex_data; + uint32_t tex_stride_px; + uint32_t frame_width; + uint32_t frame_height; + float scale; + int32_t view_x; + int32_t view_y; + int32_t view_w; + int32_t view_h; + int32_t pointer_x; + int32_t pointer_y; + uint32_t prev_buttons; + bool touching; + bool cross_held; + bool pointer_down; + bool vita2d_ready; +} TwlVita; + +#endif diff --git a/thirdparty/twl/backends/vita/input.c b/thirdparty/twl/backends/vita/input.c new file mode 100644 index 00000000..51f20335 --- /dev/null +++ b/thirdparty/twl/backends/vita/input.c @@ -0,0 +1,152 @@ +/* + * 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 + +static const struct { + uint32_t mask; + TwlKey key; +} twl_vita_button_keys[] = { + {SCE_CTRL_UP, TWL_KEY_UP}, + {SCE_CTRL_DOWN, TWL_KEY_DOWN}, + {SCE_CTRL_LEFT, TWL_KEY_LEFT}, + {SCE_CTRL_RIGHT, TWL_KEY_RIGHT}, + {SCE_CTRL_CROSS, TWL_KEY_RETURN}, + {SCE_CTRL_START, TWL_KEY_RETURN}, + {SCE_CTRL_CIRCLE, TWL_KEY_ESCAPE}, + {SCE_CTRL_SELECT, TWL_KEY_ESCAPE}, + {SCE_CTRL_TRIANGLE, TWL_KEY_I}, + {SCE_CTRL_RTRIGGER, TWL_KEY_R}, +}; + +static int32_t twl_vita_clamp(int32_t value, int32_t low, int32_t high) { + if (value < low) return low; + if (value > high) return high; + return value; +} + +static int32_t twl_vita_axis_delta(uint8_t axis) { + const int32_t value = (int32_t) axis - 128; + if (value > -32 && value < 32) return 0; + return value / 16; +} + +static void twl_vita_push(Twl *twl, TwlEvent *event) { + event->timestamp_us = twl_backend_time_microseconds(twl); + twl_internal_push_event(twl, event); +} + +static void twl_vita_push_key(Twl *twl, TwlEventType type, TwlKey key) { + TwlEvent event; + twl_internal_zero(&event, sizeof(event)); + event.type = type; + event.key = key; + twl_vita_push(twl, &event); +} + +static void twl_vita_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_vita_push(twl, &event); +} + +void twl_backend_pump_events(Twl *twl) { + TwlVita *vita = twl ? (TwlVita *) twl->backend : NULL; + SceCtrlData pad; + SceTouchData touch; + uint32_t buttons; + uint32_t pressed; + uint32_t released; + int32_t next_x; + int32_t next_y; + bool want_down; + size_t index; + if (!vita) { + return; + } + + memset(&pad, 0, sizeof(pad)); + sceCtrlPeekBufferPositive(0, &pad, 1); + buttons = pad.buttons; + pressed = buttons & ~vita->prev_buttons; + released = ~buttons & vita->prev_buttons; + vita->prev_buttons = buttons; + + for (index = 0; + index < sizeof(twl_vita_button_keys) / sizeof(twl_vita_button_keys[0]); + ++index) { + const uint32_t mask = twl_vita_button_keys[index].mask; + if (pressed & mask) { + twl_vita_push_key(twl, TWL_EVENT_KEY_DOWN, twl_vita_button_keys[index].key); + } + if (released & mask) { + twl_vita_push_key(twl, TWL_EVENT_KEY_UP, twl_vita_button_keys[index].key); + } + } + + next_x = vita->pointer_x; + next_y = vita->pointer_y; + memset(&touch, 0, sizeof(touch)); + if (sceTouchPeek(SCE_TOUCH_PORT_FRONT, &touch, 1) >= 0 && + touch.reportNum > 0) { + const int32_t view_w = vita->view_w > 0 ? vita->view_w : 1; + const int32_t view_h = vita->view_h > 0 ? vita->view_h : 1; + const int32_t screen_x = (int32_t) touch.report[0].x * + TWL_VITA_SCREEN_WIDTH / TWL_VITA_TOUCH_WIDTH; + const int32_t screen_y = (int32_t) touch.report[0].y * + TWL_VITA_SCREEN_HEIGHT / TWL_VITA_TOUCH_HEIGHT; + next_x = twl_vita_clamp( + (screen_x - vita->view_x) * (int32_t) vita->frame_width / view_w, + 0, (int32_t) vita->frame_width - 1); + next_y = twl_vita_clamp( + (screen_y - vita->view_y) * (int32_t) vita->frame_height / view_h, + 0, (int32_t) vita->frame_height - 1); + vita->touching = true; + } else { + vita->touching = false; + next_x = twl_vita_clamp( + next_x + twl_vita_axis_delta(pad.lx), 0, (int32_t) vita->frame_width - 1); + next_y = twl_vita_clamp( + next_y + twl_vita_axis_delta(pad.ly), 0, (int32_t) vita->frame_height - 1); + } + + if (next_x != vita->pointer_x || next_y != vita->pointer_y) { + vita->pointer_x = next_x; + vita->pointer_y = next_y; + twl_vita_push_pointer(twl, TWL_EVENT_POINTER_MOVE, next_x, next_y); + } + + vita->cross_held = (buttons & SCE_CTRL_CROSS) != 0u; + want_down = vita->touching || vita->cross_held; + if (want_down && !vita->pointer_down) { + twl_vita_push_pointer( + twl, TWL_EVENT_POINTER_DOWN, vita->pointer_x, vita->pointer_y); + vita->pointer_down = true; + } else if (!want_down && vita->pointer_down) { + twl_vita_push_pointer( + twl, TWL_EVENT_POINTER_UP, vita->pointer_x, vita->pointer_y); + vita->pointer_down = false; + } +} diff --git a/thirdparty/twl/backends/vita/presentation.c b/thirdparty/twl/backends/vita/presentation.c new file mode 100644 index 00000000..59b6ab7d --- /dev/null +++ b/thirdparty/twl/backends/vita/presentation.c @@ -0,0 +1,70 @@ +/* + * 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) { + TwlVita *vita = twl ? (TwlVita *) twl->backend : NULL; + const uint16_t *src; + size_t src_stride; + uint32_t y; + uint32_t x; + if (!vita || !surface || !surface->pixels) { + return TWL_RESULT_INVALID_ARGUMENT; + } + if (surface->format != TWL_PIXEL_RGB555 || + surface->width != vita->frame_width || + surface->height != vita->frame_height) { + return TWL_RESULT_INVALID_ARGUMENT; + } + if (!vita->vita2d_ready || !vita->tex_data) { + return TWL_RESULT_OK; + } + + src = (const uint16_t *) surface->pixels; + src_stride = surface->stride_bytes / sizeof(uint16_t); + for (y = 0; y < vita->frame_height; ++y) { + const uint16_t *source_row = src + (size_t) y * src_stride; + uint32_t *dest_row = vita->tex_data + (size_t) y * vita->tex_stride_px; + for (x = 0; x < vita->frame_width; ++x) { + const uint16_t pixel = source_row[x]; + const uint32_t r5 = (uint32_t) (pixel & 0x1fu); + const uint32_t g5 = (uint32_t) ((pixel >> 5) & 0x1fu); + const uint32_t b5 = (uint32_t) ((pixel >> 10) & 0x1fu); + const uint32_t r8 = (r5 << 3) | (r5 >> 2); + const uint32_t g8 = (g5 << 3) | (g5 >> 2); + const uint32_t b8 = (b5 << 3) | (b5 >> 2); + dest_row[x] = 0xff000000u | (b8 << 16) | (g8 << 8) | r8; + } + } + return TWL_RESULT_OK; +} + +TwlResult twl_backend_display_frame(Twl *twl) { + TwlVita *vita = twl ? (TwlVita *) twl->backend : NULL; + if (!vita || !vita->vita2d_ready || !vita->texture) { + return TWL_RESULT_INVALID_ARGUMENT; + } + vita2d_start_drawing(); + vita2d_clear_screen(); + vita2d_draw_texture_scale( + vita->texture, (float) vita->view_x, (float) vita->view_y, + vita->scale, vita->scale); + vita2d_end_drawing(); + vita2d_swap_buffers(); + return TWL_RESULT_OK; +} diff --git a/thirdparty/twl/backends/vita/window.c b/thirdparty/twl/backends/vita/window.c new file mode 100644 index 00000000..6ade5182 --- /dev/null +++ b/thirdparty/twl/backends/vita/window.c @@ -0,0 +1,103 @@ +/* + * 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 + +static void twl_vita_fit_viewport(TwlVita *vita) { + const float sx = (float) TWL_VITA_SCREEN_WIDTH / (float) vita->frame_width; + const float sy = (float) TWL_VITA_SCREEN_HEIGHT / (float) vita->frame_height; + vita->scale = sx < sy ? sx : sy; + vita->view_w = (int32_t) ((float) vita->frame_width * vita->scale); + vita->view_h = (int32_t) ((float) vita->frame_height * vita->scale); + vita->view_x = (TWL_VITA_SCREEN_WIDTH - vita->view_w) / 2; + vita->view_y = (TWL_VITA_SCREEN_HEIGHT - vita->view_h) / 2; +} + +size_t twl_backend_memory_alignment(void) { + return _Alignof(TwlVita); +} + +size_t twl_backend_memory_required(const TwlConfig *config) { + (void) config; + return sizeof(TwlVita); +} + +TwlResult twl_backend_init( + Twl *twl, void *memory, size_t memory_size, const TwlConfig *config) { + TwlVita *vita; + if (!twl || !memory || memory_size < sizeof(TwlVita) || !config) { + return TWL_RESULT_INVALID_ARGUMENT; + } + vita = (TwlVita *) memory; + vita->frame_width = config->width ? config->width : 640u; + vita->frame_height = config->height ? config->height : 480u; + + vita2d_init(); + vita2d_set_vblank_wait(1); + vita2d_set_clear_color(RGBA8(0, 0, 0, 255)); + + vita->texture = vita2d_create_empty_texture_format( + vita->frame_width, vita->frame_height, + SCE_GXM_TEXTURE_FORMAT_U8U8U8U8_ABGR); + if (!vita->texture) { + vita2d_fini(); + return TWL_RESULT_BACKEND_FAILURE; + } + vita2d_texture_set_filters( + vita->texture, SCE_GXM_TEXTURE_FILTER_POINT, SCE_GXM_TEXTURE_FILTER_POINT); + vita->tex_data = (uint32_t *) vita2d_texture_get_datap(vita->texture); + vita->tex_stride_px = + vita2d_texture_get_stride(vita->texture) / (uint32_t) sizeof(uint32_t); + vita->vita2d_ready = true; + + twl_vita_fit_viewport(vita); + vita->pointer_x = (int32_t) (vita->frame_width / 2u); + vita->pointer_y = (int32_t) (vita->frame_height / 2u); + + sceCtrlSetSamplingMode(SCE_CTRL_MODE_ANALOG); + sceTouchSetSamplingState( + SCE_TOUCH_PORT_FRONT, SCE_TOUCH_SAMPLING_STATE_START); + + twl_internal_set_display_size(twl, vita->frame_width, vita->frame_height); + return TWL_RESULT_OK; +} + +void twl_backend_shutdown(Twl *twl) { + TwlVita *vita = twl ? (TwlVita *) twl->backend : NULL; + if (!vita || !vita->vita2d_ready) { + return; + } + if (vita->texture) { + vita2d_free_texture(vita->texture); + vita->texture = NULL; + } + vita2d_fini(); + vita->vita2d_ready = false; +} + +uint64_t twl_backend_time_microseconds(const Twl *twl) { + (void) twl; + return sceKernelGetProcessTimeWide(); +} + +void twl_backend_sleep_microseconds(Twl *twl, uint64_t duration) { + (void) twl; + sceKernelDelayThread((SceUInt) duration); +}