Files
archived-pcsx2/pcsx2/ImGui/ImGuiOverlays.cpp

1493 lines
54 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "BuildVersion.h"
#include "Config.h"
#include "Counters.h"
#include "GS/GS.h"
#include "GS/GSCapture.h"
#include "GS/GSVector.h"
#include "GS/Renderers/Common/GSDevice.h"
#include "GS/Renderers/HW/GSTextureReplacements.h"
#include "Host.h"
#include "IconsFontAwesome.h"
#include "IconsPromptFont.h"
#include "ImGui/FullscreenUI.h"
#include "ImGui/ImGuiAnimated.h"
#include "ImGui/ImGuiFullscreen.h"
#include "ImGui/ImGuiManager.h"
#include "ImGui/ImGuiOverlays.h"
#include "Input/InputManager.h"
#include "MTGS.h"
#include "Patch.h"
#include "PerformanceMetrics.h"
#include "Recording/InputRecording.h"
#include "SIO/Pad/Pad.h"
#include "SIO/Pad/PadBase.h"
#include "USB/USB.h"
#include "VMManager.h"
#include "common/BitUtils.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/Timer.h"
#include "fmt/chrono.h"
#include "fmt/format.h"
#include "imgui.h"
#include <array>
#include <cmath>
#include <limits>
#include <span>
#include <tuple>
InputRecordingUI::InputRecordingData g_InputRecordingData;
// Start timers at 0 so we immediately get lines to cache.
static constexpr double ONE_BILLION = 1000000000;
static constexpr double UPDATE_INTERVAL = 0.1 * ONE_BILLION;
static constexpr double UPDATE_INTERVAL_CPU_INFO = 5.0 * ONE_BILLION;
Common::Timer s_last_update_timer = Common::Timer(0.0);
Common::Timer s_last_update_timer_cpu_info = Common::Timer(0.0);
ImU32 s_speed_line_color;
SmallString s_speed_line;
SmallString s_gs_stats_line;
SmallString s_gs_memory_stats_line;
SmallString s_gs_frame_times_line;
SmallString s_resolution_line;
SmallString s_hardware_info_cpu_line;
SmallString s_hardware_info_gpu_line;
SmallString s_cpu_usage_ee_line;
SmallString s_cpu_usage_gs_line;
SmallString s_cpu_usage_vu_line;
std::vector<SmallString> s_software_thread_lines;
SmallString s_capture_line;
SmallString s_gpu_usage_line;
SmallString s_speed_icon;
constexpr ImU32 white_color = IM_COL32(255, 255, 255, 255);
// OSD positioning funcs
ImVec2 CalculateOSDPosition(OsdOverlayPos position, float margin, const ImVec2& text_size, float window_width, float window_height)
{
switch (position)
{
case OsdOverlayPos::TopLeft:
return ImVec2(margin, margin);
case OsdOverlayPos::TopCenter:
return ImVec2((window_width - text_size.x) * 0.5f, margin);
case OsdOverlayPos::TopRight:
return ImVec2(window_width - margin - text_size.x, margin);
case OsdOverlayPos::CenterLeft:
return ImVec2(margin, (window_height - text_size.y) * 0.5f);
case OsdOverlayPos::Center:
return ImVec2((window_width - text_size.x) * 0.5f, (window_height - text_size.y) * 0.5f);
case OsdOverlayPos::CenterRight:
return ImVec2(window_width - margin - text_size.x, (window_height - text_size.y) * 0.5f);
case OsdOverlayPos::BottomLeft:
return ImVec2(margin, window_height - margin - text_size.y);
case OsdOverlayPos::BottomCenter:
return ImVec2((window_width - text_size.x) * 0.5f, window_height - margin - text_size.y);
case OsdOverlayPos::BottomRight:
return ImVec2(window_width - margin - text_size.x, window_height - margin - text_size.y);
case OsdOverlayPos::None:
default:
return ImVec2(0.0f, 0.0f);
}
}
ImVec2 CalculatePerformanceOverlayTextPosition(OsdOverlayPos position, float margin, const ImVec2& text_size, float window_width, float position_y)
{
const float abs_margin = std::abs(margin);
// Get the X position based on horizontal alignment
float x_pos;
switch (position)
{
case OsdOverlayPos::TopLeft:
case OsdOverlayPos::CenterLeft:
case OsdOverlayPos::BottomLeft:
x_pos = abs_margin; // Left alignment
break;
case OsdOverlayPos::TopCenter:
case OsdOverlayPos::Center:
case OsdOverlayPos::BottomCenter:
x_pos = (window_width - text_size.x) * 0.5f; // Center alignment
break;
case OsdOverlayPos::TopRight:
case OsdOverlayPos::CenterRight:
case OsdOverlayPos::BottomRight:
default:
x_pos = window_width - text_size.x - abs_margin; // Right alignment
break;
}
return ImVec2(x_pos, position_y);
}
bool ShouldUseLeftAlignment(OsdOverlayPos position)
{
return (position == OsdOverlayPos::TopLeft || position == OsdOverlayPos::CenterLeft || position == OsdOverlayPos::BottomLeft);
}
namespace ImGuiManager
{
static void FormatProcessorStat(SmallStringBase& text, double usage, double time);
static void DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawSettingsOverlay(float scale, float margin, float spacing);
static void DrawInputsOverlay(float scale, float margin, float spacing);
static void DrawInputRecordingOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawVideoCaptureOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawTextureReplacementsOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawIndicatorsOverlay(float& position_y, float scale, float margin, float spacing);
} // namespace ImGuiManager
static std::tuple<float, float> GetMinMax(std::span<const float> values)
{
GSVector4 vmin(GSVector4::load<false>(values.data()));
GSVector4 vmax(vmin);
const u32 count = static_cast<u32>(values.size());
const u32 aligned_count = Common::AlignDownPow2(count, 4);
u32 i = 4;
for (; i < aligned_count; i += 4)
{
const GSVector4 v(GSVector4::load<false>(&values[i]));
vmin = vmin.min(v);
vmax = vmax.max(v);
}
float min = std::min(vmin.x, std::min(vmin.y, std::min(vmin.z, vmin.w)));
float max = std::max(vmax.x, std::max(vmax.y, std::max(vmax.z, vmax.w)));
for (; i < count; i++)
{
min = std::min(min, values[i]);
max = std::max(max, values[i]);
}
return std::tie(min, max);
}
__ri void ImGuiManager::FormatProcessorStat(SmallStringBase& text, double usage, double time)
{
// Some values, such as GPU (and even CPU to some extent) can be out of phase with the wall clock,
// which the processor time is divided by to get a utilization percentage. Let's clamp it at 100%,
// so that people don't get confused, and remove the decimal places when it's there while we're at it.
if (usage >= 99.95)
text.append_format("100% ({:.2f}ms)", time);
else
text.append_format("{:.1f}% ({:.2f}ms)", usage, time);
}
__ri void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing)
{
const float shadow_offset = std::ceil(scale);
ImFont* const fixed_font = ImGuiManager::GetFixedFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
ImVec2 text_size;
// Adjust initial Y position based on vertical alignment
switch (GSConfig.OsdPerformancePos)
{
case OsdOverlayPos::CenterLeft:
case OsdOverlayPos::Center:
case OsdOverlayPos::CenterRight:
position_y = (GetWindowHeight() - (font_size * 8.0f)) * 0.5f;
break;
case OsdOverlayPos::BottomLeft:
case OsdOverlayPos::BottomCenter:
case OsdOverlayPos::BottomRight:
position_y = GetWindowHeight() - margin - (font_size * 15.0f + spacing * 14.0f);
break;
case OsdOverlayPos::TopLeft:
case OsdOverlayPos::TopCenter:
case OsdOverlayPos::TopRight:
default:
// Top alignment keeps the passed position_y
break;
}
#define DRAW_LINE(font, size, text, color) \
do \
{ \
text_size = font->CalcTextSizeA(size, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr); \
const ImVec2 text_pos = CalculatePerformanceOverlayTextPosition(GSConfig.OsdPerformancePos, margin, text_size, GetWindowWidth(), position_y); \
dl->AddText(font, size, ImVec2(text_pos.x + shadow_offset, text_pos.y + shadow_offset), IM_COL32(0, 0, 0, 100), (text)); \
dl->AddText(font, size, text_pos, color, (text)); \
position_y += text_size.y + spacing; \
} while (0)
if (VMManager::GetState() != VMState::Paused)
{
if (s_last_update_timer.GetTimeNanoseconds() >= UPDATE_INTERVAL)
{
s_last_update_timer.Reset();
const float speed = PerformanceMetrics::GetSpeed();
s_speed_line.clear();
if (GSConfig.OsdShowFPS)
{
switch (PerformanceMetrics::GetInternalFPSMethod())
{
case PerformanceMetrics::InternalFPSMethod::GSPrivilegedRegister:
s_speed_line.append_format("FPS: {:.2f} [P]", PerformanceMetrics::GetInternalFPS());
break;
case PerformanceMetrics::InternalFPSMethod::DISPFBBlit:
s_speed_line.append_format("FPS: {:.2f} [B]", PerformanceMetrics::GetInternalFPS());
break;
case PerformanceMetrics::InternalFPSMethod::None:
default:
s_speed_line.append("FPS: N/A");
break;
}
}
if (GSConfig.OsdShowVPS)
s_speed_line.append_format("{}VPS: {:.2f}", s_speed_line.empty() ? "" : " | ", PerformanceMetrics::GetFPS());
if (GSConfig.OsdShowSpeed)
{
s_speed_line.append_format("{}Speed: {}%", s_speed_line.empty() ? "" : " | ", static_cast<u32>(std::round(speed)));
const float target_speed = VMManager::GetTargetSpeed();
if (target_speed == 0.0f)
s_speed_line.append(" (T: Max)");
else
s_speed_line.append_format(" (T: {:.0f}%)", target_speed * 100.0f);
}
if (GSConfig.OsdShowVersion)
s_speed_line.append_format("{}PCSX2 {}", s_speed_line.empty() ? "" : " | ", BuildVersion::GitRev);
if (!s_speed_line.empty())
{
if (speed < 95.0f)
s_speed_line_color = IM_COL32(255, 100, 100, 255); // red
else if (speed > 105.0f)
s_speed_line_color = IM_COL32(100, 255, 100, 255); // green
else
s_speed_line_color = white_color;
DRAW_LINE(fixed_font, font_size, s_speed_line.c_str(), s_speed_line_color);
}
if (GSConfig.OsdShowGSStats)
{
GSgetStats(s_gs_stats_line);
GSgetMemoryStats(s_gs_memory_stats_line);
s_gs_frame_times_line.format("{} QF | Min: {:.2f}ms | Avg: {:.2f}ms | Max: {:.2f}ms",
MTGS::GetCurrentVsyncQueueSize() - 1, // subtract one for the current frame
PerformanceMetrics::GetMinimumFrameTime(),
PerformanceMetrics::GetAverageFrameTime(),
PerformanceMetrics::GetMaximumFrameTime());
if (!s_gs_stats_line.empty())
DRAW_LINE(fixed_font, font_size, s_gs_stats_line.c_str(), white_color);
if (!s_gs_memory_stats_line.empty())
DRAW_LINE(fixed_font, font_size, s_gs_memory_stats_line.c_str(), white_color);
DRAW_LINE(fixed_font, font_size, s_gs_frame_times_line.c_str(), white_color);
}
if (GSConfig.OsdShowResolution)
{
int iwidth, iheight;
GSgetInternalResolution(&iwidth, &iheight);
s_resolution_line.format("{}x{} {} {}", iwidth, iheight, ReportVideoMode(), ReportInterlaceMode());
DRAW_LINE(fixed_font, font_size, s_resolution_line.c_str(), white_color);
}
if (GSConfig.OsdShowHardwareInfo)
{
// GPU can change on the fly with settings, but CPU change of any kind is a rare edge case.
if (s_last_update_timer_cpu_info.GetTimeNanoseconds() >= UPDATE_INTERVAL_CPU_INFO)
{
s_last_update_timer_cpu_info.Reset();
// CPU
const CPUInfo& info = GetCPUInfo();
const bool has_small = info.num_small_cores > 0;
const bool has_smt = info.num_threads != info.num_big_cores + info.num_small_cores;
s_hardware_info_cpu_line.format("CPU: {}", info.name);
if (has_smt && has_small)
s_hardware_info_cpu_line.append_format(" ({}P/{}E/{}T)", info.num_big_cores, info.num_small_cores, info.num_threads);
else if (has_small)
s_hardware_info_cpu_line.append_format(" ({}P/{}E)", info.num_big_cores, info.num_small_cores);
else
s_hardware_info_cpu_line.append_format(" ({}C/{}T)", info.num_big_cores, info.num_threads);
}
DRAW_LINE(fixed_font, font_size, s_hardware_info_cpu_line.c_str(), white_color);
// GPU
s_hardware_info_gpu_line.format("GPU: {}{}", g_gs_device->GetName(), GSConfig.UseDebugDevice ? " (Debug)" : "");
DRAW_LINE(fixed_font, font_size, s_hardware_info_gpu_line.c_str(), white_color);
}
if (GSConfig.OsdShowCPU)
{
if (EmuConfig.Speedhacks.EECycleRate != 0 || EmuConfig.Speedhacks.EECycleSkip != 0)
s_cpu_usage_ee_line.format("EE[{}/{}]: ", EmuConfig.Speedhacks.EECycleRate, EmuConfig.Speedhacks.EECycleSkip);
else
s_cpu_usage_ee_line.assign("EE: ");
FormatProcessorStat(s_cpu_usage_ee_line, PerformanceMetrics::GetCPUThreadUsage(), PerformanceMetrics::GetCPUThreadAverageTime());
DRAW_LINE(fixed_font, font_size, s_cpu_usage_ee_line.c_str(), white_color);
s_cpu_usage_gs_line.assign("GS: ");
FormatProcessorStat(s_cpu_usage_gs_line, PerformanceMetrics::GetGSThreadUsage(), PerformanceMetrics::GetGSThreadAverageTime());
DRAW_LINE(fixed_font, font_size, s_cpu_usage_gs_line.c_str(), white_color);
if (THREAD_VU1)
{
s_cpu_usage_vu_line.assign("VU: ");
FormatProcessorStat(s_cpu_usage_vu_line, PerformanceMetrics::GetVUThreadUsage(), PerformanceMetrics::GetVUThreadAverageTime());
DRAW_LINE(fixed_font, font_size, s_cpu_usage_vu_line.c_str(), white_color);
}
const u32 gs_sw_threads = PerformanceMetrics::GetGSSWThreadCount();
for (u32 thread = 0; thread < gs_sw_threads; thread++)
{
if (thread < s_software_thread_lines.size())
s_software_thread_lines[thread].format("SW-{}: ", thread);
else
s_software_thread_lines.push_back(SmallString("SW-{}: ", thread));
FormatProcessorStat(s_software_thread_lines[thread], PerformanceMetrics::GetGSSWThreadUsage(thread), PerformanceMetrics::GetGSSWThreadAverageTime(thread));
DRAW_LINE(fixed_font, font_size, s_software_thread_lines[thread].c_str(), white_color);
}
if (GSCapture::IsCapturing())
{
s_capture_line.assign("CAP: ");
FormatProcessorStat(s_capture_line, PerformanceMetrics::GetCaptureThreadUsage(), PerformanceMetrics::GetCaptureThreadAverageTime());
DRAW_LINE(fixed_font, font_size, s_capture_line.c_str(), white_color);
}
}
if (GSConfig.OsdShowGPU)
{
s_gpu_usage_line.assign("GPU: ");
FormatProcessorStat(s_gpu_usage_line, PerformanceMetrics::GetGPUUsage(), PerformanceMetrics::GetGPUAverageTime());
DRAW_LINE(fixed_font, font_size, s_gpu_usage_line.c_str(), white_color);
}
}
// No refresh yet. Display cached lines.
else
{
if (GSConfig.OsdShowFPS || GSConfig.OsdShowVPS || GSConfig.OsdShowSpeed || GSConfig.OsdShowVersion)
DRAW_LINE(fixed_font, font_size, s_speed_line.c_str(), s_speed_line_color);
if (GSConfig.OsdShowGSStats)
{
if (!s_gs_stats_line.empty())
DRAW_LINE(fixed_font, font_size, s_gs_stats_line.c_str(), white_color);
if (!s_gs_memory_stats_line.empty())
DRAW_LINE(fixed_font, font_size, s_gs_memory_stats_line.c_str(), white_color);
DRAW_LINE(fixed_font, font_size, s_gs_frame_times_line.c_str(), white_color);
}
if (GSConfig.OsdShowResolution)
DRAW_LINE(fixed_font, font_size, s_resolution_line.c_str(), white_color);
if (GSConfig.OsdShowHardwareInfo)
{
DRAW_LINE(fixed_font, font_size, s_hardware_info_cpu_line.c_str(), white_color);
DRAW_LINE(fixed_font, font_size, s_hardware_info_gpu_line.c_str(), white_color);
}
if (GSConfig.OsdShowCPU)
{
DRAW_LINE(fixed_font, font_size, s_cpu_usage_ee_line.c_str(), white_color);
DRAW_LINE(fixed_font, font_size, s_cpu_usage_gs_line.c_str(), white_color);
if (THREAD_VU1)
DRAW_LINE(fixed_font, font_size, s_cpu_usage_vu_line.c_str(), white_color);
const u32 thread_count = std::min(
PerformanceMetrics::GetGSSWThreadCount(),
static_cast<u32>(s_software_thread_lines.size()));
for (u32 thread = 0; thread < thread_count; thread++)
DRAW_LINE(fixed_font, font_size, s_software_thread_lines[thread].c_str(), white_color);
if (GSCapture::IsCapturing())
DRAW_LINE(fixed_font, font_size, s_capture_line.c_str(), white_color);
}
if (GSConfig.OsdShowGPU)
DRAW_LINE(fixed_font, font_size, s_gpu_usage_line.c_str(), white_color);
}
// Check every OSD frame because this is an animation.
if (GSConfig.OsdShowFrameTimes)
{
const ImVec2 history_size(200.0f * scale, 50.0f * scale);
ImGui::SetNextWindowSize(ImVec2(history_size.x, history_size.y));
const ImVec2 window_pos = CalculatePerformanceOverlayTextPosition(GSConfig.OsdPerformancePos, margin, history_size, GetWindowWidth(), position_y);
ImGui::SetNextWindowPos(window_pos);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.25f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushFont(fixed_font, font_size);
if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs))
{
auto [min, max] = GetMinMax(PerformanceMetrics::GetFrameTimeHistory());
// add a little bit of space either side, so we're not constantly resizing
if ((max - min) < 4.0f)
{
min = min - std::fmod(min, 1.0f);
max = max - std::fmod(max, 1.0f) + 1.0f;
min = std::max(min - 2.0f, 0.0f);
max += 2.0f;
}
ImGui::PlotEx(
ImGuiPlotType_Lines, "##frame_times",
[](void*, int idx) -> float {
return PerformanceMetrics::GetFrameTimeHistory()[(
(PerformanceMetrics::GetFrameTimeHistoryPos() + idx) % PerformanceMetrics::NUM_FRAME_TIME_SAMPLES)];
},
nullptr, PerformanceMetrics::NUM_FRAME_TIME_SAMPLES, 0, nullptr, min, max, history_size);
ImDrawList* win_dl = ImGui::GetCurrentWindow()->DrawList;
const ImVec2 wpos(ImGui::GetCurrentWindow()->Pos);
SmallString frame_times_text;
frame_times_text.format("Max: {:.1f} ms", max);
text_size = fixed_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
float text_x;
switch (GSConfig.OsdPerformancePos)
{
case OsdOverlayPos::TopLeft:
case OsdOverlayPos::CenterLeft:
case OsdOverlayPos::BottomLeft:
text_x = wpos.x + 2.0f * spacing; // Left alignment within window
break;
case OsdOverlayPos::TopCenter:
case OsdOverlayPos::Center:
case OsdOverlayPos::BottomCenter:
text_x = wpos.x + (history_size.x - text_size.x) * 0.5f; // Center alignment within window
break;
case OsdOverlayPos::TopRight:
case OsdOverlayPos::CenterRight:
case OsdOverlayPos::BottomRight:
default:
text_x = wpos.x + history_size.x - text_size.x - spacing; // Right alignment within window
break;
}
win_dl->AddText(ImVec2(text_x + shadow_offset, wpos.y + shadow_offset),
IM_COL32(0, 0, 0, 100), frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
win_dl->AddText(ImVec2(text_x, wpos.y),
white_color, frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
frame_times_text.format("Min: {:.1f} ms", min);
text_size = fixed_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
float min_text_x;
switch (GSConfig.OsdPerformancePos)
{
case OsdOverlayPos::TopLeft:
case OsdOverlayPos::CenterLeft:
case OsdOverlayPos::BottomLeft:
min_text_x = wpos.x + 2.0f * spacing; // Left alignment within window
break;
case OsdOverlayPos::TopCenter:
case OsdOverlayPos::Center:
case OsdOverlayPos::BottomCenter:
min_text_x = wpos.x + (history_size.x - text_size.x) * 0.5f; // Center alignment within window
break;
case OsdOverlayPos::TopRight:
case OsdOverlayPos::CenterRight:
case OsdOverlayPos::BottomRight:
default:
min_text_x = wpos.x + history_size.x - text_size.x - spacing; // Right alignment within window
break;
}
win_dl->AddText(ImVec2(min_text_x + shadow_offset, wpos.y + history_size.y - font_size + shadow_offset),
IM_COL32(0, 0, 0, 100), frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
win_dl->AddText(ImVec2(min_text_x, wpos.y + history_size.y - font_size),
white_color, frame_times_text.c_str(), frame_times_text.c_str() + frame_times_text.length());
}
ImGui::End();
ImGui::PopFont();
ImGui::PopStyleVar(5);
ImGui::PopStyleColor(3);
}
}
#undef DRAW_LINE
}
__ri void ImGuiManager::DrawSettingsOverlay(float scale, float margin, float spacing)
{
if (!GSConfig.OsdShowSettings ||
FullscreenUI::HasActiveWindow())
return;
std::string text;
text.reserve(512);
#define APPEND(...) \
do \
{ \
fmt::format_to(std::back_inserter(text), __VA_ARGS__); \
} while (0)
if (Patch::GetAllActivePatchesCount() > 0 && EmuConfig.GS.OsdshowPatches)
APPEND("DB={} P={} C={} | ",
Patch::GetActiveGameDBPatchesCount(),
Patch::GetActivePatchesCount(),
Patch::GetActiveCheatsCount());
if (EmuConfig.Speedhacks.EECycleRate != 0)
APPEND("CR={} ", EmuConfig.Speedhacks.EECycleRate);
if (EmuConfig.Speedhacks.EECycleSkip != 0)
APPEND("CS={} ", EmuConfig.Speedhacks.EECycleSkip);
if (EmuConfig.Speedhacks.fastCDVD)
APPEND("FCDVD ");
if (EmuConfig.Speedhacks.vu1Instant)
APPEND("IVU ");
if (EmuConfig.Speedhacks.vuThread)
APPEND("MTVU ");
if (EmuConfig.GS.VsyncEnable)
APPEND("VSYNC ");
APPEND("EER={} EEC={} VUR={} VUC={} VQS={} ", static_cast<unsigned>(EmuConfig.Cpu.FPUFPCR.GetRoundMode()),
EmuConfig.Cpu.Recompiler.GetEEClampMode(), static_cast<unsigned>(EmuConfig.Cpu.VU0FPCR.GetRoundMode()),
EmuConfig.Cpu.Recompiler.GetVUClampMode(), EmuConfig.GS.VsyncQueueSize);
if (GSIsHardwareRenderer())
{
if ((GSConfig.UpscaleMultiplier - std::floor(GSConfig.UpscaleMultiplier)) > 0.01)
APPEND("IR={:.2f} ", static_cast<float>(GSConfig.UpscaleMultiplier));
else
APPEND("IR={} ", static_cast<unsigned>(GSConfig.UpscaleMultiplier));
APPEND("BL={} TPL={} ", static_cast<unsigned>(GSConfig.AccurateBlendingUnit), static_cast<unsigned>(GSConfig.TexturePreloading));
if (GSConfig.GPUPaletteConversion)
APPEND("PLTX ");
if (GSConfig.HWDownloadMode != GSHardwareDownloadMode::Enabled)
APPEND("HWDM={} ", static_cast<unsigned>(GSConfig.HWDownloadMode));
if (GSConfig.HWMipmap)
APPEND("MM ");
// deliberately test global and print local here for auto values
if (EmuConfig.GS.TextureFiltering != BiFiltering::PS2)
APPEND("BF={} ", static_cast<unsigned>(GSConfig.TextureFiltering));
if (EmuConfig.GS.TriFilter != TriFiltering::Automatic)
APPEND("TF={} ", static_cast<unsigned>(GSConfig.TriFilter));
if (GSConfig.MaxAnisotropy > 1)
APPEND("AF={} ", EmuConfig.GS.MaxAnisotropy);
if (GSConfig.Dithering != 2)
APPEND("DI={} ", GSConfig.Dithering);
if (GSConfig.UserHacks_HalfPixelOffset != GSHalfPixelOffset::Off)
APPEND("HPO={} ", static_cast<u32>(GSConfig.UserHacks_HalfPixelOffset));
if (GSConfig.UserHacks_RoundSprite > 0)
APPEND("RS={} ", GSConfig.UserHacks_RoundSprite);
if (GSConfig.UserHacks_NativeScaling > GSNativeScaling::Off)
APPEND("NS={} ", static_cast<unsigned>(GSConfig.UserHacks_NativeScaling));
if (GSConfig.UserHacks_TCOffsetX != 0 || GSConfig.UserHacks_TCOffsetY != 0)
APPEND("TCO={}/{} ", GSConfig.UserHacks_TCOffsetX, GSConfig.UserHacks_TCOffsetY);
if (GSConfig.UserHacks_CPUSpriteRenderBW != 0)
APPEND("CSBW={}/{} ", GSConfig.UserHacks_CPUSpriteRenderBW, GSConfig.UserHacks_CPUSpriteRenderLevel);
if (GSConfig.UserHacks_CPUCLUTRender != 0)
APPEND("CCLUT={} ", GSConfig.UserHacks_CPUCLUTRender);
if (GSConfig.UserHacks_GPUTargetCLUTMode != GSGPUTargetCLUTMode::Disabled)
APPEND("GCLUT={} ", static_cast<unsigned>(GSConfig.UserHacks_GPUTargetCLUTMode));
if (GSConfig.SkipDrawStart != 0 || GSConfig.SkipDrawEnd != 0)
APPEND("SD={}/{} ", GSConfig.SkipDrawStart, GSConfig.SkipDrawEnd);
if (GSConfig.UserHacks_TextureInsideRt != GSTextureInRtMode::Disabled)
APPEND("TexRT={} ", static_cast<unsigned>(GSConfig.UserHacks_TextureInsideRt));
if (GSConfig.UserHacks_Limit24BitDepth != GSLimit24BitDepth::Disabled)
APPEND("TDR={} ", static_cast<unsigned>(GSConfig.UserHacks_Limit24BitDepth));
if (GSConfig.UserHacks_BilinearHack != GSBilinearDirtyMode::Automatic)
APPEND("BLU={} ", static_cast<unsigned>(GSConfig.UserHacks_BilinearHack));
if (GSConfig.UserHacks_ForceEvenSpritePosition)
APPEND("FESP ");
if (GSConfig.UserHacks_NativePaletteDraw)
APPEND("NPD ");
if (GSConfig.UserHacks_MergePPSprite)
APPEND("MS ");
if (GSConfig.UserHacks_AlignSpriteX)
APPEND("AS ");
if (GSConfig.UserHacks_AutoFlush != GSHWAutoFlushLevel::Disabled)
APPEND("ATFL={} ", static_cast<unsigned>(GSConfig.UserHacks_AutoFlush));
if (GSConfig.UserHacks_CPUFBConversion)
APPEND("FBC ");
if (GSConfig.UserHacks_ReadTCOnClose)
APPEND("RTOC ");
if (GSConfig.UserHacks_DisableDepthSupport)
APPEND("DDC ");
if (GSConfig.UserHacks_DisablePartialInvalidation)
APPEND("DPIV ");
if (GSConfig.UserHacks_DisableSafeFeatures)
APPEND("DSF ");
if (GSConfig.UserHacks_DisableRenderFixes)
APPEND("DRF ");
if (GSConfig.PreloadFrameWithGSData)
APPEND("PLFD ");
if (GSConfig.UserHacks_EstimateTextureRegion)
APPEND("ETR ");
if (GSConfig.HWSpinGPUForReadbacks)
APPEND("RBSG ");
if (GSConfig.HWSpinCPUForReadbacks)
APPEND("RBSC ");
}
#undef APPEND
if (text.empty())
return;
else if (text.back() == ' ')
text.pop_back();
const float shadow_offset = std::ceil(scale);
ImFont* const font = ImGuiManager::GetFixedFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
const float position_y = GetWindowHeight() - margin - font_size;
ImDrawList* dl = ImGui::GetBackgroundDrawList();
ImVec2 text_size =
font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), -1.0f, text.c_str(), text.c_str() + text.length(), nullptr);
dl->AddText(font, font_size,
ImVec2(GetWindowWidth() - margin - text_size.x + shadow_offset, position_y + shadow_offset), IM_COL32(0, 0, 0, 100),
text.c_str(), text.c_str() + text.length());
dl->AddText(font, font_size, ImVec2(GetWindowWidth() - margin - text_size.x, position_y), white_color,
text.c_str(), text.c_str() + text.length());
}
__ri void ImGuiManager::DrawInputsOverlay(float scale, float margin, float spacing)
{
// Technically this is racing the CPU thread.. but it doesn't really matter, at worst, the inputs get displayed onscreen late.
if (!GSConfig.OsdShowInputs ||
FullscreenUI::HasActiveWindow())
return;
const float shadow_offset = std::ceil(scale);
ImFont* const font = ImGuiManager::GetStandardFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
static constexpr u32 text_color = IM_COL32(0xff, 0xff, 0xff, 255);
static constexpr u32 shadow_color = IM_COL32(0x00, 0x00, 0x00, 100);
const ImVec2& display_size = ImGui::GetIO().DisplaySize;
ImDrawList* dl = ImGui::GetBackgroundDrawList();
u32 num_ports = 0;
for (u32 slot = 0; slot < Pad::NUM_CONTROLLER_PORTS; slot++)
{
if (Pad::HasConnectedPad(slot))
num_ports++;
}
for (u32 port = 0; port < USB::NUM_PORTS; port++)
{
if (EmuConfig.USB.Ports[port].DeviceType >= 0)
num_ports++;
}
float current_x = ImFloor(margin);
float current_y = ImFloor(display_size.y - margin - ((static_cast<float>(num_ports) * (font_size + spacing)) - spacing));
const ImVec4 clip_rect(current_x, current_y, display_size.x - margin, display_size.y);
SmallString text;
for (u32 slot = 0; slot < Pad::NUM_CONTROLLER_PORTS; slot++)
{
const PadBase* const pad = Pad::GetPad(slot);
const Pad::ControllerType ctype = pad->GetType();
if (ctype == Pad::ControllerType::NotConnected)
continue;
const Pad::ControllerInfo& cinfo = pad->GetInfo();
text.format("{} {} • {} |", ICON_FA_GAMEPAD, slot + 1u, cinfo.icon_name ? cinfo.icon_name : ICON_FA_TRIANGLE_EXCLAMATION);
for (u32 bind = 0; bind < static_cast<u32>(cinfo.bindings.size()); bind++)
{
const InputBindingInfo& bi = cinfo.bindings[bind];
switch (bi.bind_type)
{
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
{
// axes are only shown if not resting/past deadzone. values are normalized.
const float value = pad->GetEffectiveInput(bind);
const float abs_value = std::abs(value);
if (abs_value >= (254.0f / 255.0f))
text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
else if (abs_value >= (1.0f / 255.0f))
text.append_format(" {}: {:.2f}", bi.icon_name ? bi.icon_name : bi.name, value);
}
break;
case InputBindingInfo::Type::Button:
{
// buttons display the value from 0 through 255.
const float value = pad->GetEffectiveInput(bind);
if (value >= 254.0f)
text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
else if (value > 0.0f)
text.append_format(" {}: {:.0f}", bi.icon_name ? bi.icon_name : bi.name, value);
}
break;
case InputBindingInfo::Type::Motor:
case InputBindingInfo::Type::Macro:
case InputBindingInfo::Type::Unknown:
default:
break;
}
}
dl->AddText(font, font_size, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color, text.c_str(),
text.c_str() + text.length(), 0.0f, &clip_rect);
dl->AddText(
font, font_size, ImVec2(current_x, current_y), text_color, text.c_str(), text.c_str() + text.length(), 0.0f, &clip_rect);
current_y += font_size + spacing;
}
for (u32 port = 0; port < USB::NUM_PORTS; port++)
{
if (EmuConfig.USB.Ports[port].DeviceType < 0)
continue;
const std::span<const InputBindingInfo> bindings(USB::GetDeviceBindings(port));
const char* icon = USB::GetDeviceIconName(port);
text.format("{} {} • {} | ", ICON_PF_USB, port + 1u, icon ? icon : ICON_FA_TRIANGLE_EXCLAMATION);
for (const InputBindingInfo& bi : bindings)
{
switch (bi.bind_type)
{
case InputBindingInfo::Type::Axis:
case InputBindingInfo::Type::HalfAxis:
{
// axes are only shown if not resting/past deadzone. values are normalized.
const float value = static_cast<float>(USB::GetDeviceBindValue(port, bi.bind_index));
if (value >= (254.0f / 255.0f))
text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
else if (value > (1.0f / 255.0f))
text.append_format(" {}: {:.2f}", bi.icon_name ? bi.icon_name : bi.name, value);
}
break;
case InputBindingInfo::Type::Button:
{
// buttons display the value from 0 through 255. values are normalized, so denormalize them.
const float value = static_cast<float>(USB::GetDeviceBindValue(port, bi.bind_index)) * 255.0f;
if (value >= 254.0f)
text.append_format(" {}", bi.icon_name ? bi.icon_name : bi.name);
else if (value > 0.0f)
text.append_format(" {}: {:.0f}", bi.icon_name ? bi.icon_name : bi.name, value);
}
break;
case InputBindingInfo::Type::Motor:
case InputBindingInfo::Type::Macro:
case InputBindingInfo::Type::Unknown:
default:
break;
}
}
dl->AddText(font, font_size, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color, text.c_str(),
text.c_str() + text.length(), 0.0f, &clip_rect);
dl->AddText(
font, font_size, ImVec2(current_x, current_y), text_color, text.c_str(), text.c_str() + text.length(), 0.0f, &clip_rect);
current_y += font_size + spacing;
}
}
__ri void ImGuiManager::DrawInputRecordingOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!GSConfig.OsdShowInputRec ||
!g_InputRecording.isActive() ||
FullscreenUI::HasActiveWindow())
return;
const float shadow_offset = std::ceil(scale);
ImFont* const fixed_font = ImGuiManager::GetFixedFont();
ImFont* const standard_font = ImGuiManager::GetStandardFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
std::string text;
ImVec2 text_size;
text.reserve(128);
#define DRAW_LINE(font, size, text, color) \
do \
{ \
text_size = font->CalcTextSizeA(size, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr); \
dl->AddText(font, size, \
ImVec2(GetWindowWidth() - margin - text_size.x + shadow_offset, position_y + shadow_offset), \
IM_COL32(0, 0, 0, 100), (text)); \
dl->AddText(font, size, ImVec2(GetWindowWidth() - margin - text_size.x, position_y), color, (text)); \
position_y += text_size.y + spacing; \
} while (0)
// Status Indicators
if (g_InputRecordingData.is_recording)
{
DRAW_LINE(standard_font, font_size, TinyString::from_format(TRANSLATE_FS("ImGuiOverlays", "{} Recording Input"), ICON_PF_CIRCLE).c_str(), IM_COL32(255, 0, 0, 255));
}
else
{
DRAW_LINE(standard_font, font_size, TinyString::from_format(TRANSLATE_FS("ImGuiOverlays", "{} Replaying"), ICON_FA_PLAY).c_str(), IM_COL32(97, 240, 84, 255));
}
// Input Recording Metadata
DRAW_LINE(fixed_font, font_size, g_InputRecordingData.recording_active_message.c_str(), IM_COL32(117, 255, 241, 255));
DRAW_LINE(fixed_font, font_size, g_InputRecordingData.frame_data_message.c_str(), IM_COL32(117, 255, 241, 255));
DRAW_LINE(fixed_font, font_size, g_InputRecordingData.undo_count_message.c_str(), IM_COL32(117, 255, 241, 255));
#undef DRAW_LINE
}
__ri void ImGuiManager::DrawVideoCaptureOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!GSConfig.OsdShowVideoCapture ||
!GSCapture::IsCapturing() ||
FullscreenUI::HasActiveWindow())
return;
const float shadow_offset = std::ceil(scale);
ImFont* const standard_font = ImGuiManager::GetStandardFont();
float font_size = ImGuiManager::GetFontSizeStandard();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
static constexpr const char* ICON = ICON_PF_CIRCLE;
const TinyString text_msg = TinyString::from_format(" {}", GSCapture::GetElapsedTime());
const ImVec2 icon_size = standard_font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
-1.0f, ICON, nullptr, nullptr);
const ImVec2 text_size = standard_font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
-1.0f, text_msg.c_str(), text_msg.end_ptr(), nullptr);
// Shadow
dl->AddText(standard_font, font_size,
ImVec2(GetWindowWidth() - margin - text_size.x - icon_size.x + shadow_offset, position_y + shadow_offset),
IM_COL32(0, 0, 0, 100), ICON);
dl->AddText(standard_font, font_size,
ImVec2(GetWindowWidth() - margin - text_size.x + shadow_offset, position_y + shadow_offset),
IM_COL32(0, 0, 0, 100), text_msg.c_str(), text_msg.end_ptr());
// Text
dl->AddText(standard_font, font_size,
ImVec2(GetWindowWidth() - margin - text_size.x - icon_size.x, position_y), IM_COL32(255, 0, 0, 255), ICON);
dl->AddText(standard_font, font_size,
ImVec2(GetWindowWidth() - margin - text_size.x, position_y), white_color, text_msg.c_str(),
text_msg.end_ptr());
position_y += std::max(icon_size.y, text_size.y) + spacing;
}
__ri void ImGuiManager::DrawTextureReplacementsOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!GSConfig.OsdShowTextureReplacements ||
FullscreenUI::HasActiveWindow())
return;
const bool dumping_active = GSConfig.DumpReplaceableTextures;
const bool replacement_active = GSConfig.LoadTextureReplacements;
if (!dumping_active && !replacement_active)
return;
const float shadow_offset = std::ceil(scale);
ImFont* const standard_font = ImGuiManager::GetStandardFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
SmallString texture_line;
if (replacement_active)
{
const u32 loaded_count = GSTextureReplacements::GetLoadedTextureCount();
texture_line.format("{} Replaced: {}", ICON_FA_IMAGES, loaded_count);
}
if (dumping_active)
{
if (!texture_line.empty())
texture_line.append(" | ");
const u32 dumped_count = GSTextureReplacements::GetDumpedTextureCount();
texture_line.append_format("{} Dumped: {}", ICON_FA_DOWNLOAD, dumped_count);
}
ImVec2 text_size = standard_font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), -1.0f, texture_line.c_str(), nullptr, nullptr);
const ImVec2 text_pos(GetWindowWidth() - margin - text_size.x, position_y);
dl->AddText(standard_font, font_size, ImVec2(text_pos.x + shadow_offset, text_pos.y + shadow_offset), IM_COL32(0, 0, 0, 100), texture_line.c_str());
dl->AddText(standard_font, font_size, text_pos, white_color, texture_line.c_str());
position_y += text_size.y + spacing;
}
__ri void ImGuiManager::DrawIndicatorsOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!GSConfig.OsdShowIndicators ||
FullscreenUI::HasActiveWindow())
return;
const float shadow_offset = std::ceil(scale);
ImFont* const standard_font = ImGuiManager::GetStandardFont();
const float font_size = ImGuiManager::GetFontSizeStandard();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
std::string text;
ImVec2 text_size;
text.reserve(64);
#define DRAW_LINE(font, size, text, color) \
do \
{ \
text_size = font->CalcTextSizeA(size, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr); \
dl->AddText(font, size, \
ImVec2(GetWindowWidth() - margin - text_size.x + shadow_offset, position_y + shadow_offset), \
IM_COL32(0, 0, 0, 100), (text)); \
dl->AddText(font, size, ImVec2(GetWindowWidth() - margin - text_size.x, position_y), color, (text)); \
position_y += text_size.y + spacing; \
} while (0)
if (VMManager::GetState() != VMState::Paused)
{
// Draw Speed indicator
const float target_speed = VMManager::GetTargetSpeed();
const bool is_normal_speed = (target_speed == EmuConfig.EmulationSpeed.NominalScalar ||
VMManager::IsTargetSpeedAdjustedToHost());
if (!is_normal_speed)
{
if (target_speed == EmuConfig.EmulationSpeed.SlomoScalar) // Slow-Motion
s_speed_icon = ICON_PF_SLOW_MOTION;
else if (target_speed == EmuConfig.EmulationSpeed.TurboScalar) // Turbo
s_speed_icon = ICON_FA_FORWARD_FAST;
else // Unlimited
s_speed_icon = ICON_FA_FORWARD;
DRAW_LINE(standard_font, font_size, s_speed_icon, white_color);
}
}
else
{
// Draw Pause indicator
const TinyString pause_msg = TinyString::from_format(TRANSLATE_FS("ImGuiOverlays", "{} Paused"), ICON_FA_PAUSE);
DRAW_LINE(standard_font, font_size, pause_msg, white_color);
}
#undef DRAW_LINE
}
namespace SaveStateSelectorUI
{
namespace
{
struct ListEntry
{
std::string title;
std::string summary;
std::string filename;
std::unique_ptr<GSTexture> preview_texture;
};
} // namespace
static void InitializePlaceholderListEntry(ListEntry* li, std::string path, s32 slot);
static void InitializeListEntry(const std::string& serial, u32 crc, ListEntry* li, s32 slot);
static void RefreshHotkeyLegend();
static void Draw();
static void ShowSlotOSDMessage();
static std::string GetSaveStateTimestampSummary(const std::time_t& modification_time);
bool IsOpen();
static constexpr const char* SAVED_AGO_DAYS_TIME_DATE =
TRANSLATE_NOOP("ImGuiOverlays", "Saved {0} days ago at {1:%H:%M} on {1:%a} {1:%Y/%m/%d}");
static constexpr const char* SAVED_FUTURE_TIME_DATE =
TRANSLATE_NOOP("ImGuiOverlays", "Saved in the future at {0:%H:%M} on {0:%a} {0:%Y/%m/%d}");
static constexpr const char* SAVED_AGO_HOURS_MINUTES =
TRANSLATE_NOOP("ImGuiOverlays", "Saved {0} hours, {1} minutes ago at {2:%H:%M}");
static constexpr const char* SAVED_AGO_MINUTES = TRANSLATE_NOOP("ImGuiOverlays", "Saved {0} minutes ago at {1:%H:%M}");
static constexpr const char* SAVED_AGO_SECONDS = TRANSLATE_NOOP("ImGuiOverlays", "Saved {} seconds ago");
static constexpr const char* SAVED_AGO_NOW = TRANSLATE_NOOP("ImGuiOverlays", "Saved just now");
static constexpr std::time_t ONE_HOUR = 60 * 60; // 3600
static constexpr std::time_t TWENTY_FOUR_HOURS = ONE_HOUR * 24; // 86400
static std::shared_ptr<GSTexture> s_placeholder_texture;
static std::string s_load_legend;
static std::string s_save_legend;
static std::string s_prev_legend;
static std::string s_next_legend;
static std::string s_close_legend;
static std::array<ListEntry, VMManager::NUM_SAVE_STATE_SLOTS> s_slots;
static std::atomic_int32_t s_current_slot{0};
static float s_open_time = 0.0f;
static float s_close_time = 0.0f;
static ImAnimatedFloat s_scroll_animated;
static ImAnimatedFloat s_background_animated;
static bool s_open = false;
} // namespace SaveStateSelectorUI
void SaveStateSelectorUI::Open(float open_time /* = DEFAULT_OPEN_TIME */)
{
const std::string serial = VMManager::GetDiscSerial();
if (serial.empty())
{
Host::AddIconOSDMessage("SaveStateSelectorUIUnavailable", ICON_PF_MEMORY_CARD,
TRANSLATE_SV("ImGuiOverlays", "Save state selector is unavailable without a valid game serial."));
return;
}
s_open_time = 0.0f;
s_close_time = open_time;
if (s_open)
return;
if (!s_placeholder_texture)
s_placeholder_texture = ImGuiFullscreen::LoadTexture("fullscreenui/no-save.png");
s_scroll_animated.Reset(0.0f);
s_background_animated.Reset(0.0f);
s_open = true;
RefreshList(serial, VMManager::GetDiscCRC());
RefreshHotkeyLegend();
}
bool SaveStateSelectorUI::IsOpen()
{
return s_open;
}
void SaveStateSelectorUI::Close()
{
s_open = false;
s_load_legend = {};
s_save_legend = {};
s_prev_legend = {};
s_next_legend = {};
s_close_legend = {};
}
void SaveStateSelectorUI::RefreshList(const std::string& serial, u32 crc)
{
for (ListEntry& entry : s_slots)
{
if (entry.preview_texture)
g_gs_device->Recycle(entry.preview_texture.release());
}
for (u32 i = 0; i < VMManager::NUM_SAVE_STATE_SLOTS; i++)
InitializeListEntry(serial, crc, &s_slots[i], static_cast<s32>(i + 1));
}
void SaveStateSelectorUI::Clear()
{
// called on CPU thread at shutdown, textures should already be deleted, unless running
// big picture UI, in which case we have to delete them here...
for (ListEntry& li : s_slots)
{
if (li.preview_texture)
{
MTGS::RunOnGSThread([tex = li.preview_texture.release()]() {
g_gs_device->Recycle(tex);
});
}
li = {};
}
s_current_slot.store(0, std::memory_order_release);
}
void SaveStateSelectorUI::DestroyTextures()
{
Close();
for (ListEntry& entry : s_slots)
{
if (entry.preview_texture)
g_gs_device->Recycle(entry.preview_texture.release());
}
s_placeholder_texture.reset();
}
void SaveStateSelectorUI::RefreshHotkeyLegend()
{
auto format_legend_entry = [](SmallString binding, std::string_view caption) {
InputManager::PrettifyInputBinding(binding);
if (binding.empty())
binding.append(TRANSLATE_STR("ImGuiOverlays", "Empty"));
return fmt::format("{} - {}", binding, caption);
};
s_load_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "LoadStateFromSlot"),
TRANSLATE_STR("ImGuiOverlays", "Load"));
s_save_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "SaveStateToSlot"),
TRANSLATE_STR("ImGuiOverlays", "Save"));
s_prev_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "PreviousSaveStateSlot"),
TRANSLATE_STR("ImGuiOverlays", "Select Previous"));
s_next_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "NextSaveStateSlot"),
TRANSLATE_STR("ImGuiOverlays", "Select Next"));
s_close_legend = format_legend_entry(Host::GetSmallStringSettingValue("Hotkeys", "OpenPauseMenu"),
TRANSLATE_STR("ImGuiOverlays", "Close Menu"));
}
void SaveStateSelectorUI::SelectNextSlot(bool open_selector)
{
const s32 current_slot = s_current_slot.load(std::memory_order_acquire);
s_current_slot.store((current_slot == (VMManager::NUM_SAVE_STATE_SLOTS - 1)) ? 0 : (current_slot + 1), std::memory_order_release);
if (open_selector)
{
MTGS::RunOnGSThread([]() {
if (!s_open)
Open();
s_open_time = 0.0f;
});
}
else
{
ShowSlotOSDMessage();
}
}
void SaveStateSelectorUI::SelectPreviousSlot(bool open_selector)
{
const s32 current_slot = s_current_slot.load(std::memory_order_acquire);
s_current_slot.store((current_slot == 0) ? (VMManager::NUM_SAVE_STATE_SLOTS - 1) : (current_slot - 1), std::memory_order_release);
if (open_selector)
{
MTGS::RunOnGSThread([]() {
if (!s_open)
Open();
s_open_time = 0.0f;
});
}
else
{
ShowSlotOSDMessage();
}
}
void SaveStateSelectorUI::InitializeListEntry(const std::string& serial, u32 crc, ListEntry* li, s32 slot)
{
std::string path = VMManager::GetSaveStateFileName(serial.c_str(), crc, slot);
FILESYSTEM_STAT_DATA sd;
if (!FileSystem::StatFile(path.c_str(), &sd))
{
InitializePlaceholderListEntry(li, std::move(path), slot);
return;
}
li->title = fmt::format(TRANSLATE_FS("ImGuiOverlays", "Save Slot {0}"), slot);
li->summary = GetSaveStateTimestampSummary(sd.ModificationTime);
li->filename = Path::GetFileName(path);
u32 screenshot_width, screenshot_height;
std::vector<u32> screenshot_pixels;
if (SaveState_ReadScreenshot(path, &screenshot_width, &screenshot_height, &screenshot_pixels))
{
li->preview_texture =
std::unique_ptr<GSTexture>(g_gs_device->CreateTexture(screenshot_width, screenshot_height, 1, GSTexture::Format::Color));
if (!li->preview_texture || !li->preview_texture->Update(GSVector4i(0, 0, screenshot_width, screenshot_height),
screenshot_pixels.data(), sizeof(u32) * screenshot_width))
{
Console.Error("Failed to upload save state image to GPU");
if (li->preview_texture)
g_gs_device->Recycle(li->preview_texture.release());
}
}
}
void SaveStateSelectorUI::InitializePlaceholderListEntry(ListEntry* li, std::string path, s32 slot)
{
li->title = fmt::format(TRANSLATE_FS("ImGuiOverlays", "Save Slot {0}"), slot);
li->summary = TRANSLATE_STR("ImGuiOverlays", "No save present in this slot");
li->filename = Path::GetFileName(path);
}
void SaveStateSelectorUI::Draw()
{
static constexpr float SCROLL_ANIMATION_TIME = 0.25f;
static constexpr float BG_ANIMATION_TIME = 0.15f;
const auto& io = ImGui::GetIO();
const float scale = ImGuiManager::GetGlobalScale();
const float width = (600.0f * scale);
const float height = (430.0f * scale);
const float padding_and_rounding = 10.0f * scale;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, padding_and_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding_and_rounding, padding_and_rounding));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.15f, 0.17f, 0.8f));
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), ImGuiCond_Always,
ImVec2(0.5f, 0.5f));
if (ImGui::Begin("##save_state_selector", nullptr,
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar))
{
// Leave 2 lines for the legend
const float legend_margin = ImGui::GetFontSize() * 3.0f + ImGui::GetStyle().ItemSpacing.y * 3.0f;
const float padding = 10.0f * scale;
ImGui::BeginChild("##item_list", ImVec2(0, -legend_margin), false,
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBackground);
{
const s32 current_slot = s_current_slot.load(std::memory_order_acquire);
const ImVec2 image_size = ImVec2(128.0f * scale, (128.0f / (4.0f / 3.0f)) * scale);
const float item_width = std::floor(width - (padding_and_rounding * 2.0f) - ImGui::GetStyle().ScrollbarSize);
const float item_height = std::floor(image_size.y + padding * 2.0f);
const float text_indent = image_size.x + padding + padding;
for (size_t i = 0; i < s_slots.size(); i++)
{
const ListEntry& entry = s_slots[i];
const float y_start = item_height * static_cast<float>(i);
if (i == static_cast<size_t>(current_slot))
{
ImGui::SetCursorPosY(y_start);
const ImVec2 p_start(ImGui::GetCursorScreenPos());
const ImVec2 p_end(p_start.x + item_width, p_start.y + item_height);
const ImRect item_rect(p_start, p_end);
const ImRect& window_rect = ImGui::GetCurrentWindow()->ClipRect;
if (!window_rect.Contains(item_rect))
{
float scroll_target = ImGui::GetScrollY();
if (item_rect.Min.y < window_rect.Min.y)
scroll_target = (ImGui::GetScrollY() - (window_rect.Min.y - item_rect.Min.y));
else if (item_rect.Max.y > window_rect.Max.y)
scroll_target = (ImGui::GetScrollY() + (item_rect.Max.y - window_rect.Max.y));
if (scroll_target != s_scroll_animated.GetEndValue())
s_scroll_animated.Start(ImGui::GetScrollY(), scroll_target, SCROLL_ANIMATION_TIME);
}
if (s_scroll_animated.IsActive())
ImGui::SetScrollY(s_scroll_animated.UpdateAndGetValue());
if (s_background_animated.GetEndValue() != p_start.y)
s_background_animated.Start(s_background_animated.UpdateAndGetValue(), p_start.y, BG_ANIMATION_TIME);
ImVec2 highlight_pos;
if (s_background_animated.IsActive())
highlight_pos = ImVec2(p_start.x, s_background_animated.UpdateAndGetValue());
else
highlight_pos = p_start;
ImGui::GetWindowDrawList()->AddRectFilled(highlight_pos,
ImVec2(highlight_pos.x + item_width, highlight_pos.y + item_height),
ImColor(0.22f, 0.30f, 0.34f, 0.9f), padding_and_rounding);
}
if (GSTexture* preview_texture = entry.preview_texture ? entry.preview_texture.get() : s_placeholder_texture.get())
{
ImGui::SetCursorPosY(y_start + padding);
ImGui::SetCursorPosX(padding);
ImGui::Image(reinterpret_cast<ImTextureID>(preview_texture->GetNativeHandle()), image_size);
}
ImGui::SetCursorPosY(y_start + padding);
ImGui::Indent(text_indent);
ImGui::TextUnformatted(entry.title.c_str(), entry.title.c_str() + entry.title.length());
ImGui::TextUnformatted(entry.summary.c_str(), entry.summary.c_str() + entry.summary.length());
ImGui::PushFont(ImGuiManager::GetFixedFont(), ImGuiManager::GetFontSizeStandard());
ImGui::TextUnformatted(entry.filename.c_str(), entry.filename.c_str() + entry.filename.length());
ImGui::PopFont();
ImGui::Unindent(text_indent);
ImGui::SetCursorPosY(y_start);
ImGui::ItemSize(ImVec2(item_width, item_height));
}
}
ImGui::EndChild();
ImGui::BeginChild("##legend", ImVec2(0, 0), false,
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground);
{
ImGui::SetCursorPosX(padding);
if (ImGui::BeginTable("table", 2))
{
ImGui::TableNextColumn();
ImGui::TextUnformatted(s_load_legend.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(s_prev_legend.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(s_save_legend.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(s_next_legend.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(s_close_legend.c_str());
ImGui::EndTable();
}
}
ImGui::EndChild();
}
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
// auto-close
s_open_time += io.DeltaTime;
if (s_open_time >= s_close_time)
Close();
}
s32 SaveStateSelectorUI::GetCurrentSlot()
{
return s_current_slot.load(std::memory_order_acquire) + 1;
}
void SaveStateSelectorUI::LoadCurrentSlot()
{
Host::RunOnCPUThread([slot = GetCurrentSlot()]() {
Error error;
if (!VMManager::LoadStateFromSlot(slot, false, &error))
FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false);
});
Close();
}
void SaveStateSelectorUI::LoadCurrentBackupSlot()
{
Host::RunOnCPUThread([slot = GetCurrentSlot()]() {
Error error;
if (!VMManager::LoadStateFromSlot(slot, true, &error))
FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, true);
});
Close();
}
void SaveStateSelectorUI::SaveCurrentSlot()
{
Host::RunOnCPUThread([slot = GetCurrentSlot()]() {
VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) {
FullscreenUI::ReportStateSaveError(error, slot);
});
});
Close();
}
void SaveStateSelectorUI::ShowSlotOSDMessage()
{
const s32 slot = GetCurrentSlot();
const u32 crc = VMManager::GetDiscCRC();
const std::string serial = VMManager::GetDiscSerial();
const std::string filename = VMManager::GetSaveStateFileName(serial.c_str(), crc, slot);
FILESYSTEM_STAT_DATA sd;
std::string timestamp_summary;
if (!filename.empty() && FileSystem::StatFile(filename.c_str(), &sd))
timestamp_summary = GetSaveStateTimestampSummary(sd.ModificationTime);
else
timestamp_summary = TRANSLATE_STR("ImGuiOverlays", "no save yet");
Host::AddIconOSDMessage("ShowSlotOSDMessage", ICON_FA_MAGNIFYING_GLASS,
fmt::format(TRANSLATE_FS("Hotkeys", "Save slot {0} selected ({1})."), slot, timestamp_summary),
Host::OSD_QUICK_DURATION);
}
void ImGuiManager::RenderOverlays()
{
const float scale = ImGuiManager::GetGlobalScale();
const float margin = std::ceil(10.0f * scale);
const float spacing = std::ceil(5.0f * scale);
float position_y = margin;
DrawIndicatorsOverlay(position_y, scale, margin, spacing);
DrawVideoCaptureOverlay(position_y, scale, margin, spacing);
DrawInputRecordingOverlay(position_y, scale, margin, spacing);
DrawTextureReplacementsOverlay(position_y, scale, margin, spacing);
if (GSConfig.OsdPerformancePos != OsdOverlayPos::None)
DrawPerformanceOverlay(position_y, scale, margin, spacing);
DrawSettingsOverlay(scale, margin, spacing);
DrawInputsOverlay(scale, margin, spacing);
if (SaveStateSelectorUI::s_open)
SaveStateSelectorUI::Draw();
}
std::string SaveStateSelectorUI::GetSaveStateTimestampSummary(const std::time_t& modification_time)
{
std::tm tm_modification_local = {};
#ifdef _MSC_VER
localtime_s(&tm_modification_local, &modification_time);
#else
localtime_r(&modification_time, &tm_modification_local);
#endif
const std::time_t current_time = std::time(nullptr);
const std::time_t time_since_save = current_time - std::mktime(&tm_modification_local);
if (time_since_save >= TWENTY_FOUR_HOURS)
{
return fmt::format(TRANSLATE_FS("ImGuiOverlays", SAVED_AGO_DAYS_TIME_DATE),
time_since_save / TWENTY_FOUR_HOURS, tm_modification_local);
}
else if (time_since_save >= ONE_HOUR)
{
return fmt::format(TRANSLATE_FS("ImGuiOverlays", SAVED_AGO_HOURS_MINUTES),
time_since_save / ONE_HOUR, (time_since_save / 60) % 60, tm_modification_local);
}
else if (time_since_save >= 60)
{
return fmt::format(TRANSLATE_FS("ImGuiOverlays", SAVED_AGO_MINUTES),
time_since_save / 60, tm_modification_local);
}
else if (time_since_save >= 5)
{
return fmt::format(TRANSLATE_FS("ImGuiOverlays", SAVED_AGO_SECONDS),
time_since_save);
}
else if (time_since_save >= 0)
{
return TRANSLATE_STR("ImGuiOverlays", SAVED_AGO_NOW);
}
else
{
return fmt::format(TRANSLATE_FS("ImGuiOverlays", SAVED_FUTURE_TIME_DATE),
tm_modification_local);
}
}