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) =>