From 9ec7ddb4f2cf6a20d51493ff4434c8fc29ec48a9 Mon Sep 17 00:00:00 2001 From: FakeMichau <49685661+FakeMichau@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:34:29 +0200 Subject: [PATCH] Adjust. make more configurable etc --- OptiScaler.ini | 29 +- OptiScaler/Config.cpp | 303 +++++++---- OptiScaler/Config.h | 43 +- OptiScaler/menu/menu_common.cpp | 879 ++++++++++++++++++-------------- 4 files changed, 734 insertions(+), 520 deletions(-) diff --git a/OptiScaler.ini b/OptiScaler.ini index c2b47a9d..435d768b 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -908,8 +908,10 @@ FpsOverlayPosX=auto FpsOverlayPosY=auto ; FPS overlay type -; 0 = Just FPS | 1 = Simple | 2 = Detailed | 3 = Detailed + Graph | 4 Full | 5 Full + Graph | 6 Reflex timings -; 0 to 6 - Default (auto) is 0 +; Each type before Custom is a preset of the rows, Custom shows FpsOverlayRows +; 0 = Just FPS | 1 = Simple | 2 = Detailed | 3 = Detailed + Graph | 4 Full | 5 Full + Graph | 6 Low latency timings +; 7 = Custom +; 0 to 7 - Default (auto) is 5 FpsOverlayType=auto ; Shortcut key for FPS overlay @@ -928,8 +930,8 @@ FpsCycleShortcutKey=auto ; true or false - Default (auto) is false FpsOverlayHorizontal=auto -; Shows the current upscale ratio (and render -> output resolution) in the FPS overlay -; Updated in real time, so dynamic resolution changes are visible +; Shows the current upscale ratio (and render -> output resolution) in the FPS overlay types from Simple to +; Low latency timings, Custom has the upscale row for it. Updated in real time, so dynamic resolution changes are visible ; true or false - Default (auto) is false FpsOverlayShowRatio=auto @@ -937,10 +939,15 @@ FpsOverlayShowRatio=auto ; true or false - Default (auto) is false FpsOverlayCompactLabels=auto -; FPS overlay row order, comma separated: fps, upscale, frametime, upscaler -; Rows left out keep their default place after the listed ones -; Default (auto) is fps,upscale,frametime,upscaler -FpsOverlayRowOrder=auto +; Rows of the Custom FPS overlay type in their order, comma separated, a row starting with - is hidden: +; fps, upscale, frametime, frametimegraph, upscaler, upscalergraph, lowlatency +; Rows left out are hidden. Default (auto) is the rows of Full + Graph +FpsOverlayRows=auto + +; What the FPS row of the Custom FPS overlay type shows with the FPS +; 0 = Just FPS | 1 = Frame time | 2 = Average FPS - Default (auto) is 2 +; The API, frame generation and upscaler are shown with 1 and 2 +FpsOverlayFpsRow=auto ; Space between the FPS overlay rows in pixels, before FpsScale ; 0.0 to 32.0 - Default (auto) is twice the item spacing @@ -977,14 +984,14 @@ FpsOverlayLabelScale=auto FpsOverlayValueScale=auto ; FPS Overlay colors as 0xRRGGBB, used when OverlaysUseTheme is false -; Default (auto) is 0xffffff for labels and values, 0x00ff00 for graph and 0x000000 for background +; Default (auto) is 0xd9d9d9 for labels, 0xffffff for values, 0x00ff00 for graph and 0x000000 for background FpsOverlayLabelColor=auto FpsOverlayValueColor=auto FpsOverlayGraphColor=auto FpsOverlayBGColor=auto ; FPS Overlay text shadow -; 0 = Off | 1 = Shadow | 2 = Outline - Default (auto) is 0 +; 0 = Off | 1 = Shadow | 2 = Outline - Default (auto) is 1 FpsOverlayShadow=auto ; FPS Overlay text shadow thickness and darkness, outline thickness @@ -996,7 +1003,7 @@ FpsOverlayShadowStrength=auto FpsOverlayShadowColor=auto ; FPS Overlay corner radius in pixels, 0 is square -; 0.0 to 16.0 - Default (auto) is the same as menu +; 0.0 to 16.0 - Default (auto) is 4.0 FpsOverlayRounding=auto ; Shortcut key for FG enabled/disabled diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index 8c7a3d50..b7a6e96b 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -464,6 +464,7 @@ bool Config::Reload(std::filesystem::path iniPath) OverlayMenu.set_from_config(readBool("Menu", "OverlayMenu")); ShortcutKey.set_from_config(readInt("Menu", "ShortcutKey")); ExtendedLimits.set_from_config(readBool("Menu", "ExtendedLimits")); + ShowFps.set_from_config(readBool("Menu", "ShowFps")); UseHQFont.set_from_config(readBool("Menu", "UseHQFont")); DisableSplash.set_from_config(readBool("Menu", "DisableSplash")); @@ -877,41 +878,43 @@ std::string GetFloatValue(std::optional value) return std::to_string(value.value()); } -// FPS overlay settings, in the Menu section -static const char* overlayIniKeys[] = { - "ShowFps", - "OverlaysUseTheme", - "FpsOverlayPos", - "FpsOverlayType", - "FpsOverlayHorizontal", - "FpsOverlayShowRatio", - "FpsOverlayCompactLabels", - "FpsOverlayRowOrder", - "FpsOverlayRowSpacing", - "FpsOverlayUpdateInterval", - "FpsOverlayAlpha", - "FpsScale", - "FpsOverlayGraphHeight", - "FpsOverlayPosX", - "FpsOverlayPosY", - "FpsOverlayFont", - "FpsOverlayFontSize", - "FpsOverlayLetterSpacing", - "FpsOverlayLabelScale", - "FpsOverlayValueScale", - "FpsOverlayLabelColor", - "FpsOverlayValueColor", - "FpsOverlayGraphColor", - "FpsOverlayBGColor", - "FpsOverlayShadow", - "FpsOverlayShadowStrength", - "FpsOverlayShadowColor", - "FpsOverlayRounding", +// How a key's value is packed in a settings code +enum class OverlayValueType +{ + Bool, + Index, + Float, + Color, + Text, +}; + +struct OverlayIniKey +{ + const char* name; + OverlayValueType type; +}; + +// FPS overlay settings, in the Menu section. Codes refer to a key by its place here, so new keys go at the end. +static const OverlayIniKey overlayIniKeys[] = { + { "OverlaysUseTheme", OverlayValueType::Bool }, { "FpsOverlayPos", OverlayValueType::Index }, + { "FpsOverlayType", OverlayValueType::Index }, { "FpsOverlayHorizontal", OverlayValueType::Bool }, + { "FpsOverlayShowRatio", OverlayValueType::Bool }, { "FpsOverlayCompactLabels", OverlayValueType::Bool }, + { "FpsOverlayRows", OverlayValueType::Text }, { "FpsOverlayFpsRow", OverlayValueType::Index }, + { "FpsOverlayRowSpacing", OverlayValueType::Float }, { "FpsOverlayUpdateInterval", OverlayValueType::Float }, + { "FpsOverlayAlpha", OverlayValueType::Float }, { "FpsScale", OverlayValueType::Float }, + { "FpsOverlayGraphHeight", OverlayValueType::Float }, { "FpsOverlayPosX", OverlayValueType::Float }, + { "FpsOverlayPosY", OverlayValueType::Float }, { "FpsOverlayFont", OverlayValueType::Text }, + { "FpsOverlayFontSize", OverlayValueType::Float }, { "FpsOverlayLetterSpacing", OverlayValueType::Float }, + { "FpsOverlayLabelScale", OverlayValueType::Float }, { "FpsOverlayValueScale", OverlayValueType::Float }, + { "FpsOverlayLabelColor", OverlayValueType::Color }, { "FpsOverlayValueColor", OverlayValueType::Color }, + { "FpsOverlayGraphColor", OverlayValueType::Color }, { "FpsOverlayBGColor", OverlayValueType::Color }, + { "FpsOverlayShadow", OverlayValueType::Index }, { "FpsOverlayShadowStrength", OverlayValueType::Float }, + { "FpsOverlayShadowColor", OverlayValueType::Color }, { "FpsOverlayRounding", OverlayValueType::Float }, }; void Config::ReadOverlaySettings(bool assign) { - // Loading only fills what isn't set, a pasted code replaces it + // Loading the ini fills only what isn't set yet, applying a code replaces the values auto apply = [assign](auto& setting, const auto& value) { if (assign) @@ -926,11 +929,9 @@ void Config::ReadOverlaySettings(bool assign) auto readColor = [&](const char* key) { return readUInt("Menu", key).transform([](uint32_t value) { return value & 0xFFFFFF; }); }; - // An index into an enum, the last one when it's past the end auto readIndex = [&](const char* key, uint32_t count) { return readUInt("Menu", key).transform([=](uint32_t value) { return std::min(value, count - 1); }); }; - apply(ShowFps, readBool("Menu", "ShowFps")); apply(OverlaysUseTheme, readBool("Menu", "OverlaysUseTheme")); apply(FpsOverlayPosition, readIndex("FpsOverlayPos", FpsOverlayPos_COUNT) .transform([](uint32_t value) { return (FpsOverlayPos) value; })); @@ -939,7 +940,9 @@ void Config::ReadOverlaySettings(bool assign) apply(FpsOverlayHorizontal, readBool("Menu", "FpsOverlayHorizontal")); apply(FpsOverlayShowRatio, readBool("Menu", "FpsOverlayShowRatio")); apply(FpsOverlayCompactLabels, readBool("Menu", "FpsOverlayCompactLabels")); - apply(FpsOverlayRowOrder, readString("Menu", "FpsOverlayRowOrder", true)); + apply(FpsOverlayRows, readString("Menu", "FpsOverlayRows", true)); + apply(FpsOverlayFpsRowType, readIndex("FpsOverlayFpsRow", FpsOverlayFpsRow_COUNT) + .transform([](uint32_t value) { return (FpsOverlayFpsRow) value; })); apply(FpsOverlayRowSpacing, readClamped("FpsOverlayRowSpacing", 0.0f, 32.0f)); apply(FpsOverlayUpdateInterval, readClamped("FpsOverlayUpdateInterval", 0.0f, 1000.0f)); apply(FpsOverlayAlpha, readClamped("FpsOverlayAlpha", 0.0f, 1.0f)); @@ -965,14 +968,14 @@ void Config::ReadOverlaySettings(bool assign) void Config::WriteOverlaySettings() { - ini.SetValue("Menu", "ShowFps", GetBoolValue(ShowFps.value_for_config()).c_str()); ini.SetValue("Menu", "OverlaysUseTheme", GetBoolValue(OverlaysUseTheme.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayPos", GetIntValue(FpsOverlayPosition.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayType", GetIntValue(FpsOverlayType.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayHorizontal", GetBoolValue(FpsOverlayHorizontal.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayShowRatio", GetBoolValue(FpsOverlayShowRatio.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayCompactLabels", GetBoolValue(FpsOverlayCompactLabels.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsOverlayRowOrder", FpsOverlayRowOrder.value_for_config_or("auto").c_str()); + ini.SetValue("Menu", "FpsOverlayRows", FpsOverlayRows.value_for_config_or("auto").c_str()); + ini.SetValue("Menu", "FpsOverlayFpsRow", GetIntValue(FpsOverlayFpsRowType.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayRowSpacing", GetFloatValue(FpsOverlayRowSpacing.value_for_config()).c_str()); ini.SetValue("Menu", "FpsOverlayUpdateInterval", GetFloatValue(FpsOverlayUpdateInterval.value_for_config()).c_str()); @@ -997,7 +1000,8 @@ void Config::WriteOverlaySettings() ini.SetValue("Menu", "FpsOverlayRounding", GetFloatValue(FpsOverlayRounding.value_for_config()).c_str()); } -// The code: a versioned prefix and the changed keys as base64 text, the keys without the FpsOverlay prefix +// The code: a versioned prefix and, as base64, the keys that differ from their defaults, each as its place in +// overlayIniKeys and its value packed static constexpr std::string_view overlayCodePrefix = "OptiOverlay1:"; static constexpr std::string_view overlayKeyPrefix = "FpsOverlay"; @@ -1009,10 +1013,10 @@ static std::string_view OverlayCodeKey(std::string_view key) return key; } +static constexpr std::string_view base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static std::string Base64Encode(std::string_view data) { - static constexpr char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; uint32_t bits = 0; int count = 0; @@ -1022,22 +1026,18 @@ static std::string Base64Encode(std::string_view data) bits = (bits << 8) | c; count += 8; - while (count >= 6) - { - count -= 6; - out += table[(bits >> count) & 0x3F]; - } + for (; count >= 6; count -= 6) + out += base64Chars[(bits >> (count - 6)) & 0x3F]; } + // Without the padding, the decoder doesn't need it if (count > 0) - out += table[(bits << (6 - count)) & 0x3F]; - - while (out.size() % 4 != 0) - out += '='; + out += base64Chars[(bits << (6 - count)) & 0x3F]; return out; } +// Skips whitespace, so a code a chat wrapped over lines still decodes, and the padding static std::optional Base64Decode(std::string_view data) { std::string out; @@ -1046,133 +1046,219 @@ static std::optional Base64Decode(std::string_view data) for (char c : data) { - int value; + if (c == '=' || std::isspace((unsigned char) c)) + continue; - if (c >= 'A' && c <= 'Z') - value = c - 'A'; - else if (c >= 'a' && c <= 'z') - value = c - 'a' + 26; - else if (c >= '0' && c <= '9') - value = c - '0' + 52; - else if (c == '+') - value = 62; - else if (c == '/') - value = 63; - else if (c == '=') - break; - else + const auto value = base64Chars.find(c); + if (value == std::string_view::npos) return std::nullopt; - bits = (bits << 6) | value; + bits = (bits << 6) | static_cast(value); count += 6; if (count >= 8) { count -= 8; - out += (char) ((bits >> count) & 0xFF); + out += static_cast((bits >> count) & 0xFF); } } return out; } +// A value as the ini has it, packed by its type: a byte for a bool or an index, three for a color, four for a float, +// a text as its length and characters +static void PackOverlayValue(std::string& data, OverlayValueType type, const char* value) +{ + switch (type) + { + case OverlayValueType::Bool: + data += static_cast(lstrcmpiA(value, "true") == 0); + break; + + case OverlayValueType::Index: + data += static_cast(std::strtoul(value, nullptr, 10)); + break; + + case OverlayValueType::Color: + { + const auto rgb = std::strtoul(value, nullptr, 16); + data += static_cast(rgb >> 16); + data += static_cast(rgb >> 8); + data += static_cast(rgb); + break; + } + + case OverlayValueType::Float: + { + const float number = std::strtof(value, nullptr); + data.append(reinterpret_cast(&number), sizeof(number)); + break; + } + + case OverlayValueType::Text: + { + const size_t length = std::min(std::strlen(value), 255); + data += static_cast(length); + data.append(value, length); + break; + } + } +} + +// Packed values back as the Key=Value text, nothing for an unknown key or data that ends inside a value +static std::optional UnpackOverlayValues(std::string_view data) +{ + std::string text; + + while (!data.empty()) + { + const auto index = static_cast(data.front()); + data.remove_prefix(1); + + if (index >= std::size(overlayIniKeys)) + return std::nullopt; + + const auto& key = overlayIniKeys[index]; + + size_t size = 1; + if (key.type == OverlayValueType::Color) + size = 3; + else if (key.type == OverlayValueType::Float) + size = sizeof(float); + else if (key.type == OverlayValueType::Text && !data.empty()) + size += static_cast(data.front()); + + if (data.size() < size) + return std::nullopt; + + const auto bytes = data.substr(0, size); + data.remove_prefix(size); + + auto byte = [&](size_t i) { return static_cast(static_cast(bytes[i])); }; + + std::string value; + + switch (key.type) + { + case OverlayValueType::Bool: + value = byte(0) != 0 ? "true" : "false"; + break; + + case OverlayValueType::Index: + value = std::to_string(byte(0)); + break; + + case OverlayValueType::Color: + value = std::format("{:#08x}", (byte(0) << 16) | (byte(1) << 8) | byte(2)); + break; + + case OverlayValueType::Float: + { + float number; + std::memcpy(&number, bytes.data(), sizeof(number)); + value = std::format("{}", number); + break; + } + + case OverlayValueType::Text: + value = bytes.substr(1); + break; + } + + text += std::format("{}={};", OverlayCodeKey(key.name), value); + } + + return text; +} + std::string Config::ExportOverlaySettings() { WriteOverlaySettings(); - std::string text; + std::string data; - for (auto key : overlayIniKeys) + for (size_t i = 0; i < std::size(overlayIniKeys); i++) { - const char* value = ini.GetValue("Menu", key, "auto"); + const char* value = ini.GetValue("Menu", overlayIniKeys[i].name, "auto"); if (value == nullptr || lstrcmpiA(value, "auto") == 0) continue; - text += std::format("{}={}\n", OverlayCodeKey(key), value); + data += static_cast(i); + PackOverlayValue(data, overlayIniKeys[i].type, value); } - return std::string(overlayCodePrefix) + Base64Encode(text); + return std::string(overlayCodePrefix) + Base64Encode(data); } -bool Config::ImportOverlaySettings(const std::string& code) +bool Config::ImportOverlaySettings(std::string_view code) { - std::string_view view = code; + while (!code.empty() && std::isspace((unsigned char) code.front())) + code.remove_prefix(1); - while (!view.empty() && std::isspace((unsigned char) view.front())) - view.remove_prefix(1); + while (!code.empty() && std::isspace((unsigned char) code.back())) + code.remove_suffix(1); - while (!view.empty() && std::isspace((unsigned char) view.back())) - view.remove_suffix(1); - - if (!view.starts_with(overlayCodePrefix)) + if (!code.starts_with(overlayCodePrefix)) return false; - view.remove_prefix(overlayCodePrefix.size()); + code.remove_prefix(overlayCodePrefix.size()); - auto text = Base64Decode(view); + auto data = Base64Decode(code); + if (!data.has_value()) + return false; + + auto text = UnpackOverlayValues(data.value()); if (!text.has_value()) return false; - return ApplyOverlayText(text.value()); + return ApplyOverlaySettings(text.value()); } -bool Config::ApplyOverlayPreset(std::string_view text) -{ - const auto showFps = ShowFps.value_for_config(); - - if (!ApplyOverlayText(std::string(text))) - return false; - - if (showFps.has_value()) - ShowFps = showFps.value(); - - return true; -} - -bool Config::ApplyOverlayText(const std::string& text) +bool Config::ApplyOverlaySettings(std::string_view text) { // Known keys only, their values are checked as the ini's are std::vector> values; - std::istringstream lines(text); - std::string line; + std::istringstream entries { std::string(text) }; + std::string entry; - while (std::getline(lines, line)) + while (std::getline(entries, entry, ';')) { - const auto separator = line.find('='); + const auto separator = entry.find('='); if (separator == std::string::npos) continue; - const std::string_view name = std::string_view(line).substr(0, separator); - std::string value = line.substr(separator + 1); + const std::string_view name = std::string_view(entry).substr(0, separator); + std::string value = entry.substr(separator + 1); // A font by its name in the Windows fonts folder, not a path on the computer it was copied from if (name == OverlayCodeKey("FpsOverlayFont") && value.find_first_of("\\/:") != std::string::npos) continue; - for (auto key : overlayIniKeys) + for (const auto& key : overlayIniKeys) { - if (OverlayCodeKey(key) == name) + if (OverlayCodeKey(key.name) == name) { - values.emplace_back(key, std::move(value)); + values.emplace_back(key.name, std::move(value)); break; } } } - // Nothing known in a code that isn't empty, a code of a later version or a broken one + // A code with text but no known key is broken or from a later version if (values.empty() && !text.empty()) return false; - // Everything it doesn't list back to the default - for (auto key : overlayIniKeys) - ini.SetValue("Menu", key, "auto"); + // What the code doesn't list goes back to its default + for (const auto& key : overlayIniKeys) + ini.SetValue("Menu", key.name, "auto"); for (const auto& [key, value] : values) ini.SetValue("Menu", key, value.c_str()); ReadOverlaySettings(true); - LOG_INFO("Imported {} FPS overlay settings", values.size()); + LOG_INFO("Applied {} FPS overlay settings", values.size()); return true; } @@ -1594,6 +1680,7 @@ bool Config::SaveIni() GetIntValue(Instance()->ShortcutKey.value_for_config(), setting > 0).c_str()); ini.SetValue("Menu", "ExtendedLimits", GetBoolValue(Instance()->ExtendedLimits.value_for_config()).c_str()); + ini.SetValue("Menu", "ShowFps", GetBoolValue(Instance()->ShowFps.value_for_config()).c_str()); ini.SetValue("Menu", "UseHQFont", GetBoolValue(Instance()->UseHQFont.value_for_config()).c_str()); ini.SetValue("Menu", "DisableSplash", GetBoolValue(Instance()->DisableSplash.value_for_config()).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index d9cab4cb..955735f4 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -176,10 +176,20 @@ enum FpsOverlay : uint32_t FpsOverlay_DetailedGraph, FpsOverlay_Full, FpsOverlay_FullGraph, - FpsOverlay_ReflexTimings, + FpsOverlay_LowLatencyTimings, + FpsOverlay_Custom, // FpsOverlayRows and FpsOverlayFpsRow FpsOverlay_COUNT, }; +// What the FPS row shows with the FPS +enum FpsOverlayFpsRow : uint32_t +{ + FpsOverlayFpsRow_JustFps, // Only the number + FpsOverlayFpsRow_FrameTime, // The API, frame time, frame generation and upscaler + FpsOverlayFpsRow_Average, // The API, average FPS, frame generation and upscaler + FpsOverlayFpsRow_COUNT, +}; + // Output scaling downscaler enum class Scaler : uint32_t { @@ -374,15 +384,17 @@ class Config CustomOptional FpsOverlayPosition { FpsOverlayPos_TopLeft }; /// 0 Only FPS, 1 +Avg FPS & Upscaler info 2 +Frame Time, /// 3 +Upscaler Time, 4 +Frame Time Graph, 5 +Upscaler Time Graph - /// 6 +Reflex timings - CustomOptional FpsOverlayType { FpsOverlay_JustFPS }; + /// 6 +Low latency timings, 7 Custom rows + CustomOptional FpsOverlayType { FpsOverlay_FullGraph }; CustomOptional FpsShortcutKey { VK_PRIOR }; CustomOptional FpsCycleShortcutKey { VK_NEXT }; CustomOptional FpsOverlayHorizontal { false }; CustomOptional FpsOverlayShowRatio { false }; CustomOptional FpsOverlayCompactLabels { false }; - // Comma separated row keys: fps, upscale, frametime, upscaler. No value means that order - CustomOptional FpsOverlayRowOrder; + // Rows of the Custom overlay type in their order, comma separated keys, hidden ones start with -. + // No value means the default overlay type's rows + CustomOptional FpsOverlayRows; + CustomOptional FpsOverlayFpsRowType { FpsOverlayFpsRow_Average }; // Of the Custom type CustomOptional FpsOverlayRowSpacing; // No value means the default spacing // Milliseconds the values that change every frame are kept, 0 updates them every frame CustomOptional FpsOverlayUpdateInterval { 0.0f }; @@ -400,15 +412,15 @@ class Config CustomOptional FpsOverlayLabelScale { 1.0f }; CustomOptional FpsOverlayValueScale { 1.0f }; // 0xRRGGBB, used when the overlays don't use the theme colors - CustomOptional FpsOverlayLabelColor { 0xFFFFFF }; + CustomOptional FpsOverlayLabelColor { 0xD9D9D9 }; CustomOptional FpsOverlayValueColor { 0xFFFFFF }; CustomOptional FpsOverlayGraphColor { 0x00FF00 }; CustomOptional FpsOverlayBGColor { 0x000000 }; - CustomOptional FpsOverlayShadowType { FpsOverlayShadow_Off }; + CustomOptional FpsOverlayShadowType { FpsOverlayShadow_Shadow }; CustomOptional FpsOverlayShadowStrength { 0.5f }; CustomOptional FpsOverlayShadowColor { 0x000000 }; - CustomOptional FpsOverlayRounding; // No value means same as MenuRounding - CustomOptional MenuRounding { 2.0f }; // Window corner radius, 0 is square + CustomOptional FpsOverlayRounding { 4.0f }; + CustomOptional MenuRounding { 2.0f }; CustomOptional UseHQFont { true }; CustomOptional DisableSplash { false }; CustomOptional FontSize { 14.0f }; @@ -766,14 +778,14 @@ class Config std::vector GetConfigLog(); - // The FPS overlay's settings as a code to copy, and applying a copied one: the ones that differ from the - // defaults, so they can be kept across updates and shared without another file + // The FPS overlay's settings that differ from the defaults as a code, to keep them outside the ini and share + // them without a file std::string ExportOverlaySettings(); - bool ImportOverlaySettings(const std::string& code); + bool ImportOverlaySettings(std::string_view code); - // A preset's settings, the code's text without base64: Key=Value lines, keys without the FpsOverlay prefix. - // ShowFps stays as it is. - bool ApplyOverlayPreset(std::string_view text); + // A code's settings as text, also a preset: Key=Value separated by ';', keys without the FpsOverlay prefix. + // What it doesn't list goes back to the default. + bool ApplyOverlaySettings(std::string_view text); static Config* Instance(); @@ -789,7 +801,6 @@ class Config // The FPS overlay's keys of the Menu section: read when loading the ini, or assigned for a pasted code void ReadOverlaySettings(bool assign); void WriteOverlaySettings(); - bool ApplyOverlayText(const std::string& text); // A code's text, what it doesn't list goes back to the default std::optional readString(std::string section, std::string key, bool lowercase = false); std::optional readWString(std::string section, std::string key, bool lowercase = false); diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index 7db78e35..53fea0ea 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -287,67 +287,171 @@ static const std::pair overlayFonts[] = { { "Impact", "impact.ttf" }, }; -// FPS overlay rows, FpsOverlayRowOrder lists their keys +// FPS overlay rows, FpsOverlayRows lists their keys. A graph row follows its text row. enum OverlayRow : int { OverlayRow_Fps, OverlayRow_Upscale, OverlayRow_FrameTime, + OverlayRow_FrameTimeGraph, OverlayRow_UpscalerTime, + OverlayRow_UpscalerTimeGraph, + OverlayRow_LowLatency, OverlayRow_COUNT, }; -static const char* overlayRowKeys[] = { "fps", "upscale", "frametime", "upscaler" }; -static const char* overlayRowNames[] = { "FPS", "Upscale Ratio", "Frame Time", "Upscaler Time" }; +static const char* overlayRowKeys[] = { "fps", "upscale", "frametime", "frametimegraph", + "upscaler", "upscalergraph", "lowlatency" }; +static const char* overlayRowNames[] = { + "FPS", "Upscale Ratio", "Frame Time", "Frame Time Graph", + "Upscaler Time", "Upscaler Time Graph", "Low Latency Timings" +}; +static const char* overlayRowTips[] = { + "The FPS, the frame generation and the upscaler, FPS Row below sets what's shown", + "Render -> output resolution and upscale ratio, while upscaling", + "The last frame time and the average", + "The frame times of the last frames", + "The upscaler's last time and the average", + "The upscaler's times of the last frames", + "Timings of the frame from the low latency markers, while there are any", +}; -// The configured order, rows it doesn't list keep their default place after it -static std::vector OverlayRowOrder() +struct OverlayRowEntry { - // Read on every overlay frame, parsed only when it changes - static std::string cachedKeys = "-"; - static std::vector cachedOrder; + int row; + bool shown; +}; - const auto& configKeys = Config::Instance()->FpsOverlayRowOrder.value_or(""); +// An overlay type's rows from top to bottom (left to right when horizontal), the hidden ones too so Custom keeps +// their place +static std::vector OverlayRows(FpsOverlay type) +{ + auto config = Config::Instance(); - if (configKeys == cachedKeys) - return cachedOrder; + if (type != FpsOverlay_Custom) + { + // The lowest type that shows the row, each type shows more than the one before + static constexpr FpsOverlay shownFrom[] = { + FpsOverlay_JustFPS, FpsOverlay_Simple, FpsOverlay_Detailed, FpsOverlay_DetailedGraph, + FpsOverlay_Full, FpsOverlay_FullGraph, FpsOverlay_LowLatencyTimings + }; + static_assert(std::size(shownFrom) == OverlayRow_COUNT); - std::vector order; - std::stringstream keys(configKeys); + std::vector rows; + + for (int row = 0; row < OverlayRow_COUNT; row++) + { + bool shown = type >= shownFrom[row]; + + if (row == OverlayRow_Upscale) + shown = shown && config->FpsOverlayShowRatio.value_or_default(); + + rows.push_back({ row, shown }); + } + + return rows; + } + + if (!config->FpsOverlayRows.has_value()) + return OverlayRows(FpsOverlay_FullGraph); + + std::vector rows; + std::stringstream keys(config->FpsOverlayRows.value()); std::string key; + auto listed = [&](int row) + { return std::any_of(rows.begin(), rows.end(), [row](const OverlayRowEntry& entry) { return entry.row == row; }); }; + while (std::getline(keys, key, ',')) { + std::string_view name = key; + while (!name.empty() && std::isspace((unsigned char) name.front())) + name.remove_prefix(1); + while (!name.empty() && std::isspace((unsigned char) name.back())) + name.remove_suffix(1); + + const bool shown = !name.starts_with('-'); + if (!shown) + name.remove_prefix(1); + for (int row = 0; row < OverlayRow_COUNT; row++) { - if (key == overlayRowKeys[row] && std::find(order.begin(), order.end(), row) == order.end()) - order.push_back(row); + if (name == overlayRowKeys[row] && !listed(row)) + rows.push_back({ row, shown }); } } for (int row = 0; row < OverlayRow_COUNT; row++) { - if (std::find(order.begin(), order.end(), row) == order.end()) - order.push_back(row); + if (!listed(row)) + rows.push_back({ row, false }); } - cachedKeys = configKeys; - cachedOrder = order; - - return order; + return rows; } -static void SetOverlayRowOrder(const std::vector& order) +static void SetOverlayRows(const std::vector& rows) { std::string keys; - for (auto row : order) - keys += (keys.empty() ? "" : ",") + std::string(overlayRowKeys[row]); + for (const auto& entry : rows) + keys += (keys.empty() ? "" : ",") + std::string(entry.shown ? "" : "-") + overlayRowKeys[entry.row]; - if (std::is_sorted(order.begin(), order.end())) - Config::Instance()->FpsOverlayRowOrder.reset(); - else - Config::Instance()->FpsOverlayRowOrder = keys; + Config::Instance()->FpsOverlayRows = keys; +} + +static FpsOverlayFpsRow OverlayFpsRow(FpsOverlay type) +{ + switch (type) + { + case FpsOverlay_JustFPS: + return FpsOverlayFpsRow_JustFps; + + case FpsOverlay_Simple: + return FpsOverlayFpsRow_FrameTime; + + case FpsOverlay_Custom: + return Config::Instance()->FpsOverlayFpsRowType.value_or_default(); + + default: + return FpsOverlayFpsRow_Average; + } +} + +// The digits each number of an FPS overlay value needs to keep the most it had this session, so values start as far +// left as they can and don't move back and forth as they change +static std::vector OverlayValuePadding(const char* id, std::string_view text) +{ + static std::unordered_map> mostDigits; + auto& most = mostDigits[id]; + + std::vector padding; + size_t i = 0; + + while (i < text.size()) + { + if (text[i] < '0' || text[i] > '9') + { + i++; + continue; + } + + const size_t start = i; + while (i < text.size() && text[i] >= '0' && text[i] <= '9') + i++; + + const int digits = static_cast(i - start); + const size_t number = padding.size(); + + if (number == most.size()) + most.push_back(digits); + else + most[number] = std::max(most[number], digits); + + padding.push_back(most[number] - digits); + } + + return padding; } // FPS overlay font, loaded between frames when FpsOverlayFont or its rasterizer settings change @@ -2488,7 +2592,6 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) if (config->ShowFps.value_or_default()) { bool stylePushed = false; - bool overlayFontPushed = false; const static auto defaultStyle = ImGuiStyle(); @@ -2507,10 +2610,7 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) stylePushed = true; } - // Corners, the menu's unless set - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, config->FpsOverlayRounding.has_value() - ? config->FpsOverlayRounding.value() * fpsScale - : ImGui::GetStyle().WindowRounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, config->FpsOverlayRounding.value_or_default() * fpsScale); // Set overlay position ImGui::SetNextWindowPos(overlayPosition, ImGuiCond_Always); @@ -2636,8 +2736,6 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) else ImGui::PushFontSize(overlayFontPx); - overlayFontPushed = true; - auto fg = state.CurrentFG(); auto fgText = (fg != nullptr && fg->IsActive() && !fg->IsPaused()) ? std::string(fg->Name()) : std::string(); @@ -2685,15 +2783,19 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) std::string text; bool value = false; float width = 0.0f; + std::vector padding; // Digits in front of each number of a value }; struct OverlayLine { std::vector parts; ImVec2 size { 0.0f, 0.0f }; + int row = OverlayRow_Fps; + // A graph row has no parts, nor the low latency row const char* graphId = nullptr; RingBuffer* graphData = nullptr; float graphMax = 0.0f; + bool lowLatency = false; OverlayLine& Label(std::string text) { @@ -2703,14 +2805,15 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) return *this; } - OverlayLine& Value(std::string text) + // The id keeps the value's widest width across frames + OverlayLine& Value(const char* id, std::string text) { - parts.push_back({ std::move(text), true }); + auto padding = OverlayValuePadding(id, text); + parts.push_back({ std::move(text), true, 0.0f, std::move(padding) }); return *this; } }; - // Compact labels for a narrower overlay const bool compact = config->FpsOverlayCompactLabels.value_or_default(); auto L = [compact](const char* full, const char* shorter) { return std::string(compact ? shorter : full); }; @@ -2769,25 +2872,45 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) // With frame generation also the base frame rate const std::string fpsValue = interpolatedFrameCount - ? StrFmt("%6.1f/%5.1f", shown.frameRate, shown.frameRate / (double) (interpolatedFrameCount + 1)) - : StrFmt("%6.1f", shown.frameRate); + ? StrFmt("%.1f/%.1f", shown.frameRate, shown.frameRate / (double) (interpolatedFrameCount + 1)) + : StrFmt("%.1f", shown.frameRate); + + const auto overlayRows = OverlayRows(overlayType); + std::array rowShown {}; + + for (const auto& entry : overlayRows) + rowShown[entry.row] = entry.shown; + + // Low latency timings, polled while their row is shown + constexpr auto delayBetweenPollsMs = 500; + static auto previousPoll = 0.0; + static bool gotData = false; + static TimingData timingData {}; + + if (rowShown[OverlayRow_LowLatency] && (previousPoll <= 0.001 || previousPoll + delayBetweenPollsMs < now)) + { + gotData = InputCommon::get_timing_data(timingData); + previousPoll = now; + } std::array, OverlayRow_COUNT> rows; - // FPS with the frame time (Simple) or the average, the frame generation and the upscaler - if (overlayType == FpsOverlay_JustFPS) + const auto fpsRow = OverlayFpsRow(overlayType); + + if (fpsRow == FpsOverlayFpsRow_JustFps) { - rows[OverlayRow_Fps] = OverlayLine().Value(fpsValue).Label(" "); + rows[OverlayRow_Fps] = OverlayLine().Value("fps", fpsValue).Label(" "); } else { OverlayLine line; - line.Label(api + L(" | FPS: ", " | FPS ")).Value(fpsValue); + line.Label(api + L(" | FPS: ", " | FPS ")).Value("fps", fpsValue); - if (overlayType == FpsOverlay_Simple) - line.Label(", ").Value(StrFmt("%7.2f ms", shown.frameTime)); + if (fpsRow == FpsOverlayFpsRow_FrameTime) + line.Label(", ").Value("fpsFrameTime", StrFmt("%.2f ms", shown.frameTime)); else - line.Label(L(", Avg: ", " Avg ")).Value(StrFmt("%6.1f", 1000.0 / shown.averageFrameTime)); + line.Label(L(", Avg: ", " Avg ")) + .Value("fpsAverage", StrFmt("%.1f", 1000.0 / shown.averageFrameTime)); if (!fgText.empty()) line.Label(" (" + fgText + ")"); @@ -2798,64 +2921,62 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) rows[OverlayRow_Fps] = std::move(line); } - if (overlayType >= FpsOverlay_Simple && hasFeature && config->FpsOverlayShowRatio.value_or_default() && - currentFeature->RenderWidth() > 0) + if (hasFeature && currentFeature->RenderWidth() > 0) { const float ratio = (float) currentFeature->TargetWidth() / (float) currentFeature->RenderWidth(); OverlayLine line; line.Label(L("Upscale: ", "UPS ")) - .Value(StrFmt("%ux%u -> %ux%u (%.2fx)", currentFeature->RenderWidth(), - currentFeature->RenderHeight(), currentFeature->TargetWidth(), - currentFeature->TargetHeight(), ratio)); + .Value("upscale", StrFmt("%ux%u -> %ux%u (%.2fx)", currentFeature->RenderWidth(), + currentFeature->RenderHeight(), currentFeature->TargetWidth(), + currentFeature->TargetHeight(), ratio)); rows[OverlayRow_Upscale] = std::move(line); } - if (overlayType >= FpsOverlay_Detailed) + auto graphRow = [](const char* id, RingBuffer* data, float max) { OverlayLine line; - line.Label(L("Frame Time: ", "FT ")) - .Value(StrFmt("%7.2f ms", shown.lastFrameTime)) + line.graphId = id; + line.graphData = data; + line.graphMax = max; + + return line; + }; + + rows[OverlayRow_FrameTime] = OverlayLine() + .Label(L("Frame Time: ", "FT ")) + .Value("frameTime", StrFmt("%.2f ms", shown.lastFrameTime)) + .Label(L(", Avg: ", " Avg ")) + .Value("frameTimeAverage", StrFmt("%.2f ms", shown.averageFrameTime)); + + rows[OverlayRow_FrameTimeGraph] = graphRow("##FrameTimeGraph", &gFrameTimes, 66.6f); + + rows[OverlayRow_UpscalerTime] = + OverlayLine() + .Label(L("Upscaler Time: ", "UT ")) + .Value("upscalerTime", StrFmt("%.2f ms", shown.lastUpscalerTime)) .Label(L(", Avg: ", " Avg ")) - .Value(StrFmt("%7.2f ms", shown.averageFrameTime)); + .Value("upscalerTimeAverage", StrFmt("%.2f ms", shown.averageUpscalerTime)); - if (overlayType >= FpsOverlay_DetailedGraph) - { - line.graphId = "##FrameTimeGraph"; - line.graphData = &gFrameTimes; - line.graphMax = 66.6f; - } + rows[OverlayRow_UpscalerTimeGraph] = graphRow("##UpscalerFrameTimeGraph", &gUpscalerTimes, 20.0f); - rows[OverlayRow_FrameTime] = std::move(line); - } - - if (overlayType >= FpsOverlay_Full) + if (gotData && timingData.timeRange.has_value()) { - OverlayLine line; - line.Label(L("Upscaler Time: ", "UT ")) - .Value(StrFmt("%7.2f ms", shown.lastUpscalerTime)) - .Label(L(", Avg: ", " Avg ")) - .Value(StrFmt("%7.2f ms", shown.averageUpscalerTime)); - - if (overlayType >= FpsOverlay_FullGraph) - { - line.graphId = "##UpscalerFrameTimeGraph"; - line.graphData = &gUpscalerTimes; - line.graphMax = 20.0f; - } - - rows[OverlayRow_UpscalerTime] = std::move(line); + rows[OverlayRow_LowLatency] = OverlayLine(); + rows[OverlayRow_LowLatency]->lowLatency = true; } std::vector lines; - for (auto row : OverlayRowOrder()) + for (const auto& entry : overlayRows) { - if (rows[row].has_value()) - lines.push_back(std::move(rows[row].value())); + if (entry.shown && rows[entry.row].has_value()) + { + lines.push_back(std::move(rows[entry.row].value())); + lines.back().row = entry.row; + } } - // Labels and values are scaled from the overlay's font size ImFont* font = ImGui::GetFont(); const float basePx = ImGui::GetFontSize(); const float labelPx = std::round(basePx * config->FpsOverlayLabelScale.value_or_default()); @@ -2906,13 +3027,15 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) digitWidth = std::max(digitWidth, font->CalcTextSizeA(valuePx, FLT_MAX, 0.0f, &digit, &digit + 1).x); // Calls place(begin, end, x) for the part's text, the value's digits one at a time centered in their - // width. The part's width. + // width and each number after its padding. The part's width. auto layoutPart = [&](const OverlayPart& part, auto&& place) { const float px = partPx(part); - const char* text = part.text.c_str(); + const char* begin = part.text.c_str(); + const char* text = begin; const char* end = text + part.text.size(); float x = 0.0f; + size_t number = 0; auto isDigit = [&](const char* c) { return part.value && *c >= '0' && *c <= '9'; }; @@ -2920,6 +3043,9 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) { if (isDigit(text)) { + if ((text == begin || !isDigit(text - 1)) && number < part.padding.size()) + x += part.padding[number++] * digitWidth; + const float width = font->CalcTextSizeA(px, FLT_MAX, 0.0f, text, text + 1).x; place(text, text + 1, x + std::round((digitWidth - width) * 0.5f)); x += digitWidth; @@ -2979,15 +3105,25 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) } }; + auto lineAscent = [&](const OverlayLine& line) + { + float ascent = 0.0f; + for (const auto& part : line.parts) + ascent = std::max(ascent, font->GetFontBaked(partPx(part))->Ascent); + + return ascent; + }; + + // For a horizontal overlay, where the lines side by side share one baseline + float sharedAscent = 0.0f; + // Parts of different sizes share the baseline auto drawLine = [&](OverlayLine& line) { if (line.size.y == 0.0f && !line.parts.empty()) measureLine(line); - float maxAscent = 0.0f; - for (const auto& part : line.parts) - maxAscent = std::max(maxAscent, font->GetFontBaked(partPx(part))->Ascent); + const float maxAscent = std::max(lineAscent(line), sharedAscent); auto drawList = ImGui::GetWindowDrawList(); const auto start = ImGui::GetCursorScreenPos(); @@ -3012,7 +3148,7 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) } } - ImGui::Dummy(line.size); + ImGui::Dummy(ImVec2(line.size.x, line.size.y + maxAscent - lineAscent(line))); }; const bool horizontal = config->FpsOverlayHorizontal.value_or_default(); @@ -3039,13 +3175,79 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) ? config->FpsOverlayRowSpacing.value() * fpsScale : itemSpacingY * 2.0f; - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + // The timings as lines of text, as wide as the graphs so their bars fit + const ImVec2 itemSpacing = ImGui::GetStyle().ItemSpacing; + + auto drawLowLatency = [&]() + { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, itemSpacing); + ImGui::BeginGroup(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + constexpr float offsetForText = 155; + + const auto& rangeInNs = timingData.timeRange.value().length; + + UINT64 localFrameCount = 0; + + if (fg != nullptr) + localFrameCount = fg->FrameCount(); + + ImGui::Text("FGId: %llu, RfxId: %llu", localFrameCount, state.reflexFrameId); + ImGui::Text("Low latency timings, whole frame: %.1fms", rangeInNs / 1000.0); + + const auto maxWidth = horizontal ? fpsScale * 300.0f : plotSize.x; + + const auto drawTiming = [&](const auto& timingOpt, const char* desc, ImVec4 color) + { + if (!timingOpt.has_value()) + return; + + auto toneMappedColor = toneMapColor(color); + + const auto& timing = timingOpt.value(); + float duration = static_cast(timing.length * rangeInNs / 1000.0); + + ImGui::TextColored(toneMappedColor, "%-12s %4.1fms", desc, duration); + + auto leftLimit = ImGui::GetItemRectMin().x + offsetForText * fpsScale; + + auto start = static_cast(leftLimit + (ImGui::GetItemRectMin().x + maxWidth - leftLimit) * + timing.position); + + auto end = + static_cast(start + (ImGui::GetItemRectMin().x + maxWidth - leftLimit) * timing.length); + + auto pos = ImVec2(start, ImGui::GetItemRectMin().y); + auto size = ImVec2(end, ImGui::GetItemRectMax().y); + + drawList->AddRectFilled(pos, size, ImGui::ColorConvertFloat4ToU32(toneMappedColor)); + }; + + drawTiming(timingData.simulation, "Simulation", ImVec4(0.768f, 0.169f, 0.169f, 1.0f)); + drawTiming(timingData.renderSubmit, "RenderSubmit", ImVec4(0.235f, 0.705f, 0.294f, 1.0f)); + drawTiming(timingData.present, "Present", ImVec4(1.0f, 0.88f, 0.098f, 1.0f)); + drawTiming(timingData.driver, "Driver", ImVec4(0.263f, 0.388f, 0.847f, 1.0f)); + drawTiming(timingData.osRenderQueue, "RenderQueue", ImVec4(0.76f, 0.51f, 0.188f, 1.0f)); + drawTiming(timingData.gpuRender, "GpuRender", ImVec4(0.569f, 0.117f, 0.705f, 1.0f)); + + // The bars are drawn without taking space + ImGui::Dummy(ImVec2(maxWidth, 0.0f)); + + ImGui::EndGroup(); + ImGui::PopStyleVar(); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(itemSpacing.x, 0.0f)); OverlayLine sepLine; if (horizontal) { sepLine.Label(" | "); measureLine(sepLine); + + for (const auto& line : lines) + sharedAscent = std::max(sharedAscent, lineAscent(line)); } // Draw the overlay @@ -3053,107 +3255,50 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) { auto& line = lines[i]; + // A graph right after its text row stays with it + const bool attached = line.graphData != nullptr && i > 0 && lines[i - 1].row == line.row - 1; + if (i > 0) { if (horizontal) { ImGui::SameLine(0.0f, 0.0f); - drawLine(sepLine); - ImGui::SameLine(0.0f, 0.0f); + + if (!attached) + { + drawLine(sepLine); + ImGui::SameLine(0.0f, 0.0f); + } } else { - ImGui::Dummy(ImVec2(0.0f, rowSpacing)); + ImGui::Dummy(ImVec2(0.0f, attached ? itemSpacingY : rowSpacing)); } } - drawLine(line); - if (line.graphData != nullptr) { - if (horizontal) - ImGui::SameLine(0.0f, 0.0f); - else - ImGui::Dummy(ImVec2(0.0f, itemSpacingY)); - ImGui::PlotLines( line.graphId, [](void* rb, int idx) -> float { return static_cast*>(rb)->At(idx); }, line.graphData, plotWidth, 0, nullptr, 0.0f, line.graphMax, plotSize); } + else if (line.lowLatency) + { + drawLowLatency(); + } + else + { + drawLine(line); + } } ImGui::PopStyleVar(); - if (overlayType >= FpsOverlay_ReflexTimings) - ImGui::Spacing(); - - if (config->FpsOverlayType.value_or_default() >= FpsOverlay_ReflexTimings) - { - constexpr auto delayBetweenPollsMs = 500; - static auto previousPoll = 0.0; - static bool gotData = false; - - static TimingData timingData {}; - - if (previousPoll <= 0.001 || previousPoll + delayBetweenPollsMs < now) - { - gotData = InputCommon::get_timing_data(timingData); - previousPoll = now; - } - - if (gotData && timingData.timeRange.has_value()) - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - constexpr float offsetForText = 155; - - const auto& rangeInNs = timingData.timeRange.value().length; - - UINT64 localFrameCount = 0; - - if (fg != nullptr) - localFrameCount = fg->FrameCount(); - - ImGui::Text("FGId: %llu, RfxId: %llu", localFrameCount, state.reflexFrameId); - ImGui::Text("Low latency timings, whole frame: %.1fms", rangeInNs / 1000.0); - - const auto maxWidth = - config->FpsOverlayHorizontal.value_or_default() ? ImGui::GetWindowWidth() : plotSize.x; - - const auto drawTiming = [&](const auto& timingOpt, const char* desc, ImVec4 color) - { - if (!timingOpt.has_value()) - return; - - auto toneMappedColor = toneMapColor(color); - - const auto& timing = timingOpt.value(); - float duration = static_cast(timing.length * rangeInNs / 1000.0); - - ImGui::TextColored(toneMappedColor, "%-12s %4.1fms", desc, duration); - - auto leftLimit = ImGui::GetItemRectMin().x + offsetForText * fpsScale; - - auto start = static_cast(leftLimit + (ImGui::GetItemRectMin().x + maxWidth - leftLimit) * - timing.position); - - auto end = static_cast(start + (ImGui::GetItemRectMin().x + maxWidth - leftLimit) * - timing.length); - - auto pos = ImVec2(start, ImGui::GetItemRectMin().y); - auto size = ImVec2(end, ImGui::GetItemRectMax().y); - - drawList->AddRectFilled(pos, size, ImGui::ColorConvertFloat4ToU32(toneMappedColor)); - }; - - drawTiming(timingData.simulation, "Simulation", ImVec4(0.768f, 0.169f, 0.169f, 1.0f)); - drawTiming(timingData.renderSubmit, "RenderSubmit", ImVec4(0.235f, 0.705f, 0.294f, 1.0f)); - drawTiming(timingData.present, "Present", ImVec4(1.0f, 0.88f, 0.098f, 1.0f)); - drawTiming(timingData.driver, "Driver", ImVec4(0.263f, 0.388f, 0.847f, 1.0f)); - drawTiming(timingData.osRenderQueue, "RenderQueue", ImVec4(0.76f, 0.51f, 0.188f, 1.0f)); - drawTiming(timingData.gpuRender, "GpuRender", ImVec4(0.569f, 0.117f, 0.705f, 1.0f)); - } - } + if (gOverlayFont != nullptr) + ImGui::PopFont(); + else + ImGui::PopFontSize(); } // Restore the style @@ -3165,14 +3310,6 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) // Get size for postioning overlaySize = ImGui::GetWindowSize(); - if (overlayFontPushed) - { - if (gOverlayFont != nullptr) - ImGui::PopFont(); - else - ImGui::PopFontSize(); - } - ImGui::End(); ImGui::PopStyleVar(); // Rounding @@ -3180,42 +3317,15 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) if (stylePushed) ImGui::PopStyleVar(7); - // Where the overlay sits in the free space, 0 left/top to 1 right/bottom - float alignX = 0.0f; - float alignY = 0.0f; + // Where the overlay sits in the free space for each FpsOverlayPos, 0 left/top to 1 right/bottom + static constexpr ImVec2 alignments[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 1.0f }, + { 1.0f, 1.0f }, { 0.5f, 0.0f }, { 0.5f, 1.0f } }; - switch (config->FpsOverlayPosition.value_or_default()) - { - case FpsOverlayPos_TopRight: - alignX = 1.0f; - break; - - case FpsOverlayPos_BottomLeft: - alignY = 1.0f; - break; - - case FpsOverlayPos_BottomRight: - alignX = 1.0f; - alignY = 1.0f; - break; - - case FpsOverlayPos_TopCenter: - alignX = 0.5f; - break; - - case FpsOverlayPos_BottomCenter: - alignX = 0.5f; - alignY = 1.0f; - break; - - case FpsOverlayPos_Custom: - alignX = config->FpsOverlayPosX.value_or_default(); - alignY = config->FpsOverlayPosY.value_or_default(); - break; - - default: - break; - } + const auto position = config->FpsOverlayPosition.value_or_default(); + const float alignX = + position == FpsOverlayPos_Custom ? config->FpsOverlayPosX.value_or_default() : alignments[position].x; + const float alignY = + position == FpsOverlayPos_Custom ? config->FpsOverlayPosY.value_or_default() : alignments[position].y; // Prevent overlapping with splash message at the bottom auto freeHeight = io.DisplaySize.y - overlaySize.y; @@ -7819,7 +7929,7 @@ void MenuCommon::RenderThemeSettings(RenderMenuContext& ctx) if (ImGui::SliderFloat("Corner Rounding", &rounding, 0.0f, 16.0f, rounding < 0.5f ? "Square" : "%.0f px")) { config->MenuRounding = rounding; - lastMenuScale = 0.0f; // Rescales the style with the new rounding + lastMenuScale = 0.0f; // Rescales the style with this rounding } if (_bgBlurTexture != ImTextureID_Invalid) @@ -7887,30 +7997,44 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) if (config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_Custom) { - // 0.00 is the left or top of the screen, 1.00 the right or bottom float posX = config->FpsOverlayPosX.value_or_default(); - if (ImGui::SliderFloat("Horizontal Position", &posX, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp)) + if (ImGui::SliderFloat("Horizontal Position", &posX, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp)) config->FpsOverlayPosX = posX; float posY = config->FpsOverlayPosY.value_or_default(); - if (ImGui::SliderFloat("Vertical Position", &posY, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp)) + if (ImGui::SliderFloat("Vertical Position", &posY, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp)) config->FpsOverlayPosY = posY; } - const char* fpsType[] = { "Just FPS", "Simple", "Detailed", "Detailed + Graph", - "Full", "Full + Graph", "Reflex timings" }; - const char* selectedType = fpsType[config->FpsOverlayType.value_or_default()]; + const char* fpsType[] = { + "Just FPS", "Simple", "Detailed", "Detailed + Graph", "Full", "Full + Graph", "Low latency timings", + "Custom" + }; + static_assert(std::size(fpsType) == FpsOverlay_COUNT); + const auto currentType = config->FpsOverlayType.value_or_default(); - if (ImGui::BeginCombo("Overlay Type", selectedType)) + if (ImGui::BeginCombo("Overlay Type", fpsType[currentType])) { for (int n = 0; n < std::size(fpsType); n++) { - if (ImGui::Selectable(fpsType[n], (config->FpsOverlayType.value_or_default() == n))) + if (ImGui::Selectable(fpsType[n], currentType == n)) + { + // Custom starts from the preset's rows the first time + if (n == FpsOverlay_Custom && currentType != FpsOverlay_Custom && + !config->FpsOverlayRows.has_value()) + { + SetOverlayRows(OverlayRows(currentType)); + config->FpsOverlayFpsRowType = OverlayFpsRow(currentType); + } + config->FpsOverlayType = (FpsOverlay) n; + } } ImGui::EndCombo(); } + ShowTooltip("Presets of the rows, Custom shows the ones chosen in Rows.\n" + "The Overlay Cycle key switches between them."); // 0 is the menu's scale, then 0.5 to 3.0 in 0.1 steps int currentIndex = config->FpsScale.has_value() @@ -7937,7 +8061,6 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) ShowTooltip(tip); }; - // Results and hints on lines of their own, wrapped to the menu's width auto statusText = [](const std::string& text) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); @@ -7945,98 +8068,31 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) ImGui::PopStyleColor(); }; - // Ready made looks, as the text of a settings code: what a preset doesn't list goes back to the default - if (ImGui::TreeNode("Presets")) - { - struct OverlayPreset - { - const char* name; - const char* tip; - const char* settings; - }; - - static const OverlayPreset presets[] = { - { "Opti-Like", "OptiScaler's overlay, refined: Segoe UI Semibold on a rounded panel with a shadow", - "OverlaysUseTheme=false\nType=3\nShowRatio=true\nFont=seguisb.ttf\nAlpha=0.55\nRounding=6\n" - "LabelColor=0xC8C8C8\nValueColor=0xFFFFFF\nGraphColor=0x3CD070\nShadow=1\nShadowStrength=0.6\n" - "UpdateInterval=250\n" }, - { "Afterburner-Like", "As MSI Afterburner: Unispace Bold, orange labels, outlined, no background", - "OverlaysUseTheme=false\nType=3\nCompactLabels=true\nFont=unispace bd.ttf\nAlpha=0\nRounding=0\n" - "LabelColor=0xFF9C00\nValueColor=0xFFFFFF\nGraphColor=0xFF9C00\nShadow=2\nShadowStrength=0.5\n" - "ShadowColor=0x000000\nUpdateInterval=500\n" }, - { "Glass Strip", "A strip at the top center: Bahnschrift, cyan values on a dark glass panel", - "OverlaysUseTheme=false\nPos=4\nHorizontal=true\nType=2\nCompactLabels=true\n" - "Font=bahnschrift.ttf\nAlpha=0.35\nBGColor=0x0A0F14\nRounding=10\nLabelColor=0x8A9BA8\n" - "ValueColor=0x00E5FF\nGraphColor=0x00E5FF\nLabelScale=0.85\nValueScale=1.1\nLetterSpacing=0.5\n" - "Shadow=1\nShadowStrength=0.5\nUpdateInterval=250\n" }, - }; - - static std::string presetStatus; - const float presetCount = static_cast(std::size(presets)); - const float buttonWidth = - (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * (presetCount - 1.0f)) / - presetCount; - - for (size_t i = 0; i < std::size(presets); i++) - { - if (i > 0) - ImGui::SameLine(); - - if (ImGui::Button(presets[i].name, ImVec2(buttonWidth, 0.0f))) - { - if (config->ApplyOverlayPreset(presets[i].settings)) - presetStatus = StrFmt("%s applied, Save Settings to keep it", presets[i].name); - } - - ShowTooltip(presets[i].tip); - } - - statusText(presetStatus.empty() - ? "A preset replaces the overlay's look and rows, Copy Code in Share Settings keeps yours." - : presetStatus); - - ImGui::TreePop(); - } - // Which rows are shown and their order, from top to bottom (left to right when horizontal) if (ImGui::TreeNode("Rows")) { - const auto overlayType = config->FpsOverlayType.value_or_default(); + const bool custom = currentType == FpsOverlay_Custom; + auto rows = OverlayRows(currentType); + bool rowsChanged = false; - // Rows the overlay type decides on show their state, greyed out - auto fixedCheckbox = [&](const char* label, bool shown, const char* tip) + if (!custom) { - ImGui::BeginDisabled(); - ImGui::Checkbox(label, &shown); - ImGui::EndDisabled(); + statusText(StrFmt("The rows of %s, set Overlay Type to Custom to change them.", fpsType[currentType])); + } - ShowTooltip(tip); - }; + ImGui::BeginDisabled(!custom); - auto order = OverlayRowOrder(); - bool orderChanged = false; - - if (ImGui::BeginTable("overlayRows", 3, ImGuiTableFlags_SizingFixedFit)) + if (ImGui::BeginTable("overlayRows", 2, ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Order"); ImGui::TableSetupColumn("Row", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("From"); - ImGui::TableNextRow(); - for (int i = 0; i < 3; i++) + // Drawn as they were at the frame's start, a click changes one row + const auto shownRows = rows; + + for (size_t i = 0; i < shownRows.size(); i++) { - ImGui::TableSetColumnIndex(i); - ImGui::TextDisabled("%s", ImGui::TableGetColumnName(i)); - } - - ShowTooltip("The lowest Overlay Type that shows the row"); - - // Drawn as they were at the frame's start, a click moves one row - const auto shownOrder = order; - - for (size_t i = 0; i < shownOrder.size(); i++) - { - const int row = shownOrder[i]; + const int row = shownRows[i].row; ScopedID rowId { overlayRowKeys[row] }; ImGui::TableNextRow(); @@ -8045,61 +8101,55 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) ImGui::BeginDisabled(i == 0); if (ImGui::ArrowButton("##Up", ImGuiDir_Up)) { - std::swap(order[i], order[i - 1]); - orderChanged = true; + std::swap(rows[i], rows[i - 1]); + rowsChanged = true; } ImGui::EndDisabled(); ImGui::SameLine(0.0f, 2.0f); - ImGui::BeginDisabled(i + 1 == order.size()); + ImGui::BeginDisabled(i + 1 == shownRows.size()); if (ImGui::ArrowButton("##Down", ImGuiDir_Down)) { - std::swap(order[i], order[i + 1]); - orderChanged = true; + std::swap(rows[i], rows[i + 1]); + rowsChanged = true; } ImGui::EndDisabled(); ImGui::TableNextColumn(); - const char* name = overlayRowNames[row]; - const char* from = "Simple"; - - switch (row) + // A click here means no row moved this frame + bool rowOn = shownRows[i].shown; + if (ImGui::Checkbox(overlayRowNames[row], &rowOn)) { - case OverlayRow_Fps: - fixedCheckbox(name, true, "Always shown"); - from = "Just FPS"; - break; - - case OverlayRow_Upscale: - optionCheckbox(name, config->FpsOverlayShowRatio, - "Render -> output resolution and upscale ratio"); - break; - - case OverlayRow_FrameTime: - fixedCheckbox(name, overlayType >= FpsOverlay_Detailed, "Shown by the Overlay Type"); - from = "Detailed"; - break; - - case OverlayRow_UpscalerTime: - fixedCheckbox(name, overlayType >= FpsOverlay_Full, "Shown by the Overlay Type"); - from = "Full"; - break; - - default: - break; + rows[i].shown = rowOn; + rowsChanged = true; } - ImGui::TableNextColumn(); - ImGui::TextDisabled("%s", from); + ShowTooltip(overlayRowTips[row]); } ImGui::EndTable(); } - if (orderChanged) - SetOverlayRowOrder(order); + if (rowsChanged) + SetOverlayRows(rows); + + const char* fpsRowTypes[] = { "Just FPS", "With Frame Time", "With Average FPS" }; + static_assert(std::size(fpsRowTypes) == FpsOverlayFpsRow_COUNT); + + int fpsRowType = OverlayFpsRow(currentType); + if (ImGui::Combo("FPS Row", &fpsRowType, fpsRowTypes, IM_ARRAYSIZE(fpsRowTypes))) + config->FpsOverlayFpsRowType = (FpsOverlayFpsRow) fpsRowType; + + ShowTooltip("Just the number, or with the API, frame time or average FPS, frame generation and upscaler"); + + ImGui::EndDisabled(); + + ImGui::BeginDisabled(custom); + optionCheckbox("Upscale Ratio in Presets", config->FpsOverlayShowRatio, + "The Upscale Ratio row in the presets from Simple on, Custom has it in its rows"); + ImGui::EndDisabled(); ImGui::Spacing(); @@ -8115,28 +8165,25 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) config->FpsOverlayRowSpacing = std::round(std::max(rowSpacing, 0.0f)); } - if (ImGui::Button("Reset Order & Spacing")) + if (ImGui::Button(custom ? "Reset Rows & Spacing" : "Reset Spacing")) { - config->FpsOverlayRowOrder.reset(); + if (custom) + { + config->FpsOverlayRows.reset(); + config->FpsOverlayFpsRowType.reset(); + } + config->FpsOverlayRowSpacing.reset(); } + if (custom) + ShowTooltip("Custom's rows back to the ones of Full + Graph"); + ImGui::TreePop(); } if (ImGui::TreeNode("Appearance")) { - constexpr auto colorFlags = ImGuiColorEditFlags_NoInputs; - - auto colorEdit = [&](const char* label, CustomOptional& setting) - { - auto color = RgbToColor(setting.value_or_default()); - float rgb[3] = { color.x, color.y, color.z }; - - if (ImGui::ColorEdit3(label, rgb, colorFlags)) - setting = ColorToRgb(rgb); - }; - auto scaleSlider = [&](const char* label, CustomOptional& setting) { float value = setting.value_or_default(); @@ -8144,6 +8191,47 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) setting = value; }; + // Ready made looks, as the text of a settings code: what a preset doesn't list goes back to the default + SectionTitle("Presets"); + + struct OverlayPreset + { + const char* name; + const char* tip; + const char* settings; + }; + + static const OverlayPreset presets[] = { + { "Default", "OptiScaler's overlay: every row with the graphs, grey labels with a shadow", "" }, + { "Classic", "OptiScaler's earlier overlay: just the FPS in white, no shadow", + "LabelColor=0xFFFFFF;Shadow=0;Rounding=2" }, + { "Afterburner-Like", "As MSI Afterburner: Unispace Bold, orange labels, outlined, no background", + "OverlaysUseTheme=false;Type=3;CompactLabels=true;Font=unispace bd.ttf;Alpha=0;Rounding=0;" + "LabelColor=0xFF9C00;ValueColor=0xFFFFFF;GraphColor=0xFF9C00;Shadow=2;ShadowStrength=0.5;" + "ShadowColor=0x000000;UpdateInterval=500" }, + { "Glass Strip", "A strip at the top center: Bahnschrift, cyan values on a dark glass panel", + "OverlaysUseTheme=false;Pos=4;Horizontal=true;Type=2;CompactLabels=true;" + "Font=bahnschrift.ttf;Alpha=0.35;BGColor=0x0A0F14;Rounding=10;LabelColor=0x8A9BA8;" + "ValueColor=0x00E5FF;GraphColor=0x00E5FF;LabelScale=0.85;ValueScale=1.1;LetterSpacing=0.5;" + "Shadow=1;ShadowStrength=0.5;UpdateInterval=250" }, + }; + + const float presetCount = static_cast(std::size(presets)); + const float buttonWidth = + (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * (presetCount - 1.0f)) / + presetCount; + + for (size_t i = 0; i < std::size(presets); i++) + { + if (i > 0) + ImGui::SameLine(); + + if (ImGui::Button(presets[i].name, ImVec2(buttonWidth, 0.0f))) + config->ApplyOverlaySettings(presets[i].settings); + + ShowTooltip(presets[i].tip); + } + SectionTitle("Layout"); optionCheckbox("Compact Labels", config->FpsOverlayCompactLabels, @@ -8157,23 +8245,16 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) if (ImGui::SliderFloat("Graph Height", &graphHeight, 0.5f, 3.0f, "%.1fx", ImGuiSliderFlags_AlwaysClamp)) config->FpsOverlayGraphHeight = graphHeight; - // -1 is the menu's rounding - float rounding = config->FpsOverlayRounding.value_or(-1.0f); - const char* roundingFormat = !config->FpsOverlayRounding.has_value() ? "Same as menu" - : rounding < 0.5f ? "Square" - : "%.0f px"; - if (ImGui::SliderFloat("Corner Rounding", &rounding, -1.0f, 16.0f, roundingFormat, + float rounding = config->FpsOverlayRounding.value_or_default(); + if (ImGui::SliderFloat("Corner Rounding", &rounding, 0.0f, 16.0f, rounding < 0.5f ? "Square" : "%.0f px", ImGuiSliderFlags_AlwaysClamp)) { - if (rounding < -0.5f) - config->FpsOverlayRounding.reset(); - else - config->FpsOverlayRounding = std::max(rounding, 0.0f); + config->FpsOverlayRounding = std::round(rounding); } SectionTitle("Text"); - // Font, from the Windows fonts folder (cached once to avoid per-frame filesystem checks) + // Checked once, not every frame: fonts this Windows doesn't have aren't offered static const std::vector overlayFontExists = []() { std::vector exists; @@ -8224,7 +8305,7 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) } ShowTooltip("A file in C:\\Windows\\Fonts or a full path, Enter applies it"); - // 0 is the menu's font size + // 7, below the range, is the menu's font size float fontSizeValue = config->FpsOverlayFontSize.value_or(7.0f); if (ImGui::SliderFloat("Font Size", &fontSizeValue, 7.0f, 48.0f, config->FpsOverlayFontSize.has_value() ? "%.0f px" : "Menu font size", @@ -8276,17 +8357,43 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) SectionTitle("Colors"); + // Side by side, an item that doesn't fit goes to the next line + const float rowRight = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x; + bool firstColorItem = true; + + auto colorRowItem = [&](const char* label) + { + const auto& style = ImGui::GetStyle(); + const float gap = style.ItemSpacing.x * 2.0f; + const float width = ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(label).x; + + if (!firstColorItem && ImGui::GetItemRectMax().x + gap + width <= rowRight) + ImGui::SameLine(0.0f, gap); + + firstColorItem = false; + }; + + auto colorEdit = [&](const char* label, CustomOptional& setting) + { + colorRowItem(label); + + auto color = RgbToColor(setting.value_or_default()); + float rgb[3] = { color.x, color.y, color.z }; + + if (ImGui::ColorEdit3(label, rgb, ImGuiColorEditFlags_NoInputs)) + setting = ColorToRgb(rgb); + }; + bool useTheme = config->OverlaysUseTheme.value_or_default(); - if (ImGui::Checkbox("Use Theme Colors", &useTheme)) + if (ImGui::Checkbox("Use Theme", &useTheme)) config->OverlaysUseTheme = useTheme; - // Two by two, the theme's when it's used + ShowTooltip("The menu theme's colors for the labels, values, graph and background"); + ImGui::BeginDisabled(useTheme); colorEdit("Labels", config->FpsOverlayLabelColor); - ImGui::SameLine(0.0f, 16.0f); colorEdit("Values", config->FpsOverlayValueColor); colorEdit("Graph", config->FpsOverlayGraphColor); - ImGui::SameLine(0.0f, 16.0f); colorEdit("Background", config->FpsOverlayBGColor); ImGui::EndDisabled(); @@ -10103,23 +10210,6 @@ void MenuCommon::RenderMainMenuWindow(RenderMenuContext& ctx) flags |= ImGuiWindowFlags_NoCollapse; flags |= ImGuiWindowFlags_AlwaysAutoResize; - if (lastMenuScale != menuResScale) - { - lastMenuScale = menuResScale; - - // if UI scale is changed rescale the style - ImGuiStyle& style = ImGui::GetStyle(); - ImGuiStyle styleold = style; // Backup colors - style = ImGuiStyle(); // IMPORTANT: ScaleAllSizes will change the original size, - // so we should reset all style config - - ApplyThemeStyle(); - - style.ScaleAllSizes(menuResScale); - style.MouseCursorScale = 1.0f; - CopyMemory(style.Colors, styleold.Colors, sizeof(style.Colors)); // Restore colors - } - // Fixed width, only the tab content scrolls. Keeps the menu from growing or shrinking when tabs change // Height can be changed by the user with the resize grip and is stored unscaled in the config ImGuiWindowFlags mainFlags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse | @@ -10235,6 +10325,25 @@ bool MenuCommon::RenderMenu() // 3) Draw lightweight overlay windows first, preserving the original order. ctx.menuResScale = MenuResolutionScale(ctx.io); + + // The overlays share the style with the menu, so it's scaled before them even when the menu is closed + if (ctx.newFrame && lastMenuScale != ctx.menuResScale) + { + lastMenuScale = ctx.menuResScale; + + // if UI scale is changed rescale the style + ImGuiStyle& style = ImGui::GetStyle(); + ImGuiStyle styleold = style; // Backup colors + style = ImGuiStyle(); // IMPORTANT: ScaleAllSizes will change the original size, + // so we should reset all style config + + ApplyThemeStyle(); + + style.ScaleAllSizes(ctx.menuResScale); + style.MouseCursorScale = 1.0f; + CopyMemory(style.Colors, styleold.Colors, sizeof(style.Colors)); // Restore colors + } + RenderSplashWindow(ctx); RenderNotifications(ctx); UpdateFrameTimeAverages(ctx); @@ -10377,7 +10486,7 @@ void MenuCommon::Shutdown() ImGui::DestroyContext(); - // Went with the context's font atlas + // The font was in the destroyed context's atlas gOverlayFont = nullptr; gOverlayFontName.clear(); gOverlayLetterSpacing = 0.0f;