duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

fullscreen_ui.cpp (340421B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR PolyForm-Strict-1.0.0)
      3 
      4 #include "fullscreen_ui.h"
      5 #include "achievements.h"
      6 #include "bios.h"
      7 #include "cheats.h"
      8 #include "controller.h"
      9 #include "game_list.h"
     10 #include "gpu.h"
     11 #include "host.h"
     12 #include "settings.h"
     13 #include "system.h"
     14 
     15 #include "scmversion/scmversion.h"
     16 
     17 #include "util/cd_image.h"
     18 #include "util/gpu_device.h"
     19 #include "util/imgui_fullscreen.h"
     20 #include "util/imgui_manager.h"
     21 #include "util/ini_settings_interface.h"
     22 #include "util/input_manager.h"
     23 #include "util/postprocessing.h"
     24 
     25 #include "common/error.h"
     26 #include "common/file_system.h"
     27 #include "common/log.h"
     28 #include "common/path.h"
     29 #include "common/small_string.h"
     30 #include "common/string_util.h"
     31 #include "common/timer.h"
     32 
     33 #include "IconsFontAwesome5.h"
     34 #include "IconsPromptFont.h"
     35 #include "fmt/chrono.h"
     36 #include "imgui.h"
     37 #include "imgui_internal.h"
     38 
     39 #include <atomic>
     40 #include <bitset>
     41 #include <unordered_map>
     42 #include <utility>
     43 #include <vector>
     44 
     45 Log_SetChannel(FullscreenUI);
     46 
     47 #define TR_CONTEXT "FullscreenUI"
     48 
     49 namespace {
     50 template<size_t L>
     51 class IconStackString : public SmallStackString<L>
     52 {
     53 public:
     54   ALWAYS_INLINE IconStackString(const char* icon, const char* str)
     55   {
     56     SmallStackString<L>::format("{} {}", icon, Host::TranslateToStringView(TR_CONTEXT, str));
     57   }
     58   ALWAYS_INLINE IconStackString(const char* icon, const char* str, const char* suffix)
     59   {
     60     SmallStackString<L>::format("{} {}##{}", icon, Host::TranslateToStringView(TR_CONTEXT, str), suffix);
     61   }
     62 };
     63 } // namespace
     64 
     65 #define FSUI_ICONSTR(icon, str) IconStackString<128>(icon, str).c_str()
     66 #define FSUI_STR(str) Host::TranslateToString(TR_CONTEXT, str)
     67 #define FSUI_CSTR(str) Host::TranslateToCString(TR_CONTEXT, str)
     68 #define FSUI_VSTR(str) Host::TranslateToStringView(TR_CONTEXT, str)
     69 #define FSUI_FSTR(str) fmt::runtime(Host::TranslateToStringView(TR_CONTEXT, str))
     70 #define FSUI_NSTR(str) str
     71 
     72 using ImGuiFullscreen::FocusResetType;
     73 using ImGuiFullscreen::g_large_font;
     74 using ImGuiFullscreen::g_layout_padding_left;
     75 using ImGuiFullscreen::g_layout_padding_top;
     76 using ImGuiFullscreen::g_medium_font;
     77 using ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT;
     78 using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE;
     79 using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE;
     80 using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT;
     81 using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY;
     82 using ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING;
     83 using ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING;
     84 using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT;
     85 using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH;
     86 using ImGuiFullscreen::UIBackgroundColor;
     87 using ImGuiFullscreen::UIBackgroundHighlightColor;
     88 using ImGuiFullscreen::UIBackgroundLineColor;
     89 using ImGuiFullscreen::UIBackgroundTextColor;
     90 using ImGuiFullscreen::UIDisabledColor;
     91 using ImGuiFullscreen::UIPrimaryColor;
     92 using ImGuiFullscreen::UIPrimaryDarkColor;
     93 using ImGuiFullscreen::UIPrimaryLightColor;
     94 using ImGuiFullscreen::UIPrimaryLineColor;
     95 using ImGuiFullscreen::UIPrimaryTextColor;
     96 using ImGuiFullscreen::UISecondaryColor;
     97 using ImGuiFullscreen::UISecondaryStrongColor;
     98 using ImGuiFullscreen::UISecondaryTextColor;
     99 using ImGuiFullscreen::UISecondaryWeakColor;
    100 using ImGuiFullscreen::UITextHighlightColor;
    101 
    102 using ImGuiFullscreen::ActiveButton;
    103 using ImGuiFullscreen::AddNotification;
    104 using ImGuiFullscreen::BeginFullscreenColumns;
    105 using ImGuiFullscreen::BeginFullscreenColumnWindow;
    106 using ImGuiFullscreen::BeginFullscreenWindow;
    107 using ImGuiFullscreen::BeginHorizontalMenu;
    108 using ImGuiFullscreen::BeginMenuButtons;
    109 using ImGuiFullscreen::BeginNavBar;
    110 using ImGuiFullscreen::CenterImage;
    111 using ImGuiFullscreen::CloseChoiceDialog;
    112 using ImGuiFullscreen::CloseFileSelector;
    113 using ImGuiFullscreen::CreateTextureFromImage;
    114 using ImGuiFullscreen::DefaultActiveButton;
    115 using ImGuiFullscreen::DrawShadowedText;
    116 using ImGuiFullscreen::EndFullscreenColumns;
    117 using ImGuiFullscreen::EndFullscreenColumnWindow;
    118 using ImGuiFullscreen::EndFullscreenWindow;
    119 using ImGuiFullscreen::EndHorizontalMenu;
    120 using ImGuiFullscreen::EndMenuButtons;
    121 using ImGuiFullscreen::EndNavBar;
    122 using ImGuiFullscreen::EnumChoiceButton;
    123 using ImGuiFullscreen::FloatingButton;
    124 using ImGuiFullscreen::ForceKeyNavEnabled;
    125 using ImGuiFullscreen::GetCachedTexture;
    126 using ImGuiFullscreen::GetCachedTextureAsync;
    127 using ImGuiFullscreen::GetPlaceholderTexture;
    128 using ImGuiFullscreen::HorizontalMenuItem;
    129 using ImGuiFullscreen::IsFocusResetFromWindowChange;
    130 using ImGuiFullscreen::IsFocusResetQueued;
    131 using ImGuiFullscreen::IsGamepadInputSource;
    132 using ImGuiFullscreen::LayoutScale;
    133 using ImGuiFullscreen::LoadTexture;
    134 using ImGuiFullscreen::MenuButton;
    135 using ImGuiFullscreen::MenuButtonFrame;
    136 using ImGuiFullscreen::MenuButtonWithoutSummary;
    137 using ImGuiFullscreen::MenuButtonWithValue;
    138 using ImGuiFullscreen::MenuHeading;
    139 using ImGuiFullscreen::MenuHeadingButton;
    140 using ImGuiFullscreen::MenuImageButton;
    141 using ImGuiFullscreen::ModAlpha;
    142 using ImGuiFullscreen::MulAlpha;
    143 using ImGuiFullscreen::NavButton;
    144 using ImGuiFullscreen::NavTab;
    145 using ImGuiFullscreen::NavTitle;
    146 using ImGuiFullscreen::OpenChoiceDialog;
    147 using ImGuiFullscreen::OpenConfirmMessageDialog;
    148 using ImGuiFullscreen::OpenFileSelector;
    149 using ImGuiFullscreen::OpenInputStringDialog;
    150 using ImGuiFullscreen::PopPrimaryColor;
    151 using ImGuiFullscreen::PushPrimaryColor;
    152 using ImGuiFullscreen::QueueResetFocus;
    153 using ImGuiFullscreen::RangeButton;
    154 using ImGuiFullscreen::ResetFocusHere;
    155 using ImGuiFullscreen::RightAlignNavButtons;
    156 using ImGuiFullscreen::SetFullscreenFooterText;
    157 using ImGuiFullscreen::ShowToast;
    158 using ImGuiFullscreen::ThreeWayToggleButton;
    159 using ImGuiFullscreen::ToggleButton;
    160 using ImGuiFullscreen::WantsToCloseMenu;
    161 
    162 #ifndef __ANDROID__
    163 namespace FullscreenUI {
    164 enum class MainWindowType
    165 {
    166   None,
    167   Landing,
    168   StartGame,
    169   Exit,
    170   GameList,
    171   GameListSettings,
    172   Settings,
    173   PauseMenu,
    174   Achievements,
    175   Leaderboards,
    176 };
    177 
    178 enum class PauseSubMenu
    179 {
    180   None,
    181   Exit,
    182   Achievements,
    183 };
    184 
    185 enum class SettingsPage
    186 {
    187   Summary,
    188   Interface,
    189   Console,
    190   Emulation,
    191   BIOS,
    192   Controller,
    193   Hotkey,
    194   MemoryCards,
    195   Display,
    196   PostProcessing,
    197   Audio,
    198   Achievements,
    199   Advanced,
    200   Count
    201 };
    202 
    203 enum class GameListView
    204 {
    205   Grid,
    206   List,
    207   Count
    208 };
    209 
    210 struct PostProcessingStageInfo
    211 {
    212   std::string name;
    213   std::vector<PostProcessing::ShaderOption> options;
    214 };
    215 
    216 //////////////////////////////////////////////////////////////////////////
    217 // Main
    218 //////////////////////////////////////////////////////////////////////////
    219 static void PauseForMenuOpen(bool set_pause_menu_open);
    220 static bool AreAnyDialogsOpen();
    221 static void ClosePauseMenu();
    222 static void OpenPauseSubMenu(PauseSubMenu submenu);
    223 static void DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size);
    224 static void DrawLandingWindow();
    225 static void DrawStartGameWindow();
    226 static void DrawExitWindow();
    227 static void DrawPauseMenu();
    228 static void ExitFullscreenAndOpenURL(std::string_view url);
    229 static void CopyTextToClipboard(std::string title, std::string_view text);
    230 static void DrawAboutWindow();
    231 static void OpenAboutWindow();
    232 static void FixStateIfPaused();
    233 static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel);
    234 
    235 static MainWindowType s_current_main_window = MainWindowType::None;
    236 static PauseSubMenu s_current_pause_submenu = PauseSubMenu::None;
    237 static std::string s_current_game_subtitle;
    238 static bool s_initialized = false;
    239 static bool s_tried_to_initialize = false;
    240 static bool s_pause_menu_was_open = false;
    241 static bool s_was_paused_on_quick_menu_open = false;
    242 static bool s_about_window_open = false;
    243 
    244 //////////////////////////////////////////////////////////////////////////
    245 // Resources
    246 //////////////////////////////////////////////////////////////////////////
    247 static bool LoadResources();
    248 static void DestroyResources();
    249 
    250 static std::shared_ptr<GPUTexture> s_app_icon_texture;
    251 static std::array<std::shared_ptr<GPUTexture>, static_cast<u32>(GameDatabase::CompatibilityRating::Count)>
    252   s_game_compatibility_textures;
    253 static std::shared_ptr<GPUTexture> s_fallback_disc_texture;
    254 static std::shared_ptr<GPUTexture> s_fallback_exe_texture;
    255 static std::shared_ptr<GPUTexture> s_fallback_psf_texture;
    256 static std::shared_ptr<GPUTexture> s_fallback_playlist_texture;
    257 
    258 //////////////////////////////////////////////////////////////////////////
    259 // Landing
    260 //////////////////////////////////////////////////////////////////////////
    261 static void SwitchToLanding();
    262 static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters();
    263 static void DoStartPath(std::string path, std::string state = std::string(),
    264                         std::optional<bool> fast_boot = std::nullopt);
    265 static void DoResume();
    266 static void DoStartFile();
    267 static void DoStartBIOS();
    268 static void DoStartDisc(std::string path);
    269 static void DoStartDisc();
    270 static void DoToggleFastForward();
    271 static void ConfirmIfSavingMemoryCards(std::string_view action, std::function<void(bool)> callback);
    272 static void RequestShutdown(bool save_state);
    273 static void RequestReset();
    274 static void DoChangeDiscFromFile();
    275 static void DoChangeDisc();
    276 static void DoRequestExit();
    277 static void DoDesktopMode();
    278 static void DoToggleFullscreen();
    279 static void DoCheatsMenu();
    280 static void DoToggleAnalogMode();
    281 
    282 //////////////////////////////////////////////////////////////////////////
    283 // Settings
    284 //////////////////////////////////////////////////////////////////////////
    285 
    286 static constexpr double INPUT_BINDING_TIMEOUT_SECONDS = 5.0;
    287 
    288 static void SwitchToSettings();
    289 static void SwitchToGameSettings();
    290 static void SwitchToGameSettings(const GameList::Entry* entry);
    291 static void SwitchToGameSettingsForPath(const std::string& path);
    292 static void SwitchToGameSettingsForSerial(std::string_view serial);
    293 static void DrawSettingsWindow();
    294 static void DrawSummarySettingsPage();
    295 static void DrawInterfaceSettingsPage();
    296 static void DrawBIOSSettingsPage();
    297 static void DrawConsoleSettingsPage();
    298 static void DrawEmulationSettingsPage();
    299 static void DrawDisplaySettingsPage();
    300 static void DrawPostProcessingSettingsPage();
    301 static void DrawAudioSettingsPage();
    302 static void DrawMemoryCardSettingsPage();
    303 static void DrawControllerSettingsPage();
    304 static void DrawHotkeySettingsPage();
    305 static void DrawAchievementsSettingsPage();
    306 static void DrawAdvancedSettingsPage();
    307 
    308 static bool IsEditingGameSettings(SettingsInterface* bsi);
    309 static SettingsInterface* GetEditingSettingsInterface();
    310 static SettingsInterface* GetEditingSettingsInterface(bool game_settings);
    311 static void SetSettingsChanged(SettingsInterface* bsi);
    312 static bool GetEffectiveBoolSetting(SettingsInterface* bsi, const char* section, const char* key, bool default_value);
    313 static s32 GetEffectiveIntSetting(SettingsInterface* bsi, const char* section, const char* key, s32 default_value);
    314 static u32 GetEffectiveUIntSetting(SettingsInterface* bsi, const char* section, const char* key, u32 default_value);
    315 static float GetEffectiveFloatSetting(SettingsInterface* bsi, const char* section, const char* key,
    316                                       float default_value);
    317 static TinyString GetEffectiveTinyStringSetting(SettingsInterface* bsi, const char* section, const char* key,
    318                                                 const char* default_value);
    319 static void DoCopyGameSettings();
    320 static void DoClearGameSettings();
    321 static void CopyGlobalControllerSettingsToGame();
    322 static void ResetControllerSettings();
    323 static void DoLoadInputProfile();
    324 static void DoSaveInputProfile();
    325 static void DoSaveNewInputProfile();
    326 static void DoSaveInputProfile(const std::string& name);
    327 
    328 static bool DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    329                               const char* key, bool default_value, bool enabled = true, bool allow_tristate = true,
    330                               float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
    331                               ImFont* summary_font = g_medium_font);
    332 static void DrawIntListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    333                                const char* key, int default_value, const char* const* options, size_t option_count,
    334                                bool translate_options, int option_offset = 0, bool enabled = true,
    335                                float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
    336                                ImFont* summary_font = g_medium_font, const char* tr_context = TR_CONTEXT);
    337 static void DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    338                                 const char* key, int default_value, int min_value, int max_value,
    339                                 const char* format = "%d", bool enabled = true,
    340                                 float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
    341                                 ImFont* summary_font = g_medium_font);
    342 static void DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    343                                   const char* key, int default_value, int min_value, int max_value, int step_value,
    344                                   const char* format = "%d", bool enabled = true,
    345                                   float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    346                                   ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    347 static void DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    348                                   const char* key, float default_value, float min_value, float max_value,
    349                                   const char* format = "%f", float multiplier = 1.0f, bool enabled = true,
    350                                   float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    351                                   ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    352 static void DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    353                                     const char* key, float default_value, float min_value, float max_value,
    354                                     float step_value, float multiplier, const char* format = "%f", bool enabled = true,
    355                                     float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    356                                     ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    357 #if 0
    358 static void DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    359                                const char* left_key, int default_left, const char* top_key, int default_top,
    360                                const char* right_key, int default_right, const char* bottom_key, int default_bottom,
    361                                int min_value, int max_value, const char* format = "%d", bool enabled = true,
    362                                float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
    363                                ImFont* summary_font = g_medium_font);
    364 static void DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    365                                   const char* key, const char* default_value, const char* const* options,
    366                                   const char* const* option_values, size_t option_count, bool enabled = true,
    367                                   float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    368                                   ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    369 #endif
    370 template<typename DataType, typename SizeType>
    371 static void DrawEnumSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    372                             const char* key, DataType default_value,
    373                             std::optional<DataType> (*from_string_function)(const char* str),
    374                             const char* (*to_string_function)(DataType value),
    375                             const char* (*to_display_string_function)(DataType value), SizeType option_count,
    376                             bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    377                             ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    378 static void DrawFloatListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
    379                                  const char* key, float default_value, const char* const* options,
    380                                  const float* option_values, size_t option_count, bool translate_options,
    381                                  bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    382                                  ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    383 static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key,
    384                               const std::string& runtime_var, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
    385                               ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
    386 
    387 static void PopulateGraphicsAdapterList();
    388 static void PopulateGameListDirectoryCache(SettingsInterface* si);
    389 static void PopulatePostProcessingChain(SettingsInterface* si, const char* section);
    390 static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section,
    391                               std::string_view key, std::string_view display_name);
    392 static void DrawInputBindingWindow();
    393 static void DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
    394                                    const char* name, const char* display_name, const char* icon_name,
    395                                    bool show_type = true);
    396 static void ClearInputBindingVariables();
    397 static void StartAutomaticBinding(u32 port);
    398 
    399 static SettingsPage s_settings_page = SettingsPage::Interface;
    400 static std::unique_ptr<INISettingsInterface> s_game_settings_interface;
    401 static std::unique_ptr<GameList::Entry> s_game_settings_entry;
    402 static std::vector<std::pair<std::string, bool>> s_game_list_directories_cache;
    403 static GPUDevice::AdapterInfoList s_graphics_adapter_list_cache;
    404 static std::vector<std::string> s_fullscreen_mode_list_cache;
    405 static std::vector<PostProcessingStageInfo> s_postprocessing_stages;
    406 static std::vector<const HotkeyInfo*> s_hotkey_list_cache;
    407 static std::atomic_bool s_settings_changed{false};
    408 static std::atomic_bool s_game_settings_changed{false};
    409 static InputBindingInfo::Type s_input_binding_type = InputBindingInfo::Type::Unknown;
    410 static std::string s_input_binding_section;
    411 static std::string s_input_binding_key;
    412 static std::string s_input_binding_display_name;
    413 static std::vector<InputBindingKey> s_input_binding_new_bindings;
    414 static std::vector<std::pair<InputBindingKey, std::pair<float, float>>> s_input_binding_value_ranges;
    415 static Common::Timer s_input_binding_timer;
    416 
    417 //////////////////////////////////////////////////////////////////////////
    418 // Save State List
    419 //////////////////////////////////////////////////////////////////////////
    420 struct SaveStateListEntry
    421 {
    422   std::string title;
    423   std::string summary;
    424   std::string path;
    425   std::unique_ptr<GPUTexture> preview_texture;
    426   time_t timestamp;
    427   s32 slot;
    428   bool global;
    429 };
    430 
    431 static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& serial, s32 slot,
    432                                                     bool global);
    433 static bool InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li, const std::string& serial, s32 slot,
    434                                                    bool global);
    435 static bool InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global);
    436 static void ClearSaveStateEntryList();
    437 static u32 PopulateSaveStateListEntries(const std::string& title, const std::string& serial);
    438 static bool OpenLoadStateSelectorForGame(const std::string& game_path);
    439 static bool OpenSaveStateSelector(bool is_loading);
    440 static void CloseSaveStateSelector();
    441 static void DrawSaveStateSelector(bool is_loading);
    442 static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry);
    443 static void DrawResumeStateSelector();
    444 static void DoLoadState(std::string path);
    445 static void DoSaveState(s32 slot, bool global);
    446 
    447 static std::vector<SaveStateListEntry> s_save_state_selector_slots;
    448 static std::string s_save_state_selector_game_path;
    449 static s32 s_save_state_selector_submenu_index = -1;
    450 static bool s_save_state_selector_open = false;
    451 static bool s_save_state_selector_loading = true;
    452 static bool s_save_state_selector_resuming = false;
    453 
    454 //////////////////////////////////////////////////////////////////////////
    455 // Game List
    456 //////////////////////////////////////////////////////////////////////////
    457 static void DrawGameListWindow();
    458 static void DrawGameList(const ImVec2& heading_size);
    459 static void DrawGameGrid(const ImVec2& heading_size);
    460 static void HandleGameListActivate(const GameList::Entry* entry);
    461 static void HandleGameListOptions(const GameList::Entry* entry);
    462 static void HandleSelectDiscForDiscSet(std::string_view disc_set_name);
    463 static void DrawGameListSettingsWindow();
    464 static void SwitchToGameList();
    465 static void PopulateGameListEntryList();
    466 static GPUTexture* GetTextureForGameListEntryType(GameList::EntryType type);
    467 static GPUTexture* GetGameListCover(const GameList::Entry* entry);
    468 static GPUTexture* GetCoverForCurrentGame();
    469 
    470 // Lazily populated cover images.
    471 static std::unordered_map<std::string, std::string> s_cover_image_map;
    472 static std::vector<const GameList::Entry*> s_game_list_sorted_entries;
    473 static GameListView s_game_list_view = GameListView::Grid;
    474 } // namespace FullscreenUI
    475 
    476 //////////////////////////////////////////////////////////////////////////
    477 // Utility
    478 //////////////////////////////////////////////////////////////////////////
    479 
    480 void FullscreenUI::TimeToPrintableString(SmallStringBase* str, time_t t)
    481 {
    482   struct tm lt = {};
    483 #ifdef _MSC_VER
    484   localtime_s(&lt, &t);
    485 #else
    486   localtime_r(&t, &lt);
    487 #endif
    488 
    489   char buf[256];
    490   std::strftime(buf, sizeof(buf), "%c", &lt);
    491   str->assign(buf);
    492 }
    493 
    494 void FullscreenUI::GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel)
    495 {
    496   if (IsGamepadInputSource())
    497   {
    498     ImGuiFullscreen::CreateFooterTextString(
    499       dest,
    500       std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")),
    501                  std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
    502                  std::make_pair(ICON_PF_BUTTON_B, back_instead_of_cancel ? FSUI_VSTR("Back") : FSUI_VSTR("Cancel"))});
    503   }
    504   else
    505   {
    506     ImGuiFullscreen::CreateFooterTextString(
    507       dest, std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")),
    508                        std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")),
    509                        std::make_pair(ICON_PF_ESC, back_instead_of_cancel ? FSUI_VSTR("Back") : FSUI_VSTR("Cancel"))});
    510   }
    511 }
    512 
    513 void FullscreenUI::SetStandardSelectionFooterText(bool back_instead_of_cancel)
    514 {
    515   SmallString text;
    516   GetStandardSelectionFooterText(text, back_instead_of_cancel);
    517   ImGuiFullscreen::SetFullscreenFooterText(text);
    518 }
    519 
    520 void ImGuiFullscreen::GetChoiceDialogHelpText(SmallStringBase& dest)
    521 {
    522   FullscreenUI::GetStandardSelectionFooterText(dest, false);
    523 }
    524 
    525 void ImGuiFullscreen::GetFileSelectorHelpText(SmallStringBase& dest)
    526 {
    527   if (IsGamepadInputSource())
    528   {
    529     ImGuiFullscreen::CreateFooterTextString(
    530       dest, std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")),
    531                        std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Parent Directory")),
    532                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
    533                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))});
    534   }
    535   else
    536   {
    537     ImGuiFullscreen::CreateFooterTextString(
    538       dest,
    539       std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")),
    540                  std::make_pair(ICON_PF_BACKSPACE, FSUI_VSTR("Parent Directory")),
    541                  std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))});
    542   }
    543 }
    544 
    545 void ImGuiFullscreen::GetInputDialogHelpText(SmallStringBase& dest)
    546 {
    547   if (IsGamepadInputSource())
    548   {
    549     CreateFooterTextString(dest, std::array{std::make_pair(ICON_PF_KEYBOARD, FSUI_VSTR("Enter Value")),
    550                                             std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
    551                                             std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))});
    552   }
    553   else
    554   {
    555     CreateFooterTextString(dest, std::array{std::make_pair(ICON_PF_KEYBOARD, FSUI_VSTR("Enter Value")),
    556                                             std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")),
    557                                             std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))});
    558   }
    559 }
    560 
    561 //////////////////////////////////////////////////////////////////////////
    562 // Main
    563 //////////////////////////////////////////////////////////////////////////
    564 
    565 bool FullscreenUI::Initialize()
    566 {
    567   if (s_initialized)
    568     return true;
    569 
    570   if (s_tried_to_initialize)
    571     return false;
    572 
    573   ImGuiFullscreen::SetTheme(Host::GetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", false));
    574   ImGuiFullscreen::UpdateLayoutScale();
    575 
    576   if (!ImGuiManager::AddFullscreenFontsIfMissing() || !ImGuiFullscreen::Initialize("images/placeholder.png") ||
    577       !LoadResources())
    578   {
    579     DestroyResources();
    580     ImGuiFullscreen::Shutdown();
    581     s_tried_to_initialize = true;
    582     return false;
    583   }
    584 
    585   s_initialized = true;
    586   s_current_main_window = MainWindowType::None;
    587   s_current_pause_submenu = PauseSubMenu::None;
    588   s_pause_menu_was_open = false;
    589   s_was_paused_on_quick_menu_open = false;
    590   s_about_window_open = false;
    591   s_hotkey_list_cache = InputManager::GetHotkeyList();
    592 
    593   if (!System::IsValid())
    594     SwitchToLanding();
    595 
    596   if (!System::IsRunning())
    597     Host::OnIdleStateChanged();
    598 
    599   ForceKeyNavEnabled();
    600   return true;
    601 }
    602 
    603 bool FullscreenUI::IsInitialized()
    604 {
    605   return s_initialized;
    606 }
    607 
    608 bool FullscreenUI::HasActiveWindow()
    609 {
    610   return s_initialized && (s_current_main_window != MainWindowType::None || AreAnyDialogsOpen());
    611 }
    612 
    613 bool FullscreenUI::AreAnyDialogsOpen()
    614 {
    615   return (s_save_state_selector_open || s_about_window_open ||
    616           s_input_binding_type != InputBindingInfo::Type::Unknown || ImGuiFullscreen::IsChoiceDialogOpen() ||
    617           ImGuiFullscreen::IsFileSelectorOpen());
    618 }
    619 
    620 void FullscreenUI::CheckForConfigChanges(const Settings& old_settings)
    621 {
    622   if (!IsInitialized())
    623     return;
    624 
    625   // If achievements got disabled, we might have the menu open...
    626   // That means we're going to be reading achievement state.
    627   if (old_settings.achievements_enabled && !g_settings.achievements_enabled)
    628   {
    629     if (s_current_main_window == MainWindowType::Achievements || s_current_main_window == MainWindowType::Leaderboards)
    630       ReturnToPreviousWindow();
    631   }
    632 }
    633 
    634 void FullscreenUI::OnSystemStarted()
    635 {
    636   if (!IsInitialized())
    637     return;
    638 
    639   s_current_main_window = MainWindowType::None;
    640   QueueResetFocus(FocusResetType::ViewChanged);
    641 }
    642 
    643 void FullscreenUI::OnSystemPaused()
    644 {
    645   // noop
    646 }
    647 
    648 void FullscreenUI::OnSystemResumed()
    649 {
    650   // get rid of pause menu if we unpaused another way
    651   if (s_current_main_window == MainWindowType::PauseMenu)
    652     ClosePauseMenu();
    653 }
    654 
    655 void FullscreenUI::OnSystemDestroyed()
    656 {
    657   if (!IsInitialized())
    658     return;
    659 
    660   s_pause_menu_was_open = false;
    661   s_was_paused_on_quick_menu_open = false;
    662   s_current_pause_submenu = PauseSubMenu::None;
    663   SwitchToLanding();
    664 }
    665 
    666 void FullscreenUI::OnRunningGameChanged()
    667 {
    668   if (!IsInitialized())
    669     return;
    670 
    671   const std::string& path = System::GetDiscPath();
    672   const std::string& serial = System::GetGameSerial();
    673   if (!serial.empty())
    674     s_current_game_subtitle = fmt::format("{0} - {1}", serial, Path::GetFileName(path));
    675   else
    676     s_current_game_subtitle = {};
    677 }
    678 
    679 void FullscreenUI::PauseForMenuOpen(bool set_pause_menu_open)
    680 {
    681   s_was_paused_on_quick_menu_open = (System::GetState() == System::State::Paused);
    682   if (!s_was_paused_on_quick_menu_open)
    683     Host::RunOnCPUThread([]() { System::PauseSystem(true); });
    684 
    685   s_pause_menu_was_open |= set_pause_menu_open;
    686 }
    687 
    688 void FullscreenUI::OpenPauseMenu()
    689 {
    690   if (!System::IsValid())
    691     return;
    692 
    693   if (!Initialize() || s_current_main_window != MainWindowType::None)
    694     return;
    695 
    696   PauseForMenuOpen(true);
    697   s_current_main_window = MainWindowType::PauseMenu;
    698   s_current_pause_submenu = PauseSubMenu::None;
    699   QueueResetFocus(FocusResetType::ViewChanged);
    700   ForceKeyNavEnabled();
    701   FixStateIfPaused();
    702 }
    703 
    704 void FullscreenUI::FixStateIfPaused()
    705 {
    706   if (!System::IsValid() || System::IsRunning())
    707     return;
    708 
    709   // When we're paused, we won't have trickled the key up event for escape yet. Do it now.
    710   ImGui::UpdateInputEvents(false);
    711 
    712   Host::OnIdleStateChanged();
    713   Host::RunOnCPUThread([]() {
    714     if (System::IsValid())
    715     {
    716       // Why twice? To clear the "wants keyboard input" flag.
    717       System::InvalidateDisplay();
    718       System::InvalidateDisplay();
    719     }
    720   });
    721 }
    722 
    723 void FullscreenUI::ClosePauseMenu()
    724 {
    725   if (!IsInitialized() || !System::IsValid())
    726     return;
    727 
    728   if (System::GetState() == System::State::Paused && !s_was_paused_on_quick_menu_open)
    729     Host::RunOnCPUThread([]() { System::PauseSystem(false); });
    730 
    731   s_current_main_window = MainWindowType::None;
    732   s_current_pause_submenu = PauseSubMenu::None;
    733   s_pause_menu_was_open = false;
    734   QueueResetFocus(FocusResetType::ViewChanged);
    735   FixStateIfPaused();
    736 }
    737 
    738 void FullscreenUI::OpenPauseSubMenu(PauseSubMenu submenu)
    739 {
    740   s_current_main_window = MainWindowType::PauseMenu;
    741   s_current_pause_submenu = submenu;
    742   QueueResetFocus(FocusResetType::ViewChanged);
    743 }
    744 
    745 void FullscreenUI::Shutdown()
    746 {
    747   Achievements::ClearUIState();
    748   CloseSaveStateSelector();
    749   s_cover_image_map.clear();
    750   s_game_list_sorted_entries = {};
    751   s_game_list_directories_cache = {};
    752   s_postprocessing_stages = {};
    753   s_fullscreen_mode_list_cache = {};
    754   s_graphics_adapter_list_cache = {};
    755   s_hotkey_list_cache = {};
    756   s_current_game_subtitle = {};
    757   DestroyResources();
    758   ImGuiFullscreen::Shutdown();
    759   s_initialized = false;
    760   s_tried_to_initialize = false;
    761 }
    762 
    763 void FullscreenUI::Render()
    764 {
    765   if (!s_initialized)
    766     return;
    767 
    768   ImGuiFullscreen::UploadAsyncTextures();
    769 
    770   ImGuiFullscreen::BeginLayout();
    771 
    772   // Primed achievements must come first, because we don't want the pause screen to be behind them.
    773   if (s_current_main_window == MainWindowType::None)
    774     Achievements::DrawGameOverlays();
    775 
    776   switch (s_current_main_window)
    777   {
    778     case MainWindowType::Landing:
    779       DrawLandingWindow();
    780       break;
    781     case MainWindowType::StartGame:
    782       DrawStartGameWindow();
    783       break;
    784     case MainWindowType::Exit:
    785       DrawExitWindow();
    786       break;
    787     case MainWindowType::GameList:
    788       DrawGameListWindow();
    789       break;
    790     case MainWindowType::GameListSettings:
    791       DrawGameListSettingsWindow();
    792       break;
    793     case MainWindowType::Settings:
    794       DrawSettingsWindow();
    795       break;
    796     case MainWindowType::PauseMenu:
    797       DrawPauseMenu();
    798       break;
    799     case MainWindowType::Achievements:
    800       Achievements::DrawAchievementsWindow();
    801       break;
    802     case MainWindowType::Leaderboards:
    803       Achievements::DrawLeaderboardsWindow();
    804       break;
    805     default:
    806       break;
    807   }
    808 
    809   if (s_save_state_selector_open)
    810   {
    811     if (s_save_state_selector_resuming)
    812       DrawResumeStateSelector();
    813     else
    814       DrawSaveStateSelector(s_save_state_selector_loading);
    815   }
    816 
    817   if (s_about_window_open)
    818     DrawAboutWindow();
    819 
    820   if (s_input_binding_type != InputBindingInfo::Type::Unknown)
    821     DrawInputBindingWindow();
    822 
    823   ImGuiFullscreen::EndLayout();
    824 
    825   if (s_settings_changed.load(std::memory_order_relaxed))
    826   {
    827     Host::CommitBaseSettingChanges();
    828     Host::RunOnCPUThread([]() { System::ApplySettings(false); });
    829     s_settings_changed.store(false, std::memory_order_release);
    830   }
    831   if (s_game_settings_changed.load(std::memory_order_relaxed))
    832   {
    833     if (s_game_settings_interface)
    834     {
    835       Error error;
    836       s_game_settings_interface->RemoveEmptySections();
    837 
    838       if (s_game_settings_interface->IsEmpty())
    839       {
    840         if (FileSystem::FileExists(s_game_settings_interface->GetFileName().c_str()) &&
    841             !FileSystem::DeleteFile(s_game_settings_interface->GetFileName().c_str(), &error))
    842         {
    843           ImGuiFullscreen::OpenInfoMessageDialog(
    844             FSUI_STR("Error"), fmt::format(FSUI_FSTR("An error occurred while deleting empty game settings:\n{}"),
    845                                            error.GetDescription()));
    846         }
    847       }
    848       else
    849       {
    850         if (!s_game_settings_interface->Save(&error))
    851         {
    852           ImGuiFullscreen::OpenInfoMessageDialog(
    853             FSUI_STR("Error"),
    854             fmt::format(FSUI_FSTR("An error occurred while saving game settings:\n{}"), error.GetDescription()));
    855         }
    856       }
    857 
    858       if (System::IsValid())
    859         Host::RunOnCPUThread([]() { System::ReloadGameSettings(false); });
    860     }
    861     s_game_settings_changed.store(false, std::memory_order_release);
    862   }
    863 
    864   ImGuiFullscreen::ResetCloseMenuIfNeeded();
    865 }
    866 
    867 void FullscreenUI::InvalidateCoverCache()
    868 {
    869   if (!IsInitialized())
    870     return;
    871 
    872   Host::RunOnCPUThread([]() { s_cover_image_map.clear(); });
    873 }
    874 
    875 void FullscreenUI::ReturnToPreviousWindow()
    876 {
    877   if (System::IsValid() && s_pause_menu_was_open)
    878   {
    879     s_current_main_window = MainWindowType::PauseMenu;
    880     QueueResetFocus(FocusResetType::ViewChanged);
    881   }
    882   else
    883   {
    884     ReturnToMainWindow();
    885   }
    886 }
    887 
    888 void FullscreenUI::ReturnToMainWindow()
    889 {
    890   ClosePauseMenu();
    891   s_current_main_window = System::IsValid() ? MainWindowType::None : MainWindowType::Landing;
    892   FixStateIfPaused();
    893 }
    894 
    895 bool FullscreenUI::LoadResources()
    896 {
    897   s_app_icon_texture = LoadTexture("images/duck.png");
    898 
    899   s_fallback_disc_texture = LoadTexture("fullscreenui/media-cdrom.png");
    900   s_fallback_exe_texture = LoadTexture("fullscreenui/applications-system.png");
    901   s_fallback_psf_texture = LoadTexture("fullscreenui/multimedia-player.png");
    902   s_fallback_playlist_texture = LoadTexture("fullscreenui/address-book-new.png");
    903 
    904   for (u32 i = 0; i < static_cast<u32>(GameDatabase::CompatibilityRating::Count); i++)
    905     s_game_compatibility_textures[i] = LoadTexture(TinyString::from_format("fullscreenui/star-{}.png", i).c_str());
    906 
    907   return true;
    908 }
    909 
    910 void FullscreenUI::DestroyResources()
    911 {
    912   s_app_icon_texture.reset();
    913   s_fallback_playlist_texture.reset();
    914   s_fallback_psf_texture.reset();
    915   s_fallback_exe_texture.reset();
    916   s_fallback_disc_texture.reset();
    917   for (auto& tex : s_game_compatibility_textures)
    918     tex.reset();
    919 }
    920 
    921 //////////////////////////////////////////////////////////////////////////
    922 // Utility
    923 //////////////////////////////////////////////////////////////////////////
    924 
    925 ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetDiscImageFilters()
    926 {
    927   return {"*.bin",   "*.cue",    "*.iso", "*.img", "*.chd",     "*.ecm", "*.mds",
    928           "*.psexe", "*.ps-exe", "*.exe", "*.psf", "*.minipsf", "*.m3u", "*.pbp"};
    929 }
    930 
    931 void FullscreenUI::DoStartPath(std::string path, std::string state, std::optional<bool> fast_boot)
    932 {
    933   if (System::IsValid())
    934     return;
    935 
    936   SystemBootParameters params;
    937   params.filename = std::move(path);
    938   params.save_state = std::move(state);
    939   params.override_fast_boot = std::move(fast_boot);
    940   Host::RunOnCPUThread([params = std::move(params)]() {
    941     if (System::IsValid())
    942       return;
    943 
    944     Error error;
    945     if (!System::BootSystem(std::move(params), &error))
    946     {
    947       Host::ReportErrorAsync(TRANSLATE_SV("System", "Error"),
    948                              fmt::format(TRANSLATE_FS("System", "Failed to boot system: {}"), error.GetDescription()));
    949     }
    950   });
    951 }
    952 
    953 void FullscreenUI::DoResume()
    954 {
    955   std::string path = System::GetMostRecentResumeSaveStatePath();
    956   if (path.empty())
    957   {
    958     ShowToast({}, FSUI_CSTR("No resume save state found."));
    959     return;
    960   }
    961 
    962   SaveStateListEntry slentry;
    963   if (!InitializeSaveStateListEntryFromPath(&slentry, std::move(path), -1, false))
    964     return;
    965 
    966   CloseSaveStateSelector();
    967   s_save_state_selector_slots.push_back(std::move(slentry));
    968   s_save_state_selector_game_path = {};
    969   s_save_state_selector_loading = true;
    970   s_save_state_selector_open = true;
    971   s_save_state_selector_resuming = true;
    972 }
    973 
    974 void FullscreenUI::DoStartFile()
    975 {
    976   auto callback = [](const std::string& path) {
    977     if (!path.empty())
    978       DoStartPath(path);
    979 
    980     CloseFileSelector();
    981   };
    982 
    983   OpenFileSelector(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), false, std::move(callback),
    984                    GetDiscImageFilters());
    985 }
    986 
    987 void FullscreenUI::DoStartBIOS()
    988 {
    989   DoStartDisc(std::string());
    990 }
    991 
    992 void FullscreenUI::DoStartDisc(std::string path)
    993 {
    994   Host::RunOnCPUThread([path = std::move(path)]() mutable {
    995     if (System::IsValid())
    996       return;
    997 
    998     Error error;
    999     SystemBootParameters params;
   1000     params.filename = std::move(path);
   1001     if (!System::BootSystem(std::move(params), &error))
   1002     {
   1003       Host::ReportErrorAsync(TRANSLATE_SV("System", "Error"),
   1004                              fmt::format(TRANSLATE_FS("System", "Failed to boot system: {}"), error.GetDescription()));
   1005     }
   1006   });
   1007 }
   1008 
   1009 void FullscreenUI::DoStartDisc()
   1010 {
   1011   std::vector<std::pair<std::string, std::string>> devices = CDImage::GetDeviceList();
   1012   if (devices.empty())
   1013   {
   1014     ShowToast(std::string(),
   1015               FSUI_STR("Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient "
   1016                        "permissions to access it."));
   1017     return;
   1018   }
   1019 
   1020   // if there's only one, select it automatically
   1021   if (devices.size() == 1)
   1022   {
   1023     DoStartDisc(std::move(devices.front().first));
   1024     return;
   1025   }
   1026 
   1027   ImGuiFullscreen::ChoiceDialogOptions options;
   1028   std::vector<std::string> paths;
   1029   options.reserve(devices.size());
   1030   paths.reserve(paths.size());
   1031   for (auto& [path, name] : devices)
   1032   {
   1033     options.emplace_back(std::move(name), false);
   1034     paths.push_back(std::move(path));
   1035   }
   1036   OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Drive"), false, std::move(options),
   1037                    [paths = std::move(paths)](s32 index, const std::string&, bool) mutable {
   1038                      if (index < 0)
   1039                        return;
   1040 
   1041                      DoStartDisc(std::move(paths[index]));
   1042                      CloseChoiceDialog();
   1043                    });
   1044 }
   1045 
   1046 void FullscreenUI::ConfirmIfSavingMemoryCards(std::string_view action, std::function<void(bool)> callback)
   1047 {
   1048   if (!System::IsSavingMemoryCards())
   1049   {
   1050     callback(true);
   1051     return;
   1052   }
   1053 
   1054   OpenConfirmMessageDialog(
   1055     FSUI_ICONSTR(ICON_PF_MEMORY_CARD, "Memory Card Busy"),
   1056     fmt::format(FSUI_FSTR("WARNING: Your game is still saving to the memory card. Continuing to {0} may IRREVERSIBLY "
   1057                           "DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 seconds for it to "
   1058                           "finish saving.\n\nDo you want to {0} anyway?"),
   1059                 action),
   1060     std::move(callback),
   1061     fmt::format(
   1062       fmt::runtime(FSUI_ICONSTR(ICON_FA_EXCLAMATION_TRIANGLE, "Yes, {} now and risk memory card corruption.")), action),
   1063     FSUI_ICONSTR(ICON_FA_PLAY, "No, resume the game."));
   1064 }
   1065 
   1066 void FullscreenUI::RequestShutdown(bool save_state)
   1067 {
   1068   ConfirmIfSavingMemoryCards(FSUI_VSTR("shut down"), [save_state](bool result) {
   1069     if (result)
   1070       Host::RunOnCPUThread([save_state]() { Host::RequestSystemShutdown(false, save_state); });
   1071     else
   1072       ClosePauseMenu();
   1073   });
   1074 }
   1075 
   1076 void FullscreenUI::RequestReset()
   1077 {
   1078   ConfirmIfSavingMemoryCards(FSUI_VSTR("reset"), [](bool result) {
   1079     if (result)
   1080       Host::RunOnCPUThread(System::ResetSystem);
   1081     else
   1082       ClosePauseMenu();
   1083   });
   1084 }
   1085 
   1086 void FullscreenUI::DoToggleFastForward()
   1087 {
   1088   Host::RunOnCPUThread([]() {
   1089     if (!System::IsValid())
   1090       return;
   1091 
   1092     System::SetFastForwardEnabled(!System::IsFastForwardEnabled());
   1093   });
   1094 }
   1095 
   1096 void FullscreenUI::DoChangeDiscFromFile()
   1097 {
   1098   ConfirmIfSavingMemoryCards(FSUI_VSTR("change disc"), [](bool result) {
   1099     if (!result)
   1100     {
   1101       ClosePauseMenu();
   1102       return;
   1103     }
   1104 
   1105     auto callback = [](const std::string& path) {
   1106       if (!path.empty())
   1107       {
   1108         if (!GameList::IsScannableFilename(path))
   1109         {
   1110           ShowToast({},
   1111                     fmt::format(FSUI_FSTR("{} is not a valid disc image."), FileSystem::GetDisplayNameFromPath(path)));
   1112         }
   1113         else
   1114         {
   1115           Host::RunOnCPUThread([path]() { System::InsertMedia(path.c_str()); });
   1116         }
   1117       }
   1118 
   1119       CloseFileSelector();
   1120       ReturnToPreviousWindow();
   1121     };
   1122 
   1123     OpenFileSelector(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), false, std::move(callback),
   1124                      GetDiscImageFilters(), std::string(Path::GetDirectory(System::GetDiscPath())));
   1125   });
   1126 }
   1127 
   1128 void FullscreenUI::DoChangeDisc()
   1129 {
   1130   ImGuiFullscreen::ChoiceDialogOptions options;
   1131 
   1132   if (System::HasMediaSubImages())
   1133   {
   1134     const u32 current_index = System::GetMediaSubImageIndex();
   1135     const u32 count = System::GetMediaSubImageCount();
   1136     options.reserve(count + 1);
   1137     options.emplace_back(FSUI_STR("From File..."), false);
   1138 
   1139     for (u32 i = 0; i < count; i++)
   1140       options.emplace_back(System::GetMediaSubImageTitle(i), i == current_index);
   1141 
   1142     auto callback = [](s32 index, const std::string& title, bool checked) {
   1143       if (index == 0)
   1144       {
   1145         CloseChoiceDialog();
   1146         DoChangeDiscFromFile();
   1147         return;
   1148       }
   1149       else if (index > 0)
   1150       {
   1151         System::SwitchMediaSubImage(static_cast<u32>(index - 1));
   1152       }
   1153 
   1154       CloseChoiceDialog();
   1155       ReturnToPreviousWindow();
   1156     };
   1157 
   1158     OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), true, std::move(options),
   1159                      std::move(callback));
   1160 
   1161     return;
   1162   }
   1163 
   1164   if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && !entry->disc_set_serials.empty())
   1165   {
   1166     const auto lock = GameList::GetLock();
   1167     auto matches = GameList::GetMatchingEntriesForSerial(entry->disc_set_serials);
   1168     if (matches.size() > 1)
   1169     {
   1170       options.reserve(matches.size() + 1);
   1171       options.emplace_back(FSUI_STR("From File..."), false);
   1172 
   1173       std::vector<std::string> paths;
   1174       paths.reserve(matches.size());
   1175 
   1176       const std::string& current_path = System::GetDiscPath();
   1177       for (auto& [title, glentry] : matches)
   1178       {
   1179         options.emplace_back(std::move(title), current_path == glentry->path);
   1180         paths.push_back(glentry->path);
   1181       }
   1182 
   1183       auto callback = [paths = std::move(paths)](s32 index, const std::string& title, bool checked) {
   1184         if (index == 0)
   1185         {
   1186           CloseChoiceDialog();
   1187           DoChangeDiscFromFile();
   1188           return;
   1189         }
   1190         else if (index > 0)
   1191         {
   1192           System::InsertMedia(paths[index - 1].c_str());
   1193         }
   1194 
   1195         CloseChoiceDialog();
   1196         ReturnToMainWindow();
   1197       };
   1198 
   1199       OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), true, std::move(options),
   1200                        std::move(callback));
   1201 
   1202       return;
   1203     }
   1204   }
   1205 
   1206   DoChangeDiscFromFile();
   1207 }
   1208 
   1209 void FullscreenUI::DoCheatsMenu()
   1210 {
   1211   CheatList* cl = System::GetCheatList();
   1212   if (!cl)
   1213   {
   1214     if (!System::LoadCheatListFromDatabase() || ((cl = System::GetCheatList()) == nullptr))
   1215     {
   1216       Host::AddKeyedOSDMessage("load_cheat_list",
   1217                                fmt::format(FSUI_FSTR("No cheats found for {}."), System::GetGameTitle()), 10.0f);
   1218       ReturnToPreviousWindow();
   1219       return;
   1220     }
   1221   }
   1222 
   1223   ImGuiFullscreen::ChoiceDialogOptions options;
   1224   options.reserve(cl->GetCodeCount());
   1225   for (u32 i = 0; i < cl->GetCodeCount(); i++)
   1226   {
   1227     const CheatCode& cc = cl->GetCode(i);
   1228     options.emplace_back(cc.description.c_str(), cc.enabled);
   1229   }
   1230 
   1231   auto callback = [](s32 index, const std::string& title, bool checked) {
   1232     if (index < 0)
   1233     {
   1234       ReturnToPreviousWindow();
   1235       return;
   1236     }
   1237 
   1238     CheatList* cl = System::GetCheatList();
   1239     if (!cl)
   1240       return;
   1241 
   1242     const CheatCode& cc = cl->GetCode(static_cast<u32>(index));
   1243     if (cc.activation == CheatCode::Activation::Manual)
   1244       cl->ApplyCode(static_cast<u32>(index));
   1245     else
   1246       System::SetCheatCodeState(static_cast<u32>(index), checked);
   1247   };
   1248   OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_FROWN, "Cheat List"), true, std::move(options), std::move(callback));
   1249 }
   1250 
   1251 void FullscreenUI::DoToggleAnalogMode()
   1252 {
   1253   // hacky way to toggle analog mode
   1254   for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
   1255   {
   1256     Controller* ctrl = System::GetController(i);
   1257     if (!ctrl)
   1258       continue;
   1259 
   1260     const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(ctrl->GetType());
   1261     if (!cinfo)
   1262       continue;
   1263 
   1264     for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
   1265     {
   1266       if (std::strcmp(bi.name, "Analog") == 0)
   1267       {
   1268         ctrl->SetBindState(bi.bind_index, 1.0f);
   1269         ctrl->SetBindState(bi.bind_index, 0.0f);
   1270         break;
   1271       }
   1272     }
   1273   }
   1274 }
   1275 
   1276 void FullscreenUI::DoRequestExit()
   1277 {
   1278   Host::RunOnCPUThread([]() { Host::RequestExitApplication(true); });
   1279 }
   1280 
   1281 void FullscreenUI::DoDesktopMode()
   1282 {
   1283   Host::RunOnCPUThread([]() { Host::RequestExitBigPicture(); });
   1284 }
   1285 
   1286 void FullscreenUI::DoToggleFullscreen()
   1287 {
   1288   Host::RunOnCPUThread([]() { Host::SetFullscreen(!Host::IsFullscreen()); });
   1289 }
   1290 
   1291 //////////////////////////////////////////////////////////////////////////
   1292 // Landing Window
   1293 //////////////////////////////////////////////////////////////////////////
   1294 
   1295 void FullscreenUI::SwitchToLanding()
   1296 {
   1297   s_current_main_window = MainWindowType::Landing;
   1298   QueueResetFocus(FocusResetType::ViewChanged);
   1299 }
   1300 
   1301 void FullscreenUI::DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size)
   1302 {
   1303   const ImGuiIO& io = ImGui::GetIO();
   1304   const ImVec2 heading_size =
   1305     ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   1306                                (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f));
   1307   *menu_pos = ImVec2(0.0f, heading_size.y);
   1308   *menu_size = ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT));
   1309 
   1310   if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "landing_heading", UIPrimaryColor))
   1311   {
   1312     ImFont* const heading_font = g_large_font;
   1313     ImDrawList* const dl = ImGui::GetWindowDrawList();
   1314     SmallString heading_str;
   1315 
   1316     ImGui::PushFont(heading_font);
   1317     ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   1318 
   1319     // draw branding
   1320     {
   1321       const ImVec2 logo_pos = LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING);
   1322       const ImVec2 logo_size = LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   1323       dl->AddImage(s_app_icon_texture.get(), logo_pos, logo_pos + logo_size);
   1324       dl->AddText(heading_font, heading_font->FontSize,
   1325                   ImVec2(logo_pos.x + logo_size.x + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), logo_pos.y),
   1326                   ImGui::GetColorU32(ImGuiCol_Text), "DuckStation");
   1327     }
   1328 
   1329     // draw time
   1330     ImVec2 time_pos;
   1331     {
   1332       heading_str.format(FSUI_FSTR("{:%H:%M}"), fmt::localtime(std::time(nullptr)));
   1333 
   1334       const ImVec2 time_size = heading_font->CalcTextSizeA(heading_font->FontSize, FLT_MAX, 0.0f, "00:00");
   1335       time_pos = ImVec2(heading_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING) - time_size.x,
   1336                         LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING));
   1337       ImGui::RenderTextClipped(time_pos, time_pos + time_size, heading_str.c_str(), heading_str.end_ptr(), &time_size);
   1338     }
   1339 
   1340     // draw achievements info
   1341     if (Achievements::IsActive())
   1342     {
   1343       const auto lock = Achievements::GetLock();
   1344       const char* username = Achievements::GetLoggedInUserName();
   1345       if (username)
   1346       {
   1347         const ImVec2 name_size = heading_font->CalcTextSizeA(heading_font->FontSize, FLT_MAX, 0.0f, username);
   1348         const ImVec2 name_pos =
   1349           ImVec2(time_pos.x - name_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), time_pos.y);
   1350         ImGui::RenderTextClipped(name_pos, name_pos + name_size, username, nullptr, &name_size);
   1351 
   1352         // TODO: should we cache this? heap allocations bad...
   1353         std::string badge_path = Achievements::GetLoggedInUserBadgePath();
   1354         if (!badge_path.empty()) [[likely]]
   1355         {
   1356           const ImVec2 badge_size =
   1357             LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   1358           const ImVec2 badge_pos =
   1359             ImVec2(name_pos.x - badge_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), time_pos.y);
   1360 
   1361           dl->AddImage(reinterpret_cast<ImTextureID>(GetCachedTextureAsync(badge_path)), badge_pos,
   1362                        badge_pos + badge_size);
   1363         }
   1364       }
   1365     }
   1366 
   1367     ImGui::PopStyleColor();
   1368     ImGui::PopFont();
   1369   }
   1370   EndFullscreenWindow();
   1371 }
   1372 
   1373 void FullscreenUI::DrawLandingWindow()
   1374 {
   1375   ImVec2 menu_pos, menu_size;
   1376   DrawLandingTemplate(&menu_pos, &menu_size);
   1377 
   1378   ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   1379 
   1380   if (BeginHorizontalMenu("landing_window", menu_pos, menu_size, 4))
   1381   {
   1382     ResetFocusHere();
   1383 
   1384     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/address-book-new.png"), FSUI_CSTR("Game List"),
   1385                            FSUI_CSTR("Launch a game from images scanned from your game directories.")))
   1386     {
   1387       SwitchToGameList();
   1388     }
   1389 
   1390     if (HorizontalMenuItem(
   1391           GetCachedTexture("fullscreenui/media-cdrom.png"), FSUI_CSTR("Start Game"),
   1392           FSUI_CSTR("Launch a game from a file, disc, or starts the console without any disc inserted.")))
   1393     {
   1394       s_current_main_window = MainWindowType::StartGame;
   1395       QueueResetFocus(FocusResetType::ViewChanged);
   1396     }
   1397 
   1398     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/applications-system.png"), FSUI_CSTR("Settings"),
   1399                            FSUI_CSTR("Changes settings for the application.")))
   1400     {
   1401       SwitchToSettings();
   1402     }
   1403 
   1404     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/exit.png"), FSUI_CSTR("Exit"),
   1405                            FSUI_CSTR("Return to desktop mode, or exit the application.")) ||
   1406         (!AreAnyDialogsOpen() && WantsToCloseMenu()))
   1407     {
   1408       s_current_main_window = MainWindowType::Exit;
   1409       QueueResetFocus(FocusResetType::ViewChanged);
   1410     }
   1411   }
   1412   EndHorizontalMenu();
   1413 
   1414   ImGui::PopStyleColor();
   1415 
   1416   if (!AreAnyDialogsOpen())
   1417   {
   1418     if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false))
   1419       OpenAboutWindow();
   1420     else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F3, false))
   1421       DoResume();
   1422     else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadInput, false) || ImGui::IsKeyPressed(ImGuiKey_F11, false))
   1423       DoToggleFullscreen();
   1424   }
   1425 
   1426   if (IsGamepadInputSource())
   1427   {
   1428     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_BURGER_MENU, FSUI_VSTR("About")),
   1429                                        std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Resume Last Session")),
   1430                                        std::make_pair(ICON_PF_BUTTON_X, FSUI_VSTR("Toggle Fullscreen")),
   1431                                        std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Navigate")),
   1432                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
   1433                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Exit"))});
   1434   }
   1435   else
   1436   {
   1437     SetFullscreenFooterText(std::array{
   1438       std::make_pair(ICON_PF_F1, FSUI_VSTR("About")), std::make_pair(ICON_PF_F3, FSUI_VSTR("Resume Last Session")),
   1439       std::make_pair(ICON_PF_F11, FSUI_VSTR("Toggle Fullscreen")),
   1440       std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Navigate")),
   1441       std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Exit"))});
   1442   }
   1443 }
   1444 
   1445 void FullscreenUI::DrawStartGameWindow()
   1446 {
   1447   ImVec2 menu_pos, menu_size;
   1448   DrawLandingTemplate(&menu_pos, &menu_size);
   1449 
   1450   ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   1451 
   1452   if (BeginHorizontalMenu("start_game_window", menu_pos, menu_size, 4))
   1453   {
   1454     ResetFocusHere();
   1455 
   1456     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/start-file.png"), FSUI_CSTR("Start File"),
   1457                            FSUI_CSTR("Launch a game by selecting a file/disc image.")))
   1458     {
   1459       DoStartFile();
   1460     }
   1461 
   1462     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/drive-cdrom.png"), FSUI_CSTR("Start Disc"),
   1463                            FSUI_CSTR("Start a game from a disc in your PC's DVD drive.")))
   1464     {
   1465       DoStartDisc();
   1466     }
   1467 
   1468     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/start-bios.png"), FSUI_CSTR("Start BIOS"),
   1469                            FSUI_CSTR("Start the console without any disc inserted.")))
   1470     {
   1471       DoStartBIOS();
   1472     }
   1473 
   1474     // https://www.iconpacks.net/free-icon/arrow-back-3783.html
   1475     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/back-icon.png"), FSUI_CSTR("Back"),
   1476                            FSUI_CSTR("Return to the previous menu.")) ||
   1477         (!AreAnyDialogsOpen() && WantsToCloseMenu()))
   1478     {
   1479       s_current_main_window = MainWindowType::Landing;
   1480       QueueResetFocus(FocusResetType::ViewChanged);
   1481     }
   1482   }
   1483   EndHorizontalMenu();
   1484 
   1485   ImGui::PopStyleColor();
   1486 
   1487   if (!AreAnyDialogsOpen())
   1488   {
   1489     if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false))
   1490       OpenSaveStateSelector(true);
   1491   }
   1492 
   1493   if (IsGamepadInputSource())
   1494   {
   1495     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Navigate")),
   1496                                        std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Load Global State")),
   1497                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
   1498                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))});
   1499   }
   1500   else
   1501   {
   1502     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Navigate")),
   1503                                        std::make_pair(ICON_PF_F1, FSUI_VSTR("Load Global State")),
   1504                                        std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")),
   1505                                        std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))});
   1506   }
   1507 }
   1508 
   1509 void FullscreenUI::DrawExitWindow()
   1510 {
   1511   ImVec2 menu_pos, menu_size;
   1512   DrawLandingTemplate(&menu_pos, &menu_size);
   1513 
   1514   ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   1515 
   1516   if (BeginHorizontalMenu("exit_window", menu_pos, menu_size, 3))
   1517   {
   1518     ResetFocusHere();
   1519 
   1520     // https://www.iconpacks.net/free-icon/arrow-back-3783.html
   1521     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/back-icon.png"), FSUI_CSTR("Back"),
   1522                            FSUI_CSTR("Return to the previous menu.")) ||
   1523         WantsToCloseMenu())
   1524     {
   1525       s_current_main_window = MainWindowType::Landing;
   1526       QueueResetFocus(FocusResetType::ViewChanged);
   1527     }
   1528 
   1529     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/exit.png"), FSUI_CSTR("Exit DuckStation"),
   1530                            FSUI_CSTR("Completely exits the application, returning you to your desktop.")))
   1531     {
   1532       DoRequestExit();
   1533     }
   1534 
   1535     if (HorizontalMenuItem(GetCachedTexture("fullscreenui/desktop-mode.png"), FSUI_CSTR("Desktop Mode"),
   1536                            FSUI_CSTR("Exits Big Picture mode, returning to the desktop interface.")))
   1537     {
   1538       DoDesktopMode();
   1539     }
   1540   }
   1541   EndHorizontalMenu();
   1542 
   1543   ImGui::PopStyleColor();
   1544 
   1545   SetStandardSelectionFooterText(true);
   1546 }
   1547 
   1548 bool FullscreenUI::IsEditingGameSettings(SettingsInterface* bsi)
   1549 {
   1550   return (bsi == s_game_settings_interface.get());
   1551 }
   1552 
   1553 SettingsInterface* FullscreenUI::GetEditingSettingsInterface()
   1554 {
   1555   return s_game_settings_interface ? s_game_settings_interface.get() : Host::Internal::GetBaseSettingsLayer();
   1556 }
   1557 
   1558 SettingsInterface* FullscreenUI::GetEditingSettingsInterface(bool game_settings)
   1559 {
   1560   return (game_settings && s_game_settings_interface) ? s_game_settings_interface.get() :
   1561                                                         Host::Internal::GetBaseSettingsLayer();
   1562 }
   1563 
   1564 void FullscreenUI::SetSettingsChanged(SettingsInterface* bsi)
   1565 {
   1566   if (bsi && bsi == s_game_settings_interface.get())
   1567     s_game_settings_changed.store(true, std::memory_order_release);
   1568   else
   1569     s_settings_changed.store(true, std::memory_order_release);
   1570 }
   1571 
   1572 bool FullscreenUI::GetEffectiveBoolSetting(SettingsInterface* bsi, const char* section, const char* key,
   1573                                            bool default_value)
   1574 {
   1575   if (IsEditingGameSettings(bsi))
   1576   {
   1577     std::optional<bool> value = bsi->GetOptionalBoolValue(section, key, std::nullopt);
   1578     if (value.has_value())
   1579       return value.value();
   1580   }
   1581 
   1582   return Host::Internal::GetBaseSettingsLayer()->GetBoolValue(section, key, default_value);
   1583 }
   1584 
   1585 s32 FullscreenUI::GetEffectiveIntSetting(SettingsInterface* bsi, const char* section, const char* key,
   1586                                          s32 default_value)
   1587 {
   1588   if (IsEditingGameSettings(bsi))
   1589   {
   1590     std::optional<s32> value = bsi->GetOptionalIntValue(section, key, std::nullopt);
   1591     if (value.has_value())
   1592       return value.value();
   1593   }
   1594 
   1595   return Host::Internal::GetBaseSettingsLayer()->GetIntValue(section, key, default_value);
   1596 }
   1597 
   1598 u32 FullscreenUI::GetEffectiveUIntSetting(SettingsInterface* bsi, const char* section, const char* key,
   1599                                           u32 default_value)
   1600 {
   1601   if (IsEditingGameSettings(bsi))
   1602   {
   1603     std::optional<u32> value = bsi->GetOptionalUIntValue(section, key, std::nullopt);
   1604     if (value.has_value())
   1605       return value.value();
   1606   }
   1607 
   1608   return Host::Internal::GetBaseSettingsLayer()->GetUIntValue(section, key, default_value);
   1609 }
   1610 
   1611 float FullscreenUI::GetEffectiveFloatSetting(SettingsInterface* bsi, const char* section, const char* key,
   1612                                              float default_value)
   1613 {
   1614   if (IsEditingGameSettings(bsi))
   1615   {
   1616     std::optional<float> value = bsi->GetOptionalFloatValue(section, key, std::nullopt);
   1617     if (value.has_value())
   1618       return value.value();
   1619   }
   1620 
   1621   return Host::Internal::GetBaseSettingsLayer()->GetFloatValue(section, key, default_value);
   1622 }
   1623 
   1624 TinyString FullscreenUI::GetEffectiveTinyStringSetting(SettingsInterface* bsi, const char* section, const char* key,
   1625                                                        const char* default_value)
   1626 {
   1627   TinyString ret;
   1628   std::optional<TinyString> value;
   1629 
   1630   if (IsEditingGameSettings(bsi))
   1631     value = bsi->GetOptionalTinyStringValue(section, key, std::nullopt);
   1632 
   1633   if (value.has_value())
   1634     ret = std::move(value.value());
   1635   else
   1636     ret = Host::Internal::GetBaseSettingsLayer()->GetTinyStringValue(section, key, default_value);
   1637 
   1638   return ret;
   1639 }
   1640 
   1641 void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
   1642                                           const char* name, const char* display_name, const char* icon_name,
   1643                                           bool show_type)
   1644 {
   1645   if (type == InputBindingInfo::Type::Pointer)
   1646     return;
   1647 
   1648   TinyString title;
   1649   title.format("{}/{}", section, name);
   1650 
   1651   SmallString value = bsi->GetSmallStringValue(section, name);
   1652   const bool oneline = value.count('&') <= 1;
   1653 
   1654   ImRect bb;
   1655   bool visible, hovered, clicked;
   1656   clicked = MenuButtonFrame(title, true,
   1657                             oneline ? ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY :
   1658                                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
   1659                             &visible, &hovered, &bb.Min, &bb.Max);
   1660   if (!visible)
   1661     return;
   1662 
   1663   if (oneline)
   1664     InputManager::PrettifyInputBinding(value);
   1665 
   1666   if (show_type)
   1667   {
   1668     if (icon_name)
   1669     {
   1670       title.format("{} {}", icon_name, display_name);
   1671     }
   1672     else
   1673     {
   1674       switch (type)
   1675       {
   1676         case InputBindingInfo::Type::Button:
   1677           title.format(ICON_FA_DOT_CIRCLE " {}", display_name);
   1678           break;
   1679         case InputBindingInfo::Type::Axis:
   1680         case InputBindingInfo::Type::HalfAxis:
   1681           title.format(ICON_FA_BULLSEYE " {}", display_name);
   1682           break;
   1683         case InputBindingInfo::Type::Motor:
   1684           title.format(ICON_FA_BELL " {}", display_name);
   1685           break;
   1686         case InputBindingInfo::Type::Macro:
   1687           title.format(ICON_FA_PIZZA_SLICE " {}", display_name);
   1688           break;
   1689         default:
   1690           title = display_name;
   1691           break;
   1692       }
   1693     }
   1694   }
   1695 
   1696   const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
   1697 
   1698   if (oneline)
   1699   {
   1700     ImGui::PushFont(g_large_font);
   1701 
   1702     const ImVec2 value_size(ImGui::CalcTextSize(value.empty() ? FSUI_CSTR("-") : value.c_str(), nullptr));
   1703     const float text_end = bb.Max.x - value_size.x;
   1704     const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint));
   1705 
   1706     ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, show_type ? title.c_str() : display_name, nullptr, nullptr,
   1707                              ImVec2(0.0f, 0.0f), &title_bb);
   1708     ImGui::RenderTextClipped(bb.Min, bb.Max, value.empty() ? FSUI_CSTR("-") : value.c_str(), nullptr, &value_size,
   1709                              ImVec2(1.0f, 0.5f), &bb);
   1710     ImGui::PopFont();
   1711   }
   1712   else
   1713   {
   1714     const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
   1715     const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
   1716 
   1717     ImGui::PushFont(g_large_font);
   1718     ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, show_type ? title.c_str() : display_name, nullptr, nullptr,
   1719                              ImVec2(0.0f, 0.0f), &title_bb);
   1720     ImGui::PopFont();
   1721 
   1722     ImGui::PushFont(g_medium_font);
   1723     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, value.empty() ? FSUI_CSTR("No Binding") : value.c_str(),
   1724                              nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb);
   1725     ImGui::PopFont();
   1726   }
   1727 
   1728   if (clicked)
   1729   {
   1730     BeginInputBinding(bsi, type, section, name, display_name);
   1731   }
   1732   else if (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))
   1733   {
   1734     bsi->DeleteValue(section, name);
   1735     SetSettingsChanged(bsi);
   1736   }
   1737 }
   1738 
   1739 void FullscreenUI::ClearInputBindingVariables()
   1740 {
   1741   s_input_binding_type = InputBindingInfo::Type::Unknown;
   1742   s_input_binding_section = {};
   1743   s_input_binding_key = {};
   1744   s_input_binding_display_name = {};
   1745   s_input_binding_new_bindings = {};
   1746   s_input_binding_value_ranges = {};
   1747 }
   1748 
   1749 void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section,
   1750                                      std::string_view key, std::string_view display_name)
   1751 {
   1752   if (s_input_binding_type != InputBindingInfo::Type::Unknown)
   1753   {
   1754     InputManager::RemoveHook();
   1755     ClearInputBindingVariables();
   1756   }
   1757 
   1758   s_input_binding_type = type;
   1759   s_input_binding_section = section;
   1760   s_input_binding_key = key;
   1761   s_input_binding_display_name = display_name;
   1762   s_input_binding_new_bindings = {};
   1763   s_input_binding_value_ranges = {};
   1764   s_input_binding_timer.Reset();
   1765 
   1766   const bool game_settings = IsEditingGameSettings(bsi);
   1767 
   1768   InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult {
   1769     if (s_input_binding_type == InputBindingInfo::Type::Unknown)
   1770       return InputInterceptHook::CallbackResult::StopProcessingEvent;
   1771 
   1772     // holding the settings lock here will protect the input binding list
   1773     auto lock = Host::GetSettingsLock();
   1774 
   1775     float initial_value = value;
   1776     float min_value = value;
   1777     auto it = std::find_if(s_input_binding_value_ranges.begin(), s_input_binding_value_ranges.end(),
   1778                            [key](const auto& it) { return it.first.bits == key.bits; });
   1779     if (it != s_input_binding_value_ranges.end())
   1780     {
   1781       initial_value = it->second.first;
   1782       min_value = it->second.second = std::min(it->second.second, value);
   1783     }
   1784     else
   1785     {
   1786       s_input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
   1787     }
   1788 
   1789     const float abs_value = std::abs(value);
   1790     const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
   1791 
   1792     for (InputBindingKey& other_key : s_input_binding_new_bindings)
   1793     {
   1794       // if this key is in our new binding list, it's a "release", and we're done
   1795       if (other_key.MaskDirection() == key.MaskDirection())
   1796       {
   1797         // for pedals, we wait for it to go back to near its starting point to commit the binding
   1798         if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
   1799         {
   1800           // did we go the full range?
   1801           if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
   1802             other_key.modifier = InputModifier::FullAxis;
   1803 
   1804           SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   1805           const std::string new_binding(InputManager::ConvertInputBindingKeysToString(
   1806             s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size()));
   1807           bsi->SetStringValue(s_input_binding_section.c_str(), s_input_binding_key.c_str(), new_binding.c_str());
   1808           SetSettingsChanged(bsi);
   1809           ClearInputBindingVariables();
   1810           return InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent;
   1811         }
   1812 
   1813         // otherwise, keep waiting
   1814         return InputInterceptHook::CallbackResult::StopProcessingEvent;
   1815       }
   1816     }
   1817 
   1818     // new binding, add it to the list, but wait for a decent distance first, and then wait for release
   1819     if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
   1820     {
   1821       InputBindingKey key_to_add = key;
   1822       key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
   1823       key_to_add.invert = reverse_threshold;
   1824       s_input_binding_new_bindings.push_back(key_to_add);
   1825     }
   1826 
   1827     return InputInterceptHook::CallbackResult::StopProcessingEvent;
   1828   });
   1829 }
   1830 
   1831 void FullscreenUI::DrawInputBindingWindow()
   1832 {
   1833   DebugAssert(s_input_binding_type != InputBindingInfo::Type::Unknown);
   1834 
   1835   const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds();
   1836   if (time_remaining <= 0.0)
   1837   {
   1838     InputManager::RemoveHook();
   1839     ClearInputBindingVariables();
   1840     return;
   1841   }
   1842 
   1843   const char* title = FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding");
   1844   ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f));
   1845   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   1846   ImGui::OpenPopup(title);
   1847 
   1848   ImGui::PushFont(g_large_font);
   1849   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   1850   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   1851                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   1852   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   1853   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   1854 
   1855   if (ImGui::BeginPopupModal(title, nullptr,
   1856                              ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs))
   1857   {
   1858     ImGui::TextWrapped("%s", SmallString::from_format(FSUI_FSTR("Setting {} binding {}."), s_input_binding_section,
   1859                                                       s_input_binding_display_name)
   1860                                .c_str());
   1861     ImGui::TextUnformatted(FSUI_CSTR("Push a controller button or axis now."));
   1862     ImGui::NewLine();
   1863     ImGui::TextUnformatted(SmallString::from_format(FSUI_FSTR("Timing out in {:.0f} seconds..."), time_remaining));
   1864     ImGui::EndPopup();
   1865   }
   1866 
   1867   ImGui::PopStyleVar(4);
   1868   ImGui::PopFont();
   1869 }
   1870 
   1871 bool FullscreenUI::DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary,
   1872                                      const char* section, const char* key, bool default_value, bool enabled,
   1873                                      bool allow_tristate, float height, ImFont* font, ImFont* summary_font)
   1874 {
   1875   if (!allow_tristate || !IsEditingGameSettings(bsi))
   1876   {
   1877     bool value = bsi->GetBoolValue(section, key, default_value);
   1878     if (!ToggleButton(title, summary, &value, enabled, height, font, summary_font))
   1879       return false;
   1880 
   1881     bsi->SetBoolValue(section, key, value);
   1882   }
   1883   else
   1884   {
   1885     std::optional<bool> value(false);
   1886     if (!bsi->GetBoolValue(section, key, &value.value()))
   1887       value.reset();
   1888     if (!ThreeWayToggleButton(title, summary, &value, enabled, height, font, summary_font))
   1889       return false;
   1890 
   1891     if (value.has_value())
   1892       bsi->SetBoolValue(section, key, value.value());
   1893     else
   1894       bsi->DeleteValue(section, key);
   1895   }
   1896 
   1897   SetSettingsChanged(bsi);
   1898   return true;
   1899 }
   1900 
   1901 void FullscreenUI::DrawIntListSetting(SettingsInterface* bsi, const char* title, const char* summary,
   1902                                       const char* section, const char* key, int default_value,
   1903                                       const char* const* options, size_t option_count, bool translate_options,
   1904                                       int option_offset, bool enabled, float height, ImFont* font, ImFont* summary_font,
   1905                                       const char* tr_context)
   1906 {
   1907   const bool game_settings = IsEditingGameSettings(bsi);
   1908 
   1909   if (options && option_count == 0)
   1910   {
   1911     while (options[option_count] != nullptr)
   1912       option_count++;
   1913   }
   1914 
   1915   const std::optional<int> value =
   1916     bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional<int>(default_value));
   1917   const int index = value.has_value() ? (value.value() - option_offset) : std::numeric_limits<int>::min();
   1918   const char* value_text =
   1919     (value.has_value()) ?
   1920       ((index < 0 || static_cast<size_t>(index) >= option_count) ?
   1921          FSUI_CSTR("Unknown") :
   1922          (translate_options ? Host::TranslateToCString(tr_context, options[index]) : options[index])) :
   1923       FSUI_CSTR("Use Global Setting");
   1924 
   1925   if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font))
   1926   {
   1927     ImGuiFullscreen::ChoiceDialogOptions cd_options;
   1928     cd_options.reserve(option_count + 1);
   1929     if (game_settings)
   1930       cd_options.emplace_back(FSUI_STR("Use Global Setting"), !value.has_value());
   1931     for (size_t i = 0; i < option_count; i++)
   1932     {
   1933       cd_options.emplace_back(translate_options ? Host::TranslateToString(tr_context, options[i]) :
   1934                                                   std::string(options[i]),
   1935                               (i == static_cast<size_t>(index)));
   1936     }
   1937     OpenChoiceDialog(title, false, std::move(cd_options),
   1938                      [game_settings, section = TinyString(section), key = TinyString(key),
   1939                       option_offset](s32 index, const std::string& title, bool checked) {
   1940                        if (index >= 0)
   1941                        {
   1942                          auto lock = Host::GetSettingsLock();
   1943                          SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   1944                          if (game_settings)
   1945                          {
   1946                            if (index == 0)
   1947                              bsi->DeleteValue(section, key);
   1948                            else
   1949                              bsi->SetIntValue(section, key, index - 1 + option_offset);
   1950                          }
   1951                          else
   1952                          {
   1953                            bsi->SetIntValue(section, key, index + option_offset);
   1954                          }
   1955 
   1956                          SetSettingsChanged(bsi);
   1957                        }
   1958 
   1959                        CloseChoiceDialog();
   1960                      });
   1961   }
   1962 }
   1963 
   1964 void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const char* summary,
   1965                                        const char* section, const char* key, int default_value, int min_value,
   1966                                        int max_value, const char* format, bool enabled, float height, ImFont* font,
   1967                                        ImFont* summary_font)
   1968 {
   1969   const bool game_settings = IsEditingGameSettings(bsi);
   1970   const std::optional<int> value =
   1971     bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional<int>(default_value));
   1972   const SmallString value_text =
   1973     value.has_value() ? SmallString::from_sprintf(format, value.value()) : SmallString(FSUI_VSTR("Use Global Setting"));
   1974 
   1975   if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font))
   1976     ImGui::OpenPopup(title);
   1977 
   1978   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   1979   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   1980 
   1981   ImGui::PushFont(g_large_font);
   1982   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   1983   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   1984                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   1985   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   1986   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   1987 
   1988   bool is_open = true;
   1989   if (ImGui::BeginPopupModal(title, &is_open,
   1990                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   1991   {
   1992     BeginMenuButtons();
   1993 
   1994     const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   1995     ImGui::SetNextItemWidth(end);
   1996     s32 dlg_value = static_cast<s32>(value.value_or(default_value));
   1997     if (ImGui::SliderInt("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput))
   1998     {
   1999       if (IsEditingGameSettings(bsi) && dlg_value == default_value)
   2000         bsi->DeleteValue(section, key);
   2001       else
   2002         bsi->SetIntValue(section, key, dlg_value);
   2003 
   2004       SetSettingsChanged(bsi);
   2005     }
   2006 
   2007     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2008     if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   2009                                  ImVec2(0.5f, 0.0f)))
   2010     {
   2011       ImGui::CloseCurrentPopup();
   2012     }
   2013     EndMenuButtons();
   2014 
   2015     ImGui::EndPopup();
   2016   }
   2017 
   2018   ImGui::PopStyleVar(4);
   2019   ImGui::PopFont();
   2020 }
   2021 
   2022 void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2023                                          const char* section, const char* key, float default_value, float min_value,
   2024                                          float max_value, const char* format, float multiplier, bool enabled,
   2025                                          float height, ImFont* font, ImFont* summary_font)
   2026 {
   2027   const bool game_settings = IsEditingGameSettings(bsi);
   2028   const std::optional<float> value =
   2029     bsi->GetOptionalFloatValue(section, key, game_settings ? std::nullopt : std::optional<float>(default_value));
   2030   const SmallString value_text = value.has_value() ? SmallString::from_sprintf(format, value.value() * multiplier) :
   2031                                                      SmallString(FSUI_VSTR("Use Global Setting"));
   2032 
   2033   if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font))
   2034     ImGui::OpenPopup(title);
   2035 
   2036   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   2037   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2038 
   2039   ImGui::PushFont(g_large_font);
   2040   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2041   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   2042                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   2043   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2044   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   2045 
   2046   bool is_open = true;
   2047   if (ImGui::BeginPopupModal(title, &is_open,
   2048                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2049   {
   2050     BeginMenuButtons();
   2051 
   2052     const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   2053     ImGui::SetNextItemWidth(end);
   2054     float dlg_value = value.value_or(default_value) * multiplier;
   2055     if (ImGui::SliderFloat("##value", &dlg_value, min_value * multiplier, max_value * multiplier, format,
   2056                            ImGuiSliderFlags_NoInput))
   2057     {
   2058       dlg_value /= multiplier;
   2059 
   2060       if (IsEditingGameSettings(bsi) && dlg_value == default_value)
   2061         bsi->DeleteValue(section, key);
   2062       else
   2063         bsi->SetFloatValue(section, key, dlg_value);
   2064 
   2065       SetSettingsChanged(bsi);
   2066     }
   2067 
   2068     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2069     if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   2070                                  ImVec2(0.5f, 0.0f)))
   2071     {
   2072       ImGui::CloseCurrentPopup();
   2073     }
   2074     EndMenuButtons();
   2075 
   2076     ImGui::EndPopup();
   2077   }
   2078 
   2079   ImGui::PopStyleVar(4);
   2080   ImGui::PopFont();
   2081 }
   2082 
   2083 void FullscreenUI::DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2084                                            const char* section, const char* key, float default_value, float min_value,
   2085                                            float max_value, float step_value, float multiplier, const char* format,
   2086                                            bool enabled, float height, ImFont* font, ImFont* summary_font)
   2087 {
   2088   const bool game_settings = IsEditingGameSettings(bsi);
   2089   const std::optional<float> value =
   2090     bsi->GetOptionalFloatValue(section, key, game_settings ? std::nullopt : std::optional<float>(default_value));
   2091   const SmallString value_text = value.has_value() ? SmallString::from_sprintf(format, value.value() * multiplier) :
   2092                                                      SmallString(FSUI_VSTR("Use Global Setting"));
   2093 
   2094   static bool manual_input = false;
   2095 
   2096   if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font))
   2097   {
   2098     ImGui::OpenPopup(title);
   2099     manual_input = false;
   2100   }
   2101 
   2102   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   2103   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2104 
   2105   ImGui::PushFont(g_large_font);
   2106   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2107   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   2108                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   2109   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2110   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   2111 
   2112   bool is_open = true;
   2113   if (ImGui::BeginPopupModal(title, &is_open,
   2114                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2115   {
   2116     BeginMenuButtons();
   2117 
   2118     float dlg_value = value.value_or(default_value) * multiplier;
   2119     bool dlg_value_changed = false;
   2120 
   2121     char str_value[32];
   2122     std::snprintf(str_value, std::size(str_value), format, dlg_value);
   2123 
   2124     if (manual_input)
   2125     {
   2126       const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   2127       ImGui::SetNextItemWidth(end);
   2128 
   2129       // round trip to drop any suffixes (e.g. percent)
   2130       if (auto tmp_value = StringUtil::FromChars<float>(str_value); tmp_value.has_value())
   2131       {
   2132         std::snprintf(str_value, std::size(str_value),
   2133                       ((tmp_value.value() - std::floor(tmp_value.value())) < 0.01f) ? "%.0f" : "%f", tmp_value.value());
   2134       }
   2135 
   2136       if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal))
   2137       {
   2138         dlg_value = StringUtil::FromChars<float>(str_value).value_or(dlg_value);
   2139         dlg_value_changed = true;
   2140       }
   2141 
   2142       ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2143     }
   2144     else
   2145     {
   2146       const ImVec2& padding(ImGui::GetStyle().FramePadding);
   2147       ImVec2 button_pos(ImGui::GetCursorPos());
   2148 
   2149       // Align value text in middle.
   2150       ImGui::SetCursorPosY(
   2151         button_pos.y +
   2152         ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - g_large_font->FontSize) * 0.5f);
   2153       ImGui::TextUnformatted(str_value);
   2154 
   2155       float step = 0;
   2156       if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font,
   2157                          &button_pos, true))
   2158       {
   2159         step = step_value;
   2160       }
   2161       if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2162                          g_large_font, &button_pos, true))
   2163       {
   2164         step = -step_value;
   2165       }
   2166       if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2167                          g_large_font, &button_pos))
   2168       {
   2169         manual_input = true;
   2170       }
   2171       if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2172                          g_large_font, &button_pos))
   2173       {
   2174         dlg_value = default_value * multiplier;
   2175         dlg_value_changed = true;
   2176       }
   2177 
   2178       if (step != 0)
   2179       {
   2180         dlg_value += step * multiplier;
   2181         dlg_value_changed = true;
   2182       }
   2183 
   2184       ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) +
   2185                            LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f));
   2186     }
   2187 
   2188     if (dlg_value_changed)
   2189     {
   2190       dlg_value = std::clamp(dlg_value / multiplier, min_value, max_value);
   2191       if (IsEditingGameSettings(bsi) && dlg_value == default_value)
   2192         bsi->DeleteValue(section, key);
   2193       else
   2194         bsi->SetFloatValue(section, key, dlg_value);
   2195 
   2196       SetSettingsChanged(bsi);
   2197     }
   2198 
   2199     if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   2200                                  ImVec2(0.5f, 0.0f)))
   2201     {
   2202       ImGui::CloseCurrentPopup();
   2203     }
   2204     EndMenuButtons();
   2205 
   2206     ImGui::EndPopup();
   2207   }
   2208 
   2209   ImGui::PopStyleVar(4);
   2210   ImGui::PopFont();
   2211 }
   2212 
   2213 #if 0
   2214 void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2215                                       const char* section, const char* left_key, int default_left, const char* top_key,
   2216                                       int default_top, const char* right_key, int default_right, const char* bottom_key,
   2217                                       int default_bottom, int min_value, int max_value, const char* format,
   2218                                       bool enabled, float height, ImFont* font, ImFont* summary_font)
   2219 {
   2220   const bool game_settings = IsEditingGameSettings(bsi);
   2221   const std::optional<int> left_value =
   2222     bsi->GetOptionalIntValue(section, left_key, game_settings ? std::nullopt : std::optional<int>(default_left));
   2223   const std::optional<int> top_value =
   2224     bsi->GetOptionalIntValue(section, top_key, game_settings ? std::nullopt : std::optional<int>(default_top));
   2225   const std::optional<int> right_value =
   2226     bsi->GetOptionalIntValue(section, right_key, game_settings ? std::nullopt : std::optional<int>(default_right));
   2227   const std::optional<int> bottom_value =
   2228     bsi->GetOptionalIntValue(section, bottom_key, game_settings ? std::nullopt : std::optional<int>(default_bottom));
   2229   const SmallString value_text = SmallString::from_format(
   2230     "{}/{}/{}/{}",
   2231     left_value.has_value() ? TinyString::from_sprintf(format, left_value.value()) : TinyString(FSUI_VSTR("Default")),
   2232     top_value.has_value() ? TinyString::from_sprintf(format, top_value.value()) : TinyString(FSUI_VSTR("Default")),
   2233     right_value.has_value() ? TinyString::from_sprintf(format, right_value.value()) : TinyString(FSUI_VSTR("Default")),
   2234     bottom_value.has_value() ? TinyString::from_sprintf(format, bottom_value.value()) : TinyString(FSUI_VSTR("Default")));
   2235 
   2236   if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font))
   2237     ImGui::OpenPopup(title);
   2238 
   2239   ImGui::SetNextWindowSize(LayoutScale(500.0f, 370.0f));
   2240   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2241 
   2242   ImGui::PushFont(g_large_font);
   2243   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2244   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   2245                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   2246   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2247   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   2248 
   2249   bool is_open = true;
   2250   if (ImGui::BeginPopupModal(title, &is_open,
   2251                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2252   {
   2253     s32 dlg_left_value = static_cast<s32>(left_value.value_or(default_left));
   2254     s32 dlg_top_value = static_cast<s32>(top_value.value_or(default_top));
   2255     s32 dlg_right_value = static_cast<s32>(right_value.value_or(default_right));
   2256     s32 dlg_bottom_value = static_cast<s32>(bottom_value.value_or(default_bottom));
   2257 
   2258     BeginMenuButtons();
   2259 
   2260     const float midpoint = LayoutScale(150.0f);
   2261     const float end = (ImGui::GetCurrentWindow()->WorkRect.GetWidth() - midpoint) + ImGui::GetStyle().WindowPadding.x;
   2262     ImGui::TextUnformatted("Left: ");
   2263     ImGui::SameLine(midpoint);
   2264     ImGui::SetNextItemWidth(end);
   2265     const bool left_modified =
   2266       ImGui::SliderInt("##left", &dlg_left_value, min_value, max_value, format, ImGuiSliderFlags_NoInput);
   2267     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2268     ImGui::TextUnformatted("Top: ");
   2269     ImGui::SameLine(midpoint);
   2270     ImGui::SetNextItemWidth(end);
   2271     const bool top_modified =
   2272       ImGui::SliderInt("##top", &dlg_top_value, min_value, max_value, format, ImGuiSliderFlags_NoInput);
   2273     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2274     ImGui::TextUnformatted("Right: ");
   2275     ImGui::SameLine(midpoint);
   2276     ImGui::SetNextItemWidth(end);
   2277     const bool right_modified =
   2278       ImGui::SliderInt("##right", &dlg_right_value, min_value, max_value, format, ImGuiSliderFlags_NoInput);
   2279     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2280     ImGui::TextUnformatted("Bottom: ");
   2281     ImGui::SameLine(midpoint);
   2282     ImGui::SetNextItemWidth(end);
   2283     const bool bottom_modified =
   2284       ImGui::SliderInt("##bottom", &dlg_bottom_value, min_value, max_value, format, ImGuiSliderFlags_NoInput);
   2285     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2286     if (left_modified)
   2287     {
   2288       if (IsEditingGameSettings(bsi) && dlg_left_value == default_left)
   2289         bsi->DeleteValue(section, left_key);
   2290       else
   2291         bsi->SetIntValue(section, left_key, dlg_left_value);
   2292     }
   2293     if (top_modified)
   2294     {
   2295       if (IsEditingGameSettings(bsi) && dlg_top_value == default_top)
   2296         bsi->DeleteValue(section, top_key);
   2297       else
   2298         bsi->SetIntValue(section, top_key, dlg_top_value);
   2299     }
   2300     if (right_modified)
   2301     {
   2302       if (IsEditingGameSettings(bsi) && dlg_right_value == default_right)
   2303         bsi->DeleteValue(section, right_key);
   2304       else
   2305         bsi->SetIntValue(section, right_key, dlg_right_value);
   2306     }
   2307     if (bottom_modified)
   2308     {
   2309       if (IsEditingGameSettings(bsi) && dlg_bottom_value == default_bottom)
   2310         bsi->DeleteValue(section, bottom_key);
   2311       else
   2312         bsi->SetIntValue(section, bottom_key, dlg_bottom_value);
   2313     }
   2314 
   2315     if (left_modified || top_modified || right_modified || bottom_modified)
   2316       SetSettingsChanged(bsi);
   2317 
   2318     if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f)))
   2319     {
   2320       ImGui::CloseCurrentPopup();
   2321     }
   2322     EndMenuButtons();
   2323 
   2324     ImGui::EndPopup();
   2325   }
   2326 
   2327   ImGui::PopStyleVar(4);
   2328   ImGui::PopFont();
   2329 }
   2330 #endif
   2331 
   2332 void FullscreenUI::DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2333                                          const char* section, const char* key, int default_value, int min_value,
   2334                                          int max_value, int step_value, const char* format, bool enabled, float height,
   2335                                          ImFont* font, ImFont* summary_font)
   2336 {
   2337   const bool game_settings = IsEditingGameSettings(bsi);
   2338   const std::optional<int> value =
   2339     bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional<int>(default_value));
   2340   TinyString value_text;
   2341   if (value.has_value())
   2342     value_text.sprintf(format, value.value());
   2343   else
   2344     value_text = FSUI_VSTR("Use Global Setting");
   2345 
   2346   static bool manual_input = false;
   2347 
   2348   if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font))
   2349   {
   2350     ImGui::OpenPopup(title);
   2351     manual_input = false;
   2352   }
   2353 
   2354   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   2355   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2356 
   2357   ImGui::PushFont(g_large_font);
   2358   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2359   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   2360                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   2361   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2362   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   2363 
   2364   bool is_open = true;
   2365   if (ImGui::BeginPopupModal(title, &is_open,
   2366                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2367   {
   2368     BeginMenuButtons();
   2369 
   2370     s32 dlg_value = static_cast<s32>(value.value_or(default_value));
   2371     bool dlg_value_changed = false;
   2372 
   2373     char str_value[32];
   2374     std::snprintf(str_value, std::size(str_value), format, dlg_value);
   2375 
   2376     if (manual_input)
   2377     {
   2378       const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   2379       ImGui::SetNextItemWidth(end);
   2380 
   2381       std::snprintf(str_value, std::size(str_value), "%d", dlg_value);
   2382       if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal))
   2383       {
   2384         dlg_value = StringUtil::FromChars<s32>(str_value).value_or(dlg_value);
   2385         dlg_value_changed = true;
   2386       }
   2387 
   2388       ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2389     }
   2390     else
   2391     {
   2392       const ImVec2& padding(ImGui::GetStyle().FramePadding);
   2393       ImVec2 button_pos(ImGui::GetCursorPos());
   2394 
   2395       // Align value text in middle.
   2396       ImGui::SetCursorPosY(
   2397         button_pos.y +
   2398         ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - g_large_font->FontSize) * 0.5f);
   2399       ImGui::TextUnformatted(str_value);
   2400 
   2401       s32 step = 0;
   2402       if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font,
   2403                          &button_pos, true))
   2404       {
   2405         step = step_value;
   2406       }
   2407       if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2408                          g_large_font, &button_pos, true))
   2409       {
   2410         step = -step_value;
   2411       }
   2412       if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2413                          g_large_font, &button_pos))
   2414       {
   2415         manual_input = true;
   2416       }
   2417       if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
   2418                          g_large_font, &button_pos))
   2419       {
   2420         dlg_value = default_value;
   2421         dlg_value_changed = true;
   2422       }
   2423 
   2424       if (step != 0)
   2425       {
   2426         dlg_value += step;
   2427         dlg_value_changed = true;
   2428       }
   2429 
   2430       ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) +
   2431                            LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f));
   2432     }
   2433 
   2434     if (dlg_value_changed)
   2435     {
   2436       dlg_value = std::clamp(dlg_value, min_value, max_value);
   2437       if (IsEditingGameSettings(bsi) && dlg_value == default_value)
   2438         bsi->DeleteValue(section, key);
   2439       else
   2440         bsi->SetIntValue(section, key, dlg_value);
   2441 
   2442       SetSettingsChanged(bsi);
   2443     }
   2444 
   2445     if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   2446                                  ImVec2(0.5f, 0.0f)))
   2447     {
   2448       ImGui::CloseCurrentPopup();
   2449     }
   2450     EndMenuButtons();
   2451 
   2452     ImGui::EndPopup();
   2453   }
   2454 
   2455   ImGui::PopStyleVar(4);
   2456   ImGui::PopFont();
   2457 }
   2458 
   2459 #if 0
   2460 void FullscreenUI::DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2461                                          const char* section, const char* key, const char* default_value,
   2462                                          const char* const* options, const char* const* option_values,
   2463                                          size_t option_count, bool enabled, float height, ImFont* font,
   2464                                          ImFont* summary_font)
   2465 {
   2466   const bool game_settings = IsEditingGameSettings(bsi);
   2467   const std::optional<SmallString> value(bsi->GetOptionalSmallStringValue(
   2468     section, key, game_settings ? std::nullopt : std::optional<const char*>(default_value)));
   2469 
   2470   if (option_count == 0)
   2471   {
   2472     // select from null entry
   2473     while (options && options[option_count] != nullptr)
   2474       option_count++;
   2475   }
   2476 
   2477   size_t index = option_count;
   2478   if (value.has_value())
   2479   {
   2480     for (size_t i = 0; i < option_count; i++)
   2481     {
   2482       if (value == option_values[i])
   2483       {
   2484         index = i;
   2485         break;
   2486       }
   2487     }
   2488   }
   2489 
   2490   if (MenuButtonWithValue(title, summary,
   2491                           value.has_value() ? ((index < option_count) ? options[index] : "Unknown") :
   2492                                               "Use Global Setting",
   2493                           enabled, height, font, summary_font))
   2494   {
   2495     ImGuiFullscreen::ChoiceDialogOptions cd_options;
   2496     cd_options.reserve(option_count + 1);
   2497     if (game_settings)
   2498       cd_options.emplace_back("Use Global Setting", !value.has_value());
   2499     for (size_t i = 0; i < option_count; i++)
   2500       cd_options.emplace_back(options[i], (value.has_value() && i == static_cast<size_t>(index)));
   2501     OpenChoiceDialog(title, false, std::move(cd_options),
   2502                      [game_settings, section, key, option_values](s32 index, const std::string& title, bool checked) {
   2503                        if (index >= 0)
   2504                        {
   2505                          auto lock = Host::GetSettingsLock();
   2506                          SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   2507                          if (game_settings)
   2508                          {
   2509                            if (index == 0)
   2510                              bsi->DeleteValue(section, key);
   2511                            else
   2512                              bsi->SetStringValue(section, key, option_values[index - 1]);
   2513                          }
   2514                          else
   2515                          {
   2516                            bsi->SetStringValue(section, key, option_values[index]);
   2517                          }
   2518 
   2519                          SetSettingsChanged(bsi);
   2520                        }
   2521 
   2522                        CloseChoiceDialog();
   2523                      });
   2524   }
   2525 }
   2526 #endif
   2527 
   2528 template<typename DataType, typename SizeType>
   2529 void FullscreenUI::DrawEnumSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
   2530                                    const char* key, DataType default_value,
   2531                                    std::optional<DataType> (*from_string_function)(const char* str),
   2532                                    const char* (*to_string_function)(DataType value),
   2533                                    const char* (*to_display_string_function)(DataType value), SizeType option_count,
   2534                                    bool enabled /*= true*/,
   2535                                    float height /*= ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT*/,
   2536                                    ImFont* font /*= g_large_font*/, ImFont* summary_font /*= g_medium_font*/)
   2537 {
   2538   const bool game_settings = IsEditingGameSettings(bsi);
   2539   const std::optional<SmallString> value(bsi->GetOptionalSmallStringValue(
   2540     section, key, game_settings ? std::nullopt : std::optional<const char*>(to_string_function(default_value))));
   2541 
   2542   const std::optional<DataType> typed_value(value.has_value() ? from_string_function(value->c_str()) : std::nullopt);
   2543 
   2544   if (MenuButtonWithValue(title, summary,
   2545                           typed_value.has_value() ? to_display_string_function(typed_value.value()) :
   2546                                                     FSUI_CSTR("Use Global Setting"),
   2547                           enabled, height, font, summary_font))
   2548   {
   2549     ImGuiFullscreen::ChoiceDialogOptions cd_options;
   2550     cd_options.reserve(static_cast<u32>(option_count) + 1);
   2551     if (game_settings)
   2552       cd_options.emplace_back(FSUI_CSTR("Use Global Setting"), !value.has_value());
   2553     for (u32 i = 0; i < static_cast<u32>(option_count); i++)
   2554       cd_options.emplace_back(to_display_string_function(static_cast<DataType>(i)),
   2555                               (typed_value.has_value() && i == static_cast<u32>(typed_value.value())));
   2556     OpenChoiceDialog(title, false, std::move(cd_options),
   2557                      [section = TinyString(section), key = TinyString(key), to_string_function,
   2558                       game_settings](s32 index, const std::string& title, bool checked) {
   2559                        if (index >= 0)
   2560                        {
   2561                          auto lock = Host::GetSettingsLock();
   2562                          SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   2563                          if (game_settings)
   2564                          {
   2565                            if (index == 0)
   2566                              bsi->DeleteValue(section, key);
   2567                            else
   2568                              bsi->SetStringValue(section, key, to_string_function(static_cast<DataType>(index - 1)));
   2569                          }
   2570                          else
   2571                          {
   2572                            bsi->SetStringValue(section, key, to_string_function(static_cast<DataType>(index)));
   2573                          }
   2574 
   2575                          SetSettingsChanged(bsi);
   2576                        }
   2577 
   2578                        CloseChoiceDialog();
   2579                      });
   2580   }
   2581 }
   2582 void FullscreenUI::DrawFloatListSetting(SettingsInterface* bsi, const char* title, const char* summary,
   2583                                         const char* section, const char* key, float default_value,
   2584                                         const char* const* options, const float* option_values, size_t option_count,
   2585                                         bool translate_options, bool enabled, float height, ImFont* font,
   2586                                         ImFont* summary_font)
   2587 {
   2588   const bool game_settings = IsEditingGameSettings(bsi);
   2589   const std::optional<float> value(
   2590     bsi->GetOptionalFloatValue(section, key, game_settings ? std::nullopt : std::optional<float>(default_value)));
   2591 
   2592   if (option_count == 0)
   2593   {
   2594     // select from null entry
   2595     while (options && options[option_count] != nullptr)
   2596       option_count++;
   2597   }
   2598 
   2599   size_t index = option_count;
   2600   if (value.has_value())
   2601   {
   2602     for (size_t i = 0; i < option_count; i++)
   2603     {
   2604       if (value == option_values[i])
   2605       {
   2606         index = i;
   2607         break;
   2608       }
   2609     }
   2610   }
   2611 
   2612   if (MenuButtonWithValue(
   2613         title, summary,
   2614         value.has_value() ?
   2615           ((index < option_count) ?
   2616              (translate_options ? Host::TranslateToCString(TR_CONTEXT, options[index]) : options[index]) :
   2617              FSUI_CSTR("Unknown")) :
   2618           FSUI_CSTR("Use Global Setting"),
   2619         enabled, height, font, summary_font))
   2620   {
   2621     ImGuiFullscreen::ChoiceDialogOptions cd_options;
   2622     cd_options.reserve(option_count + 1);
   2623     if (game_settings)
   2624       cd_options.emplace_back(FSUI_CSTR("Use Global Setting"), !value.has_value());
   2625     for (size_t i = 0; i < option_count; i++)
   2626     {
   2627       cd_options.emplace_back(translate_options ? Host::TranslateToString(TR_CONTEXT, options[i]) :
   2628                                                   std::string(options[i]),
   2629                               (value.has_value() && i == static_cast<size_t>(index)));
   2630     }
   2631     OpenChoiceDialog(title, false, std::move(cd_options),
   2632                      [game_settings, section = TinyString(section), key = TinyString(key),
   2633                       option_values](s32 index, const std::string& title, bool checked) {
   2634                        if (index >= 0)
   2635                        {
   2636                          auto lock = Host::GetSettingsLock();
   2637                          SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   2638                          if (game_settings)
   2639                          {
   2640                            if (index == 0)
   2641                              bsi->DeleteValue(section, key);
   2642                            else
   2643                              bsi->SetFloatValue(section, key, option_values[index - 1]);
   2644                          }
   2645                          else
   2646                          {
   2647                            bsi->SetFloatValue(section, key, option_values[index]);
   2648                          }
   2649 
   2650                          SetSettingsChanged(bsi);
   2651                        }
   2652 
   2653                        CloseChoiceDialog();
   2654                      });
   2655   }
   2656 }
   2657 
   2658 void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key,
   2659                                      const std::string& runtime_var,
   2660                                      float height /* = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT */,
   2661                                      ImFont* font /* = g_large_font */, ImFont* summary_font /* = g_medium_font */)
   2662 {
   2663   if (MenuButton(title, runtime_var.c_str()))
   2664   {
   2665     OpenFileSelector(title, true,
   2666                      [game_settings = IsEditingGameSettings(bsi), section = TinyString(section),
   2667                       key = TinyString(key)](const std::string& dir) {
   2668                        if (dir.empty())
   2669                          return;
   2670 
   2671                        auto lock = Host::GetSettingsLock();
   2672                        SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   2673                        std::string relative_path(Path::MakeRelative(dir, EmuFolders::DataRoot));
   2674                        bsi->SetStringValue(section.c_str(), key.c_str(), relative_path.c_str());
   2675                        SetSettingsChanged(bsi);
   2676 
   2677                        Host::RunOnCPUThread(&EmuFolders::Update);
   2678                        s_cover_image_map.clear();
   2679 
   2680                        CloseFileSelector();
   2681                      });
   2682   }
   2683 }
   2684 
   2685 void FullscreenUI::StartAutomaticBinding(u32 port)
   2686 {
   2687   std::vector<std::pair<std::string, std::string>> devices(InputManager::EnumerateDevices());
   2688   if (devices.empty())
   2689   {
   2690     ShowToast({}, FSUI_STR("Automatic mapping failed, no devices are available."));
   2691     return;
   2692   }
   2693 
   2694   std::vector<std::string> names;
   2695   ImGuiFullscreen::ChoiceDialogOptions options;
   2696   options.reserve(devices.size());
   2697   names.reserve(devices.size());
   2698   for (auto& [name, display_name] : devices)
   2699   {
   2700     names.push_back(std::move(name));
   2701     options.emplace_back(std::move(display_name), false);
   2702   }
   2703   OpenChoiceDialog(FSUI_CSTR("Select Device"), false, std::move(options),
   2704                    [port, names = std::move(names)](s32 index, const std::string& title, bool checked) {
   2705                      if (index < 0)
   2706                        return;
   2707 
   2708                      const std::string& name = names[index];
   2709                      auto lock = Host::GetSettingsLock();
   2710                      SettingsInterface* bsi = GetEditingSettingsInterface();
   2711                      const bool result =
   2712                        InputManager::MapController(*bsi, port, InputManager::GetGenericBindingMapping(name));
   2713                      SetSettingsChanged(bsi);
   2714 
   2715                      // and the toast needs to happen on the UI thread.
   2716                      ShowToast({}, result ? fmt::format(FSUI_FSTR("Automatic mapping completed for {}."), name) :
   2717                                             fmt::format(FSUI_FSTR("Automatic mapping failed for {}."), name));
   2718                      CloseChoiceDialog();
   2719                    });
   2720 }
   2721 
   2722 void FullscreenUI::SwitchToSettings()
   2723 {
   2724   s_game_settings_entry.reset();
   2725   s_game_settings_interface.reset();
   2726 
   2727   PopulateGraphicsAdapterList();
   2728   PopulatePostProcessingChain(GetEditingSettingsInterface(), PostProcessing::Config::DISPLAY_CHAIN_SECTION);
   2729 
   2730   s_current_main_window = MainWindowType::Settings;
   2731   s_settings_page = SettingsPage::Interface;
   2732 }
   2733 
   2734 void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial)
   2735 {
   2736   s_game_settings_entry.reset();
   2737   s_game_settings_interface = std::make_unique<INISettingsInterface>(System::GetGameSettingsPath(serial));
   2738   s_game_settings_interface->Load();
   2739   s_current_main_window = MainWindowType::Settings;
   2740   s_settings_page = SettingsPage::Summary;
   2741   QueueResetFocus(FocusResetType::ViewChanged);
   2742 }
   2743 
   2744 void FullscreenUI::SwitchToGameSettings()
   2745 {
   2746   if (System::GetGameSerial().empty())
   2747     return;
   2748 
   2749   auto lock = GameList::GetLock();
   2750   const GameList::Entry* entry = GameList::GetEntryForPath(System::GetDiscPath());
   2751   if (!entry)
   2752   {
   2753     SwitchToGameSettingsForSerial(System::GetGameSerial());
   2754     return;
   2755   }
   2756 
   2757   SwitchToGameSettings(entry);
   2758 }
   2759 
   2760 void FullscreenUI::SwitchToGameSettingsForPath(const std::string& path)
   2761 {
   2762   auto lock = GameList::GetLock();
   2763   const GameList::Entry* entry = GameList::GetEntryForPath(path);
   2764   if (entry)
   2765     SwitchToGameSettings(entry);
   2766 }
   2767 
   2768 void FullscreenUI::SwitchToGameSettings(const GameList::Entry* entry)
   2769 {
   2770   SwitchToGameSettingsForSerial(entry->serial);
   2771   s_game_settings_entry = std::make_unique<GameList::Entry>(*entry);
   2772 }
   2773 
   2774 void FullscreenUI::PopulateGraphicsAdapterList()
   2775 {
   2776   const GPURenderer renderer =
   2777     Settings::ParseRendererName(GetEffectiveTinyStringSetting(GetEditingSettingsInterface(false), "GPU", "Renderer",
   2778                                                               Settings::GetRendererName(Settings::DEFAULT_GPU_RENDERER))
   2779                                   .c_str())
   2780       .value_or(Settings::DEFAULT_GPU_RENDERER);
   2781 
   2782   s_graphics_adapter_list_cache = GPUDevice::GetAdapterListForAPI(Settings::GetRenderAPIForRenderer(renderer));
   2783 }
   2784 
   2785 void FullscreenUI::PopulateGameListDirectoryCache(SettingsInterface* si)
   2786 {
   2787   s_game_list_directories_cache.clear();
   2788   for (std::string& dir : si->GetStringList("GameList", "Paths"))
   2789     s_game_list_directories_cache.emplace_back(std::move(dir), false);
   2790   for (std::string& dir : si->GetStringList("GameList", "RecursivePaths"))
   2791     s_game_list_directories_cache.emplace_back(std::move(dir), true);
   2792 }
   2793 
   2794 void FullscreenUI::DoCopyGameSettings()
   2795 {
   2796   if (!s_game_settings_interface)
   2797     return;
   2798 
   2799   Settings temp_settings;
   2800   temp_settings.Load(*GetEditingSettingsInterface(false), *GetEditingSettingsInterface(false));
   2801   temp_settings.Save(*s_game_settings_interface, true);
   2802   SetSettingsChanged(s_game_settings_interface.get());
   2803 
   2804   ShowToast("Game Settings Copied", fmt::format(FSUI_FSTR("Game settings initialized with global settings for '{}'."),
   2805                                                 Path::GetFileTitle(s_game_settings_interface->GetFileName())));
   2806 }
   2807 
   2808 void FullscreenUI::DoClearGameSettings()
   2809 {
   2810   if (!s_game_settings_interface)
   2811     return;
   2812 
   2813   s_game_settings_interface->Clear();
   2814   if (!s_game_settings_interface->GetFileName().empty())
   2815     FileSystem::DeleteFile(s_game_settings_interface->GetFileName().c_str());
   2816 
   2817   SetSettingsChanged(s_game_settings_interface.get());
   2818 
   2819   ShowToast("Game Settings Cleared", fmt::format(FSUI_FSTR("Game settings have been cleared for '{}'."),
   2820                                                  Path::GetFileTitle(s_game_settings_interface->GetFileName())));
   2821 }
   2822 
   2823 void FullscreenUI::DrawSettingsWindow()
   2824 {
   2825   ImGuiIO& io = ImGui::GetIO();
   2826   const ImVec2 heading_size =
   2827     ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   2828                                (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f));
   2829 
   2830   const float bg_alpha = System::IsValid() ? (s_settings_page == SettingsPage::PostProcessing ? 0.50f : 0.90f) : 1.0f;
   2831 
   2832   if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category",
   2833                             ImVec4(UIPrimaryColor.x, UIPrimaryColor.y, UIPrimaryColor.z, bg_alpha)))
   2834   {
   2835     static constexpr float ITEM_WIDTH = 25.0f;
   2836 
   2837     static constexpr const char* global_icons[] = {
   2838       ICON_FA_TV,           ICON_FA_DICE_D20,    ICON_FA_COGS,   ICON_PF_MICROCHIP,
   2839       ICON_PF_PICTURE,      ICON_FA_MAGIC,       ICON_PF_SOUND,  ICON_PF_GAMEPAD_ALT,
   2840       ICON_PF_KEYBOARD_ALT, ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE};
   2841     static constexpr const char* per_game_icons[] = {ICON_FA_PARAGRAPH,   ICON_FA_HDD,    ICON_FA_COGS,
   2842                                                      ICON_PF_PICTURE,     ICON_PF_SOUND,  ICON_PF_GAMEPAD_ALT,
   2843                                                      ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE};
   2844     static constexpr SettingsPage global_pages[] = {
   2845       SettingsPage::Interface, SettingsPage::Console,        SettingsPage::Emulation,    SettingsPage::BIOS,
   2846       SettingsPage::Display,   SettingsPage::PostProcessing, SettingsPage::Audio,        SettingsPage::Controller,
   2847       SettingsPage::Hotkey,    SettingsPage::MemoryCards,    SettingsPage::Achievements, SettingsPage::Advanced};
   2848     static constexpr SettingsPage per_game_pages[] = {
   2849       SettingsPage::Summary,     SettingsPage::Console,      SettingsPage::Emulation,
   2850       SettingsPage::Display,     SettingsPage::Audio,        SettingsPage::Controller,
   2851       SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced};
   2852     static constexpr std::array<const char*, static_cast<u32>(SettingsPage::Count)> titles = {
   2853       {FSUI_NSTR("Summary"), FSUI_NSTR("Interface Settings"), FSUI_NSTR("Console Settings"),
   2854        FSUI_NSTR("Emulation Settings"), FSUI_NSTR("BIOS Settings"), FSUI_NSTR("Controller Settings"),
   2855        FSUI_NSTR("Hotkey Settings"), FSUI_NSTR("Memory Card Settings"), FSUI_NSTR("Graphics Settings"),
   2856        FSUI_NSTR("Post-Processing Settings"), FSUI_NSTR("Audio Settings"), FSUI_NSTR("Achievements Settings"),
   2857        FSUI_NSTR("Advanced Settings")}};
   2858 
   2859     const bool game_settings = IsEditingGameSettings(GetEditingSettingsInterface());
   2860     const u32 count =
   2861       game_settings ? static_cast<u32>(std::size(per_game_pages)) : static_cast<u32>(std::size(global_pages));
   2862     const char* const* icons = game_settings ? per_game_icons : global_icons;
   2863     const SettingsPage* pages = game_settings ? per_game_pages : global_pages;
   2864     u32 index = 0;
   2865     for (u32 i = 0; i < count; i++)
   2866     {
   2867       if (pages[i] == s_settings_page)
   2868       {
   2869         index = i;
   2870         break;
   2871       }
   2872     }
   2873 
   2874     BeginNavBar();
   2875 
   2876     if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup))
   2877     {
   2878       if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) ||
   2879           ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true))
   2880       {
   2881         index = (index == 0) ? (count - 1) : (index - 1);
   2882         s_settings_page = pages[index];
   2883         QueueResetFocus(FocusResetType::Other);
   2884       }
   2885       else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) ||
   2886                ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) ||
   2887                ImGui::IsKeyPressed(ImGuiKey_RightArrow, true))
   2888       {
   2889         index = (index + 1) % count;
   2890         s_settings_page = pages[index];
   2891         QueueResetFocus(FocusResetType::Other);
   2892       }
   2893     }
   2894 
   2895     if (NavButton(ICON_FA_BACKWARD, true, true))
   2896       ReturnToPreviousWindow();
   2897 
   2898     if (s_game_settings_entry)
   2899       NavTitle(s_game_settings_entry->title.c_str());
   2900     else
   2901       NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast<u32>(pages[index])]));
   2902 
   2903     RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   2904 
   2905     for (u32 i = 0; i < count; i++)
   2906     {
   2907       if (NavButton(icons[i], i == index, true, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   2908       {
   2909         s_settings_page = pages[i];
   2910         QueueResetFocus(FocusResetType::Other);
   2911       }
   2912     }
   2913 
   2914     EndNavBar();
   2915   }
   2916 
   2917   EndFullscreenWindow();
   2918 
   2919   // we have to do this here, because otherwise it uses target, and jumps a frame later.
   2920   // don't do it for popups opening/closing, otherwise we lose our position
   2921   if (IsFocusResetFromWindowChange())
   2922     ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));
   2923 
   2924   if (BeginFullscreenWindow(
   2925         ImVec2(0.0f, heading_size.y),
   2926         ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)),
   2927         TinyString::from_format("settings_page_{}", static_cast<u32>(s_settings_page)).c_str(),
   2928         ImVec4(UIBackgroundColor.x, UIBackgroundColor.y, UIBackgroundColor.z, bg_alpha), 0.0f,
   2929         ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f)))
   2930   {
   2931     ResetFocusHere();
   2932 
   2933     if (ImGui::IsWindowFocused() && WantsToCloseMenu())
   2934       ReturnToPreviousWindow();
   2935 
   2936     auto lock = Host::GetSettingsLock();
   2937 
   2938     switch (s_settings_page)
   2939     {
   2940       case SettingsPage::Summary:
   2941         DrawSummarySettingsPage();
   2942         break;
   2943 
   2944       case SettingsPage::Interface:
   2945         DrawInterfaceSettingsPage();
   2946         break;
   2947 
   2948       case SettingsPage::BIOS:
   2949         DrawBIOSSettingsPage();
   2950         break;
   2951 
   2952       case SettingsPage::Emulation:
   2953         DrawEmulationSettingsPage();
   2954         break;
   2955 
   2956       case SettingsPage::Console:
   2957         DrawConsoleSettingsPage();
   2958         break;
   2959 
   2960       case SettingsPage::Display:
   2961         DrawDisplaySettingsPage();
   2962         break;
   2963 
   2964       case SettingsPage::PostProcessing:
   2965         DrawPostProcessingSettingsPage();
   2966         break;
   2967 
   2968       case SettingsPage::Audio:
   2969         DrawAudioSettingsPage();
   2970         break;
   2971 
   2972       case SettingsPage::MemoryCards:
   2973         DrawMemoryCardSettingsPage();
   2974         break;
   2975 
   2976       case SettingsPage::Controller:
   2977         DrawControllerSettingsPage();
   2978         break;
   2979 
   2980       case SettingsPage::Hotkey:
   2981         DrawHotkeySettingsPage();
   2982         break;
   2983 
   2984       case SettingsPage::Achievements:
   2985         DrawAchievementsSettingsPage();
   2986         break;
   2987 
   2988       case SettingsPage::Advanced:
   2989         DrawAdvancedSettingsPage();
   2990         break;
   2991 
   2992       default:
   2993         break;
   2994     }
   2995   }
   2996 
   2997   EndFullscreenWindow();
   2998 
   2999   if (IsGamepadInputSource())
   3000   {
   3001     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Change Page")),
   3002                                        std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Navigate")),
   3003                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
   3004                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))});
   3005   }
   3006   else
   3007   {
   3008     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Change Page")),
   3009                                        std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Navigate")),
   3010                                        std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")),
   3011                                        std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))});
   3012   }
   3013 }
   3014 
   3015 void FullscreenUI::DrawSummarySettingsPage()
   3016 {
   3017   BeginMenuButtons();
   3018 
   3019   MenuHeading(FSUI_CSTR("Details"));
   3020 
   3021   if (s_game_settings_entry)
   3022   {
   3023     if (MenuButton(FSUI_ICONSTR(ICON_FA_WINDOW_MAXIMIZE, "Title"), s_game_settings_entry->title.c_str(), true))
   3024       CopyTextToClipboard(FSUI_STR("Game title copied to clipboard."), s_game_settings_entry->title);
   3025     if (MenuButton(FSUI_ICONSTR(ICON_FA_PAGER, "Serial"), s_game_settings_entry->serial.c_str(), true))
   3026       CopyTextToClipboard(FSUI_STR("Game serial copied to clipboard."), s_game_settings_entry->serial);
   3027     if (MenuButton(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Type"),
   3028                    GameList::GetEntryTypeDisplayName(s_game_settings_entry->type), true))
   3029     {
   3030       CopyTextToClipboard(FSUI_STR("Game type copied to clipboard."),
   3031                           GameList::GetEntryTypeDisplayName(s_game_settings_entry->type));
   3032     }
   3033     if (MenuButton(FSUI_ICONSTR(ICON_FA_BOX, "Region"),
   3034                    Settings::GetDiscRegionDisplayName(s_game_settings_entry->region), true))
   3035     {
   3036       CopyTextToClipboard(FSUI_STR("Game region copied to clipboard."),
   3037                           Settings::GetDiscRegionDisplayName(s_game_settings_entry->region));
   3038     }
   3039     if (MenuButton(FSUI_ICONSTR(ICON_FA_STAR, "Compatibility Rating"),
   3040                    GameDatabase::GetCompatibilityRatingDisplayName(s_game_settings_entry->compatibility), true))
   3041     {
   3042       CopyTextToClipboard(FSUI_STR("Game compatibility rating copied to clipboard."),
   3043                           GameDatabase::GetCompatibilityRatingDisplayName(s_game_settings_entry->compatibility));
   3044     }
   3045     if (MenuButton(FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Path"), s_game_settings_entry->path.c_str(), true))
   3046     {
   3047       CopyTextToClipboard(FSUI_STR("Game path copied to clipboard."), s_game_settings_entry->path);
   3048     }
   3049   }
   3050   else
   3051   {
   3052     MenuButton(FSUI_ICONSTR(ICON_FA_BAN, "Details unavailable for game not scanned in game list."), "");
   3053   }
   3054 
   3055   MenuHeading(FSUI_CSTR("Options"));
   3056 
   3057   if (MenuButton(FSUI_ICONSTR(ICON_FA_COPY, "Copy Settings"),
   3058                  FSUI_CSTR("Copies the current global settings to this game.")))
   3059   {
   3060     DoCopyGameSettings();
   3061   }
   3062   if (MenuButton(FSUI_ICONSTR(ICON_FA_TRASH, "Clear Settings"), FSUI_CSTR("Clears all settings set for this game.")))
   3063   {
   3064     DoClearGameSettings();
   3065   }
   3066 
   3067   EndMenuButtons();
   3068 }
   3069 
   3070 void FullscreenUI::DrawInterfaceSettingsPage()
   3071 {
   3072   SettingsInterface* bsi = GetEditingSettingsInterface();
   3073 
   3074   BeginMenuButtons();
   3075 
   3076   MenuHeading(FSUI_CSTR("Behavior"));
   3077 
   3078   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_PAUSE, "Pause On Start"),
   3079                     FSUI_CSTR("Pauses the emulator when a game is started."), "Main", "StartPaused", false);
   3080   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_VIDEO, "Pause On Focus Loss"),
   3081                     FSUI_CSTR("Pauses the emulator when you minimize the window or switch to another "
   3082                               "application, and unpauses when you switch back."),
   3083                     "Main", "PauseOnFocusLoss", false);
   3084   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_GAMEPAD, "Pause On Controller Disconnection"),
   3085                     FSUI_CSTR("Pauses the emulator when a controller with bindings is disconnected."), "Main",
   3086                     "PauseOnControllerDisconnection", false);
   3087   DrawToggleSetting(
   3088     bsi, FSUI_ICONSTR(ICON_FA_POWER_OFF, "Confirm Power Off"),
   3089     FSUI_CSTR("Determines whether a prompt will be displayed to confirm shutting down the emulator/game "
   3090               "when the hotkey is pressed."),
   3091     "Main", "ConfirmPowerOff", true);
   3092   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_SAVE, "Save State On Exit"),
   3093                     FSUI_CSTR("Automatically saves the emulator state when powering down or exiting. You can then "
   3094                               "resume directly from where you left off next time."),
   3095                     "Main", "SaveStateOnExit", true);
   3096   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WINDOW_MAXIMIZE, "Start Fullscreen"),
   3097                     FSUI_CSTR("Automatically switches to fullscreen mode when the program is started."), "Main",
   3098                     "StartFullscreen", false);
   3099   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MOUSE, "Double-Click Toggles Fullscreen"),
   3100                     FSUI_CSTR("Switches between full screen and windowed when the window is double-clicked."), "Main",
   3101                     "DoubleClickTogglesFullscreen", true);
   3102   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MOUSE_POINTER, "Hide Cursor In Fullscreen"),
   3103                     FSUI_CSTR("Hides the mouse pointer/cursor when the emulator is in fullscreen mode."), "Main",
   3104                     "HideCursorInFullscreen", true);
   3105   DrawToggleSetting(
   3106     bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Inhibit Screensaver"),
   3107     FSUI_CSTR("Prevents the screen saver from activating and the host from sleeping while emulation is running."),
   3108     "Main", "InhibitScreensaver", true);
   3109 
   3110   if (DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_PAINT_BRUSH, "Use Light Theme"),
   3111                         FSUI_CSTR("Uses a light coloured theme instead of the default dark theme."), "Main",
   3112                         "UseLightFullscreenUITheme", false))
   3113   {
   3114     ImGuiFullscreen::SetTheme(bsi->GetBoolValue("Main", "UseLightFullscreenUITheme", false));
   3115   }
   3116 
   3117   {
   3118     // Have to do this the annoying way, because it's host-derived.
   3119     const auto language_list = Host::GetAvailableLanguageList();
   3120     TinyString current_language = bsi->GetTinyStringValue("Main", "Language", "");
   3121     const char* current_language_name = "Unknown";
   3122     for (const auto& [language, code] : language_list)
   3123     {
   3124       if (current_language == code)
   3125         current_language_name = language;
   3126     }
   3127     if (MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_LANGUAGE, "UI Language"),
   3128                             FSUI_CSTR("Chooses the language used for UI elements."), current_language_name))
   3129     {
   3130       ImGuiFullscreen::ChoiceDialogOptions options;
   3131       for (const auto& [language, code] : language_list)
   3132         options.emplace_back(fmt::format("{} [{}]", language, code), (current_language == code));
   3133       OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_LANGUAGE, "UI Language"), false, std::move(options),
   3134                        [language_list](s32 index, const std::string& title, bool checked) {
   3135                          if (static_cast<u32>(index) >= language_list.size())
   3136                            return;
   3137 
   3138                          Host::RunOnCPUThread(
   3139                            [language = language_list[index].second]() { Host::ChangeLanguage(language); });
   3140                          ImGuiFullscreen::CloseChoiceDialog();
   3141                        });
   3142     }
   3143   }
   3144 
   3145   MenuHeading(FSUI_CSTR("Integration"));
   3146   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_CHARGING_STATION, "Enable Discord Presence"),
   3147                     FSUI_CSTR("Shows the game you are currently playing as part of your profile in Discord."), "Main",
   3148                     "EnableDiscordPresence", false);
   3149 
   3150   MenuHeading(FSUI_CSTR("On-Screen Display"));
   3151   DrawIntSpinBoxSetting(bsi, FSUI_ICONSTR(ICON_FA_SEARCH, "OSD Scale"),
   3152                         FSUI_CSTR("Determines how large the on-screen messages and monitor are."), "Display",
   3153                         "OSDScale", 100, 25, 500, 1, "%d%%");
   3154   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST, "Show OSD Messages"),
   3155                     FSUI_CSTR("Shows on-screen-display messages when events occur."), "Display", "ShowOSDMessages",
   3156                     true);
   3157   DrawToggleSetting(
   3158     bsi, FSUI_ICONSTR(ICON_FA_CLOCK, "Show Speed"),
   3159     FSUI_CSTR(
   3160       "Shows the current emulation speed of the system in the top-right corner of the display as a percentage."),
   3161     "Display", "ShowSpeed", false);
   3162   DrawToggleSetting(
   3163     bsi, FSUI_ICONSTR(ICON_FA_RULER, "Show FPS"),
   3164     FSUI_CSTR("Shows the number of frames (or v-syncs) displayed per second by the system in the top-right "
   3165               "corner of the display."),
   3166     "Display", "ShowFPS", false);
   3167   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BARS, "Show GPU Statistics"),
   3168                     FSUI_CSTR("Shows information about the emulated GPU in the top-right corner of the display."),
   3169                     "Display", "ShowGPUStatistics", false);
   3170   DrawToggleSetting(
   3171     bsi, FSUI_ICONSTR(ICON_FA_STOPWATCH, "Show Latency Statistics"),
   3172     FSUI_CSTR("Shows information about input and audio latency in the top-right corner of the display."), "Display",
   3173     "ShowLatencyStatistics", false);
   3174   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BATTERY_HALF, "Show CPU Usage"),
   3175                     FSUI_CSTR("Shows the host's CPU usage based on threads in the top-right corner of the display."),
   3176                     "Display", "ShowCPU", false);
   3177   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_SPINNER, "Show GPU Usage"),
   3178                     FSUI_CSTR("Shows the host's GPU usage in the top-right corner of the display."), "Display",
   3179                     "ShowGPU", false);
   3180   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_RULER_HORIZONTAL, "Show Frame Times"),
   3181                     FSUI_CSTR("Shows a visual history of frame times in the upper-left corner of the display."),
   3182                     "Display", "ShowFrameTimes", false);
   3183   DrawToggleSetting(
   3184     bsi, FSUI_ICONSTR(ICON_FA_RULER_VERTICAL, "Show Resolution"),
   3185     FSUI_CSTR("Shows the current rendering resolution of the system in the top-right corner of the display."),
   3186     "Display", "ShowResolution", false);
   3187   DrawToggleSetting(
   3188     bsi, FSUI_ICONSTR(ICON_FA_GAMEPAD, "Show Controller Input"),
   3189     FSUI_CSTR("Shows the current controller state of the system in the bottom-left corner of the display."), "Display",
   3190     "ShowInputs", false);
   3191 
   3192   EndMenuButtons();
   3193 }
   3194 
   3195 void FullscreenUI::DrawBIOSSettingsPage()
   3196 {
   3197   static constexpr const std::array config_keys = {"", "PathNTSCJ", "PathNTSCU", "PathPAL"};
   3198 
   3199   SettingsInterface* bsi = GetEditingSettingsInterface();
   3200   const bool game_settings = IsEditingGameSettings(bsi);
   3201 
   3202   BeginMenuButtons();
   3203 
   3204   MenuHeading(FSUI_CSTR("BIOS Selection"));
   3205 
   3206   for (u32 i = 0; i < static_cast<u32>(ConsoleRegion::Count); i++)
   3207   {
   3208     const ConsoleRegion region = static_cast<ConsoleRegion>(i);
   3209     if (region == ConsoleRegion::Auto)
   3210       continue;
   3211 
   3212     TinyString title;
   3213     title.format(FSUI_FSTR("BIOS for {}"), Settings::GetConsoleRegionDisplayName(region));
   3214 
   3215     const std::optional<SmallString> filename(bsi->GetOptionalSmallStringValue(
   3216       "BIOS", config_keys[i], game_settings ? std::nullopt : std::optional<const char*>("")));
   3217 
   3218     if (MenuButtonWithValue(title,
   3219                             SmallString::from_format(FSUI_FSTR("BIOS to use when emulating {} consoles."),
   3220                                                      Settings::GetConsoleRegionDisplayName(region)),
   3221                             filename.has_value() ? (filename->empty() ? FSUI_CSTR("Auto-Detect") : filename->c_str()) :
   3222                                                    FSUI_CSTR("Use Global Setting")))
   3223     {
   3224       ImGuiFullscreen::ChoiceDialogOptions options;
   3225       auto images = BIOS::FindBIOSImagesInDirectory(EmuFolders::Bios.c_str());
   3226       options.reserve(images.size() + 2);
   3227       if (IsEditingGameSettings(bsi))
   3228         options.emplace_back(FSUI_STR("Use Global Setting"), !filename.has_value());
   3229       options.emplace_back(FSUI_STR("Auto-Detect"), filename.has_value() && filename->empty());
   3230       for (auto& [path, info] : images)
   3231       {
   3232         const bool selected = (filename.has_value() && filename.value() == path);
   3233         options.emplace_back(std::move(path), selected);
   3234       }
   3235 
   3236       OpenChoiceDialog(title, false, std::move(options),
   3237                        [game_settings, i](s32 index, const std::string& path, bool checked) {
   3238                          if (index >= 0)
   3239                          {
   3240                            auto lock = Host::GetSettingsLock();
   3241                            SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   3242                            if (game_settings && index == 0)
   3243                              bsi->DeleteValue("BIOS", config_keys[i]);
   3244                            else
   3245                              bsi->SetStringValue("BIOS", config_keys[i], path.c_str());
   3246                            SetSettingsChanged(bsi);
   3247                          }
   3248                          CloseChoiceDialog();
   3249                        });
   3250     }
   3251   }
   3252 
   3253   DrawFolderSetting(bsi, FSUI_CSTR("BIOS Directory"), "BIOS", "SearchDirectory", EmuFolders::Bios);
   3254 
   3255   MenuHeading(FSUI_CSTR("Patches"));
   3256 
   3257   DrawToggleSetting(bsi, FSUI_CSTR("Enable Fast Boot"),
   3258                     FSUI_CSTR("Patches the BIOS to skip the boot animation. Safe to enable."), "BIOS", "PatchFastBoot",
   3259                     Settings::DEFAULT_FAST_BOOT_VALUE);
   3260   DrawToggleSetting(bsi, FSUI_CSTR("Enable TTY Logging"),
   3261                     FSUI_CSTR("Logs BIOS calls to printf(). Not all games contain debugging messages."), "BIOS",
   3262                     "TTYLogging", false);
   3263 
   3264   EndMenuButtons();
   3265 }
   3266 
   3267 void FullscreenUI::DrawConsoleSettingsPage()
   3268 {
   3269   static constexpr const std::array cdrom_read_speeds = {
   3270     FSUI_NSTR("None (Double Speed)"), FSUI_NSTR("2x (Quad Speed)"), FSUI_NSTR("3x (6x Speed)"),
   3271     FSUI_NSTR("4x (8x Speed)"),       FSUI_NSTR("5x (10x Speed)"),  FSUI_NSTR("6x (12x Speed)"),
   3272     FSUI_NSTR("7x (14x Speed)"),      FSUI_NSTR("8x (16x Speed)"),  FSUI_NSTR("9x (18x Speed)"),
   3273     FSUI_NSTR("10x (20x Speed)"),
   3274   };
   3275 
   3276   static constexpr const std::array cdrom_seek_speeds = {
   3277     FSUI_NSTR("Infinite/Instantaneous"),
   3278     FSUI_NSTR("None (Normal Speed)"),
   3279     FSUI_NSTR("2x"),
   3280     FSUI_NSTR("3x"),
   3281     FSUI_NSTR("4x"),
   3282     FSUI_NSTR("5x"),
   3283     FSUI_NSTR("6x"),
   3284     FSUI_NSTR("7x"),
   3285     FSUI_NSTR("8x"),
   3286     FSUI_NSTR("9x"),
   3287     FSUI_NSTR("10x"),
   3288   };
   3289 
   3290   SettingsInterface* bsi = GetEditingSettingsInterface();
   3291 
   3292   BeginMenuButtons();
   3293 
   3294   MenuHeading(FSUI_CSTR("Console Settings"));
   3295 
   3296   DrawEnumSetting(bsi, FSUI_ICONSTR(ICON_FA_GLOBE, "Region"), FSUI_CSTR("Determines the emulated hardware type."),
   3297                   "Console", "Region", Settings::DEFAULT_CONSOLE_REGION, &Settings::ParseConsoleRegionName,
   3298                   &Settings::GetConsoleRegionName, &Settings::GetConsoleRegionDisplayName, ConsoleRegion::Count);
   3299   DrawToggleSetting(
   3300     bsi, FSUI_ICONSTR(ICON_FA_MEMORY, "Enable 8MB RAM"),
   3301     FSUI_CSTR("Enables an additional 6MB of RAM to obtain a total of 2+6 = 8MB, usually present on dev consoles."),
   3302     "Console", "Enable8MBRAM", false);
   3303   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Disable All Enhancements"),
   3304                     FSUI_CSTR("Temporarily disables all enhancements, useful when testing."), "Main",
   3305                     "DisableAllEnhancements", false);
   3306   DrawToggleSetting(
   3307     bsi, FSUI_ICONSTR(ICON_FA_FROWN, "Enable Cheats"),
   3308     FSUI_CSTR("Automatically loads and applies cheats on game start. Cheats can break games and saves."), "Console",
   3309     "EnableCheats", false);
   3310 
   3311   MenuHeading(FSUI_CSTR("CPU Emulation"));
   3312 
   3313   DrawEnumSetting(bsi, FSUI_ICONSTR(ICON_FA_BOLT, "Execution Mode"),
   3314                   FSUI_CSTR("Determines how the emulated CPU executes instructions."), "CPU", "ExecutionMode",
   3315                   Settings::DEFAULT_CPU_EXECUTION_MODE, &Settings::ParseCPUExecutionMode,
   3316                   &Settings::GetCPUExecutionModeName, &Settings::GetCPUExecutionModeDisplayName,
   3317                   CPUExecutionMode::Count);
   3318 
   3319   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_TACHOMETER_ALT, "Enable Overclocking"),
   3320                     FSUI_CSTR("When this option is chosen, the clock speed set below will be used."), "CPU",
   3321                     "OverclockEnable", false);
   3322 
   3323   const bool oc_enable = GetEffectiveBoolSetting(bsi, "CPU", "OverclockEnable", false);
   3324   if (oc_enable)
   3325   {
   3326     u32 oc_numerator = GetEffectiveUIntSetting(bsi, "CPU", "OverclockNumerator", 1);
   3327     u32 oc_denominator = GetEffectiveUIntSetting(bsi, "CPU", "OverclockDenominator", 1);
   3328     s32 oc_percent = static_cast<s32>(Settings::CPUOverclockFractionToPercent(oc_numerator, oc_denominator));
   3329     if (RangeButton(FSUI_ICONSTR(ICON_FA_TACHOMETER_ALT, "Overclocking Percentage"),
   3330                     FSUI_CSTR("Selects the percentage of the normal clock speed the emulated hardware will run at."),
   3331                     &oc_percent, 10, 1000, 10, "%d%%"))
   3332     {
   3333       Settings::CPUOverclockPercentToFraction(oc_percent, &oc_numerator, &oc_denominator);
   3334       bsi->SetUIntValue("CPU", "OverclockNumerator", oc_numerator);
   3335       bsi->SetUIntValue("CPU", "OverclockDenominator", oc_denominator);
   3336       SetSettingsChanged(bsi);
   3337     }
   3338   }
   3339 
   3340   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MICROCHIP, "Enable Recompiler ICache"),
   3341                     FSUI_CSTR("Makes games run closer to their console framerate, at a small cost to performance."),
   3342                     "CPU", "RecompilerICache", false);
   3343 
   3344   MenuHeading(FSUI_CSTR("CD-ROM Emulation"));
   3345 
   3346   DrawIntListSetting(
   3347     bsi, FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Read Speedup"),
   3348     FSUI_CSTR(
   3349       "Speeds up CD-ROM reads by the specified factor. May improve loading speeds in some games, and break others."),
   3350     "CDROM", "ReadSpeedup", 1, cdrom_read_speeds.data(), cdrom_read_speeds.size(), true, 1);
   3351   DrawIntListSetting(
   3352     bsi, FSUI_ICONSTR(ICON_FA_SEARCH, "Seek Speedup"),
   3353     FSUI_CSTR(
   3354       "Speeds up CD-ROM seeks by the specified factor. May improve loading speeds in some games, and break others."),
   3355     "CDROM", "SeekSpeedup", 1, cdrom_seek_speeds.data(), cdrom_seek_speeds.size(), true);
   3356 
   3357   DrawIntRangeSetting(
   3358     bsi, FSUI_ICONSTR(ICON_FA_FAST_FORWARD, "Readahead Sectors"),
   3359     FSUI_CSTR("Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread."),
   3360     "CDROM", "ReadaheadSectors", Settings::DEFAULT_CDROM_READAHEAD_SECTORS, 0, 32, FSUI_CSTR("%d sectors"));
   3361 
   3362   DrawToggleSetting(
   3363     bsi, FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Preload Images to RAM"),
   3364     FSUI_CSTR("Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay."),
   3365     "CDROM", "LoadImageToRAM", false);
   3366   DrawToggleSetting(
   3367     bsi, FSUI_ICONSTR(ICON_FA_VEST_PATCHES, "Apply Image Patches"),
   3368     FSUI_CSTR("Automatically applies patches to disc images when they are present, currently only PPF is supported."),
   3369     "CDROM", "LoadImagePatches", false);
   3370 
   3371   EndMenuButtons();
   3372 }
   3373 
   3374 void FullscreenUI::DrawEmulationSettingsPage()
   3375 {
   3376   static constexpr const std::array emulation_speed_values = {
   3377     0.0f,  0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.25f, 1.5f,
   3378     1.75f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f,  10.0f,
   3379   };
   3380   static constexpr const std::array emulation_speed_titles = {
   3381     FSUI_NSTR("Unlimited"),
   3382     "10% [6 FPS (NTSC) / 5 FPS (PAL)]",
   3383     FSUI_NSTR("20% [12 FPS (NTSC) / 10 FPS (PAL)]"),
   3384     FSUI_NSTR("30% [18 FPS (NTSC) / 15 FPS (PAL)]"),
   3385     FSUI_NSTR("40% [24 FPS (NTSC) / 20 FPS (PAL)]"),
   3386     FSUI_NSTR("50% [30 FPS (NTSC) / 25 FPS (PAL)]"),
   3387     FSUI_NSTR("60% [36 FPS (NTSC) / 30 FPS (PAL)]"),
   3388     FSUI_NSTR("70% [42 FPS (NTSC) / 35 FPS (PAL)]"),
   3389     FSUI_NSTR("80% [48 FPS (NTSC) / 40 FPS (PAL)]"),
   3390     FSUI_NSTR("90% [54 FPS (NTSC) / 45 FPS (PAL)]"),
   3391     FSUI_NSTR("100% [60 FPS (NTSC) / 50 FPS (PAL)]"),
   3392     FSUI_NSTR("125% [75 FPS (NTSC) / 62 FPS (PAL)]"),
   3393     FSUI_NSTR("150% [90 FPS (NTSC) / 75 FPS (PAL)]"),
   3394     FSUI_NSTR("175% [105 FPS (NTSC) / 87 FPS (PAL)]"),
   3395     FSUI_NSTR("200% [120 FPS (NTSC) / 100 FPS (PAL)]"),
   3396     FSUI_NSTR("250% [150 FPS (NTSC) / 125 FPS (PAL)]"),
   3397     FSUI_NSTR("300% [180 FPS (NTSC) / 150 FPS (PAL)]"),
   3398     FSUI_NSTR("350% [210 FPS (NTSC) / 175 FPS (PAL)]"),
   3399     FSUI_NSTR("400% [240 FPS (NTSC) / 200 FPS (PAL)]"),
   3400     FSUI_NSTR("450% [270 FPS (NTSC) / 225 FPS (PAL)]"),
   3401     FSUI_NSTR("500% [300 FPS (NTSC) / 250 FPS (PAL)]"),
   3402     FSUI_NSTR("600% [360 FPS (NTSC) / 300 FPS (PAL)]"),
   3403     FSUI_NSTR("700% [420 FPS (NTSC) / 350 FPS (PAL)]"),
   3404     FSUI_NSTR("800% [480 FPS (NTSC) / 400 FPS (PAL)]"),
   3405     FSUI_NSTR("900% [540 FPS (NTSC) / 450 FPS (PAL)]"),
   3406     FSUI_NSTR("1000% [600 FPS (NTSC) / 500 FPS (PAL)]"),
   3407   };
   3408 
   3409   SettingsInterface* bsi = GetEditingSettingsInterface();
   3410 
   3411   BeginMenuButtons();
   3412 
   3413   MenuHeading(FSUI_CSTR("Speed Control"));
   3414   DrawFloatListSetting(
   3415     bsi, FSUI_ICONSTR(ICON_FA_STOPWATCH, "Emulation Speed"),
   3416     FSUI_CSTR("Sets the target emulation speed. It is not guaranteed that this speed will be reached on all systems."),
   3417     "Main", "EmulationSpeed", 1.0f, emulation_speed_titles.data(), emulation_speed_values.data(),
   3418     emulation_speed_titles.size(), true);
   3419   DrawFloatListSetting(
   3420     bsi, FSUI_ICONSTR(ICON_FA_BOLT, "Fast Forward Speed"),
   3421     FSUI_CSTR("Sets the fast forward speed. It is not guaranteed that this speed will be reached on all systems."),
   3422     "Main", "FastForwardSpeed", 0.0f, emulation_speed_titles.data(), emulation_speed_values.data(),
   3423     emulation_speed_titles.size(), true);
   3424   DrawFloatListSetting(
   3425     bsi, FSUI_ICONSTR(ICON_FA_BOLT, "Turbo Speed"),
   3426     FSUI_CSTR("Sets the turbo speed. It is not guaranteed that this speed will be reached on all systems."), "Main",
   3427     "TurboSpeed", 2.0f, emulation_speed_titles.data(), emulation_speed_values.data(), emulation_speed_titles.size(),
   3428     true);
   3429 
   3430   MenuHeading(FSUI_CSTR("Latency Control"));
   3431   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_TV, "Vertical Sync (VSync)"),
   3432                     FSUI_CSTR("Synchronizes presentation of the console's frames to the host. GSync/FreeSync users "
   3433                               "should enable Optimal Frame Pacing instead."),
   3434                     "Display", "VSync", false);
   3435 
   3436   DrawToggleSetting(
   3437     bsi, FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Sync To Host Refresh Rate"),
   3438     FSUI_CSTR("Adjusts the emulation speed so the console's refresh rate matches the host when VSync is enabled."),
   3439     "Main", "SyncToHostRefreshRate", false);
   3440 
   3441   DrawToggleSetting(
   3442     bsi, FSUI_ICONSTR(ICON_FA_TACHOMETER_ALT, "Optimal Frame Pacing"),
   3443     FSUI_CSTR("Ensures every frame generated is displayed for optimal pacing. Enable for variable refresh displays, "
   3444               "such as GSync/FreeSync. Disable if you are having speed or sound issues."),
   3445     "Display", "OptimalFramePacing", false);
   3446 
   3447   const bool optimal_frame_pacing_active = GetEffectiveBoolSetting(bsi, "Display", "OptimalFramePacing", false);
   3448   DrawToggleSetting(
   3449     bsi, FSUI_ICONSTR(ICON_FA_STOPWATCH_20, "Reduce Input Latency"),
   3450     FSUI_CSTR("Reduces input latency by delaying the start of frame until closer to the presentation time."), "Display",
   3451     "PreFrameSleep", false, optimal_frame_pacing_active);
   3452 
   3453   DrawToggleSetting(
   3454     bsi, FSUI_ICONSTR(ICON_FA_CHARGING_STATION, "Skip Duplicate Frame Display"),
   3455     FSUI_CSTR("Skips the presentation/display of frames that are not unique. Can result in worse frame pacing."),
   3456     "Display", "SkipPresentingDuplicateFrames", false,
   3457     !(GetEffectiveBoolSetting(bsi, "Display", "VSync", false) &&
   3458       GetEffectiveBoolSetting(bsi, "Main", "SyncToHostRefreshRate", false)));
   3459 
   3460   const bool pre_frame_sleep_active =
   3461     (optimal_frame_pacing_active && GetEffectiveBoolSetting(bsi, "Display", "PreFrameSleep", false));
   3462   DrawFloatRangeSetting(
   3463     bsi, FSUI_ICONSTR(ICON_FA_BATTERY_FULL, "Frame Time Buffer"),
   3464     FSUI_CSTR("Specifies the amount of buffer time added, which reduces the additional sleep time introduced."),
   3465     "Display", "PreFrameSleepBuffer", Settings::DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER, 0.0f, 20.0f, "%.1f", 1.0f,
   3466     pre_frame_sleep_active);
   3467 
   3468   MenuHeading(FSUI_CSTR("Runahead/Rewind"));
   3469 
   3470   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BACKWARD, "Enable Rewinding"),
   3471                     FSUI_CSTR("Saves state periodically so you can rewind any mistakes while playing."), "Main",
   3472                     "RewindEnable", false);
   3473   DrawFloatRangeSetting(
   3474     bsi, FSUI_ICONSTR(ICON_FA_SAVE, "Rewind Save Frequency"),
   3475     FSUI_CSTR("How often a rewind state will be created. Higher frequencies have greater system requirements."), "Main",
   3476     "RewindFrequency", 10.0f, 0.0f, 3600.0f, FSUI_CSTR("%.2f Seconds"));
   3477   DrawIntRangeSetting(
   3478     bsi, FSUI_ICONSTR(ICON_FA_GLASS_WHISKEY, "Rewind Save Slots"),
   3479     FSUI_CSTR("How many saves will be kept for rewinding. Higher values have greater memory requirements."), "Main",
   3480     "RewindSaveSlots", 10, 1, 10000, FSUI_CSTR("%d Frames"));
   3481 
   3482   const s32 runahead_frames = GetEffectiveIntSetting(bsi, "Main", "RunaheadFrameCount", 0);
   3483   const bool runahead_enabled = (runahead_frames > 0);
   3484   const bool rewind_enabled = GetEffectiveBoolSetting(bsi, "Main", "RewindEnable", false);
   3485 
   3486   static constexpr const std::array runahead_options = {
   3487     FSUI_NSTR("Disabled"), FSUI_NSTR("1 Frame"),  FSUI_NSTR("2 Frames"), FSUI_NSTR("3 Frames"),
   3488     FSUI_NSTR("4 Frames"), FSUI_NSTR("5 Frames"), FSUI_NSTR("6 Frames"), FSUI_NSTR("7 Frames"),
   3489     FSUI_NSTR("8 Frames"), FSUI_NSTR("9 Frames"), FSUI_NSTR("10 Frames")};
   3490 
   3491   DrawIntListSetting(
   3492     bsi, FSUI_ICONSTR(ICON_FA_RUNNING, "Runahead"),
   3493     FSUI_CSTR(
   3494       "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very high system requirements."),
   3495     "Main", "RunaheadFrameCount", 0, runahead_options.data(), runahead_options.size(), true);
   3496 
   3497   TinyString rewind_summary;
   3498   if (runahead_enabled)
   3499   {
   3500     rewind_summary = FSUI_VSTR("Rewind is disabled because runahead is enabled. Runahead will significantly increase "
   3501                                "system requirements.");
   3502   }
   3503   else if (rewind_enabled)
   3504   {
   3505     const u32 resolution_scale = GetEffectiveUIntSetting(bsi, "GPU", "ResolutionScale", 1);
   3506     const float rewind_frequency = GetEffectiveFloatSetting(bsi, "Main", "RewindFrequency", 10.0f);
   3507     const s32 rewind_save_slots = GetEffectiveIntSetting(bsi, "Main", "RewindSaveSlots", 10);
   3508     const float duration =
   3509       ((rewind_frequency <= std::numeric_limits<float>::epsilon()) ? (1.0f / 60.0f) : rewind_frequency) *
   3510       static_cast<float>(rewind_save_slots);
   3511 
   3512     u64 ram_usage, vram_usage;
   3513     System::CalculateRewindMemoryUsage(rewind_save_slots, resolution_scale, &ram_usage, &vram_usage);
   3514     rewind_summary.format(
   3515       FSUI_FSTR("Rewind for {0} frames, lasting {1:.2f} seconds will require up to {2} MB of RAM and {3} MB of VRAM."),
   3516       rewind_save_slots, duration, ram_usage / 1048576, vram_usage / 1048576);
   3517   }
   3518   else
   3519   {
   3520     rewind_summary = FSUI_VSTR("Rewind is not enabled. Please note that enabling rewind may significantly increase "
   3521                                "system requirements.");
   3522   }
   3523 
   3524   ActiveButton(rewind_summary, false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font);
   3525 
   3526   EndMenuButtons();
   3527 }
   3528 
   3529 void FullscreenUI::CopyGlobalControllerSettingsToGame()
   3530 {
   3531   SettingsInterface* dsi = GetEditingSettingsInterface(true);
   3532   SettingsInterface* ssi = GetEditingSettingsInterface(false);
   3533 
   3534   InputManager::CopyConfiguration(dsi, *ssi, true, true, false);
   3535   SetSettingsChanged(dsi);
   3536 
   3537   ShowToast(std::string(), FSUI_STR("Per-game controller configuration initialized with global settings."));
   3538 }
   3539 
   3540 void FullscreenUI::DoLoadInputProfile()
   3541 {
   3542   std::vector<std::string> profiles = InputManager::GetInputProfileNames();
   3543   if (profiles.empty())
   3544   {
   3545     ShowToast(std::string(), FSUI_STR("No input profiles available."));
   3546     return;
   3547   }
   3548 
   3549   ImGuiFullscreen::ChoiceDialogOptions coptions;
   3550   coptions.reserve(profiles.size());
   3551   for (std::string& name : profiles)
   3552     coptions.emplace_back(std::move(name), false);
   3553   OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Load Profile"), false, std::move(coptions),
   3554                    [](s32 index, const std::string& title, bool checked) {
   3555                      if (index < 0)
   3556                        return;
   3557 
   3558                      INISettingsInterface ssi(System::GetInputProfilePath(title));
   3559                      if (!ssi.Load())
   3560                      {
   3561                        ShowToast(std::string(), fmt::format(FSUI_FSTR("Failed to load '{}'."), title));
   3562                        CloseChoiceDialog();
   3563                        return;
   3564                      }
   3565 
   3566                      auto lock = Host::GetSettingsLock();
   3567                      SettingsInterface* dsi = GetEditingSettingsInterface();
   3568                      InputManager::CopyConfiguration(dsi, ssi, true, true, IsEditingGameSettings(dsi));
   3569                      SetSettingsChanged(dsi);
   3570                      ShowToast(std::string(), fmt::format(FSUI_FSTR("Input profile '{}' loaded."), title));
   3571                      CloseChoiceDialog();
   3572                    });
   3573 }
   3574 
   3575 void FullscreenUI::DoSaveInputProfile(const std::string& name)
   3576 {
   3577   INISettingsInterface dsi(System::GetInputProfilePath(name));
   3578 
   3579   auto lock = Host::GetSettingsLock();
   3580   SettingsInterface* ssi = GetEditingSettingsInterface();
   3581   InputManager::CopyConfiguration(&dsi, *ssi, true, true, IsEditingGameSettings(ssi));
   3582   if (dsi.Save())
   3583     ShowToast(std::string(), fmt::format(FSUI_FSTR("Input profile '{}' saved."), name));
   3584   else
   3585     ShowToast(std::string(), fmt::format(FSUI_FSTR("Failed to save input profile '{}'."), name));
   3586 }
   3587 
   3588 void FullscreenUI::DoSaveNewInputProfile()
   3589 {
   3590   OpenInputStringDialog(FSUI_ICONSTR(ICON_FA_SAVE, "Save Profile"),
   3591                         FSUI_STR("Enter the name of the input profile you wish to create."), std::string(),
   3592                         FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Create"), [](std::string title) {
   3593                           if (!title.empty())
   3594                             DoSaveInputProfile(title);
   3595                         });
   3596 }
   3597 
   3598 void FullscreenUI::DoSaveInputProfile()
   3599 {
   3600   std::vector<std::string> profiles = InputManager::GetInputProfileNames();
   3601   if (profiles.empty())
   3602   {
   3603     DoSaveNewInputProfile();
   3604     return;
   3605   }
   3606 
   3607   ImGuiFullscreen::ChoiceDialogOptions coptions;
   3608   coptions.reserve(profiles.size() + 1);
   3609   coptions.emplace_back(FSUI_STR("Create New..."), false);
   3610   for (std::string& name : profiles)
   3611     coptions.emplace_back(std::move(name), false);
   3612   OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_SAVE, "Save Profile"), false, std::move(coptions),
   3613                    [](s32 index, const std::string& title, bool checked) {
   3614                      if (index < 0)
   3615                        return;
   3616 
   3617                      if (index > 0)
   3618                      {
   3619                        DoSaveInputProfile(title);
   3620                        CloseChoiceDialog();
   3621                      }
   3622                      else
   3623                      {
   3624                        CloseChoiceDialog();
   3625                        DoSaveNewInputProfile();
   3626                      }
   3627                    });
   3628 }
   3629 
   3630 void FullscreenUI::ResetControllerSettings()
   3631 {
   3632   SettingsInterface* dsi = GetEditingSettingsInterface();
   3633 
   3634   Settings::SetDefaultControllerConfig(*dsi);
   3635   ShowToast(std::string(), FSUI_STR("Controller settings reset to default."));
   3636 }
   3637 
   3638 void FullscreenUI::DrawControllerSettingsPage()
   3639 {
   3640   BeginMenuButtons();
   3641 
   3642   SettingsInterface* bsi = GetEditingSettingsInterface();
   3643   const bool game_settings = IsEditingGameSettings(bsi);
   3644 
   3645   MenuHeading(FSUI_CSTR("Configuration"));
   3646 
   3647   if (IsEditingGameSettings(bsi))
   3648   {
   3649     if (DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_COG, "Per-Game Configuration"),
   3650                           FSUI_CSTR("Uses game-specific settings for controllers for this game."), "ControllerPorts",
   3651                           "UseGameSettingsForController", false, IsEditingGameSettings(bsi), false))
   3652     {
   3653       // did we just enable per-game for the first time?
   3654       if (bsi->GetBoolValue("ControllerPorts", "UseGameSettingsForController", false) &&
   3655           !bsi->GetBoolValue("ControllerPorts", "GameSettingsInitialized", false))
   3656       {
   3657         bsi->SetBoolValue("ControllerPorts", "GameSettingsInitialized", true);
   3658         CopyGlobalControllerSettingsToGame();
   3659       }
   3660     }
   3661   }
   3662 
   3663   if (IsEditingGameSettings(bsi) && !bsi->GetBoolValue("ControllerPorts", "UseGameSettingsForController", false))
   3664   {
   3665     // nothing to edit..
   3666     EndMenuButtons();
   3667     return;
   3668   }
   3669 
   3670   if (IsEditingGameSettings(bsi))
   3671   {
   3672     if (MenuButton(FSUI_ICONSTR(ICON_FA_COPY, "Copy Global Settings"),
   3673                    FSUI_CSTR("Copies the global controller configuration to this game.")))
   3674       CopyGlobalControllerSettingsToGame();
   3675   }
   3676   else
   3677   {
   3678     if (MenuButton(FSUI_ICONSTR(ICON_FA_DUMPSTER_FIRE, "Reset Settings"),
   3679                    FSUI_CSTR("Resets all configuration to defaults (including bindings).")))
   3680     {
   3681       ResetControllerSettings();
   3682     }
   3683   }
   3684 
   3685   if (MenuButton(FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Load Profile"),
   3686                  FSUI_CSTR("Replaces these settings with a previously saved input profile.")))
   3687   {
   3688     DoLoadInputProfile();
   3689   }
   3690   if (MenuButton(FSUI_ICONSTR(ICON_FA_SAVE, "Save Profile"),
   3691                  FSUI_CSTR("Stores the current settings to an input profile.")))
   3692   {
   3693     DoSaveInputProfile();
   3694   }
   3695 
   3696   MenuHeading(FSUI_CSTR("Input Sources"));
   3697 
   3698   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_COG, "Enable SDL Input Source"),
   3699                     FSUI_CSTR("The SDL input source supports most controllers."), "InputSources", "SDL", true, true,
   3700                     false);
   3701   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WIFI, "SDL DualShock 4 / DualSense Enhanced Mode"),
   3702                     FSUI_CSTR("Provides vibration and LED control support over Bluetooth."), "InputSources",
   3703                     "SDLControllerEnhancedMode", false, bsi->GetBoolValue("InputSources", "SDL", true), false);
   3704   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIGHTBULB, "SDL DualSense Player LED"),
   3705                     FSUI_CSTR("Enable/Disable the Player LED on DualSense controllers."), "InputSources",
   3706                     "SDLPS5PlayerLED", false, bsi->GetBoolValue("InputSources", "SDLControllerEnhancedMode", true),
   3707                     false);
   3708 #ifdef _WIN32
   3709   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_COG, "Enable XInput Input Source"),
   3710                     FSUI_CSTR("The XInput source provides support for XBox 360/XBox One/XBox Series controllers."),
   3711                     "InputSources", "XInput", false);
   3712 #endif
   3713 
   3714   MenuHeading(FSUI_CSTR("Multitap"));
   3715   DrawEnumSetting(bsi, FSUI_ICONSTR(ICON_FA_PLUS_SQUARE, "Multitap Mode"),
   3716                   FSUI_CSTR("Enables an additional three controller slots on each port. Not supported in all games."),
   3717                   "ControllerPorts", "MultitapMode", Settings::DEFAULT_MULTITAP_MODE, &Settings::ParseMultitapModeName,
   3718                   &Settings::GetMultitapModeName, &Settings::GetMultitapModeDisplayName, MultitapMode::Count);
   3719 
   3720   // load mtap settings
   3721   MultitapMode mtap_mode = g_settings.multitap_mode;
   3722   if (IsEditingGameSettings(bsi))
   3723   {
   3724     mtap_mode = Settings::ParseMultitapModeName(bsi->GetTinyStringValue("ControllerPorts", "MultitapMode", "").c_str())
   3725                   .value_or(g_settings.multitap_mode);
   3726   }
   3727   const std::array<bool, 2> mtap_enabled = {
   3728     {(mtap_mode == MultitapMode::Port1Only || mtap_mode == MultitapMode::BothPorts),
   3729      (mtap_mode == MultitapMode::Port2Only || mtap_mode == MultitapMode::BothPorts)}};
   3730 
   3731   // we reorder things a little to make it look less silly for mtap
   3732   static constexpr const std::array<char, 4> mtap_slot_names = {{'A', 'B', 'C', 'D'}};
   3733   static constexpr const std::array<u32, NUM_CONTROLLER_AND_CARD_PORTS> mtap_port_order = {{0, 2, 3, 4, 1, 5, 6, 7}};
   3734 
   3735   // create the ports
   3736   for (u32 global_slot : mtap_port_order)
   3737   {
   3738     const auto [mtap_port, mtap_slot] = Controller::ConvertPadToPortAndSlot(global_slot);
   3739     const bool is_mtap_port = Controller::PortAndSlotIsMultitap(mtap_port, mtap_slot);
   3740     if (is_mtap_port && !mtap_enabled[mtap_port])
   3741       continue;
   3742 
   3743     if (mtap_enabled[mtap_port])
   3744     {
   3745       MenuHeading(TinyString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_PLUG, "Controller Port {}{}")),
   3746                                           mtap_port + 1, mtap_slot_names[mtap_slot]));
   3747     }
   3748     else
   3749     {
   3750       MenuHeading(
   3751         TinyString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_PLUG, "Controller Port {}")), mtap_port + 1));
   3752     }
   3753 
   3754     const TinyString section = TinyString::from_format("Pad{}", global_slot + 1);
   3755     const TinyString type =
   3756       bsi->GetTinyStringValue(section.c_str(), "Type", Controller::GetDefaultPadType(global_slot));
   3757     const Controller::ControllerInfo* ci = Controller::GetControllerInfo(type);
   3758     if (MenuButton(TinyString::from_format("{}##type{}", FSUI_ICONSTR(ICON_FA_GAMEPAD, "Controller Type"), global_slot),
   3759                    ci ? Host::TranslateToCString("ControllerType", ci->display_name) : FSUI_CSTR("Unknown")))
   3760     {
   3761       std::vector<std::pair<std::string, std::string>> raw_options(Controller::GetControllerTypeNames());
   3762       ImGuiFullscreen::ChoiceDialogOptions options;
   3763       options.reserve(raw_options.size());
   3764       for (auto& it : raw_options)
   3765       {
   3766         options.emplace_back(std::move(it.second), type == it.first);
   3767       }
   3768       OpenChoiceDialog(TinyString::from_format(FSUI_FSTR("Port {} Controller Type"), global_slot + 1), false,
   3769                        std::move(options),
   3770                        [game_settings, section,
   3771                         raw_options = std::move(raw_options)](s32 index, const std::string& title, bool checked) {
   3772                          if (index < 0)
   3773                            return;
   3774 
   3775                          auto lock = Host::GetSettingsLock();
   3776                          SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   3777                          bsi->SetStringValue(section.c_str(), "Type", raw_options[index].first.c_str());
   3778                          SetSettingsChanged(bsi);
   3779                          CloseChoiceDialog();
   3780                        });
   3781     }
   3782 
   3783     if (!ci || ci->bindings.empty())
   3784       continue;
   3785 
   3786     if (MenuButton(FSUI_ICONSTR(ICON_FA_MAGIC, "Automatic Mapping"),
   3787                    FSUI_CSTR("Attempts to map the selected port to a chosen controller.")))
   3788     {
   3789       StartAutomaticBinding(global_slot);
   3790     }
   3791 
   3792     for (const Controller::ControllerBindingInfo& bi : ci->bindings)
   3793     {
   3794       DrawInputBindingButton(bsi, bi.type, section.c_str(), bi.name,
   3795                              Host::TranslateToCString(ci->name, bi.display_name), bi.icon_name, true);
   3796     }
   3797 
   3798     if (mtap_enabled[mtap_port])
   3799     {
   3800       MenuHeading(SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_MICROCHIP, "Controller Port {}{} Macros")),
   3801                                            mtap_port + 1, mtap_slot_names[mtap_slot]));
   3802     }
   3803     else
   3804     {
   3805       MenuHeading(SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_MICROCHIP, "Controller Port {} Macros")),
   3806                                            mtap_port + 1));
   3807     }
   3808 
   3809     for (u32 macro_index = 0; macro_index < InputManager::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_index++)
   3810     {
   3811       DrawInputBindingButton(bsi, InputBindingInfo::Type::Macro, section.c_str(),
   3812                              TinyString::from_format("Macro{}", macro_index + 1),
   3813                              TinyString::from_format(FSUI_FSTR("Macro {} Trigger"), macro_index + 1), nullptr);
   3814       DrawToggleSetting(bsi,
   3815                         TinyString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Macro {} Press To Toggle")),
   3816                                                 macro_index + 1),
   3817                         nullptr, section.c_str(), TinyString::from_format("Macro{}Toggle", macro_index + 1), false,
   3818                         true, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   3819 
   3820       SmallString binds_string =
   3821         bsi->GetSmallStringValue(section.c_str(), fmt::format("Macro{}Binds", macro_index + 1).c_str());
   3822       TinyString pretty_binds_string;
   3823       if (!binds_string.empty())
   3824       {
   3825         for (const std::string_view& bind : StringUtil::SplitString(binds_string, '&', true))
   3826         {
   3827           const char* dispname = nullptr;
   3828           for (const Controller::ControllerBindingInfo& bi : ci->bindings)
   3829           {
   3830             if (bind == bi.name)
   3831             {
   3832               dispname = bi.icon_name ? bi.icon_name : Host::TranslateToCString(ci->name, bi.display_name);
   3833               break;
   3834             }
   3835           }
   3836           pretty_binds_string.append_format("{}{}", pretty_binds_string.empty() ? "" : " ", dispname);
   3837         }
   3838       }
   3839       if (MenuButtonWithValue(
   3840             TinyString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_KEYBOARD, "Macro {} Buttons")), macro_index + 1),
   3841             nullptr, pretty_binds_string.empty() ? FSUI_CSTR("-") : pretty_binds_string.c_str(), true,
   3842             LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   3843       {
   3844         std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));
   3845         ImGuiFullscreen::ChoiceDialogOptions options;
   3846         for (const Controller::ControllerBindingInfo& bi : ci->bindings)
   3847         {
   3848           if (bi.type != InputBindingInfo::Type::Button && bi.type != InputBindingInfo::Type::Axis &&
   3849               bi.type != InputBindingInfo::Type::HalfAxis)
   3850           {
   3851             continue;
   3852           }
   3853           options.emplace_back(Host::TranslateToString(ci->name, bi.display_name),
   3854                                std::any_of(buttons_split.begin(), buttons_split.end(),
   3855                                            [bi](const std::string_view& it) { return (it == bi.name); }));
   3856         }
   3857 
   3858         OpenChoiceDialog(
   3859           TinyString::from_format(FSUI_FSTR("Select Macro {} Binds"), macro_index + 1), true, std::move(options),
   3860           [game_settings, section, macro_index, ci](s32 index, const std::string& title, bool checked) {
   3861             // convert display name back to bind name
   3862             std::string_view to_modify;
   3863             for (const Controller::ControllerBindingInfo& bi : ci->bindings)
   3864             {
   3865               if (title == Host::TranslateToStringView(ci->name, bi.display_name))
   3866               {
   3867                 to_modify = bi.name;
   3868                 break;
   3869               }
   3870             }
   3871             if (to_modify.empty())
   3872             {
   3873               // wtf?
   3874               return;
   3875             }
   3876 
   3877             auto lock = Host::GetSettingsLock();
   3878             SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   3879             const TinyString key = TinyString::from_format("Macro{}Binds", macro_index + 1);
   3880 
   3881             std::string binds_string = bsi->GetStringValue(section.c_str(), key.c_str());
   3882             std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));
   3883             auto it = std::find(buttons_split.begin(), buttons_split.end(), to_modify);
   3884             if (checked)
   3885             {
   3886               if (it == buttons_split.end())
   3887                 buttons_split.push_back(to_modify);
   3888             }
   3889             else
   3890             {
   3891               if (it != buttons_split.end())
   3892                 buttons_split.erase(it);
   3893             }
   3894 
   3895             binds_string = StringUtil::JoinString(buttons_split.begin(), buttons_split.end(), " & ");
   3896             if (binds_string.empty())
   3897               bsi->DeleteValue(section.c_str(), key.c_str());
   3898             else
   3899               bsi->SetStringValue(section.c_str(), key.c_str(), binds_string.c_str());
   3900           });
   3901       }
   3902 
   3903       const TinyString freq_key = TinyString::from_format("Macro{}Frequency", macro_index + 1);
   3904       const SmallString freq_title =
   3905         SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Macro {} Frequency")), macro_index + 1);
   3906       s32 frequency = bsi->GetIntValue(section.c_str(), freq_key.c_str(), 0);
   3907       SmallString freq_summary;
   3908       if (frequency == 0)
   3909         freq_summary = FSUI_VSTR("Disabled");
   3910       else
   3911         freq_summary.format(FSUI_FSTR("{} Frames"), frequency);
   3912       if (MenuButtonWithValue(freq_title, nullptr, freq_summary, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   3913         ImGui::OpenPopup(freq_title);
   3914 
   3915       ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f));
   3916 
   3917       ImGui::PushFont(g_large_font);
   3918       ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   3919       ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   3920                                                                   ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   3921       ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   3922 
   3923       if (ImGui::BeginPopupModal(freq_title, nullptr,
   3924                                  ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   3925       {
   3926         ImGui::SetNextItemWidth(LayoutScale(450.0f));
   3927         if (ImGui::SliderInt("##value", &frequency, 0, 60, FSUI_CSTR("Toggle every %d frames"),
   3928                              ImGuiSliderFlags_NoInput))
   3929         {
   3930           if (frequency == 0)
   3931             bsi->DeleteValue(section.c_str(), freq_key.c_str());
   3932           else
   3933             bsi->SetIntValue(section.c_str(), freq_key.c_str(), frequency);
   3934         }
   3935 
   3936         BeginMenuButtons();
   3937         if (MenuButton(FSUI_CSTR("OK"), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   3938           ImGui::CloseCurrentPopup();
   3939         EndMenuButtons();
   3940 
   3941         ImGui::EndPopup();
   3942       }
   3943 
   3944       ImGui::PopStyleVar(3);
   3945       ImGui::PopFont();
   3946     }
   3947 
   3948     if (!ci->settings.empty())
   3949     {
   3950       if (mtap_enabled[mtap_port])
   3951       {
   3952         MenuHeading(
   3953           SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_SLIDERS_H, "Controller Port {}{} Settings")),
   3954                                    mtap_port + 1, mtap_slot_names[mtap_slot]));
   3955       }
   3956       else
   3957       {
   3958         MenuHeading(SmallString::from_format(
   3959           fmt::runtime(FSUI_ICONSTR(ICON_FA_SLIDERS_H, "Controller Port {} Settings")), mtap_port + 1));
   3960       }
   3961 
   3962       for (const SettingInfo& si : ci->settings)
   3963       {
   3964         TinyString title;
   3965         title.format(ICON_FA_COG "{}", Host::TranslateToStringView(ci->name, si.display_name));
   3966         const char* description = Host::TranslateToCString(ci->name, si.description);
   3967         switch (si.type)
   3968         {
   3969           case SettingInfo::Type::Boolean:
   3970             DrawToggleSetting(bsi, title, description, section.c_str(), si.name, si.BooleanDefaultValue(), true, false);
   3971             break;
   3972           case SettingInfo::Type::Integer:
   3973             DrawIntRangeSetting(bsi, title, description, section.c_str(), si.name, si.IntegerDefaultValue(),
   3974                                 si.IntegerMinValue(), si.IntegerMaxValue(), si.format, true);
   3975             break;
   3976           case SettingInfo::Type::IntegerList:
   3977             DrawIntListSetting(bsi, title, description, section.c_str(), si.name, si.IntegerDefaultValue(), si.options,
   3978                                0, true, si.IntegerMinValue(), true, LAYOUT_MENU_BUTTON_HEIGHT, g_large_font,
   3979                                g_medium_font, ci->name);
   3980             break;
   3981           case SettingInfo::Type::Float:
   3982             DrawFloatSpinBoxSetting(bsi, title, description, section.c_str(), si.name, si.FloatDefaultValue(),
   3983                                     si.FloatMinValue(), si.FloatMaxValue(), si.FloatStepValue(), si.multiplier,
   3984                                     si.format, true);
   3985             break;
   3986           default:
   3987             break;
   3988         }
   3989       }
   3990     }
   3991   }
   3992 
   3993   EndMenuButtons();
   3994 }
   3995 
   3996 void FullscreenUI::DrawHotkeySettingsPage()
   3997 {
   3998   SettingsInterface* bsi = GetEditingSettingsInterface();
   3999 
   4000   BeginMenuButtons();
   4001 
   4002   const HotkeyInfo* last_category = nullptr;
   4003   for (const HotkeyInfo* hotkey : s_hotkey_list_cache)
   4004   {
   4005     if (!last_category || std::strcmp(hotkey->category, last_category->category) != 0)
   4006     {
   4007       MenuHeading(Host::TranslateToCString("Hotkeys", hotkey->category));
   4008       last_category = hotkey;
   4009     }
   4010 
   4011     DrawInputBindingButton(bsi, InputBindingInfo::Type::Button, "Hotkeys", hotkey->name,
   4012                            Host::TranslateToCString("Hotkeys", hotkey->display_name), nullptr, false);
   4013   }
   4014 
   4015   EndMenuButtons();
   4016 }
   4017 
   4018 void FullscreenUI::DrawMemoryCardSettingsPage()
   4019 {
   4020   static constexpr const std::array type_keys = {"Card1Type", "Card2Type"};
   4021   static constexpr const std::array path_keys = {"Card1Path", "Card2Path"};
   4022 
   4023   SettingsInterface* bsi = GetEditingSettingsInterface();
   4024   const bool game_settings = IsEditingGameSettings(bsi);
   4025 
   4026   BeginMenuButtons();
   4027 
   4028   MenuHeading(FSUI_CSTR("Settings and Operations"));
   4029   DrawFolderSetting(bsi, FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Memory Card Directory"), "MemoryCards", "Directory",
   4030                     EmuFolders::MemoryCards);
   4031 
   4032   if (!game_settings && MenuButton(FSUI_ICONSTR(ICON_FA_MAGIC, "Reset Memory Card Directory"),
   4033                                    FSUI_CSTR("Resets memory card directory to default (user directory).")))
   4034   {
   4035     bsi->SetStringValue("MemoryCards", "Directory", "memcards");
   4036     SetSettingsChanged(bsi);
   4037   }
   4038 
   4039   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_SEARCH, "Use Single Card For Multi-Disc Games"),
   4040                     FSUI_CSTR("When playing a multi-disc game and using per-game (title) memory cards, "
   4041                               "use a single memory card for all discs."),
   4042                     "MemoryCards", "UsePlaylistTitle", true);
   4043 
   4044   for (u32 i = 0; i < 2; i++)
   4045   {
   4046     MenuHeading(TinyString::from_format(FSUI_FSTR("Memory Card Port {}"), i + 1));
   4047 
   4048     const MemoryCardType default_type =
   4049       (i == 0) ? Settings::DEFAULT_MEMORY_CARD_1_TYPE : Settings::DEFAULT_MEMORY_CARD_2_TYPE;
   4050     DrawEnumSetting(
   4051       bsi, TinyString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_SD_CARD, "Memory Card {} Type")), i + 1),
   4052       SmallString::from_format(FSUI_FSTR("Sets which sort of memory card image will be used for slot {}."), i + 1),
   4053       "MemoryCards", type_keys[i], default_type, &Settings::ParseMemoryCardTypeName, &Settings::GetMemoryCardTypeName,
   4054       &Settings::GetMemoryCardTypeDisplayName, MemoryCardType::Count);
   4055 
   4056     const MemoryCardType effective_type =
   4057       Settings::ParseMemoryCardTypeName(
   4058         GetEffectiveTinyStringSetting(bsi, "MemoryCards", type_keys[i], Settings::GetMemoryCardTypeName(default_type))
   4059           .c_str())
   4060         .value_or(default_type);
   4061     const bool is_shared = (effective_type == MemoryCardType::Shared);
   4062     std::optional<SmallString> path_value(bsi->GetOptionalSmallStringValue(
   4063       "MemoryCards", path_keys[i],
   4064       IsEditingGameSettings(bsi) ? std::nullopt :
   4065                                    std::optional<const char*>((i == 0) ? "shared_card_1.mcd" : "shared_card_2.mcd")));
   4066 
   4067     TinyString title;
   4068     title.format("{}##card_name_{}", FSUI_ICONSTR(ICON_FA_FILE, "Shared Card Name"), i);
   4069     if (MenuButtonWithValue(title,
   4070                             FSUI_CSTR("The selected memory card image will be used in shared mode for this slot."),
   4071                             path_value.has_value() ? path_value->c_str() : FSUI_CSTR("Use Global Setting"), is_shared))
   4072     {
   4073       ImGuiFullscreen::ChoiceDialogOptions options;
   4074       std::vector<std::string> names;
   4075       if (IsEditingGameSettings(bsi))
   4076         options.emplace_back("Use Global Setting", !path_value.has_value());
   4077       if (path_value.has_value() && !path_value->empty())
   4078       {
   4079         options.emplace_back(fmt::format("{} (Current)", path_value.value()), true);
   4080         names.emplace_back(path_value.value().view());
   4081       }
   4082 
   4083       FileSystem::FindResultsArray results;
   4084       FileSystem::FindFiles(EmuFolders::MemoryCards.c_str(), "*.mcd",
   4085                             FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS,
   4086                             &results);
   4087       for (FILESYSTEM_FIND_DATA& ffd : results)
   4088       {
   4089         const bool selected = (path_value.has_value() && path_value.value() == ffd.FileName);
   4090         options.emplace_back(std::move(ffd.FileName), selected);
   4091       }
   4092 
   4093       OpenChoiceDialog(
   4094         title, false, std::move(options),
   4095         [game_settings = IsEditingGameSettings(bsi), i](s32 index, const std::string& title, bool checked) {
   4096           if (index < 0)
   4097             return;
   4098 
   4099           auto lock = Host::GetSettingsLock();
   4100           SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   4101           if (game_settings && index == 0)
   4102           {
   4103             bsi->DeleteValue("MemoryCards", path_keys[i]);
   4104           }
   4105           else
   4106           {
   4107             if (game_settings)
   4108               index--;
   4109             bsi->SetStringValue("MemoryCards", path_keys[i], title.c_str());
   4110           }
   4111           SetSettingsChanged(bsi);
   4112           CloseChoiceDialog();
   4113         });
   4114     }
   4115   }
   4116 
   4117   EndMenuButtons();
   4118 }
   4119 
   4120 void FullscreenUI::DrawDisplaySettingsPage()
   4121 {
   4122   static constexpr const std::array resolution_scales = {
   4123     FSUI_NSTR("Automatic based on window size"),
   4124     FSUI_NSTR("1x"),
   4125     FSUI_NSTR("2x"),
   4126     FSUI_NSTR("3x (for 720p)"),
   4127     FSUI_NSTR("4x"),
   4128     FSUI_NSTR("5x (for 1080p)"),
   4129     FSUI_NSTR("6x (for 1440p)"),
   4130     FSUI_NSTR("7x"),
   4131     FSUI_NSTR("8x"),
   4132     FSUI_NSTR("9x (for 4K)"),
   4133     FSUI_NSTR("10x"),
   4134     FSUI_NSTR("11x"),
   4135     FSUI_NSTR("12x"),
   4136     FSUI_NSTR("13x"),
   4137     FSUI_NSTR("14x"),
   4138     FSUI_NSTR("15x"),
   4139     FSUI_NSTR("16x"),
   4140   };
   4141 
   4142   SettingsInterface* bsi = GetEditingSettingsInterface();
   4143   const bool game_settings = IsEditingGameSettings(bsi);
   4144   const u32 resolution_scale = GetEffectiveUIntSetting(bsi, "GPU", "ResolutionScale", 1);
   4145 
   4146   BeginMenuButtons();
   4147 
   4148   MenuHeading(FSUI_CSTR("Device Settings"));
   4149 
   4150   DrawEnumSetting(bsi, FSUI_CSTR("GPU Renderer"),
   4151                   FSUI_CSTR("Chooses the backend to use for rendering the console/game visuals."), "GPU", "Renderer",
   4152                   Settings::DEFAULT_GPU_RENDERER, &Settings::ParseRendererName, &Settings::GetRendererName,
   4153                   &Settings::GetRendererDisplayName, GPURenderer::Count);
   4154 
   4155   const GPURenderer renderer =
   4156     Settings::ParseRendererName(
   4157       GetEffectiveTinyStringSetting(bsi, "GPU", "Renderer", Settings::GetRendererName(Settings::DEFAULT_GPU_RENDERER))
   4158         .c_str())
   4159       .value_or(Settings::DEFAULT_GPU_RENDERER);
   4160   const bool is_hardware = (renderer != GPURenderer::Software);
   4161 
   4162   std::optional<SmallString> current_adapter =
   4163     bsi->GetOptionalSmallStringValue("GPU", "Adapter", game_settings ? std::nullopt : std::optional<const char*>(""));
   4164 
   4165   if (MenuButtonWithValue(FSUI_CSTR("GPU Adapter"), FSUI_CSTR("Selects the GPU to use for rendering."),
   4166                           current_adapter.has_value() ?
   4167                             (current_adapter->empty() ? FSUI_CSTR("Default") : current_adapter->c_str()) :
   4168                             FSUI_CSTR("Use Global Setting")))
   4169   {
   4170     ImGuiFullscreen::ChoiceDialogOptions options;
   4171     options.reserve(s_graphics_adapter_list_cache.size() + 2);
   4172     if (game_settings)
   4173       options.emplace_back(FSUI_STR("Use Global Setting"), !current_adapter.has_value());
   4174     options.emplace_back(FSUI_STR("Default"), current_adapter.has_value() && current_adapter->empty());
   4175     for (const GPUDevice::AdapterInfo& adapter : s_graphics_adapter_list_cache)
   4176     {
   4177       const bool checked = (current_adapter.has_value() && current_adapter.value() == adapter.name);
   4178       options.emplace_back(adapter.name, checked);
   4179     }
   4180 
   4181     auto callback = [game_settings](s32 index, const std::string& title, bool checked) {
   4182       if (index < 0)
   4183         return;
   4184 
   4185       const char* value;
   4186       if (game_settings && index == 0)
   4187         value = nullptr;
   4188       else if ((!game_settings && index == 0) || (game_settings && index == 1))
   4189         value = "";
   4190       else
   4191         value = title.c_str();
   4192 
   4193       SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   4194       if (!value)
   4195         bsi->DeleteValue("GPU", "Adapter");
   4196       else
   4197         bsi->SetStringValue("GPU", "Adapter", value);
   4198       SetSettingsChanged(bsi);
   4199       ShowToast(std::string(), FSUI_STR("GPU adapter will be applied after restarting."), 10.0f);
   4200       CloseChoiceDialog();
   4201     };
   4202     OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_TV, "GPU Adapter"), false, std::move(options), std::move(callback));
   4203   }
   4204 
   4205   const bool true_color_enabled = (is_hardware && GetEffectiveBoolSetting(bsi, "GPU", "TrueColor", false));
   4206   const bool pgxp_enabled = (is_hardware && GetEffectiveBoolSetting(bsi, "GPU", "PGXPEnable", false));
   4207   const bool texture_correction_enabled =
   4208     (pgxp_enabled && GetEffectiveBoolSetting(bsi, "GPU", "PGXPTextureCorrection", true));
   4209 
   4210   MenuHeading(FSUI_CSTR("Rendering"));
   4211 
   4212   if (is_hardware)
   4213   {
   4214     DrawIntListSetting(
   4215       bsi, FSUI_CSTR("Internal Resolution"),
   4216       FSUI_CSTR("Scales internal VRAM resolution by the specified multiplier. Some games require 1x VRAM resolution."),
   4217       "GPU", "ResolutionScale", 1, resolution_scales.data(), resolution_scales.size(), true, 0);
   4218 
   4219     DrawEnumSetting(bsi, FSUI_CSTR("Downsampling"),
   4220                     FSUI_CSTR("Downsamples the rendered image prior to displaying it. Can improve "
   4221                               "overall image quality in mixed 2D/3D games."),
   4222                     "GPU", "DownsampleMode", Settings::DEFAULT_GPU_DOWNSAMPLE_MODE, &Settings::ParseDownsampleModeName,
   4223                     &Settings::GetDownsampleModeName, &Settings::GetDownsampleModeDisplayName, GPUDownsampleMode::Count,
   4224                     (renderer != GPURenderer::Software));
   4225     if (Settings::ParseDownsampleModeName(
   4226           GetEffectiveTinyStringSetting(bsi, "GPU", "DownsampleMode",
   4227                                         Settings::GetDownsampleModeName(Settings::DEFAULT_GPU_DOWNSAMPLE_MODE))
   4228             .c_str())
   4229           .value_or(Settings::DEFAULT_GPU_DOWNSAMPLE_MODE) == GPUDownsampleMode::Box)
   4230     {
   4231       DrawIntRangeSetting(bsi, FSUI_CSTR("Downsampling Display Scale"),
   4232                           FSUI_CSTR("Selects the resolution scale that will be applied to the final image. 1x will "
   4233                                     "downsample to the original console resolution."),
   4234                           "GPU", "DownsampleScale", 1, 1, GPU::MAX_RESOLUTION_SCALE, "%dx");
   4235     }
   4236 
   4237     DrawEnumSetting(bsi, FSUI_CSTR("Texture Filtering"),
   4238                     FSUI_CSTR("Smooths out the blockiness of magnified textures on 3D objects."), "GPU",
   4239                     "TextureFilter", Settings::DEFAULT_GPU_TEXTURE_FILTER, &Settings::ParseTextureFilterName,
   4240                     &Settings::GetTextureFilterName, &Settings::GetTextureFilterDisplayName, GPUTextureFilter::Count);
   4241 
   4242     DrawEnumSetting(bsi, FSUI_CSTR("Sprite Texture Filtering"),
   4243                     FSUI_CSTR("Smooths out the blockiness of magnified textures on 2D objects."), "GPU",
   4244                     "SpriteTextureFilter", Settings::DEFAULT_GPU_TEXTURE_FILTER, &Settings::ParseTextureFilterName,
   4245                     &Settings::GetTextureFilterName, &Settings::GetTextureFilterDisplayName, GPUTextureFilter::Count);
   4246   }
   4247 
   4248   DrawEnumSetting(bsi, FSUI_CSTR("Aspect Ratio"),
   4249                   FSUI_CSTR("Changes the aspect ratio used to display the console's output to the screen."), "Display",
   4250                   "AspectRatio", Settings::DEFAULT_DISPLAY_ASPECT_RATIO, &Settings::ParseDisplayAspectRatio,
   4251                   &Settings::GetDisplayAspectRatioName, &Settings::GetDisplayAspectRatioDisplayName,
   4252                   DisplayAspectRatio::Count);
   4253 
   4254   DrawEnumSetting(
   4255     bsi, FSUI_CSTR("Deinterlacing Mode"),
   4256     FSUI_CSTR(
   4257       "Determines which algorithm is used to convert interlaced frames to progressive for display on your system."),
   4258     "Display", "DeinterlacingMode", Settings::DEFAULT_DISPLAY_DEINTERLACING_MODE,
   4259     &Settings::ParseDisplayDeinterlacingMode, &Settings::GetDisplayDeinterlacingModeName,
   4260     &Settings::GetDisplayDeinterlacingModeDisplayName, DisplayDeinterlacingMode::Count);
   4261 
   4262   DrawEnumSetting(bsi, FSUI_CSTR("Crop Mode"),
   4263                   FSUI_CSTR("Determines how much of the area typically not visible on a consumer TV set to crop/hide."),
   4264                   "Display", "CropMode", Settings::DEFAULT_DISPLAY_CROP_MODE, &Settings::ParseDisplayCropMode,
   4265                   &Settings::GetDisplayCropModeName, &Settings::GetDisplayCropModeDisplayName, DisplayCropMode::Count);
   4266 
   4267   DrawEnumSetting(
   4268     bsi, FSUI_CSTR("Scaling"),
   4269     FSUI_CSTR("Determines how the emulated console's output is upscaled or downscaled to your monitor's resolution."),
   4270     "Display", "Scaling", Settings::DEFAULT_DISPLAY_SCALING, &Settings::ParseDisplayScaling,
   4271     &Settings::GetDisplayScalingName, &Settings::GetDisplayScalingDisplayName, DisplayScalingMode::Count);
   4272 
   4273   if (is_hardware)
   4274   {
   4275     DrawToggleSetting(bsi, FSUI_CSTR("True Color Rendering"),
   4276                       FSUI_CSTR("Disables dithering and uses the full 8 bits per channel of color information."), "GPU",
   4277                       "TrueColor", true);
   4278   }
   4279 
   4280   DrawToggleSetting(bsi, FSUI_CSTR("Widescreen Rendering"),
   4281                     FSUI_CSTR("Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games."),
   4282                     "GPU", "WidescreenHack", false);
   4283 
   4284   if (is_hardware)
   4285   {
   4286     DrawToggleSetting(
   4287       bsi, FSUI_CSTR("PGXP Geometry Correction"),
   4288       FSUI_CSTR("Reduces \"wobbly\" polygons by attempting to preserve the fractional component through memory "
   4289                 "transfers."),
   4290       "GPU", "PGXPEnable", false);
   4291 
   4292     DrawToggleSetting(bsi, FSUI_CSTR("PGXP Depth Buffer"),
   4293                       FSUI_CSTR("Reduces polygon Z-fighting through depth testing. Low compatibility with games."),
   4294                       "GPU", "PGXPDepthBuffer", false, pgxp_enabled && texture_correction_enabled);
   4295   }
   4296 
   4297   DrawToggleSetting(
   4298     bsi, FSUI_CSTR("Force 4:3 For FMVs"),
   4299     FSUI_CSTR("Switches back to 4:3 display aspect ratio when displaying 24-bit content, usually FMVs."), "Display",
   4300     "Force4_3For24Bit", false);
   4301 
   4302   DrawToggleSetting(bsi, FSUI_CSTR("FMV Chroma Smoothing"),
   4303                     FSUI_CSTR("Smooths out blockyness between colour transitions in 24-bit content, usually FMVs."),
   4304                     "GPU", "ChromaSmoothing24Bit", false);
   4305 
   4306   DrawToggleSetting(
   4307     bsi, FSUI_CSTR("Disable Interlacing"),
   4308     FSUI_CSTR("Disables interlaced rendering and display in the GPU. Some games can render in 480p this way, "
   4309               "but others will break."),
   4310     "GPU", "DisableInterlacing", true);
   4311 
   4312   DrawToggleSetting(
   4313     bsi, FSUI_CSTR("Force NTSC Timings"),
   4314     FSUI_CSTR("Forces PAL games to run at NTSC timings, i.e. 60hz. Some PAL games will run at their \"normal\" "
   4315               "speeds, while others will break."),
   4316     "GPU", "ForceNTSCTimings", false);
   4317 
   4318   MenuHeading(FSUI_CSTR("Advanced"));
   4319 
   4320   std::optional<SmallString> strvalue = bsi->GetOptionalSmallStringValue(
   4321     "GPU", "FullscreenMode", game_settings ? std::nullopt : std::optional<const char*>(""));
   4322 
   4323   if (MenuButtonWithValue(
   4324         FSUI_CSTR("Fullscreen Resolution"), FSUI_CSTR("Selects the resolution to use in fullscreen modes."),
   4325         strvalue.has_value() ? (strvalue->empty() ? FSUI_CSTR("Borderless Fullscreen") : strvalue->c_str()) :
   4326                                FSUI_CSTR("Use Global Setting")))
   4327   {
   4328     const GPUDevice::AdapterInfo* selected_adapter = nullptr;
   4329     if (current_adapter.has_value())
   4330     {
   4331       for (const GPUDevice::AdapterInfo& ai : s_graphics_adapter_list_cache)
   4332       {
   4333         if (ai.name == current_adapter->view())
   4334         {
   4335           selected_adapter = &ai;
   4336           break;
   4337         }
   4338       }
   4339     }
   4340     else
   4341     {
   4342       if (!s_graphics_adapter_list_cache.empty())
   4343         selected_adapter = &s_graphics_adapter_list_cache.front();
   4344     }
   4345 
   4346     ImGuiFullscreen::ChoiceDialogOptions options;
   4347     options.reserve((selected_adapter ? selected_adapter->fullscreen_modes.size() : 0) + 2);
   4348     if (game_settings)
   4349       options.emplace_back(FSUI_STR("Use Global Setting"), !strvalue.has_value());
   4350     options.emplace_back(FSUI_STR("Borderless Fullscreen"), strvalue.has_value() && strvalue->empty());
   4351     if (selected_adapter)
   4352     {
   4353       for (const std::string& mode : selected_adapter->fullscreen_modes)
   4354       {
   4355         const bool checked = (strvalue.has_value() && strvalue.value() == mode);
   4356         options.emplace_back(mode, checked);
   4357       }
   4358     }
   4359 
   4360     auto callback = [game_settings](s32 index, const std::string& title, bool checked) {
   4361       if (index < 0)
   4362         return;
   4363 
   4364       const char* value;
   4365       if (game_settings && index == 0)
   4366         value = nullptr;
   4367       else if ((!game_settings && index == 0) || (game_settings && index == 1))
   4368         value = "";
   4369       else
   4370         value = title.c_str();
   4371 
   4372       SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
   4373       if (!value)
   4374         bsi->DeleteValue("GPU", "FullscreenMode");
   4375       else
   4376         bsi->SetStringValue("GPU", "FullscreenMode", value);
   4377       SetSettingsChanged(bsi);
   4378       ShowToast(std::string(), FSUI_STR("Resolution change will be applied after restarting."), 10.0f);
   4379       CloseChoiceDialog();
   4380     };
   4381     OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_TV, "Fullscreen Resolution"), false, std::move(options), std::move(callback));
   4382   }
   4383 
   4384   DrawEnumSetting(bsi, FSUI_CSTR("Screen Position"),
   4385                   FSUI_CSTR("Determines the position on the screen when black borders must be added."), "Display",
   4386                   "Alignment", Settings::DEFAULT_DISPLAY_ALIGNMENT, &Settings::ParseDisplayAlignment,
   4387                   &Settings::GetDisplayAlignmentName, &Settings::GetDisplayAlignmentDisplayName,
   4388                   DisplayAlignment::Count);
   4389 
   4390   DrawEnumSetting(bsi, FSUI_CSTR("Screen Rotation"), FSUI_CSTR("Determines the rotation of the simulated TV screen."),
   4391                   "Display", "Rotation", Settings::DEFAULT_DISPLAY_ROTATION, &Settings::ParseDisplayRotation,
   4392                   &Settings::GetDisplayRotationName, &Settings::GetDisplayRotationDisplayName, DisplayRotation::Count);
   4393 
   4394   if (is_hardware)
   4395   {
   4396     DrawEnumSetting(bsi, FSUI_CSTR("Line Detection"),
   4397                     FSUI_CSTR("Attempts to detect one pixel high/wide lines that rely on non-upscaled rasterization "
   4398                               "behavior, filling in gaps introduced by upscaling."),
   4399                     "GPU", "LineDetectMode", Settings::DEFAULT_GPU_LINE_DETECT_MODE, &Settings::ParseLineDetectModeName,
   4400                     &Settings::GetLineDetectModeName, &Settings::GetLineDetectModeDisplayName, GPULineDetectMode::Count,
   4401                     resolution_scale > 1);
   4402 
   4403     DrawToggleSetting(
   4404       bsi, FSUI_CSTR("True Color Debanding"),
   4405       FSUI_CSTR("Applies modern dithering techniques to further smooth out gradients when true color is enabled."),
   4406       "GPU", "Debanding", false, true_color_enabled);
   4407 
   4408     DrawToggleSetting(
   4409       bsi, FSUI_CSTR("Scaled Dithering"),
   4410       FSUI_CSTR("Scales the dithering pattern with the internal rendering resolution, making it less noticeable. "
   4411                 "Usually safe to enable."),
   4412       "GPU", "ScaledDithering", true, !true_color_enabled);
   4413 
   4414     DrawToggleSetting(bsi, FSUI_CSTR("Accurate Blending"),
   4415                       FSUI_CSTR("Forces blending to be done in the shader at 16-bit precision, when not using true "
   4416                                 "color. Non-trivial performance impact, and unnecessary for most games."),
   4417                       "GPU", "AccurateBlending", false, !true_color_enabled);
   4418 
   4419     const GPUTextureFilter texture_filtering =
   4420       Settings::ParseTextureFilterName(
   4421         GetEffectiveTinyStringSetting(bsi, "GPU", "TextureFilter",
   4422                                       Settings::GetTextureFilterName(Settings::DEFAULT_GPU_TEXTURE_FILTER)))
   4423         .value_or(Settings::DEFAULT_GPU_TEXTURE_FILTER);
   4424 
   4425     DrawToggleSetting(
   4426       bsi, FSUI_CSTR("Round Upscaled Texture Coordinates"),
   4427       FSUI_CSTR("Rounds texture coordinates instead of flooring when upscaling. Can fix misaligned "
   4428                 "textures in some games, but break others, and is incompatible with texture filtering."),
   4429       "GPU", "ForceRoundTextureCoordinates", false,
   4430       resolution_scale > 1 && texture_filtering == GPUTextureFilter::Nearest);
   4431 
   4432     DrawToggleSetting(
   4433       bsi, FSUI_CSTR("Use Software Renderer For Readbacks"),
   4434       FSUI_CSTR("Runs the software renderer in parallel for VRAM readbacks. On some systems, this may result "
   4435                 "in greater performance."),
   4436       "GPU", "UseSoftwareRendererForReadbacks", false);
   4437   }
   4438 
   4439   DrawToggleSetting(
   4440     bsi, FSUI_CSTR("Stretch Display Vertically"),
   4441     FSUI_CSTR("Stretches the display to match the aspect ratio by multiplying vertically instead of horizontally."),
   4442     "Display", "StretchVertically", false);
   4443 
   4444   DrawToggleSetting(
   4445     bsi, FSUI_CSTR("Disable Mailbox Presentation"),
   4446     FSUI_CSTR("Forces the use of FIFO over Mailbox presentation, i.e. double buffering instead of triple buffering. "
   4447               "Usually results in worse frame pacing."),
   4448     "Display", "DisableMailboxPresentation", false);
   4449 
   4450   switch (renderer)
   4451   {
   4452 #ifdef _WIN32
   4453     case GPURenderer::HardwareD3D11:
   4454     {
   4455       DrawToggleSetting(
   4456         bsi, FSUI_CSTR("Use Blit Swap Chain"),
   4457         FSUI_CSTR("Uses a blit presentation model instead of flipping. This may be needed on some systems."), "Display",
   4458         "UseBlitSwapChain", false);
   4459     }
   4460     break;
   4461 #endif
   4462 
   4463 #ifdef ENABLE_VULKAN
   4464     case GPURenderer::HardwareVulkan:
   4465     {
   4466       DrawToggleSetting(bsi, FSUI_CSTR("Threaded Presentation"),
   4467                         FSUI_CSTR("Presents frames on a background thread when fast forwarding or vsync is disabled."),
   4468                         "GPU", "ThreadedPresentation", true);
   4469     }
   4470     break;
   4471 #endif
   4472 
   4473     case GPURenderer::Software:
   4474     {
   4475       DrawToggleSetting(bsi, FSUI_CSTR("Threaded Rendering"),
   4476                         FSUI_CSTR("Uses a second thread for drawing graphics. Speed boost, and safe to use."), "GPU",
   4477                         "UseThread", true);
   4478     }
   4479     break;
   4480 
   4481     default:
   4482       break;
   4483   }
   4484 
   4485   if (is_hardware && pgxp_enabled)
   4486   {
   4487     MenuHeading(FSUI_CSTR("PGXP (Precision Geometry Transform Pipeline)"));
   4488 
   4489     DrawToggleSetting(
   4490       bsi, FSUI_CSTR("Perspective Correct Textures"),
   4491       FSUI_CSTR("Uses perspective-correct interpolation for texture coordinates, straightening out warped textures."),
   4492       "GPU", "PGXPTextureCorrection", true, pgxp_enabled);
   4493     DrawToggleSetting(
   4494       bsi, FSUI_CSTR("Perspective Correct Colors"),
   4495       FSUI_CSTR("Uses perspective-correct interpolation for colors, which can improve visuals in some games."), "GPU",
   4496       "PGXPColorCorrection", false, pgxp_enabled);
   4497     DrawToggleSetting(
   4498       bsi, FSUI_CSTR("Culling Correction"),
   4499       FSUI_CSTR("Increases the precision of polygon culling, reducing the number of holes in geometry."), "GPU",
   4500       "PGXPCulling", true, pgxp_enabled);
   4501     DrawToggleSetting(
   4502       bsi, FSUI_CSTR("Preserve Projection Precision"),
   4503       FSUI_CSTR("Adds additional precision to PGXP data post-projection. May improve visuals in some games."), "GPU",
   4504       "PGXPPreserveProjFP", false, pgxp_enabled);
   4505 
   4506     DrawToggleSetting(bsi, FSUI_CSTR("CPU Mode"),
   4507                       FSUI_CSTR("Uses PGXP for all instructions, not just memory operations."), "GPU", "PGXPCPU", false,
   4508                       pgxp_enabled);
   4509 
   4510     DrawToggleSetting(bsi, FSUI_CSTR("Vertex Cache"),
   4511                       FSUI_CSTR("Uses screen positions to resolve PGXP data. May improve visuals in some games."),
   4512                       "GPU", "PGXPVertexCache", pgxp_enabled);
   4513 
   4514     DrawToggleSetting(
   4515       bsi, FSUI_CSTR("Disable on 2D Polygons"),
   4516       FSUI_CSTR("Uses native resolution coordinates for 2D polygons, instead of precise coordinates. Can "
   4517                 "fix misaligned UI in some games, but otherwise should be left disabled."),
   4518       "GPU", "PGXPDisableOn2DPolygons", false, pgxp_enabled);
   4519 
   4520     DrawFloatRangeSetting(
   4521       bsi, FSUI_CSTR("Geometry Tolerance"),
   4522       FSUI_CSTR("Sets a threshold for discarding precise values when exceeded. May help with glitches in some games."),
   4523       "GPU", "PGXPTolerance", -1.0f, -1.0f, 10.0f, "%.1f", pgxp_enabled);
   4524 
   4525     DrawFloatRangeSetting(
   4526       bsi, FSUI_CSTR("Depth Clear Threshold"),
   4527       FSUI_CSTR("Sets a threshold for discarding the emulated depth buffer. May help in some games."), "GPU",
   4528       "PGXPDepthBuffer", Settings::DEFAULT_GPU_PGXP_DEPTH_THRESHOLD, 0.0f, 4096.0f, "%.1f", pgxp_enabled);
   4529   }
   4530 
   4531   MenuHeading(FSUI_CSTR("Capture"));
   4532 
   4533   DrawEnumSetting(bsi, FSUI_CSTR("Screenshot Size"),
   4534                   FSUI_CSTR("Determines the size of screenshots created by DuckStation."), "Display", "ScreenshotMode",
   4535                   Settings::DEFAULT_DISPLAY_SCREENSHOT_MODE, &Settings::ParseDisplayScreenshotMode,
   4536                   &Settings::GetDisplayScreenshotModeName, &Settings::GetDisplayScreenshotModeDisplayName,
   4537                   DisplayScreenshotMode::Count);
   4538   DrawEnumSetting(bsi, FSUI_CSTR("Screenshot Format"),
   4539                   FSUI_CSTR("Determines the format that screenshots will be saved/compressed with."), "Display",
   4540                   "ScreenshotFormat", Settings::DEFAULT_DISPLAY_SCREENSHOT_FORMAT,
   4541                   &Settings::ParseDisplayScreenshotFormat, &Settings::GetDisplayScreenshotFormatName,
   4542                   &Settings::GetDisplayScreenshotFormatDisplayName, DisplayScreenshotFormat::Count);
   4543   DrawIntRangeSetting(bsi, FSUI_CSTR("Screenshot Quality"),
   4544                       FSUI_CSTR("Selects the quality at which screenshots will be compressed."), "Display",
   4545                       "ScreenshotQuality", Settings::DEFAULT_DISPLAY_SCREENSHOT_QUALITY, 1, 100, "%d%%");
   4546 
   4547   MenuHeading(FSUI_CSTR("Texture Replacements"));
   4548 
   4549   DrawToggleSetting(bsi, FSUI_CSTR("Enable VRAM Write Texture Replacement"),
   4550                     FSUI_CSTR("Enables the replacement of background textures in supported games."),
   4551                     "TextureReplacements", "EnableVRAMWriteReplacements", false);
   4552   DrawToggleSetting(bsi, FSUI_CSTR("Preload Replacement Textures"),
   4553                     FSUI_CSTR("Loads all replacement texture to RAM, reducing stuttering at runtime."),
   4554                     "TextureReplacements", "PreloadTextures", false);
   4555   DrawToggleSetting(bsi, FSUI_CSTR("Use Old MDEC Routines"),
   4556                     FSUI_CSTR("Enables the older, less accurate MDEC decoding routines. May be required for old "
   4557                               "replacement backgrounds to match/load."),
   4558                     "Hacks", "UseOldMDECRoutines", false);
   4559 
   4560   DrawToggleSetting(bsi, FSUI_CSTR("Dump Replaceable VRAM Writes"),
   4561                     FSUI_CSTR("Writes textures which can be replaced to the dump directory."), "TextureReplacements",
   4562                     "DumpVRAMWrites", false);
   4563   DrawToggleSetting(bsi, FSUI_CSTR("Set VRAM Write Dump Alpha Channel"),
   4564                     FSUI_CSTR("Clears the mask/transparency bit in VRAM write dumps."), "TextureReplacements",
   4565                     "DumpVRAMWriteForceAlphaChannel", true);
   4566 
   4567   EndMenuButtons();
   4568 }
   4569 
   4570 void FullscreenUI::PopulatePostProcessingChain(SettingsInterface* si, const char* section)
   4571 {
   4572   const u32 stages = PostProcessing::Config::GetStageCount(*si, section);
   4573   s_postprocessing_stages.clear();
   4574   s_postprocessing_stages.reserve(stages);
   4575   for (u32 i = 0; i < stages; i++)
   4576   {
   4577     PostProcessingStageInfo psi;
   4578     psi.name = PostProcessing::Config::GetStageShaderName(*si, section, i);
   4579     psi.options = PostProcessing::Config::GetStageOptions(*si, section, i);
   4580     s_postprocessing_stages.push_back(std::move(psi));
   4581   }
   4582 }
   4583 
   4584 enum
   4585 {
   4586   POSTPROCESSING_ACTION_NONE = 0,
   4587   POSTPROCESSING_ACTION_REMOVE,
   4588   POSTPROCESSING_ACTION_MOVE_UP,
   4589   POSTPROCESSING_ACTION_MOVE_DOWN,
   4590 };
   4591 
   4592 void FullscreenUI::DrawPostProcessingSettingsPage()
   4593 {
   4594   SettingsInterface* bsi = GetEditingSettingsInterface();
   4595   static constexpr const char* section = PostProcessing::Config::DISPLAY_CHAIN_SECTION;
   4596 
   4597   BeginMenuButtons();
   4598 
   4599   MenuHeading(FSUI_CSTR("Controls"));
   4600 
   4601   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Enable Post Processing"),
   4602                     FSUI_CSTR("If not enabled, the current post processing chain will be ignored."), "PostProcessing",
   4603                     "Enabled", false);
   4604 
   4605   if (MenuButton(FSUI_ICONSTR(ICON_FA_SEARCH, "Reload Shaders"),
   4606                  FSUI_CSTR("Reloads the shaders from disk, applying any changes."),
   4607                  bsi->GetBoolValue("PostProcessing", "Enabled", false)))
   4608   {
   4609     if (System::IsValid() && PostProcessing::ReloadShaders())
   4610       ShowToast(std::string(), FSUI_STR("Post-processing shaders reloaded."));
   4611   }
   4612 
   4613   MenuHeading(FSUI_CSTR("Operations"));
   4614 
   4615   if (MenuButton(FSUI_ICONSTR(ICON_FA_PLUS, "Add Shader"), FSUI_CSTR("Adds a new shader to the chain.")))
   4616   {
   4617     std::vector<std::pair<std::string, std::string>> shaders = PostProcessing::GetAvailableShaderNames();
   4618     ImGuiFullscreen::ChoiceDialogOptions options;
   4619     options.reserve(shaders.size());
   4620     for (auto& [display_name, name] : shaders)
   4621       options.emplace_back(std::move(display_name), false);
   4622 
   4623     OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_PLUS, "Add Shader"), false, std::move(options),
   4624                      [shaders = std::move(shaders)](s32 index, const std::string& title, bool checked) {
   4625                        if (index < 0 || static_cast<u32>(index) >= shaders.size())
   4626                          return;
   4627 
   4628                        const std::string& shader_name = shaders[index].second;
   4629                        SettingsInterface* bsi = GetEditingSettingsInterface();
   4630                        Error error;
   4631                        if (PostProcessing::Config::AddStage(*bsi, section, shader_name, &error))
   4632                        {
   4633                          ShowToast(std::string(), fmt::format(FSUI_FSTR("Shader {} added as stage {}."), title,
   4634                                                               PostProcessing::Config::GetStageCount(*bsi, section)));
   4635                          PopulatePostProcessingChain(bsi, section);
   4636                          SetSettingsChanged(bsi);
   4637                        }
   4638                        else
   4639                        {
   4640                          ShowToast(std::string(),
   4641                                    fmt::format(FSUI_FSTR("Failed to load shader {}. It may be invalid.\nError was:"),
   4642                                                title, error.GetDescription()));
   4643                        }
   4644 
   4645                        CloseChoiceDialog();
   4646                      });
   4647   }
   4648 
   4649   if (MenuButton(FSUI_ICONSTR(ICON_FA_TIMES, "Clear Shaders"), FSUI_CSTR("Clears a shader from the chain.")))
   4650   {
   4651     OpenConfirmMessageDialog(
   4652       FSUI_ICONSTR(ICON_FA_TIMES, "Clear Shaders"),
   4653       FSUI_CSTR("Are you sure you want to clear the current post-processing chain? All configuration will be lost."),
   4654       [](bool confirmed) {
   4655         if (!confirmed)
   4656           return;
   4657 
   4658         SettingsInterface* bsi = GetEditingSettingsInterface();
   4659         PostProcessing::Config::ClearStages(*bsi, section);
   4660         PopulatePostProcessingChain(bsi, section);
   4661         SetSettingsChanged(bsi);
   4662         ShowToast(std::string(), FSUI_STR("Post-processing chain cleared."));
   4663       });
   4664   }
   4665 
   4666   u32 postprocessing_action = POSTPROCESSING_ACTION_NONE;
   4667   u32 postprocessing_action_index = 0;
   4668 
   4669   SmallString str;
   4670   SmallString tstr;
   4671   for (u32 stage_index = 0; stage_index < static_cast<u32>(s_postprocessing_stages.size()); stage_index++)
   4672   {
   4673     PostProcessingStageInfo& si = s_postprocessing_stages[stage_index];
   4674 
   4675     ImGui::PushID(stage_index);
   4676     str.format(FSUI_FSTR("Stage {}: {}"), stage_index + 1, si.name);
   4677     MenuHeading(str);
   4678 
   4679     if (MenuButton(FSUI_ICONSTR(ICON_FA_TIMES, "Remove From Chain"), FSUI_CSTR("Removes this shader from the chain.")))
   4680     {
   4681       postprocessing_action = POSTPROCESSING_ACTION_REMOVE;
   4682       postprocessing_action_index = stage_index;
   4683     }
   4684 
   4685     if (MenuButton(FSUI_ICONSTR(ICON_FA_ARROW_UP, "Move Up"),
   4686                    FSUI_CSTR("Moves this shader higher in the chain, applying it earlier."), (stage_index > 0)))
   4687     {
   4688       postprocessing_action = POSTPROCESSING_ACTION_MOVE_UP;
   4689       postprocessing_action_index = stage_index;
   4690     }
   4691 
   4692     if (MenuButton(FSUI_ICONSTR(ICON_FA_ARROW_DOWN, "Move Down"),
   4693                    FSUI_CSTR("Moves this shader lower in the chain, applying it later."),
   4694                    (stage_index != (s_postprocessing_stages.size() - 1))))
   4695     {
   4696       postprocessing_action = POSTPROCESSING_ACTION_MOVE_DOWN;
   4697       postprocessing_action_index = stage_index;
   4698     }
   4699 
   4700     for (PostProcessing::ShaderOption& opt : si.options)
   4701     {
   4702       if (opt.ui_name.empty())
   4703         continue;
   4704 
   4705       switch (opt.type)
   4706       {
   4707         case PostProcessing::ShaderOption::Type::Bool:
   4708         {
   4709           bool value = (opt.value[0].int_value != 0);
   4710           tstr.format(ICON_FA_COGS "{}", opt.ui_name);
   4711           if (ToggleButton(tstr,
   4712                            (opt.default_value[0].int_value != 0) ? FSUI_CSTR("Default: Enabled") :
   4713                                                                    FSUI_CSTR("Default: Disabled"),
   4714                            &value))
   4715           {
   4716             opt.value[0].int_value = (value != 0);
   4717             PostProcessing::Config::SetStageOption(*bsi, section, stage_index, opt);
   4718             SetSettingsChanged(bsi);
   4719           }
   4720         }
   4721         break;
   4722 
   4723         case PostProcessing::ShaderOption::Type::Float:
   4724         {
   4725           tstr.format(ICON_FA_RULER_VERTICAL "{}##{}", opt.ui_name, opt.name);
   4726           str.format(FSUI_FSTR("Value: {} | Default: {} | Minimum: {} | Maximum: {}"), opt.value[0].float_value,
   4727                      opt.default_value[0].float_value, opt.min_value[0].float_value, opt.max_value[0].float_value);
   4728           if (MenuButton(tstr, str))
   4729             ImGui::OpenPopup(tstr);
   4730 
   4731           ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f));
   4732           ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   4733 
   4734           ImGui::PushFont(g_large_font);
   4735           ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   4736           ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   4737                                                                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   4738           ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   4739 
   4740           bool is_open = true;
   4741           if (ImGui::BeginPopupModal(tstr, &is_open,
   4742                                      ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   4743           {
   4744             BeginMenuButtons();
   4745 
   4746             const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   4747 
   4748 #if 0
   4749             for (u32 i = 0; i < opt.vector_size; i++)
   4750             {
   4751               static constexpr const char* components[] = { "X", "Y", "Z", "W" };
   4752               if (opt.vector_size == 1)
   4753                 tstr.Assign("##value");
   4754               else
   4755                 tstr.Fmt("{}##value{}", components[i], i);
   4756 
   4757               ImGui::SetNextItemWidth(end);
   4758               if (ImGui::SliderFloat(tstr, &opt.value[i].float_value, opt.min_value[i].float_value,
   4759                 opt.max_value[i].float_value, "%f", ImGuiSliderFlags_NoInput))
   4760               {
   4761                 SavePostProcessingChain();
   4762               }
   4763             }
   4764 #else
   4765             ImGui::SetNextItemWidth(end);
   4766 
   4767             bool changed = false;
   4768             switch (opt.vector_size)
   4769             {
   4770               case 1:
   4771               {
   4772                 changed = ImGui::SliderFloat("##value", &opt.value[0].float_value, opt.min_value[0].float_value,
   4773                                              opt.max_value[0].float_value);
   4774               }
   4775               break;
   4776 
   4777               case 2:
   4778               {
   4779                 changed = ImGui::SliderFloat2("##value", &opt.value[0].float_value, opt.min_value[0].float_value,
   4780                                               opt.max_value[0].float_value);
   4781               }
   4782               break;
   4783 
   4784               case 3:
   4785               {
   4786                 changed = ImGui::SliderFloat3("##value", &opt.value[0].float_value, opt.min_value[0].float_value,
   4787                                               opt.max_value[0].float_value);
   4788               }
   4789               break;
   4790 
   4791               case 4:
   4792               {
   4793                 changed = ImGui::SliderFloat4("##value", &opt.value[0].float_value, opt.min_value[0].float_value,
   4794                                               opt.max_value[0].float_value);
   4795               }
   4796               break;
   4797             }
   4798 
   4799             if (changed)
   4800             {
   4801               PostProcessing::Config::SetStageOption(*bsi, section, stage_index, opt);
   4802               SetSettingsChanged(bsi);
   4803             }
   4804 #endif
   4805 
   4806             ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   4807             if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   4808                                          ImVec2(0.5f, 0.0f)))
   4809             {
   4810               ImGui::CloseCurrentPopup();
   4811             }
   4812             EndMenuButtons();
   4813 
   4814             ImGui::EndPopup();
   4815           }
   4816 
   4817           ImGui::PopStyleVar(3);
   4818           ImGui::PopFont();
   4819         }
   4820         break;
   4821 
   4822         case PostProcessing::ShaderOption::Type::Int:
   4823         {
   4824           tstr.format(ICON_FA_RULER_VERTICAL "{}##{}", opt.ui_name, opt.name);
   4825           str.format(FSUI_FSTR("Value: {} | Default: {} | Minimum: {} | Maximum: {}"), opt.value[0].int_value,
   4826                      opt.default_value[0].int_value, opt.min_value[0].int_value, opt.max_value[0].int_value);
   4827           if (MenuButton(tstr, str))
   4828             ImGui::OpenPopup(tstr);
   4829 
   4830           ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f));
   4831           ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   4832 
   4833           ImGui::PushFont(g_large_font);
   4834           ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   4835           ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   4836                                                                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   4837           ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   4838 
   4839           bool is_open = true;
   4840           if (ImGui::BeginPopupModal(tstr, &is_open,
   4841                                      ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   4842           {
   4843             BeginMenuButtons();
   4844 
   4845             const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   4846 
   4847 #if 0
   4848             for (u32 i = 0; i < opt.vector_size; i++)
   4849             {
   4850               static constexpr const char* components[] = { "X", "Y", "Z", "W" };
   4851               if (opt.vector_size == 1)
   4852                 tstr.Assign("##value");
   4853               else
   4854                 tstr.Fmt("{}##value{}", components[i], i);
   4855 
   4856               ImGui::SetNextItemWidth(end);
   4857               if (ImGui::SliderInt(tstr, &opt.value[i].int_value, opt.min_value[i].int_value,
   4858                 opt.max_value[i].int_value, "%d", ImGuiSliderFlags_NoInput))
   4859               {
   4860                 SavePostProcessingChain();
   4861               }
   4862             }
   4863 #else
   4864             bool changed = false;
   4865             ImGui::SetNextItemWidth(end);
   4866             switch (opt.vector_size)
   4867             {
   4868               case 1:
   4869               {
   4870                 changed = ImGui::SliderInt("##value", &opt.value[0].int_value, opt.min_value[0].int_value,
   4871                                            opt.max_value[0].int_value);
   4872               }
   4873               break;
   4874 
   4875               case 2:
   4876               {
   4877                 changed = ImGui::SliderInt2("##value", &opt.value[0].int_value, opt.min_value[0].int_value,
   4878                                             opt.max_value[0].int_value);
   4879               }
   4880               break;
   4881 
   4882               case 3:
   4883               {
   4884                 changed = ImGui::SliderInt3("##value", &opt.value[0].int_value, opt.min_value[0].int_value,
   4885                                             opt.max_value[0].int_value);
   4886               }
   4887               break;
   4888 
   4889               case 4:
   4890               {
   4891                 changed = ImGui::SliderInt4("##value", &opt.value[0].int_value, opt.min_value[0].int_value,
   4892                                             opt.max_value[0].int_value);
   4893               }
   4894               break;
   4895             }
   4896 
   4897             if (changed)
   4898             {
   4899               PostProcessing::Config::SetStageOption(*bsi, section, stage_index, opt);
   4900               SetSettingsChanged(bsi);
   4901             }
   4902 #endif
   4903 
   4904             ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   4905             if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font,
   4906                                          ImVec2(0.5f, 0.0f)))
   4907             {
   4908               ImGui::CloseCurrentPopup();
   4909             }
   4910             EndMenuButtons();
   4911 
   4912             ImGui::EndPopup();
   4913           }
   4914 
   4915           ImGui::PopStyleVar(3);
   4916           ImGui::PopFont();
   4917         }
   4918         break;
   4919 
   4920         default:
   4921           break;
   4922       }
   4923     }
   4924 
   4925     ImGui::PopID();
   4926   }
   4927 
   4928   switch (postprocessing_action)
   4929   {
   4930     case POSTPROCESSING_ACTION_REMOVE:
   4931     {
   4932       const PostProcessingStageInfo& si = s_postprocessing_stages[postprocessing_action_index];
   4933       ShowToast(std::string(),
   4934                 fmt::format(FSUI_FSTR("Removed stage {} ({})."), postprocessing_action_index + 1, si.name));
   4935       PostProcessing::Config::RemoveStage(*bsi, section, postprocessing_action_index);
   4936       PopulatePostProcessingChain(bsi, section);
   4937       SetSettingsChanged(bsi);
   4938     }
   4939     break;
   4940     case POSTPROCESSING_ACTION_MOVE_UP:
   4941     {
   4942       PostProcessing::Config::MoveStageUp(*bsi, section, postprocessing_action_index);
   4943       PopulatePostProcessingChain(bsi, section);
   4944       SetSettingsChanged(bsi);
   4945     }
   4946     break;
   4947     case POSTPROCESSING_ACTION_MOVE_DOWN:
   4948     {
   4949       PostProcessing::Config::MoveStageDown(*bsi, section, postprocessing_action_index);
   4950       PopulatePostProcessingChain(bsi, section);
   4951       SetSettingsChanged(bsi);
   4952     }
   4953     break;
   4954     default:
   4955       break;
   4956   }
   4957 
   4958   EndMenuButtons();
   4959 }
   4960 
   4961 void FullscreenUI::DrawAudioSettingsPage()
   4962 {
   4963   SettingsInterface* bsi = GetEditingSettingsInterface();
   4964 
   4965   BeginMenuButtons();
   4966 
   4967   MenuHeading(FSUI_CSTR("Audio Control"));
   4968 
   4969   DrawIntRangeSetting(bsi, FSUI_CSTR("Output Volume"),
   4970                       FSUI_CSTR("Controls the volume of the audio played on the host."), "Audio", "OutputVolume", 100,
   4971                       0, 200, "%d%%");
   4972   DrawIntRangeSetting(bsi, FSUI_CSTR("Fast Forward Volume"),
   4973                       FSUI_CSTR("Controls the volume of the audio played on the host when fast forwarding."), "Audio",
   4974                       "FastForwardVolume", 200, 0, 100, "%d%%");
   4975   DrawToggleSetting(bsi, FSUI_CSTR("Mute All Sound"),
   4976                     FSUI_CSTR("Prevents the emulator from producing any audible sound."), "Audio", "OutputMuted",
   4977                     false);
   4978   DrawToggleSetting(bsi, FSUI_CSTR("Mute CD Audio"),
   4979                     FSUI_CSTR("Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to "
   4980                               "disable background music in some games."),
   4981                     "CDROM", "MuteCDAudio", false);
   4982 
   4983   MenuHeading(FSUI_CSTR("Backend Settings"));
   4984 
   4985   DrawEnumSetting(
   4986     bsi, FSUI_CSTR("Audio Backend"),
   4987     FSUI_CSTR("The audio backend determines how frames produced by the emulator are submitted to the host."), "Audio",
   4988     "Backend", AudioStream::DEFAULT_BACKEND, &AudioStream::ParseBackendName, &AudioStream::GetBackendName,
   4989     &AudioStream::GetBackendDisplayName, AudioBackend::Count);
   4990   DrawEnumSetting(bsi, FSUI_CSTR("Expansion Mode"),
   4991                   FSUI_CSTR("Determines how audio is expanded from stereo to surround for supported games."), "Audio",
   4992                   "ExpansionMode", AudioStreamParameters::DEFAULT_EXPANSION_MODE, &AudioStream::ParseExpansionMode,
   4993                   &AudioStream::GetExpansionModeName, &AudioStream::GetExpansionModeDisplayName,
   4994                   AudioExpansionMode::Count);
   4995   DrawEnumSetting(bsi, FSUI_CSTR("Stretch Mode"),
   4996                   FSUI_CSTR("Determines quality of audio when not running at 100% speed."), "Audio", "StretchMode",
   4997                   AudioStreamParameters::DEFAULT_STRETCH_MODE, &AudioStream::ParseStretchMode,
   4998                   &AudioStream::GetStretchModeName, &AudioStream::GetStretchModeDisplayName, AudioStretchMode::Count);
   4999   DrawIntRangeSetting(bsi, FSUI_CSTR("Buffer Size"),
   5000                       FSUI_CSTR("Determines the amount of audio buffered before being pulled by the host API."),
   5001                       "Audio", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS, 10, 500, FSUI_CSTR("%d ms"));
   5002   if (!GetEffectiveBoolSetting(bsi, "Audio", "OutputLatencyMinimal",
   5003                                AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MINIMAL))
   5004   {
   5005     DrawIntRangeSetting(
   5006       bsi, FSUI_CSTR("Output Latency"),
   5007       FSUI_CSTR("Determines how much latency there is between the audio being picked up by the host API, and "
   5008                 "played through speakers."),
   5009       "Audio", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS, 1, 500, FSUI_CSTR("%d ms"));
   5010   }
   5011   DrawToggleSetting(bsi, FSUI_CSTR("Minimal Output Latency"),
   5012                     FSUI_CSTR("When enabled, the minimum supported output latency will be used for the host API."),
   5013                     "Audio", "OutputLatencyMinimal", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MINIMAL);
   5014 
   5015   EndMenuButtons();
   5016 }
   5017 
   5018 void FullscreenUI::DrawAchievementsSettingsPage()
   5019 {
   5020 #ifdef ENABLE_RAINTEGRATION
   5021   if (Achievements::IsUsingRAIntegration())
   5022   {
   5023     BeginMenuButtons();
   5024     ActiveButton(
   5025       FSUI_ICONSTR(ICON_FA_BAN,
   5026                    FSUI_CSTR("RAIntegration is being used instead of the built-in achievements implementation.")),
   5027       false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5028     EndMenuButtons();
   5029     return;
   5030   }
   5031 #endif
   5032 
   5033   SettingsInterface* bsi = GetEditingSettingsInterface();
   5034 
   5035   BeginMenuButtons();
   5036 
   5037   MenuHeading(FSUI_CSTR("Settings"));
   5038   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_TROPHY, "Enable Achievements"),
   5039                     FSUI_CSTR("When enabled and logged in, DuckStation will scan for achievements on startup."),
   5040                     "Cheevos", "Enabled", false);
   5041 
   5042   const bool enabled = bsi->GetBoolValue("Cheevos", "Enabled", false);
   5043 
   5044   if (DrawToggleSetting(
   5045         bsi, FSUI_ICONSTR(ICON_FA_HARD_HAT, "Hardcore Mode"),
   5046         FSUI_CSTR("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, "
   5047                   "cheats, and slowdown functions."),
   5048         "Cheevos", "ChallengeMode", false, enabled))
   5049   {
   5050     if (System::IsValid() && bsi->GetBoolValue("Cheevos", "ChallengeMode", false))
   5051       ShowToast(std::string(), FSUI_STR("Hardcore mode will be enabled on next game restart."));
   5052   }
   5053   DrawToggleSetting(
   5054     bsi, FSUI_ICONSTR(ICON_FA_INBOX, "Achievement Notifications"),
   5055     FSUI_CSTR("Displays popup messages on events such as achievement unlocks and leaderboard submissions."), "Cheevos",
   5056     "Notifications", true, enabled);
   5057   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST_OL, "Leaderboard Notifications"),
   5058                     FSUI_CSTR("Displays popup messages when starting, submitting, or failing a leaderboard challenge."),
   5059                     "Cheevos", "LeaderboardNotifications", true, enabled);
   5060   DrawToggleSetting(
   5061     bsi, FSUI_ICONSTR(ICON_FA_HEADPHONES, "Sound Effects"),
   5062     FSUI_CSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos",
   5063     "SoundEffects", true, enabled);
   5064   DrawToggleSetting(
   5065     bsi, FSUI_ICONSTR(ICON_FA_MAGIC, "Enable In-Game Overlays"),
   5066     FSUI_CSTR("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active."),
   5067     "Cheevos", "Overlays", true, enabled);
   5068   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_USER_FRIENDS, "Encore Mode"),
   5069                     FSUI_CSTR("When enabled, each session will behave as if no achievements have been unlocked."),
   5070                     "Cheevos", "EncoreMode", false, enabled);
   5071   DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_STETHOSCOPE, "Spectator Mode"),
   5072                     FSUI_CSTR("When enabled, DuckStation will assume all achievements are locked and not send any "
   5073                               "unlock notifications to the server."),
   5074                     "Cheevos", "SpectatorMode", false, enabled);
   5075   DrawToggleSetting(
   5076     bsi, FSUI_ICONSTR(ICON_FA_MEDAL, "Test Unofficial Achievements"),
   5077     FSUI_CSTR("When enabled, DuckStation will list achievements from unofficial sets. These achievements are not "
   5078               "tracked by RetroAchievements."),
   5079     "Cheevos", "UnofficialTestMode", false, enabled);
   5080 
   5081   if (!IsEditingGameSettings(bsi))
   5082   {
   5083     MenuHeading(FSUI_CSTR("Account"));
   5084     if (bsi->ContainsValue("Cheevos", "Token"))
   5085     {
   5086       ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]);
   5087       ActiveButton(SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_USER, "Username: {}")),
   5088                                             bsi->GetTinyStringValue("Cheevos", "Username")),
   5089                    false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5090 
   5091       TinyString ts_string;
   5092       ts_string.format(
   5093         FSUI_FSTR("{:%Y-%m-%d %H:%M:%S}"),
   5094         fmt::localtime(
   5095           StringUtil::FromChars<u64>(bsi->GetTinyStringValue("Cheevos", "LoginTimestamp", "0")).value_or(0)));
   5096       ActiveButton(
   5097         SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_CLOCK, "Login token generated on {}")), ts_string),
   5098         false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5099       ImGui::PopStyleColor();
   5100 
   5101       if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Logout"), FSUI_CSTR("Logs out of RetroAchievements.")))
   5102       {
   5103         Host::RunOnCPUThread([]() { Achievements::Logout(); });
   5104       }
   5105     }
   5106     else
   5107     {
   5108       ActiveButton(FSUI_ICONSTR(ICON_FA_USER, "Not Logged In"), false, false,
   5109                    ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5110 
   5111       if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), FSUI_CSTR("Logs in to RetroAchievements.")))
   5112         Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason::UserInitiated);
   5113     }
   5114 
   5115     MenuHeading(FSUI_CSTR("Current Game"));
   5116     if (Achievements::HasActiveGame())
   5117     {
   5118       const auto lock = Achievements::GetLock();
   5119 
   5120       ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]);
   5121       ActiveButton(SmallString::from_format(fmt::runtime(FSUI_ICONSTR(ICON_FA_BOOKMARK, "Game: {} ({})")),
   5122                                             Achievements::GetGameID(), Achievements::GetGameTitle()),
   5123                    false, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5124 
   5125       const std::string& rich_presence_string = Achievements::GetRichPresenceString();
   5126       if (!rich_presence_string.empty())
   5127       {
   5128         ActiveButton(SmallString::from_format(ICON_FA_MAP "{}", rich_presence_string), false, false,
   5129                      LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5130       }
   5131       else
   5132       {
   5133         ActiveButton(FSUI_ICONSTR(ICON_FA_MAP, "Rich presence inactive or unsupported."), false, false,
   5134                      LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5135       }
   5136 
   5137       ImGui::PopStyleColor();
   5138     }
   5139     else
   5140     {
   5141       ActiveButton(FSUI_ICONSTR(ICON_FA_BAN, "Game not loaded or no RetroAchievements available."), false, false,
   5142                    LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5143     }
   5144   }
   5145 
   5146   EndMenuButtons();
   5147 }
   5148 
   5149 void FullscreenUI::DrawAdvancedSettingsPage()
   5150 {
   5151   SettingsInterface* bsi = GetEditingSettingsInterface();
   5152 
   5153   BeginMenuButtons();
   5154 
   5155   MenuHeading(FSUI_CSTR("Logging Settings"));
   5156   DrawEnumSetting(bsi, FSUI_CSTR("Log Level"),
   5157                   FSUI_CSTR("Sets the verbosity of messages logged. Higher levels will log more messages."), "Logging",
   5158                   "LogLevel", Settings::DEFAULT_LOG_LEVEL, &Settings::ParseLogLevelName, &Settings::GetLogLevelName,
   5159                   &Settings::GetLogLevelDisplayName, LOGLEVEL_COUNT);
   5160   DrawToggleSetting(bsi, FSUI_CSTR("Log To System Console"), FSUI_CSTR("Logs messages to the console window."),
   5161                     FSUI_CSTR("Logging"), "LogToConsole", Settings::DEFAULT_LOG_TO_CONSOLE);
   5162   DrawToggleSetting(bsi, FSUI_CSTR("Log To Debug Console"),
   5163                     FSUI_CSTR("Logs messages to the debug console where supported."), "Logging", "LogToDebug", false);
   5164   DrawToggleSetting(bsi, FSUI_CSTR("Log To File"), FSUI_CSTR("Logs messages to duckstation.log in the user directory."),
   5165                     "Logging", "LogToFile", false);
   5166 
   5167   MenuHeading(FSUI_CSTR("Debugging Settings"));
   5168 
   5169   DrawToggleSetting(bsi, FSUI_CSTR("Use Debug GPU Device"),
   5170                     FSUI_CSTR("Enable debugging when supported by the host's renderer API. Only for developer use."),
   5171                     "GPU", "UseDebugDevice", false);
   5172 
   5173 #ifdef _WIN32
   5174   DrawToggleSetting(bsi, FSUI_CSTR("Increase Timer Resolution"),
   5175                     FSUI_CSTR("Enables more precise frame pacing at the cost of battery life."), "Main",
   5176                     "IncreaseTimerResolution", true);
   5177 #endif
   5178 
   5179   DrawToggleSetting(bsi, FSUI_CSTR("Allow Booting Without SBI File"),
   5180                     FSUI_CSTR("Allows loading protected games without subchannel information."), "CDROM",
   5181                     "AllowBootingWithoutSBIFile", false);
   5182 
   5183   DrawToggleSetting(bsi, FSUI_CSTR("Create Save State Backups"),
   5184                     FSUI_CSTR("Renames existing save states when saving to a backup file."), "Main",
   5185                     "CreateSaveStateBackups", false);
   5186   DrawToggleSetting(
   5187     bsi, FSUI_CSTR("Load Devices From Save States"),
   5188     FSUI_CSTR("When enabled, memory cards and controllers will be overwritten when save states are loaded."), "Main",
   5189     "LoadDevicesFromSaveStates", false);
   5190   DrawEnumSetting(bsi, FSUI_CSTR("Save State Compression"),
   5191                   FSUI_CSTR("Reduces the size of save states by compressing the data before saving."), "Main",
   5192                   "SaveStateCompression", Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE,
   5193                   &Settings::ParseSaveStateCompressionModeName, &Settings::GetSaveStateCompressionModeName,
   5194                   &Settings::GetSaveStateCompressionModeDisplayName, SaveStateCompressionMode::Count);
   5195 
   5196   MenuHeading(FSUI_CSTR("Display Settings"));
   5197   DrawToggleSetting(bsi, FSUI_CSTR("Show Status Indicators"),
   5198                     FSUI_CSTR("Shows persistent icons when turbo is active or when paused."), "Display",
   5199                     "ShowStatusIndicators", true);
   5200   DrawToggleSetting(bsi, FSUI_CSTR("Show Enhancement Settings"),
   5201                     FSUI_CSTR("Shows enhancement settings in the bottom-right corner of the screen."), "Display",
   5202                     "ShowEnhancements", false);
   5203   DrawEnumSetting(bsi, FSUI_CSTR("Wireframe Rendering"),
   5204                   FSUI_CSTR("Overlays or replaces normal triangle drawing with a wireframe/line view."), "GPU",
   5205                   "WireframeMode", GPUWireframeMode::Disabled, &Settings::ParseGPUWireframeMode,
   5206                   &Settings::GetGPUWireframeModeName, &Settings::GetGPUWireframeModeDisplayName,
   5207                   GPUWireframeMode::Count);
   5208 
   5209   MenuHeading(FSUI_CSTR("CPU Emulation"));
   5210 
   5211   DrawToggleSetting(
   5212     bsi, FSUI_CSTR("Enable Recompiler ICache"),
   5213     FSUI_CSTR("Simulates the CPU's instruction cache in the recompiler. Can help with games running too fast."), "CPU",
   5214     "RecompilerICache", false);
   5215   DrawToggleSetting(bsi, FSUI_CSTR("Enable Recompiler Memory Exceptions"),
   5216                     FSUI_CSTR("Enables alignment and bus exceptions. Not needed for any known games."), "CPU",
   5217                     "RecompilerMemoryExceptions", false);
   5218   DrawToggleSetting(
   5219     bsi, FSUI_CSTR("Enable Recompiler Block Linking"),
   5220     FSUI_CSTR("Performance enhancement - jumps directly between blocks instead of returning to the dispatcher."), "CPU",
   5221     "RecompilerBlockLinking", true);
   5222   DrawEnumSetting(bsi, FSUI_CSTR("Recompiler Fast Memory Access"),
   5223                   FSUI_CSTR("Avoids calls to C++ code, significantly speeding up the recompiler."), "CPU",
   5224                   "FastmemMode", Settings::DEFAULT_CPU_FASTMEM_MODE, &Settings::ParseCPUFastmemMode,
   5225                   &Settings::GetCPUFastmemModeName, &Settings::GetCPUFastmemModeDisplayName, CPUFastmemMode::Count);
   5226 
   5227   MenuHeading(FSUI_CSTR("CD-ROM Emulation"));
   5228 
   5229   DrawToggleSetting(bsi, FSUI_CSTR("Enable Region Check"),
   5230                     FSUI_CSTR("Simulates the region check present in original, unmodified consoles."), "CDROM",
   5231                     "RegionCheck", false);
   5232 
   5233   EndMenuButtons();
   5234 }
   5235 
   5236 void FullscreenUI::DrawPauseMenu()
   5237 {
   5238   SmallString buffer;
   5239 
   5240   ImDrawList* dl = ImGui::GetBackgroundDrawList();
   5241   const ImVec2 display_size(ImGui::GetIO().DisplaySize);
   5242   const ImU32 text_color = ImGui::GetColorU32(UIBackgroundTextColor) | IM_COL32_A_MASK;
   5243   dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size,
   5244                     (ImGui::GetColorU32(UIBackgroundColor) & ~IM_COL32_A_MASK) | (200 << IM_COL32_A_SHIFT));
   5245 
   5246   // title info
   5247   {
   5248     const std::string& title = System::GetGameTitle();
   5249     const std::string& serial = System::GetGameSerial();
   5250 
   5251     if (!serial.empty())
   5252       buffer.format("{} - ", serial);
   5253     buffer.append(Path::GetFileName(System::GetDiscPath()));
   5254 
   5255     const float image_width = 60.0f;
   5256     const float image_height = 60.0f;
   5257 
   5258     const ImVec2 title_size(
   5259       g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, title.c_str()));
   5260     const ImVec2 subtitle_size(
   5261       g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, buffer.c_str()));
   5262 
   5263     ImVec2 title_pos(display_size.x - LayoutScale(10.0f + image_width + 20.0f) - title_size.x,
   5264                      display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - LayoutScale(10.0f + image_height));
   5265     ImVec2 subtitle_pos(display_size.x - LayoutScale(10.0f + image_width + 20.0f) - subtitle_size.x,
   5266                         title_pos.y + g_large_font->FontSize + LayoutScale(4.0f));
   5267 
   5268     float rp_height = 0.0f;
   5269     {
   5270       const auto lock = Achievements::GetLock();
   5271       const std::string& rp = Achievements::IsActive() ? Achievements::GetRichPresenceString() : std::string();
   5272 
   5273       if (!rp.empty())
   5274       {
   5275         const float wrap_width = LayoutScale(350.0f);
   5276         const ImVec2 rp_size = g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(),
   5277                                                             wrap_width, rp.data(), rp.data() + rp.length());
   5278 
   5279         // Add a small extra gap if any Rich Presence is displayed
   5280         rp_height = rp_size.y - g_medium_font->FontSize + LayoutScale(2.0f);
   5281 
   5282         const ImVec2 rp_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - rp_size.x,
   5283                             subtitle_pos.y + g_medium_font->FontSize + LayoutScale(4.0f) - rp_height);
   5284 
   5285         title_pos.y -= rp_height;
   5286         subtitle_pos.y -= rp_height;
   5287 
   5288         DrawShadowedText(dl, g_medium_font, rp_pos, text_color, rp.data(), rp.data() + rp.length(), wrap_width);
   5289       }
   5290     }
   5291 
   5292     DrawShadowedText(dl, g_large_font, title_pos, text_color, title.c_str());
   5293     DrawShadowedText(dl, g_medium_font, subtitle_pos, text_color, buffer.c_str());
   5294 
   5295     GPUTexture* const cover = GetCoverForCurrentGame();
   5296     const ImVec2 image_min(display_size.x - LayoutScale(10.0f + image_width),
   5297                            display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - LayoutScale(10.0f + image_height) -
   5298                              rp_height);
   5299     const ImVec2 image_max(image_min.x + LayoutScale(image_width), image_min.y + LayoutScale(image_height) + rp_height);
   5300     const ImRect image_rect(CenterImage(ImRect(image_min, image_max), ImVec2(static_cast<float>(cover->GetWidth()),
   5301                                                                              static_cast<float>(cover->GetHeight()))));
   5302     dl->AddImage(cover, image_rect.Min, image_rect.Max);
   5303   }
   5304 
   5305   // current time / play time
   5306   {
   5307     buffer.format("{:%X}", fmt::localtime(std::time(nullptr)));
   5308 
   5309     const ImVec2 time_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f,
   5310                                                        buffer.c_str(), buffer.end_ptr()));
   5311     const ImVec2 time_pos(display_size.x - LayoutScale(10.0f) - time_size.x, LayoutScale(10.0f));
   5312     DrawShadowedText(dl, g_large_font, time_pos, text_color, buffer.c_str(), buffer.end_ptr());
   5313 
   5314     const std::string& serial = System::GetGameSerial();
   5315     if (!serial.empty())
   5316     {
   5317       const std::time_t cached_played_time = GameList::GetCachedPlayedTimeForSerial(serial);
   5318       const std::time_t session_time = static_cast<std::time_t>(System::GetSessionPlayedTime());
   5319 
   5320       buffer.format(FSUI_FSTR("Session: {}"), GameList::FormatTimespan(session_time, true));
   5321       const ImVec2 session_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(),
   5322                                                              -1.0f, buffer.c_str(), buffer.end_ptr()));
   5323       const ImVec2 session_pos(display_size.x - LayoutScale(10.0f) - session_size.x,
   5324                                time_pos.y + g_large_font->FontSize + LayoutScale(4.0f));
   5325       DrawShadowedText(dl, g_medium_font, session_pos, text_color, buffer.c_str(), buffer.end_ptr());
   5326 
   5327       buffer.format(FSUI_FSTR("All Time: {}"), GameList::FormatTimespan(cached_played_time + session_time, true));
   5328       const ImVec2 total_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(),
   5329                                                            -1.0f, buffer.c_str(), buffer.end_ptr()));
   5330       const ImVec2 total_pos(display_size.x - LayoutScale(10.0f) - total_size.x,
   5331                              session_pos.y + g_medium_font->FontSize + LayoutScale(4.0f));
   5332       DrawShadowedText(dl, g_medium_font, total_pos, text_color, buffer.c_str(), buffer.end_ptr());
   5333     }
   5334   }
   5335 
   5336   const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT));
   5337   const ImVec2 window_pos(0.0f, display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - window_size.y);
   5338 
   5339   if (BeginFullscreenWindow(window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f,
   5340                             ImVec2(10.0f, 10.0f), ImGuiWindowFlags_NoBackground))
   5341   {
   5342     static constexpr u32 submenu_item_count[] = {
   5343       12, // None
   5344       4,  // Exit
   5345       3,  // Achievements
   5346     };
   5347 
   5348     ResetFocusHere();
   5349     BeginMenuButtons(submenu_item_count[static_cast<u32>(s_current_pause_submenu)], 1.0f,
   5350                      ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING,
   5351                      ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   5352 
   5353     switch (s_current_pause_submenu)
   5354     {
   5355       case PauseSubMenu::None:
   5356       {
   5357         // NOTE: Menu close must come first, because otherwise VM destruction options will race.
   5358         const bool has_game = System::IsValid() && !System::GetGameSerial().empty();
   5359 
   5360         if (DefaultActiveButton(FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false) || WantsToCloseMenu())
   5361           ClosePauseMenu();
   5362 
   5363         if (ActiveButton(FSUI_ICONSTR(ICON_FA_FAST_FORWARD, "Toggle Fast Forward"), false))
   5364         {
   5365           ClosePauseMenu();
   5366           DoToggleFastForward();
   5367         }
   5368 
   5369         if (ActiveButton(FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false, has_game))
   5370         {
   5371           if (OpenSaveStateSelector(true))
   5372             s_current_main_window = MainWindowType::None;
   5373         }
   5374 
   5375         if (ActiveButton(FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Save State"), false, has_game))
   5376         {
   5377           if (OpenSaveStateSelector(false))
   5378             s_current_main_window = MainWindowType::None;
   5379         }
   5380 
   5381         if (ActiveButton(FSUI_ICONSTR(ICON_FA_FROWN_OPEN, "Cheat List"), false,
   5382                          !System::GetGameSerial().empty() && g_settings.enable_cheats))
   5383         {
   5384           s_current_main_window = MainWindowType::None;
   5385           DoCheatsMenu();
   5386         }
   5387 
   5388         if (ActiveButton(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Toggle Analog"), false))
   5389         {
   5390           ClosePauseMenu();
   5391           DoToggleAnalogMode();
   5392         }
   5393 
   5394         if (ActiveButton(FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false, has_game))
   5395         {
   5396           SwitchToGameSettings();
   5397         }
   5398 
   5399         if (ActiveButton(FSUI_ICONSTR(ICON_FA_TROPHY, "Achievements"), false,
   5400                          Achievements::HasAchievementsOrLeaderboards()))
   5401         {
   5402           // skip second menu and go straight to cheevos if there's no lbs
   5403           if (!Achievements::HasLeaderboards())
   5404             OpenAchievementsWindow();
   5405           else
   5406             OpenPauseSubMenu(PauseSubMenu::Achievements);
   5407         }
   5408 
   5409         if (ActiveButton(FSUI_ICONSTR(ICON_FA_CAMERA, "Save Screenshot"), false))
   5410         {
   5411           System::SaveScreenshot();
   5412           ClosePauseMenu();
   5413         }
   5414 
   5415         if (ActiveButton(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Change Disc"), false))
   5416         {
   5417           s_current_main_window = MainWindowType::None;
   5418           DoChangeDisc();
   5419         }
   5420 
   5421         if (ActiveButton(FSUI_ICONSTR(ICON_FA_SLIDERS_H, "Settings"), false))
   5422           SwitchToSettings();
   5423 
   5424         if (ActiveButton(FSUI_ICONSTR(ICON_FA_POWER_OFF, "Close Game"), false))
   5425         {
   5426           // skip submenu when we can't save anyway
   5427           if (!has_game)
   5428             RequestShutdown(false);
   5429           else
   5430             OpenPauseSubMenu(PauseSubMenu::Exit);
   5431         }
   5432       }
   5433       break;
   5434 
   5435       case PauseSubMenu::Exit:
   5436       {
   5437         if (ActiveButton(FSUI_ICONSTR(ICON_FA_BACKWARD, "Back To Pause Menu"), false) || WantsToCloseMenu())
   5438           OpenPauseSubMenu(PauseSubMenu::None);
   5439 
   5440         if (ActiveButton(FSUI_ICONSTR(ICON_FA_SYNC, "Reset System"), false))
   5441         {
   5442           ClosePauseMenu();
   5443           RequestReset();
   5444         }
   5445 
   5446         if (ActiveButton(FSUI_ICONSTR(ICON_FA_SAVE, "Exit And Save State"), false))
   5447           RequestShutdown(true);
   5448 
   5449         if (DefaultActiveButton(FSUI_ICONSTR(ICON_FA_POWER_OFF, "Exit Without Saving"), false))
   5450           RequestShutdown(false);
   5451       }
   5452       break;
   5453 
   5454       case PauseSubMenu::Achievements:
   5455       {
   5456         if (ActiveButton(FSUI_ICONSTR(ICON_FA_BACKWARD, "Back To Pause Menu"), false) || WantsToCloseMenu())
   5457           OpenPauseSubMenu(PauseSubMenu::None);
   5458 
   5459         if (DefaultActiveButton(FSUI_ICONSTR(ICON_FA_TROPHY, "Achievements"), false))
   5460           OpenAchievementsWindow();
   5461 
   5462         if (ActiveButton(FSUI_ICONSTR(ICON_FA_STOPWATCH, "Leaderboards"), false))
   5463           OpenLeaderboardsWindow();
   5464       }
   5465       break;
   5466     }
   5467 
   5468     EndMenuButtons();
   5469 
   5470     EndFullscreenWindow();
   5471   }
   5472 
   5473   Achievements::DrawPauseMenuOverlays();
   5474 
   5475   if (IsGamepadInputSource())
   5476   {
   5477     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")),
   5478                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")),
   5479                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Return To Game"))});
   5480   }
   5481   else
   5482   {
   5483     SetFullscreenFooterText(std::array{
   5484       std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")),
   5485       std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Return To Game"))});
   5486   }
   5487 }
   5488 
   5489 void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& serial, s32 slot,
   5490                                                            bool global)
   5491 {
   5492   li->title = (global || slot > 0) ? fmt::format(global ? FSUI_FSTR("Global Slot {0}##global_slot_{0}") :
   5493                                                           FSUI_FSTR("Game Slot {0}##game_slot_{0}"),
   5494                                                  slot) :
   5495                                      FSUI_STR("Quick Save");
   5496   li->summary = FSUI_STR("No save present in this slot.");
   5497   li->path = {};
   5498   li->timestamp = 0;
   5499   li->slot = slot;
   5500   li->preview_texture = {};
   5501   li->global = global;
   5502 }
   5503 
   5504 bool FullscreenUI::InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li, const std::string& serial, s32 slot,
   5505                                                           bool global)
   5506 {
   5507   const std::string path =
   5508     (global ? System::GetGlobalSaveStateFileName(slot) : System::GetGameSaveStateFileName(serial, slot));
   5509   if (!InitializeSaveStateListEntryFromPath(li, path.c_str(), slot, global))
   5510   {
   5511     InitializePlaceholderSaveStateListEntry(li, serial, slot, global);
   5512     return false;
   5513   }
   5514 
   5515   return true;
   5516 }
   5517 
   5518 bool FullscreenUI::InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global)
   5519 {
   5520   std::optional<ExtendedSaveStateInfo> ssi(System::GetExtendedSaveStateInfo(path.c_str()));
   5521   if (!ssi.has_value())
   5522     return false;
   5523 
   5524   if (global)
   5525   {
   5526     li->title = fmt::format(FSUI_FSTR("Global Slot {0} - {1}##global_slot_{0}"), slot, ssi->serial);
   5527   }
   5528   else
   5529   {
   5530     li->title = (slot > 0) ? fmt::format(FSUI_FSTR("Game Slot {0}##game_slot_{0}"), slot) : FSUI_STR("Game Quick Save");
   5531   }
   5532 
   5533   li->summary = fmt::format(FSUI_FSTR("Saved {:%c}"), fmt::localtime(ssi->timestamp));
   5534   li->timestamp = ssi->timestamp;
   5535   li->slot = slot;
   5536   li->path = std::move(path);
   5537   li->global = global;
   5538   if (ssi->screenshot.IsValid())
   5539     li->preview_texture = CreateTextureFromImage(ssi->screenshot);
   5540 
   5541   return true;
   5542 }
   5543 
   5544 void FullscreenUI::ClearSaveStateEntryList()
   5545 {
   5546   for (SaveStateListEntry& entry : s_save_state_selector_slots)
   5547   {
   5548     if (entry.preview_texture)
   5549       g_gpu_device->RecycleTexture(std::move(entry.preview_texture));
   5550   }
   5551   s_save_state_selector_slots.clear();
   5552 }
   5553 
   5554 u32 FullscreenUI::PopulateSaveStateListEntries(const std::string& title, const std::string& serial)
   5555 {
   5556   ClearSaveStateEntryList();
   5557 
   5558   if (s_save_state_selector_loading)
   5559   {
   5560     std::optional<ExtendedSaveStateInfo> ssi = System::GetUndoSaveStateInfo();
   5561     if (ssi)
   5562     {
   5563       SaveStateListEntry li;
   5564       li.title = FSUI_STR("Undo Load State");
   5565       li.summary = FSUI_STR("Restores the state of the system prior to the last state loaded.");
   5566       if (ssi->screenshot.IsValid())
   5567         li.preview_texture = CreateTextureFromImage(ssi->screenshot);
   5568       s_save_state_selector_slots.push_back(std::move(li));
   5569     }
   5570   }
   5571 
   5572   if (!serial.empty())
   5573   {
   5574     for (s32 i = 1; i <= System::PER_GAME_SAVE_STATE_SLOTS; i++)
   5575     {
   5576       SaveStateListEntry li;
   5577       if (InitializeSaveStateListEntryFromSerial(&li, serial, i, false) || !s_save_state_selector_loading)
   5578         s_save_state_selector_slots.push_back(std::move(li));
   5579     }
   5580   }
   5581 
   5582   for (s32 i = 1; i <= System::GLOBAL_SAVE_STATE_SLOTS; i++)
   5583   {
   5584     SaveStateListEntry li;
   5585     if (InitializeSaveStateListEntryFromSerial(&li, serial, i, true) || !s_save_state_selector_loading)
   5586       s_save_state_selector_slots.push_back(std::move(li));
   5587   }
   5588 
   5589   return static_cast<u32>(s_save_state_selector_slots.size());
   5590 }
   5591 
   5592 bool FullscreenUI::OpenLoadStateSelectorForGame(const std::string& game_path)
   5593 {
   5594   auto lock = GameList::GetLock();
   5595   const GameList::Entry* entry = GameList::GetEntryForPath(game_path);
   5596   if (entry)
   5597   {
   5598     s_save_state_selector_loading = true;
   5599     if (PopulateSaveStateListEntries(entry->title, entry->serial) > 0)
   5600     {
   5601       s_save_state_selector_open = true;
   5602       s_save_state_selector_resuming = false;
   5603       s_save_state_selector_game_path = game_path;
   5604       return true;
   5605     }
   5606   }
   5607 
   5608   ShowToast({}, FSUI_STR("No save states found."), 5.0f);
   5609   return false;
   5610 }
   5611 
   5612 bool FullscreenUI::OpenSaveStateSelector(bool is_loading)
   5613 {
   5614   s_save_state_selector_game_path = {};
   5615   s_save_state_selector_loading = is_loading;
   5616   s_save_state_selector_resuming = false;
   5617   if (PopulateSaveStateListEntries(System::GetGameTitle(), System::GetGameSerial()) > 0)
   5618   {
   5619     s_save_state_selector_open = true;
   5620     QueueResetFocus(FocusResetType::PopupOpened);
   5621     return true;
   5622   }
   5623 
   5624   ShowToast({}, FSUI_STR("No save states found."), 5.0f);
   5625   return false;
   5626 }
   5627 
   5628 void FullscreenUI::CloseSaveStateSelector()
   5629 {
   5630   if (s_save_state_selector_open)
   5631     QueueResetFocus(FocusResetType::PopupClosed);
   5632 
   5633   ClearSaveStateEntryList();
   5634   s_save_state_selector_open = false;
   5635   s_save_state_selector_loading = false;
   5636   s_save_state_selector_resuming = false;
   5637   s_save_state_selector_game_path = {};
   5638 }
   5639 
   5640 void FullscreenUI::DrawSaveStateSelector(bool is_loading)
   5641 {
   5642   ImGuiIO& io = ImGui::GetIO();
   5643 
   5644   ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
   5645   ImGui::SetNextWindowSize(io.DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT));
   5646 
   5647   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
   5648   ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
   5649   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
   5650   ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 0.0f);
   5651   ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f);
   5652 
   5653   const char* window_title = is_loading ? FSUI_CSTR("Load State") : FSUI_CSTR("Save State");
   5654   ImGui::OpenPopup(window_title);
   5655 
   5656   bool is_open = true;
   5657   const bool valid =
   5658     ImGui::BeginPopupModal(window_title, &is_open,
   5659                            ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
   5660                              ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoBackground);
   5661   if (!valid || !is_open)
   5662   {
   5663     if (valid)
   5664       ImGui::EndPopup();
   5665 
   5666     ImGui::PopStyleVar(5);
   5667     if (!is_open)
   5668     {
   5669       CloseSaveStateSelector();
   5670       ReturnToPreviousWindow();
   5671     }
   5672     return;
   5673   }
   5674 
   5675   const ImVec2 heading_size =
   5676     ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   5677                                (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f));
   5678 
   5679   ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIPrimaryColor, 0.9f));
   5680 
   5681   bool closed = false;
   5682   bool was_close_not_back = false;
   5683   if (ImGui::BeginChild("state_titlebar", heading_size, false, ImGuiWindowFlags_NavFlattened))
   5684   {
   5685     BeginNavBar();
   5686     if (NavButton(ICON_FA_BACKWARD, true, true))
   5687       closed = true;
   5688 
   5689     NavTitle(is_loading ? FSUI_CSTR("Load State") : FSUI_CSTR("Save State"));
   5690     EndNavBar();
   5691     ImGui::EndChild();
   5692   }
   5693 
   5694   ImGui::PopStyleColor();
   5695   ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIBackgroundColor, 0.9f));
   5696   ImGui::SetCursorPos(ImVec2(0.0f, heading_size.y));
   5697 
   5698   if (IsFocusResetFromWindowChange())
   5699     ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));
   5700 
   5701 
   5702   if (ImGui::BeginChild("state_list",
   5703                         ImVec2(io.DisplaySize.x, io.DisplaySize.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - heading_size.y),
   5704                         false, ImGuiWindowFlags_NavFlattened))
   5705   {
   5706     ResetFocusHere();
   5707     BeginMenuButtons();
   5708 
   5709     const ImGuiStyle& style = ImGui::GetStyle();
   5710 
   5711     const float title_spacing = LayoutScale(10.0f);
   5712     const float summary_spacing = LayoutScale(4.0f);
   5713     const float item_spacing = LayoutScale(20.0f);
   5714     const float item_width_with_spacing = std::floor(LayoutScale(LAYOUT_SCREEN_WIDTH / 4.0f));
   5715     const float item_width = item_width_with_spacing - item_spacing;
   5716     const float image_width = item_width - (style.FramePadding.x * 2.0f);
   5717     const float image_height = image_width / 1.33f;
   5718     const ImVec2 image_size(image_width, image_height);
   5719     const float item_height = (style.FramePadding.y * 2.0f) + image_height + title_spacing + g_large_font->FontSize +
   5720                               summary_spacing + g_medium_font->FontSize;
   5721     const ImVec2 item_size(item_width, item_height);
   5722     const u32 grid_count_x = static_cast<u32>(std::floor(ImGui::GetWindowWidth() / item_width_with_spacing));
   5723     const float start_x =
   5724       (static_cast<float>(ImGui::GetWindowWidth()) - (item_width_with_spacing * static_cast<float>(grid_count_x))) *
   5725       0.5f;
   5726 
   5727     u32 grid_x = 0;
   5728     ImGui::SetCursorPos(ImVec2(start_x, 0.0f));
   5729     for (u32 i = 0; i < s_save_state_selector_slots.size();)
   5730     {
   5731       SaveStateListEntry& entry = s_save_state_selector_slots[i];
   5732       if (static_cast<s32>(i) == s_save_state_selector_submenu_index)
   5733       {
   5734         // can't use a choice dialog here, because we're already in a modal...
   5735         ImGuiFullscreen::PushResetLayout();
   5736         ImGui::PushFont(g_large_font);
   5737         ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   5738         ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
   5739                             LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
   5740         ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   5741         ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   5742         ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
   5743         ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
   5744 
   5745         const float width = LayoutScale(600.0f);
   5746         const float title_height =
   5747           g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f;
   5748         const float height =
   5749           title_height +
   5750           LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * 3.0f;
   5751         ImGui::SetNextWindowSize(ImVec2(width, height));
   5752         ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   5753         ImGui::OpenPopup(entry.title.c_str());
   5754 
   5755         bool removed = false;
   5756         if (ImGui::BeginPopupModal(entry.title.c_str(), &is_open,
   5757                                    ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   5758         {
   5759           ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   5760 
   5761           BeginMenuButtons();
   5762 
   5763           if (ActiveButton(is_loading ? FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Load State") :
   5764                                         FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Save State"),
   5765                            false, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   5766           {
   5767             if (is_loading)
   5768               DoLoadState(std::move(entry.path));
   5769             else
   5770               DoSaveState(entry.slot, entry.global);
   5771 
   5772             closed = true;
   5773             was_close_not_back = true;
   5774           }
   5775 
   5776           if (!entry.path.empty() && ActiveButton(FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Delete Save"), false, true,
   5777                                                   LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   5778           {
   5779             if (!FileSystem::FileExists(entry.path.c_str()))
   5780             {
   5781               ShowToast({}, fmt::format(FSUI_FSTR("{} does not exist."), ImGuiFullscreen::RemoveHash(entry.title)));
   5782               is_open = true;
   5783             }
   5784             else if (FileSystem::DeleteFile(entry.path.c_str()))
   5785             {
   5786               ShowToast({}, fmt::format(FSUI_FSTR("{} deleted."), ImGuiFullscreen::RemoveHash(entry.title)));
   5787               s_save_state_selector_slots.erase(s_save_state_selector_slots.begin() + i);
   5788               removed = true;
   5789 
   5790               if (s_save_state_selector_slots.empty())
   5791               {
   5792                 closed = true;
   5793                 was_close_not_back = true;
   5794               }
   5795               else
   5796               {
   5797                 is_open = false;
   5798               }
   5799             }
   5800             else
   5801             {
   5802               ShowToast({}, fmt::format(FSUI_FSTR("Failed to delete {}."), ImGuiFullscreen::RemoveHash(entry.title)));
   5803               is_open = false;
   5804             }
   5805           }
   5806 
   5807           if (ActiveButton(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false, true,
   5808                            LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   5809           {
   5810             is_open = false;
   5811           }
   5812 
   5813           EndMenuButtons();
   5814 
   5815           ImGui::PopStyleColor();
   5816           ImGui::EndPopup();
   5817         }
   5818 
   5819         if (!is_open)
   5820           s_save_state_selector_submenu_index = -1;
   5821 
   5822         ImGui::PopStyleColor(3);
   5823         ImGui::PopStyleVar(3);
   5824         ImGui::PopFont();
   5825         ImGuiFullscreen::PopResetLayout();
   5826 
   5827         if (removed)
   5828           continue;
   5829       }
   5830 
   5831       ImGuiWindow* window = ImGui::GetCurrentWindow();
   5832       if (window->SkipItems)
   5833       {
   5834         i++;
   5835         continue;
   5836       }
   5837 
   5838       const ImGuiID id = window->GetID(static_cast<int>(i));
   5839       const ImVec2 pos(window->DC.CursorPos);
   5840       ImRect bb(pos, pos + item_size);
   5841       ImGui::ItemSize(item_size);
   5842       if (ImGui::ItemAdd(bb, id))
   5843       {
   5844         bool held;
   5845         bool hovered;
   5846         bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
   5847         if (hovered)
   5848         {
   5849           const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
   5850 
   5851           const float t = std::min(static_cast<float>(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f);
   5852           ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
   5853 
   5854           ImGuiFullscreen::DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   5855 
   5856           ImGui::PopStyleColor();
   5857         }
   5858 
   5859         bb.Min += style.FramePadding;
   5860         bb.Max -= style.FramePadding;
   5861 
   5862         GPUTexture* const screenshot =
   5863           entry.preview_texture ? entry.preview_texture.get() : GetCachedTextureAsync("no-save.png");
   5864         const ImRect image_rect(
   5865           CenterImage(ImRect(bb.Min, bb.Min + image_size),
   5866                       ImVec2(static_cast<float>(screenshot->GetWidth()), static_cast<float>(screenshot->GetHeight()))));
   5867 
   5868         ImGui::GetWindowDrawList()->AddImage(screenshot, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
   5869                                              ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
   5870 
   5871         const ImVec2 title_pos(bb.Min.x, bb.Min.y + image_height + title_spacing);
   5872         const ImRect title_bb(title_pos, ImVec2(bb.Max.x, title_pos.y + g_large_font->FontSize));
   5873         ImGui::PushFont(g_large_font);
   5874         ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
   5875                                  &title_bb);
   5876         ImGui::PopFont();
   5877 
   5878         if (!entry.summary.empty())
   5879         {
   5880           const ImVec2 summary_pos(bb.Min.x, title_pos.y + g_large_font->FontSize + summary_spacing);
   5881           const ImRect summary_bb(summary_pos, ImVec2(bb.Max.x, summary_pos.y + g_medium_font->FontSize));
   5882           ImGui::PushFont(g_medium_font);
   5883           ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, entry.summary.c_str(), nullptr, nullptr,
   5884                                    ImVec2(0.0f, 0.0f), &summary_bb);
   5885           ImGui::PopFont();
   5886         }
   5887 
   5888         if (pressed)
   5889         {
   5890           if (is_loading)
   5891             DoLoadState(entry.path);
   5892           else
   5893             DoSaveState(entry.slot, entry.global);
   5894 
   5895           closed = true;
   5896           was_close_not_back = true;
   5897         }
   5898         else if (hovered &&
   5899                  (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) ||
   5900                   ImGui::IsKeyPressed(ImGuiKey_F1, false)))
   5901         {
   5902           s_save_state_selector_submenu_index = static_cast<s32>(i);
   5903         }
   5904       }
   5905 
   5906       grid_x++;
   5907       if (grid_x == grid_count_x)
   5908       {
   5909         grid_x = 0;
   5910         ImGui::SetCursorPosX(start_x);
   5911         ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_spacing);
   5912       }
   5913       else
   5914       {
   5915         ImGui::SameLine(start_x + static_cast<float>(grid_x) * (item_width + item_spacing));
   5916       }
   5917 
   5918       i++;
   5919     }
   5920 
   5921     EndMenuButtons();
   5922     ImGui::EndChild();
   5923   }
   5924 
   5925   ImGui::PopStyleColor();
   5926 
   5927   ImGui::EndPopup();
   5928   ImGui::PopStyleVar(5);
   5929 
   5930   if (IsGamepadInputSource())
   5931   {
   5932     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD, FSUI_VSTR("Select State")),
   5933                                        std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Delete State")),
   5934                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Load State")),
   5935                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))});
   5936   }
   5937   else
   5938   {
   5939     SetFullscreenFooterText(std::array{
   5940       std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT,
   5941                      FSUI_VSTR("Select State")),
   5942       std::make_pair(ICON_PF_F1, FSUI_VSTR("Delete State")), std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Load State")),
   5943       std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))});
   5944   }
   5945 
   5946   if (WantsToCloseMenu() || closed)
   5947   {
   5948     CloseSaveStateSelector();
   5949     if (was_close_not_back)
   5950       ReturnToMainWindow();
   5951     else if (s_current_main_window != MainWindowType::GameList)
   5952       ReturnToPreviousWindow();
   5953   }
   5954 }
   5955 
   5956 bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry)
   5957 {
   5958   SaveStateListEntry slentry;
   5959   if (!InitializeSaveStateListEntryFromSerial(&slentry, entry->serial, -1, false))
   5960     return false;
   5961 
   5962   CloseSaveStateSelector();
   5963   s_save_state_selector_slots.push_back(std::move(slentry));
   5964   s_save_state_selector_game_path = entry->path;
   5965   s_save_state_selector_loading = true;
   5966   s_save_state_selector_open = true;
   5967   s_save_state_selector_resuming = true;
   5968   QueueResetFocus(FocusResetType::PopupOpened);
   5969   return true;
   5970 }
   5971 
   5972 void FullscreenUI::DrawResumeStateSelector()
   5973 {
   5974   ImGui::SetNextWindowSize(LayoutScale(800.0f, 602.0f));
   5975   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   5976   ImGui::OpenPopup(FSUI_CSTR("Load Resume State"));
   5977 
   5978   ImGui::PushFont(g_large_font);
   5979   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   5980   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   5981 
   5982   bool is_open = true;
   5983   if (ImGui::BeginPopupModal(FSUI_CSTR("Load Resume State"), &is_open,
   5984                              ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
   5985   {
   5986     SaveStateListEntry& entry = s_save_state_selector_slots.front();
   5987     SmallString time;
   5988     TimeToPrintableString(&time, entry.timestamp);
   5989     ImGui::TextWrapped(
   5990       FSUI_CSTR("A resume save state created at %s was found.\n\nDo you want to load this save and continue?"),
   5991       time.c_str());
   5992 
   5993     const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get();
   5994     const float image_height = LayoutScale(250.0f);
   5995     const float image_width =
   5996       image_height * (static_cast<float>(image->GetWidth()) / static_cast<float>(image->GetHeight()));
   5997     const ImVec2 pos(ImGui::GetCursorScreenPos() +
   5998                      ImVec2((ImGui::GetCurrentWindow()->WorkRect.GetWidth() - image_width) * 0.5f, LayoutScale(20.0f)));
   5999     const ImRect image_bb(pos, pos + ImVec2(image_width, image_height));
   6000     ImGui::GetWindowDrawList()->AddImage(
   6001       static_cast<ImTextureID>(entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get()),
   6002       image_bb.Min, image_bb.Max);
   6003 
   6004     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + image_height + LayoutScale(40.0f));
   6005 
   6006     BeginMenuButtons();
   6007 
   6008     if (ActiveButton(FSUI_ICONSTR(ICON_FA_PLAY, "Load State"), false))
   6009     {
   6010       DoStartPath(s_save_state_selector_game_path, std::move(entry.path));
   6011       is_open = false;
   6012     }
   6013 
   6014     if (ActiveButton(FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Clean Boot"), false))
   6015     {
   6016       DoStartPath(s_save_state_selector_game_path);
   6017       is_open = false;
   6018     }
   6019 
   6020     if (ActiveButton(FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Delete State"), false))
   6021     {
   6022       if (FileSystem::DeleteFile(entry.path.c_str()))
   6023       {
   6024         DoStartPath(s_save_state_selector_game_path);
   6025         is_open = false;
   6026       }
   6027       else
   6028       {
   6029         ShowToast(std::string(), FSUI_STR("Failed to delete save state."));
   6030       }
   6031     }
   6032 
   6033     if (ActiveButton(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Cancel"), false) || WantsToCloseMenu())
   6034     {
   6035       ImGui::CloseCurrentPopup();
   6036       is_open = false;
   6037     }
   6038     EndMenuButtons();
   6039 
   6040     ImGui::EndPopup();
   6041   }
   6042 
   6043   ImGui::PopStyleVar(2);
   6044   ImGui::PopFont();
   6045 
   6046   if (!is_open)
   6047   {
   6048     ClearSaveStateEntryList();
   6049     s_save_state_selector_open = false;
   6050     s_save_state_selector_loading = false;
   6051     s_save_state_selector_resuming = false;
   6052     s_save_state_selector_game_path = {};
   6053   }
   6054   else
   6055   {
   6056     SetStandardSelectionFooterText(false);
   6057   }
   6058 }
   6059 
   6060 void FullscreenUI::DoLoadState(std::string path)
   6061 {
   6062   Host::RunOnCPUThread([boot_path = s_save_state_selector_game_path, path = std::move(path)]() {
   6063     CloseSaveStateSelector();
   6064 
   6065     if (System::IsValid())
   6066     {
   6067       if (path.empty())
   6068       {
   6069         // Loading undo state.
   6070         if (!System::UndoLoadState())
   6071           ShowToast(std::string(), TRANSLATE_STR("System", "Failed to undo load state."));
   6072       }
   6073       else
   6074       {
   6075         Error error;
   6076         if (!System::LoadState(path.c_str(), &error, true))
   6077         {
   6078           ShowToast(std::string(),
   6079                     fmt::format(TRANSLATE_FS("System", "Failed to load state: {}"), error.GetDescription()));
   6080         }
   6081       }
   6082     }
   6083     else
   6084     {
   6085       DoStartPath(std::move(boot_path), std::move(path));
   6086     }
   6087   });
   6088 }
   6089 
   6090 void FullscreenUI::DoSaveState(s32 slot, bool global)
   6091 {
   6092   Host::RunOnCPUThread([slot, global]() {
   6093     CloseSaveStateSelector();
   6094     if (!System::IsValid())
   6095       return;
   6096 
   6097     std::string filename(global ? System::GetGlobalSaveStateFileName(slot) :
   6098                                   System::GetGameSaveStateFileName(System::GetGameSerial(), slot));
   6099     Error error;
   6100     if (!System::SaveState(filename.c_str(), &error, g_settings.create_save_state_backups))
   6101     {
   6102       ShowToast(std::string(), fmt::format(TRANSLATE_FS("System", "Failed to save state: {}"), error.GetDescription()));
   6103     }
   6104   });
   6105 }
   6106 
   6107 void FullscreenUI::PopulateGameListEntryList()
   6108 {
   6109   const s32 sort = Host::GetBaseIntSettingValue("Main", "FullscreenUIGameSort", 0);
   6110   const bool reverse = Host::GetBaseBoolSettingValue("Main", "FullscreenUIGameSortReverse", false);
   6111   const bool merge_disc_sets = Host::GetBaseBoolSettingValue("Main", "FullscreenUIMergeDiscSets", true);
   6112 
   6113   const u32 count = GameList::GetEntryCount();
   6114   s_game_list_sorted_entries.clear();
   6115   s_game_list_sorted_entries.reserve(count);
   6116   for (u32 i = 0; i < count; i++)
   6117   {
   6118     const GameList::Entry* entry = GameList::GetEntryByIndex(i);
   6119     if (merge_disc_sets)
   6120     {
   6121       if (entry->disc_set_member)
   6122         continue;
   6123     }
   6124     else
   6125     {
   6126       if (entry->IsDiscSet())
   6127         continue;
   6128     }
   6129 
   6130     s_game_list_sorted_entries.push_back(entry);
   6131   }
   6132 
   6133   std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(),
   6134             [sort, reverse](const GameList::Entry* lhs, const GameList::Entry* rhs) {
   6135               switch (sort)
   6136               {
   6137                 case 0: // Type
   6138                 {
   6139                   const GameList::EntryType lst = lhs->GetSortType();
   6140                   const GameList::EntryType rst = rhs->GetSortType();
   6141                   if (lst != rst)
   6142                     return reverse ? (lst > rst) : (lst < rst);
   6143                 }
   6144                 break;
   6145 
   6146                 case 1: // Serial
   6147                 {
   6148                   if (lhs->serial != rhs->serial)
   6149                     return reverse ? (lhs->serial > rhs->serial) : (lhs->serial < rhs->serial);
   6150                 }
   6151                 break;
   6152 
   6153                 case 2: // Title
   6154                   break;
   6155 
   6156                 case 3: // File Title
   6157                 {
   6158                   const std::string_view lhs_title(Path::GetFileTitle(lhs->path));
   6159                   const std::string_view rhs_title(Path::GetFileTitle(rhs->path));
   6160                   const int res = StringUtil::Strncasecmp(lhs_title.data(), rhs_title.data(),
   6161                                                           std::min(lhs_title.size(), rhs_title.size()));
   6162                   if (res != 0)
   6163                     return reverse ? (res > 0) : (res < 0);
   6164                 }
   6165                 break;
   6166 
   6167                 case 4: // Time Played
   6168                 {
   6169                   if (lhs->total_played_time != rhs->total_played_time)
   6170                   {
   6171                     return reverse ? (lhs->total_played_time > rhs->total_played_time) :
   6172                                      (lhs->total_played_time < rhs->total_played_time);
   6173                   }
   6174                 }
   6175                 break;
   6176 
   6177                 case 5: // Last Played (reversed by default)
   6178                 {
   6179                   if (lhs->last_played_time != rhs->last_played_time)
   6180                   {
   6181                     return reverse ? (lhs->last_played_time < rhs->last_played_time) :
   6182                                      (lhs->last_played_time > rhs->last_played_time);
   6183                   }
   6184                 }
   6185                 break;
   6186 
   6187                 case 6: // File Size
   6188                 {
   6189                   if (lhs->file_size != rhs->file_size)
   6190                   {
   6191                     return reverse ? (lhs->file_size > rhs->file_size) : (lhs->file_size < rhs->file_size);
   6192                   }
   6193                 }
   6194                 break;
   6195 
   6196                 case 7: // Uncompressed Size
   6197                 {
   6198                   if (lhs->uncompressed_size != rhs->uncompressed_size)
   6199                   {
   6200                     return reverse ? (lhs->uncompressed_size > rhs->uncompressed_size) :
   6201                                      (lhs->uncompressed_size < rhs->uncompressed_size);
   6202                   }
   6203                 }
   6204                 break;
   6205               }
   6206 
   6207               // fallback to title when all else is equal
   6208               const int res = StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str());
   6209               return reverse ? (res > 0) : (res < 0);
   6210             });
   6211 }
   6212 
   6213 void FullscreenUI::DrawGameListWindow()
   6214 {
   6215   auto game_list_lock = GameList::GetLock();
   6216   PopulateGameListEntryList();
   6217 
   6218   ImGuiIO& io = ImGui::GetIO();
   6219   const ImVec2 heading_size =
   6220     ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   6221                                (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f));
   6222 
   6223   const float bg_alpha = System::IsValid() ? 0.90f : 1.0f;
   6224 
   6225   if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", MulAlpha(UIPrimaryColor, bg_alpha)))
   6226   {
   6227     static constexpr float ITEM_WIDTH = 25.0f;
   6228     static constexpr const char* icons[] = {ICON_FA_BORDER_ALL, ICON_FA_LIST};
   6229     static constexpr const char* titles[] = {FSUI_NSTR("Game Grid"), FSUI_NSTR("Game List")};
   6230     static constexpr u32 count = static_cast<u32>(std::size(titles));
   6231 
   6232     BeginNavBar();
   6233 
   6234     if (NavButton(ICON_FA_BACKWARD, true, true))
   6235       ReturnToPreviousWindow();
   6236 
   6237     NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast<u32>(s_game_list_view)]));
   6238     RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   6239 
   6240     for (u32 i = 0; i < count; i++)
   6241     {
   6242       if (NavButton(icons[i], static_cast<GameListView>(i) == s_game_list_view, true, ITEM_WIDTH,
   6243                     LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   6244       {
   6245         s_game_list_view = static_cast<GameListView>(i);
   6246       }
   6247     }
   6248 
   6249     EndNavBar();
   6250   }
   6251 
   6252   EndFullscreenWindow();
   6253 
   6254   if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadInput, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false))
   6255   {
   6256     s_game_list_view = (s_game_list_view == GameListView::Grid) ? GameListView::List : GameListView::Grid;
   6257   }
   6258   else if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F2))
   6259   {
   6260     s_current_main_window = MainWindowType::GameListSettings;
   6261     QueueResetFocus(FocusResetType::ViewChanged);
   6262   }
   6263 
   6264   switch (s_game_list_view)
   6265   {
   6266     case GameListView::Grid:
   6267       DrawGameGrid(heading_size);
   6268       break;
   6269     case GameListView::List:
   6270       DrawGameList(heading_size);
   6271       break;
   6272     default:
   6273       break;
   6274   }
   6275 
   6276   if (IsGamepadInputSource())
   6277   {
   6278     SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD, FSUI_VSTR("Select Game")),
   6279                                        std::make_pair(ICON_PF_BUTTON_X, FSUI_VSTR("Change View")),
   6280                                        std::make_pair(ICON_PF_BURGER_MENU, FSUI_VSTR("Settings")),
   6281                                        std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Launch Options")),
   6282                                        std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Start Game")),
   6283                                        std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))});
   6284   }
   6285   else
   6286   {
   6287     SetFullscreenFooterText(std::array{
   6288       std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT,
   6289                      FSUI_VSTR("Select Game")),
   6290       std::make_pair(ICON_PF_F1, FSUI_VSTR("Change View")), std::make_pair(ICON_PF_F2, FSUI_VSTR("Settings")),
   6291       std::make_pair(ICON_PF_F3, FSUI_VSTR("Launch Options")), std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Start Game")),
   6292       std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))});
   6293   }
   6294 }
   6295 
   6296 void FullscreenUI::DrawGameList(const ImVec2& heading_size)
   6297 {
   6298   if (!BeginFullscreenColumns(nullptr, heading_size.y, true, true))
   6299   {
   6300     EndFullscreenColumns();
   6301     return;
   6302   }
   6303 
   6304   if (!AreAnyDialogsOpen() && WantsToCloseMenu())
   6305     ReturnToPreviousWindow();
   6306 
   6307   auto game_list_lock = GameList::GetLock();
   6308   const GameList::Entry* selected_entry = nullptr;
   6309   PopulateGameListEntryList();
   6310 
   6311   if (IsFocusResetFromWindowChange())
   6312     ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));
   6313 
   6314   if (BeginFullscreenColumnWindow(0.0f, -530.0f, "game_list_entries"))
   6315   {
   6316     const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT));
   6317 
   6318     ResetFocusHere();
   6319 
   6320     BeginMenuButtons();
   6321 
   6322     SmallString summary;
   6323 
   6324     for (const GameList::Entry* entry : s_game_list_sorted_entries)
   6325     {
   6326       ImRect bb;
   6327       bool visible, hovered;
   6328       bool pressed =
   6329         MenuButtonFrame(entry->path.c_str(), true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max);
   6330       if (!visible)
   6331         continue;
   6332 
   6333       GPUTexture* cover_texture = GetGameListCover(entry);
   6334 
   6335       if (entry->serial.empty())
   6336         summary.format("{} - ", Settings::GetDiscRegionDisplayName(entry->region));
   6337       else
   6338         summary.format("{} - {} - ", entry->serial, Settings::GetDiscRegionDisplayName(entry->region));
   6339 
   6340       summary.append(Path::GetFileName(entry->path));
   6341 
   6342       const ImRect image_rect(
   6343         CenterImage(ImRect(bb.Min, bb.Min + image_size), ImVec2(static_cast<float>(cover_texture->GetWidth()),
   6344                                                                 static_cast<float>(cover_texture->GetHeight()))));
   6345 
   6346       ImGui::GetWindowDrawList()->AddImage(cover_texture, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
   6347                                            ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
   6348 
   6349       const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
   6350       const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f);
   6351       const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
   6352       const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);
   6353 
   6354       ImGui::PushFont(g_large_font);
   6355       ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry->title.c_str(),
   6356                                entry->title.c_str() + entry->title.size(), nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   6357       ImGui::PopFont();
   6358 
   6359       if (!summary.empty())
   6360       {
   6361         ImGui::PushFont(g_medium_font);
   6362         ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary.c_str(), summary.end_ptr(), nullptr,
   6363                                  ImVec2(0.0f, 0.0f), &summary_bb);
   6364         ImGui::PopFont();
   6365       }
   6366 
   6367       if (pressed)
   6368       {
   6369         HandleGameListActivate(entry);
   6370       }
   6371       else
   6372       {
   6373         if (hovered)
   6374           selected_entry = entry;
   6375 
   6376         if (selected_entry &&
   6377             (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) ||
   6378              ImGui::IsKeyPressed(ImGuiKey_F3, false)))
   6379         {
   6380           HandleGameListOptions(selected_entry);
   6381         }
   6382       }
   6383     }
   6384 
   6385     EndMenuButtons();
   6386   }
   6387   EndFullscreenColumnWindow();
   6388 
   6389   if (BeginFullscreenColumnWindow(-530.0f, 0.0f, "game_list_info", UIPrimaryDarkColor))
   6390   {
   6391     const GPUTexture* cover_texture =
   6392       selected_entry ? GetGameListCover(selected_entry) : GetTextureForGameListEntryType(GameList::EntryType::Count);
   6393     if (cover_texture)
   6394     {
   6395       const ImRect image_rect(
   6396         CenterImage(LayoutScale(ImVec2(350.0f, 350.0f)), ImVec2(static_cast<float>(cover_texture->GetWidth()),
   6397                                                                 static_cast<float>(cover_texture->GetHeight()))));
   6398 
   6399       ImGui::SetCursorPos(LayoutScale(ImVec2(90.0f, 0.0f)) + image_rect.Min);
   6400       ImGui::Image(selected_entry ? GetGameListCover(selected_entry) :
   6401                                     GetTextureForGameListEntryType(GameList::EntryType::Count),
   6402                    image_rect.GetSize());
   6403     }
   6404 
   6405     const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   6406     constexpr float field_margin_y = 10.0f;
   6407     constexpr float start_x = 50.0f;
   6408     float text_y = 400.0f;
   6409     float text_width;
   6410 
   6411     PushPrimaryColor();
   6412     ImGui::SetCursorPos(LayoutScale(start_x, text_y));
   6413     ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y));
   6414     ImGui::BeginGroup();
   6415 
   6416     if (selected_entry)
   6417     {
   6418       // title
   6419       ImGui::PushFont(g_large_font);
   6420       text_width = ImGui::CalcTextSize(selected_entry->title.c_str(), nullptr, false, work_width).x;
   6421       ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
   6422       ImGui::TextWrapped("%s", selected_entry->title.c_str());
   6423       ImGui::PopFont();
   6424 
   6425       ImGui::PushFont(g_medium_font);
   6426 
   6427       // developer
   6428       if (!selected_entry->developer.empty())
   6429       {
   6430         text_width =
   6431           ImGui::CalcTextSize(selected_entry->developer.c_str(),
   6432                               selected_entry->developer.c_str() + selected_entry->developer.length(), false, work_width)
   6433             .x;
   6434         ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
   6435         ImGui::TextWrapped("%s", selected_entry->developer.c_str());
   6436       }
   6437 
   6438       // code
   6439       text_width = ImGui::CalcTextSize(selected_entry->serial.c_str(), nullptr, false, work_width).x;
   6440       ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
   6441       ImGui::TextWrapped("%s", selected_entry->serial.c_str());
   6442       ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 15.0f);
   6443 
   6444       // region
   6445       {
   6446         const TinyString flag_texture =
   6447           TinyString::from_format("fullscreenui/{}.png", Settings::GetDiscRegionName(selected_entry->region));
   6448         ImGui::TextUnformatted(FSUI_CSTR("Region: "));
   6449         ImGui::SameLine();
   6450         ImGui::Image(GetCachedTextureAsync(flag_texture.c_str()), LayoutScale(23.0f, 16.0f));
   6451         ImGui::SameLine();
   6452         ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region));
   6453       }
   6454 
   6455       // genre
   6456       ImGui::Text(FSUI_CSTR("Genre: %s"), selected_entry->genre.c_str());
   6457 
   6458       // release date
   6459       char release_date_str[64];
   6460       selected_entry->GetReleaseDateString(release_date_str, sizeof(release_date_str));
   6461       ImGui::Text(FSUI_CSTR("Release Date: %s"), release_date_str);
   6462 
   6463       // compatibility
   6464       ImGui::TextUnformatted(FSUI_CSTR("Compatibility: "));
   6465       ImGui::SameLine();
   6466       if (selected_entry->compatibility != GameDatabase::CompatibilityRating::Unknown)
   6467       {
   6468         ImGui::Image(s_game_compatibility_textures[static_cast<u32>(selected_entry->compatibility)].get(),
   6469                      LayoutScale(64.0f, 16.0f));
   6470         ImGui::SameLine();
   6471       }
   6472       ImGui::Text(" (%s)", GameDatabase::GetCompatibilityRatingDisplayName(selected_entry->compatibility));
   6473 
   6474       // play time
   6475       ImGui::Text(FSUI_CSTR("Time Played: %s"), GameList::FormatTimespan(selected_entry->total_played_time).c_str());
   6476       ImGui::Text(FSUI_CSTR("Last Played: %s"), GameList::FormatTimestamp(selected_entry->last_played_time).c_str());
   6477 
   6478       // size
   6479       if (selected_entry->file_size >= 0)
   6480         ImGui::Text(FSUI_CSTR("File Size: %.2f MB"), static_cast<float>(selected_entry->file_size) / 1048576.0f);
   6481       else
   6482         ImGui::TextUnformatted(FSUI_CSTR("Unknown File Size"));
   6483       ImGui::Text(FSUI_CSTR("Uncompressed Size: %.2f MB"),
   6484                   static_cast<float>(selected_entry->uncompressed_size) / 1048576.0f);
   6485 
   6486       ImGui::PopFont();
   6487     }
   6488     else
   6489     {
   6490       // title
   6491       const char* title = FSUI_CSTR("No Game Selected");
   6492       ImGui::PushFont(g_large_font);
   6493       text_width = ImGui::CalcTextSize(title, nullptr, false, work_width).x;
   6494       ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
   6495       ImGui::TextWrapped("%s", title);
   6496       ImGui::PopFont();
   6497     }
   6498 
   6499     ImGui::EndGroup();
   6500     ImGui::PopStyleVar();
   6501     PopPrimaryColor();
   6502   }
   6503   EndFullscreenColumnWindow();
   6504 
   6505   EndFullscreenColumns();
   6506 }
   6507 
   6508 void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
   6509 {
   6510   if (IsFocusResetFromWindowChange())
   6511     ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));
   6512 
   6513   ImGuiIO& io = ImGui::GetIO();
   6514   if (!BeginFullscreenWindow(
   6515         ImVec2(0.0f, heading_size.y),
   6516         ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "game_grid",
   6517         UIBackgroundColor))
   6518   {
   6519     EndFullscreenWindow();
   6520     return;
   6521   }
   6522 
   6523   if (ImGui::IsWindowFocused() && WantsToCloseMenu())
   6524     ReturnToPreviousWindow();
   6525 
   6526   ResetFocusHere();
   6527   BeginMenuButtons();
   6528 
   6529   const ImGuiStyle& style = ImGui::GetStyle();
   6530 
   6531   const float title_spacing = LayoutScale(10.0f);
   6532   const float item_spacing = LayoutScale(20.0f);
   6533   const float item_width_with_spacing = std::floor(LayoutScale(LAYOUT_SCREEN_WIDTH / 5.0f));
   6534   const float item_width = item_width_with_spacing - item_spacing;
   6535   const float image_width = item_width - (style.FramePadding.x * 2.0f);
   6536   const float image_height = image_width;
   6537   const ImVec2 image_size(image_width, image_height);
   6538   const float item_height = (style.FramePadding.y * 2.0f) + image_height + title_spacing + g_medium_font->FontSize;
   6539   const ImVec2 item_size(item_width, item_height);
   6540   const u32 grid_count_x = static_cast<u32>(std::floor(ImGui::GetWindowWidth() / item_width_with_spacing));
   6541   const float start_x =
   6542     (static_cast<float>(ImGui::GetWindowWidth()) - (item_width_with_spacing * static_cast<float>(grid_count_x))) * 0.5f;
   6543 
   6544   SmallString draw_title;
   6545 
   6546   u32 grid_x = 0;
   6547   ImGui::SetCursorPos(ImVec2(start_x, 0.0f));
   6548   for (const GameList::Entry* entry : s_game_list_sorted_entries)
   6549   {
   6550     ImGuiWindow* window = ImGui::GetCurrentWindow();
   6551     if (window->SkipItems)
   6552       continue;
   6553 
   6554     const ImGuiID id = window->GetID(entry->path.c_str(), entry->path.c_str() + entry->path.length());
   6555     const ImVec2 pos(window->DC.CursorPos);
   6556     ImRect bb(pos, pos + item_size);
   6557     ImGui::ItemSize(item_size);
   6558     if (ImGui::ItemAdd(bb, id))
   6559     {
   6560       bool held;
   6561       bool hovered;
   6562       bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
   6563       if (hovered)
   6564       {
   6565         const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
   6566 
   6567         const float t = static_cast<float>(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0));
   6568         ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
   6569 
   6570         ImGuiFullscreen::DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   6571 
   6572         ImGui::PopStyleColor();
   6573       }
   6574 
   6575       bb.Min += style.FramePadding;
   6576       bb.Max -= style.FramePadding;
   6577 
   6578       GPUTexture* const cover_texture = GetGameListCover(entry);
   6579       const ImRect image_rect(
   6580         CenterImage(ImRect(bb.Min, bb.Min + image_size), ImVec2(static_cast<float>(cover_texture->GetWidth()),
   6581                                                                 static_cast<float>(cover_texture->GetHeight()))));
   6582 
   6583       ImGui::GetWindowDrawList()->AddImage(cover_texture, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
   6584                                            ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
   6585 
   6586       const ImRect title_bb(ImVec2(bb.Min.x, bb.Min.y + image_height + title_spacing), bb.Max);
   6587       const std::string_view title(
   6588         std::string_view(entry->title).substr(0, (entry->title.length() > 31) ? 31 : std::string_view::npos));
   6589       draw_title.format("{}{}", title, (title.length() == entry->title.length()) ? "" : "...");
   6590       ImGui::PushFont(g_medium_font);
   6591       ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, draw_title.c_str(), draw_title.end_ptr(), nullptr,
   6592                                ImVec2(0.5f, 0.0f), &title_bb);
   6593       ImGui::PopFont();
   6594 
   6595       if (pressed)
   6596       {
   6597         HandleGameListActivate(entry);
   6598       }
   6599       else if (hovered &&
   6600                (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) ||
   6601                 ImGui::IsKeyPressed(ImGuiKey_F3, false)))
   6602       {
   6603         HandleGameListOptions(entry);
   6604       }
   6605     }
   6606 
   6607     grid_x++;
   6608     if (grid_x == grid_count_x)
   6609     {
   6610       grid_x = 0;
   6611       ImGui::SetCursorPosX(start_x);
   6612       ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_spacing);
   6613     }
   6614     else
   6615     {
   6616       ImGui::SameLine(start_x + static_cast<float>(grid_x) * (item_width + item_spacing));
   6617     }
   6618   }
   6619 
   6620   EndMenuButtons();
   6621   EndFullscreenWindow();
   6622 }
   6623 
   6624 void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
   6625 {
   6626   if (entry->IsDiscSet())
   6627   {
   6628     HandleSelectDiscForDiscSet(entry->path);
   6629     return;
   6630   }
   6631 
   6632   // launch game
   6633   if (!OpenLoadStateSelectorForGameResume(entry))
   6634     DoStartPath(entry->path);
   6635 }
   6636 
   6637 void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
   6638 {
   6639   if (!entry->IsDiscSet())
   6640   {
   6641     ImGuiFullscreen::ChoiceDialogOptions options = {
   6642       {FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
   6643       {FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open Containing Directory"), false},
   6644       {FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false},
   6645       {FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false},
   6646       {FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Default Boot"), false},
   6647       {FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Fast Boot"), false},
   6648       {FSUI_ICONSTR(ICON_FA_MAGIC, "Slow Boot"), false},
   6649       {FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Reset Play Time"), false},
   6650       {FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
   6651     };
   6652 
   6653     OpenChoiceDialog(
   6654       entry->title.c_str(), false, std::move(options),
   6655       [entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) {
   6656         switch (index)
   6657         {
   6658           case 0: // Open Game Properties
   6659             SwitchToGameSettingsForPath(entry_path);
   6660             break;
   6661           case 1: // Open Containing Directory
   6662             ExitFullscreenAndOpenURL(Path::CreateFileURL(Path::GetDirectory(entry_path)));
   6663             break;
   6664           case 2: // Resume Game
   6665             DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1));
   6666             break;
   6667           case 3: // Load State
   6668             OpenLoadStateSelectorForGame(entry_path);
   6669             break;
   6670           case 4: // Default Boot
   6671             DoStartPath(entry_path);
   6672             break;
   6673           case 5: // Fast Boot
   6674             DoStartPath(entry_path, {}, true);
   6675             break;
   6676           case 6: // Slow Boot
   6677             DoStartPath(entry_path, {}, false);
   6678             break;
   6679           case 7: // Reset Play Time
   6680             GameList::ClearPlayedTimeForSerial(entry_serial);
   6681             break;
   6682           default:
   6683             break;
   6684         }
   6685 
   6686         CloseChoiceDialog();
   6687       });
   6688   }
   6689   else
   6690   {
   6691     // shouldn't fail
   6692     const GameList::Entry* first_disc_entry = GameList::GetFirstDiscSetMember(entry->path);
   6693     if (!first_disc_entry)
   6694       return;
   6695 
   6696     ImGuiFullscreen::ChoiceDialogOptions options = {
   6697       {FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
   6698       {FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc"), false},
   6699       {FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
   6700     };
   6701 
   6702     OpenChoiceDialog(entry->title.c_str(), false, std::move(options),
   6703                      [entry_path = first_disc_entry->path,
   6704                       disc_set_name = entry->path](s32 index, const std::string& title, bool checked) {
   6705                        switch (index)
   6706                        {
   6707                          case 0: // Open Game Properties
   6708                            SwitchToGameSettingsForPath(entry_path);
   6709                            break;
   6710                          case 1: // Select Disc
   6711                            HandleSelectDiscForDiscSet(disc_set_name);
   6712                            break;
   6713                          default:
   6714                            break;
   6715                        }
   6716 
   6717                        CloseChoiceDialog();
   6718                      });
   6719   }
   6720 }
   6721 
   6722 void FullscreenUI::HandleSelectDiscForDiscSet(std::string_view disc_set_name)
   6723 {
   6724   auto lock = GameList::GetLock();
   6725   const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name, true);
   6726   if (entries.empty())
   6727     return;
   6728 
   6729   ImGuiFullscreen::ChoiceDialogOptions options;
   6730   std::vector<std::string> paths;
   6731   paths.reserve(entries.size());
   6732 
   6733   for (const GameList::Entry* entry : entries)
   6734   {
   6735     std::string title = fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Disc {} | {}")),
   6736                                     entry->disc_set_index + 1, Path::GetFileName(entry->path));
   6737     options.emplace_back(std::move(title), false);
   6738     paths.push_back(entry->path);
   6739   }
   6740   options.emplace_back(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false);
   6741 
   6742   OpenChoiceDialog(SmallString::from_format("Select Disc for {}", disc_set_name), false, std::move(options),
   6743                    [paths = std::move(paths)](s32 index, const std::string& title, bool checked) {
   6744                      if (static_cast<u32>(index) < paths.size())
   6745                      {
   6746                        auto lock = GameList::GetLock();
   6747                        const GameList::Entry* entry = GameList::GetEntryForPath(paths[index]);
   6748                        if (entry)
   6749                          HandleGameListActivate(entry);
   6750                      }
   6751 
   6752                      CloseChoiceDialog();
   6753                    });
   6754 }
   6755 
   6756 void FullscreenUI::DrawGameListSettingsWindow()
   6757 {
   6758   ImGuiIO& io = ImGui::GetIO();
   6759   const ImVec2 heading_size =
   6760     ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   6761                                (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f));
   6762 
   6763   const float bg_alpha = System::IsValid() ? 0.90f : 1.0f;
   6764 
   6765   if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", MulAlpha(UIPrimaryColor, bg_alpha)))
   6766   {
   6767     BeginNavBar();
   6768 
   6769     if (NavButton(ICON_FA_BACKWARD, true, true))
   6770     {
   6771       s_current_main_window = MainWindowType::GameList;
   6772       QueueResetFocus(FocusResetType::Other);
   6773     }
   6774 
   6775     NavTitle(FSUI_CSTR("Game List Settings"));
   6776     EndNavBar();
   6777   }
   6778 
   6779   EndFullscreenWindow();
   6780 
   6781   if (!BeginFullscreenWindow(
   6782         ImVec2(0.0f, heading_size.y),
   6783         ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)),
   6784         "settings_parent", UIBackgroundColor, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f)))
   6785   {
   6786     EndFullscreenWindow();
   6787     return;
   6788   }
   6789 
   6790   if (ImGui::IsWindowFocused() && WantsToCloseMenu())
   6791   {
   6792     s_current_main_window = MainWindowType::GameList;
   6793     QueueResetFocus(FocusResetType::ViewChanged);
   6794   }
   6795 
   6796   auto lock = Host::GetSettingsLock();
   6797   SettingsInterface* bsi = GetEditingSettingsInterface(false);
   6798 
   6799   BeginMenuButtons();
   6800 
   6801   MenuHeading(FSUI_CSTR("Search Directories"));
   6802   if (MenuButton(FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Add Search Directory"),
   6803                  FSUI_CSTR("Adds a new directory to the game search list.")))
   6804   {
   6805     OpenFileSelector(FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Add Search Directory"), true, [](const std::string& dir) {
   6806       if (!dir.empty())
   6807       {
   6808         auto lock = Host::GetSettingsLock();
   6809         SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer();
   6810 
   6811         bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str());
   6812         bsi->RemoveFromStringList("GameList", "Paths", dir.c_str());
   6813         SetSettingsChanged(bsi);
   6814         PopulateGameListDirectoryCache(bsi);
   6815         Host::RefreshGameListAsync(false);
   6816       }
   6817 
   6818       CloseFileSelector();
   6819     });
   6820   }
   6821 
   6822   for (const auto& it : s_game_list_directories_cache)
   6823   {
   6824     if (MenuButton(SmallString::from_format(ICON_FA_FOLDER " {}", it.first),
   6825                    it.second ? FSUI_CSTR("Scanning Subdirectories") : FSUI_CSTR("Not Scanning Subdirectories")))
   6826     {
   6827       ImGuiFullscreen::ChoiceDialogOptions options = {
   6828         {FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open in File Browser"), false},
   6829         {it.second ? (FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Disable Subdirectory Scanning")) :
   6830                      (FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Enable Subdirectory Scanning")),
   6831          false},
   6832         {FSUI_ICONSTR(ICON_FA_TIMES, "Remove From List"), false},
   6833         {FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
   6834       };
   6835 
   6836       OpenChoiceDialog(it.first.c_str(), false, std::move(options),
   6837                        [dir = it.first, recursive = it.second](s32 index, const std::string& title, bool checked) {
   6838                          if (index < 0)
   6839                            return;
   6840 
   6841                          if (index == 0)
   6842                          {
   6843                            // Open in file browser
   6844                            ExitFullscreenAndOpenURL(Path::CreateFileURL(dir));
   6845                          }
   6846                          else if (index == 1)
   6847                          {
   6848                            // toggle subdirectory scanning
   6849                            {
   6850                              auto lock = Host::GetSettingsLock();
   6851                              SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer();
   6852                              if (!recursive)
   6853                              {
   6854                                bsi->RemoveFromStringList("GameList", "Paths", dir.c_str());
   6855                                bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str());
   6856                              }
   6857                              else
   6858                              {
   6859                                bsi->RemoveFromStringList("GameList", "RecursivePaths", dir.c_str());
   6860                                bsi->AddToStringList("GameList", "Paths", dir.c_str());
   6861                              }
   6862 
   6863                              SetSettingsChanged(bsi);
   6864                              PopulateGameListDirectoryCache(bsi);
   6865                            }
   6866 
   6867                            Host::RefreshGameListAsync(false);
   6868                          }
   6869                          else if (index == 2)
   6870                          {
   6871                            // remove from list
   6872                            auto lock = Host::GetSettingsLock();
   6873                            SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer();
   6874                            bsi->RemoveFromStringList("GameList", "Paths", dir.c_str());
   6875                            bsi->RemoveFromStringList("GameList", "RecursivePaths", dir.c_str());
   6876                            SetSettingsChanged(bsi);
   6877                            PopulateGameListDirectoryCache(bsi);
   6878                            Host::RefreshGameListAsync(false);
   6879                          }
   6880 
   6881                          CloseChoiceDialog();
   6882                        });
   6883     }
   6884   }
   6885 
   6886   MenuHeading(FSUI_CSTR("List Settings"));
   6887   {
   6888     static constexpr const char* view_types[] = {FSUI_NSTR("Game Grid"), FSUI_NSTR("Game List")};
   6889     static constexpr const char* sort_types[] = {
   6890       FSUI_NSTR("Type"),        FSUI_NSTR("Serial"),      FSUI_NSTR("Title"),     FSUI_NSTR("File Title"),
   6891       FSUI_NSTR("Time Played"), FSUI_NSTR("Last Played"), FSUI_NSTR("File Size"), FSUI_NSTR("Uncompressed Size")};
   6892 
   6893     DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_BORDER_ALL, "Default View"),
   6894                        FSUI_CSTR("Selects the view that the game list will open to."), "Main",
   6895                        "DefaultFullscreenUIGameView", 0, view_types, std::size(view_types), true);
   6896     DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_SORT, "Sort By"),
   6897                        FSUI_CSTR("Determines that field that the game list will be sorted by."), "Main",
   6898                        "FullscreenUIGameSort", 0, sort_types, std::size(sort_types), true);
   6899     DrawToggleSetting(
   6900       bsi, FSUI_ICONSTR(ICON_FA_SORT_ALPHA_DOWN, "Sort Reversed"),
   6901       FSUI_CSTR("Reverses the game list sort order from the default (usually ascending to descending)."), "Main",
   6902       "FullscreenUIGameSortReverse", false);
   6903     DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST, "Merge Multi-Disc Games"),
   6904                       FSUI_CSTR("Merges multi-disc games into one item in the game list."), "Main",
   6905                       "FullscreenUIMergeDiscSets", true);
   6906   }
   6907 
   6908   MenuHeading(FSUI_CSTR("Cover Settings"));
   6909   {
   6910     DrawFolderSetting(bsi, FSUI_ICONSTR(ICON_FA_FOLDER, "Covers Directory"), "Folders", "Covers", EmuFolders::Covers);
   6911     if (MenuButton(FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Download Covers"),
   6912                    FSUI_CSTR("Downloads covers from a user-specified URL template.")))
   6913     {
   6914       Host::OnCoverDownloaderOpenRequested();
   6915     }
   6916   }
   6917 
   6918   MenuHeading(FSUI_CSTR("Operations"));
   6919   {
   6920     if (MenuButton(FSUI_ICONSTR(ICON_FA_SEARCH, "Scan For New Games"),
   6921                    FSUI_CSTR("Identifies any new files added to the game directories.")))
   6922     {
   6923       Host::RefreshGameListAsync(false);
   6924     }
   6925     if (MenuButton(FSUI_ICONSTR(ICON_FA_SEARCH_PLUS, "Rescan All Games"),
   6926                    FSUI_CSTR("Forces a full rescan of all games previously identified.")))
   6927     {
   6928       Host::RefreshGameListAsync(true);
   6929     }
   6930   }
   6931 
   6932   EndMenuButtons();
   6933 
   6934   EndFullscreenWindow();
   6935 
   6936   SetStandardSelectionFooterText(true);
   6937 }
   6938 
   6939 void FullscreenUI::SwitchToGameList()
   6940 {
   6941   s_current_main_window = MainWindowType::GameList;
   6942   s_game_list_view = static_cast<GameListView>(Host::GetBaseIntSettingValue("Main", "DefaultFullscreenUIGameView", 0));
   6943   {
   6944     auto lock = Host::GetSettingsLock();
   6945     PopulateGameListDirectoryCache(Host::Internal::GetBaseSettingsLayer());
   6946   }
   6947   QueueResetFocus(FocusResetType::ViewChanged);
   6948 }
   6949 
   6950 GPUTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry)
   6951 {
   6952   // lookup and grab cover image
   6953   auto cover_it = s_cover_image_map.find(entry->path);
   6954   if (cover_it == s_cover_image_map.end())
   6955   {
   6956     std::string cover_path(GameList::GetCoverImagePathForEntry(entry));
   6957     cover_it = s_cover_image_map.emplace(entry->path, std::move(cover_path)).first;
   6958   }
   6959 
   6960   GPUTexture* tex = (!cover_it->second.empty()) ? GetCachedTextureAsync(cover_it->second.c_str()) : nullptr;
   6961   return tex ? tex : GetTextureForGameListEntryType(entry->type);
   6962 }
   6963 
   6964 GPUTexture* FullscreenUI::GetTextureForGameListEntryType(GameList::EntryType type)
   6965 {
   6966   switch (type)
   6967   {
   6968     case GameList::EntryType::PSExe:
   6969       return s_fallback_exe_texture.get();
   6970 
   6971     case GameList::EntryType::Playlist:
   6972       return s_fallback_playlist_texture.get();
   6973 
   6974     case GameList::EntryType::PSF:
   6975       return s_fallback_psf_texture.get();
   6976 
   6977     case GameList::EntryType::Disc:
   6978     default:
   6979       return s_fallback_disc_texture.get();
   6980   }
   6981 }
   6982 
   6983 GPUTexture* FullscreenUI::GetCoverForCurrentGame()
   6984 {
   6985   auto lock = GameList::GetLock();
   6986 
   6987   const GameList::Entry* entry = GameList::GetEntryForPath(System::GetDiscPath());
   6988   if (!entry)
   6989     return s_fallback_disc_texture.get();
   6990 
   6991   return GetGameListCover(entry);
   6992 }
   6993 
   6994 //////////////////////////////////////////////////////////////////////////
   6995 // Overlays
   6996 //////////////////////////////////////////////////////////////////////////
   6997 
   6998 void FullscreenUI::OpenAboutWindow()
   6999 {
   7000   s_about_window_open = true;
   7001 }
   7002 
   7003 void FullscreenUI::ExitFullscreenAndOpenURL(std::string_view url)
   7004 {
   7005   Host::RunOnCPUThread([url = std::string(url)]() {
   7006     if (Host::IsFullscreen())
   7007       Host::SetFullscreen(false);
   7008 
   7009     Host::OpenURL(url);
   7010   });
   7011 }
   7012 
   7013 void FullscreenUI::CopyTextToClipboard(std::string title, std::string_view text)
   7014 {
   7015   if (Host::CopyTextToClipboard(text))
   7016     ShowToast(std::string(), std::move(title));
   7017   else
   7018     ShowToast(std::string(), FSUI_STR("Failed to copy text to clipboard."));
   7019 }
   7020 
   7021 void FullscreenUI::DrawAboutWindow()
   7022 {
   7023   ImGui::SetNextWindowSize(LayoutScale(1000.0f, 540.0f));
   7024   ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   7025   ImGui::OpenPopup(FSUI_CSTR("About DuckStation"));
   7026 
   7027   ImGui::PushFont(g_large_font);
   7028   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   7029   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(30.0f, 30.0f));
   7030 
   7031   if (ImGui::BeginPopupModal(FSUI_CSTR("About DuckStation"), &s_about_window_open,
   7032                              ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
   7033   {
   7034     ImGui::TextWrapped("%s",
   7035                        FSUI_CSTR("DuckStation is a free and open-source simulator/emulator of the Sony PlayStation(TM) "
   7036                                  "console, focusing on playability, speed, and long-term maintainability."));
   7037     ImGui::NewLine();
   7038     ImGui::TextWrapped(FSUI_CSTR("Version: %s"), g_scm_tag_str);
   7039     ImGui::NewLine();
   7040     ImGui::TextWrapped(
   7041       "%s", FSUI_CSTR("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)"));
   7042     ImGui::NewLine();
   7043     ImGui::TextWrapped(
   7044       "%s", FSUI_CSTR("\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe "
   7045                       "Limited. This software is not affiliated in any way with Sony Interactive Entertainment."));
   7046 
   7047     ImGui::NewLine();
   7048 
   7049     BeginMenuButtons();
   7050     if (ActiveButton(FSUI_ICONSTR(ICON_FA_GLOBE, "GitHub Repository"), false))
   7051       ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/");
   7052     if (ActiveButton(FSUI_ICONSTR(ICON_FA_COMMENT, "Discord Server"), false))
   7053       ExitFullscreenAndOpenURL("https://www.duckstation.org/discord.html");
   7054     if (ActiveButton(FSUI_ICONSTR(ICON_FA_PEOPLE_CARRY, "Contributor List"), false))
   7055       ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md");
   7056 
   7057     if (ActiveButton(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close"), false) || WantsToCloseMenu())
   7058     {
   7059       ImGui::CloseCurrentPopup();
   7060       s_about_window_open = false;
   7061     }
   7062     else
   7063     {
   7064       SetStandardSelectionFooterText(true);
   7065     }
   7066 
   7067     EndMenuButtons();
   7068 
   7069     ImGui::EndPopup();
   7070   }
   7071 
   7072   ImGui::PopStyleVar(2);
   7073   ImGui::PopFont();
   7074 }
   7075 
   7076 void FullscreenUI::OpenAchievementsWindow()
   7077 {
   7078   if (!Achievements::IsActive())
   7079   {
   7080     Host::AddKeyedOSDMessage("achievements_disabled", FSUI_STR("Achievements are not enabled."),
   7081                              Host::OSD_INFO_DURATION);
   7082     return;
   7083   }
   7084 
   7085   if (!System::IsValid() || !Initialize())
   7086     return;
   7087 
   7088   if (!Achievements::HasAchievements() || !Achievements::PrepareAchievementsWindow())
   7089   {
   7090     ShowToast(std::string(), FSUI_STR("This game has no achievements."));
   7091     return;
   7092   }
   7093 
   7094   if (s_current_main_window != MainWindowType::PauseMenu)
   7095   {
   7096     PauseForMenuOpen(false);
   7097     ForceKeyNavEnabled();
   7098   }
   7099 
   7100   s_current_main_window = MainWindowType::Achievements;
   7101   QueueResetFocus(FocusResetType::ViewChanged);
   7102   FixStateIfPaused();
   7103 }
   7104 
   7105 bool FullscreenUI::IsAchievementsWindowOpen()
   7106 {
   7107   return (s_current_main_window == MainWindowType::Achievements);
   7108 }
   7109 
   7110 void FullscreenUI::OpenLeaderboardsWindow()
   7111 {
   7112   if (!Achievements::IsActive())
   7113   {
   7114     Host::AddKeyedOSDMessage("achievements_disabled", FSUI_STR("Leaderboards are not enabled."),
   7115                              Host::OSD_INFO_DURATION);
   7116     return;
   7117   }
   7118 
   7119   if (!System::IsValid() || !Initialize())
   7120     return;
   7121 
   7122   if (!Achievements::HasLeaderboards() || !Achievements::PrepareLeaderboardsWindow())
   7123   {
   7124     ShowToast(std::string(), FSUI_STR("This game has no leaderboards."));
   7125     return;
   7126   }
   7127 
   7128   if (s_current_main_window != MainWindowType::PauseMenu)
   7129   {
   7130     PauseForMenuOpen(false);
   7131     ForceKeyNavEnabled();
   7132   }
   7133 
   7134   s_current_main_window = MainWindowType::Leaderboards;
   7135   QueueResetFocus(FocusResetType::ViewChanged);
   7136   FixStateIfPaused();
   7137 }
   7138 
   7139 bool FullscreenUI::IsLeaderboardsWindowOpen()
   7140 {
   7141   return (s_current_main_window == MainWindowType::Leaderboards);
   7142 }
   7143 
   7144 #endif // __ANDROID__
   7145 
   7146 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
   7147 // Translation String Area
   7148 // To avoid having to type T_RANSLATE("FullscreenUI", ...) everywhere, we use the shorter macros at the top
   7149 // of the file, then preprocess and generate a bunch of noops here to define the strings. Sadly that means
   7150 // the view in Linguist is gonna suck, but you can search the file for the string for more context.
   7151 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
   7152 
   7153 #if 0
   7154 // TRANSLATION-STRING-AREA-BEGIN
   7155 TRANSLATE_NOOP("FullscreenUI", "%.2f Seconds");
   7156 TRANSLATE_NOOP("FullscreenUI", "%d Frames");
   7157 TRANSLATE_NOOP("FullscreenUI", "%d ms");
   7158 TRANSLATE_NOOP("FullscreenUI", "%d sectors");
   7159 TRANSLATE_NOOP("FullscreenUI", "-");
   7160 TRANSLATE_NOOP("FullscreenUI", "1 Frame");
   7161 TRANSLATE_NOOP("FullscreenUI", "10 Frames");
   7162 TRANSLATE_NOOP("FullscreenUI", "100% [60 FPS (NTSC) / 50 FPS (PAL)]");
   7163 TRANSLATE_NOOP("FullscreenUI", "1000% [600 FPS (NTSC) / 500 FPS (PAL)]");
   7164 TRANSLATE_NOOP("FullscreenUI", "10x");
   7165 TRANSLATE_NOOP("FullscreenUI", "10x (20x Speed)");
   7166 TRANSLATE_NOOP("FullscreenUI", "11x");
   7167 TRANSLATE_NOOP("FullscreenUI", "125% [75 FPS (NTSC) / 62 FPS (PAL)]");
   7168 TRANSLATE_NOOP("FullscreenUI", "12x");
   7169 TRANSLATE_NOOP("FullscreenUI", "13x");
   7170 TRANSLATE_NOOP("FullscreenUI", "14x");
   7171 TRANSLATE_NOOP("FullscreenUI", "150% [90 FPS (NTSC) / 75 FPS (PAL)]");
   7172 TRANSLATE_NOOP("FullscreenUI", "15x");
   7173 TRANSLATE_NOOP("FullscreenUI", "16x");
   7174 TRANSLATE_NOOP("FullscreenUI", "175% [105 FPS (NTSC) / 87 FPS (PAL)]");
   7175 TRANSLATE_NOOP("FullscreenUI", "1x");
   7176 TRANSLATE_NOOP("FullscreenUI", "2 Frames");
   7177 TRANSLATE_NOOP("FullscreenUI", "20% [12 FPS (NTSC) / 10 FPS (PAL)]");
   7178 TRANSLATE_NOOP("FullscreenUI", "200% [120 FPS (NTSC) / 100 FPS (PAL)]");
   7179 TRANSLATE_NOOP("FullscreenUI", "250% [150 FPS (NTSC) / 125 FPS (PAL)]");
   7180 TRANSLATE_NOOP("FullscreenUI", "2x");
   7181 TRANSLATE_NOOP("FullscreenUI", "2x (Quad Speed)");
   7182 TRANSLATE_NOOP("FullscreenUI", "3 Frames");
   7183 TRANSLATE_NOOP("FullscreenUI", "30% [18 FPS (NTSC) / 15 FPS (PAL)]");
   7184 TRANSLATE_NOOP("FullscreenUI", "300% [180 FPS (NTSC) / 150 FPS (PAL)]");
   7185 TRANSLATE_NOOP("FullscreenUI", "350% [210 FPS (NTSC) / 175 FPS (PAL)]");
   7186 TRANSLATE_NOOP("FullscreenUI", "3x");
   7187 TRANSLATE_NOOP("FullscreenUI", "3x (6x Speed)");
   7188 TRANSLATE_NOOP("FullscreenUI", "3x (for 720p)");
   7189 TRANSLATE_NOOP("FullscreenUI", "4 Frames");
   7190 TRANSLATE_NOOP("FullscreenUI", "40% [24 FPS (NTSC) / 20 FPS (PAL)]");
   7191 TRANSLATE_NOOP("FullscreenUI", "400% [240 FPS (NTSC) / 200 FPS (PAL)]");
   7192 TRANSLATE_NOOP("FullscreenUI", "450% [270 FPS (NTSC) / 225 FPS (PAL)]");
   7193 TRANSLATE_NOOP("FullscreenUI", "4x");
   7194 TRANSLATE_NOOP("FullscreenUI", "4x (8x Speed)");
   7195 TRANSLATE_NOOP("FullscreenUI", "5 Frames");
   7196 TRANSLATE_NOOP("FullscreenUI", "50% [30 FPS (NTSC) / 25 FPS (PAL)]");
   7197 TRANSLATE_NOOP("FullscreenUI", "500% [300 FPS (NTSC) / 250 FPS (PAL)]");
   7198 TRANSLATE_NOOP("FullscreenUI", "5x");
   7199 TRANSLATE_NOOP("FullscreenUI", "5x (10x Speed)");
   7200 TRANSLATE_NOOP("FullscreenUI", "5x (for 1080p)");
   7201 TRANSLATE_NOOP("FullscreenUI", "6 Frames");
   7202 TRANSLATE_NOOP("FullscreenUI", "60% [36 FPS (NTSC) / 30 FPS (PAL)]");
   7203 TRANSLATE_NOOP("FullscreenUI", "600% [360 FPS (NTSC) / 300 FPS (PAL)]");
   7204 TRANSLATE_NOOP("FullscreenUI", "6x");
   7205 TRANSLATE_NOOP("FullscreenUI", "6x (12x Speed)");
   7206 TRANSLATE_NOOP("FullscreenUI", "6x (for 1440p)");
   7207 TRANSLATE_NOOP("FullscreenUI", "7 Frames");
   7208 TRANSLATE_NOOP("FullscreenUI", "70% [42 FPS (NTSC) / 35 FPS (PAL)]");
   7209 TRANSLATE_NOOP("FullscreenUI", "700% [420 FPS (NTSC) / 350 FPS (PAL)]");
   7210 TRANSLATE_NOOP("FullscreenUI", "7x");
   7211 TRANSLATE_NOOP("FullscreenUI", "7x (14x Speed)");
   7212 TRANSLATE_NOOP("FullscreenUI", "8 Frames");
   7213 TRANSLATE_NOOP("FullscreenUI", "80% [48 FPS (NTSC) / 40 FPS (PAL)]");
   7214 TRANSLATE_NOOP("FullscreenUI", "800% [480 FPS (NTSC) / 400 FPS (PAL)]");
   7215 TRANSLATE_NOOP("FullscreenUI", "8x");
   7216 TRANSLATE_NOOP("FullscreenUI", "8x (16x Speed)");
   7217 TRANSLATE_NOOP("FullscreenUI", "9 Frames");
   7218 TRANSLATE_NOOP("FullscreenUI", "90% [54 FPS (NTSC) / 45 FPS (PAL)]");
   7219 TRANSLATE_NOOP("FullscreenUI", "900% [540 FPS (NTSC) / 450 FPS (PAL)]");
   7220 TRANSLATE_NOOP("FullscreenUI", "9x");
   7221 TRANSLATE_NOOP("FullscreenUI", "9x (18x Speed)");
   7222 TRANSLATE_NOOP("FullscreenUI", "9x (for 4K)");
   7223 TRANSLATE_NOOP("FullscreenUI", "A resume save state created at %s was found.\n\nDo you want to load this save and continue?");
   7224 TRANSLATE_NOOP("FullscreenUI", "About");
   7225 TRANSLATE_NOOP("FullscreenUI", "About DuckStation");
   7226 TRANSLATE_NOOP("FullscreenUI", "Account");
   7227 TRANSLATE_NOOP("FullscreenUI", "Accurate Blending");
   7228 TRANSLATE_NOOP("FullscreenUI", "Achievement Notifications");
   7229 TRANSLATE_NOOP("FullscreenUI", "Achievements");
   7230 TRANSLATE_NOOP("FullscreenUI", "Achievements Settings");
   7231 TRANSLATE_NOOP("FullscreenUI", "Achievements are not enabled.");
   7232 TRANSLATE_NOOP("FullscreenUI", "Add Search Directory");
   7233 TRANSLATE_NOOP("FullscreenUI", "Add Shader");
   7234 TRANSLATE_NOOP("FullscreenUI", "Adds a new directory to the game search list.");
   7235 TRANSLATE_NOOP("FullscreenUI", "Adds a new shader to the chain.");
   7236 TRANSLATE_NOOP("FullscreenUI", "Adds additional precision to PGXP data post-projection. May improve visuals in some games.");
   7237 TRANSLATE_NOOP("FullscreenUI", "Adjusts the emulation speed so the console's refresh rate matches the host when VSync is enabled.");
   7238 TRANSLATE_NOOP("FullscreenUI", "Advanced");
   7239 TRANSLATE_NOOP("FullscreenUI", "Advanced Settings");
   7240 TRANSLATE_NOOP("FullscreenUI", "All Time: {}");
   7241 TRANSLATE_NOOP("FullscreenUI", "Allow Booting Without SBI File");
   7242 TRANSLATE_NOOP("FullscreenUI", "Allows loading protected games without subchannel information.");
   7243 TRANSLATE_NOOP("FullscreenUI", "An error occurred while deleting empty game settings:\n{}");
   7244 TRANSLATE_NOOP("FullscreenUI", "An error occurred while saving game settings:\n{}");
   7245 TRANSLATE_NOOP("FullscreenUI", "Applies modern dithering techniques to further smooth out gradients when true color is enabled.");
   7246 TRANSLATE_NOOP("FullscreenUI", "Apply Image Patches");
   7247 TRANSLATE_NOOP("FullscreenUI", "Are you sure you want to clear the current post-processing chain? All configuration will be lost.");
   7248 TRANSLATE_NOOP("FullscreenUI", "Aspect Ratio");
   7249 TRANSLATE_NOOP("FullscreenUI", "Attempts to detect one pixel high/wide lines that rely on non-upscaled rasterization behavior, filling in gaps introduced by upscaling.");
   7250 TRANSLATE_NOOP("FullscreenUI", "Attempts to map the selected port to a chosen controller.");
   7251 TRANSLATE_NOOP("FullscreenUI", "Audio Backend");
   7252 TRANSLATE_NOOP("FullscreenUI", "Audio Control");
   7253 TRANSLATE_NOOP("FullscreenUI", "Audio Settings");
   7254 TRANSLATE_NOOP("FullscreenUI", "Auto-Detect");
   7255 TRANSLATE_NOOP("FullscreenUI", "Automatic Mapping");
   7256 TRANSLATE_NOOP("FullscreenUI", "Automatic based on window size");
   7257 TRANSLATE_NOOP("FullscreenUI", "Automatic mapping completed for {}.");
   7258 TRANSLATE_NOOP("FullscreenUI", "Automatic mapping failed for {}.");
   7259 TRANSLATE_NOOP("FullscreenUI", "Automatic mapping failed, no devices are available.");
   7260 TRANSLATE_NOOP("FullscreenUI", "Automatically applies patches to disc images when they are present, currently only PPF is supported.");
   7261 TRANSLATE_NOOP("FullscreenUI", "Automatically loads and applies cheats on game start. Cheats can break games and saves.");
   7262 TRANSLATE_NOOP("FullscreenUI", "Automatically saves the emulator state when powering down or exiting. You can then resume directly from where you left off next time.");
   7263 TRANSLATE_NOOP("FullscreenUI", "Automatically switches to fullscreen mode when the program is started.");
   7264 TRANSLATE_NOOP("FullscreenUI", "Avoids calls to C++ code, significantly speeding up the recompiler.");
   7265 TRANSLATE_NOOP("FullscreenUI", "BIOS Directory");
   7266 TRANSLATE_NOOP("FullscreenUI", "BIOS Selection");
   7267 TRANSLATE_NOOP("FullscreenUI", "BIOS Settings");
   7268 TRANSLATE_NOOP("FullscreenUI", "BIOS for {}");
   7269 TRANSLATE_NOOP("FullscreenUI", "BIOS to use when emulating {} consoles.");
   7270 TRANSLATE_NOOP("FullscreenUI", "Back");
   7271 TRANSLATE_NOOP("FullscreenUI", "Back To Pause Menu");
   7272 TRANSLATE_NOOP("FullscreenUI", "Backend Settings");
   7273 TRANSLATE_NOOP("FullscreenUI", "Behavior");
   7274 TRANSLATE_NOOP("FullscreenUI", "Borderless Fullscreen");
   7275 TRANSLATE_NOOP("FullscreenUI", "Buffer Size");
   7276 TRANSLATE_NOOP("FullscreenUI", "CD-ROM Emulation");
   7277 TRANSLATE_NOOP("FullscreenUI", "CPU Emulation");
   7278 TRANSLATE_NOOP("FullscreenUI", "CPU Mode");
   7279 TRANSLATE_NOOP("FullscreenUI", "Cancel");
   7280 TRANSLATE_NOOP("FullscreenUI", "Capture");
   7281 TRANSLATE_NOOP("FullscreenUI", "Change Disc");
   7282 TRANSLATE_NOOP("FullscreenUI", "Change Page");
   7283 TRANSLATE_NOOP("FullscreenUI", "Change Selection");
   7284 TRANSLATE_NOOP("FullscreenUI", "Change View");
   7285 TRANSLATE_NOOP("FullscreenUI", "Changes settings for the application.");
   7286 TRANSLATE_NOOP("FullscreenUI", "Changes the aspect ratio used to display the console's output to the screen.");
   7287 TRANSLATE_NOOP("FullscreenUI", "Cheat List");
   7288 TRANSLATE_NOOP("FullscreenUI", "Chooses the backend to use for rendering the console/game visuals.");
   7289 TRANSLATE_NOOP("FullscreenUI", "Chooses the language used for UI elements.");
   7290 TRANSLATE_NOOP("FullscreenUI", "Clean Boot");
   7291 TRANSLATE_NOOP("FullscreenUI", "Clear Settings");
   7292 TRANSLATE_NOOP("FullscreenUI", "Clear Shaders");
   7293 TRANSLATE_NOOP("FullscreenUI", "Clears a shader from the chain.");
   7294 TRANSLATE_NOOP("FullscreenUI", "Clears all settings set for this game.");
   7295 TRANSLATE_NOOP("FullscreenUI", "Clears the mask/transparency bit in VRAM write dumps.");
   7296 TRANSLATE_NOOP("FullscreenUI", "Close");
   7297 TRANSLATE_NOOP("FullscreenUI", "Close Game");
   7298 TRANSLATE_NOOP("FullscreenUI", "Close Menu");
   7299 TRANSLATE_NOOP("FullscreenUI", "Compatibility Rating");
   7300 TRANSLATE_NOOP("FullscreenUI", "Compatibility: ");
   7301 TRANSLATE_NOOP("FullscreenUI", "Completely exits the application, returning you to your desktop.");
   7302 TRANSLATE_NOOP("FullscreenUI", "Configuration");
   7303 TRANSLATE_NOOP("FullscreenUI", "Confirm Power Off");
   7304 TRANSLATE_NOOP("FullscreenUI", "Console Settings");
   7305 TRANSLATE_NOOP("FullscreenUI", "Contributor List");
   7306 TRANSLATE_NOOP("FullscreenUI", "Controller Port {}");
   7307 TRANSLATE_NOOP("FullscreenUI", "Controller Port {} Macros");
   7308 TRANSLATE_NOOP("FullscreenUI", "Controller Port {} Settings");
   7309 TRANSLATE_NOOP("FullscreenUI", "Controller Port {}{}");
   7310 TRANSLATE_NOOP("FullscreenUI", "Controller Port {}{} Macros");
   7311 TRANSLATE_NOOP("FullscreenUI", "Controller Port {}{} Settings");
   7312 TRANSLATE_NOOP("FullscreenUI", "Controller Settings");
   7313 TRANSLATE_NOOP("FullscreenUI", "Controller Type");
   7314 TRANSLATE_NOOP("FullscreenUI", "Controller settings reset to default.");
   7315 TRANSLATE_NOOP("FullscreenUI", "Controls");
   7316 TRANSLATE_NOOP("FullscreenUI", "Controls the volume of the audio played on the host when fast forwarding.");
   7317 TRANSLATE_NOOP("FullscreenUI", "Controls the volume of the audio played on the host.");
   7318 TRANSLATE_NOOP("FullscreenUI", "Copies the current global settings to this game.");
   7319 TRANSLATE_NOOP("FullscreenUI", "Copies the global controller configuration to this game.");
   7320 TRANSLATE_NOOP("FullscreenUI", "Copy Global Settings");
   7321 TRANSLATE_NOOP("FullscreenUI", "Copy Settings");
   7322 TRANSLATE_NOOP("FullscreenUI", "Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient permissions to access it.");
   7323 TRANSLATE_NOOP("FullscreenUI", "Cover Settings");
   7324 TRANSLATE_NOOP("FullscreenUI", "Covers Directory");
   7325 TRANSLATE_NOOP("FullscreenUI", "Create");
   7326 TRANSLATE_NOOP("FullscreenUI", "Create New...");
   7327 TRANSLATE_NOOP("FullscreenUI", "Create Save State Backups");
   7328 TRANSLATE_NOOP("FullscreenUI", "Crop Mode");
   7329 TRANSLATE_NOOP("FullscreenUI", "Culling Correction");
   7330 TRANSLATE_NOOP("FullscreenUI", "Current Game");
   7331 TRANSLATE_NOOP("FullscreenUI", "Debugging Settings");
   7332 TRANSLATE_NOOP("FullscreenUI", "Default");
   7333 TRANSLATE_NOOP("FullscreenUI", "Default Boot");
   7334 TRANSLATE_NOOP("FullscreenUI", "Default View");
   7335 TRANSLATE_NOOP("FullscreenUI", "Default: Disabled");
   7336 TRANSLATE_NOOP("FullscreenUI", "Default: Enabled");
   7337 TRANSLATE_NOOP("FullscreenUI", "Deinterlacing Mode");
   7338 TRANSLATE_NOOP("FullscreenUI", "Delete Save");
   7339 TRANSLATE_NOOP("FullscreenUI", "Delete State");
   7340 TRANSLATE_NOOP("FullscreenUI", "Depth Clear Threshold");
   7341 TRANSLATE_NOOP("FullscreenUI", "Desktop Mode");
   7342 TRANSLATE_NOOP("FullscreenUI", "Details");
   7343 TRANSLATE_NOOP("FullscreenUI", "Details unavailable for game not scanned in game list.");
   7344 TRANSLATE_NOOP("FullscreenUI", "Determines how audio is expanded from stereo to surround for supported games.");
   7345 TRANSLATE_NOOP("FullscreenUI", "Determines how large the on-screen messages and monitor are.");
   7346 TRANSLATE_NOOP("FullscreenUI", "Determines how much latency there is between the audio being picked up by the host API, and played through speakers.");
   7347 TRANSLATE_NOOP("FullscreenUI", "Determines how much of the area typically not visible on a consumer TV set to crop/hide.");
   7348 TRANSLATE_NOOP("FullscreenUI", "Determines how the emulated CPU executes instructions.");
   7349 TRANSLATE_NOOP("FullscreenUI", "Determines how the emulated console's output is upscaled or downscaled to your monitor's resolution.");
   7350 TRANSLATE_NOOP("FullscreenUI", "Determines quality of audio when not running at 100% speed.");
   7351 TRANSLATE_NOOP("FullscreenUI", "Determines that field that the game list will be sorted by.");
   7352 TRANSLATE_NOOP("FullscreenUI", "Determines the amount of audio buffered before being pulled by the host API.");
   7353 TRANSLATE_NOOP("FullscreenUI", "Determines the emulated hardware type.");
   7354 TRANSLATE_NOOP("FullscreenUI", "Determines the format that screenshots will be saved/compressed with.");
   7355 TRANSLATE_NOOP("FullscreenUI", "Determines the position on the screen when black borders must be added.");
   7356 TRANSLATE_NOOP("FullscreenUI", "Determines the rotation of the simulated TV screen.");
   7357 TRANSLATE_NOOP("FullscreenUI", "Determines the size of screenshots created by DuckStation.");
   7358 TRANSLATE_NOOP("FullscreenUI", "Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed.");
   7359 TRANSLATE_NOOP("FullscreenUI", "Determines which algorithm is used to convert interlaced frames to progressive for display on your system.");
   7360 TRANSLATE_NOOP("FullscreenUI", "Device Settings");
   7361 TRANSLATE_NOOP("FullscreenUI", "Disable All Enhancements");
   7362 TRANSLATE_NOOP("FullscreenUI", "Disable Interlacing");
   7363 TRANSLATE_NOOP("FullscreenUI", "Disable Mailbox Presentation");
   7364 TRANSLATE_NOOP("FullscreenUI", "Disable Subdirectory Scanning");
   7365 TRANSLATE_NOOP("FullscreenUI", "Disable on 2D Polygons");
   7366 TRANSLATE_NOOP("FullscreenUI", "Disabled");
   7367 TRANSLATE_NOOP("FullscreenUI", "Disables dithering and uses the full 8 bits per channel of color information.");
   7368 TRANSLATE_NOOP("FullscreenUI", "Disables interlaced rendering and display in the GPU. Some games can render in 480p this way, but others will break.");
   7369 TRANSLATE_NOOP("FullscreenUI", "Disc {} | {}");
   7370 TRANSLATE_NOOP("FullscreenUI", "Discord Server");
   7371 TRANSLATE_NOOP("FullscreenUI", "Display Settings");
   7372 TRANSLATE_NOOP("FullscreenUI", "Displays popup messages on events such as achievement unlocks and leaderboard submissions.");
   7373 TRANSLATE_NOOP("FullscreenUI", "Displays popup messages when starting, submitting, or failing a leaderboard challenge.");
   7374 TRANSLATE_NOOP("FullscreenUI", "Double-Click Toggles Fullscreen");
   7375 TRANSLATE_NOOP("FullscreenUI", "Download Covers");
   7376 TRANSLATE_NOOP("FullscreenUI", "Downloads covers from a user-specified URL template.");
   7377 TRANSLATE_NOOP("FullscreenUI", "Downsamples the rendered image prior to displaying it. Can improve overall image quality in mixed 2D/3D games.");
   7378 TRANSLATE_NOOP("FullscreenUI", "Downsampling");
   7379 TRANSLATE_NOOP("FullscreenUI", "Downsampling Display Scale");
   7380 TRANSLATE_NOOP("FullscreenUI", "Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)");
   7381 TRANSLATE_NOOP("FullscreenUI", "DuckStation is a free and open-source simulator/emulator of the Sony PlayStation(TM) console, focusing on playability, speed, and long-term maintainability.");
   7382 TRANSLATE_NOOP("FullscreenUI", "Dump Replaceable VRAM Writes");
   7383 TRANSLATE_NOOP("FullscreenUI", "Emulation Settings");
   7384 TRANSLATE_NOOP("FullscreenUI", "Emulation Speed");
   7385 TRANSLATE_NOOP("FullscreenUI", "Enable 8MB RAM");
   7386 TRANSLATE_NOOP("FullscreenUI", "Enable Achievements");
   7387 TRANSLATE_NOOP("FullscreenUI", "Enable Cheats");
   7388 TRANSLATE_NOOP("FullscreenUI", "Enable Discord Presence");
   7389 TRANSLATE_NOOP("FullscreenUI", "Enable Fast Boot");
   7390 TRANSLATE_NOOP("FullscreenUI", "Enable In-Game Overlays");
   7391 TRANSLATE_NOOP("FullscreenUI", "Enable Overclocking");
   7392 TRANSLATE_NOOP("FullscreenUI", "Enable Post Processing");
   7393 TRANSLATE_NOOP("FullscreenUI", "Enable Recompiler Block Linking");
   7394 TRANSLATE_NOOP("FullscreenUI", "Enable Recompiler ICache");
   7395 TRANSLATE_NOOP("FullscreenUI", "Enable Recompiler Memory Exceptions");
   7396 TRANSLATE_NOOP("FullscreenUI", "Enable Region Check");
   7397 TRANSLATE_NOOP("FullscreenUI", "Enable Rewinding");
   7398 TRANSLATE_NOOP("FullscreenUI", "Enable SDL Input Source");
   7399 TRANSLATE_NOOP("FullscreenUI", "Enable Subdirectory Scanning");
   7400 TRANSLATE_NOOP("FullscreenUI", "Enable TTY Logging");
   7401 TRANSLATE_NOOP("FullscreenUI", "Enable VRAM Write Texture Replacement");
   7402 TRANSLATE_NOOP("FullscreenUI", "Enable XInput Input Source");
   7403 TRANSLATE_NOOP("FullscreenUI", "Enable debugging when supported by the host's renderer API. Only for developer use.");
   7404 TRANSLATE_NOOP("FullscreenUI", "Enable/Disable the Player LED on DualSense controllers.");
   7405 TRANSLATE_NOOP("FullscreenUI", "Enables alignment and bus exceptions. Not needed for any known games.");
   7406 TRANSLATE_NOOP("FullscreenUI", "Enables an additional 6MB of RAM to obtain a total of 2+6 = 8MB, usually present on dev consoles.");
   7407 TRANSLATE_NOOP("FullscreenUI", "Enables an additional three controller slots on each port. Not supported in all games.");
   7408 TRANSLATE_NOOP("FullscreenUI", "Enables more precise frame pacing at the cost of battery life.");
   7409 TRANSLATE_NOOP("FullscreenUI", "Enables the older, less accurate MDEC decoding routines. May be required for old replacement backgrounds to match/load.");
   7410 TRANSLATE_NOOP("FullscreenUI", "Enables the replacement of background textures in supported games.");
   7411 TRANSLATE_NOOP("FullscreenUI", "Encore Mode");
   7412 TRANSLATE_NOOP("FullscreenUI", "Ensures every frame generated is displayed for optimal pacing. Enable for variable refresh displays, such as GSync/FreeSync. Disable if you are having speed or sound issues.");
   7413 TRANSLATE_NOOP("FullscreenUI", "Enter Value");
   7414 TRANSLATE_NOOP("FullscreenUI", "Enter the name of the input profile you wish to create.");
   7415 TRANSLATE_NOOP("FullscreenUI", "Error");
   7416 TRANSLATE_NOOP("FullscreenUI", "Execution Mode");
   7417 TRANSLATE_NOOP("FullscreenUI", "Exit");
   7418 TRANSLATE_NOOP("FullscreenUI", "Exit And Save State");
   7419 TRANSLATE_NOOP("FullscreenUI", "Exit DuckStation");
   7420 TRANSLATE_NOOP("FullscreenUI", "Exit Without Saving");
   7421 TRANSLATE_NOOP("FullscreenUI", "Exits Big Picture mode, returning to the desktop interface.");
   7422 TRANSLATE_NOOP("FullscreenUI", "Expansion Mode");
   7423 TRANSLATE_NOOP("FullscreenUI", "FMV Chroma Smoothing");
   7424 TRANSLATE_NOOP("FullscreenUI", "Failed to copy text to clipboard.");
   7425 TRANSLATE_NOOP("FullscreenUI", "Failed to delete save state.");
   7426 TRANSLATE_NOOP("FullscreenUI", "Failed to delete {}.");
   7427 TRANSLATE_NOOP("FullscreenUI", "Failed to load '{}'.");
   7428 TRANSLATE_NOOP("FullscreenUI", "Failed to load shader {}. It may be invalid.\nError was:");
   7429 TRANSLATE_NOOP("FullscreenUI", "Failed to save input profile '{}'.");
   7430 TRANSLATE_NOOP("FullscreenUI", "Fast Boot");
   7431 TRANSLATE_NOOP("FullscreenUI", "Fast Forward Speed");
   7432 TRANSLATE_NOOP("FullscreenUI", "Fast Forward Volume");
   7433 TRANSLATE_NOOP("FullscreenUI", "File Size");
   7434 TRANSLATE_NOOP("FullscreenUI", "File Size: %.2f MB");
   7435 TRANSLATE_NOOP("FullscreenUI", "File Title");
   7436 TRANSLATE_NOOP("FullscreenUI", "Force 4:3 For FMVs");
   7437 TRANSLATE_NOOP("FullscreenUI", "Force NTSC Timings");
   7438 TRANSLATE_NOOP("FullscreenUI", "Forces PAL games to run at NTSC timings, i.e. 60hz. Some PAL games will run at their \"normal\" speeds, while others will break.");
   7439 TRANSLATE_NOOP("FullscreenUI", "Forces a full rescan of all games previously identified.");
   7440 TRANSLATE_NOOP("FullscreenUI", "Forces blending to be done in the shader at 16-bit precision, when not using true color. Non-trivial performance impact, and unnecessary for most games.");
   7441 TRANSLATE_NOOP("FullscreenUI", "Forces the use of FIFO over Mailbox presentation, i.e. double buffering instead of triple buffering. Usually results in worse frame pacing.");
   7442 TRANSLATE_NOOP("FullscreenUI", "Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable background music in some games.");
   7443 TRANSLATE_NOOP("FullscreenUI", "Frame Time Buffer");
   7444 TRANSLATE_NOOP("FullscreenUI", "From File...");
   7445 TRANSLATE_NOOP("FullscreenUI", "Fullscreen Resolution");
   7446 TRANSLATE_NOOP("FullscreenUI", "GPU Adapter");
   7447 TRANSLATE_NOOP("FullscreenUI", "GPU Renderer");
   7448 TRANSLATE_NOOP("FullscreenUI", "GPU adapter will be applied after restarting.");
   7449 TRANSLATE_NOOP("FullscreenUI", "Game Grid");
   7450 TRANSLATE_NOOP("FullscreenUI", "Game List");
   7451 TRANSLATE_NOOP("FullscreenUI", "Game List Settings");
   7452 TRANSLATE_NOOP("FullscreenUI", "Game Properties");
   7453 TRANSLATE_NOOP("FullscreenUI", "Game Quick Save");
   7454 TRANSLATE_NOOP("FullscreenUI", "Game Slot {0}##game_slot_{0}");
   7455 TRANSLATE_NOOP("FullscreenUI", "Game compatibility rating copied to clipboard.");
   7456 TRANSLATE_NOOP("FullscreenUI", "Game not loaded or no RetroAchievements available.");
   7457 TRANSLATE_NOOP("FullscreenUI", "Game path copied to clipboard.");
   7458 TRANSLATE_NOOP("FullscreenUI", "Game region copied to clipboard.");
   7459 TRANSLATE_NOOP("FullscreenUI", "Game serial copied to clipboard.");
   7460 TRANSLATE_NOOP("FullscreenUI", "Game settings have been cleared for '{}'.");
   7461 TRANSLATE_NOOP("FullscreenUI", "Game settings initialized with global settings for '{}'.");
   7462 TRANSLATE_NOOP("FullscreenUI", "Game title copied to clipboard.");
   7463 TRANSLATE_NOOP("FullscreenUI", "Game type copied to clipboard.");
   7464 TRANSLATE_NOOP("FullscreenUI", "Game: {} ({})");
   7465 TRANSLATE_NOOP("FullscreenUI", "Genre: %s");
   7466 TRANSLATE_NOOP("FullscreenUI", "Geometry Tolerance");
   7467 TRANSLATE_NOOP("FullscreenUI", "GitHub Repository");
   7468 TRANSLATE_NOOP("FullscreenUI", "Global Slot {0} - {1}##global_slot_{0}");
   7469 TRANSLATE_NOOP("FullscreenUI", "Global Slot {0}##global_slot_{0}");
   7470 TRANSLATE_NOOP("FullscreenUI", "Graphics Settings");
   7471 TRANSLATE_NOOP("FullscreenUI", "Hardcore Mode");
   7472 TRANSLATE_NOOP("FullscreenUI", "Hardcore mode will be enabled on next game restart.");
   7473 TRANSLATE_NOOP("FullscreenUI", "Hide Cursor In Fullscreen");
   7474 TRANSLATE_NOOP("FullscreenUI", "Hides the mouse pointer/cursor when the emulator is in fullscreen mode.");
   7475 TRANSLATE_NOOP("FullscreenUI", "Hotkey Settings");
   7476 TRANSLATE_NOOP("FullscreenUI", "How many saves will be kept for rewinding. Higher values have greater memory requirements.");
   7477 TRANSLATE_NOOP("FullscreenUI", "How often a rewind state will be created. Higher frequencies have greater system requirements.");
   7478 TRANSLATE_NOOP("FullscreenUI", "Identifies any new files added to the game directories.");
   7479 TRANSLATE_NOOP("FullscreenUI", "If not enabled, the current post processing chain will be ignored.");
   7480 TRANSLATE_NOOP("FullscreenUI", "Increase Timer Resolution");
   7481 TRANSLATE_NOOP("FullscreenUI", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games.");
   7482 TRANSLATE_NOOP("FullscreenUI", "Increases the precision of polygon culling, reducing the number of holes in geometry.");
   7483 TRANSLATE_NOOP("FullscreenUI", "Infinite/Instantaneous");
   7484 TRANSLATE_NOOP("FullscreenUI", "Inhibit Screensaver");
   7485 TRANSLATE_NOOP("FullscreenUI", "Input Sources");
   7486 TRANSLATE_NOOP("FullscreenUI", "Input profile '{}' loaded.");
   7487 TRANSLATE_NOOP("FullscreenUI", "Input profile '{}' saved.");
   7488 TRANSLATE_NOOP("FullscreenUI", "Integration");
   7489 TRANSLATE_NOOP("FullscreenUI", "Interface Settings");
   7490 TRANSLATE_NOOP("FullscreenUI", "Internal Resolution");
   7491 TRANSLATE_NOOP("FullscreenUI", "Last Played");
   7492 TRANSLATE_NOOP("FullscreenUI", "Last Played: %s");
   7493 TRANSLATE_NOOP("FullscreenUI", "Latency Control");
   7494 TRANSLATE_NOOP("FullscreenUI", "Launch Options");
   7495 TRANSLATE_NOOP("FullscreenUI", "Launch a game by selecting a file/disc image.");
   7496 TRANSLATE_NOOP("FullscreenUI", "Launch a game from a file, disc, or starts the console without any disc inserted.");
   7497 TRANSLATE_NOOP("FullscreenUI", "Launch a game from images scanned from your game directories.");
   7498 TRANSLATE_NOOP("FullscreenUI", "Leaderboard Notifications");
   7499 TRANSLATE_NOOP("FullscreenUI", "Leaderboards");
   7500 TRANSLATE_NOOP("FullscreenUI", "Leaderboards are not enabled.");
   7501 TRANSLATE_NOOP("FullscreenUI", "Line Detection");
   7502 TRANSLATE_NOOP("FullscreenUI", "List Settings");
   7503 TRANSLATE_NOOP("FullscreenUI", "Load Devices From Save States");
   7504 TRANSLATE_NOOP("FullscreenUI", "Load Global State");
   7505 TRANSLATE_NOOP("FullscreenUI", "Load Profile");
   7506 TRANSLATE_NOOP("FullscreenUI", "Load Resume State");
   7507 TRANSLATE_NOOP("FullscreenUI", "Load State");
   7508 TRANSLATE_NOOP("FullscreenUI", "Loads all replacement texture to RAM, reducing stuttering at runtime.");
   7509 TRANSLATE_NOOP("FullscreenUI", "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay.");
   7510 TRANSLATE_NOOP("FullscreenUI", "Log Level");
   7511 TRANSLATE_NOOP("FullscreenUI", "Log To Debug Console");
   7512 TRANSLATE_NOOP("FullscreenUI", "Log To File");
   7513 TRANSLATE_NOOP("FullscreenUI", "Log To System Console");
   7514 TRANSLATE_NOOP("FullscreenUI", "Logging");
   7515 TRANSLATE_NOOP("FullscreenUI", "Logging Settings");
   7516 TRANSLATE_NOOP("FullscreenUI", "Login");
   7517 TRANSLATE_NOOP("FullscreenUI", "Login token generated on {}");
   7518 TRANSLATE_NOOP("FullscreenUI", "Logout");
   7519 TRANSLATE_NOOP("FullscreenUI", "Logs BIOS calls to printf(). Not all games contain debugging messages.");
   7520 TRANSLATE_NOOP("FullscreenUI", "Logs in to RetroAchievements.");
   7521 TRANSLATE_NOOP("FullscreenUI", "Logs messages to duckstation.log in the user directory.");
   7522 TRANSLATE_NOOP("FullscreenUI", "Logs messages to the console window.");
   7523 TRANSLATE_NOOP("FullscreenUI", "Logs messages to the debug console where supported.");
   7524 TRANSLATE_NOOP("FullscreenUI", "Logs out of RetroAchievements.");
   7525 TRANSLATE_NOOP("FullscreenUI", "Macro {} Buttons");
   7526 TRANSLATE_NOOP("FullscreenUI", "Macro {} Frequency");
   7527 TRANSLATE_NOOP("FullscreenUI", "Macro {} Press To Toggle");
   7528 TRANSLATE_NOOP("FullscreenUI", "Macro {} Trigger");
   7529 TRANSLATE_NOOP("FullscreenUI", "Makes games run closer to their console framerate, at a small cost to performance.");
   7530 TRANSLATE_NOOP("FullscreenUI", "Memory Card Busy");
   7531 TRANSLATE_NOOP("FullscreenUI", "Memory Card Directory");
   7532 TRANSLATE_NOOP("FullscreenUI", "Memory Card Port {}");
   7533 TRANSLATE_NOOP("FullscreenUI", "Memory Card Settings");
   7534 TRANSLATE_NOOP("FullscreenUI", "Memory Card {} Type");
   7535 TRANSLATE_NOOP("FullscreenUI", "Merge Multi-Disc Games");
   7536 TRANSLATE_NOOP("FullscreenUI", "Merges multi-disc games into one item in the game list.");
   7537 TRANSLATE_NOOP("FullscreenUI", "Minimal Output Latency");
   7538 TRANSLATE_NOOP("FullscreenUI", "Move Down");
   7539 TRANSLATE_NOOP("FullscreenUI", "Move Up");
   7540 TRANSLATE_NOOP("FullscreenUI", "Moves this shader higher in the chain, applying it earlier.");
   7541 TRANSLATE_NOOP("FullscreenUI", "Moves this shader lower in the chain, applying it later.");
   7542 TRANSLATE_NOOP("FullscreenUI", "Multitap");
   7543 TRANSLATE_NOOP("FullscreenUI", "Multitap Mode");
   7544 TRANSLATE_NOOP("FullscreenUI", "Mute All Sound");
   7545 TRANSLATE_NOOP("FullscreenUI", "Mute CD Audio");
   7546 TRANSLATE_NOOP("FullscreenUI", "Navigate");
   7547 TRANSLATE_NOOP("FullscreenUI", "No Binding");
   7548 TRANSLATE_NOOP("FullscreenUI", "No Game Selected");
   7549 TRANSLATE_NOOP("FullscreenUI", "No cheats found for {}.");
   7550 TRANSLATE_NOOP("FullscreenUI", "No input profiles available.");
   7551 TRANSLATE_NOOP("FullscreenUI", "No resume save state found.");
   7552 TRANSLATE_NOOP("FullscreenUI", "No save present in this slot.");
   7553 TRANSLATE_NOOP("FullscreenUI", "No save states found.");
   7554 TRANSLATE_NOOP("FullscreenUI", "No, resume the game.");
   7555 TRANSLATE_NOOP("FullscreenUI", "None (Double Speed)");
   7556 TRANSLATE_NOOP("FullscreenUI", "None (Normal Speed)");
   7557 TRANSLATE_NOOP("FullscreenUI", "Not Logged In");
   7558 TRANSLATE_NOOP("FullscreenUI", "Not Scanning Subdirectories");
   7559 TRANSLATE_NOOP("FullscreenUI", "OK");
   7560 TRANSLATE_NOOP("FullscreenUI", "OSD Scale");
   7561 TRANSLATE_NOOP("FullscreenUI", "On-Screen Display");
   7562 TRANSLATE_NOOP("FullscreenUI", "Open Containing Directory");
   7563 TRANSLATE_NOOP("FullscreenUI", "Open in File Browser");
   7564 TRANSLATE_NOOP("FullscreenUI", "Operations");
   7565 TRANSLATE_NOOP("FullscreenUI", "Optimal Frame Pacing");
   7566 TRANSLATE_NOOP("FullscreenUI", "Options");
   7567 TRANSLATE_NOOP("FullscreenUI", "Output Latency");
   7568 TRANSLATE_NOOP("FullscreenUI", "Output Volume");
   7569 TRANSLATE_NOOP("FullscreenUI", "Overclocking Percentage");
   7570 TRANSLATE_NOOP("FullscreenUI", "Overlays or replaces normal triangle drawing with a wireframe/line view.");
   7571 TRANSLATE_NOOP("FullscreenUI", "PGXP (Precision Geometry Transform Pipeline)");
   7572 TRANSLATE_NOOP("FullscreenUI", "PGXP Depth Buffer");
   7573 TRANSLATE_NOOP("FullscreenUI", "PGXP Geometry Correction");
   7574 TRANSLATE_NOOP("FullscreenUI", "Parent Directory");
   7575 TRANSLATE_NOOP("FullscreenUI", "Patches");
   7576 TRANSLATE_NOOP("FullscreenUI", "Patches the BIOS to skip the boot animation. Safe to enable.");
   7577 TRANSLATE_NOOP("FullscreenUI", "Path");
   7578 TRANSLATE_NOOP("FullscreenUI", "Pause On Controller Disconnection");
   7579 TRANSLATE_NOOP("FullscreenUI", "Pause On Focus Loss");
   7580 TRANSLATE_NOOP("FullscreenUI", "Pause On Start");
   7581 TRANSLATE_NOOP("FullscreenUI", "Pauses the emulator when a controller with bindings is disconnected.");
   7582 TRANSLATE_NOOP("FullscreenUI", "Pauses the emulator when a game is started.");
   7583 TRANSLATE_NOOP("FullscreenUI", "Pauses the emulator when you minimize the window or switch to another application, and unpauses when you switch back.");
   7584 TRANSLATE_NOOP("FullscreenUI", "Per-Game Configuration");
   7585 TRANSLATE_NOOP("FullscreenUI", "Per-game controller configuration initialized with global settings.");
   7586 TRANSLATE_NOOP("FullscreenUI", "Performance enhancement - jumps directly between blocks instead of returning to the dispatcher.");
   7587 TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Colors");
   7588 TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Textures");
   7589 TRANSLATE_NOOP("FullscreenUI", "Plays sound effects for events such as achievement unlocks and leaderboard submissions.");
   7590 TRANSLATE_NOOP("FullscreenUI", "Port {} Controller Type");
   7591 TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings");
   7592 TRANSLATE_NOOP("FullscreenUI", "Post-processing chain cleared.");
   7593 TRANSLATE_NOOP("FullscreenUI", "Post-processing shaders reloaded.");
   7594 TRANSLATE_NOOP("FullscreenUI", "Preload Images to RAM");
   7595 TRANSLATE_NOOP("FullscreenUI", "Preload Replacement Textures");
   7596 TRANSLATE_NOOP("FullscreenUI", "Presents frames on a background thread when fast forwarding or vsync is disabled.");
   7597 TRANSLATE_NOOP("FullscreenUI", "Preserve Projection Precision");
   7598 TRANSLATE_NOOP("FullscreenUI", "Prevents the emulator from producing any audible sound.");
   7599 TRANSLATE_NOOP("FullscreenUI", "Prevents the screen saver from activating and the host from sleeping while emulation is running.");
   7600 TRANSLATE_NOOP("FullscreenUI", "Provides vibration and LED control support over Bluetooth.");
   7601 TRANSLATE_NOOP("FullscreenUI", "Push a controller button or axis now.");
   7602 TRANSLATE_NOOP("FullscreenUI", "Quick Save");
   7603 TRANSLATE_NOOP("FullscreenUI", "RAIntegration is being used instead of the built-in achievements implementation.");
   7604 TRANSLATE_NOOP("FullscreenUI", "Read Speedup");
   7605 TRANSLATE_NOOP("FullscreenUI", "Readahead Sectors");
   7606 TRANSLATE_NOOP("FullscreenUI", "Recompiler Fast Memory Access");
   7607 TRANSLATE_NOOP("FullscreenUI", "Reduce Input Latency");
   7608 TRANSLATE_NOOP("FullscreenUI", "Reduces \"wobbly\" polygons by attempting to preserve the fractional component through memory transfers.");
   7609 TRANSLATE_NOOP("FullscreenUI", "Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread.");
   7610 TRANSLATE_NOOP("FullscreenUI", "Reduces input latency by delaying the start of frame until closer to the presentation time.");
   7611 TRANSLATE_NOOP("FullscreenUI", "Reduces polygon Z-fighting through depth testing. Low compatibility with games.");
   7612 TRANSLATE_NOOP("FullscreenUI", "Reduces the size of save states by compressing the data before saving.");
   7613 TRANSLATE_NOOP("FullscreenUI", "Region");
   7614 TRANSLATE_NOOP("FullscreenUI", "Region: ");
   7615 TRANSLATE_NOOP("FullscreenUI", "Release Date: %s");
   7616 TRANSLATE_NOOP("FullscreenUI", "Reload Shaders");
   7617 TRANSLATE_NOOP("FullscreenUI", "Reloads the shaders from disk, applying any changes.");
   7618 TRANSLATE_NOOP("FullscreenUI", "Remove From Chain");
   7619 TRANSLATE_NOOP("FullscreenUI", "Remove From List");
   7620 TRANSLATE_NOOP("FullscreenUI", "Removed stage {} ({}).");
   7621 TRANSLATE_NOOP("FullscreenUI", "Removes this shader from the chain.");
   7622 TRANSLATE_NOOP("FullscreenUI", "Renames existing save states when saving to a backup file.");
   7623 TRANSLATE_NOOP("FullscreenUI", "Rendering");
   7624 TRANSLATE_NOOP("FullscreenUI", "Replaces these settings with a previously saved input profile.");
   7625 TRANSLATE_NOOP("FullscreenUI", "Rescan All Games");
   7626 TRANSLATE_NOOP("FullscreenUI", "Reset Memory Card Directory");
   7627 TRANSLATE_NOOP("FullscreenUI", "Reset Play Time");
   7628 TRANSLATE_NOOP("FullscreenUI", "Reset Settings");
   7629 TRANSLATE_NOOP("FullscreenUI", "Reset System");
   7630 TRANSLATE_NOOP("FullscreenUI", "Resets all configuration to defaults (including bindings).");
   7631 TRANSLATE_NOOP("FullscreenUI", "Resets memory card directory to default (user directory).");
   7632 TRANSLATE_NOOP("FullscreenUI", "Resolution change will be applied after restarting.");
   7633 TRANSLATE_NOOP("FullscreenUI", "Restores the state of the system prior to the last state loaded.");
   7634 TRANSLATE_NOOP("FullscreenUI", "Resume Game");
   7635 TRANSLATE_NOOP("FullscreenUI", "Resume Last Session");
   7636 TRANSLATE_NOOP("FullscreenUI", "Return To Game");
   7637 TRANSLATE_NOOP("FullscreenUI", "Return to desktop mode, or exit the application.");
   7638 TRANSLATE_NOOP("FullscreenUI", "Return to the previous menu.");
   7639 TRANSLATE_NOOP("FullscreenUI", "Reverses the game list sort order from the default (usually ascending to descending).");
   7640 TRANSLATE_NOOP("FullscreenUI", "Rewind Save Frequency");
   7641 TRANSLATE_NOOP("FullscreenUI", "Rewind Save Slots");
   7642 TRANSLATE_NOOP("FullscreenUI", "Rewind for {0} frames, lasting {1:.2f} seconds will require up to {2} MB of RAM and {3} MB of VRAM.");
   7643 TRANSLATE_NOOP("FullscreenUI", "Rewind is disabled because runahead is enabled. Runahead will significantly increase system requirements.");
   7644 TRANSLATE_NOOP("FullscreenUI", "Rewind is not enabled. Please note that enabling rewind may significantly increase system requirements.");
   7645 TRANSLATE_NOOP("FullscreenUI", "Rich presence inactive or unsupported.");
   7646 TRANSLATE_NOOP("FullscreenUI", "Round Upscaled Texture Coordinates");
   7647 TRANSLATE_NOOP("FullscreenUI", "Rounds texture coordinates instead of flooring when upscaling. Can fix misaligned textures in some games, but break others, and is incompatible with texture filtering.");
   7648 TRANSLATE_NOOP("FullscreenUI", "Runahead");
   7649 TRANSLATE_NOOP("FullscreenUI", "Runahead/Rewind");
   7650 TRANSLATE_NOOP("FullscreenUI", "Runs the software renderer in parallel for VRAM readbacks. On some systems, this may result in greater performance.");
   7651 TRANSLATE_NOOP("FullscreenUI", "SDL DualSense Player LED");
   7652 TRANSLATE_NOOP("FullscreenUI", "SDL DualShock 4 / DualSense Enhanced Mode");
   7653 TRANSLATE_NOOP("FullscreenUI", "Save Profile");
   7654 TRANSLATE_NOOP("FullscreenUI", "Save Screenshot");
   7655 TRANSLATE_NOOP("FullscreenUI", "Save State");
   7656 TRANSLATE_NOOP("FullscreenUI", "Save State Compression");
   7657 TRANSLATE_NOOP("FullscreenUI", "Save State On Exit");
   7658 TRANSLATE_NOOP("FullscreenUI", "Saved {:%c}");
   7659 TRANSLATE_NOOP("FullscreenUI", "Saves state periodically so you can rewind any mistakes while playing.");
   7660 TRANSLATE_NOOP("FullscreenUI", "Scaled Dithering");
   7661 TRANSLATE_NOOP("FullscreenUI", "Scales internal VRAM resolution by the specified multiplier. Some games require 1x VRAM resolution.");
   7662 TRANSLATE_NOOP("FullscreenUI", "Scales the dithering pattern with the internal rendering resolution, making it less noticeable. Usually safe to enable.");
   7663 TRANSLATE_NOOP("FullscreenUI", "Scaling");
   7664 TRANSLATE_NOOP("FullscreenUI", "Scan For New Games");
   7665 TRANSLATE_NOOP("FullscreenUI", "Scanning Subdirectories");
   7666 TRANSLATE_NOOP("FullscreenUI", "Screen Position");
   7667 TRANSLATE_NOOP("FullscreenUI", "Screen Rotation");
   7668 TRANSLATE_NOOP("FullscreenUI", "Screenshot Format");
   7669 TRANSLATE_NOOP("FullscreenUI", "Screenshot Quality");
   7670 TRANSLATE_NOOP("FullscreenUI", "Screenshot Size");
   7671 TRANSLATE_NOOP("FullscreenUI", "Search Directories");
   7672 TRANSLATE_NOOP("FullscreenUI", "Seek Speedup");
   7673 TRANSLATE_NOOP("FullscreenUI", "Select");
   7674 TRANSLATE_NOOP("FullscreenUI", "Select Device");
   7675 TRANSLATE_NOOP("FullscreenUI", "Select Disc");
   7676 TRANSLATE_NOOP("FullscreenUI", "Select Disc Drive");
   7677 TRANSLATE_NOOP("FullscreenUI", "Select Disc Image");
   7678 TRANSLATE_NOOP("FullscreenUI", "Select Game");
   7679 TRANSLATE_NOOP("FullscreenUI", "Select Macro {} Binds");
   7680 TRANSLATE_NOOP("FullscreenUI", "Select State");
   7681 TRANSLATE_NOOP("FullscreenUI", "Selects the GPU to use for rendering.");
   7682 TRANSLATE_NOOP("FullscreenUI", "Selects the percentage of the normal clock speed the emulated hardware will run at.");
   7683 TRANSLATE_NOOP("FullscreenUI", "Selects the quality at which screenshots will be compressed.");
   7684 TRANSLATE_NOOP("FullscreenUI", "Selects the resolution scale that will be applied to the final image. 1x will downsample to the original console resolution.");
   7685 TRANSLATE_NOOP("FullscreenUI", "Selects the resolution to use in fullscreen modes.");
   7686 TRANSLATE_NOOP("FullscreenUI", "Selects the view that the game list will open to.");
   7687 TRANSLATE_NOOP("FullscreenUI", "Serial");
   7688 TRANSLATE_NOOP("FullscreenUI", "Session: {}");
   7689 TRANSLATE_NOOP("FullscreenUI", "Set Input Binding");
   7690 TRANSLATE_NOOP("FullscreenUI", "Set VRAM Write Dump Alpha Channel");
   7691 TRANSLATE_NOOP("FullscreenUI", "Sets a threshold for discarding precise values when exceeded. May help with glitches in some games.");
   7692 TRANSLATE_NOOP("FullscreenUI", "Sets a threshold for discarding the emulated depth buffer. May help in some games.");
   7693 TRANSLATE_NOOP("FullscreenUI", "Sets the fast forward speed. It is not guaranteed that this speed will be reached on all systems.");
   7694 TRANSLATE_NOOP("FullscreenUI", "Sets the target emulation speed. It is not guaranteed that this speed will be reached on all systems.");
   7695 TRANSLATE_NOOP("FullscreenUI", "Sets the turbo speed. It is not guaranteed that this speed will be reached on all systems.");
   7696 TRANSLATE_NOOP("FullscreenUI", "Sets the verbosity of messages logged. Higher levels will log more messages.");
   7697 TRANSLATE_NOOP("FullscreenUI", "Sets which sort of memory card image will be used for slot {}.");
   7698 TRANSLATE_NOOP("FullscreenUI", "Setting {} binding {}.");
   7699 TRANSLATE_NOOP("FullscreenUI", "Settings");
   7700 TRANSLATE_NOOP("FullscreenUI", "Settings and Operations");
   7701 TRANSLATE_NOOP("FullscreenUI", "Shader {} added as stage {}.");
   7702 TRANSLATE_NOOP("FullscreenUI", "Shared Card Name");
   7703 TRANSLATE_NOOP("FullscreenUI", "Show CPU Usage");
   7704 TRANSLATE_NOOP("FullscreenUI", "Show Controller Input");
   7705 TRANSLATE_NOOP("FullscreenUI", "Show Enhancement Settings");
   7706 TRANSLATE_NOOP("FullscreenUI", "Show FPS");
   7707 TRANSLATE_NOOP("FullscreenUI", "Show Frame Times");
   7708 TRANSLATE_NOOP("FullscreenUI", "Show GPU Statistics");
   7709 TRANSLATE_NOOP("FullscreenUI", "Show GPU Usage");
   7710 TRANSLATE_NOOP("FullscreenUI", "Show Latency Statistics");
   7711 TRANSLATE_NOOP("FullscreenUI", "Show OSD Messages");
   7712 TRANSLATE_NOOP("FullscreenUI", "Show Resolution");
   7713 TRANSLATE_NOOP("FullscreenUI", "Show Speed");
   7714 TRANSLATE_NOOP("FullscreenUI", "Show Status Indicators");
   7715 TRANSLATE_NOOP("FullscreenUI", "Shows a visual history of frame times in the upper-left corner of the display.");
   7716 TRANSLATE_NOOP("FullscreenUI", "Shows enhancement settings in the bottom-right corner of the screen.");
   7717 TRANSLATE_NOOP("FullscreenUI", "Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.");
   7718 TRANSLATE_NOOP("FullscreenUI", "Shows information about input and audio latency in the top-right corner of the display.");
   7719 TRANSLATE_NOOP("FullscreenUI", "Shows information about the emulated GPU in the top-right corner of the display.");
   7720 TRANSLATE_NOOP("FullscreenUI", "Shows on-screen-display messages when events occur.");
   7721 TRANSLATE_NOOP("FullscreenUI", "Shows persistent icons when turbo is active or when paused.");
   7722 TRANSLATE_NOOP("FullscreenUI", "Shows the current controller state of the system in the bottom-left corner of the display.");
   7723 TRANSLATE_NOOP("FullscreenUI", "Shows the current emulation speed of the system in the top-right corner of the display as a percentage.");
   7724 TRANSLATE_NOOP("FullscreenUI", "Shows the current rendering resolution of the system in the top-right corner of the display.");
   7725 TRANSLATE_NOOP("FullscreenUI", "Shows the game you are currently playing as part of your profile in Discord.");
   7726 TRANSLATE_NOOP("FullscreenUI", "Shows the host's CPU usage based on threads in the top-right corner of the display.");
   7727 TRANSLATE_NOOP("FullscreenUI", "Shows the host's GPU usage in the top-right corner of the display.");
   7728 TRANSLATE_NOOP("FullscreenUI", "Shows the number of frames (or v-syncs) displayed per second by the system in the top-right corner of the display.");
   7729 TRANSLATE_NOOP("FullscreenUI", "Simulates the CPU's instruction cache in the recompiler. Can help with games running too fast.");
   7730 TRANSLATE_NOOP("FullscreenUI", "Simulates the region check present in original, unmodified consoles.");
   7731 TRANSLATE_NOOP("FullscreenUI", "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very high system requirements.");
   7732 TRANSLATE_NOOP("FullscreenUI", "Skip Duplicate Frame Display");
   7733 TRANSLATE_NOOP("FullscreenUI", "Skips the presentation/display of frames that are not unique. Can result in worse frame pacing.");
   7734 TRANSLATE_NOOP("FullscreenUI", "Slow Boot");
   7735 TRANSLATE_NOOP("FullscreenUI", "Smooths out blockyness between colour transitions in 24-bit content, usually FMVs.");
   7736 TRANSLATE_NOOP("FullscreenUI", "Smooths out the blockiness of magnified textures on 2D objects.");
   7737 TRANSLATE_NOOP("FullscreenUI", "Smooths out the blockiness of magnified textures on 3D objects.");
   7738 TRANSLATE_NOOP("FullscreenUI", "Sort By");
   7739 TRANSLATE_NOOP("FullscreenUI", "Sort Reversed");
   7740 TRANSLATE_NOOP("FullscreenUI", "Sound Effects");
   7741 TRANSLATE_NOOP("FullscreenUI", "Specifies the amount of buffer time added, which reduces the additional sleep time introduced.");
   7742 TRANSLATE_NOOP("FullscreenUI", "Spectator Mode");
   7743 TRANSLATE_NOOP("FullscreenUI", "Speed Control");
   7744 TRANSLATE_NOOP("FullscreenUI", "Speeds up CD-ROM reads by the specified factor. May improve loading speeds in some games, and break others.");
   7745 TRANSLATE_NOOP("FullscreenUI", "Speeds up CD-ROM seeks by the specified factor. May improve loading speeds in some games, and break others.");
   7746 TRANSLATE_NOOP("FullscreenUI", "Sprite Texture Filtering");
   7747 TRANSLATE_NOOP("FullscreenUI", "Stage {}: {}");
   7748 TRANSLATE_NOOP("FullscreenUI", "Start BIOS");
   7749 TRANSLATE_NOOP("FullscreenUI", "Start Disc");
   7750 TRANSLATE_NOOP("FullscreenUI", "Start File");
   7751 TRANSLATE_NOOP("FullscreenUI", "Start Fullscreen");
   7752 TRANSLATE_NOOP("FullscreenUI", "Start Game");
   7753 TRANSLATE_NOOP("FullscreenUI", "Start a game from a disc in your PC's DVD drive.");
   7754 TRANSLATE_NOOP("FullscreenUI", "Start the console without any disc inserted.");
   7755 TRANSLATE_NOOP("FullscreenUI", "Stores the current settings to an input profile.");
   7756 TRANSLATE_NOOP("FullscreenUI", "Stretch Display Vertically");
   7757 TRANSLATE_NOOP("FullscreenUI", "Stretch Mode");
   7758 TRANSLATE_NOOP("FullscreenUI", "Stretches the display to match the aspect ratio by multiplying vertically instead of horizontally.");
   7759 TRANSLATE_NOOP("FullscreenUI", "Summary");
   7760 TRANSLATE_NOOP("FullscreenUI", "Switches back to 4:3 display aspect ratio when displaying 24-bit content, usually FMVs.");
   7761 TRANSLATE_NOOP("FullscreenUI", "Switches between full screen and windowed when the window is double-clicked.");
   7762 TRANSLATE_NOOP("FullscreenUI", "Sync To Host Refresh Rate");
   7763 TRANSLATE_NOOP("FullscreenUI", "Synchronizes presentation of the console's frames to the host. GSync/FreeSync users should enable Optimal Frame Pacing instead.");
   7764 TRANSLATE_NOOP("FullscreenUI", "Temporarily disables all enhancements, useful when testing.");
   7765 TRANSLATE_NOOP("FullscreenUI", "Test Unofficial Achievements");
   7766 TRANSLATE_NOOP("FullscreenUI", "Texture Filtering");
   7767 TRANSLATE_NOOP("FullscreenUI", "Texture Replacements");
   7768 TRANSLATE_NOOP("FullscreenUI", "The SDL input source supports most controllers.");
   7769 TRANSLATE_NOOP("FullscreenUI", "The XInput source provides support for XBox 360/XBox One/XBox Series controllers.");
   7770 TRANSLATE_NOOP("FullscreenUI", "The audio backend determines how frames produced by the emulator are submitted to the host.");
   7771 TRANSLATE_NOOP("FullscreenUI", "The selected memory card image will be used in shared mode for this slot.");
   7772 TRANSLATE_NOOP("FullscreenUI", "This game has no achievements.");
   7773 TRANSLATE_NOOP("FullscreenUI", "This game has no leaderboards.");
   7774 TRANSLATE_NOOP("FullscreenUI", "Threaded Presentation");
   7775 TRANSLATE_NOOP("FullscreenUI", "Threaded Rendering");
   7776 TRANSLATE_NOOP("FullscreenUI", "Time Played");
   7777 TRANSLATE_NOOP("FullscreenUI", "Time Played: %s");
   7778 TRANSLATE_NOOP("FullscreenUI", "Timing out in {:.0f} seconds...");
   7779 TRANSLATE_NOOP("FullscreenUI", "Title");
   7780 TRANSLATE_NOOP("FullscreenUI", "Toggle Analog");
   7781 TRANSLATE_NOOP("FullscreenUI", "Toggle Fast Forward");
   7782 TRANSLATE_NOOP("FullscreenUI", "Toggle Fullscreen");
   7783 TRANSLATE_NOOP("FullscreenUI", "Toggle every %d frames");
   7784 TRANSLATE_NOOP("FullscreenUI", "True Color Debanding");
   7785 TRANSLATE_NOOP("FullscreenUI", "True Color Rendering");
   7786 TRANSLATE_NOOP("FullscreenUI", "Turbo Speed");
   7787 TRANSLATE_NOOP("FullscreenUI", "Type");
   7788 TRANSLATE_NOOP("FullscreenUI", "UI Language");
   7789 TRANSLATE_NOOP("FullscreenUI", "Uncompressed Size");
   7790 TRANSLATE_NOOP("FullscreenUI", "Uncompressed Size: %.2f MB");
   7791 TRANSLATE_NOOP("FullscreenUI", "Undo Load State");
   7792 TRANSLATE_NOOP("FullscreenUI", "Unknown");
   7793 TRANSLATE_NOOP("FullscreenUI", "Unknown File Size");
   7794 TRANSLATE_NOOP("FullscreenUI", "Unlimited");
   7795 TRANSLATE_NOOP("FullscreenUI", "Use Blit Swap Chain");
   7796 TRANSLATE_NOOP("FullscreenUI", "Use Debug GPU Device");
   7797 TRANSLATE_NOOP("FullscreenUI", "Use Global Setting");
   7798 TRANSLATE_NOOP("FullscreenUI", "Use Light Theme");
   7799 TRANSLATE_NOOP("FullscreenUI", "Use Old MDEC Routines");
   7800 TRANSLATE_NOOP("FullscreenUI", "Use Single Card For Multi-Disc Games");
   7801 TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For Readbacks");
   7802 TRANSLATE_NOOP("FullscreenUI", "Username: {}");
   7803 TRANSLATE_NOOP("FullscreenUI", "Uses PGXP for all instructions, not just memory operations.");
   7804 TRANSLATE_NOOP("FullscreenUI", "Uses a blit presentation model instead of flipping. This may be needed on some systems.");
   7805 TRANSLATE_NOOP("FullscreenUI", "Uses a light coloured theme instead of the default dark theme.");
   7806 TRANSLATE_NOOP("FullscreenUI", "Uses a second thread for drawing graphics. Speed boost, and safe to use.");
   7807 TRANSLATE_NOOP("FullscreenUI", "Uses game-specific settings for controllers for this game.");
   7808 TRANSLATE_NOOP("FullscreenUI", "Uses native resolution coordinates for 2D polygons, instead of precise coordinates. Can fix misaligned UI in some games, but otherwise should be left disabled.");
   7809 TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for colors, which can improve visuals in some games.");
   7810 TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for texture coordinates, straightening out warped textures.");
   7811 TRANSLATE_NOOP("FullscreenUI", "Uses screen positions to resolve PGXP data. May improve visuals in some games.");
   7812 TRANSLATE_NOOP("FullscreenUI", "Value: {} | Default: {} | Minimum: {} | Maximum: {}");
   7813 TRANSLATE_NOOP("FullscreenUI", "Vertex Cache");
   7814 TRANSLATE_NOOP("FullscreenUI", "Vertical Sync (VSync)");
   7815 TRANSLATE_NOOP("FullscreenUI", "WARNING: Your game is still saving to the memory card. Continuing to {0} may IRREVERSIBLY DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 seconds for it to finish saving.\n\nDo you want to {0} anyway?");
   7816 TRANSLATE_NOOP("FullscreenUI", "When enabled and logged in, DuckStation will scan for achievements on startup.");
   7817 TRANSLATE_NOOP("FullscreenUI", "When enabled, DuckStation will assume all achievements are locked and not send any unlock notifications to the server.");
   7818 TRANSLATE_NOOP("FullscreenUI", "When enabled, DuckStation will list achievements from unofficial sets. These achievements are not tracked by RetroAchievements.");
   7819 TRANSLATE_NOOP("FullscreenUI", "When enabled, each session will behave as if no achievements have been unlocked.");
   7820 TRANSLATE_NOOP("FullscreenUI", "When enabled, memory cards and controllers will be overwritten when save states are loaded.");
   7821 TRANSLATE_NOOP("FullscreenUI", "When enabled, the minimum supported output latency will be used for the host API.");
   7822 TRANSLATE_NOOP("FullscreenUI", "When playing a multi-disc game and using per-game (title) memory cards, use a single memory card for all discs.");
   7823 TRANSLATE_NOOP("FullscreenUI", "When this option is chosen, the clock speed set below will be used.");
   7824 TRANSLATE_NOOP("FullscreenUI", "Widescreen Rendering");
   7825 TRANSLATE_NOOP("FullscreenUI", "Wireframe Rendering");
   7826 TRANSLATE_NOOP("FullscreenUI", "Writes textures which can be replaced to the dump directory.");
   7827 TRANSLATE_NOOP("FullscreenUI", "Yes, {} now and risk memory card corruption.");
   7828 TRANSLATE_NOOP("FullscreenUI", "\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, cheats, and slowdown functions.");
   7829 TRANSLATE_NOOP("FullscreenUI", "\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe Limited. This software is not affiliated in any way with Sony Interactive Entertainment.");
   7830 TRANSLATE_NOOP("FullscreenUI", "change disc");
   7831 TRANSLATE_NOOP("FullscreenUI", "reset");
   7832 TRANSLATE_NOOP("FullscreenUI", "shut down");
   7833 TRANSLATE_NOOP("FullscreenUI", "{:%H:%M}");
   7834 TRANSLATE_NOOP("FullscreenUI", "{:%Y-%m-%d %H:%M:%S}");
   7835 TRANSLATE_NOOP("FullscreenUI", "{} Frames");
   7836 TRANSLATE_NOOP("FullscreenUI", "{} deleted.");
   7837 TRANSLATE_NOOP("FullscreenUI", "{} does not exist.");
   7838 TRANSLATE_NOOP("FullscreenUI", "{} is not a valid disc image.");
   7839 TRANSLATE_NOOP("FullscreenUI", "Version: %s");
   7840 // TRANSLATION-STRING-AREA-END
   7841 #endif