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

imgui_overlays.cpp (45233B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "imgui_overlays.h"
      5 #include "cdrom.h"
      6 #include "controller.h"
      7 #include "cpu_core_private.h"
      8 #include "dma.h"
      9 #include "fullscreen_ui.h"
     10 #include "gpu.h"
     11 #include "host.h"
     12 #include "mdec.h"
     13 #include "settings.h"
     14 #include "spu.h"
     15 #include "system.h"
     16 
     17 #include "util/gpu_device.h"
     18 #include "util/imgui_animated.h"
     19 #include "util/imgui_fullscreen.h"
     20 #include "util/imgui_manager.h"
     21 #include "util/input_manager.h"
     22 #include "util/media_capture.h"
     23 
     24 #include "common/align.h"
     25 #include "common/error.h"
     26 #include "common/file_system.h"
     27 #include "common/gsvector.h"
     28 #include "common/log.h"
     29 #include "common/path.h"
     30 #include "common/string_util.h"
     31 #include "common/thirdparty/SmallVector.h"
     32 #include "common/timer.h"
     33 
     34 #include "IconsEmoji.h"
     35 #include "IconsPromptFont.h"
     36 #include "fmt/chrono.h"
     37 #include "imgui.h"
     38 #include "imgui_internal.h"
     39 
     40 #include <atomic>
     41 #include <chrono>
     42 #include <cmath>
     43 #include <deque>
     44 #include <mutex>
     45 #include <span>
     46 
     47 Log_SetChannel(ImGuiManager);
     48 
     49 namespace ImGuiManager {
     50 static void FormatProcessorStat(SmallStringBase& text, double usage, double time);
     51 static void DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing);
     52 static void DrawMediaCaptureOverlay(float& position_y, float scale, float margin, float spacing);
     53 static void DrawFrameTimeOverlay(float& position_y, float scale, float margin, float spacing);
     54 static void DrawEnhancementsOverlay();
     55 static void DrawInputsOverlay();
     56 } // namespace ImGuiManager
     57 
     58 static std::tuple<float, float> GetMinMax(std::span<const float> values)
     59 {
     60   GSVector4 vmin(GSVector4::load<false>(values.data()));
     61   GSVector4 vmax(vmin);
     62 
     63   const u32 count = static_cast<u32>(values.size());
     64   const u32 aligned_count = Common::AlignDownPow2(count, 4);
     65   u32 i = 4;
     66   for (; i < aligned_count; i += 4)
     67   {
     68     const GSVector4 v(GSVector4::load<false>(&values[i]));
     69     vmin = vmin.min(v);
     70     vmax = vmax.max(v);
     71   }
     72 
     73   float min = std::min(vmin.x, std::min(vmin.y, std::min(vmin.z, vmin.w)));
     74   float max = std::max(vmax.x, std::max(vmax.y, std::max(vmax.z, vmax.w)));
     75   for (; i < count; i++)
     76   {
     77     min = std::min(min, values[i]);
     78     max = std::max(max, values[i]);
     79   }
     80 
     81   return std::tie(min, max);
     82 }
     83 
     84 void Host::DisplayLoadingScreen(const char* message, int progress_min /*= -1*/, int progress_max /*= -1*/,
     85                                 int progress_value /*= -1*/)
     86 {
     87   if (!g_gpu_device)
     88   {
     89     INFO_LOG("{}: {}/{}", message, progress_value, progress_max);
     90     return;
     91   }
     92 
     93   const auto& io = ImGui::GetIO();
     94   const float scale = ImGuiManager::GetGlobalScale();
     95   const float width = (400.0f * scale);
     96   const bool has_progress = (progress_min < progress_max);
     97 
     98   // eat the last imgui frame, it might've been partially rendered by the caller.
     99   ImGui::EndFrame();
    100   ImGui::NewFrame();
    101 
    102   const float logo_width = 260.0f * scale;
    103   const float logo_height = 260.0f * scale;
    104 
    105   ImGui::SetNextWindowSize(ImVec2(logo_width, logo_height), ImGuiCond_Always);
    106   ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, (io.DisplaySize.y * 0.5f) - (50.0f * scale)),
    107                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
    108   if (ImGui::Begin("LoadingScreenLogo", nullptr,
    109                    ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove |
    110                      ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav |
    111                      ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing |
    112                      ImGuiWindowFlags_NoBackground))
    113   {
    114     GPUTexture* tex = ImGuiFullscreen::GetCachedTexture("images/duck.png");
    115     if (tex)
    116       ImGui::Image(tex, ImVec2(logo_width, logo_height));
    117   }
    118   ImGui::End();
    119 
    120   const float padding_and_rounding = 18.0f * scale;
    121   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, padding_and_rounding);
    122   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding_and_rounding, padding_and_rounding));
    123   ImGui::SetNextWindowSize(ImVec2(width, (has_progress ? 90.0f : 55.0f) * scale), ImGuiCond_Always);
    124   ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, (io.DisplaySize.y * 0.5f) + (100.0f * scale)),
    125                           ImGuiCond_Always, ImVec2(0.5f, 0.0f));
    126   if (ImGui::Begin("LoadingScreen", nullptr,
    127                    ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove |
    128                      ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav |
    129                      ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing))
    130   {
    131     if (has_progress)
    132     {
    133       ImGui::TextUnformatted(message);
    134 
    135       TinyString buf;
    136       buf.format("{}/{}", progress_value, progress_max);
    137 
    138       const ImVec2 prog_size = ImGui::CalcTextSize(buf.c_str(), buf.end_ptr());
    139       ImGui::SameLine();
    140       ImGui::SetCursorPosX(width - padding_and_rounding - prog_size.x);
    141       ImGui::TextUnformatted(buf.c_str(), buf.end_ptr());
    142       ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5.0f);
    143 
    144       ImGui::ProgressBar(static_cast<float>(progress_value) / static_cast<float>(progress_max - progress_min),
    145                          ImVec2(-1.0f, 0.0f), "");
    146       INFO_LOG("{}: {}", message, buf);
    147     }
    148     else
    149     {
    150       const ImVec2 text_size(ImGui::CalcTextSize(message));
    151       ImGui::SetCursorPosX((width - text_size.x) / 2.0f);
    152       ImGui::TextUnformatted(message);
    153       INFO_LOG(message);
    154     }
    155   }
    156   ImGui::End();
    157   ImGui::PopStyleVar(2);
    158 
    159   ImGui::EndFrame();
    160 
    161   // TODO: Glass effect or something.
    162 
    163   if (g_gpu_device->BeginPresent(false))
    164   {
    165     g_gpu_device->RenderImGui();
    166     g_gpu_device->EndPresent(false);
    167   }
    168 
    169   ImGui::NewFrame();
    170 }
    171 
    172 void ImGuiManager::RenderDebugWindows()
    173 {
    174   if (System::IsValid())
    175   {
    176     if (g_settings.debugging.show_gpu_state)
    177       g_gpu->DrawDebugStateWindow();
    178     if (g_settings.debugging.show_cdrom_state)
    179       CDROM::DrawDebugWindow();
    180     if (g_settings.debugging.show_timers_state)
    181       Timers::DrawDebugStateWindow();
    182     if (g_settings.debugging.show_spu_state)
    183       SPU::DrawDebugStateWindow();
    184     if (g_settings.debugging.show_mdec_state)
    185       MDEC::DrawDebugStateWindow();
    186     if (g_settings.debugging.show_dma_state)
    187       DMA::DrawDebugStateWindow();
    188   }
    189 }
    190 
    191 void ImGuiManager::RenderTextOverlays()
    192 {
    193   const System::State state = System::GetState();
    194   if (state != System::State::Shutdown)
    195   {
    196     const float scale = ImGuiManager::GetGlobalScale();
    197     const float margin = std::ceil(10.0f * scale);
    198     const float spacing = std::ceil(5.0f * scale);
    199     float position_y = margin;
    200     DrawPerformanceOverlay(position_y, scale, margin, spacing);
    201     DrawFrameTimeOverlay(position_y, scale, margin, spacing);
    202     DrawMediaCaptureOverlay(position_y, scale, margin, spacing);
    203 
    204     if (g_settings.display_show_enhancements && state != System::State::Paused)
    205       DrawEnhancementsOverlay();
    206 
    207     if (g_settings.display_show_inputs && state != System::State::Paused)
    208       DrawInputsOverlay();
    209   }
    210 }
    211 
    212 void ImGuiManager::FormatProcessorStat(SmallStringBase& text, double usage, double time)
    213 {
    214   // Some values, such as GPU (and even CPU to some extent) can be out of phase with the wall clock,
    215   // which the processor time is divided by to get a utilization percentage. Let's clamp it at 100%,
    216   // so that people don't get confused, and remove the decimal places when it's there while we're at it.
    217   if (usage >= 99.95)
    218     text.append_format("100% ({:.2f}ms)", time);
    219   else
    220     text.append_format("{:.1f}% ({:.2f}ms)", usage, time);
    221 }
    222 
    223 void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing)
    224 {
    225   if (!(g_settings.display_show_fps || g_settings.display_show_speed || g_settings.display_show_gpu_stats ||
    226         g_settings.display_show_resolution || g_settings.display_show_cpu_usage ||
    227         (g_settings.display_show_status_indicators &&
    228          (System::IsPaused() || System::IsFastForwardEnabled() || System::IsTurboEnabled()))))
    229   {
    230     return;
    231   }
    232 
    233   const float shadow_offset = std::ceil(1.0f * scale);
    234   ImFont* fixed_font = ImGuiManager::GetFixedFont();
    235   ImFont* standard_font = ImGuiManager::GetStandardFont();
    236   ImDrawList* dl = ImGui::GetBackgroundDrawList();
    237   SmallString text;
    238   ImVec2 text_size;
    239 
    240 #define DRAW_LINE(font, text, color)                                                                                   \
    241   do                                                                                                                   \
    242   {                                                                                                                    \
    243     text_size =                                                                                                        \
    244       font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr);         \
    245     dl->AddText(                                                                                                       \
    246       font, font->FontSize,                                                                                            \
    247       ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset),         \
    248       IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());                                                           \
    249     dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), color,  \
    250                 (text));                                                                                               \
    251     position_y += text_size.y + spacing;                                                                               \
    252   } while (0)
    253 
    254   const System::State state = System::GetState();
    255   if (state == System::State::Running)
    256   {
    257     const float speed = System::GetEmulationSpeed();
    258     if (g_settings.display_show_fps)
    259       text.append_format("G: {:.2f} | V: {:.2f}", System::GetFPS(), System::GetVPS());
    260     if (g_settings.display_show_speed)
    261     {
    262       text.append_format("{}{}%", text.empty() ? "" : " | ", static_cast<u32>(std::round(speed)));
    263 
    264       const float target_speed = System::GetTargetSpeed();
    265       if (target_speed <= 0.0f)
    266         text.append(" (Max)");
    267       else
    268         text.append_format(" ({:.0f}%)", target_speed * 100.0f);
    269     }
    270     if (!text.empty())
    271     {
    272       ImU32 color;
    273       if (speed < 95.0f)
    274         color = IM_COL32(255, 100, 100, 255);
    275       else if (speed > 105.0f)
    276         color = IM_COL32(100, 255, 100, 255);
    277       else
    278         color = IM_COL32(255, 255, 255, 255);
    279 
    280       DRAW_LINE(fixed_font, text, color);
    281     }
    282 
    283     if (g_settings.display_show_gpu_stats)
    284     {
    285       g_gpu->GetStatsString(text);
    286       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    287 
    288       g_gpu->GetMemoryStatsString(text);
    289       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    290     }
    291 
    292     if (g_settings.display_show_resolution)
    293     {
    294       // TODO: this seems wrong?
    295       const auto [effective_width, effective_height] = g_gpu->GetEffectiveDisplayResolution();
    296       const bool interlaced = g_gpu->IsInterlacedDisplayEnabled();
    297       const bool pal = g_gpu->IsInPALMode();
    298       text.format("{}x{} {} {}", effective_width, effective_height, pal ? "PAL" : "NTSC",
    299                   interlaced ? "Interlaced" : "Progressive");
    300       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    301     }
    302 
    303     if (g_settings.display_show_latency_stats)
    304     {
    305       System::FormatLatencyStats(text);
    306       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    307     }
    308 
    309     if (g_settings.display_show_cpu_usage)
    310     {
    311       text.format("{:.2f}ms | {:.2f}ms | {:.2f}ms", System::GetMinimumFrameTime(), System::GetAverageFrameTime(),
    312                   System::GetMaximumFrameTime());
    313       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    314 
    315       if (g_settings.cpu_overclock_active || CPU::g_state.using_interpreter ||
    316           g_settings.cpu_execution_mode != CPUExecutionMode::Recompiler || g_settings.cpu_recompiler_icache ||
    317           g_settings.cpu_recompiler_memory_exceptions)
    318       {
    319         bool first = true;
    320         text.assign("CPU[");
    321         if (g_settings.cpu_overclock_active)
    322         {
    323           text.append_format("{}", g_settings.GetCPUOverclockPercent());
    324           first = false;
    325         }
    326         if (CPU::g_state.using_interpreter)
    327         {
    328           text.append_format("{}{}", first ? "" : "/", "I");
    329           first = false;
    330         }
    331         else if (g_settings.cpu_execution_mode == CPUExecutionMode::CachedInterpreter)
    332         {
    333           text.append_format("{}{}", first ? "" : "/", "CI");
    334           first = false;
    335         }
    336         else if (g_settings.cpu_execution_mode == CPUExecutionMode::NewRec)
    337         {
    338           text.append_format("{}{}", first ? "" : "/", "NR");
    339           first = false;
    340         }
    341         else
    342         {
    343           if (g_settings.cpu_recompiler_icache)
    344           {
    345             text.append_format("{}{}", first ? "" : "/", "IC");
    346             first = false;
    347           }
    348           if (g_settings.cpu_recompiler_memory_exceptions)
    349           {
    350             text.append_format("{}{}", first ? "" : "/", "ME");
    351             first = false;
    352           }
    353         }
    354 
    355         text.append("]: ");
    356       }
    357       else
    358       {
    359         text.assign("CPU: ");
    360       }
    361       FormatProcessorStat(text, System::GetCPUThreadUsage(), System::GetCPUThreadAverageTime());
    362       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    363 
    364       if (g_gpu->GetSWThread())
    365       {
    366         text.assign("SW: ");
    367         FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
    368         DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    369       }
    370 
    371 #ifndef __ANDROID__
    372       if (MediaCapture* cap = System::GetMediaCapture())
    373       {
    374         text.assign("CAP: ");
    375         FormatProcessorStat(text, cap->GetCaptureThreadUsage(), cap->GetCaptureThreadTime());
    376         DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    377       }
    378 #endif
    379     }
    380 
    381     if (g_settings.display_show_gpu_usage && g_gpu_device->IsGPUTimingEnabled())
    382     {
    383       text.assign("GPU: ");
    384       FormatProcessorStat(text, System::GetGPUUsage(), System::GetGPUAverageTime());
    385       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
    386     }
    387 
    388     if (g_settings.display_show_status_indicators)
    389     {
    390       const bool rewinding = System::IsRewinding();
    391       if (rewinding || System::IsFastForwardEnabled() || System::IsTurboEnabled())
    392       {
    393         text.assign(rewinding ? ICON_EMOJI_FAST_REVERSE : ICON_EMOJI_FAST_FORWARD);
    394         DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));
    395       }
    396     }
    397   }
    398   else if (g_settings.display_show_status_indicators && state == System::State::Paused &&
    399            !FullscreenUI::HasActiveWindow())
    400   {
    401     text.assign(ICON_EMOJI_PAUSE);
    402     DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));
    403   }
    404 
    405 #undef DRAW_LINE
    406 }
    407 
    408 void ImGuiManager::DrawEnhancementsOverlay()
    409 {
    410   LargeString text;
    411   text.append_format("{} {}-{}", Settings::GetConsoleRegionName(System::GetRegion()),
    412                      GPUDevice::RenderAPIToString(g_gpu_device->GetRenderAPI()),
    413                      g_gpu->IsHardwareRenderer() ? "HW" : "SW");
    414 
    415   if (g_settings.rewind_enable)
    416     text.append_format(" RW={}/{}", g_settings.rewind_save_frequency, g_settings.rewind_save_slots);
    417   if (g_settings.IsRunaheadEnabled())
    418     text.append_format(" RA={}", g_settings.runahead_frames);
    419 
    420   if (g_settings.cpu_overclock_active)
    421     text.append_format(" CPU={}%", g_settings.GetCPUOverclockPercent());
    422   if (g_settings.enable_8mb_ram)
    423     text.append(" 8MB");
    424   if (g_settings.cdrom_read_speedup != 1)
    425     text.append_format(" CDR={}x", g_settings.cdrom_read_speedup);
    426   if (g_settings.cdrom_seek_speedup != 1)
    427     text.append_format(" CDS={}x", g_settings.cdrom_seek_speedup);
    428   if (g_settings.gpu_resolution_scale != 1)
    429     text.append_format(" IR={}x", g_settings.gpu_resolution_scale);
    430   if (g_settings.gpu_multisamples != 1)
    431   {
    432     text.append_format(" {}x{}", g_settings.gpu_multisamples, g_settings.gpu_per_sample_shading ? "SSAA" : "MSAA");
    433   }
    434   if (g_settings.gpu_true_color)
    435   {
    436     if (g_settings.gpu_debanding)
    437     {
    438       text.append(" TrueColDeband");
    439     }
    440     else
    441     {
    442       text.append(" TrueCol");
    443     }
    444   }
    445   if (g_settings.gpu_disable_interlacing)
    446     text.append(" ForceProg");
    447   if (g_settings.gpu_force_ntsc_timings && System::GetRegion() == ConsoleRegion::PAL)
    448     text.append(" PAL60");
    449   if (g_settings.gpu_texture_filter != GPUTextureFilter::Nearest)
    450   {
    451     if (g_settings.gpu_sprite_texture_filter != g_settings.gpu_texture_filter)
    452     {
    453       text.append_format(" {}/{}", Settings::GetTextureFilterName(g_settings.gpu_texture_filter),
    454                          Settings::GetTextureFilterName(g_settings.gpu_sprite_texture_filter));
    455     }
    456     else
    457     {
    458       text.append_format(" {}", Settings::GetTextureFilterName(g_settings.gpu_texture_filter));
    459     }
    460   }
    461   if (g_settings.gpu_widescreen_hack && g_settings.display_aspect_ratio != DisplayAspectRatio::Auto &&
    462       g_settings.display_aspect_ratio != DisplayAspectRatio::R4_3)
    463   {
    464     text.append(" WSHack");
    465   }
    466   if (g_settings.gpu_line_detect_mode != GPULineDetectMode::Disabled)
    467     text.append_format(" LD={}", Settings::GetLineDetectModeName(g_settings.gpu_line_detect_mode));
    468   if (g_settings.gpu_pgxp_enable)
    469   {
    470     text.append(" PGXP");
    471     if (g_settings.gpu_pgxp_culling)
    472       text.append("/Cull");
    473     if (g_settings.gpu_pgxp_texture_correction)
    474       text.append("/Tex");
    475     if (g_settings.gpu_pgxp_color_correction)
    476       text.append("/Col");
    477     if (g_settings.gpu_pgxp_vertex_cache)
    478       text.append("/VC");
    479     if (g_settings.gpu_pgxp_cpu)
    480       text.append("/CPU");
    481     if (g_settings.gpu_pgxp_depth_buffer)
    482       text.append("/Depth");
    483   }
    484 
    485   const float scale = ImGuiManager::GetGlobalScale();
    486   const float shadow_offset = 1.0f * scale;
    487   const float margin = 10.0f * scale;
    488   ImFont* font = ImGuiManager::GetFixedFont();
    489   const float position_y = ImGui::GetIO().DisplaySize.y - margin - font->FontSize;
    490 
    491   ImDrawList* dl = ImGui::GetBackgroundDrawList();
    492   ImVec2 text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), -1.0f, text.c_str(),
    493                                          text.end_ptr(), nullptr);
    494   dl->AddText(font, font->FontSize,
    495               ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset),
    496               IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
    497   dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y),
    498               IM_COL32(255, 255, 255, 255), text.c_str(), text.end_ptr());
    499 }
    500 
    501 void ImGuiManager::DrawMediaCaptureOverlay(float& position_y, float scale, float margin, float spacing)
    502 {
    503 #ifndef __ANDROID__
    504   MediaCapture* const cap = System::GetMediaCapture();
    505   if (!cap || FullscreenUI::HasActiveWindow())
    506     return;
    507 
    508   const float shadow_offset = std::ceil(scale);
    509   ImFont* const standard_font = ImGuiManager::GetStandardFont();
    510   ImDrawList* dl = ImGui::GetBackgroundDrawList();
    511 
    512   static constexpr const char* ICON = ICON_PF_CIRCLE;
    513   const time_t elapsed_time = cap->GetElapsedTime();
    514   const TinyString text_msg = TinyString::from_format(" {:02d}:{:02d}:{:02d}", elapsed_time / 3600,
    515                                                       (elapsed_time % 3600) / 60, (elapsed_time % 3600) % 60);
    516   const ImVec2 icon_size = standard_font->CalcTextSizeA(standard_font->FontSize, std::numeric_limits<float>::max(),
    517                                                         -1.0f, ICON, nullptr, nullptr);
    518   const ImVec2 text_size = standard_font->CalcTextSizeA(standard_font->FontSize, std::numeric_limits<float>::max(),
    519                                                         -1.0f, text_msg.c_str(), text_msg.end_ptr(), nullptr);
    520 
    521   const float box_margin = 5.0f * scale;
    522   const ImVec2 box_size = ImVec2(icon_size.x + shadow_offset + text_size.x + box_margin * 2.0f,
    523                                  std::max(icon_size.x, text_size.y) + box_margin * 2.0f);
    524   const ImVec2 box_pos = ImVec2(ImGui::GetIO().DisplaySize.x - margin - box_size.x, position_y);
    525   dl->AddRectFilled(box_pos, box_pos + box_size, IM_COL32(0, 0, 0, 64), box_margin);
    526 
    527   const ImVec2 text_start = ImVec2(box_pos.x + box_margin, box_pos.y + box_margin);
    528   dl->AddText(standard_font, standard_font->FontSize,
    529               ImVec2(text_start.x + shadow_offset, text_start.y + shadow_offset), IM_COL32(0, 0, 0, 100), ICON);
    530   dl->AddText(standard_font, standard_font->FontSize,
    531               ImVec2(text_start.x + icon_size.x + shadow_offset, text_start.y + shadow_offset), IM_COL32(0, 0, 0, 100),
    532               text_msg.c_str(), text_msg.end_ptr());
    533   dl->AddText(standard_font, standard_font->FontSize, text_start, IM_COL32(255, 0, 0, 255), ICON);
    534   dl->AddText(standard_font, standard_font->FontSize, ImVec2(text_start.x + icon_size.x, text_start.y),
    535               IM_COL32(255, 255, 255, 255), text_msg.c_str(), text_msg.end_ptr());
    536 
    537   position_y += box_size.y + spacing;
    538 #endif
    539 }
    540 
    541 void ImGuiManager::DrawFrameTimeOverlay(float& position_y, float scale, float margin, float spacing)
    542 {
    543   if (!g_settings.display_show_frame_times || System::IsPaused())
    544     return;
    545 
    546   const float shadow_offset = std::ceil(1.0f * scale);
    547   ImFont* fixed_font = ImGuiManager::GetFixedFont();
    548 
    549   const ImVec2 history_size(200.0f * scale, 50.0f * scale);
    550   ImGui::SetNextWindowSize(ImVec2(history_size.x, history_size.y));
    551   ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - margin - history_size.x, position_y));
    552   ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.25f));
    553   ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
    554   ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
    555   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    556   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
    557   ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    558   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
    559   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
    560   if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs))
    561   {
    562     ImGui::PushFont(fixed_font);
    563 
    564     auto [min, max] = GetMinMax(System::GetFrameTimeHistory());
    565 
    566     // add a little bit of space either side, so we're not constantly resizing
    567     if ((max - min) < 4.0f)
    568     {
    569       min = min - std::fmod(min, 1.0f);
    570       max = max - std::fmod(max, 1.0f) + 1.0f;
    571       min = std::max(min - 2.0f, 0.0f);
    572       max += 2.0f;
    573     }
    574 
    575     ImGui::PlotEx(
    576       ImGuiPlotType_Lines, "##frame_times",
    577       [](void*, int idx) -> float {
    578         return System::GetFrameTimeHistory()[((System::GetFrameTimeHistoryPos() + idx) %
    579                                               System::NUM_FRAME_TIME_SAMPLES)];
    580       },
    581       nullptr, System::NUM_FRAME_TIME_SAMPLES, 0, nullptr, min, max, history_size);
    582 
    583     ImDrawList* win_dl = ImGui::GetCurrentWindow()->DrawList;
    584     const ImVec2 wpos(ImGui::GetCurrentWindow()->Pos);
    585 
    586     TinyString text;
    587     text.format("{:.1f} ms", max);
    588     ImVec2 text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
    589     win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset, wpos.y + shadow_offset),
    590                     IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
    591     win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y), IM_COL32(255, 255, 255, 255),
    592                     text.c_str(), text.end_ptr());
    593 
    594     text.format("{:.1f} ms", min);
    595     text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
    596     win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset,
    597                            wpos.y + history_size.y - fixed_font->FontSize + shadow_offset),
    598                     IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
    599     win_dl->AddText(
    600       ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y + history_size.y - fixed_font->FontSize),
    601       IM_COL32(255, 255, 255, 255), text.c_str(), text.end_ptr());
    602     ImGui::PopFont();
    603   }
    604   ImGui::End();
    605   ImGui::PopStyleVar(5);
    606   ImGui::PopStyleColor(3);
    607 
    608   position_y += history_size.y + spacing;
    609 }
    610 
    611 void ImGuiManager::DrawInputsOverlay()
    612 {
    613   const float scale = ImGuiManager::GetGlobalScale();
    614   const float shadow_offset = 1.0f * scale;
    615   const float margin = 10.0f * scale;
    616   const float spacing = 5.0f * scale;
    617   ImFont* font = ImGuiManager::GetStandardFont();
    618 
    619   static constexpr u32 text_color = IM_COL32(0xff, 0xff, 0xff, 255);
    620   static constexpr u32 shadow_color = IM_COL32(0x00, 0x00, 0x00, 100);
    621 
    622   const ImVec2& display_size = ImGui::GetIO().DisplaySize;
    623   ImDrawList* dl = ImGui::GetBackgroundDrawList();
    624 
    625   u32 num_ports = 0;
    626   for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
    627   {
    628     if (g_settings.controller_types[port] != ControllerType::None)
    629       num_ports++;
    630   }
    631 
    632   float current_x = margin;
    633   float current_y = display_size.y - margin - ((static_cast<float>(num_ports) * (font->FontSize + spacing)) - spacing);
    634 
    635   const ImVec4 clip_rect(current_x, current_y, display_size.x - margin, display_size.y - margin);
    636 
    637   SmallString text;
    638 
    639   for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
    640   {
    641     if (g_settings.controller_types[port] == ControllerType::None)
    642       continue;
    643 
    644     const Controller* controller = System::GetController(port);
    645     const Controller::ControllerInfo* cinfo =
    646       controller ? Controller::GetControllerInfo(controller->GetType()) : nullptr;
    647     if (!cinfo)
    648       continue;
    649 
    650     float text_start_x = current_x;
    651     if (cinfo->icon_name)
    652     {
    653       const ImVec2 icon_size = font->CalcTextSizeA(font->FontSize, FLT_MAX, 0.0f, cinfo->icon_name);
    654       const u32 icon_color = controller->GetInputOverlayIconColor();
    655       dl->AddText(font, font->FontSize, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color,
    656                   cinfo->icon_name, nullptr, 0.0f, &clip_rect);
    657       dl->AddText(font, font->FontSize, ImVec2(current_x, current_y), icon_color, cinfo->icon_name, nullptr, 0.0f,
    658                   &clip_rect);
    659       text_start_x += icon_size.x;
    660       text.format(" {}", port + 1u);
    661     }
    662     else
    663     {
    664       text.format("{} |", port + 1u);
    665     }
    666 
    667     for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
    668     {
    669       switch (bi.type)
    670       {
    671         case InputBindingInfo::Type::Axis:
    672         case InputBindingInfo::Type::HalfAxis:
    673         {
    674           // axes are always shown
    675           const float value = controller->GetBindState(bi.bind_index);
    676           if (value >= (254.0f / 255.0f))
    677             text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
    678           else if (value > (1.0f / 255.0f))
    679             text.append_format(" {}: {:.2f}", bi.icon_name ? bi.icon_name : bi.name, value);
    680         }
    681         break;
    682 
    683         case InputBindingInfo::Type::Button:
    684         {
    685           // buttons only shown when active
    686           const float value = controller->GetBindState(bi.bind_index);
    687           if (value >= 0.5f)
    688             text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
    689         }
    690         break;
    691 
    692         case InputBindingInfo::Type::Motor:
    693         case InputBindingInfo::Type::Macro:
    694         case InputBindingInfo::Type::Unknown:
    695         case InputBindingInfo::Type::Pointer:
    696         default:
    697           break;
    698       }
    699     }
    700 
    701     dl->AddText(font, font->FontSize, ImVec2(text_start_x + shadow_offset, current_y + shadow_offset), shadow_color,
    702                 text.c_str(), text.end_ptr(), 0.0f, &clip_rect);
    703     dl->AddText(font, font->FontSize, ImVec2(text_start_x, current_y), text_color, text.c_str(), text.end_ptr(), 0.0f,
    704                 &clip_rect);
    705 
    706     current_y += font->FontSize + spacing;
    707   }
    708 }
    709 
    710 namespace SaveStateSelectorUI {
    711 namespace {
    712 struct ListEntry
    713 {
    714   std::string summary;
    715   std::string game_details; // only in global slots
    716   std::string filename;
    717   std::unique_ptr<GPUTexture> preview_texture;
    718   s32 slot;
    719   bool global;
    720 };
    721 } // namespace
    722 
    723 static void InitializePlaceholderListEntry(ListEntry* li, const std::string& path, s32 slot, bool global);
    724 static void InitializeListEntry(ListEntry* li, ExtendedSaveStateInfo* ssi, const std::string& path, s32 slot,
    725                                 bool global);
    726 
    727 static void DestroyTextures();
    728 static void RefreshHotkeyLegend();
    729 static void Draw();
    730 static void ShowSlotOSDMessage();
    731 static std::string GetCurrentSlotPath();
    732 
    733 static constexpr const char* DATE_TIME_FORMAT =
    734   TRANSLATE_NOOP("SaveStateSelectorUI", "Saved at {0:%H:%M} on {0:%a} {0:%Y/%m/%d}.");
    735 
    736 static std::shared_ptr<GPUTexture> s_placeholder_texture;
    737 
    738 static std::string s_load_legend;
    739 static std::string s_save_legend;
    740 static std::string s_prev_legend;
    741 static std::string s_next_legend;
    742 
    743 static llvm::SmallVector<ListEntry, System::PER_GAME_SAVE_STATE_SLOTS + System::GLOBAL_SAVE_STATE_SLOTS> s_slots;
    744 static s32 s_current_slot = 0;
    745 static bool s_current_slot_global = false;
    746 
    747 static float s_open_time = 0.0f;
    748 static float s_close_time = 0.0f;
    749 
    750 static ImAnimatedFloat s_scroll_animated;
    751 static ImAnimatedFloat s_background_animated;
    752 
    753 static bool s_open = false;
    754 } // namespace SaveStateSelectorUI
    755 
    756 bool SaveStateSelectorUI::IsOpen()
    757 {
    758   return s_open;
    759 }
    760 
    761 void SaveStateSelectorUI::Open(float open_time /* = DEFAULT_OPEN_TIME */)
    762 {
    763   const std::string& serial = System::GetGameSerial();
    764 
    765   s_open_time = 0.0f;
    766   s_close_time = open_time;
    767 
    768   if (s_open)
    769     return;
    770 
    771   if (!s_placeholder_texture)
    772     s_placeholder_texture = ImGuiFullscreen::LoadTexture("no-save.png");
    773 
    774   s_open = true;
    775   RefreshList(serial);
    776   RefreshHotkeyLegend();
    777 }
    778 
    779 void SaveStateSelectorUI::Close()
    780 {
    781   s_open = false;
    782   s_load_legend = {};
    783   s_save_legend = {};
    784   s_prev_legend = {};
    785   s_next_legend = {};
    786 }
    787 
    788 void SaveStateSelectorUI::RefreshList(const std::string& serial)
    789 {
    790   for (ListEntry& entry : s_slots)
    791   {
    792     if (entry.preview_texture)
    793       g_gpu_device->RecycleTexture(std::move(entry.preview_texture));
    794   }
    795   s_slots.clear();
    796 
    797   if (System::IsShutdown())
    798     return;
    799 
    800   if (!serial.empty())
    801   {
    802     for (s32 i = 1; i <= System::PER_GAME_SAVE_STATE_SLOTS; i++)
    803     {
    804       std::string path(System::GetGameSaveStateFileName(serial, i));
    805       std::optional<ExtendedSaveStateInfo> ssi = System::GetExtendedSaveStateInfo(path.c_str());
    806 
    807       ListEntry li;
    808       if (ssi)
    809         InitializeListEntry(&li, &ssi.value(), std::move(path), i, false);
    810       else
    811         InitializePlaceholderListEntry(&li, std::move(path), i, false);
    812 
    813       s_slots.push_back(std::move(li));
    814     }
    815   }
    816 
    817   for (s32 i = 1; i <= System::GLOBAL_SAVE_STATE_SLOTS; i++)
    818   {
    819     std::string path(System::GetGlobalSaveStateFileName(i));
    820     std::optional<ExtendedSaveStateInfo> ssi = System::GetExtendedSaveStateInfo(path.c_str());
    821 
    822     ListEntry li;
    823     if (ssi)
    824       InitializeListEntry(&li, &ssi.value(), std::move(path), i, true);
    825     else
    826       InitializePlaceholderListEntry(&li, std::move(path), i, true);
    827 
    828     s_slots.push_back(std::move(li));
    829   }
    830 }
    831 
    832 void SaveStateSelectorUI::Clear()
    833 {
    834   // called on CPU thread at shutdown, textures should already be deleted, unless running
    835   // big picture UI, in which case we have to delete them here...
    836   ClearList();
    837 
    838   s_current_slot = 0;
    839   s_current_slot_global = false;
    840   s_scroll_animated.Reset(0.0f);
    841   s_background_animated.Reset(0.0f);
    842 }
    843 
    844 void SaveStateSelectorUI::ClearList()
    845 {
    846   for (ListEntry& li : s_slots)
    847   {
    848     if (li.preview_texture)
    849       g_gpu_device->RecycleTexture(std::move(li.preview_texture));
    850   }
    851   s_slots.clear();
    852 }
    853 
    854 void SaveStateSelectorUI::DestroyTextures()
    855 {
    856   Close();
    857 
    858   for (ListEntry& entry : s_slots)
    859   {
    860     if (entry.preview_texture)
    861       g_gpu_device->RecycleTexture(std::move(entry.preview_texture));
    862   }
    863 
    864   s_placeholder_texture.reset();
    865 }
    866 
    867 void SaveStateSelectorUI::RefreshHotkeyLegend()
    868 {
    869   auto format_legend_entry = [](SmallString binding, std::string_view caption) {
    870     InputManager::PrettifyInputBinding(binding);
    871     return fmt::format("{} - {}", binding, caption);
    872   };
    873 
    874   s_load_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "LoadSelectedSaveState"),
    875                                       TRANSLATE_SV("SaveStateSelectorUI", "Load"));
    876   s_save_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "SaveSelectedSaveState"),
    877                                       TRANSLATE_SV("SaveStateSelectorUI", "Save"));
    878   s_prev_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "SelectPreviousSaveStateSlot"),
    879                                       TRANSLATE_SV("SaveStateSelectorUI", "Select Previous"));
    880   s_next_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "SelectNextSaveStateSlot"),
    881                                       TRANSLATE_SV("SaveStateSelectorUI", "Select Next"));
    882 }
    883 
    884 void SaveStateSelectorUI::SelectNextSlot(bool open_selector)
    885 {
    886   const s32 total_slots = s_current_slot_global ? System::GLOBAL_SAVE_STATE_SLOTS : System::PER_GAME_SAVE_STATE_SLOTS;
    887   s_current_slot++;
    888   if (s_current_slot >= total_slots)
    889   {
    890     s_current_slot -= total_slots;
    891     s_current_slot_global = !s_current_slot_global;
    892     if (System::GetGameSerial().empty() && !s_current_slot_global)
    893     {
    894       s_current_slot_global = false;
    895       s_current_slot = 0;
    896     }
    897   }
    898 
    899   if (open_selector)
    900   {
    901     if (!s_open)
    902       Open();
    903 
    904     s_open_time = 0.0f;
    905   }
    906   else
    907   {
    908     ShowSlotOSDMessage();
    909   }
    910 }
    911 
    912 void SaveStateSelectorUI::SelectPreviousSlot(bool open_selector)
    913 {
    914   s_current_slot--;
    915   if (s_current_slot < 0)
    916   {
    917     s_current_slot_global = !s_current_slot_global;
    918     s_current_slot += s_current_slot_global ? System::GLOBAL_SAVE_STATE_SLOTS : System::PER_GAME_SAVE_STATE_SLOTS;
    919     if (System::GetGameSerial().empty() && !s_current_slot_global)
    920     {
    921       s_current_slot_global = false;
    922       s_current_slot = 0;
    923     }
    924   }
    925 
    926   if (open_selector)
    927   {
    928     if (!s_open)
    929       Open();
    930 
    931     s_open_time = 0.0f;
    932   }
    933   else
    934   {
    935     ShowSlotOSDMessage();
    936   }
    937 }
    938 
    939 void SaveStateSelectorUI::InitializeListEntry(ListEntry* li, ExtendedSaveStateInfo* ssi, const std::string& path,
    940                                               s32 slot, bool global)
    941 {
    942   if (global)
    943     li->game_details = fmt::format(TRANSLATE_FS("SaveStateSelectorUI", "{} ({})"), ssi->title, ssi->serial);
    944 
    945   li->summary = fmt::format(TRANSLATE_FS("SaveStateSelectorUI", DATE_TIME_FORMAT), fmt::localtime(ssi->timestamp));
    946   li->filename = Path::GetFileName(path);
    947   li->slot = slot;
    948   li->global = global;
    949 
    950   // Might not have a display yet, we're called at startup..
    951   if (g_gpu_device)
    952   {
    953     g_gpu_device->RecycleTexture(std::move(li->preview_texture));
    954 
    955     if (ssi->screenshot.IsValid())
    956     {
    957       li->preview_texture = g_gpu_device->FetchTexture(ssi->screenshot.GetWidth(), ssi->screenshot.GetHeight(), 1, 1, 1,
    958                                                        GPUTexture::Type::Texture, GPUTexture::Format::RGBA8,
    959                                                        ssi->screenshot.GetPixels(), ssi->screenshot.GetPitch());
    960       if (!li->preview_texture) [[unlikely]]
    961         ERROR_LOG("Failed to upload save state image to GPU");
    962     }
    963   }
    964 }
    965 
    966 void SaveStateSelectorUI::InitializePlaceholderListEntry(ListEntry* li, const std::string& path, s32 slot, bool global)
    967 {
    968   li->summary = TRANSLATE_STR("SaveStateSelectorUI", "No save present in this slot.");
    969   li->slot = slot;
    970   li->global = global;
    971 }
    972 
    973 void SaveStateSelectorUI::Draw()
    974 {
    975   static constexpr float SCROLL_ANIMATION_TIME = 0.25f;
    976   static constexpr float BG_ANIMATION_TIME = 0.15f;
    977 
    978   const auto& io = ImGui::GetIO();
    979   const float scale = ImGuiManager::GetGlobalScale();
    980   const float width = (640.0f * scale);
    981   const float height = (450.0f * scale);
    982 
    983   const float padding_and_rounding = 18.0f * scale;
    984   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, padding_and_rounding);
    985   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding_and_rounding, padding_and_rounding));
    986   ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.15f, 0.17f, 0.8f));
    987   ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
    988   ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), ImGuiCond_Always,
    989                           ImVec2(0.5f, 0.5f));
    990 
    991   if (ImGui::Begin("##save_state_selector", nullptr,
    992                    ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
    993                      ImGuiWindowFlags_NoScrollbar))
    994   {
    995     // Leave 2 lines for the legend
    996     const float legend_margin = ImGui::GetFontSize() * 2.0f + ImGui::GetStyle().ItemSpacing.y * 3.0f;
    997     const float padding = 12.0f * scale;
    998 
    999     ImGui::BeginChild("##item_list", ImVec2(0, -legend_margin), false,
   1000                       ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
   1001                         ImGuiWindowFlags_NoBackground);
   1002     {
   1003       const s32 current_slot = GetCurrentSlot();
   1004       const bool current_slot_global = IsCurrentSlotGlobal();
   1005       const ImVec2 image_size = ImVec2(128.0f * scale, (128.0f / (4.0f / 3.0f)) * scale);
   1006       const float item_width = std::floor(width - (padding_and_rounding * 2.0f) - ImGui::GetStyle().ScrollbarSize);
   1007       const float item_height = std::floor(image_size.y + padding * 2.0f);
   1008       const float text_indent = image_size.x + padding + padding;
   1009 
   1010       for (size_t i = 0; i < s_slots.size(); i++)
   1011       {
   1012         const ListEntry& entry = s_slots[i];
   1013         const float y_start = item_height * static_cast<float>(i);
   1014 
   1015         if (entry.slot == current_slot && entry.global == current_slot_global)
   1016         {
   1017           ImGui::SetCursorPosY(y_start);
   1018 
   1019           const ImVec2 p_start(ImGui::GetCursorScreenPos());
   1020           const ImVec2 p_end(p_start.x + item_width, p_start.y + item_height);
   1021           const ImRect item_rect(p_start, p_end);
   1022           const ImRect& window_rect = ImGui::GetCurrentWindow()->ClipRect;
   1023           if (!window_rect.Contains(item_rect))
   1024           {
   1025             float scroll_target = ImGui::GetScrollY();
   1026             if (item_rect.Min.y < window_rect.Min.y)
   1027               scroll_target = (ImGui::GetScrollY() - (window_rect.Min.y - item_rect.Min.y));
   1028             else if (item_rect.Max.y > window_rect.Max.y)
   1029               scroll_target = (ImGui::GetScrollY() + (item_rect.Max.y - window_rect.Max.y));
   1030 
   1031             if (scroll_target != s_scroll_animated.GetEndValue())
   1032               s_scroll_animated.Start(ImGui::GetScrollY(), scroll_target, SCROLL_ANIMATION_TIME);
   1033           }
   1034 
   1035           if (s_scroll_animated.IsActive())
   1036             ImGui::SetScrollY(s_scroll_animated.UpdateAndGetValue());
   1037 
   1038           if (s_background_animated.GetEndValue() != p_start.y)
   1039             s_background_animated.Start(s_background_animated.UpdateAndGetValue(), p_start.y, BG_ANIMATION_TIME);
   1040 
   1041           ImVec2 highlight_pos;
   1042           if (s_background_animated.IsActive())
   1043             highlight_pos = ImVec2(p_start.x, s_background_animated.UpdateAndGetValue());
   1044           else
   1045             highlight_pos = p_start;
   1046 
   1047           ImGui::GetWindowDrawList()->AddRectFilled(highlight_pos,
   1048                                                     ImVec2(highlight_pos.x + item_width, highlight_pos.y + item_height),
   1049                                                     ImColor(0.22f, 0.30f, 0.34f, 0.9f), padding_and_rounding);
   1050         }
   1051 
   1052         if (GPUTexture* preview_texture =
   1053               entry.preview_texture ? entry.preview_texture.get() : s_placeholder_texture.get())
   1054         {
   1055           ImGui::SetCursorPosY(y_start + padding);
   1056           ImGui::SetCursorPosX(padding);
   1057           ImGui::Image(preview_texture, image_size);
   1058         }
   1059 
   1060         ImGui::SetCursorPosY(y_start + padding);
   1061 
   1062         ImGui::Indent(text_indent);
   1063 
   1064         ImGui::TextUnformatted(TinyString::from_format(entry.global ?
   1065                                                          TRANSLATE_FS("SaveStateSelectorUI", "Global Slot {}") :
   1066                                                          TRANSLATE_FS("SaveStateSelectorUI", "Game Slot {}"),
   1067                                                        entry.slot)
   1068                                  .c_str());
   1069         if (entry.global)
   1070           ImGui::TextUnformatted(entry.game_details.c_str(), entry.game_details.c_str() + entry.game_details.length());
   1071         ImGui::TextUnformatted(entry.summary.c_str(), entry.summary.c_str() + entry.summary.length());
   1072         ImGui::PushFont(ImGuiManager::GetFixedFont());
   1073         ImGui::TextUnformatted(entry.filename.data(), entry.filename.data() + entry.filename.length());
   1074         ImGui::PopFont();
   1075 
   1076         ImGui::Unindent(text_indent);
   1077         ImGui::SetCursorPosY(y_start);
   1078         ImGui::ItemSize(ImVec2(item_width, item_height));
   1079       }
   1080     }
   1081     ImGui::EndChild();
   1082 
   1083     ImGui::BeginChild("##legend", ImVec2(0, 0), false,
   1084                       ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
   1085                         ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground);
   1086     {
   1087       ImGui::SetCursorPosX(padding);
   1088       if (ImGui::BeginTable("table", 2))
   1089       {
   1090         ImGui::TableNextColumn();
   1091         ImGui::TextUnformatted(s_load_legend.c_str());
   1092         ImGui::TableNextColumn();
   1093         ImGui::TextUnformatted(s_prev_legend.c_str());
   1094         ImGui::TableNextColumn();
   1095         ImGui::TextUnformatted(s_save_legend.c_str());
   1096         ImGui::TableNextColumn();
   1097         ImGui::TextUnformatted(s_next_legend.c_str());
   1098 
   1099         ImGui::EndTable();
   1100       }
   1101     }
   1102     ImGui::EndChild();
   1103   }
   1104   ImGui::End();
   1105 
   1106   ImGui::PopStyleVar(2);
   1107   ImGui::PopStyleColor();
   1108 
   1109   // auto-close
   1110   s_open_time += io.DeltaTime;
   1111   if (s_open_time >= s_close_time)
   1112     Close();
   1113 }
   1114 
   1115 s32 SaveStateSelectorUI::GetCurrentSlot()
   1116 {
   1117   return s_current_slot + 1;
   1118 }
   1119 
   1120 bool SaveStateSelectorUI::IsCurrentSlotGlobal()
   1121 {
   1122   return s_current_slot_global;
   1123 }
   1124 
   1125 std::string SaveStateSelectorUI::GetCurrentSlotPath()
   1126 {
   1127   std::string filename;
   1128   if (!s_current_slot_global)
   1129   {
   1130     if (const std::string& serial = System::GetGameSerial(); !serial.empty())
   1131       filename = System::GetGameSaveStateFileName(serial, s_current_slot + 1);
   1132   }
   1133   else
   1134   {
   1135     filename = System::GetGlobalSaveStateFileName(s_current_slot + 1);
   1136   }
   1137 
   1138   return filename;
   1139 }
   1140 
   1141 void SaveStateSelectorUI::LoadCurrentSlot()
   1142 {
   1143   if (std::string path = GetCurrentSlotPath(); !path.empty())
   1144   {
   1145     if (FileSystem::FileExists(path.c_str()))
   1146     {
   1147       Error error;
   1148       if (!System::LoadState(path.c_str(), &error, true))
   1149       {
   1150         Host::AddKeyedOSDMessage("LoadState",
   1151                                  fmt::format(TRANSLATE_FS("OSDMessage", "Failed to load state from slot {0}:\n{1}"),
   1152                                              GetCurrentSlot(), error.GetDescription()),
   1153                                  Host::OSD_ERROR_DURATION);
   1154       }
   1155     }
   1156     else
   1157     {
   1158       Host::AddIconOSDMessage(
   1159         "LoadState", ICON_EMOJI_FLOPPY_DISK,
   1160         IsCurrentSlotGlobal() ?
   1161           fmt::format(TRANSLATE_FS("SaveStateSelectorUI", "No save state found in Global Slot {}."), GetCurrentSlot()) :
   1162           fmt::format(TRANSLATE_FS("SaveStateSelectorUI", "No save state found in Slot {}."), GetCurrentSlot()),
   1163         Host::OSD_INFO_DURATION);
   1164     }
   1165   }
   1166 
   1167   Close();
   1168 }
   1169 
   1170 void SaveStateSelectorUI::SaveCurrentSlot()
   1171 {
   1172   if (std::string path = GetCurrentSlotPath(); !path.empty())
   1173   {
   1174     Error error;
   1175     if (!System::SaveState(path.c_str(), &error, g_settings.create_save_state_backups))
   1176     {
   1177       Host::AddIconOSDMessage("SaveState", ICON_EMOJI_WARNING,
   1178                               fmt::format(TRANSLATE_FS("OSDMessage", "Failed to save state to slot {0}:\n{1}"),
   1179                                           GetCurrentSlot(), error.GetDescription()),
   1180                               Host::OSD_ERROR_DURATION);
   1181     }
   1182   }
   1183 
   1184   Close();
   1185 }
   1186 
   1187 void SaveStateSelectorUI::ShowSlotOSDMessage()
   1188 {
   1189   const std::string path = GetCurrentSlotPath();
   1190   FILESYSTEM_STAT_DATA sd;
   1191   std::string date;
   1192   if (!path.empty() && FileSystem::StatFile(path.c_str(), &sd))
   1193     date = fmt::format(TRANSLATE_FS("SaveStateSelectorUI", DATE_TIME_FORMAT), fmt::localtime(sd.ModificationTime));
   1194   else
   1195     date = TRANSLATE_STR("SaveStateSelectorUI", "no save yet");
   1196 
   1197   Host::AddIconOSDMessage(
   1198     "ShowSlotOSDMessage", ICON_EMOJI_MAGNIFIYING_GLASS_TILTED_LEFT,
   1199     IsCurrentSlotGlobal() ?
   1200       fmt::format(TRANSLATE_FS("SaveStateSelectorUI", "Global Save Slot {0} selected ({1})."), GetCurrentSlot(), date) :
   1201       fmt::format(TRANSLATE_FS("SaveStateSelectorUI", "Save Slot {0} selected ({1})."), GetCurrentSlot(), date),
   1202     Host::OSD_QUICK_DURATION);
   1203 }
   1204 
   1205 void ImGuiManager::RenderOverlayWindows()
   1206 {
   1207   const System::State state = System::GetState();
   1208   if (state != System::State::Shutdown)
   1209   {
   1210     if (SaveStateSelectorUI::s_open)
   1211       SaveStateSelectorUI::Draw();
   1212   }
   1213 }
   1214 
   1215 void ImGuiManager::DestroyOverlayTextures()
   1216 {
   1217   SaveStateSelectorUI::DestroyTextures();
   1218 }