Integrate standalone FSUI into Panda SDL frontend

This commit is contained in:
moonpower
2026-04-03 06:16:04 +02:00
parent 6be36b20ed
commit f2bc79352f
14 changed files with 2348 additions and 471 deletions

3
.gitmodules vendored
View File

@@ -80,3 +80,6 @@
[submodule "third_party/xbyak"]
path = third_party/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/elfio/)
include_directories(third_party/hips/include/)
include_directories(third_party/imgui/)
include_directories(third_party/dynarmic/src)
include_directories(third_party/cityhash/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/miniaudio)
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(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything
@@ -202,21 +208,36 @@ endif()
if (NOT ANDROID)
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
"${MINGW_UWP_DIR}/deps/include"
"${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)
find_package(SDL2 CONFIG REQUIRED)
target_link_libraries(AlberCore PUBLIC SDL2::SDL2)
else()
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)
endif()
set(SDL_TEST OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/SDL2)
if(IMGUI_FRONTEND)
target_link_libraries(AlberCore PUBLIC SDL2::SDL2)
else()
target_link_libraries(AlberCore PUBLIC SDL2-static)
endif()
endif()
endif()
add_subdirectory(third_party/fmt)
@@ -228,6 +249,16 @@ include_directories(third_party/duckstation)
include_directories(third_party/host_memory/include)
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_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost")
@@ -243,8 +274,6 @@ if(ANDROID)
target_link_libraries(AlberCore PRIVATE EGL log)
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
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC)
add_subdirectory(third_party/cryptoppwin)
@@ -500,18 +529,21 @@ cmrc_add_resource_library(
"src/core/services/fonts/SharedFontReplacement.bin"
)
set(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
third_party/cityhash/cityhash.cpp
set(THIRD_PARTY_SOURCE_FILES third_party/cityhash/cityhash.cpp
third_party/xxhash/xxhash.c
third_party/host_memory/host_memory.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)
# 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)
list(APPEND FRONTEND_SOURCE_FILES
src/panda_sdl/imgui_layer.cpp
third_party/imgui/backends/imgui_impl_sdl.cpp
third_party/imgui/backends/imgui_impl_opengl3.cpp
src/panda_sdl/panda_fsui.cpp
)
list(APPEND FRONTEND_HEADER_FILES
"include/panda_sdl/imgui_layer.hpp"
"include/panda_sdl/panda_fsui.hpp"
)
if(WIN32)
list(APPEND FRONTEND_LIBRARIES imm32)
@@ -916,6 +948,10 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
if(FRONTEND_LIBRARIES)
target_link_libraries(Alber PRIVATE ${FRONTEND_LIBRARIES})
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})
elseif(BUILD_HYDRA_CORE)
target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1)

View File

@@ -1,6 +1,7 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include "audio/dsp_core.hpp"
#include "frontend_settings.hpp"
@@ -110,6 +111,15 @@ struct EmulatorConfig {
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 = "";
// Frontend window settings
struct WindowSettings {

View File

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

View File

@@ -28,6 +28,7 @@ struct FrontendSettings {
WindowIcon icon = WindowIcon::Rpog;
std::string language = "en";
bool showImGuiDebugPanel = true;
bool enableFullscreenUI = false;
#ifdef IMGUI_FRONTEND
bool stretchImGuiOutputToWindow = true;
#else
@@ -39,4 +40,5 @@ struct FrontendSettings {
static WindowIcon iconFromString(std::string inString);
static const char* iconToString(WindowIcon icon);
static bool defaultFullscreenUIEnabled();
};

View File

@@ -4,10 +4,17 @@
#include <SDL.h>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include "emulator.hpp"
#include "fsui/backend_sdl.hpp"
#include "fsui/fsui.hpp"
#include "panda_sdl/panda_fsui.hpp"
struct ImFont;
class ImGuiLayer {
public:
@@ -30,13 +37,24 @@ class ImGuiLayer {
void showPauseMenuFromController();
private:
bool useFullscreenUI();
void drawDebugPanel();
void drawPausePanel();
void drawSettingsPanel();
void drawClassicPausePanel();
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_GLContext glContext = nullptr;
Emulator& emu;
fsui::FontStack fontStack;
fsui::SdlImGuiBackend imguiBackend;
PandaFsuiAdapter fsuiAdapter;
bool showDebug = true;
bool showPauseMenu = false;
@@ -44,6 +62,7 @@ class ImGuiLayer {
bool isPaused = false;
bool captureKeyboard = false;
bool captureMouse = false;
bool fullscreenSelectorMode = false;
std::function<void(bool)> onPauseChange;
std::function<void(bool)> onVsyncChange;

View File

@@ -0,0 +1,92 @@
#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;
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
// 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() {
const std::filesystem::path& path = filePath;
@@ -70,6 +73,31 @@ void EmulatorConfig::load() {
}
}
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));
}
}
}
}
}
if (data.contains("Window")) {
auto windowResult = toml::expect<toml::value>(data.at("Window"));
if (windowResult.is_ok()) {
@@ -170,6 +198,14 @@ void EmulatorConfig::load() {
frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(ui, "WindowIcon", "rpog"));
frontendSettings.language = toml::find_or<std::string>(ui, "Language", "en");
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);
#ifdef IMGUI_FRONTEND
frontendSettings.stretchImGuiOutputToWindow = toml::find_or<toml::boolean>(ui, "StretchImGuiOutputToWindow", true);
#else
@@ -177,6 +213,14 @@ void EmulatorConfig::load() {
#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", "");
}
}
}
void EmulatorConfig::save() {
@@ -212,6 +256,18 @@ void EmulatorConfig::save() {
}
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"]["RememberWindowPosition"] = windowSettings.rememberPosition;
data["Window"]["WindowPosX"] = windowSettings.x;
@@ -250,7 +306,15 @@ void EmulatorConfig::save() {
data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon));
data["UI"]["Language"] = frontendSettings.language;
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"]["StretchImGuiOutputToWindow"] = frontendSettings.stretchImGuiOutputToWindow;
data["Folders"]["Covers"] = fsuiCoversPath.string();
std::ofstream file(path, std::ios::out);
file << data;

View File

@@ -4,6 +4,10 @@
#include <cctype>
#include <unordered_map>
#if defined(__WINRT__) && !defined(__ANDROID__)
#include <SDL.h>
#endif
// Frontend setting serialization/deserialization functions
FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) {
@@ -63,3 +67,13 @@ const char* FrontendSettings::iconToString(WindowIcon icon) {
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

@@ -298,7 +298,20 @@ void FrontendSDL::initialize(SDL_Window* existingWindow, SDL_GLContext existingC
#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() {
#ifdef IMGUI_FRONTEND
@@ -717,7 +730,9 @@ void FrontendSDL::run() {
// TODO: Should this be uncommented?
// kernel.evalReschedule();
#ifndef IMGUI_FRONTEND
SDL_GL_SwapWindow(window);
#endif
}
#ifdef IMGUI_FRONTEND

File diff suppressed because it is too large Load Diff

1418
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 ddbe335536