From 18cca24bef857bc9bae77569401f089037260483 Mon Sep 17 00:00:00 2001 From: moonpower Date: Fri, 3 Apr 2026 16:39:33 +0200 Subject: [PATCH] Add overlays for inputs, settings, and performance in FSUI --- .gitignore | 1 + include/config.hpp | 3 ++ include/panda_sdl/panda_fsui.hpp | 3 ++ src/config.cpp | 6 +++ src/panda_sdl/panda_fsui.cpp | 70 ++++++++++++++++++++++++++++++++ third_party/fsui | 2 +- 6 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 13db40cd..8c22c642 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ Enabled/ Disabled/ build/ build_uwp/ +build_fsui/ .vs/ .vscode/*.log .cache/ diff --git a/include/config.hpp b/include/config.hpp index 4b1adafd..af4b642b 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -120,6 +120,9 @@ struct EmulatorConfig { std::string fsuiTheme = "Dark"; std::string fsuiPromptIconPack = "Auto"; std::filesystem::path fsuiBackgroundImagePath = ""; + bool fsuiShowInputsOverlay = false; + bool fsuiShowSettingsOverlay = false; + bool fsuiShowPerformanceOverlay = false; // Frontend window settings struct WindowSettings { diff --git a/include/panda_sdl/panda_fsui.hpp b/include/panda_sdl/panda_fsui.hpp index 4139b8c8..b6cef620 100644 --- a/include/panda_sdl/panda_fsui.hpp +++ b/include/panda_sdl/panda_fsui.hpp @@ -71,6 +71,9 @@ class PandaFsuiAdapter { std::string formatPercent(float value) const; std::string formatInteger(int value) const; std::string formatTitleId(std::uint64_t program_id) const; + std::vector buildPerformanceOverlayLines() const; + std::vector buildSettingsOverlayLines() const; + std::vector buildInputOverlayDevices() const; void openFileAndLaunch(); void requestLaunchPath(const std::filesystem::path& path); void requestClassicUi(bool open_settings); diff --git a/src/config.cpp b/src/config.cpp index 77e8be5c..fa3e3d91 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -206,6 +206,9 @@ void EmulatorConfig::load() { fsuiDefaultGameView = static_cast(toml::find_or(ui, "DefaultFullscreenUIGameView", 0)); fsuiGameSort = static_cast(toml::find_or(ui, "FullscreenUIGameSort", 0)); fsuiGameSortReverse = toml::find_or(ui, "FullscreenUIGameSortReverse", false); + fsuiShowInputsOverlay = toml::find_or(ui, "FullscreenUIShowInputs", false); + fsuiShowSettingsOverlay = toml::find_or(ui, "FullscreenUIShowSettings", false); + fsuiShowPerformanceOverlay = toml::find_or(ui, "FullscreenUIShowPerformance", false); #ifdef IMGUI_FRONTEND frontendSettings.stretchImGuiOutputToWindow = toml::find_or(ui, "StretchImGuiOutputToWindow", true); #else @@ -313,6 +316,9 @@ void EmulatorConfig::save() { 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["Folders"]["Covers"] = fsuiCoversPath.string(); diff --git a/src/panda_sdl/panda_fsui.cpp b/src/panda_sdl/panda_fsui.cpp index 66b47c3b..7d11308f 100644 --- a/src/panda_sdl/panda_fsui.cpp +++ b/src/panda_sdl/panda_fsui.cpp @@ -25,7 +25,9 @@ #include "fsui/imgui_fullscreen.hpp" #include "fsui/platform_sdl2.hpp" #include "io_file.hpp" +#include "services/hid.hpp" #include "services/region_codes.hpp" +#include "version.hpp" namespace { @@ -288,6 +290,9 @@ void PandaFsuiAdapter::syncUiStateFromConfig() uiState.game_list_paths = cfg.fsuiGameListPaths; uiState.game_list_recursive_paths = cfg.fsuiGameListRecursivePaths; uiState.covers_path = cfg.fsuiCoversPath.empty() ? (emu.getAppDataRoot() / "covers") : cfg.fsuiCoversPath; + uiState.show_inputs_overlay = cfg.fsuiShowInputsOverlay; + uiState.show_settings_overlay = cfg.fsuiShowSettingsOverlay; + uiState.show_performance_overlay = cfg.fsuiShowPerformanceOverlay; fsuiContext.app_icon_path = resolveFsuiAppIconPath(cfg.frontendSettings.icon); } @@ -303,6 +308,9 @@ void PandaFsuiAdapter::persistUiState(bool reload) cfg.fsuiGameListPaths = uiState.game_list_paths; cfg.fsuiGameListRecursivePaths = uiState.game_list_recursive_paths; cfg.fsuiCoversPath = uiState.covers_path; + cfg.fsuiShowInputsOverlay = uiState.show_inputs_overlay; + cfg.fsuiShowSettingsOverlay = uiState.show_settings_overlay; + cfg.fsuiShowPerformanceOverlay = uiState.show_performance_overlay; cfg.save(); if (reload) { emu.reloadSettings(); @@ -664,6 +672,59 @@ std::string PandaFsuiAdapter::currentGameSubtitle() const return rom_path.has_value() ? rom_path->filename().string() : "No title is currently running."; } +std::vector PandaFsuiAdapter::buildPerformanceOverlayLines() const +{ + char fps[64] = {}; + std::snprintf(fps, sizeof(fps), "FPS: %.1f", ImGui::GetIO().Framerate); + return { + fsui::OverlayTextLine{.text = fps}, + fsui::OverlayTextLine{.text = "Renderer: OpenGL 4.1"}, + fsui::OverlayTextLine{.text = std::string("Version: ") + PANDA3DS_VERSION}, + }; +} + +std::vector PandaFsuiAdapter::buildSettingsOverlayLines() const +{ + const EmulatorConfig& cfg = emu.getConfig(); + return { + fsui::OverlayTextLine{.text = std::string("VSync: ") + (cfg.vsyncEnabled ? "On" : "Off")}, + fsui::OverlayTextLine{.text = std::string("Layout: ") + s_screen_layout_names[static_cast(findArrayIndex(s_screen_layout_values, cfg.screenLayout))]}, + fsui::OverlayTextLine{.text = std::string("Shader JIT: ") + (cfg.shaderJitEnabled ? "On" : "Off")}, + fsui::OverlayTextLine{.text = std::string("Ubershaders: ") + (cfg.useUbershaders ? "On" : "Off")}, + }; +} + +std::vector PandaFsuiAdapter::buildInputOverlayDevices() const +{ + HIDService& hid = emu.getServiceManager().getHID(); + const u32 buttons = hid.getOldButtons(); + auto button_value = [buttons](u32 mask) { return (buttons & mask) ? 1.0f : 0.0f; }; + + fsui::InputOverlayDeviceState device; + device.title = "3DS"; + device.bindings = { + {.label = "Up", .glyph = ICON_PF_DPAD_UP, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Up)}, + {.label = "Down", .glyph = ICON_PF_DPAD_DOWN, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Down)}, + {.label = "Left", .glyph = ICON_PF_DPAD_LEFT, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Left)}, + {.label = "Right", .glyph = ICON_PF_DPAD_RIGHT, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Right)}, + {.label = "A", .glyph = ICON_PF_BUTTON_A, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::A)}, + {.label = "B", .glyph = ICON_PF_BUTTON_B, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::B)}, + {.label = "X", .glyph = ICON_PF_BUTTON_X, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::X)}, + {.label = "Y", .glyph = ICON_PF_BUTTON_Y, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Y)}, + {.label = "L", .glyph = ICON_PF_LEFT_SHOULDER_L1, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::L)}, + {.label = "R", .glyph = ICON_PF_RIGHT_SHOULDER_R1, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::R)}, + {.label = "ZL", .glyph = ICON_PF_LEFT_TRIGGER_ZL, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::ZL)}, + {.label = "ZR", .glyph = ICON_PF_RIGHT_TRIGGER_ZR, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::ZR)}, + {.label = "Start", .glyph = ICON_PF_START, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Start)}, + {.label = "Select", .glyph = ICON_PF_SELECT_SHARE, .kind = fsui::InputOverlayBindingKind::Button, .value = button_value(HID::Keys::Select)}, + {.label = "CP X", .kind = fsui::InputOverlayBindingKind::Axis, .value = static_cast(hid.getCirclepadX()) / 156.0f}, + {.label = "CP Y", .kind = fsui::InputOverlayBindingKind::Axis, .value = static_cast(hid.getCirclepadY()) / 156.0f}, + {.label = "C X", .kind = fsui::InputOverlayBindingKind::Axis, .value = static_cast(hid.getCStickX()) / 156.0f}, + {.label = "C Y", .kind = fsui::InputOverlayBindingKind::Axis, .value = static_cast(hid.getCStickY()) / 156.0f}, + }; + return {device}; +} + void PandaFsuiAdapter::requestLaunchPath(const std::filesystem::path& path) { pendingLaunchPath = path; @@ -1270,6 +1331,15 @@ bool PandaFsuiAdapter::initialize(const fsui::FontStack& fonts) } }; fsuiContext.host.detect_prompt_icon_pack = []() { return fsui::DetectPromptIconPackFromSDL(); }; + fsuiContext.host.detect_swap_north_west_gamepad_buttons = []() { return false; }; + fsuiContext.host.runtime_overlay_options = fsui::RuntimeOverlayOptions{ + .show_inputs = true, + .show_settings = true, + .show_performance = true, + }; + fsuiContext.host.get_input_overlay_devices = [this]() { return buildInputOverlayDevices(); }; + fsuiContext.host.get_settings_overlay_lines = [this]() { return buildSettingsOverlayLines(); }; + fsuiContext.host.get_performance_overlay_lines = [this]() { return buildPerformanceOverlayLines(); }; fsuiContext.host.request_classic_ui = [this]() { requestClassicUi(true); }; fsuiContext.host.launch_path = [this](const std::filesystem::path& path) { requestLaunchPath(path); }; fsuiContext.host.close_selector = [this]() { closeSelectorRequested = true; }; diff --git a/third_party/fsui b/third_party/fsui index ddbe3355..31ca34f5 160000 --- a/third_party/fsui +++ b/third_party/fsui @@ -1 +1 @@ -Subproject commit ddbe335536bd7ccc51c674824936e5c12fb2e7ad +Subproject commit 31ca34f55626d8932d3cd4fd7887a48ef896e23d