Compare commits

...

10 Commits

Author SHA1 Message Date
moonpower
18cca24bef Add overlays for inputs, settings, and performance in FSUI 2026-04-03 16:39:33 +02:00
moonpower
f2bc79352f Integrate standalone FSUI into Panda SDL frontend 2026-04-03 06:16:04 +02:00
MoonPower
6be36b20ed Merge branch 'wheremyfoodat:master' into uwp_clean 2026-04-01 22:50:54 +02:00
wheremyfoodat
944b9892f9 Qt: Add recent games list (#824)
* Qt: Add recent games list

* Tests: Remove 3ds-examples build until devkitpro docker image is updated
2026-02-24 20:33:48 +02:00
wheremyfoodat
7e0150b303 Tests: Remove 3ds-examples build until devkitpro docker image is updated 2026-02-24 20:04:59 +02:00
wheremyfoodat
0c1ccbd177 Qt: Add recent games list 2026-02-24 19:56:51 +02:00
moonpower
8299059dd4 feat: add controller support for pause menu and update ImGuiLayer functionality 2026-02-02 03:08:01 +03:00
moonpower
cb9d09aca0 feat: enable gamepad navigation in ImGui and update .gitignore for UWP build 2026-02-02 02:30:56 +03:00
moonpower
8cc51b8223 feat: enable stretching ImGui output based on frontend settings and adjust window sizes 2026-02-02 01:59:36 +03:00
moonpower
9a9d24cb62 chore: update UWP image assets and mark cmrc subproject as dirty 2026-02-02 01:41:28 +03:00
25 changed files with 2597 additions and 473 deletions

View File

@@ -35,7 +35,9 @@ jobs:
- name: Clone and compile 3ds-examples - name: Clone and compile 3ds-examples
run: | run: |
git clone --recursive https://github.com/devkitPro/3ds-examples tests/3ds-examples git clone --recursive https://github.com/devkitPro/3ds-examples tests/3ds-examples
make -C tests/3ds-examples # The devkitpro docker image is outdated and cannot build 3ds-examples at the moment
# TODO: Reenable this when it's updated again
# make -C tests/3ds-examples
- name: Upload binaries - name: Upload binaries
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

3
.gitignore vendored
View File

@@ -16,7 +16,8 @@ ReleaseWithClangCL/
Enabled/ Enabled/
Disabled/ Disabled/
build/ build/
build_uwp/
build_fsui/
.vs/ .vs/
.vscode/*.log .vscode/*.log
.cache/ .cache/

3
.gitmodules vendored
View File

@@ -80,3 +80,6 @@
[submodule "third_party/xbyak"] [submodule "third_party/xbyak"]
path = third_party/xbyak path = third_party/xbyak
url = https://github.com/Panda3DS-emu/xbyak url = https://github.com/Panda3DS-emu/xbyak
[submodule "third_party/fsui"]
path = third_party/fsui
url = https://git.nanodata.cloud/moonpower/fsui-lib.git

View File

@@ -176,7 +176,6 @@ include_directories(${FMT_INCLUDE_DIR})
include_directories(third_party/boost/) include_directories(third_party/boost/)
include_directories(third_party/elfio/) include_directories(third_party/elfio/)
include_directories(third_party/hips/include/) include_directories(third_party/hips/include/)
include_directories(third_party/imgui/)
include_directories(third_party/dynarmic/src) include_directories(third_party/dynarmic/src)
include_directories(third_party/cityhash/include) include_directories(third_party/cityhash/include)
include_directories(third_party/result/include) include_directories(third_party/result/include)
@@ -186,6 +185,13 @@ include_directories(third_party/stb)
include_directories(third_party/opengl) include_directories(third_party/opengl)
include_directories(third_party/miniaudio) include_directories(third_party/miniaudio)
include_directories(third_party/mio/single_include) include_directories(third_party/mio/single_include)
if(IMGUI_FRONTEND)
include_directories(third_party/fsui/include)
include_directories(third_party/fsui/third_party/imgui)
include_directories(third_party/fsui/third_party/imgui/backends)
else()
include_directories(third_party/imgui/)
endif()
add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it
add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything
@@ -202,22 +208,37 @@ endif()
if (NOT ANDROID) if (NOT ANDROID)
if (UWP_BUILD) if (UWP_BUILD)
if (NOT TARGET SDL2::SDL2)
add_library(SDL2::SDL2 SHARED IMPORTED GLOBAL)
set_target_properties(SDL2::SDL2 PROPERTIES
IMPORTED_IMPLIB "${MINGW_UWP_DIR}/deps/lib/SDL2.lib"
INTERFACE_INCLUDE_DIRECTORIES "${MINGW_UWP_DIR}/deps/include;${MINGW_UWP_DIR}/deps/include/SDL2"
)
endif()
target_include_directories(AlberCore PUBLIC target_include_directories(AlberCore PUBLIC
"${MINGW_UWP_DIR}/deps/include" "${MINGW_UWP_DIR}/deps/include"
"${MINGW_UWP_DIR}/deps/include/SDL2" "${MINGW_UWP_DIR}/deps/include/SDL2"
) )
target_link_libraries(AlberCore PUBLIC "${MINGW_UWP_DIR}/deps/lib/SDL2.lib") target_link_libraries(AlberCore PUBLIC SDL2::SDL2)
elseif (USE_SYSTEM_SDL2) elseif (USE_SYSTEM_SDL2)
find_package(SDL2 CONFIG REQUIRED) find_package(SDL2 CONFIG REQUIRED)
target_link_libraries(AlberCore PUBLIC SDL2::SDL2) target_link_libraries(AlberCore PUBLIC SDL2::SDL2)
else() else()
set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE)
if(IMGUI_FRONTEND)
set(SDL_SHARED ON CACHE BOOL "" FORCE)
else()
set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE)
endif()
set(SDL_TEST OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/SDL2) add_subdirectory(third_party/SDL2)
if(IMGUI_FRONTEND)
target_link_libraries(AlberCore PUBLIC SDL2::SDL2)
else()
target_link_libraries(AlberCore PUBLIC SDL2-static) target_link_libraries(AlberCore PUBLIC SDL2-static)
endif() endif()
endif() endif()
endif()
add_subdirectory(third_party/fmt) add_subdirectory(third_party/fmt)
add_subdirectory(third_party/toml11) add_subdirectory(third_party/toml11)
@@ -228,6 +249,16 @@ include_directories(third_party/duckstation)
include_directories(third_party/host_memory/include) include_directories(third_party/host_memory/include)
add_subdirectory(third_party/cmrc) add_subdirectory(third_party/cmrc)
add_subdirectory(third_party/glad)
if(IMGUI_FRONTEND)
set(FSUI_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(FSUI_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
set(FSUI_PLATFORM_BACKEND SDL2 CACHE STRING "" FORCE)
add_subdirectory(third_party/fsui)
if(TARGET fsui-renderer-opengl)
target_compile_definitions(fsui-renderer-opengl PRIVATE FSUI_USE_LEGACY_GLAD=1)
endif()
endif()
set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/third_party/boost") set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/third_party/boost")
set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost") set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost")
@@ -243,8 +274,6 @@ if(ANDROID)
target_link_libraries(AlberCore PRIVATE EGL log) target_link_libraries(AlberCore PRIVATE EGL log)
endif() endif()
add_subdirectory(third_party/glad)
# Cryptopp doesn't support compiling under clang-cl, so we have to include it as a prebuilt MSVC static library # Cryptopp doesn't support compiling under clang-cl, so we have to include it as a prebuilt MSVC static library
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC)
add_subdirectory(third_party/cryptoppwin) add_subdirectory(third_party/cryptoppwin)
@@ -500,18 +529,21 @@ cmrc_add_resource_library(
"src/core/services/fonts/SharedFontReplacement.bin" "src/core/services/fonts/SharedFontReplacement.bin"
) )
set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp set(THIRD_PARTY_SOURCE_FILES third_party/cityhash/cityhash.cpp
third_party/imgui/imgui_draw.cpp
third_party/imgui/imgui_tables.cpp
third_party/imgui/imgui_widgets.cpp
third_party/imgui/imgui_demo.cpp
third_party/cityhash/cityhash.cpp
third_party/xxhash/xxhash.c third_party/xxhash/xxhash.c
third_party/host_memory/host_memory.cpp third_party/host_memory/host_memory.cpp
third_party/host_memory/virtual_buffer.cpp third_party/host_memory/virtual_buffer.cpp
) )
if(NOT IMGUI_FRONTEND)
list(APPEND THIRD_PARTY_SOURCE_FILES
third_party/imgui/imgui.cpp
third_party/imgui/imgui_draw.cpp
third_party/imgui/imgui_tables.cpp
third_party/imgui/imgui_widgets.cpp
third_party/imgui/imgui_demo.cpp
)
endif()
if(ENABLE_LUAJIT AND NOT ANDROID) if(ENABLE_LUAJIT AND NOT ANDROID)
# Build luv and libuv for Lua TCP server usage if we're not on Android # Build luv and libuv for Lua TCP server usage if we're not on Android
@@ -900,11 +932,11 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
if(IMGUI_FRONTEND) if(IMGUI_FRONTEND)
list(APPEND FRONTEND_SOURCE_FILES list(APPEND FRONTEND_SOURCE_FILES
src/panda_sdl/imgui_layer.cpp src/panda_sdl/imgui_layer.cpp
third_party/imgui/backends/imgui_impl_sdl.cpp src/panda_sdl/panda_fsui.cpp
third_party/imgui/backends/imgui_impl_opengl3.cpp
) )
list(APPEND FRONTEND_HEADER_FILES list(APPEND FRONTEND_HEADER_FILES
"include/panda_sdl/imgui_layer.hpp" "include/panda_sdl/imgui_layer.hpp"
"include/panda_sdl/panda_fsui.hpp"
) )
if(WIN32) if(WIN32)
list(APPEND FRONTEND_LIBRARIES imm32) list(APPEND FRONTEND_LIBRARIES imm32)
@@ -916,6 +948,10 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
if(FRONTEND_LIBRARIES) if(FRONTEND_LIBRARIES)
target_link_libraries(Alber PRIVATE ${FRONTEND_LIBRARIES}) target_link_libraries(Alber PRIVATE ${FRONTEND_LIBRARIES})
endif() endif()
if(IMGUI_FRONTEND)
target_link_libraries(Alber PRIVATE FSUI::backend-sdl FSUI::donor)
target_compile_definitions(Alber PRIVATE PANDA3DS_FSUI_ICON_DIR="${PROJECT_SOURCE_DIR}/docs/img")
endif()
target_sources(Alber PRIVATE ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES} ${GL_CONTEXT_SOURCE_FILES} ${APP_RESOURCES}) target_sources(Alber PRIVATE ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES} ${GL_CONTEXT_SOURCE_FILES} ${APP_RESOURCES})
elseif(BUILD_HYDRA_CORE) elseif(BUILD_HYDRA_CORE)
target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1) target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1)

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <string> #include <string>
#include <vector>
#include "audio/dsp_core.hpp" #include "audio/dsp_core.hpp"
#include "frontend_settings.hpp" #include "frontend_settings.hpp"
@@ -108,6 +109,21 @@ struct EmulatorConfig {
std::filesystem::path defaultRomPath = ""; std::filesystem::path defaultRomPath = "";
std::filesystem::path filePath; std::filesystem::path filePath;
static constexpr size_t maxRecentGames = 8;
std::vector<std::filesystem::path> recentlyPlayed;
std::vector<std::filesystem::path> fsuiGameListPaths;
std::vector<std::filesystem::path> fsuiGameListRecursivePaths;
std::filesystem::path fsuiCoversPath = "";
int fsuiDefaultGameView = 0;
int fsuiGameSort = 0;
bool fsuiGameSortReverse = false;
std::string fsuiTheme = "Dark";
std::string fsuiPromptIconPack = "Auto";
std::filesystem::path fsuiBackgroundImagePath = "";
bool fsuiShowInputsOverlay = false;
bool fsuiShowSettingsOverlay = false;
bool fsuiShowPerformanceOverlay = false;
// Frontend window settings // Frontend window settings
struct WindowSettings { struct WindowSettings {
static constexpr int defaultX = 200; static constexpr int defaultX = 200;
@@ -132,6 +148,8 @@ struct EmulatorConfig {
void load(); void load();
void save(); void save();
void addToRecentGames(const std::filesystem::path& path);
static LanguageCodes languageCodeFromString(std::string inString); static LanguageCodes languageCodeFromString(std::string inString);
static const char* languageCodeToString(LanguageCodes code); static const char* languageCodeToString(LanguageCodes code);
}; };

View File

@@ -126,6 +126,7 @@ class Emulator {
ServiceManager& getServiceManager() { return kernel.getServiceManager(); } ServiceManager& getServiceManager() { return kernel.getServiceManager(); }
LuaManager& getLua() { return lua; } LuaManager& getLua() { return lua; }
AudioDeviceInterface& getAudioDevice() { return audioDevice; } AudioDeviceInterface& getAudioDevice() { return audioDevice; }
const std::optional<std::filesystem::path>& getROMPath() const { return romPath; }
RendererType getRendererType() const { return config.rendererType; } RendererType getRendererType() const { return config.rendererType; }
Renderer* getRenderer() { return gpu.getRenderer(); } Renderer* getRenderer() { return gpu.getRenderer(); }

View File

@@ -28,11 +28,17 @@ struct FrontendSettings {
WindowIcon icon = WindowIcon::Rpog; WindowIcon icon = WindowIcon::Rpog;
std::string language = "en"; std::string language = "en";
bool showImGuiDebugPanel = true; bool showImGuiDebugPanel = true;
bool enableFullscreenUI = false;
#ifdef IMGUI_FRONTEND
bool stretchImGuiOutputToWindow = true;
#else
bool stretchImGuiOutputToWindow = false; bool stretchImGuiOutputToWindow = false;
#endif
static Theme themeFromString(std::string inString); static Theme themeFromString(std::string inString);
static const char* themeToString(Theme theme); static const char* themeToString(Theme theme);
static WindowIcon iconFromString(std::string inString); static WindowIcon iconFromString(std::string inString);
static const char* iconToString(WindowIcon icon); static const char* iconToString(WindowIcon icon);
static bool defaultFullscreenUIEnabled();
}; };

View File

@@ -103,6 +103,7 @@ class MainWindow : public QMainWindow {
std::vector<EmulatorMessage> messageQueue; std::vector<EmulatorMessage> messageQueue;
QMenuBar* menuBar = nullptr; QMenuBar* menuBar = nullptr;
QMenu* recentsMenu = nullptr;
InputMappings keyboardMappings; InputMappings keyboardMappings;
ScreenWidget* screen; ScreenWidget* screen;
AboutWindow* aboutWindow; AboutWindow* aboutWindow;
@@ -123,6 +124,8 @@ class MainWindow : public QMainWindow {
void emuThreadMainLoop(); void emuThreadMainLoop();
void selectLuaFile(); void selectLuaFile();
void selectROM(); void selectROM();
void loadROMFromPath(const std::filesystem::path& path);
void updateRecentsMenu();
void dumpDspFirmware(); void dumpDspFirmware();
void dumpRomFS(); void dumpRomFS();
void showAboutMenu(); void showAboutMenu();

View File

@@ -64,6 +64,11 @@ class FrontendSDL {
bool keyboardAnalogY = false; bool keyboardAnalogY = false;
bool emuPaused = false; bool emuPaused = false;
bool returnToSelector = false; bool returnToSelector = false;
#ifdef IMGUI_FRONTEND
bool controllerStartHeld = false;
bool controllerSelectHeld = false;
bool controllerPauseComboArmed = true;
#endif
private: private:
void setupControllerSensors(SDL_GameController* controller); void setupControllerSensors(SDL_GameController* controller);

View File

@@ -4,10 +4,17 @@
#include <SDL.h> #include <SDL.h>
#include <filesystem>
#include <functional> #include <functional>
#include <memory>
#include <optional> #include <optional>
#include "emulator.hpp" #include "emulator.hpp"
#include "fsui/backend_sdl.hpp"
#include "fsui/fsui.hpp"
#include "panda_sdl/panda_fsui.hpp"
struct ImFont;
class ImGuiLayer { class ImGuiLayer {
public: public:
@@ -27,15 +34,27 @@ class ImGuiLayer {
void setPauseCallback(std::function<void(bool)> callback) { onPauseChange = std::move(callback); } void setPauseCallback(std::function<void(bool)> callback) { onPauseChange = std::move(callback); }
void setVsyncCallback(std::function<void(bool)> callback) { onVsyncChange = std::move(callback); } void setVsyncCallback(std::function<void(bool)> callback) { onVsyncChange = std::move(callback); }
void setExitToSelectorCallback(std::function<void()> callback) { onExitToSelector = std::move(callback); } void setExitToSelectorCallback(std::function<void()> callback) { onExitToSelector = std::move(callback); }
void showPauseMenuFromController();
private: private:
bool useFullscreenUI();
void drawDebugPanel(); void drawDebugPanel();
void drawPausePanel(); void drawClassicPausePanel();
void drawSettingsPanel(); void drawClassicSettingsPanel();
void drawSettingsGeneralSection(bool& reloadSettings);
void drawSettingsWindowSection(bool& reloadSettings);
void drawSettingsUISection(bool& reloadSettings);
void drawSettingsGraphicsSection(bool& reloadSettings);
void drawSettingsAudioSection(bool& reloadSettings);
void drawSettingsBatterySection(bool& reloadSettings);
void drawSettingsSDSection(bool& reloadSettings);
SDL_Window* window = nullptr; SDL_Window* window = nullptr;
SDL_GLContext glContext = nullptr; SDL_GLContext glContext = nullptr;
Emulator& emu; Emulator& emu;
fsui::FontStack fontStack;
fsui::SdlImGuiBackend imguiBackend;
PandaFsuiAdapter fsuiAdapter;
bool showDebug = true; bool showDebug = true;
bool showPauseMenu = false; bool showPauseMenu = false;
@@ -43,6 +62,7 @@ class ImGuiLayer {
bool isPaused = false; bool isPaused = false;
bool captureKeyboard = false; bool captureKeyboard = false;
bool captureMouse = false; bool captureMouse = false;
bool fullscreenSelectorMode = false;
std::function<void(bool)> onPauseChange; std::function<void(bool)> onPauseChange;
std::function<void(bool)> onVsyncChange; std::function<void(bool)> onVsyncChange;

View File

@@ -0,0 +1,95 @@
#pragma once
#ifdef IMGUI_FRONTEND
#include <SDL.h>
#include <filesystem>
#include <functional>
#include <optional>
#include <vector>
#include "fsui/fsui.hpp"
class Emulator;
class PandaFsuiAdapter {
public:
enum class ClassicUiRequest {
None,
Return,
OpenSettings,
};
PandaFsuiAdapter(SDL_Window* window, Emulator& emu);
bool initialize(const fsui::FontStack& fonts);
void shutdown(bool clear_state);
void render();
void consumeCommands();
void setSelectorMode(bool selector_mode);
bool isSelectorMode() const;
bool hasActiveWindow() const;
void openPauseMenu();
void returnToPreviousWindow();
void returnToMainWindow();
void switchToSettings();
std::optional<std::filesystem::path> consumeLaunchPath();
bool consumeCloseSelector();
ClassicUiRequest consumeClassicUiRequest();
void setPauseCallback(std::function<void(bool)> callback);
void setVsyncCallback(std::function<void(bool)> callback);
void setExitToSelectorCallback(std::function<void()> callback);
private:
struct ParsedMetadata;
struct LanguageOption;
void syncUiStateFromConfig();
void persistUiState(bool reload);
void seedGameListPathsIfNeeded();
std::vector<std::filesystem::path> currentScanRoots() const;
bool hasSupportedExtension(const std::filesystem::path& path) const;
std::vector<fsui::GameEntry> buildGameList();
std::vector<fsui::SettingsPageDescriptor> buildSettingsPages(fsui::SettingsScope scope);
fsui::CurrentGameInfo buildCurrentGameInfo();
std::vector<fsui::MenuItemDescriptor> buildLandingItems();
std::vector<fsui::MenuItemDescriptor> buildStartItems();
std::vector<fsui::MenuItemDescriptor> buildExitItems();
std::vector<fsui::MenuItemDescriptor> buildPauseItems();
std::vector<fsui::MenuItemDescriptor> buildGameLaunchOptions(const fsui::GameEntry& entry);
std::optional<ParsedMetadata> readMetadataForPath(const std::filesystem::path& path) const;
fsui::GameEntry buildGameListEntry(const std::filesystem::directory_entry& entry) const;
std::filesystem::path defaultCoverDirectory() const;
std::filesystem::path findCoverPath(const std::filesystem::path& rom_path, const std::string& title_id) const;
std::string currentGameTitle() const;
std::string currentGameSubtitle() const;
std::string formatPercent(float value) const;
std::string formatInteger(int value) const;
std::string formatTitleId(std::uint64_t program_id) const;
std::vector<fsui::OverlayTextLine> buildPerformanceOverlayLines() const;
std::vector<fsui::OverlayTextLine> buildSettingsOverlayLines() const;
std::vector<fsui::InputOverlayDeviceState> buildInputOverlayDevices() const;
void openFileAndLaunch();
void requestLaunchPath(const std::filesystem::path& path);
void requestClassicUi(bool open_settings);
void openUnsupportedPrompt(std::string title, std::string message) const;
SDL_Window* window = nullptr;
Emulator& emu;
fsui::UiContext fsuiContext;
fsui::UiState uiState;
std::vector<fsui::GameEntry> cachedGameList;
std::optional<std::filesystem::path> pendingLaunchPath;
bool closeSelectorRequested = false;
ClassicUiRequest pendingClassicUiRequest = ClassicUiRequest::None;
std::function<void(bool)> onPauseChange;
std::function<void(bool)> onVsyncChange;
std::function<void()> onExitToSelector;
};
#endif

View File

@@ -15,7 +15,10 @@
// We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions // We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions
// However we still want to follow the license as closely as possible and offer the proper attributions. // However we still want to follow the license as closely as possible and offer the proper attributions.
EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) { load(); } EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) {
frontendSettings.enableFullscreenUI = FrontendSettings::defaultFullscreenUIEnabled();
load();
}
void EmulatorConfig::load() { void EmulatorConfig::load() {
const std::filesystem::path& path = filePath; const std::filesystem::path& path = filePath;
@@ -50,6 +53,48 @@ void EmulatorConfig::load() {
circlePadProEnabled = toml::find_or<toml::boolean>(general, "EnableCirclePadPro", true); circlePadProEnabled = toml::find_or<toml::boolean>(general, "EnableCirclePadPro", true);
fastmemEnabled = toml::find_or<toml::boolean>(general, "EnableFastmem", enableFastmemDefault); fastmemEnabled = toml::find_or<toml::boolean>(general, "EnableFastmem", enableFastmemDefault);
systemLanguage = languageCodeFromString(toml::find_or<std::string>(general, "SystemLanguage", "en")); systemLanguage = languageCodeFromString(toml::find_or<std::string>(general, "SystemLanguage", "en"));
// Load recent games list
if (general.contains("RecentGames") && general.at("RecentGames").is_array()) {
const auto& recentsArray = general.at("RecentGames").as_array();
recentlyPlayed.clear();
for (const auto& item : recentsArray) {
if (item.is_string()) {
std::filesystem::path gamePath = toml::get<std::string>(item);
recentlyPlayed.push_back(gamePath);
if (recentlyPlayed.size() >= maxRecentGames) {
break;
}
}
}
}
}
}
if (data.contains("GameList")) {
auto gameListResult = toml::expect<toml::value>(data.at("GameList"));
if (gameListResult.is_ok()) {
auto gameList = gameListResult.unwrap();
fsuiGameListPaths.clear();
if (gameList.contains("Paths") && gameList.at("Paths").is_array()) {
for (const auto& item : gameList.at("Paths").as_array()) {
if (item.is_string()) {
fsuiGameListPaths.emplace_back(toml::get<std::string>(item));
}
}
}
fsuiGameListRecursivePaths.clear();
if (gameList.contains("RecursivePaths") && gameList.at("RecursivePaths").is_array()) {
for (const auto& item : gameList.at("RecursivePaths").as_array()) {
if (item.is_string()) {
fsuiGameListRecursivePaths.emplace_back(toml::get<std::string>(item));
}
}
}
} }
} }
@@ -153,7 +198,30 @@ void EmulatorConfig::load() {
frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(ui, "WindowIcon", "rpog")); frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(ui, "WindowIcon", "rpog"));
frontendSettings.language = toml::find_or<std::string>(ui, "Language", "en"); frontendSettings.language = toml::find_or<std::string>(ui, "Language", "en");
frontendSettings.showImGuiDebugPanel = toml::find_or<toml::boolean>(ui, "ShowImGuiDebugPanel", true); frontendSettings.showImGuiDebugPanel = toml::find_or<toml::boolean>(ui, "ShowImGuiDebugPanel", true);
frontendSettings.enableFullscreenUI =
toml::find_or<toml::boolean>(ui, "EnableFullscreenUI", FrontendSettings::defaultFullscreenUIEnabled());
fsuiTheme = toml::find_or<std::string>(ui, "FullscreenUITheme", "Dark");
fsuiPromptIconPack = toml::find_or<std::string>(ui, "FullscreenUIPromptIcons", "Auto");
fsuiBackgroundImagePath = toml::find_or<std::string>(ui, "FullscreenUIBackgroundImage", "");
fsuiDefaultGameView = static_cast<int>(toml::find_or<toml::integer>(ui, "DefaultFullscreenUIGameView", 0));
fsuiGameSort = static_cast<int>(toml::find_or<toml::integer>(ui, "FullscreenUIGameSort", 0));
fsuiGameSortReverse = toml::find_or<toml::boolean>(ui, "FullscreenUIGameSortReverse", false);
fsuiShowInputsOverlay = toml::find_or<toml::boolean>(ui, "FullscreenUIShowInputs", false);
fsuiShowSettingsOverlay = toml::find_or<toml::boolean>(ui, "FullscreenUIShowSettings", false);
fsuiShowPerformanceOverlay = toml::find_or<toml::boolean>(ui, "FullscreenUIShowPerformance", false);
#ifdef IMGUI_FRONTEND
frontendSettings.stretchImGuiOutputToWindow = toml::find_or<toml::boolean>(ui, "StretchImGuiOutputToWindow", true);
#else
frontendSettings.stretchImGuiOutputToWindow = toml::find_or<toml::boolean>(ui, "StretchImGuiOutputToWindow", false); frontendSettings.stretchImGuiOutputToWindow = toml::find_or<toml::boolean>(ui, "StretchImGuiOutputToWindow", false);
#endif
}
}
if (data.contains("Folders")) {
auto foldersResult = toml::expect<toml::value>(data.at("Folders"));
if (foldersResult.is_ok()) {
auto folders = foldersResult.unwrap();
fsuiCoversPath = toml::find_or<std::string>(folders, "Covers", "");
} }
} }
} }
@@ -185,6 +253,24 @@ void EmulatorConfig::save() {
data["General"]["EnableCirclePadPro"] = circlePadProEnabled; data["General"]["EnableCirclePadPro"] = circlePadProEnabled;
data["General"]["EnableFastmem"] = fastmemEnabled; data["General"]["EnableFastmem"] = fastmemEnabled;
toml::array recentsArray;
for (const auto& gamePath : recentlyPlayed) {
recentsArray.push_back(gamePath.string());
}
data["General"]["RecentGames"] = recentsArray;
toml::array pathsArray;
for (const auto& path : fsuiGameListPaths) {
pathsArray.push_back(path.string());
}
data["GameList"]["Paths"] = pathsArray;
toml::array recursivePathsArray;
for (const auto& path : fsuiGameListRecursivePaths) {
recursivePathsArray.push_back(path.string());
}
data["GameList"]["RecursivePaths"] = recursivePathsArray;
data["Window"]["AppVersionOnWindow"] = windowSettings.showAppVersion; data["Window"]["AppVersionOnWindow"] = windowSettings.showAppVersion;
data["Window"]["RememberWindowPosition"] = windowSettings.rememberPosition; data["Window"]["RememberWindowPosition"] = windowSettings.rememberPosition;
data["Window"]["WindowPosX"] = windowSettings.x; data["Window"]["WindowPosX"] = windowSettings.x;
@@ -223,7 +309,18 @@ void EmulatorConfig::save() {
data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon)); data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon));
data["UI"]["Language"] = frontendSettings.language; data["UI"]["Language"] = frontendSettings.language;
data["UI"]["ShowImGuiDebugPanel"] = frontendSettings.showImGuiDebugPanel; data["UI"]["ShowImGuiDebugPanel"] = frontendSettings.showImGuiDebugPanel;
data["UI"]["EnableFullscreenUI"] = frontendSettings.enableFullscreenUI;
data["UI"]["FullscreenUITheme"] = fsuiTheme;
data["UI"]["FullscreenUIPromptIcons"] = fsuiPromptIconPack;
data["UI"]["FullscreenUIBackgroundImage"] = fsuiBackgroundImagePath.string();
data["UI"]["DefaultFullscreenUIGameView"] = fsuiDefaultGameView;
data["UI"]["FullscreenUIGameSort"] = fsuiGameSort;
data["UI"]["FullscreenUIGameSortReverse"] = fsuiGameSortReverse;
data["UI"]["FullscreenUIShowInputs"] = fsuiShowInputsOverlay;
data["UI"]["FullscreenUIShowSettings"] = fsuiShowSettingsOverlay;
data["UI"]["FullscreenUIShowPerformance"] = fsuiShowPerformanceOverlay;
data["UI"]["StretchImGuiOutputToWindow"] = frontendSettings.stretchImGuiOutputToWindow; data["UI"]["StretchImGuiOutputToWindow"] = frontendSettings.stretchImGuiOutputToWindow;
data["Folders"]["Covers"] = fsuiCoversPath.string();
std::ofstream file(path, std::ios::out); std::ofstream file(path, std::ios::out);
file << data; file << data;
@@ -282,3 +379,17 @@ const char* EmulatorConfig::languageCodeToString(LanguageCodes code) {
return codes[static_cast<u32>(code)]; return codes[static_cast<u32>(code)];
} }
} }
void EmulatorConfig::addToRecentGames(const std::filesystem::path& path) {
// Remove path if it's already in the list
auto it = std::find(recentlyPlayed.begin(), recentlyPlayed.end(), path);
if (it != recentlyPlayed.end()) {
recentlyPlayed.erase(it);
}
recentlyPlayed.insert(recentlyPlayed.begin(), path);
// Limit how many games can be saved
if (recentlyPlayed.size() > maxRecentGames) {
recentlyPlayed.resize(maxRecentGames);
}
}

View File

@@ -4,6 +4,10 @@
#include <cctype> #include <cctype>
#include <unordered_map> #include <unordered_map>
#if defined(__WINRT__) && !defined(__ANDROID__)
#include <SDL.h>
#endif
// Frontend setting serialization/deserialization functions // Frontend setting serialization/deserialization functions
FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) { FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) {
@@ -63,3 +67,13 @@ const char* FrontendSettings::iconToString(WindowIcon icon) {
default: return "rpog"; default: return "rpog";
} }
} }
bool FrontendSettings::defaultFullscreenUIEnabled() {
#if defined(__WINRT__) && !defined(__ANDROID__)
return SDL_WinRTGetDeviceFamily() == SDL_WINRT_DEVICEFAMILY_XBOX;
#elif defined(__XBOXONE__) || defined(__XBOXSERIES__)
return true;
#else
return false;
#endif
}

View File

@@ -53,6 +53,11 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
// Create and bind actions for them // Create and bind actions for them
auto loadGameAction = fileMenu->addAction(tr("Load game")); auto loadGameAction = fileMenu->addAction(tr("Load game"));
recentsMenu = fileMenu->addMenu(tr("Recents"));
updateRecentsMenu();
fileMenu->addSeparator();
auto loadLuaAction = fileMenu->addAction(tr("Load Lua script")); auto loadLuaAction = fileMenu->addAction(tr("Load Lua script"));
auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder")); auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder"));
@@ -140,6 +145,10 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
if (!emu->loadROM(romPath)) { if (!emu->loadROM(romPath)) {
// For some reason just .c_str() doesn't show the proper path // For some reason just .c_str() doesn't show the proper path
Helpers::warn("Failed to load ROM file: %s", romPath.string().c_str()); Helpers::warn("Failed to load ROM file: %s", romPath.string().c_str());
} else {
emu->getConfig().addToRecentGames(romPath);
emu->getConfig().save();
updateRecentsMenu();
} }
} }
@@ -240,11 +249,48 @@ void MainWindow::selectROM() {
); );
if (!path.isEmpty()) { if (!path.isEmpty()) {
std::filesystem::path* p = new std::filesystem::path(path.toStdU16String()); loadROMFromPath(std::filesystem::path(path.toStdU16String()));
}
}
void MainWindow::loadROMFromPath(const std::filesystem::path& path) {
std::filesystem::path* p = new std::filesystem::path(path);
EmulatorMessage message{.type = MessageType::LoadROM}; EmulatorMessage message{.type = MessageType::LoadROM};
message.path.p = p; message.path.p = p;
sendMessage(message); sendMessage(message);
emu->getConfig().addToRecentGames(path);
emu->getConfig().save();
updateRecentsMenu();
}
void MainWindow::updateRecentsMenu() {
recentsMenu->clear();
const auto& recentGames = emu->getConfig().recentlyPlayed;
if (recentGames.empty()) {
// Add a disabled "No recent games" item
QAction* noRecentsAction = recentsMenu->addAction(tr("No recent games"));
noRecentsAction->setEnabled(false);
} else {
for (const auto& gamePath : recentGames) {
QString displayName = QString::fromStdU16String(gamePath.filename().u16string());
QAction* action = recentsMenu->addAction(displayName);
// Store the full path in the action's data, set tooltip to show full path
action->setData(QString::fromStdU16String(gamePath.u16string()));
action->setToolTip(QString::fromStdU16String(gamePath.u16string()));
connect(action, &QAction::triggered, this, [this, gamePath]() { loadROMFromPath(gamePath); });
}
recentsMenu->addSeparator();
QAction* clearAction = recentsMenu->addAction(tr("Clear recent games"));
connect(clearAction, &QAction::triggered, this, [this]() {
emu->getConfig().recentlyPlayed.clear();
emu->getConfig().save();
updateRecentsMenu();
});
} }
} }

View File

@@ -22,8 +22,8 @@ FrontendSDL::FrontendSDL(SDL_Window* existingWindow, SDL_GLContext existingConte
FrontendSDL::ImGuiWindowContext FrontendSDL::createImGuiWindowContext(const EmulatorConfig& bootConfig, const char* windowTitle) { FrontendSDL::ImGuiWindowContext FrontendSDL::createImGuiWindowContext(const EmulatorConfig& bootConfig, const char* windowTitle) {
int windowX = SDL_WINDOWPOS_CENTERED; int windowX = SDL_WINDOWPOS_CENTERED;
int windowY = SDL_WINDOWPOS_CENTERED; int windowY = SDL_WINDOWPOS_CENTERED;
int windowW = 400; int windowW = 640;
int windowH = 480; int windowH = 360;
if (bootConfig.windowSettings.rememberPosition) { if (bootConfig.windowSettings.rememberPosition) {
windowX = bootConfig.windowSettings.x; windowX = bootConfig.windowSettings.x;
windowY = bootConfig.windowSettings.y; windowY = bootConfig.windowSettings.y;
@@ -115,8 +115,13 @@ void FrontendSDL::initialize(SDL_Window* existingWindow, SDL_GLContext existingC
} else { } else {
windowX = SDL_WINDOWPOS_CENTERED; windowX = SDL_WINDOWPOS_CENTERED;
windowY = SDL_WINDOWPOS_CENTERED; windowY = SDL_WINDOWPOS_CENTERED;
#ifdef IMGUI_FRONTEND
windowWidth = 640;
windowHeight = 360;
#else
windowWidth = 400; windowWidth = 400;
windowHeight = 480; windowHeight = 480;
#endif
} }
// Initialize output size and screen layout // Initialize output size and screen layout
@@ -293,7 +298,20 @@ void FrontendSDL::initialize(SDL_Window* existingWindow, SDL_GLContext existingC
#endif #endif
} }
bool FrontendSDL::loadROM(const std::filesystem::path& path) { return emu.loadROM(path); } bool FrontendSDL::loadROM(const std::filesystem::path& path) {
const bool loaded = emu.loadROM(path);
if (loaded) {
emuPaused = false;
#ifdef IMGUI_FRONTEND
if (imgui) {
imgui->setPaused(false);
}
#endif
emu.getConfig().addToRecentGames(path);
emu.getConfig().save();
}
return loaded;
}
std::optional<std::filesystem::path> FrontendSDL::selectGame() { std::optional<std::filesystem::path> FrontendSDL::selectGame() {
#ifdef IMGUI_FRONTEND #ifdef IMGUI_FRONTEND
@@ -309,22 +327,32 @@ void FrontendSDL::run() {
keyboardAnalogX = false; keyboardAnalogX = false;
keyboardAnalogY = false; keyboardAnalogY = false;
holdingRightClick = false; holdingRightClick = false;
#ifdef IMGUI_FRONTEND
int lastDrawableW = -1;
int lastDrawableH = -1;
controllerStartHeld = false;
controllerSelectHeld = false;
controllerPauseComboArmed = true;
#endif
while (programRunning) { while (programRunning) {
#ifdef IMGUI_FRONTEND #ifdef IMGUI_FRONTEND
const auto& cfg = emu.getConfig(); const auto& cfg = emu.getConfig();
if (cfg.frontendSettings.stretchImGuiOutputToWindow) {
int drawableW = 0; int drawableW = 0;
int drawableH = 0; int drawableH = 0;
SDL_GL_GetDrawableSize(window, &drawableW, &drawableH); SDL_GL_GetDrawableSize(window, &drawableW, &drawableH);
if (drawableW > 0 && drawableH > 0) { if (drawableW > 0 && drawableH > 0 && (drawableW != lastDrawableW || drawableH != lastDrawableH)) {
lastDrawableW = drawableW;
lastDrawableH = drawableH;
if (cfg.frontendSettings.stretchImGuiOutputToWindow) {
windowWidth = u32(drawableW); windowWidth = u32(drawableW);
windowHeight = u32(drawableH); windowHeight = u32(drawableH);
emu.setOutputSize(windowWidth, windowHeight); emu.setOutputSize(windowWidth, windowHeight);
}
if (cfg.frontendSettings.stretchImGuiOutputToWindow) {
ScreenLayout::calculateCoordinates( ScreenLayout::calculateCoordinates(
screenCoordinates, windowWidth, windowHeight, cfg.topScreenSize, cfg.screenLayout screenCoordinates, windowWidth, windowHeight, cfg.topScreenSize, cfg.screenLayout
); );
glViewport(0, 0, drawableW, drawableH);
} }
} }
#endif #endif
@@ -511,6 +539,24 @@ void FrontendSDL::run() {
case SDL_CONTROLLER_BUTTON_START: key = Keys::Start; break; case SDL_CONTROLLER_BUTTON_START: key = Keys::Start; break;
} }
#ifdef IMGUI_FRONTEND
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_START) {
controllerStartHeld = event.cbutton.state == SDL_PRESSED;
}
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_BACK) {
controllerSelectHeld = event.cbutton.state == SDL_PRESSED;
}
if (event.cbutton.state == SDL_PRESSED && controllerStartHeld && controllerSelectHeld && controllerPauseComboArmed) {
controllerPauseComboArmed = false;
if (imgui) {
imgui->showPauseMenuFromController();
}
}
if (event.cbutton.state == SDL_RELEASED && (!controllerStartHeld && !controllerSelectHeld)) {
controllerPauseComboArmed = true;
}
#endif
if (key != 0) { if (key != 0) {
if (event.cbutton.state == SDL_PRESSED) { if (event.cbutton.state == SDL_PRESSED) {
hid.pressKey(key); hid.pressKey(key);
@@ -684,7 +730,9 @@ void FrontendSDL::run() {
// TODO: Should this be uncommented? // TODO: Should this be uncommented?
// kernel.evalReschedule(); // kernel.evalReschedule();
#ifndef IMGUI_FRONTEND
SDL_GL_SwapWindow(window); SDL_GL_SwapWindow(window);
#endif
} }
#ifdef IMGUI_FRONTEND #ifdef IMGUI_FRONTEND

File diff suppressed because it is too large Load Diff

1488
src/panda_sdl/panda_fsui.cpp Normal file

File diff suppressed because it is too large Load Diff

1
third_party/fsui vendored Submodule

Submodule third_party/fsui added at 31ca34f556

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 19 KiB