mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Custom panel background. Theme / library / black covered three of the four asks; "an own background" needed a picker. Takes the persistable read grant like the library's own picker -- without it the URI works until the process restarts and then resolves to nothing, which reads as the background disappearing on its own. Darkened by the same scrim as the library backdrop, because an arbitrary photo has no obligation to be dark and tile text still has to be readable. Clock and battery move into a status bar across the top instead of being two grid cells. Same information, but it stops the clock competing for space with the things you actually press, and the grid gets two cells back. The rest of the in-game OSD's figures reach the panel: VPS, EE / GS / GPU load and frame time. These were not missing by choice -- getFPS() was the only figure with a way across the JNI boundary, so the panel could show frames and a percentage of nominal and nothing else. PerformanceMetrics already computed all of it for the overlay. Each getter returns 0 with no VM rather than the last value, so an idle panel reads as idle instead of frozen on whatever the last game was doing. Tile height is now settable. Columns already decided width -- tiles split the row equally, so choosing columns IS choosing width, and a second width control would only be a way to disagree with it. Height had no control at all, which is why a panel could only ever be as tall as its text. A display can be told to stay out of it. "The second screen also still appears on the external monitor when connected via usbc" is not a bug by the display-picking rule -- a USB-C monitor is a perfectly good second display -- so this records a preference instead of guessing: the panel's own Not-this- screen tile drops the display it is on, and settings can re-enable them. Keyed by display NAME, since ids are reassigned across replugs. Guessing from internal-vs-external would have been wrong anyway; Android has no stable public display type before API 34. Device temperatures on the performance overlay, which is where they were asked for. The core cannot read a temperature -- there is no portable API, and on Android the only route is a vendor-specific sysfs the app layer already discovers for the panel -- so the app pushes the values in and the overlay draws what it was given. Atomics because the writer is a UI-thread poll and the reader is the GS thread. A sensor that could not be read is omitted rather than drawn as a zero.
2238 lines
85 KiB
C++
2238 lines
85 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 <string>
|
|
#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_lsfg_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
|
|
|
|
#ifdef ENABLE_VULKAN
|
|
// Declared here rather than reached through GSLsfg.h, which includes VKLoader.h: that drags the
|
|
// Vulkan headers — and under X11, all of Xlib's macros, which is why VKLoader has to #undef None
|
|
// and Status — into a cross-platform translation unit that wants one string out of it.
|
|
namespace GSLsfg
|
|
{
|
|
std::string GetStatusText();
|
|
}
|
|
#endif
|
|
|
|
/// Frame generation's own status line, or empty when the user has not switched it on. Behind a
|
|
/// function so the draw code below carries no #ifdef: LSFG lives in the Vulkan backend and there
|
|
/// is nothing to ask in a build without one.
|
|
static std::string LsfgStatusText()
|
|
{
|
|
#ifdef ENABLE_VULKAN
|
|
return GSLsfg::GetStatusText();
|
|
#else
|
|
return {};
|
|
#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
|
|
// Frame generation gets its OWN bit rather than riding on one of the toggles above, for two
|
|
// reasons: it is driven by the LSFG setting and not by any OsdShow* flag, and the shrink-to-fit
|
|
// below keys off this word changing — sharing a bit would leave the block sized for the wrong
|
|
// set of lines the moment the LSFG line appeared or went away.
|
|
const std::string lsfg_status = LsfgStatusText();
|
|
enabled_lines |= static_cast<u32>(!lsfg_status.empty()) << 12;
|
|
if (enabled_lines == 0)
|
|
return;
|
|
|
|
// Not cached behind the 100ms refresh like the lines below it. It is already built once per
|
|
// frame for the bit above, and caching it would leave the line on screen for up to a refresh
|
|
// after frame generation was switched off.
|
|
s_lsfg_line.assign(lsfg_status);
|
|
|
|
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__)
|
|
if (const u32 skip = GSGetManualFrameSkip(); skip > 0)
|
|
s_speed_line.append_format("{}SKIP: {}", s_speed_line.empty() ? "" : " | ", skip);
|
|
|
|
// Device thermals, pushed in from the Android side (Cotcho: "temp sensor on
|
|
// applicable device as part of stats OSD"). The core cannot read them itself --
|
|
// there is no portable API, and on Android the only route is a vendor-specific
|
|
// sysfs the app layer already discovers. So this draws what it was given and knows
|
|
// nothing about where it came from; a sensor that could not be read is simply
|
|
// absent rather than shown as a zero.
|
|
if (Armsx2Thermals::show.load(std::memory_order_relaxed))
|
|
{
|
|
const float cpu_t = Armsx2Thermals::cpu.load(std::memory_order_relaxed);
|
|
const float gpu_t = Armsx2Thermals::gpu.load(std::memory_order_relaxed);
|
|
const float bat_t = Armsx2Thermals::battery.load(std::memory_order_relaxed);
|
|
if (cpu_t > ARMSX2_THERMAL_NONE)
|
|
s_speed_line.append_format("{}CPU {:.0f}\xc2\xb0", s_speed_line.empty() ? "" : " | ", cpu_t);
|
|
if (gpu_t > ARMSX2_THERMAL_NONE)
|
|
s_speed_line.append_format("{}GPU {:.0f}\xc2\xb0", s_speed_line.empty() ? "" : " | ", gpu_t);
|
|
if (bat_t > ARMSX2_THERMAL_NONE)
|
|
s_speed_line.append_format("{}BAT {:.0f}\xc2\xb0", s_speed_line.empty() ? "" : " | ", bat_t);
|
|
}
|
|
#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 (const u32 fps_cap_milli = GSGetMaxPresentMilliFps();
|
|
fps_cap_milli > 0 && fps_cap_milli != 60000 && !GSGetPresentCapSuspended())
|
|
{
|
|
std::string fps_cap = fmt::format("{:.3f}", static_cast<double>(fps_cap_milli) / 1000.0);
|
|
while (fps_cap.back() == '0')
|
|
fps_cap.pop_back();
|
|
if (fps_cap.back() == '.')
|
|
fps_cap.pop_back();
|
|
s_speed_line.append_format(" (Cap {} FPS)", fps_cap);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Straight after the speed line because it is the same kind of number, and because a
|
|
// user comparing "the game runs at 30" with "the screen gets 60" wants them adjacent.
|
|
if (!s_lsfg_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_lsfg_line.c_str(), OsdTextColor());
|
|
|
|
#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.empty() ? info.name : "Unknown");
|
|
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.Renderer != GSRendererType::SW)
|
|
{
|
|
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 (!s_lsfg_line.empty())
|
|
DRAW_LINE(osd_font, font_size, s_lsfg_line.c_str(), OsdTextColor());
|
|
|
|
#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 room for the legend.
|
|
const float legend_margin = ImGui::GetTextLineHeightWithSpacing() * 4.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__
|
|
// Device temperatures, written by the Android app layer and read by the perf overlay above.
|
|
// Atomics because the writer is a UI-thread poll and the reader is the GS thread; relaxed
|
|
// because these are three independent display values with no ordering relationship to
|
|
// anything -- a torn read would at worst show one stale number for one frame.
|
|
namespace Armsx2Thermals
|
|
{
|
|
std::atomic<float> cpu{ARMSX2_THERMAL_NONE};
|
|
std::atomic<float> gpu{ARMSX2_THERMAL_NONE};
|
|
std::atomic<float> battery{ARMSX2_THERMAL_NONE};
|
|
std::atomic<bool> show{false};
|
|
} // namespace Armsx2Thermals
|
|
|
|
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);
|
|
}
|
|
}
|