mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Under GSBackThreadMode >= Lockstep roughly half the GS work moves to a second thread, and every surface that reports GS cost -- OSD, PerfLog, the Qt status bar, PINE stats, gsrunner's @HWSTAT@ block -- measured the MTGS thread alone. So the split read as a large GS saving. It is not: on a Rogue Galaxy savestate here, mode 0 costs 15.8% / 2.63 ms and mode 3 costs 17.0% / 2.84 ms plus 14.2% / 2.37 ms on the back thread -- about twice the total GS CPU time, bought to halve the critical path. That is a real trade, but nobody could see it. The back thread registers its own handle at entry, as the SW rasterizer workers do; StopBackThread clears it after the join. Unlike every other handle here it is written by a thread other than the one sampling it, so the handle and its running total sit behind a mutex taken twice a second. Installing a handle rebases the total off it, so the first window after a GSreopen respawn measures the new thread rather than its difference against the retired one's. The figure is omitted, not reported as zero, wherever a back thread does not exist -- otherwise a mode 0 vs mode 3 comparison reads a permanent 0% as meaningful. gsrunner latches the presence flag during the run because DumpStats executes after VMManager::Shutdown, by which point the thread has joined.
2162 lines
81 KiB
C++
2162 lines
81 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/GSShaderCompileIndicator.h"
|
|
#include "GS/GSCapture.h"
|
|
#include "GS/GSVector.h"
|
|
#include "GS/Renderers/Common/GSDevice.h"
|
|
#ifdef _WIN32
|
|
#include "GS/Renderers/DX12/GSDevice12.h"
|
|
#endif
|
|
#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"
|
|
|
|
#if defined(__APPLE__)
|
|
#include <TargetConditionals.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#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_jit_line;
|
|
SmallString s_cpu_usage_ee_line;
|
|
SmallString s_cpu_usage_gs_line;
|
|
SmallString s_cpu_usage_gs_back_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_gpu_debug_info_line;
|
|
SmallString s_gpu_stats_line;
|
|
SmallString s_speed_icon;
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
SmallString s_ios_device_stats_line;
|
|
|
|
// Set by the iOS CMakeLists alongside the bundle version, so the overlay and
|
|
// the About screen cannot disagree. "dev" only shows up in a build that came
|
|
// from somewhere else.
|
|
#if defined(ARMSX2_VERSION_STR)
|
|
#define ARMSX2_IOS_OSD_VERSION ARMSX2_VERSION_STR
|
|
#else
|
|
#define ARMSX2_IOS_OSD_VERSION "dev"
|
|
#endif
|
|
#endif
|
|
|
|
// Shrink-to-fit for the performance overlay. Only ever comes down, and only far enough for the widest
|
|
// line to fit, so a value gaining a digit can't resize the block under the reader.
|
|
static float s_osd_font_size = 0.0f;
|
|
static float s_osd_fit_avail = -1.0f;
|
|
static float s_osd_fit_base = -1.0f;
|
|
static u32 s_osd_fit_lines = 0;
|
|
static float s_osd_widest = 0.0f;
|
|
|
|
constexpr ImU32 white_color = IM_COL32(255, 255, 255, 255);
|
|
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// Battery, heat and RAM come from the iOS frontend. There is no header for these three; the bridge
|
|
// forward-declares its own the same way.
|
|
extern "C" bool ARMSX2_iOSShouldShowDeviceStatsOverlay();
|
|
extern "C" int ARMSX2_iOSGetDeviceStatsOverlaySeverity();
|
|
extern "C" const char* ARMSX2_iOSGetDeviceStatsOverlayLine();
|
|
#endif
|
|
|
|
/// The OSD's normal text colour. GSConfig.OsdColor is 0xRRGGBB, and 0 means "unset" — every
|
|
/// frontend but Android leaves it there, so the overlay keeps its classic white by default.
|
|
/// Read per line rather than cached: it's a field load, and caching it would need invalidating
|
|
/// on every settings apply for no measurable gain.
|
|
__fi static ImU32 OsdTextColor()
|
|
{
|
|
const u32 rgb = GSConfig.OsdColor;
|
|
if (rgb == 0)
|
|
return white_color;
|
|
return IM_COL32((rgb >> 16) & 0xFFu, (rgb >> 8) & 0xFFu, rgb & 0xFFu, 255);
|
|
}
|
|
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// Same red as the sub-95% speed line, so the OSD keeps one vocabulary of alarm.
|
|
static ImU32 ARMSX2IOSDeviceStatsColor()
|
|
{
|
|
switch (ARMSX2_iOSGetDeviceStatsOverlaySeverity())
|
|
{
|
|
case 2:
|
|
return IM_COL32(255, 100, 100, 255);
|
|
case 1:
|
|
return IM_COL32(255, 220, 100, 255);
|
|
default:
|
|
return OsdTextColor();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// 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;
|
|
}
|
|
|
|
// A line wider than the window would otherwise start at negative x and run off the left edge,
|
|
// which is far worse than being clipped on the right.
|
|
return ImVec2(std::max(abs_margin, 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 bottom_margin, float spacing);
|
|
static void DrawShaderCompileIndicator(float scale, float margin, float bottom_margin, float spacing);
|
|
static void DrawSettingsOverlay(float scale, float margin, float bottom_margin, float spacing);
|
|
static void DrawInputsOverlay(float scale, float margin, float bottom_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 bottom_margin, float spacing)
|
|
{
|
|
// The perf-OSD flags in GSConfig are refreshed from the authoritative EmuConfig.GS at
|
|
// the top of RenderOverlays (see the note there). When every perf line is off, draw
|
|
// nothing and return BEFORE any draw call, so a line string cached before a pause can't
|
|
// linger on screen (the rebuild block below is skipped while the VM is paused, which is
|
|
// why toggling in the menu looked inert). Packed rather than a chain of ors because the
|
|
// shrink-to-fit below needs to notice the set changing, not just emptying.
|
|
u32 enabled_lines =
|
|
(static_cast<u32>(GSConfig.OsdShowFPS) << 0) | (static_cast<u32>(GSConfig.OsdShowVPS) << 1) |
|
|
(static_cast<u32>(GSConfig.OsdShowSpeed) << 2) | (static_cast<u32>(GSConfig.OsdShowResolution) << 3) |
|
|
(static_cast<u32>(GSConfig.OsdShowCPU) << 4) | (static_cast<u32>(GSConfig.OsdShowGPU) << 5) |
|
|
(static_cast<u32>(GSConfig.OsdShowGSStats) << 6) | (static_cast<u32>(GSConfig.OsdShowFrameTimes) << 7) |
|
|
(static_cast<u32>(GSConfig.OsdShowHardwareInfo) << 8) | (static_cast<u32>(GSConfig.OsdShowVersion) << 9) |
|
|
(static_cast<u32>(GSConfig.OsdShowGPUStats) << 10);
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// A Custom preset with nothing but Device Stats ticked is reachable, and it lands here.
|
|
enabled_lines |= static_cast<u32>(ARMSX2_iOSShouldShowDeviceStatsOverlay()) << 11;
|
|
#endif
|
|
if (enabled_lines == 0)
|
|
return;
|
|
|
|
const float shadow_offset = std::ceil(scale);
|
|
|
|
ImFont* const osd_font = ImGuiManager::GetOSDFont();
|
|
const float base_font_size = ImGuiManager::GetFontSizeStandard();
|
|
const float avail = GetWindowWidth() - 2.0f * margin;
|
|
|
|
// Deriving the size from the current text every frame is what made the block dance — the widest
|
|
// line changes width constantly, and right-aligned rows each move by their own share of the
|
|
// rescale. Start over only when the space or the line set changes; in between, ratchet and hold.
|
|
if (avail != s_osd_fit_avail || base_font_size != s_osd_fit_base || enabled_lines != s_osd_fit_lines)
|
|
{
|
|
s_osd_fit_avail = avail;
|
|
s_osd_fit_base = base_font_size;
|
|
s_osd_fit_lines = enabled_lines;
|
|
s_osd_font_size = base_font_size;
|
|
s_osd_widest = 0.0f;
|
|
}
|
|
else if (avail > 0.0f && s_osd_widest > avail)
|
|
{
|
|
// Widths are linear in the size, so one step gets there. Integral, or ImGui bakes a fresh
|
|
// atlas for every sub-pixel wobble; half size is as small as this stays readable.
|
|
const float needed = std::floor(s_osd_font_size * avail / s_osd_widest);
|
|
s_osd_font_size = std::clamp(needed, std::max(1.0f, std::floor(base_font_size * 0.5f)), s_osd_font_size);
|
|
}
|
|
s_osd_widest = 0.0f;
|
|
|
|
const float font_size = s_osd_font_size;
|
|
const float fit = font_size / base_font_size;
|
|
const float line_height = ImGuiFullscreen::GetLineHeight({ osd_font, font_size });
|
|
|
|
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() - (line_height * 8.0f)) * 0.5f;
|
|
break;
|
|
|
|
case OsdOverlayPos::BottomLeft:
|
|
case OsdOverlayPos::BottomCenter:
|
|
case OsdOverlayPos::BottomRight:
|
|
|
|
position_y = GetWindowHeight() - bottom_margin - (line_height * 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); \
|
|
s_osd_widest = std::max(s_osd_widest, text_size.x); \
|
|
const ImVec2 text_pos = CalculatePerformanceOverlayTextPosition(GSConfig.OsdPerformancePos, margin, text_size, GetWindowWidth(), position_y); \
|
|
const bool __bold_osd = GSConfig.OsdBoldText; \
|
|
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)); \
|
|
const auto __is_all_digits = [](const char* __begin, const char* __end) -> bool \
|
|
{ \
|
|
for (const char* __c = __begin; __c < __end; __c++) \
|
|
{ \
|
|
if (!std::isdigit(static_cast<unsigned char>(*__c))) \
|
|
return false; \
|
|
} \
|
|
return true; \
|
|
}; \
|
|
const auto __is_all_alpha = [](const char* __begin, const char* __end) -> bool \
|
|
{ \
|
|
for (const char* __c = __begin; __c < __end; __c++) \
|
|
{ \
|
|
if (!std::isalpha(static_cast<unsigned char>(*__c))) \
|
|
return false; \
|
|
} \
|
|
return true; \
|
|
}; \
|
|
for (const char* __p = (text); __p && *__p;) \
|
|
{ \
|
|
const char* __sep = strstr(__p, " | "); \
|
|
const char* __seg_end = __sep ? __sep : __p + strlen(__p); \
|
|
const char* __label_begin = nullptr; \
|
|
const char* __label_end = nullptr; \
|
|
const unsigned char __first = static_cast<unsigned char>(*__p); \
|
|
const bool __starts_numeric = (std::isdigit(__first) || __first == '+' || __first == '-' || __first == '.'); \
|
|
for (const char* __c = __p; __c < __seg_end; __c++) \
|
|
{ \
|
|
if (*__c == ':') \
|
|
{ \
|
|
__label_begin = __p; \
|
|
__label_end = __c + 1; \
|
|
break; \
|
|
} \
|
|
} \
|
|
if (!__label_begin) \
|
|
{ \
|
|
if (!__starts_numeric) \
|
|
{ \
|
|
__label_begin = __p; \
|
|
__label_end = __seg_end; \
|
|
} \
|
|
else \
|
|
{ \
|
|
const char* __first_space = __p; \
|
|
while (__first_space < __seg_end && *__first_space != ' ') \
|
|
__first_space++; \
|
|
if (__first_space < __seg_end) \
|
|
{ \
|
|
const char* __x = __p; \
|
|
while (__x < __first_space && *__x != 'x' && *__x != 'X') \
|
|
__x++; \
|
|
const bool __has_resolution_prefix = \
|
|
(__x > __p && (__x + 1) < __first_space && \
|
|
__is_all_digits(__p, __x) && __is_all_digits(__x + 1, __first_space)); \
|
|
if (__has_resolution_prefix && (__first_space + 1) < __seg_end) \
|
|
{ \
|
|
__label_begin = __first_space + 1; \
|
|
__label_end = __seg_end; \
|
|
const char* __trim_end = __label_end; \
|
|
while (__trim_end > __label_begin && std::isspace(static_cast<unsigned char>(*(__trim_end - 1)))) \
|
|
__trim_end--; \
|
|
if (__trim_end > __label_begin && *(__trim_end - 1) == ')') \
|
|
{ \
|
|
const char* __open = __trim_end - 1; \
|
|
while (__open > __label_begin && *__open != '(') \
|
|
__open--; \
|
|
if (__open > __label_begin && *__open == '(' && *(__open - 1) == ' ') \
|
|
__label_end = __open - 1; \
|
|
} \
|
|
} \
|
|
} \
|
|
if (!__label_begin) \
|
|
{ \
|
|
const char* __space = __seg_end; \
|
|
while (__space > __p && *(__space - 1) != ' ') \
|
|
__space--; \
|
|
if (__space > __p && __space < __seg_end && __is_all_alpha(__space, __seg_end)) \
|
|
{ \
|
|
__label_begin = __space; \
|
|
__label_end = __seg_end; \
|
|
} \
|
|
} \
|
|
} \
|
|
} \
|
|
if (__label_begin && __label_end && __label_begin < __label_end) \
|
|
{ \
|
|
const float __x0 = font->CalcTextSizeA(size, FLT_MAX, -1.0f, (text), __label_begin).x; \
|
|
const ImVec2 __pos(text_pos.x + __x0, text_pos.y); \
|
|
if (__bold_osd) \
|
|
{ \
|
|
dl->AddText(font, size, __pos, color, __label_begin, __label_end); \
|
|
dl->AddText(font, size, ImVec2(__pos.x + 0.6f, __pos.y), color, __label_begin, __label_end); \
|
|
} \
|
|
} \
|
|
__p = __sep ? __sep + 3 : nullptr; \
|
|
} \
|
|
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 defined(__ANDROID__)
|
|
// Display-rate limiters, FIRST in the line so they read as a qualifier on the FPS that
|
|
// follows rather than a stray value. Shown only while ACTIVE, so they cost nothing and
|
|
// need no toggle of their own. Both lower the ON-SCREEN rate while emulation keeps
|
|
// running full speed, so without a label a capped display is indistinguishable from the
|
|
// emulator running badly — which is a good part of why these looked broken.
|
|
if (const u32 fps_cap = GSGetMaxPresentFps(); fps_cap > 0)
|
|
{
|
|
s_speed_line.append_format("FPS CAP: {}", fps_cap);
|
|
// The cap is deliberately bypassed while fast-forwarding so the speed-up stays
|
|
// visible; say so, or it looks like the cap is simply being ignored.
|
|
if (GSGetPresentCapSuspended())
|
|
s_speed_line.append(" (off: FF)");
|
|
}
|
|
if (const u32 skip = GSGetManualFrameSkip(); skip > 0)
|
|
s_speed_line.append_format("{}SKIP: {}", s_speed_line.empty() ? "" : " | ", skip);
|
|
#endif
|
|
|
|
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)
|
|
{
|
|
#if defined(__APPLE__) && !TARGET_OS_IPHONE
|
|
if (BuildVersion::GitTagHi != 0 || BuildVersion::GitTagMid != 0 || BuildVersion::GitTagLo != 0)
|
|
{
|
|
s_speed_line.append_format("{}ARMSX2-MacOS 2.1 | Core: {}.{}.{}",
|
|
s_speed_line.empty() ? "" : " | ", BuildVersion::GitTagHi, BuildVersion::GitTagMid, BuildVersion::GitTagLo);
|
|
}
|
|
else
|
|
{
|
|
s_speed_line.append_format("{}ARMSX2-MacOS 2.1 | Core: {}",
|
|
s_speed_line.empty() ? "" : " | ", BuildVersion::GitRev);
|
|
}
|
|
#elif defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// Version comes from the one place it is set, so this cannot drift
|
|
// the way the two branches either side of it have.
|
|
s_speed_line.append_format("{}ARMSX2 " ARMSX2_IOS_OSD_VERSION " | Core: {}",
|
|
s_speed_line.empty() ? "" : " | ", BuildVersion::GitRev);
|
|
#elif defined(__ANDROID__)
|
|
s_speed_line.append_format("{}ARMSX2 2.7", s_speed_line.empty() ? "" : " | ");
|
|
#else
|
|
s_speed_line.append_format("{}PCSX2 {}", s_speed_line.empty() ? "" : " | ", BuildVersion::GitRev);
|
|
#endif
|
|
}
|
|
|
|
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 = OsdTextColor();
|
|
|
|
DRAW_LINE(osd_font, font_size, s_speed_line.c_str(), s_speed_line_color);
|
|
}
|
|
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
if (ARMSX2_iOSShouldShowDeviceStatsOverlay())
|
|
{
|
|
const char* stats_line = ARMSX2_iOSGetDeviceStatsOverlayLine();
|
|
s_ios_device_stats_line.assign(stats_line ? stats_line : "");
|
|
if (!s_ios_device_stats_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_ios_device_stats_line.c_str(), ARMSX2IOSDeviceStatsColor());
|
|
}
|
|
#endif
|
|
|
|
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(osd_font, font_size, s_gs_stats_line.c_str(), OsdTextColor());
|
|
if (!s_gs_memory_stats_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_gs_memory_stats_line.c_str(), OsdTextColor());
|
|
DRAW_LINE(osd_font, font_size, s_gs_frame_times_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowResolution)
|
|
{
|
|
int iwidth, iheight;
|
|
GSgetInternalResolution(&iwidth, &iheight);
|
|
|
|
s_resolution_line.format("{}x{} {} {}", iwidth, iheight, ReportVideoMode(), ReportInterlaceMode());
|
|
DRAW_LINE(osd_font, font_size, s_resolution_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
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(osd_font, font_size, s_hardware_info_cpu_line.c_str(), OsdTextColor());
|
|
|
|
// GPU
|
|
const char* gpu_suffix = "";
|
|
if (GSConfig.UseDebugDevice && GSConfig.HWROV)
|
|
gpu_suffix = " (Debug & ROV)";
|
|
else if (GSConfig.UseDebugDevice)
|
|
gpu_suffix = " (Debug)";
|
|
else if (GSConfig.HWROV)
|
|
gpu_suffix = " (ROV)";
|
|
|
|
s_hardware_info_gpu_line.format(
|
|
"GPU: {}{}",
|
|
g_gs_device->GetName(),
|
|
gpu_suffix);
|
|
|
|
DRAW_LINE(osd_font, font_size, s_hardware_info_gpu_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowCPU)
|
|
{
|
|
#if defined(__APPLE__) && !TARGET_OS_IPHONE
|
|
s_cpu_jit_line.format("EE:{} | IOP:{} | VU0:{} | VU1:{}",
|
|
EmuConfig.Cpu.Recompiler.EnableEE ? "JIT" : "INT",
|
|
EmuConfig.Cpu.Recompiler.EnableIOP ? "JIT" : "INT",
|
|
EmuConfig.Cpu.Recompiler.EnableVU0 ? "JIT" : "INT",
|
|
EmuConfig.Cpu.Recompiler.EnableVU1 ? "JIT" : "INT");
|
|
DRAW_LINE(osd_font, font_size, s_cpu_jit_line.c_str(), OsdTextColor());
|
|
#endif
|
|
|
|
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(osd_font, font_size, s_cpu_usage_ee_line.c_str(), OsdTextColor());
|
|
|
|
s_cpu_usage_gs_line.assign("GS: ");
|
|
FormatProcessorStat(s_cpu_usage_gs_line, PerformanceMetrics::GetGSThreadUsage(), PerformanceMetrics::GetGSThreadAverageTime());
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_gs_line.c_str(), OsdTextColor());
|
|
|
|
// Only exists under GSBackThreadMode >= Lockstep. The line above is the MTGS
|
|
// thread alone, so without this one the split's second half is invisible.
|
|
if (PerformanceMetrics::HasGSBackThread())
|
|
{
|
|
s_cpu_usage_gs_back_line.assign("GSB: ");
|
|
FormatProcessorStat(s_cpu_usage_gs_back_line, PerformanceMetrics::GetGSBackThreadUsage(),
|
|
PerformanceMetrics::GetGSBackThreadAverageTime());
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_gs_back_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (THREAD_VU1)
|
|
{
|
|
s_cpu_usage_vu_line.assign("VU: ");
|
|
FormatProcessorStat(s_cpu_usage_vu_line, PerformanceMetrics::GetVUThreadUsage(), PerformanceMetrics::GetVUThreadAverageTime());
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_vu_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
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(osd_font, font_size, s_software_thread_lines[thread].c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSCapture::IsCapturing())
|
|
{
|
|
s_capture_line.assign("CAP: ");
|
|
FormatProcessorStat(s_capture_line, PerformanceMetrics::GetCaptureThreadUsage(), PerformanceMetrics::GetCaptureThreadAverageTime());
|
|
DRAW_LINE(osd_font, font_size, s_capture_line.c_str(), OsdTextColor());
|
|
}
|
|
}
|
|
|
|
if (GSConfig.OsdShowGPU)
|
|
{
|
|
s_gpu_usage_line.assign("GPU: ");
|
|
FormatProcessorStat(s_gpu_usage_line, PerformanceMetrics::GetGPUUsage(), PerformanceMetrics::GetGPUAverageTime());
|
|
DRAW_LINE(osd_font, font_size, s_gpu_usage_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowGPUDebug)
|
|
{
|
|
#ifdef _WIN32
|
|
if (g_gs_device->GetRenderAPI() == RenderAPI::D3D12)
|
|
{
|
|
GSDevice12* dev12 = static_cast<GSDevice12*>(g_gs_device.get());
|
|
|
|
s_gpu_debug_info_line.format("D3D12 Descriptor Heaps | SRV/UAV: {}/{} | RTV: {}/{} | DSV {}/{}",
|
|
dev12->GetDescriptorHeapManager().GetAllocatedDescriptors(), dev12->GetDescriptorHeapManager().GetNumDescriptors(),
|
|
dev12->GetRTVHeapManager().GetAllocatedDescriptors(), dev12->GetRTVHeapManager().GetNumDescriptors(),
|
|
dev12->GetDSVHeapManager().GetAllocatedDescriptors(), dev12->GetDSVHeapManager().GetNumDescriptors());
|
|
DRAW_LINE(osd_font, font_size, s_gpu_debug_info_line.c_str(), OsdTextColor());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (GSConfig.OsdShowGPUStats)
|
|
{
|
|
const auto FormatUnits = [](double val) {
|
|
if (val >= 1e9)
|
|
return fmt::format("{:.5}B", val / 1e9);
|
|
if (val >= 1e6)
|
|
return fmt::format("{:.5}M", val / 1e6);
|
|
if (val >= 1e3)
|
|
return fmt::format("{:.5}K", val / 1e3);
|
|
return fmt::format("{:.5}", val);
|
|
};
|
|
|
|
s_gpu_stats_line.format("VSI: {} | PSI: {}",
|
|
FormatUnits(PerformanceMetrics::GetGPUAverageVSInvocations()),
|
|
FormatUnits(PerformanceMetrics::GetGPUAveragePSInvocations()));
|
|
DRAW_LINE(osd_font, font_size, s_gpu_stats_line.c_str(), OsdTextColor());
|
|
}
|
|
}
|
|
// No refresh yet. Display cached lines.
|
|
else
|
|
{
|
|
if (GSConfig.OsdShowFPS || GSConfig.OsdShowVPS || GSConfig.OsdShowSpeed || GSConfig.OsdShowVersion)
|
|
DRAW_LINE(osd_font, font_size, s_speed_line.c_str(), s_speed_line_color);
|
|
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
if (ARMSX2_iOSShouldShowDeviceStatsOverlay() && !s_ios_device_stats_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_ios_device_stats_line.c_str(), ARMSX2IOSDeviceStatsColor());
|
|
#endif
|
|
|
|
if (GSConfig.OsdShowGSStats)
|
|
{
|
|
if (!s_gs_stats_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_gs_stats_line.c_str(), OsdTextColor());
|
|
if (!s_gs_memory_stats_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_gs_memory_stats_line.c_str(), OsdTextColor());
|
|
DRAW_LINE(osd_font, font_size, s_gs_frame_times_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowResolution)
|
|
DRAW_LINE(osd_font, font_size, s_resolution_line.c_str(), OsdTextColor());
|
|
|
|
if (GSConfig.OsdShowHardwareInfo)
|
|
{
|
|
DRAW_LINE(osd_font, font_size, s_hardware_info_cpu_line.c_str(), OsdTextColor());
|
|
DRAW_LINE(osd_font, font_size, s_hardware_info_gpu_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowCPU)
|
|
{
|
|
#if defined(__APPLE__) && !TARGET_OS_IPHONE
|
|
if (!s_cpu_jit_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_cpu_jit_line.c_str(), OsdTextColor());
|
|
#endif
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_ee_line.c_str(), OsdTextColor());
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_gs_line.c_str(), OsdTextColor());
|
|
if (PerformanceMetrics::HasGSBackThread())
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_gs_back_line.c_str(), OsdTextColor());
|
|
if (THREAD_VU1)
|
|
DRAW_LINE(osd_font, font_size, s_cpu_usage_vu_line.c_str(), OsdTextColor());
|
|
|
|
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(osd_font, font_size, s_software_thread_lines[thread].c_str(), OsdTextColor());
|
|
|
|
if (GSCapture::IsCapturing())
|
|
DRAW_LINE(osd_font, font_size, s_capture_line.c_str(), OsdTextColor());
|
|
}
|
|
|
|
if (GSConfig.OsdShowGPU)
|
|
DRAW_LINE(osd_font, font_size, s_gpu_usage_line.c_str(), OsdTextColor());
|
|
|
|
if (GSConfig.OsdShowGPUDebug)
|
|
{
|
|
#ifdef _WIN32
|
|
if (g_gs_device->GetRenderAPI() == RenderAPI::D3D12)
|
|
DRAW_LINE(osd_font, font_size, s_gpu_debug_info_line.c_str(), OsdTextColor());
|
|
#endif
|
|
}
|
|
|
|
if (GSConfig.OsdShowGPUStats)
|
|
{
|
|
DRAW_LINE(osd_font, font_size, s_gpu_stats_line.c_str(), OsdTextColor());
|
|
}
|
|
}
|
|
|
|
// Check every OSD frame because this is an animation.
|
|
if (GSConfig.OsdShowFrameTimes)
|
|
{
|
|
const auto& history = PerformanceMetrics::GetFrameTimeHistory();
|
|
const u32 sample_count = PerformanceMetrics::NUM_FRAME_TIME_SAMPLES;
|
|
const u32 hist_pos = PerformanceMetrics::GetFrameTimeHistoryPos();
|
|
static constexpr u32 SCALE_WINDOW = 60u;
|
|
static constexpr float DEFAULT_FT_MIN = 0.0f;
|
|
static constexpr float DEFAULT_FT_MAX = 20.0f;
|
|
static constexpr float DEFAULT_VPS_MIN = 0.0f;
|
|
static constexpr float DEFAULT_VPS_MAX = 100.0f;
|
|
static constexpr float SCALE_SMOOTHING = 0.25f;
|
|
|
|
const auto compute_window_extents = [&](const auto& values) {
|
|
float lo = 1.0e9f;
|
|
float hi = 0.0f;
|
|
for (u32 k = 0; k < SCALE_WINDOW && k < sample_count; k++)
|
|
{
|
|
const float v = values[(hist_pos + sample_count - SCALE_WINDOW + k) % sample_count];
|
|
if (v > 0.0f)
|
|
{
|
|
lo = std::min(lo, v);
|
|
hi = std::max(hi, v);
|
|
}
|
|
}
|
|
return std::pair(lo, hi);
|
|
};
|
|
|
|
auto [initial_lo, initial_hi] = compute_window_extents(history);
|
|
auto [min_val, max_val] = [&]() {
|
|
float lo = initial_lo;
|
|
float hi = initial_hi;
|
|
if (hi < lo)
|
|
return std::pair(DEFAULT_FT_MIN, DEFAULT_FT_MAX);
|
|
if ((hi - lo) < 4.0f)
|
|
{
|
|
lo = lo - std::fmod(lo, 1.0f);
|
|
hi = hi - std::fmod(hi, 1.0f) + 1.0f;
|
|
lo = std::max(lo - 2.0f, 0.0f);
|
|
hi += 2.0f;
|
|
}
|
|
return std::pair(lo, std::max(hi, lo + 1.0f));
|
|
}();
|
|
|
|
PerformanceMetrics::FrameTimeHistory ft_history = history;
|
|
|
|
float last_ft = 0.0f;
|
|
for (u32 k = 0; k < sample_count; k++)
|
|
{
|
|
const u32 idx = (hist_pos + sample_count - 1 - k) % sample_count;
|
|
const float ft = ft_history[idx];
|
|
if (ft > 0.0f)
|
|
{
|
|
last_ft = ft;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (u32 i = 0; i < sample_count; i++)
|
|
{
|
|
const u32 idx = (hist_pos + i) % sample_count;
|
|
float& ft = ft_history[idx];
|
|
if (ft > 0.0f)
|
|
{
|
|
last_ft = ft;
|
|
continue;
|
|
}
|
|
|
|
ft = last_ft;
|
|
}
|
|
|
|
std::array<float, PerformanceMetrics::NUM_FRAME_TIME_SAMPLES> vps_history;
|
|
for (u32 i = 0; i < sample_count; i++)
|
|
vps_history[i] = (ft_history[i] >= 0.01f) ? std::min(10000.0f, 1000.0f / ft_history[i]) : 0.0f;
|
|
|
|
auto [vps_initial_lo, vps_initial_hi] = compute_window_extents(vps_history);
|
|
auto [vps_scale_min, vps_scale_max] = [&]() {
|
|
float lo = vps_initial_lo;
|
|
float hi = vps_initial_hi;
|
|
if (hi < lo)
|
|
return std::pair(DEFAULT_VPS_MIN, DEFAULT_VPS_MAX);
|
|
if (hi - lo < 10.0f)
|
|
{
|
|
lo = std::floor(lo / 50.0f) * 50.0f;
|
|
hi = std::ceil(hi / 50.0f) * 50.0f;
|
|
lo = std::max(0.0f, lo - 5.0f);
|
|
hi = std::max(hi + 5.0f, lo + 10.0f);
|
|
}
|
|
return std::pair(lo, hi);
|
|
}();
|
|
|
|
static float s_ft_min = DEFAULT_FT_MIN;
|
|
static float s_ft_max = DEFAULT_FT_MAX;
|
|
static float s_vps_min = DEFAULT_VPS_MIN;
|
|
static float s_vps_max = DEFAULT_VPS_MAX;
|
|
s_ft_min += (min_val - s_ft_min) * SCALE_SMOOTHING;
|
|
s_ft_max += (max_val - s_ft_max) * SCALE_SMOOTHING;
|
|
s_vps_min += (vps_scale_min - s_vps_min) * SCALE_SMOOTHING;
|
|
s_vps_max += (vps_scale_max - s_vps_max) * SCALE_SMOOTHING;
|
|
min_val = s_ft_min;
|
|
max_val = std::max(s_ft_max, min_val + 1.0f);
|
|
float min_vps = s_vps_min;
|
|
float max_vps = std::max(s_vps_max, min_vps + 10.0f);
|
|
|
|
// The graph has to come down with the text or it overhangs a shrunken block. Off the size
|
|
// the text actually got, not the ratio we asked for, or the two disagree by up to a step.
|
|
const float graph_scale = scale * fit;
|
|
|
|
SmallString label_buf;
|
|
label_buf.format("{:.1f}", max_val);
|
|
const float y_label_w = osd_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, label_buf.c_str(), label_buf.c_str() + label_buf.length()).x + 4.0f * graph_scale;
|
|
label_buf.format("{:.0f}", max_vps);
|
|
const float right_label_w = osd_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, label_buf.c_str(), label_buf.c_str() + label_buf.length()).x + 4.0f * graph_scale;
|
|
|
|
const float pad = 4.0f * graph_scale;
|
|
const float row_gap = 2.0f * graph_scale;
|
|
const float legend_h = (font_size * 2.0f) + row_gap + pad;
|
|
const ImVec2 graph_size(200.0f * graph_scale, 60.0f * graph_scale);
|
|
const ImVec2 total_size(y_label_w + graph_size.x + right_label_w + 2.0f * pad, graph_size.y + legend_h + 2.0f * pad);
|
|
s_osd_widest = std::max(s_osd_widest, total_size.x);
|
|
|
|
ImGui::SetNextWindowSize(total_size);
|
|
ImGui::SetNextWindowPos(CalculatePerformanceOverlayTextPosition(GSConfig.OsdPerformancePos, margin, total_size, GetWindowWidth(), position_y));
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.45f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * graph_scale);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
ImGui::PushFont(osd_font, font_size);
|
|
if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs))
|
|
{
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
const ImVec2 wpos(ImGui::GetWindowPos());
|
|
const ImVec2 plot_tl(wpos.x + pad + y_label_w, wpos.y + pad);
|
|
const ImVec2 plot_br(plot_tl.x + graph_size.x, plot_tl.y + graph_size.y);
|
|
|
|
dl->AddRectFilled(plot_tl, plot_br, IM_COL32(0, 0, 0, 60));
|
|
|
|
const int num_ticks = std::max(1, std::min(8, static_cast<int>(graph_size.y / (font_size * 1.1f))));
|
|
const float left_label_x = wpos.x + pad + y_label_w;
|
|
const float right_label_x = plot_br.x + 2.0f * graph_scale;
|
|
|
|
const ImU32 ft_col = IM_COL32(100, 200, 255, 230);
|
|
const ImU32 vps_col = IM_COL32(100, 255, 100, 230);
|
|
|
|
auto draw_grid_and_labels = [&](int ticks) {
|
|
SmallString s;
|
|
for (int i = 0; i <= ticks; i++)
|
|
{
|
|
const float frac = static_cast<float>(i) / ticks;
|
|
const float grid_y = plot_br.y - frac * graph_size.y;
|
|
const float ly = grid_y - font_size * 0.5f;
|
|
|
|
dl->AddLine(ImVec2(plot_tl.x, grid_y), ImVec2(plot_br.x, grid_y), IM_COL32(255, 255, 255, 40), 1.0f);
|
|
|
|
s.format("{:.1f}", min_val + (max_val - min_val) * frac);
|
|
const float left_text_w = osd_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.c_str(), s.c_str() + s.length()).x;
|
|
const float lx = left_label_x - left_text_w - 2.0f * graph_scale;
|
|
dl->AddText(osd_font, font_size, ImVec2(lx + shadow_offset, ly + shadow_offset), IM_COL32(0, 0, 0, 100), s.c_str(), s.c_str() + s.length());
|
|
dl->AddText(osd_font, font_size, ImVec2(lx, ly), ft_col, s.c_str(), s.c_str() + s.length());
|
|
|
|
s.format("{:.0f}", min_vps + (max_vps - min_vps) * frac);
|
|
dl->AddText(osd_font, font_size, ImVec2(right_label_x + shadow_offset, ly + shadow_offset), IM_COL32(0, 0, 0, 100), s.c_str(), s.c_str() + s.length());
|
|
dl->AddText(osd_font, font_size, ImVec2(right_label_x, ly), vps_col, s.c_str(), s.c_str() + s.length());
|
|
}
|
|
};
|
|
|
|
draw_grid_and_labels(num_ticks);
|
|
|
|
const auto col32_to_vec4 = [](ImU32 col) -> ImVec4 {
|
|
return ImVec4(
|
|
static_cast<float>((col >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f,
|
|
static_cast<float>((col >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f,
|
|
static_cast<float>((col >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f,
|
|
static_cast<float>((col >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f);
|
|
};
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
|
|
|
ImGui::SetCursorScreenPos(plot_tl);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotLines, col32_to_vec4(ft_col));
|
|
ImGui::PlotLines("##frame_time_plot", ft_history.data(), static_cast<int>(sample_count), static_cast<int>(hist_pos),
|
|
nullptr, min_val, max_val, graph_size);
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::SetCursorScreenPos(plot_tl);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotLines, col32_to_vec4(vps_col));
|
|
ImGui::PlotLines("##vps_plot", vps_history.data(), static_cast<int>(sample_count), static_cast<int>(hist_pos),
|
|
nullptr, min_vps, max_vps, graph_size);
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor(2);
|
|
|
|
const float legend_y = plot_br.y + pad * 0.5f;
|
|
const float legend_square_size = font_size * 0.65f;
|
|
const float legend_gap = 4.0f * graph_scale;
|
|
SmallString frame_part, vps_part;
|
|
frame_part.format("Frame: {:.2f} ms", PerformanceMetrics::GetAverageFrameTime());
|
|
vps_part.format("V-Blank: {:.2f}", PerformanceMetrics::GetFPS());
|
|
const float fw = osd_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, frame_part.c_str(), nullptr).x;
|
|
const float vw = osd_font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, vps_part.c_str(), nullptr).x;
|
|
const float max_text_w = std::max(fw, vw);
|
|
const float row_width = legend_square_size + legend_gap + max_text_w;
|
|
float base_x = wpos.x + (total_size.x - row_width) * 0.5f;
|
|
auto draw_legend_entry = [&](ImU32 col, const char* text, float text_w, float y) {
|
|
float lx = base_x;
|
|
dl->AddRectFilled(ImVec2(lx, y + (font_size - legend_square_size) * 0.5f),
|
|
ImVec2(lx + legend_square_size, y + (font_size + legend_square_size) * 0.5f), col);
|
|
lx += legend_square_size + legend_gap;
|
|
dl->AddText(osd_font, font_size, ImVec2(lx + shadow_offset, y + shadow_offset), IM_COL32(0, 0, 0, 100), text, nullptr);
|
|
dl->AddText(osd_font, font_size, ImVec2(lx, y), OsdTextColor(), text, nullptr);
|
|
};
|
|
draw_legend_entry(IM_COL32(100, 200, 255, 230), frame_part.c_str(), fw, legend_y);
|
|
draw_legend_entry(IM_COL32(100, 255, 100, 230), vps_part.c_str(), vw, legend_y + font_size + row_gap);
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopFont();
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::PopStyleColor(1);
|
|
}
|
|
}
|
|
|
|
#undef DRAW_LINE
|
|
}
|
|
|
|
// How tall the settings string ended up. It wraps now, so the indicator below can no longer
|
|
// assume one line. DrawSettingsOverlay runs first, so this is current.
|
|
static float s_settings_overlay_height = 0.0f;
|
|
|
|
__ri void ImGuiManager::DrawShaderCompileIndicator(float scale, float margin, float bottom_margin, float spacing)
|
|
{
|
|
static bool s_indicator_was_visible = false;
|
|
static double s_indicator_fade_in_start = 0.0;
|
|
|
|
if (!GSConfig.OsdShowGPU || !GSShaderCompileIndicator::IsVisible())
|
|
{
|
|
s_indicator_was_visible = false;
|
|
return;
|
|
}
|
|
|
|
const double imgui_time = ImGui::GetTime();
|
|
if (!s_indicator_was_visible)
|
|
s_indicator_fade_in_start = imgui_time;
|
|
s_indicator_was_visible = true;
|
|
|
|
constexpr double fade_in_seconds = 0.12;
|
|
const float fade_in_alpha = static_cast<float>(
|
|
std::min(1.0, (imgui_time - s_indicator_fade_in_start) / fade_in_seconds));
|
|
const float fade_out_alpha = GSShaderCompileIndicator::GetFadeAlpha();
|
|
const float alpha = std::clamp(fade_in_alpha * fade_out_alpha, 0.0f, 1.0f);
|
|
const ImU32 text_col = IM_COL32(255, 255, 255, static_cast<int>(std::lround(255.0f * alpha)));
|
|
const ImU32 shadow_col = IM_COL32(0, 0, 0, static_cast<int>(std::lround(100.0f * alpha)));
|
|
const ImU32 spinner_track_col = IM_COL32(255, 255, 255, static_cast<int>(std::lround(45.0f * alpha)));
|
|
|
|
static constexpr const char* COMPILED_ONE =
|
|
TRANSLATE_NOOP("ImGuiOverlays", "Compiled {0} shader in {1}ms");
|
|
static constexpr const char* COMPILED_MANY =
|
|
TRANSLATE_NOOP("ImGuiOverlays", "Compiled {0} shaders in {1}ms");
|
|
|
|
const u32 count = GSShaderCompileIndicator::GetCount();
|
|
const u32 time_ms = GSShaderCompileIndicator::GetTimeMs();
|
|
const std::string label = (count == 1) ?
|
|
fmt::format(TRANSLATE_FS("ImGuiOverlays", COMPILED_ONE), count, time_ms) :
|
|
fmt::format(TRANSLATE_FS("ImGuiOverlays", COMPILED_MANY), count, time_ms);
|
|
|
|
ImFont* const font = ImGuiManager::GetOSDFont();
|
|
const float font_size = ImGuiManager::GetFontSizeStandard();
|
|
const float baseline_y = GetWindowHeight() - bottom_margin - s_settings_overlay_height;
|
|
const float radius = std::ceil(10.0f * scale);
|
|
const float cx = GetWindowWidth() - margin - radius;
|
|
const float cy = baseline_y - spacing - radius;
|
|
const ImVec2 center(cx, cy);
|
|
|
|
const ImVec2 text_size = font->CalcTextSizeA(
|
|
font_size, std::numeric_limits<float>::max(), -1.0f, label.c_str(), label.c_str() + label.length(), nullptr);
|
|
const float shadow_offset = std::ceil(scale);
|
|
const float text_gap = std::ceil(6.0f * scale);
|
|
const float text_x = cx - radius - text_gap - text_size.x;
|
|
const float text_y = cy - font_size * 0.5f;
|
|
|
|
ImDrawList* const dl = ImGui::GetBackgroundDrawList();
|
|
const float a0 = static_cast<float>(ImGui::GetTime()) * 10.0f;
|
|
const float a1 = a0 + IM_PI * 1.12f;
|
|
const float thickness = std::ceil(2.5f * scale);
|
|
|
|
dl->AddText(font, font_size, ImVec2(text_x + shadow_offset, text_y + shadow_offset), shadow_col,
|
|
label.c_str(), label.c_str() + label.length());
|
|
dl->AddText(font, font_size, ImVec2(text_x, text_y), text_col, label.c_str(), label.c_str() + label.length());
|
|
if (GSConfig.OsdBoldText)
|
|
{
|
|
dl->AddText(font, font_size, ImVec2(text_x + 0.6f, text_y), text_col, label.c_str(),
|
|
label.c_str() + label.length());
|
|
}
|
|
|
|
dl->PathClear();
|
|
dl->PathArcTo(center, radius, 0.0f, 2.0f * IM_PI, 32);
|
|
dl->PathStroke(spinner_track_col, std::max(1.0f, thickness * 0.65f), false);
|
|
|
|
dl->PathClear();
|
|
dl->PathArcTo(center, radius, a0, a1, 24);
|
|
dl->PathStroke(text_col, thickness, false);
|
|
}
|
|
|
|
__ri void ImGuiManager::DrawSettingsOverlay(float scale, float margin, float bottom_margin, float spacing)
|
|
{
|
|
s_settings_overlay_height = 0.0f;
|
|
|
|
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 ");
|
|
|
|
if (GSConfig.HWAccurateAlphaTest)
|
|
APPEND("AAT ");
|
|
|
|
if (GSConfig.HWAA1)
|
|
APPEND("AA1 ");
|
|
|
|
if (GSConfig.HWROV)
|
|
APPEND("ROV ");
|
|
|
|
if (GSConfig.HWROVBarriersVK)
|
|
APPEND("RBVK ");
|
|
|
|
// 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("LDR={} ", 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.UserHacks_DrawBuffering)
|
|
APPEND("DRWB ");
|
|
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::GetOSDFont();
|
|
const float base_font_size = ImGuiManager::GetFontSizeStandard();
|
|
const float avail = GetWindowWidth() - 2.0f * margin;
|
|
|
|
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
|
ImVec2 text_size =
|
|
font->CalcTextSizeA(base_font_size, std::numeric_limits<float>::max(), -1.0f, text.c_str(), text.c_str() + text.length(), nullptr);
|
|
|
|
// This one runs to a couple of hundred characters, so shrinking it far enough to fit on a single
|
|
// line would leave it unreadable. Take it down a little, then let it wrap onto two or three.
|
|
float font_size = base_font_size;
|
|
float wrap_width = 0.0f;
|
|
if (avail > 0.0f && text_size.x > avail)
|
|
{
|
|
font_size = std::max(1.0f, std::floor(base_font_size * std::clamp(avail / text_size.x, 0.6f, 1.0f)));
|
|
wrap_width = avail;
|
|
text_size = font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), wrap_width, text.c_str(), text.c_str() + text.length(), nullptr);
|
|
}
|
|
|
|
s_settings_overlay_height = text_size.y;
|
|
|
|
const float position_y = GetWindowHeight() - bottom_margin - text_size.y;
|
|
const ImVec2 text_pos(std::max(margin, GetWindowWidth() - margin - text_size.x), position_y);
|
|
const bool bold_osd = GSConfig.OsdBoldText;
|
|
dl->AddText(font, font_size,
|
|
ImVec2(text_pos.x + shadow_offset, text_pos.y + shadow_offset), IM_COL32(0, 0, 0, 100),
|
|
text.c_str(), text.c_str() + text.length(), wrap_width);
|
|
dl->AddText(font, font_size, text_pos, white_color,
|
|
text.c_str(), text.c_str() + text.length(), wrap_width);
|
|
if (bold_osd)
|
|
{
|
|
dl->AddText(font, font_size, ImVec2(text_pos.x + 0.6f, text_pos.y), white_color,
|
|
text.c_str(), text.c_str() + text.length(), wrap_width);
|
|
}
|
|
}
|
|
|
|
__ri void ImGuiManager::DrawInputsOverlay(float scale, float margin, float bottom_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();
|
|
const float line_height = ImGuiFullscreen::GetLineHeight({ font, font_size });
|
|
|
|
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 - bottom_margin - ((static_cast<float>(num_ports) * (line_height + 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 += line_height + 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 += line_height + 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 osd_font = ImGuiManager::GetOSDFont();
|
|
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(osd_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(osd_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(osd_font, font_size, g_InputRecordingData.recording_active_message.c_str(), IM_COL32(117, 255, 241, 255));
|
|
DRAW_LINE(osd_font, font_size, g_InputRecordingData.frame_data_message.c_str(), IM_COL32(117, 255, 241, 255));
|
|
DRAW_LINE(osd_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 osd_font = ImGuiManager::GetOSDFont();
|
|
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 = osd_font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
|
|
-1.0f, ICON, nullptr, nullptr);
|
|
const ImVec2 text_size = osd_font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
|
|
-1.0f, text_msg.c_str(), text_msg.end_ptr(), nullptr);
|
|
|
|
// Shadow
|
|
dl->AddText(osd_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(osd_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(osd_font, font_size,
|
|
ImVec2(GetWindowWidth() - margin - text_size.x - icon_size.x, position_y), IM_COL32(255, 0, 0, 255), ICON);
|
|
dl->AddText(osd_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 osd_font = ImGuiManager::GetOSDFont();
|
|
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 = osd_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(osd_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(osd_font, font_size, text_pos, OsdTextColor(), 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 osd_font = ImGuiManager::GetOSDFont();
|
|
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(osd_font, font_size, s_speed_icon, OsdTextColor());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Draw Pause indicator
|
|
const TinyString pause_msg = TinyString::from_format(TRANSLATE_FS("ImGuiOverlays", "{} Paused"), ICON_FA_PAUSE);
|
|
DRAW_LINE(osd_font, font_size, pause_msg, OsdTextColor());
|
|
}
|
|
#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);
|
|
}
|
|
|
|
#ifdef __ANDROID__
|
|
namespace {
|
|
// Reload-immune snapshot of the Android UI's OSD choice. VMManager::ApplySettings
|
|
// re-derives EmuConfig.GS from the layered settings interface (base + per-game) every
|
|
// time it runs, which can resurrect an OSD the user just turned off (a per-game layer
|
|
// or a reload race overriding the base). The native applyOsdSetting() choke point
|
|
// records the user's intent HERE after every OSD change, and RenderOverlays honours
|
|
// it — so no core settings reload can revert the on-screen state.
|
|
struct AndroidOSDVisibility
|
|
{
|
|
bool valid = false;
|
|
bool fps = false, vps = false, speed = false, resolution = false, cpu = false,
|
|
gpu = false, gsStats = false, frameTimes = false, hardwareInfo = false,
|
|
version = false, gpuStats = false, settings = false, inputs = false;
|
|
};
|
|
AndroidOSDVisibility s_android_osd_vis;
|
|
} // namespace
|
|
|
|
void ImGuiManager::SetAndroidOSDVisibility(bool fps, bool vps, bool speed, bool resolution,
|
|
bool cpu, bool gpu, bool gsStats, bool frameTimes, bool hardwareInfo, bool version,
|
|
bool gpuStats, bool settings, bool inputs)
|
|
{
|
|
s_android_osd_vis = {true, fps, vps, speed, resolution, cpu, gpu, gsStats, frameTimes,
|
|
hardwareInfo, version, gpuStats, settings, inputs};
|
|
}
|
|
#endif
|
|
|
|
void ImGuiManager::RenderOverlays()
|
|
{
|
|
// Android: the live GSConfig can be stale — the GS device reopens whenever the
|
|
// pause/overlay releases the render surface, and GSopen re-derives GSConfig from a
|
|
// freshly-loaded config, so an OSD the user turned off silently came back. The Android
|
|
// UI records the user's authoritative OSD choice via SetAndroidOSDVisibility (immune to
|
|
// any VMManager::ApplySettings reload); honour that here when present, else fall back to
|
|
// EmuConfig.GS. Mirror every perf / settings-summary / inputs flag once, before any
|
|
// overlay draws, so the perf, settings and inputs overlays all honour their switches.
|
|
#ifdef __ANDROID__
|
|
if (s_android_osd_vis.valid)
|
|
{
|
|
GSConfig.OsdShowFPS = s_android_osd_vis.fps;
|
|
GSConfig.OsdShowVPS = s_android_osd_vis.vps;
|
|
GSConfig.OsdShowSpeed = s_android_osd_vis.speed;
|
|
GSConfig.OsdShowResolution = s_android_osd_vis.resolution;
|
|
GSConfig.OsdShowCPU = s_android_osd_vis.cpu;
|
|
GSConfig.OsdShowGPU = s_android_osd_vis.gpu;
|
|
GSConfig.OsdShowGSStats = s_android_osd_vis.gsStats;
|
|
GSConfig.OsdShowFrameTimes = s_android_osd_vis.frameTimes;
|
|
GSConfig.OsdShowHardwareInfo = s_android_osd_vis.hardwareInfo;
|
|
GSConfig.OsdShowVersion = s_android_osd_vis.version;
|
|
GSConfig.OsdShowGPUStats = s_android_osd_vis.gpuStats;
|
|
GSConfig.OsdShowSettings = s_android_osd_vis.settings;
|
|
GSConfig.OsdShowInputs = s_android_osd_vis.inputs;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
GSConfig.OsdShowFPS = EmuConfig.GS.OsdShowFPS;
|
|
GSConfig.OsdShowVPS = EmuConfig.GS.OsdShowVPS;
|
|
GSConfig.OsdShowSpeed = EmuConfig.GS.OsdShowSpeed;
|
|
GSConfig.OsdShowResolution = EmuConfig.GS.OsdShowResolution;
|
|
GSConfig.OsdShowCPU = EmuConfig.GS.OsdShowCPU;
|
|
GSConfig.OsdShowGPU = EmuConfig.GS.OsdShowGPU;
|
|
GSConfig.OsdShowGSStats = EmuConfig.GS.OsdShowGSStats;
|
|
GSConfig.OsdShowFrameTimes = EmuConfig.GS.OsdShowFrameTimes;
|
|
GSConfig.OsdShowHardwareInfo = EmuConfig.GS.OsdShowHardwareInfo;
|
|
GSConfig.OsdShowVersion = EmuConfig.GS.OsdShowVersion;
|
|
GSConfig.OsdShowGPUStats = EmuConfig.GS.OsdShowGPUStats;
|
|
GSConfig.OsdShowSettings = EmuConfig.GS.OsdShowSettings;
|
|
GSConfig.OsdShowInputs = EmuConfig.GS.OsdShowInputs;
|
|
}
|
|
|
|
const float scale = ImGuiManager::GetGlobalScale();
|
|
const float base_margin = std::ceil(GSConfig.OsdMargin * scale);
|
|
const float spacing = std::ceil(5.0f * scale);
|
|
|
|
// The frontend hands us the cut-out and home-indicator clearance on every rotation. Only one
|
|
// horizontal margin is threaded through the draw functions, so take the worse side; the bottom
|
|
// needs its own, or content anchored down there is spaced off the notch instead of the indicator.
|
|
float inset_left = 0.0f, inset_top = 0.0f, inset_right = 0.0f, inset_bottom = 0.0f;
|
|
ImGuiManager::GetOSDSafeAreaInsets(&inset_left, &inset_top, &inset_right, &inset_bottom);
|
|
const float margin = base_margin + std::max(inset_left, inset_right);
|
|
const float bottom_margin = base_margin + inset_bottom;
|
|
float position_y = base_margin + inset_top;
|
|
|
|
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, bottom_margin, spacing);
|
|
DrawSettingsOverlay(scale, margin, bottom_margin, spacing);
|
|
DrawShaderCompileIndicator(scale, margin, bottom_margin, spacing);
|
|
DrawInputsOverlay(scale, margin, bottom_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);
|
|
}
|
|
}
|