diff --git a/.github/workflows/vita.yml b/.github/workflows/vita.yml new file mode 100644 index 00000000..32572f75 --- /dev/null +++ b/.github/workflows/vita.yml @@ -0,0 +1,55 @@ +name: Build PlayStation Vita VPK + +on: + push: + branches: + - master + 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 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +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 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/assets/vita/sce_sys/icon0.png b/assets/vita/sce_sys/icon0.png new file mode 100644 index 00000000..8865ec3f Binary files /dev/null and b/assets/vita/sce_sys/icon0.png differ diff --git a/documentation/vita-port.md b/documentation/vita-port.md new file mode 100644 index 00000000..b965eb3d --- /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. 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/resources/item_inventory_resource.cpp b/src/SF_EXE/resources/item_inventory_resource.cpp index db0240b8..c092921a 100644 --- a/src/SF_EXE/resources/item_inventory_resource.cpp +++ b/src/SF_EXE/resources/item_inventory_resource.cpp @@ -81,8 +81,8 @@ bool ItemInventoryResource::prepareGroups( 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(data_root_, retail_path), @@ -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( diff --git a/src/SF_EXE/runtime/game_runtime.cpp b/src/SF_EXE/runtime/game_runtime.cpp index 087d3689..ed40a548 100644 --- a/src/SF_EXE/runtime/game_runtime.cpp +++ b/src/SF_EXE/runtime/game_runtime.cpp @@ -9,6 +9,9 @@ #include "runtime/gameplay_artwork.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" @@ -328,13 +331,30 @@ class Runtime final : public osf::runtime::FrameApplication { running = false; } break; - } - case osf::GameState::character_select: { - input_.characterSelect().saved_game_count = - resources_.savedGameCount(); - characterFrame_ = + } + case osf::GameState::character_select: { + input_.characterSelect().saved_game_count = + resources_.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) { @@ -552,6 +572,9 @@ class Runtime final : public osf::runtime::FrameApplication { osf::debug::FrameProfiler profiler_; #endif osf::GameConfig gameConfig_; +#ifdef OSF_PLATFORM_VITA + std::string pendingCharacterName_; +#endif osf::PlayerLoadRequest gameplayPlayer_; std::filesystem::path dataRoot_; osf::ResourceManager resources_; 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/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); +} diff --git a/tools/vita/build-vpk.sh b/tools/vita/build-vpk.sh new file mode 100644 index 00000000..01bc0402 --- /dev/null +++ b/tools/vita/build-vpk.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh + +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +VITASDK=${VITASDK:-/usr/local/vitasdk} +BUILD_DIR="$ROOT_DIR/build/vita/release" +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 + 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 \ + -DOPENSHADOWFLARE_BUILD_EXE=OFF \ + -DOPENSHADOWFLARE_C99_PRESENTATION_HZ=30 +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"