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 }