diff --git a/Info.plist b/Info.plist
index 97c0ba1..395c32f 100644
--- a/Info.plist
+++ b/Info.plist
@@ -6,20 +6,35 @@
en
CFBundleExecutable
armsx
+ CFBundleIconFile
+ armsx.icns
CFBundleIdentifier
armsx.app.0
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- armsx
+ ARMSX
CFBundlePackageType
APPL
CFBundleShortVersionString
0.10.4-alpha
CFBundleVersion
0.10.4-alpha
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Viewer
+ CFBundleURLName
+ cloud.nanodata.armsx
+ CFBundleURLSchemes
+
+ armsx
+
+
+
LSMinimumSystemVersion
- 10.9
+ 10.15
NSHighResolutionCapable
diff --git a/Makefile b/Makefile
index d1dc190..5bf4fdd 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,8 @@ IOS_DEPLOYMENT_TARGET ?= 14.0
MACOS_DEPLOYMENT_TARGET ?= 10.15
WINDOWS_TARGET ?= 0
UWP_TARGET ?= 0
+PSVITA_TARGET ?= 0
+VITASDK ?=
IOS_SDL_FRAMEWORK ?= $(CURDIR)/ios/Frameworks/SDL2.xcframework/ios-arm64/SDL2.framework
IOS_SDL_FRAMEWORK_PARENT := $(dir $(IOS_SDL_FRAMEWORK))
SDKROOT ?=
@@ -41,6 +43,19 @@ FSUI_DIR := third_party/fsui-lib
FSUI_BUILD_DIR ?= build/fsui/native
WASM_HTML_POSTPROCESS := $(FSUI_DIR)/cmake/postprocess_web_html.cmake
+ifeq ($(PSVITA_TARGET),1)
+ ifeq ($(strip $(VITASDK)),)
+$(error VITASDK must be set when PSVITA_TARGET=1)
+ endif
+ SDL_CONFIG := $(VITASDK)/bin/arm-vita-eabi-pkg-config
+ CC := $(VITASDK)/bin/arm-vita-eabi-gcc
+ CXX := $(VITASDK)/bin/arm-vita-eabi-g++
+ AR ?= $(VITASDK)/bin/arm-vita-eabi-ar
+ RANLIB ?= $(VITASDK)/bin/arm-vita-eabi-ranlib
+ PLATFORM := Vita
+ SDL_STATIC := 1
+endif
+
ifeq ($(IOS_TARGET),1)
SDKROOT ?= $(shell xcrun --sdk $(IOS_SDK) --show-sdk-path)
CC ?= $(shell xcrun --sdk $(IOS_SDK) --find clang)
@@ -78,11 +93,21 @@ ifeq ($(CONTROLLER_GENERIC),1)
endif
ifeq ($(UWP_TARGET),1)
- BASE_CFLAGS += -DUWP_TARGET
- BASE_CXXFLAGS += -DUWP_TARGET
+ BASE_CFLAGS += -DUWP_TARGET
+ BASE_CXXFLAGS += -DUWP_TARGET
+endif
+
+ifeq ($(PSVITA_TARGET),1)
+ BASE_CFLAGS += -DPSVITA_TARGET -D__DLL_BUILD
+ BASE_CXXFLAGS += -DPSVITA_TARGET -D__DLL_BUILD
endif
ifeq ($(WASM_TARGET),wasm)
+ BASE_CFLAGS := $(filter-out -flto,$(BASE_CFLAGS))
+ BASE_CXXFLAGS := $(filter-out -flto,$(BASE_CXXFLAGS))
+endif
+
+ifeq ($(PSVITA_TARGET),1)
BASE_CFLAGS := $(filter-out -flto,$(BASE_CFLAGS))
BASE_CXXFLAGS := $(filter-out -flto,$(BASE_CXXFLAGS))
endif
@@ -94,6 +119,8 @@ endif
ifeq ($(WASM_TARGET),wasm)
OS_INFO := Emscripten
+else ifeq ($(PSVITA_TARGET),1)
+OS_INFO := PS Vita
else ifneq ($(IOS_TARGET),1)
OS_INFO := $(shell uname -rmo)
else
@@ -131,6 +158,8 @@ COMMIT_HASH := $(shell git rev-parse --short HEAD)
BIN_DIR := bin
ifeq ($(WASM_TARGET),wasm)
BIN_DIR := bin/wasm
+else ifeq ($(PSVITA_TARGET),1)
+BIN_DIR := bin/psvita
endif
OBJ_DIR := $(BIN_DIR)/obj
@@ -141,8 +170,10 @@ else ifeq ($(WINDOWS_TARGET),1)
BIN := $(BIN_DIR)/armsx.exe
endif
SHARED_BIN := $(BIN_DIR)/libarmsx$(SHARED_EXT)
-RUNTIME_ICON_SRC := icons/AppIconLarge.png
-RUNTIME_ICON_DEST := $(BIN_DIR)/icons/AppIconLarge.png
+VITA_NATIVE_LIB := $(BIN_DIR)/libarmsx_vita.a
+RUNTIME_ICON_SRC := icons/FsuiAppIcon.png
+RUNTIME_ICON_DEST := $(BIN_DIR)/icons/FsuiAppIcon.png
+WINDRES ?= windres
C_SOURCES := $(wildcard psx/*.c) \
$(wildcard psx/dev/*.c) \
@@ -163,15 +194,22 @@ FSUI_LIBS := \
$(FSUI_BUILD_DIR)/libfsui-platform-sdl2.a \
$(FSUI_BUILD_DIR)/libfsui-renderer-sdl2.a \
$(FSUI_BUILD_DIR)/libfsui-renderer-sdl2surface.a \
- $(FSUI_BUILD_DIR)/libfsui-renderer-opengl.a \
$(FSUI_BUILD_DIR)/libfsui-core.a \
$(FSUI_BUILD_DIR)/libfsui_imgui.a \
$(FSUI_BUILD_DIR)/libfsui_resources.a
+ifeq ($(PSVITA_TARGET),1)
+FSUI_LIBS += \
+ $(FSUI_BUILD_DIR)/libfsui-renderer-opengl.a \
+ $(FSUI_BUILD_DIR)/libfsui_glad.a
+else
+
ifneq ($(WASM_TARGET),wasm)
FSUI_LIBS += \
+ $(FSUI_BUILD_DIR)/libfsui-renderer-opengl.a \
$(FSUI_BUILD_DIR)/libfsui_glad.a
endif
+endif
PLATFORM_EXTRA_LDFLAGS :=
PLATFORM_EXTRA_LIBS :=
@@ -196,7 +234,12 @@ endif
C_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(C_SOURCES))
C_OBJS_SHARED := $(patsubst %.c,$(OBJ_DIR)/%.o,$(C_SOURCES_SHARED))
CPP_OBJS := $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(CPP_SOURCES))
-ALL_OBJS := $(C_OBJS) $(CPP_OBJS)
+RC_SOURCES :=
+ifeq ($(WINDOWS_TARGET),1)
+RC_SOURCES += resources/windows/armsx.rc
+endif
+RC_OBJS := $(patsubst %.rc,$(OBJ_DIR)/%.o,$(RC_SOURCES))
+ALL_OBJS := $(C_OBJS) $(CPP_OBJS) $(RC_OBJS)
ALL_OBJS_SHARED := $(C_OBJS_SHARED) $(CPP_OBJS)
# Force dynamic SDL when building the shared library
@@ -214,7 +257,7 @@ endif
SDL_LIBS := $(if $(filter 1,$(SDL_STATIC)),$(SDL_LIBS_STATIC),$(SDL_LIBS_DYNAMIC))
SDL_LIBS_SHARED := $(SDL_LIBS_DYNAMIC)
-.PHONY: all clean shared wasm
+.PHONY: all clean shared wasm psvita-lib
all: $(BIN)
@@ -222,6 +265,8 @@ shared: $(SHARED_BIN)
wasm: $(BIN)
+psvita-lib: $(VITA_NATIVE_LIB)
+
$(BIN_DIR):
mkdir -p $(BIN_DIR)
@@ -242,6 +287,10 @@ $(OBJ_DIR)/%.o: %.cpp | $(OBJ_DIR)
-DREP_VERSION="$(VERSION_TAG)" \
-DREP_COMMIT_HASH="$(COMMIT_HASH)"
+$(OBJ_DIR)/%.o: %.rc | $(OBJ_DIR)
+ mkdir -p $(dir $@)
+ $(WINDRES) -I. $< $@
+
$(RUNTIME_ICON_DEST): $(RUNTIME_ICON_SRC) | $(BIN_DIR)
mkdir -p $(dir $@)
cp $< $@
@@ -252,6 +301,10 @@ ifeq ($(WASM_TARGET),wasm)
cmake -DINPUT_FILE="$(BIN)" -P "$(WASM_HTML_POSTPROCESS)"
endif
+$(VITA_NATIVE_LIB): $(ALL_OBJS) $(RUNTIME_ICON_DEST) | $(BIN_DIR)
+ $(AR) rcs $@ $(ALL_OBJS)
+ $(RANLIB) $@
+
$(SHARED_BIN): $(ALL_OBJS_SHARED) | $(BIN_DIR)
$(CXX) $(SHARED_LDFLAGS) $(ALL_OBJS_SHARED) $(FSUI_LIBS) -o $(SHARED_BIN) $(SDL_LIBS_SHARED) $(PLATFORM_EXTRA_LDFLAGS) $(PLATFORM_EXTRA_LIBS)
diff --git a/build.sh b/build.sh
index cf081ed..42f6149 100755
--- a/build.sh
+++ b/build.sh
@@ -9,6 +9,7 @@ set -e
# ./build.sh macosapp -> build desktop exe and bundle armsx.app
# ./build.sh wasm -> build WebAssembly target to bin/wasm using emscripten
# ./build.sh android -> build SDL2/libarmsx for Android and stage under android/app/src/main/jniLibs
+# ./build.sh psvita -> build Vita native archive + Rust SDL host and package ARMSX.vpk
MODE="$1"
BUILD_JOBS="${BUILD_JOBS:-4}"
@@ -42,15 +43,33 @@ build_fsui_wasm() {
cmake --build build/fsui/wasm -j"${BUILD_JOBS}"
}
+build_fsui_psvita() {
+ if [ -z "${VITASDK:-}" ]; then
+ echo "VITASDK must be set for PSVita builds."
+ exit 1
+ fi
+
+ cmake -S cmake/psvita-fsui -B build/fsui/psvita \
+ -DCMAKE_TOOLCHAIN_FILE="${VITASDK}/share/vita.toolchain.cmake" \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DVITASDK="${VITASDK}" \
+ -DFSUI_BUILD_SAMPLES=OFF \
+ -DFSUI_ENABLE_INSTALL=OFF \
+ -DFSUI_PLATFORM_BACKEND=SDL2 \
+ -DFSUI_IMGUI_OPENGL_ES3=ON
+ cmake --build build/fsui/psvita -j"${BUILD_JOBS}"
+}
+
bundle_macos_app() {
- rm -rf armsx.app/Contents/Libraries
- mkdir -p armsx.app/Contents/MacOS
- mkdir -p armsx.app/Contents/Resources/icons
- cp bin/armsx armsx.app/Contents/MacOS
- cp -R icons/. armsx.app/Contents/Resources/icons/
- chmod 777 armsx.app/Contents/MacOS/armsx
- dylibbundler -b -x ./armsx.app/Contents/MacOS/armsx -d ./armsx.app/Contents/Libraries/ -p @executable_path/../Libraries/ -cd
- cp Info.plist armsx.app/Contents/Info.plist
+ rm -rf armsx.app/Contents/Libraries
+ mkdir -p armsx.app/Contents/MacOS
+ mkdir -p armsx.app/Contents/Resources/icons
+ cp bin/armsx armsx.app/Contents/MacOS
+ cp icons/ArmsxDesktop.icns armsx.app/Contents/Resources/armsx.icns
+ cp -R icons/. armsx.app/Contents/Resources/icons/
+ chmod 777 armsx.app/Contents/MacOS/armsx
+ dylibbundler -b -x ./armsx.app/Contents/MacOS/armsx -d ./armsx.app/Contents/Libraries/ -p @executable_path/../Libraries/ -cd
+ cp Info.plist armsx.app/Contents/Info.plist
}
stage_android_runtime_icons() {
@@ -250,6 +269,64 @@ elif [ "$MODE" = "android" ]; then
cp "${SDL_SHARED_LIB}" "${JNI_LIB_DIR}/"
cp bin/libarmsx.so "${JNI_LIB_DIR}/"
+elif [ "$MODE" = "psvita" ]; then
+ if [ -z "${VITASDK:-}" ]; then
+ echo "VITASDK must be set for PSVita builds."
+ exit 1
+ fi
+
+ if ! command -v cargo >/dev/null 2>&1; then
+ echo "cargo is required for the PSVita Rust host build."
+ exit 1
+ fi
+
+ export PATH="${VITASDK}/bin:${PATH}"
+ build_fsui_psvita
+
+ make clean
+ VITASDK="${VITASDK}" PSVITA_TARGET=1 FSUI_BUILD_DIR="$(pwd)/build/fsui/psvita" make psvita-lib
+
+ export PKG_CONFIG_ALLOW_CROSS=1
+ export PKG_CONFIG="${VITASDK}/bin/arm-vita-eabi-pkg-config"
+ export CARGO_TARGET_ARMV7_SONY_VITA_NEWLIBEABIHF_LINKER=arm-vita-eabi-g++
+ export ARMSX_VITA_NATIVE_DIR="$(pwd)/bin/psvita"
+ export ARMSX_VITA_FSUI_DIR="$(pwd)/build/fsui/psvita"
+
+ cargo build \
+ --manifest-path psvita/Cargo.toml \
+ --release \
+ --target armv7-sony-vita-newlibeabihf \
+ -Z build-std=std,panic_abort
+
+ VITA_PACKAGE_DIR="$(pwd)/build/psvita/package"
+ VITA_TARGET_DIR="$(pwd)/psvita/target/armv7-sony-vita-newlibeabihf/release"
+ VITA_ELF="${VITA_TARGET_DIR}/ARMSX_vita"
+ VITA_VELF="${VITA_PACKAGE_DIR}/eboot.velf"
+ VITA_EBOOT="${VITA_PACKAGE_DIR}/eboot.bin"
+ VITA_PARAM="${VITA_PACKAGE_DIR}/param.sfo"
+ VITA_VPK="${VITA_PACKAGE_DIR}/ARMSX.vpk"
+
+ if [ ! -f "${VITA_ELF}" ]; then
+ echo "Expected Vita host ELF not found at ${VITA_ELF}"
+ exit 1
+ fi
+
+ rm -rf "${VITA_PACKAGE_DIR}"
+ mkdir -p "${VITA_PACKAGE_DIR}/sce_sys"
+ cp icons/FsuiAppIcon.png "${VITA_PACKAGE_DIR}/sce_sys/icon0.png"
+
+ vita-mksfoex -s TITLE_ID=ARMSX001 "ARMSX PSVita" "${VITA_PARAM}"
+ vita-elf-create -s "${VITA_ELF}" "${VITA_VELF}"
+ vita-make-fself -s "${VITA_VELF}" "${VITA_EBOOT}"
+ vita-pack-vpk "${VITA_VPK}" \
+ -s "${VITA_PARAM}" \
+ -b "${VITA_EBOOT}" \
+ -a "$(pwd)/icons=icons" \
+ -a "${VITA_PACKAGE_DIR}/sce_sys=sce_sys"
+
+ cp "${VITA_VPK}" bin/psvita/
+ echo "Packaged Vita build at bin/psvita/ARMSX.vpk"
+
elif [ "$MODE" = "wasm" ]; then
build_fsui_wasm
make clean
diff --git a/cmake/psvita-fsui/CMakeLists.txt b/cmake/psvita-fsui/CMakeLists.txt
new file mode 100644
index 0000000..3cc7172
--- /dev/null
+++ b/cmake/psvita-fsui/CMakeLists.txt
@@ -0,0 +1,47 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(armsx_psvita_fsui LANGUAGES C CXX)
+
+if(NOT DEFINED VITASDK)
+ if(DEFINED ENV{VITASDK})
+ set(VITASDK "$ENV{VITASDK}")
+ else()
+ message(FATAL_ERROR "VITASDK must be set for PSVita FSUI builds.")
+ endif()
+endif()
+
+include_directories(BEFORE "${CMAKE_CURRENT_LIST_DIR}/include")
+
+add_library(SDL2::SDL2 INTERFACE IMPORTED GLOBAL)
+target_include_directories(SDL2::SDL2 INTERFACE
+ "${VITASDK}/arm-vita-eabi/include"
+ "${VITASDK}/arm-vita-eabi/include/SDL2"
+)
+target_link_directories(SDL2::SDL2 INTERFACE "${VITASDK}/arm-vita-eabi/lib")
+target_link_libraries(SDL2::SDL2 INTERFACE
+ SDL2
+ SceGxm_stub
+ SceDisplay_stub
+ SceCtrl_stub
+ SceAppMgr_stub
+ SceAppUtil_stub
+ SceAudio_stub
+ SceAudioIn_stub
+ SceSysmodule_stub
+ SceIofilemgr_stub
+ SceCommonDialog_stub
+ SceTouch_stub
+ SceHid_stub
+ SceMotion_stub
+ ScePower_stub
+ SceProcessmgr_stub
+ m
+)
+
+set(FSUI_BUILD_SAMPLES OFF CACHE BOOL "Build the fsui sample applications" FORCE)
+set(FSUI_ENABLE_INSTALL OFF CACHE BOOL "Enable install/export rules for the fsui package" FORCE)
+set(FSUI_PLATFORM_BACKEND SDL2 CACHE STRING "Platform backend to build" FORCE)
+set(FSUI_IMGUI_OPENGL_ES3 ON CACHE BOOL "Build the fsui OpenGL backend in OpenGL ES 3 mode" FORCE)
+set(FSUI_USE_SYSTEM_SDL2 OFF CACHE BOOL "Prefer a system SDL2 package when an SDL target is not already available" FORCE)
+
+add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../third_party/fsui-lib" "${CMAKE_BINARY_DIR}/fsui-lib")
diff --git a/cmake/psvita-fsui/include/glad/gl.h b/cmake/psvita-fsui/include/glad/gl.h
new file mode 100644
index 0000000..fb91539
--- /dev/null
+++ b/cmake/psvita-fsui/include/glad/gl.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include
diff --git a/frontend/main.cpp b/frontend/main.cpp
index 1122142..b061d6d 100644
--- a/frontend/main.cpp
+++ b/frontend/main.cpp
@@ -36,7 +36,7 @@
#if !defined(UWP_TARGET)
#include
#endif
-#elif !defined(__EMSCRIPTEN__) && !defined(__ANDROID__)
+#elif !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(PSVITA_TARGET)
#include
#include
#endif
@@ -69,10 +69,10 @@ extern "C" {
namespace {
constexpr Uint32 kPauseChordGraceMs = 120;
-constexpr std::string_view kCustomFsuiAppIconPath = "icons/AppIconLarge.png";
+constexpr std::string_view kCustomFsuiAppIconPath = "icons/FsuiAppIcon.png";
constexpr bool SupportsManagedWindowSizing() {
-#if defined(__ANDROID__) || defined(IOS_TARGET) || defined(__EMSCRIPTEN__) || defined(UWP_TARGET)
+#if defined(__ANDROID__) || defined(IOS_TARGET) || defined(__EMSCRIPTEN__) || defined(UWP_TARGET) || defined(PSVITA_TARGET)
return false;
#else
return true;
@@ -307,17 +307,14 @@ std::filesystem::path DefaultBrowseDirectory() {
return std::filesystem::current_path();
}
-std::optional UwpExternalArmsxPath() {
-#if defined(UWP_TARGET)
- const std::filesystem::path candidate("E:/armsx");
- std::error_code ec;
-
- if (std::filesystem::exists(candidate, ec) && std::filesystem::is_directory(candidate, ec)) {
- return candidate;
+std::string AppendBrowseRootHint(std::string summary) {
+#if defined(_WIN32)
+ if (!summary.empty() && summary.back() != ' ') {
+ summary.push_back(' ');
}
+ summary += "Use Parent Directory at a drive root to switch drives.";
#endif
-
- return std::nullopt;
+ return summary;
}
bool PathsMatch(const std::filesystem::path& left, const std::filesystem::path& right) {
@@ -401,6 +398,66 @@ std::string NormalizePathString(std::string value) {
return value;
}
+bool StartsWithCaseInsensitive(std::string_view value, std::string_view prefix) {
+ if (value.size() < prefix.size()) {
+ return false;
+ }
+
+ for (size_t index = 0; index < prefix.size(); index++) {
+ const unsigned char lhs = static_cast(value[index]);
+ const unsigned char rhs = static_cast(prefix[index]);
+ if (std::tolower(lhs) != std::tolower(rhs)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+int HexValue(unsigned char ch) {
+ if (ch >= '0' && ch <= '9') {
+ return ch - '0';
+ }
+ if (ch >= 'a' && ch <= 'f') {
+ return 10 + (ch - 'a');
+ }
+ if (ch >= 'A' && ch <= 'F') {
+ return 10 + (ch - 'A');
+ }
+ return -1;
+}
+
+std::string PercentDecode(std::string_view value) {
+ std::string decoded;
+ decoded.reserve(value.size());
+
+ for (size_t index = 0; index < value.size(); index++) {
+ const unsigned char ch = static_cast(value[index]);
+ if ((ch == '%') && ((index + 2) < value.size())) {
+ const int hi = HexValue(static_cast(value[index + 1]));
+ const int lo = HexValue(static_cast(value[index + 2]));
+ if (hi >= 0 && lo >= 0) {
+ decoded.push_back(static_cast((hi << 4) | lo));
+ index += 2;
+ continue;
+ }
+ }
+
+ decoded.push_back(ch == '+' ? ' ' : static_cast(ch));
+ }
+
+ return decoded;
+}
+
+std::optional NormalizedPickerSelection(const std::string& path) {
+ const std::string trimmed = Trim(path);
+ if (trimmed.empty()) {
+ return std::nullopt;
+ }
+
+ return NormalizePathString(trimmed);
+}
+
std::string WithHiddenId(std::string_view label, std::string_view id) {
std::string out(label);
out += "##";
@@ -643,9 +700,166 @@ int RegionFromSettings(const FrontendSettings& settings) {
LaunchRequest LaunchForPath(const std::filesystem::path& path) {
LaunchRequest request;
- request.path = path;
- request.label = StemToTitle(path);
- request.kind = IsExePath(path) ? LaunchKind::Exe : LaunchKind::Disc;
+ if (path.empty()) {
+ return request;
+ }
+
+ request.path = std::filesystem::path(NormalizePathString(path.string()));
+ request.label = StemToTitle(request.path);
+
+ if (IsExePath(request.path)) {
+ request.kind = LaunchKind::Exe;
+ } else if (IsDiscPath(request.path)) {
+ request.kind = LaunchKind::Disc;
+ }
+
+ return request;
+}
+
+std::optional ParseUriLaunchKind(std::string_view value) {
+ const std::string lower = ToLower(Trim(value));
+
+ if (lower == "bios") {
+ return LaunchKind::Bios;
+ }
+ if (lower == "disc" || lower == "cdrom") {
+ return LaunchKind::Disc;
+ }
+ if (lower == "exe") {
+ return LaunchKind::Exe;
+ }
+
+ return std::nullopt;
+}
+
+std::optional QueryValue(std::string_view query, std::string_view key) {
+ size_t begin = 0;
+
+ while (begin <= query.size()) {
+ const size_t end = query.find('&', begin);
+ const std::string_view pair = query.substr(begin, end == std::string_view::npos ? query.size() - begin : end - begin);
+ const size_t sep = pair.find('=');
+ const std::string_view pair_key = pair.substr(0, sep);
+
+ if (ToLower(PercentDecode(pair_key)) == ToLower(key)) {
+ if (sep == std::string_view::npos) {
+ return std::string();
+ }
+ return PercentDecode(pair.substr(sep + 1));
+ }
+
+ if (end == std::string_view::npos) {
+ break;
+ }
+ begin = end + 1;
+ }
+
+ return std::nullopt;
+}
+
+std::string DecodeLaunchUriPathPayload(std::string_view payload) {
+ std::string decoded = PercentDecode(payload);
+
+ if (decoded.rfind("///", 0) == 0) {
+ decoded.erase(0, 2);
+ } else if (decoded.rfind("//", 0) == 0) {
+ decoded.erase(0, 2);
+ }
+
+#if defined(_WIN32)
+ if (decoded.size() >= 3 &&
+ decoded[0] == '/' &&
+ std::isalpha(static_cast(decoded[1])) &&
+ decoded[2] == ':') {
+ decoded.erase(decoded.begin());
+ }
+#endif
+
+ return NormalizePathString(decoded);
+}
+
+LaunchRequest LaunchForUri(std::string_view uri) {
+ LaunchRequest request;
+
+ if (!StartsWithCaseInsensitive(uri, "armsx:")) {
+ return request;
+ }
+
+ std::string_view remainder = uri.substr(6);
+ const size_t fragment_pos = remainder.find('#');
+ if (fragment_pos != std::string_view::npos) {
+ remainder = remainder.substr(0, fragment_pos);
+ }
+
+ std::string_view query;
+ const size_t query_pos = remainder.find('?');
+ if (query_pos != std::string_view::npos) {
+ query = remainder.substr(query_pos + 1);
+ remainder = remainder.substr(0, query_pos);
+ }
+
+ std::optional kind = std::nullopt;
+ if (const std::optional kind_value = QueryValue(query, "kind")) {
+ kind = ParseUriLaunchKind(*kind_value);
+ }
+
+ if (kind == LaunchKind::Bios) {
+ request.kind = LaunchKind::Bios;
+ request.label = "PlayStation BIOS";
+ return request;
+ }
+
+ std::string launch_path;
+ if (const std::optional query_path = QueryValue(query, "path")) {
+ launch_path = NormalizePathString(*query_path);
+ } else {
+ launch_path = DecodeLaunchUriPathPayload(remainder);
+ }
+
+ const std::string launch_path_lower = ToLower(Trim(launch_path));
+ if (launch_path_lower == "bios" || launch_path_lower == "/bios") {
+ request.kind = LaunchKind::Bios;
+ request.label = "PlayStation BIOS";
+ return request;
+ }
+
+ request = LaunchForPath(std::filesystem::path(launch_path));
+ if (request.kind == LaunchKind::None) {
+ return request;
+ }
+
+ if (kind == LaunchKind::Disc || kind == LaunchKind::Exe) {
+ request.kind = *kind;
+ }
+
+ return request;
+}
+
+LaunchRequest LaunchForArgument(std::string_view argument, std::optional forced_kind = std::nullopt) {
+ LaunchRequest request;
+ const std::string trimmed = Trim(argument);
+ if (trimmed.empty()) {
+ return request;
+ }
+
+ if (StartsWithCaseInsensitive(trimmed, "armsx:")) {
+ request = LaunchForUri(trimmed);
+ } else {
+ request = LaunchForPath(std::filesystem::path(trimmed));
+ }
+
+ if (request.kind == LaunchKind::None) {
+ return request;
+ }
+
+ if (forced_kind == LaunchKind::Bios) {
+ return LaunchRequest{.kind = LaunchKind::Bios, .path = {}, .label = "PlayStation BIOS"};
+ }
+
+ if ((forced_kind == LaunchKind::Disc || forced_kind == LaunchKind::Exe) && !request.path.empty()) {
+ request.kind = *forced_kind;
+ }
+
return request;
}
@@ -2160,19 +2374,10 @@ class ArmsxApp {
onWasmFile(*g_pending_wasm_path);
g_pending_wasm_path.reset();
} else if (cli_.has_boot_request) {
- LaunchRequest request;
- if (cli_.exe && !pending_cli_path_.empty()) {
- request.kind = LaunchKind::Exe;
- request.path = pending_cli_path_;
- request.label = StemToTitle(request.path);
- } else if ((cli_.cdrom || !pending_cli_path_.empty()) && !pending_cli_path_.empty()) {
- request.kind = IsExePath(pending_cli_path_) ? LaunchKind::Exe : LaunchKind::Disc;
- request.path = pending_cli_path_;
- request.label = StemToTitle(request.path);
- }
-
- if (request.kind != LaunchKind::None) {
- launchSession(request, false);
+ if (pending_cli_launch_.has_value()) {
+ launchSession(*pending_cli_launch_, false);
+ } else if (!pending_cli_argument_.empty()) {
+ pending_error_dialog_ = "Unsupported launch path or URI.";
}
}
@@ -2226,6 +2431,36 @@ class ArmsxApp {
}
private:
+ void queueLaunchRequest(const LaunchRequest& request, bool close_ui) {
+ if (request.kind == LaunchKind::None) {
+ return;
+ }
+
+ deferred_launch_ = request;
+ close_ui_after_launch_ = close_ui;
+ }
+
+ void queueLaunchSelection(const std::string& path, std::optional forced_kind = std::nullopt) {
+ const std::optional selection = NormalizedPickerSelection(path);
+ if (!selection.has_value()) {
+ return;
+ }
+
+ const LaunchRequest request = LaunchForArgument(*selection, forced_kind);
+ if (request.kind != LaunchKind::None) {
+ queueLaunchRequest(request, true);
+ }
+ }
+
+ void queueDiscSwapSelection(const std::string& path) {
+ const std::optional selection = NormalizedPickerSelection(path);
+ if (!selection.has_value()) {
+ return;
+ }
+
+ deferred_change_disc_ = std::filesystem::path(*selection);
+ }
+
Uint32 managedRendererFlags(bool vsync_enabled) const {
Uint32 renderer_flags = SDL_RENDERER_ACCELERATED;
#if !defined(__EMSCRIPTEN__)
@@ -2633,16 +2868,28 @@ class ArmsxApp {
if (argc_ > 1) {
for (int index = 1; index < argc_; index++) {
const std::string_view arg(argv_[index] ? argv_[index] : "");
- if (arg == "--cdrom" || arg == "-x" || arg == "--exe") {
+
+ auto set_boot_argument = [&](std::string_view value, std::optional forced_kind = std::nullopt) {
+ pending_cli_argument_ = std::string(value);
+ pending_cli_launch_ = LaunchForArgument(value, forced_kind);
+ };
+
+ if (arg == "--cdrom") {
if ((index + 1) < argc_) {
- pending_cli_path_ = argv_[index + 1];
+ set_boot_argument(argv_[index + 1] ? argv_[index + 1] : "", LaunchKind::Disc);
+ index++;
+ }
+ } else if (arg == "-x" || arg == "--exe") {
+ if ((index + 1) < argc_) {
+ set_boot_argument(argv_[index + 1] ? argv_[index + 1] : "", LaunchKind::Exe);
+ index++;
}
} else if (arg.starts_with("--cdrom=")) {
- pending_cli_path_ = std::string(arg.substr(8));
+ set_boot_argument(arg.substr(8), LaunchKind::Disc);
} else if (arg.starts_with("--exe=")) {
- pending_cli_path_ = std::string(arg.substr(6));
+ set_boot_argument(arg.substr(6), LaunchKind::Exe);
} else if (!arg.empty() && arg[0] != '-') {
- pending_cli_path_ = argv_[index];
+ set_boot_argument(arg);
}
}
}
@@ -2915,8 +3162,7 @@ class ArmsxApp {
switch (command.type) {
case fsui::CommandType::LaunchPath:
- deferred_launch_ = LaunchForPath(command.path);
- close_ui_after_launch_ = true;
+ queueLaunchRequest(LaunchForPath(command.path), true);
break;
case fsui::CommandType::RequestQuit:
@@ -2963,7 +3209,9 @@ class ArmsxApp {
psxe_diag_breadcrumbf("Deferred launch kind=%s path=%s",
LaunchKindTitle(request.kind),
request.path.empty() ? "(none)" : request.path.string().c_str());
- launchSession(request, close_ui_after_launch_);
+ if (request.kind != LaunchKind::None) {
+ launchSession(request, close_ui_after_launch_);
+ }
close_ui_after_launch_ = false;
}
@@ -2985,6 +3233,11 @@ class ArmsxApp {
const std::filesystem::path path = *deferred_change_disc_;
deferred_change_disc_.reset();
+ if (path.empty()) {
+ returnToMainWindow();
+ return;
+ }
+
if (!session_.swapDisc(path)) {
pending_error_dialog_ = "Failed to swap to the selected disc image.";
} else {
@@ -3158,9 +3411,6 @@ class ArmsxApp {
#if defined(UWP_TARGET)
add_scan_root(DefaultBrowseDirectory(), true);
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- add_scan_root(*external, true);
- }
#endif
for (const auto& [root, recursive] : scan_roots) {
@@ -3228,8 +3478,7 @@ class ArmsxApp {
"Start File",
false,
[this](const std::string& path) {
- deferred_launch_ = LaunchForPath(path);
- close_ui_after_launch_ = true;
+ queueLaunchSelection(path);
},
{".cue", ".bin", ".iso", ".img", ".exe", ".ps-exe", ".psexe"},
default_dir.string()
@@ -3264,8 +3513,7 @@ class ArmsxApp {
"Start Disc",
false,
[this](const std::string& path) {
- deferred_launch_ = LaunchForPath(path);
- close_ui_after_launch_ = true;
+ queueLaunchSelection(path, LaunchKind::Disc);
},
{".cue", ".bin", ".iso", ".img"},
default_dir.string()
@@ -3342,7 +3590,7 @@ class ArmsxApp {
ImGuiFullscreen::OpenFileSelector(
"Change Disc",
false,
- [this](const std::string& path) { deferred_change_disc_ = std::filesystem::path(path); },
+ [this](const std::string& path) { queueDiscSwapSelection(path); },
{".cue", ".bin", ".iso", ".img"},
initialBrowseDirectory().string()
);
@@ -3434,11 +3682,7 @@ class ArmsxApp {
folder.id = "bios-folder";
folder.title = ICON_FA_FOLDER " BIOS Folder";
if (settings_.bios_search.empty()) {
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- folder.summary = "Choose a folder that contains BIOS files. External UWP path available: " + external->string();
- } else {
- folder.summary = "Choose a folder that contains BIOS files.";
- }
+ folder.summary = AppendBrowseRootHint("Choose a folder that contains BIOS files.");
} else {
folder.summary = settings_.bios_search;
}
@@ -3447,8 +3691,10 @@ class ArmsxApp {
"BIOS Folder",
true,
[this](const std::string& path) {
- settings_.bios_search = NormalizePathString(path);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.bios_search = *selection;
+ SaveSettings(settings_);
+ }
},
{},
browseDirectoryForPath(settings_.bios_search, true).string()
@@ -3456,20 +3702,6 @@ class ArmsxApp {
};
rows.push_back(std::move(folder));
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- fsui::SettingsRowDescriptor external_folder;
- external_folder.kind = fsui::SettingsRowKind::Action;
- external_folder.id = "bios-folder-uwp-external";
- external_folder.title = ICON_FA_FOLDER_OPEN " Use E:/armsx";
- external_folder.summary = "Use the external UWP path for BIOS scanning if the drive is mounted.";
- external_folder.enabled = !PathsMatch(std::filesystem::path(settings_.bios_search), *external);
- external_folder.on_activate = [this, path = external->string()]() {
- settings_.bios_search = NormalizePathString(path);
- SaveSettings(settings_);
- };
- rows.push_back(std::move(external_folder));
- }
-
fsui::SettingsRowDescriptor override;
override.kind = fsui::SettingsRowKind::Action;
override.id = "bios-override";
@@ -3481,8 +3713,10 @@ class ArmsxApp {
"BIOS Override",
false,
[this](const std::string& path) {
- settings_.bios_override = NormalizePathString(path);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.bios_override = *selection;
+ SaveSettings(settings_);
+ }
},
{".bin", ".rom"},
browseDirectoryForPath(browse_seed, false).string()
@@ -3540,8 +3774,10 @@ class ArmsxApp {
"Expansion ROM",
false,
[this](const std::string& path) {
- settings_.exp_path = NormalizePathString(path);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.exp_path = *selection;
+ SaveSettings(settings_);
+ }
},
{".bin", ".rom"},
initialBrowseDirectory().string()
@@ -3559,8 +3795,10 @@ class ArmsxApp {
"Default PS-X EXE",
false,
[this](const std::string& path) {
- settings_.default_exe_path = NormalizePathString(path);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.default_exe_path = *selection;
+ SaveSettings(settings_);
+ }
},
{".exe", ".ps-exe", ".psexe"},
initialBrowseDirectory().string()
@@ -3586,32 +3824,30 @@ class ArmsxApp {
folders_page.build_rows = [this]() {
std::vector rows;
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- fsui::SettingsRowDescriptor external_notice;
- external_notice.kind = fsui::SettingsRowKind::Notice;
- external_notice.id = "uwp-external-path";
- external_notice.title = ICON_FA_FOLDER_OPEN " External Path";
- external_notice.summary = external->string() + " is available for BIOS and game scans on UWP.";
- rows.push_back(std::move(external_notice));
- }
+#if defined(UWP_TARGET)
+ fsui::SettingsRowDescriptor browse_notice;
+ browse_notice.kind = fsui::SettingsRowKind::Notice;
+ browse_notice.id = "uwp-filesystem-roots";
+ browse_notice.title = ICON_FA_FOLDER_OPEN " Mounted Drives";
+ browse_notice.summary = "Mounted drives are available from the file selector. Use Parent Directory at a drive root to switch drives.";
+ rows.push_back(std::move(browse_notice));
+#endif
fsui::SettingsRowDescriptor add_folder;
add_folder.kind = fsui::SettingsRowKind::Action;
add_folder.id = "add-folder";
add_folder.title = ICON_FA_FOLDER_PLUS " Add Library Folder";
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- add_folder.summary = "Scan only the selected directory. External UWP path available: " + external->string();
- } else {
- add_folder.summary = "Scan only the selected directory.";
- }
+ add_folder.summary = AppendBrowseRootHint("Scan only the selected directory.");
add_folder.on_activate = [this]() {
ImGuiFullscreen::OpenFileSelector(
"Add Library Folder",
true,
[this](const std::string& path) {
- settings_.ui_state.game_list_paths.emplace_back(NormalizePathString(path));
- refreshGameList(true);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.ui_state.game_list_paths.emplace_back(*selection);
+ refreshGameList(true);
+ SaveSettings(settings_);
+ }
},
{},
initialBrowseDirectory().string()
@@ -3619,39 +3855,21 @@ class ArmsxApp {
};
rows.push_back(std::move(add_folder));
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- fsui::SettingsRowDescriptor add_external;
- add_external.kind = fsui::SettingsRowKind::Action;
- add_external.id = "add-uwp-external-folder";
- add_external.title = ICON_FA_FOLDER_PLUS " Add E:/armsx";
- add_external.summary = "Scan the external UWP path and its children.";
- add_external.enabled = !PathListContains(settings_.ui_state.game_list_recursive_paths, *external)
- && !PathListContains(settings_.ui_state.game_list_paths, *external);
- add_external.on_activate = [this, path = *external]() {
- settings_.ui_state.game_list_recursive_paths.emplace_back(path);
- refreshGameList(true);
- SaveSettings(settings_);
- };
- rows.push_back(std::move(add_external));
- }
-
fsui::SettingsRowDescriptor add_recursive;
add_recursive.kind = fsui::SettingsRowKind::Action;
add_recursive.id = "add-recursive-folder";
add_recursive.title = ICON_FA_FOLDER_PLUS " Add Recursive Folder";
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- add_recursive.summary = "Scan the selected directory and its children. External UWP path available: " + external->string();
- } else {
- add_recursive.summary = "Scan the selected directory and its children.";
- }
+ add_recursive.summary = AppendBrowseRootHint("Scan the selected directory and its children.");
add_recursive.on_activate = [this]() {
ImGuiFullscreen::OpenFileSelector(
"Add Recursive Library Folder",
true,
[this](const std::string& path) {
- settings_.ui_state.game_list_recursive_paths.emplace_back(NormalizePathString(path));
- refreshGameList(true);
- SaveSettings(settings_);
+ if (const std::optional selection = NormalizedPickerSelection(path)) {
+ settings_.ui_state.game_list_recursive_paths.emplace_back(*selection);
+ refreshGameList(true);
+ SaveSettings(settings_);
+ }
},
{},
initialBrowseDirectory().string()
@@ -3798,7 +4016,7 @@ class ArmsxApp {
ImGuiFullscreen::OpenFileSelector(
"Change Disc",
false,
- [this](const std::string& path) { deferred_change_disc_ = std::filesystem::path(path); },
+ [this](const std::string& path) { queueDiscSwapSelection(path); },
{".cue", ".bin", ".iso", ".img"},
initialBrowseDirectory().string()
);
@@ -4017,10 +4235,6 @@ class ArmsxApp {
return settings_.ui_state.game_list_recursive_paths.front();
}
- if (const auto external = UwpExternalArmsxPath(); external.has_value()) {
- return *external;
- }
-
return DefaultBrowseDirectory();
}
@@ -4075,7 +4289,8 @@ class ArmsxApp {
std::optional deferred_change_disc_{};
std::optional deferred_vsync_{};
std::optional pending_settings_page_restore_{};
- std::string pending_cli_path_;
+ std::optional pending_cli_launch_{};
+ std::string pending_cli_argument_;
bool close_ui_after_launch_ = false;
bool deferred_exit_to_library_ = false;
bool deferred_reset_ = false;
@@ -4155,7 +4370,7 @@ void WriteNativeStackTraceImpl() {
#if !defined(UWP_TARGET)
SymCleanup(process);
#endif
-#elif !defined(__EMSCRIPTEN__) && !defined(__ANDROID__)
+#elif !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(PSVITA_TARGET)
void* frames[64] = {};
const int frame_count = backtrace(frames, static_cast(std::size(frames)));
psxe_diag_logf("crash", "Native stack trace (%d frames):", frame_count);
diff --git a/icons/ArmsxDesktop.icns b/icons/ArmsxDesktop.icns
new file mode 100644
index 0000000..230aa5c
Binary files /dev/null and b/icons/ArmsxDesktop.icns differ
diff --git a/icons/ArmsxDesktop.ico b/icons/ArmsxDesktop.ico
new file mode 100644
index 0000000..3951701
Binary files /dev/null and b/icons/ArmsxDesktop.ico differ
diff --git a/icons/ArmsxDesktop.png b/icons/ArmsxDesktop.png
new file mode 100644
index 0000000..4efdffb
Binary files /dev/null and b/icons/ArmsxDesktop.png differ
diff --git a/icons/FsuiAppIcon.png b/icons/FsuiAppIcon.png
new file mode 100644
index 0000000..2882c30
Binary files /dev/null and b/icons/FsuiAppIcon.png differ
diff --git a/psvita/Cargo.toml b/psvita/Cargo.toml
index 03807a4..01cbd9a 100644
--- a/psvita/Cargo.toml
+++ b/psvita/Cargo.toml
@@ -3,10 +3,9 @@ name = "ARMSX_vita"
version = "0.1.0"
edition = "2024"
-[dependencies]
-sdl2 = "0.34"
-
[package.metadata.vita]
title_id = "ARMSX001"
title_name = "ARMSX PSVita"
+[profile.release]
+panic = "abort"
diff --git a/psvita/build.rs b/psvita/build.rs
index 983381a..cc58282 100644
--- a/psvita/build.rs
+++ b/psvita/build.rs
@@ -1,19 +1,83 @@
+use std::env;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
fn main() {
- let target = std::env::var("TARGET").unwrap();
- if target.contains("vita") {
- let vitasdk = std::env::var("VITASDK").expect("VITASDK environment variable must be set");
- println!("cargo:rustc-link-search=native={}/arm-vita-eabi/lib", vitasdk);
- println!("cargo:rustc-link-lib=SDL2");
- println!("cargo:rustc-link-lib=SceGxm_stub");
- println!("cargo:rustc-link-lib=SceDisplay_stub");
- println!("cargo:rustc-link-lib=SceAudio_stub");
- println!("cargo:rustc-link-lib=SceAudioIn_stub");
- println!("cargo:rustc-link-lib=SceCtrl_stub");
- println!("cargo:rustc-link-lib=SceHid_stub");
- println!("cargo:rustc-link-lib=SceMotion_stub");
- println!("cargo:rustc-link-lib=SceIme_stub");
- println!("cargo:rustc-link-lib=SceCommonDialog_stub");
- println!("cargo:rustc-link-lib=SceTouch_stub");
- println!("cargo:rustc-link-lib=SceLibKernel_stub");
+ println!("cargo:rerun-if-env-changed=VITASDK");
+ println!("cargo:rerun-if-env-changed=ARMSX_VITA_NATIVE_DIR");
+ println!("cargo:rerun-if-env-changed=ARMSX_VITA_FSUI_DIR");
+
+ let target = env::var("TARGET").unwrap_or_default();
+ if !target.contains("vita") {
+ return;
}
-}
\ No newline at end of file
+
+ let vitasdk = env::var("VITASDK").expect("VITASDK environment variable must be set");
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"));
+ let repo_root = manifest_dir.parent().expect("psvita crate must live under the repo root");
+ let native_dir = env::var_os("ARMSX_VITA_NATIVE_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| repo_root.join("bin/psvita"));
+ let fsui_dir = env::var_os("ARMSX_VITA_FSUI_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| repo_root.join("build/fsui/psvita"));
+
+ let native_archive = require_file(native_dir.join("libarmsx_vita.a"));
+ let fsui_archives = [
+ "libfsui-donor.a",
+ "libfsui-backend-sdl.a",
+ "libfsui-platform-sdl2.a",
+ "libfsui-renderer-opengl.a",
+ "libfsui-renderer-sdl2.a",
+ "libfsui-renderer-sdl2surface.a",
+ "libfsui-core.a",
+ "libfsui_imgui.a",
+ "libfsui_resources.a",
+ "libfsui_glad.a",
+ ]
+ .into_iter()
+ .map(|name| require_file(fsui_dir.join(name)))
+ .collect::>();
+
+ println!("cargo:rustc-link-search=native={}/arm-vita-eabi/lib", vitasdk);
+ println!("cargo:rustc-link-lib=stdc++");
+ println!("cargo:rustc-link-arg=-Wl,--start-group");
+ println!("cargo:rustc-link-arg={}", native_archive.display());
+ for archive in &fsui_archives {
+ println!("cargo:rustc-link-arg={}", archive.display());
+ }
+ println!("cargo:rustc-link-arg=-Wl,--end-group");
+
+ emit_sdl2_link_flags(&vitasdk);
+}
+
+fn require_file(path: PathBuf) -> PathBuf {
+ assert!(path.is_file(), "required Vita build artifact is missing: {}", path.display());
+ path
+}
+
+fn emit_sdl2_link_flags(vitasdk: &str) {
+ let pkg_config = Path::new(vitasdk).join("bin/arm-vita-eabi-pkg-config");
+ let output = Command::new(&pkg_config)
+ .args(["--libs", "sdl2"])
+ .output()
+ .expect("failed to execute arm-vita-eabi-pkg-config");
+
+ if !output.status.success() {
+ panic!(
+ "arm-vita-eabi-pkg-config --libs sdl2 failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ let libs = String::from_utf8(output.stdout).expect("pkg-config output must be UTF-8");
+ for token in libs.split_whitespace() {
+ if let Some(path) = token.strip_prefix("-L") {
+ println!("cargo:rustc-link-search=native={path}");
+ } else if let Some(lib) = token.strip_prefix("-l") {
+ println!("cargo:rustc-link-lib={lib}");
+ } else if token.starts_with("-Wl,") {
+ println!("cargo:rustc-link-arg={token}");
+ }
+ }
+}
diff --git a/psvita/src/main.rs b/psvita/src/main.rs
index 5c296d3..3d27628 100644
--- a/psvita/src/main.rs
+++ b/psvita/src/main.rs
@@ -1,44 +1,104 @@
-extern crate sdl2;
-
-use std::ffi::CString;
+use std::env;
+use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
unsafe extern "C" {
fn external_main(argc: c_int, argv: *const *const c_char, external_window: *mut c_void, external_renderer: *mut c_void) -> c_int;
+ fn SDL_Init(flags: u32) -> c_int;
+ fn SDL_Quit();
+ fn SDL_CreateWindow(
+ title: *const c_char,
+ x: c_int,
+ y: c_int,
+ w: c_int,
+ h: c_int,
+ flags: u32,
+ ) -> *mut c_void;
+ fn SDL_CreateRenderer(window: *mut c_void, index: c_int, flags: u32) -> *mut c_void;
+ fn SDL_DestroyRenderer(renderer: *mut c_void);
+ fn SDL_DestroyWindow(window: *mut c_void);
+ fn SDL_GetError() -> *const c_char;
}
fn main() -> Result<(), String> {
- let sdl_context = sdl2::init()?;
- let video_subsystem = sdl_context.video()?;
+ const SDL_INIT_AUDIO: u32 = 0x0000_0010;
+ const SDL_INIT_VIDEO: u32 = 0x0000_0020;
+ const SDL_INIT_GAMECONTROLLER: u32 = 0x0000_2000;
+ const SDL_INIT_EVENTS: u32 = 0x0000_4000;
+ const SDL_WINDOWPOS_CENTERED: i32 = 0x2FFF_0000u32 as i32;
+ const SDL_RENDERER_ACCELERATED: u32 = 0x0000_0002;
+ const SDL_RENDERER_PRESENTVSYNC: u32 = 0x0000_0004;
+ let _ = env::set_current_dir("app0:/");
+ unsafe {
+ if SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0 {
+ return Err(sdl_error("SDL_Init"));
+ }
+ }
+
+ let title = CString::new("ARMSX PSVita Host").map_err(|err| err.to_string())?;
let width = 960;
let height = 544;
- let window = video_subsystem
- .window("ARMSX PSVita Host", width, height)
- .position_centered()
- .build()
- .map_err(|e| e.to_string())?;
+ let raw_window = unsafe {
+ SDL_CreateWindow(
+ title.as_ptr(),
+ SDL_WINDOWPOS_CENTERED,
+ SDL_WINDOWPOS_CENTERED,
+ width,
+ height,
+ 0,
+ )
+ };
+ if raw_window.is_null() {
+ unsafe { SDL_Quit() };
+ return Err(sdl_error("SDL_CreateWindow"));
+ }
- let canvas = window
- .into_canvas()
- .present_vsync()
- .accelerated()
- .build()
- .map_err(|e| e.to_string())?;
+ let raw_renderer =
+ unsafe { SDL_CreateRenderer(raw_window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) };
+ if raw_renderer.is_null() {
+ unsafe {
+ SDL_DestroyWindow(raw_window);
+ SDL_Quit();
+ }
+ return Err(sdl_error("SDL_CreateRenderer"));
+ }
- let args = vec!["psvita", "", ""];
- let cstrings: Vec = args.iter().map(|s| CString::new(*s).unwrap()).collect();
+ let mut args = env::args().collect::>();
+ if args.is_empty() {
+ args.push(String::from("armsx"));
+ }
+ let cstrings: Vec = args
+ .iter()
+ .map(|s| CString::new(s.as_str()).map_err(|err| err.to_string()))
+ .collect::>()?;
let mut ptrs: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
ptrs.push(std::ptr::null());
- let raw_window = canvas.window().raw() as *mut c_void;
- let raw_renderer = canvas.raw() as *mut c_void;
-
let ret = unsafe { external_main((ptrs.len() - 1) as c_int, ptrs.as_ptr(), raw_window, raw_renderer) };
+ unsafe {
+ SDL_DestroyRenderer(raw_renderer);
+ SDL_DestroyWindow(raw_window);
+ SDL_Quit();
+ }
+
if ret != 0 {
return Err(format!("external_main returned non-zero: {}", ret));
}
Ok(())
-}
\ No newline at end of file
+}
+
+fn sdl_error(prefix: &str) -> String {
+ let message = unsafe {
+ let raw = SDL_GetError();
+ if raw.is_null() {
+ String::from("unknown SDL error")
+ } else {
+ CStr::from_ptr(raw).to_string_lossy().into_owned()
+ }
+ };
+
+ format!("{prefix} failed: {message}")
+}
diff --git a/resources/windows/armsx.rc b/resources/windows/armsx.rc
new file mode 100644
index 0000000..ec15c6b
--- /dev/null
+++ b/resources/windows/armsx.rc
@@ -0,0 +1 @@
+APPICON ICON "icons/ArmsxDesktop.ico"
diff --git a/uwp/ARMSX/Assets/LockScreenLogo.scale-200.png b/uwp/ARMSX/Assets/LockScreenLogo.scale-200.png
index 735f57a..1c41f0e 100644
Binary files a/uwp/ARMSX/Assets/LockScreenLogo.scale-200.png and b/uwp/ARMSX/Assets/LockScreenLogo.scale-200.png differ
diff --git a/uwp/ARMSX/Assets/SplashScreen.scale-200.png b/uwp/ARMSX/Assets/SplashScreen.scale-200.png
index 023e7f1..2980230 100644
Binary files a/uwp/ARMSX/Assets/SplashScreen.scale-200.png and b/uwp/ARMSX/Assets/SplashScreen.scale-200.png differ
diff --git a/uwp/ARMSX/Assets/Square150x150Logo.scale-200.png b/uwp/ARMSX/Assets/Square150x150Logo.scale-200.png
index af49fec..97f38c3 100644
Binary files a/uwp/ARMSX/Assets/Square150x150Logo.scale-200.png and b/uwp/ARMSX/Assets/Square150x150Logo.scale-200.png differ
diff --git a/uwp/ARMSX/Assets/Square44x44Logo.scale-200.png b/uwp/ARMSX/Assets/Square44x44Logo.scale-200.png
index ce342a2..c5fdaab 100644
Binary files a/uwp/ARMSX/Assets/Square44x44Logo.scale-200.png and b/uwp/ARMSX/Assets/Square44x44Logo.scale-200.png differ
diff --git a/uwp/ARMSX/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/uwp/ARMSX/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
index f6c02ce..a00d4db 100644
Binary files a/uwp/ARMSX/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/uwp/ARMSX/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/uwp/ARMSX/Assets/StoreLogo.png b/uwp/ARMSX/Assets/StoreLogo.png
index 7385b56..0e1eb02 100644
Binary files a/uwp/ARMSX/Assets/StoreLogo.png and b/uwp/ARMSX/Assets/StoreLogo.png differ
diff --git a/uwp/ARMSX/Assets/Wide310x150Logo.scale-200.png b/uwp/ARMSX/Assets/Wide310x150Logo.scale-200.png
index 288995b..65cd80b 100644
Binary files a/uwp/ARMSX/Assets/Wide310x150Logo.scale-200.png and b/uwp/ARMSX/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/uwp/ARMSX/Package.appxmanifest b/uwp/ARMSX/Package.appxmanifest
index 58a787e..3f90a18 100644
--- a/uwp/ARMSX/Package.appxmanifest
+++ b/uwp/ARMSX/Package.appxmanifest
@@ -36,14 +36,23 @@
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"
Description="ARMSX"
- BackgroundColor="transparent">
+ BackgroundColor="#B8B4AA">
+
+
+
+ ARMSX Protocol
+ Assets\Square44x44Logo.png
+
+
+
+
-
\ No newline at end of file
+
diff --git a/uwp/ARMSX/Program.cs b/uwp/ARMSX/Program.cs
index 4cdda0f..d2e0dea 100644
--- a/uwp/ARMSX/Program.cs
+++ b/uwp/ARMSX/Program.cs
@@ -4,6 +4,8 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
+using Windows.ApplicationModel;
+using Windows.ApplicationModel.Activation;
using Windows.Storage;
using SDL2;
@@ -23,7 +25,7 @@ namespace ARMSX
static void Main(string[] args)
{
- launchArgs = args ?? Array.Empty();
+ launchArgs = ResolveLaunchArgs(args ?? Array.Empty());
nativeLogPath = BuildNativeLogPath();
InstallExceptionHandlers();
LogHost($"ARMSX UWP host starting. Native log path: {nativeLogPath}");
@@ -97,6 +99,29 @@ namespace ARMSX
return nativeArgs;
}
+ private static string[] ResolveLaunchArgs(string[] args)
+ {
+ try
+ {
+ IActivatedEventArgs activatedArgs = AppInstance.GetActivatedEventArgs();
+ if (activatedArgs is ProtocolActivatedEventArgs protocolArgs && protocolArgs.Uri != null)
+ {
+ string protocolUri = protocolArgs.Uri.OriginalString;
+ if (!string.IsNullOrWhiteSpace(protocolUri))
+ {
+ LogHost($"Protocol activation URI: {protocolUri}");
+ return new[] { protocolUri };
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManagedException("ResolveLaunchArgs", ex);
+ }
+
+ return args ?? Array.Empty();
+ }
+
private static void InstallExceptionHandlers()
{
AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>