diff --git a/OptiScaler.ini b/OptiScaler.ini index 9d044430..435d768b 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -892,18 +892,26 @@ DisableSplash=auto ; true or false - Default (auto) is false ShowFps=auto -; In-game FPS Overlay scale -; 0.5 to 2.0 - Default (auto) is using the same scale as menu +; In-game FPS Overlay scale, independent from the menu scale when set +; 0.5 to 3.0 - Default (auto) is using the same scale as menu FpsScale=auto ; FPS overlay position ; 0 = Top Left | 1 = Top Right | 2 = Bottom Left | 3 Bottom Right -; 0 to 3 - Default (auto) is 0 +; 4 = Top Center | 5 = Bottom Center | 6 = Custom (FpsOverlayPosX/Y) +; 0 to 6 - Default (auto) is 0 FpsOverlayPos=auto +; FPS overlay position when FpsOverlayPos is 6, 0.0 is left/top and 1.0 right/bottom of the screen +; 0.0 to 1.0 - Default (auto) is 0.0 +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 @@ -922,21 +930,92 @@ 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 +; Shorter labels in the FPS overlay (UPS, FT, UT...), for a narrower overlay +; true or false - Default (auto) is false +FpsOverlayCompactLabels=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 +FpsOverlayRowSpacing=auto + +; How often the FPS overlay's FPS, frame time and upscaler time change in milliseconds, averaged in between so they +; can be read. 0 updates them every frame +; 0.0 to 1000.0 - Default (auto) is 0.0 +FpsOverlayUpdateInterval=auto + ; FPS Overlay background alpha ; 0.0 to 1.0 - Default (auto) is 0.4 FpsOverlayAlpha=auto +; FPS Overlay graph height multiplier +; 0.5 to 3.0 - Default (auto) is 1.0 +FpsOverlayGraphHeight=auto + +; FPS Overlay font, a file name in C:\Windows\Fonts (e.g. segoeui.ttf) or a full path +; Default (auto) is the menu font +FpsOverlayFont=auto + +; FPS Overlay font size in pixels, before FpsScale +; 8.0 to 48.0 - Default (auto) is the menu font size +FpsOverlayFontSize=auto + +; FPS Overlay extra horizontal spacing between characters in pixels +; -2.0 to 8.0 - Default (auto) is 0.0 +FpsOverlayLetterSpacing=auto + +; FPS Overlay size of the labels (names like FPS:) and values (numbers), multiplies the font size +; 0.5 to 2.0 - Default (auto) is 1.0 +FpsOverlayLabelScale=auto +FpsOverlayValueScale=auto + +; FPS Overlay colors as 0xRRGGBB, used when OverlaysUseTheme is false +; 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 1 +FpsOverlayShadow=auto + +; FPS Overlay text shadow thickness and darkness, outline thickness +; 0.0 to 1.0 - Default (auto) is 0.5 +FpsOverlayShadowStrength=auto + +; FPS Overlay text shadow or outline color as 0xRRGGBB +; Default (auto) is 0x000000 +FpsOverlayShadowColor=auto + +; FPS Overlay corner radius in pixels, 0 is square +; 0.0 to 16.0 - Default (auto) is 4.0 +FpsOverlayRounding=auto + ; Shortcut key for FG enabled/disabled ; https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes ; Integer value - Default (auto) is 0x23 -> VK_END/End key ; -1 -> No shortcut key FGShortcutKey=auto +; Menu corner radius in pixels, 0 is square +; 0.0 to 16.0 - Default (auto) is 2.0 +Rounding=auto + ; Overlays uses the theme colors ; true or false - Default (auto) is false OverlaysUseTheme=auto diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index 224f3573..b7a6e96b 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -468,27 +468,13 @@ bool Config::Reload(std::filesystem::path iniPath) UseHQFont.set_from_config(readBool("Menu", "UseHQFont")); DisableSplash.set_from_config(readBool("Menu", "DisableSplash")); - if (auto setting = readUInt("Menu", "FpsOverlayPos"); setting.has_value()) - { - FpsOverlayPosition.set_from_config( - (FpsOverlayPos) std::clamp(setting.value(), 0U, FpsOverlayPos_COUNT - 1)); - } - - if (auto setting = readUInt("Menu", "FpsOverlayType"); setting.has_value()) - { - FpsOverlayType.set_from_config((FpsOverlay) std::clamp(setting.value(), 0U, FpsOverlay_COUNT - 1)); - } + ReadOverlaySettings(false); FpsShortcutKey.set_from_config(readInt("Menu", "FpsShortcutKey")); FpsCycleShortcutKey.set_from_config(readInt("Menu", "FpsCycleShortcutKey")); - FpsOverlayHorizontal.set_from_config(readBool("Menu", "FpsOverlayHorizontal")); - FpsOverlayShowRatio.set_from_config(readBool("Menu", "FpsOverlayShowRatio")); - if (auto setting = readFloat("Menu", "FpsOverlayAlpha"); setting.has_value()) - FpsOverlayAlpha.set_from_config(std::clamp(setting.value(), 0.0f, 1.0f)); - - if (auto setting = readFloat("Menu", "FpsScale"); setting.has_value()) - FpsScale.set_from_config(std::clamp(setting.value(), 0.5f, 2.0f)); + if (auto setting = readFloat("Menu", "Rounding"); setting.has_value()) + MenuRounding.set_from_config(std::clamp(setting.value(), 0.0f, 16.0f)); FontSize.set_from_config(readFloat("Menu", "FontSize")); TTFFontPath.set_from_config(readWString("Menu", "TTFFontPath")); @@ -496,7 +482,6 @@ bool Config::Reload(std::filesystem::path iniPath) FGShortcutKey.set_from_config(readInt("Menu", "FGShortcutKey")); LightTheme.set_from_config(readBool("Menu", "LightTheme")); - OverlaysUseTheme.set_from_config(readBool("Menu", "OverlaysUseTheme")); MenuAccentColorR.set_from_config(readFloat("Menu", "AccentColorR")); MenuAccentColorG.set_from_config(readFloat("Menu", "AccentColorG")); MenuAccentColorB.set_from_config(readFloat("Menu", "AccentColorB")); @@ -893,6 +878,391 @@ std::string GetFloatValue(std::optional value) return std::to_string(value.value()); } +// 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 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) + setting = value; + else + setting.set_from_config(value); + }; + + auto readClamped = [&](const char* key, float low, float high) + { return readFloat("Menu", key).transform([=](float value) { return std::clamp(value, low, high); }); }; + + auto readColor = [&](const char* key) + { return readUInt("Menu", key).transform([](uint32_t value) { return value & 0xFFFFFF; }); }; + + auto readIndex = [&](const char* key, uint32_t count) + { return readUInt("Menu", key).transform([=](uint32_t value) { return std::min(value, count - 1); }); }; + + apply(OverlaysUseTheme, readBool("Menu", "OverlaysUseTheme")); + apply(FpsOverlayPosition, readIndex("FpsOverlayPos", FpsOverlayPos_COUNT) + .transform([](uint32_t value) { return (FpsOverlayPos) value; })); + apply(FpsOverlayType, + readIndex("FpsOverlayType", FpsOverlay_COUNT).transform([](uint32_t value) { return (FpsOverlay) value; })); + apply(FpsOverlayHorizontal, readBool("Menu", "FpsOverlayHorizontal")); + apply(FpsOverlayShowRatio, readBool("Menu", "FpsOverlayShowRatio")); + apply(FpsOverlayCompactLabels, readBool("Menu", "FpsOverlayCompactLabels")); + 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)); + apply(FpsScale, readClamped("FpsScale", 0.5f, 3.0f)); + apply(FpsOverlayGraphHeight, readClamped("FpsOverlayGraphHeight", 0.5f, 3.0f)); + apply(FpsOverlayPosX, readClamped("FpsOverlayPosX", 0.0f, 1.0f)); + apply(FpsOverlayPosY, readClamped("FpsOverlayPosY", 0.0f, 1.0f)); + apply(FpsOverlayFont, readString("Menu", "FpsOverlayFont")); + apply(FpsOverlayFontSize, readClamped("FpsOverlayFontSize", 8.0f, 48.0f)); + apply(FpsOverlayLetterSpacing, readClamped("FpsOverlayLetterSpacing", -2.0f, 8.0f)); + apply(FpsOverlayLabelScale, readClamped("FpsOverlayLabelScale", 0.5f, 2.0f)); + apply(FpsOverlayValueScale, readClamped("FpsOverlayValueScale", 0.5f, 2.0f)); + apply(FpsOverlayLabelColor, readColor("FpsOverlayLabelColor")); + apply(FpsOverlayValueColor, readColor("FpsOverlayValueColor")); + apply(FpsOverlayGraphColor, readColor("FpsOverlayGraphColor")); + apply(FpsOverlayBGColor, readColor("FpsOverlayBGColor")); + apply(FpsOverlayShadowType, readIndex("FpsOverlayShadow", FpsOverlayShadow_COUNT) + .transform([](uint32_t value) { return (FpsOverlayShadow) value; })); + apply(FpsOverlayShadowStrength, readClamped("FpsOverlayShadowStrength", 0.0f, 1.0f)); + apply(FpsOverlayShadowColor, readColor("FpsOverlayShadowColor")); + apply(FpsOverlayRounding, readClamped("FpsOverlayRounding", 0.0f, 16.0f)); +} + +void Config::WriteOverlaySettings() +{ + 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", "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()); + ini.SetValue("Menu", "FpsOverlayAlpha", GetFloatValue(FpsOverlayAlpha.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsScale", GetFloatValue(FpsScale.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayGraphHeight", GetFloatValue(FpsOverlayGraphHeight.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayPosX", GetFloatValue(FpsOverlayPosX.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayPosY", GetFloatValue(FpsOverlayPosY.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayFont", FpsOverlayFont.value_for_config_or("auto").c_str()); + ini.SetValue("Menu", "FpsOverlayFontSize", GetFloatValue(FpsOverlayFontSize.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayLetterSpacing", GetFloatValue(FpsOverlayLetterSpacing.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayLabelScale", GetFloatValue(FpsOverlayLabelScale.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayValueScale", GetFloatValue(FpsOverlayValueScale.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayLabelColor", GetIntValue(FpsOverlayLabelColor.value_for_config(), true).c_str()); + ini.SetValue("Menu", "FpsOverlayValueColor", GetIntValue(FpsOverlayValueColor.value_for_config(), true).c_str()); + ini.SetValue("Menu", "FpsOverlayGraphColor", GetIntValue(FpsOverlayGraphColor.value_for_config(), true).c_str()); + ini.SetValue("Menu", "FpsOverlayBGColor", GetIntValue(FpsOverlayBGColor.value_for_config(), true).c_str()); + ini.SetValue("Menu", "FpsOverlayShadow", GetIntValue(FpsOverlayShadowType.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayShadowStrength", + GetFloatValue(FpsOverlayShadowStrength.value_for_config()).c_str()); + ini.SetValue("Menu", "FpsOverlayShadowColor", GetIntValue(FpsOverlayShadowColor.value_for_config(), true).c_str()); + ini.SetValue("Menu", "FpsOverlayRounding", GetFloatValue(FpsOverlayRounding.value_for_config()).c_str()); +} + +// 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"; + +static std::string_view OverlayCodeKey(std::string_view key) +{ + if (key.starts_with(overlayKeyPrefix)) + key.remove_prefix(overlayKeyPrefix.size()); + + return key; +} + +static constexpr std::string_view base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static std::string Base64Encode(std::string_view data) +{ + std::string out; + uint32_t bits = 0; + int count = 0; + + for (unsigned char c : data) + { + bits = (bits << 8) | c; + count += 8; + + for (; count >= 6; count -= 6) + out += base64Chars[(bits >> (count - 6)) & 0x3F]; + } + + // Without the padding, the decoder doesn't need it + if (count > 0) + 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; + uint32_t bits = 0; + int count = 0; + + for (char c : data) + { + if (c == '=' || std::isspace((unsigned char) c)) + continue; + + const auto value = base64Chars.find(c); + if (value == std::string_view::npos) + return std::nullopt; + + bits = (bits << 6) | static_cast(value); + count += 6; + + if (count >= 8) + { + count -= 8; + 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 data; + + for (size_t i = 0; i < std::size(overlayIniKeys); i++) + { + const char* value = ini.GetValue("Menu", overlayIniKeys[i].name, "auto"); + + if (value == nullptr || lstrcmpiA(value, "auto") == 0) + continue; + + data += static_cast(i); + PackOverlayValue(data, overlayIniKeys[i].type, value); + } + + return std::string(overlayCodePrefix) + Base64Encode(data); +} + +bool Config::ImportOverlaySettings(std::string_view code) +{ + while (!code.empty() && std::isspace((unsigned char) code.front())) + code.remove_prefix(1); + + while (!code.empty() && std::isspace((unsigned char) code.back())) + code.remove_suffix(1); + + if (!code.starts_with(overlayCodePrefix)) + return false; + + code.remove_prefix(overlayCodePrefix.size()); + + auto data = Base64Decode(code); + if (!data.has_value()) + return false; + + auto text = UnpackOverlayValues(data.value()); + if (!text.has_value()) + return false; + + return ApplyOverlaySettings(text.value()); +} + +bool Config::ApplyOverlaySettings(std::string_view text) +{ + // Known keys only, their values are checked as the ini's are + std::vector> values; + std::istringstream entries { std::string(text) }; + std::string entry; + + while (std::getline(entries, entry, ';')) + { + const auto separator = entry.find('='); + if (separator == std::string::npos) + continue; + + 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 (const auto& key : overlayIniKeys) + { + if (OverlayCodeKey(key.name) == name) + { + values.emplace_back(key.name, std::move(value)); + break; + } + } + } + + // A code with text but no known key is broken or from a later version + if (values.empty() && !text.empty()) + return false; + + // 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("Applied {} FPS overlay settings", values.size()); + + return true; +} + bool Config::SaveIni() { // Upscalers @@ -1326,20 +1696,14 @@ bool Config::SaveIni() ini.SetValue("Menu", "FpsCycleShortcutKey", GetIntValue(Instance()->FpsCycleShortcutKey.value_for_config(), setting > 0).c_str()); - ini.SetValue("Menu", "FpsOverlayPos", GetIntValue(Instance()->FpsOverlayPosition.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsOverlayType", GetIntValue(Instance()->FpsOverlayType.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsOverlayHorizontal", - GetBoolValue(Instance()->FpsOverlayHorizontal.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsOverlayShowRatio", - GetBoolValue(Instance()->FpsOverlayShowRatio.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsOverlayAlpha", GetFloatValue(Instance()->FpsOverlayAlpha.value_for_config()).c_str()); - ini.SetValue("Menu", "FpsScale", GetFloatValue(Instance()->FpsScale.value_for_config()).c_str()); + WriteOverlaySettings(); + + ini.SetValue("Menu", "Rounding", GetFloatValue(Instance()->MenuRounding.value_for_config()).c_str()); ini.SetValue("Menu", "FontSize", GetFloatValue(Instance()->FontSize.value_for_config()).c_str()); ini.SetValue("Menu", "TTFFontPath", wstring_to_string(Instance()->TTFFontPath.value_for_config_or(L"auto")).c_str()); ini.SetValue("Menu", "LightTheme", GetBoolValue(Instance()->LightTheme.value_for_config()).c_str()); - ini.SetValue("Menu", "OverlaysUseTheme", GetBoolValue(Instance()->OverlaysUseTheme.value_for_config()).c_str()); ini.SetValue("Menu", "AccentColorR", GetFloatValue(Instance()->MenuAccentColorR.value_for_config()).c_str()); ini.SetValue("Menu", "AccentColorG", GetFloatValue(Instance()->MenuAccentColorG.value_for_config()).c_str()); ini.SetValue("Menu", "AccentColorB", GetFloatValue(Instance()->MenuAccentColorB.value_for_config()).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index 87072dbf..955735f4 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -154,9 +154,20 @@ enum FpsOverlayPos : uint32_t FpsOverlayPos_TopRight, FpsOverlayPos_BottomLeft, FpsOverlayPos_BottomRight, + FpsOverlayPos_TopCenter, + FpsOverlayPos_BottomCenter, + FpsOverlayPos_Custom, // FpsOverlayPosX/Y FpsOverlayPos_COUNT, }; +enum FpsOverlayShadow : uint32_t +{ + FpsOverlayShadow_Off, + FpsOverlayShadow_Shadow, + FpsOverlayShadow_Outline, + FpsOverlayShadow_COUNT, +}; + enum FpsOverlay : uint32_t { FpsOverlay_JustFPS, @@ -165,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 { @@ -363,14 +384,43 @@ 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 }; + // 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 }; CustomOptional FpsOverlayAlpha { 0.4f }; CustomOptional FpsScale; // No value means same as MenuScale + CustomOptional FpsOverlayGraphHeight { 1.0f }; + // Position for FpsOverlayPos_Custom, 0.0 left/top to 1.0 right/bottom + CustomOptional FpsOverlayPosX { 0.0f }; + CustomOptional FpsOverlayPosY { 0.0f }; + // Font file in the Windows fonts folder or a full path, no value means the menu font + CustomOptional FpsOverlayFont; + CustomOptional FpsOverlayFontSize; // No value means the menu font size + CustomOptional FpsOverlayLetterSpacing { 0.0f }; + // Labels are the names like FPS:, values their numbers + CustomOptional FpsOverlayLabelScale { 1.0f }; + CustomOptional FpsOverlayValueScale { 1.0f }; + // 0xRRGGBB, used when the overlays don't use the theme colors + CustomOptional FpsOverlayLabelColor { 0xD9D9D9 }; + CustomOptional FpsOverlayValueColor { 0xFFFFFF }; + CustomOptional FpsOverlayGraphColor { 0x00FF00 }; + CustomOptional FpsOverlayBGColor { 0x000000 }; + CustomOptional FpsOverlayShadowType { FpsOverlayShadow_Shadow }; + CustomOptional FpsOverlayShadowStrength { 0.5f }; + CustomOptional FpsOverlayShadowColor { 0x000000 }; + CustomOptional FpsOverlayRounding { 4.0f }; + CustomOptional MenuRounding { 2.0f }; CustomOptional UseHQFont { true }; CustomOptional DisableSplash { false }; CustomOptional FontSize { 14.0f }; @@ -728,6 +778,15 @@ class Config std::vector GetConfigLog(); + // 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(std::string_view code); + + // 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(); private: @@ -739,6 +798,10 @@ class Config bool Reload(std::filesystem::path iniPath); + // 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(); + std::optional readString(std::string section, std::string key, bool lowercase = false); std::optional readWString(std::string section, std::string key, bool lowercase = false); std::optional readFloat(std::string section, std::string key); diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index 06f49c18..53fea0ea 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -32,6 +32,7 @@ #include #include +#include #include #include @@ -230,6 +231,322 @@ const int plotWidth = 360; static RingBuffer gFrameTimes; static RingBuffer gUpscalerTimes; +// FPS overlay fonts offered in the menu, from the Windows fonts folder +static const std::pair overlayFonts[] = { + { "Segoe UI", "segoeui.ttf" }, + { "Segoe UI Semilight", "segoeuisl.ttf" }, + { "Segoe UI Semibold", "seguisb.ttf" }, + { "Segoe UI Bold", "segoeuib.ttf" }, + { "Segoe UI Italic", "segoeuii.ttf" }, + { "Segoe UI Black", "seguibl.ttf" }, + { "Segoe UI Variable", "SegUIVar.ttf" }, + { "Bahnschrift", "bahnschrift.ttf" }, + { "Consolas", "consola.ttf" }, + { "Consolas Bold", "consolab.ttf" }, + { "Consolas Italic", "consolai.ttf" }, + { "Cascadia Mono", "CascadiaMono.ttf" }, + { "Cascadia Code", "CascadiaCode.ttf" }, + { "Unispace Bold", "unispace bd.ttf" }, + { "Lucida Console", "lucon.ttf" }, + { "Lucida Sans Unicode", "l_10646.ttf" }, + { "Courier New", "cour.ttf" }, + { "Courier New Bold", "courbd.ttf" }, + { "Arial", "arial.ttf" }, + { "Arial Bold", "arialbd.ttf" }, + { "Arial Black", "ariblk.ttf" }, + { "Verdana", "verdana.ttf" }, + { "Verdana Bold", "verdanab.ttf" }, + { "Tahoma", "tahoma.ttf" }, + { "Tahoma Bold", "tahomabd.ttf" }, + { "Calibri", "calibri.ttf" }, + { "Calibri Bold", "calibrib.ttf" }, + { "Trebuchet MS", "trebuc.ttf" }, + { "Trebuchet MS Bold", "trebucbd.ttf" }, + { "Century Gothic", "GOTHIC.TTF" }, + { "Century Gothic Bold", "GOTHICB.TTF" }, + { "Franklin Gothic Medium", "framd.ttf" }, + { "Franklin Gothic Demi", "FRADM.TTF" }, + { "Corbel", "corbel.ttf" }, + { "Corbel Bold", "corbelb.ttf" }, + { "Candara", "Candara.ttf" }, + { "Candara Bold", "Candarab.ttf" }, + { "Constantia", "constan.ttf" }, + { "Constantia Bold", "constanb.ttf" }, + { "Palatino Linotype", "pala.ttf" }, + { "Palatino Linotype Bold", "palab.ttf" }, + { "Georgia", "georgia.ttf" }, + { "Georgia Bold", "georgiab.ttf" }, + { "Garamond", "GARA.TTF" }, + { "Garamond Bold", "GARABD.TTF" }, + { "Sitka Text", "Sitka.ttc" }, + { "Sylfaen", "sylfaen.ttf" }, + { "Times New Roman", "times.ttf" }, + { "Times New Roman Bold", "timesbd.ttf" }, + { "Comic Sans MS", "comic.ttf" }, + { "Comic Sans MS Bold", "comicbd.ttf" }, + { "Impact", "impact.ttf" }, +}; + +// 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", "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", +}; + +struct OverlayRowEntry +{ + int row; + bool shown; +}; + +// 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 (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 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 (name == overlayRowKeys[row] && !listed(row)) + rows.push_back({ row, shown }); + } + } + + for (int row = 0; row < OverlayRow_COUNT; row++) + { + if (!listed(row)) + rows.push_back({ row, false }); + } + + return rows; +} + +static void SetOverlayRows(const std::vector& rows) +{ + std::string keys; + + for (const auto& entry : rows) + keys += (keys.empty() ? "" : ",") + std::string(entry.shown ? "" : "-") + overlayRowKeys[entry.row]; + + 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 +static ImFont* gOverlayFont = nullptr; +static std::string gOverlayFontName; +static float gOverlayLetterSpacing = 0.0f; + +static std::filesystem::path OverlayFontPath(const std::string& name) +{ + std::filesystem::path path(name); + + if (path.has_parent_path()) + return path; + + static const std::filesystem::path fontsDir = []() + { + wchar_t windowsDir[MAX_PATH] {}; + GetWindowsDirectoryW(windowsDir, MAX_PATH); + return std::filesystem::path(windowsDir) / L"Fonts"; + }(); + + return fontsDir / path; +} + +static void UpdateOverlayFont() +{ + auto config = Config::Instance(); + auto wanted = config->FpsOverlayFont.value_or(""); + const float letterSpacing = config->FpsOverlayLetterSpacing.value_or_default(); + + if (wanted == gOverlayFontName && std::abs(letterSpacing - gOverlayLetterSpacing) < 0.001f) + { + return; + } + + gOverlayFontName = wanted; + gOverlayLetterSpacing = letterSpacing; + + auto& io = ImGui::GetIO(); + + if (gOverlayFont != nullptr) + { + io.Fonts->RemoveFont(gOverlayFont); + gOverlayFont = nullptr; + } + + if (wanted.empty() && std::abs(letterSpacing) < 0.01f) + return; + + // The menu keeps its font, the first one added would become the default + if (io.FontDefault == nullptr) + io.FontDefault = io.Fonts->Fonts.empty() ? io.Fonts->AddFontDefault() : io.Fonts->Fonts[0]; + + ImFontConfig fontConfig; + fontConfig.GlyphExtraAdvanceX = letterSpacing; + + if (!wanted.empty()) + { + auto path = OverlayFontPath(wanted); + + if (!std::filesystem::exists(path)) + { + LOG_WARN("FPS overlay font not found: {}", wstring_to_string(path.wstring())); + return; + } + + gOverlayFont = io.Fonts->AddFontFromFileTTF(wstring_to_string(path.wstring()).c_str(), fontSize, &fontConfig); + } + else if (config->UseHQFont.value_or_default()) + { + if (config->TTFFontPath.has_value()) + { + gOverlayFont = io.Fonts->AddFontFromFileTTF(wstring_to_string(config->TTFFontPath.value()).c_str(), + fontSize, &fontConfig, io.Fonts->GetGlyphRangesDefault()); + } + else + { + gOverlayFont = io.Fonts->AddFontFromMemoryCompressedBase85TTF(hack_compressed_compressed_data_base85, + fontSize, &fontConfig); + } + } +} + +// FPS overlay colors are stored as 0xRRGGBB +static ImVec4 RgbToColor(uint32_t rgb) +{ + return ImVec4(((rgb >> 16) & 0xFF) / 255.0f, ((rgb >> 8) & 0xFF) / 255.0f, (rgb & 0xFF) / 255.0f, 1.0f); +} + +static uint32_t ColorToRgb(const float* color) +{ + auto channel = [](float c) { return (uint32_t) std::lround(std::clamp(c, 0.0f, 1.0f) * 255.0f); }; + return (channel(color[0]) << 16) | (channel(color[1]) << 8) | channel(color[2]); +} + struct FsExistsCache { std::wstring lastPath; @@ -1518,7 +1835,7 @@ void MenuCommon::ApplyThemeStyle() auto conf = Config::Instance(); bool lightTheme = conf->LightTheme.value_or_default(); - style.WindowRounding = 2.0f; + style.WindowRounding = conf->MenuRounding.value_or_default(); style.ChildRounding = 2.0f; style.FrameRounding = 2.0f; style.PopupRounding = 2.0f; @@ -2090,6 +2407,7 @@ void MenuCommon::BeginMenuFrameIfNeeded(RenderMenuContext& ctx) OptiInput::FeedImGui(_isVisible); MenuHdrCheck(io); + UpdateOverlayFont(); ImGui::NewFrame(); newFrame = true; @@ -2292,6 +2610,8 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) stylePushed = true; } + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, config->FpsOverlayRounding.value_or_default() * fpsScale); + // Set overlay position ImGui::SetNextWindowPos(overlayPosition, ImGuiCond_Always); @@ -2301,16 +2621,19 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) if (!config->OverlaysUseTheme.value_or_default()) { - ImGui::PushStyleColor(ImGuiCol_Text, toneMapColor(ImVec4(1.0f, 1.0f, 1.0f, 1.0f))); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, + toneMapColor(RgbToColor(config->FpsOverlayLabelColor.value_or_default()))); + ImGui::PushStyleColor(ImGuiCol_WindowBg, + toneMapColor(RgbToColor(config->FpsOverlayBGColor.value_or_default()))); } - ImGui::SetNextWindowBgAlpha(config->FpsOverlayAlpha.value_or_default()); // Transparent background + // The background is drawn below, so a corner can stay square + ImGui::SetNextWindowBgAlpha(0.0f); if (!config->OverlaysUseTheme.value_or_default()) { - ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_PlotLines, toneMapColor(green)); + ImGui::PushStyleColor(ImGuiCol_PlotLines, + toneMapColor(RgbToColor(config->FpsOverlayGraphColor.value_or_default()))); } if (ImGui::Begin("Performance Overlay", nullptr, @@ -2318,6 +2641,37 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + // Rounded corners, except one that touches two edges of the screen + { + const ImVec2 min = ImGui::GetWindowPos(); + const ImVec2 max = min + ImGui::GetWindowSize(); + const bool left = min.x <= 0.5f; + const bool right = max.x >= io.DisplaySize.x - 0.5f; + const bool top = min.y <= 0.5f; + const bool bottom = max.y >= io.DisplaySize.y - 0.5f; + + ImDrawFlags corners = 0; + if (!(top && left)) + corners |= ImDrawFlags_RoundCornersTopLeft; + if (!(top && right)) + corners |= ImDrawFlags_RoundCornersTopRight; + if (!(bottom && left)) + corners |= ImDrawFlags_RoundCornersBottomLeft; + if (!(bottom && right)) + corners |= ImDrawFlags_RoundCornersBottomRight; + + auto background = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); + background.w = config->FpsOverlayAlpha.value_or_default() * ImGui::GetStyle().Alpha; + + // The whole window, its content is clipped inside the padding + auto drawList = ImGui::GetWindowDrawList(); + drawList->PushClipRect(min, max, false); + drawList->AddRectFilled(min, max, ImGui::ColorConvertFloat4ToU32(background), + ImGui::GetStyle().WindowRounding, + corners != 0 ? corners : ImDrawFlags_RoundCornersNone); + drawList->PopClipRect(); + } + std::string api; if (IdentifyGpu::gameUsesDxvk()) { @@ -2374,25 +2728,28 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) } } - if (config->UseHQFont.value_or_default()) - ImGui::PushFontSize(std::round(fpsScale * fontSize)); + // Overlay font and size, the menu's unless set + const float overlayFontPx = std::round(fpsScale * config->FpsOverlayFontSize.value_or(fontSize)); + + if (gOverlayFont != nullptr) + ImGui::PushFont(gOverlayFont, overlayFontPx); else - ImGui::SetWindowFontScale(fpsScale); + ImGui::PushFontSize(overlayFontPx); auto fg = state.CurrentFG(); - auto fgText = (fg != nullptr && fg->IsActive() && !fg->IsPaused()) ? (" (" + std::string(fg->Name()) + ")") - : std::string(); + auto fgText = + (fg != nullptr && fg->IsActive() && !fg->IsPaused()) ? std::string(fg->Name()) : std::string(); const int fakeFramesCount = state.dlssgDetectedInterpolationCount; auto formatFg = [&](std::string_view name, int maxFakeFrames) { if (fakeFramesCount > maxFakeFrames) - return std::format(" ({} Doesn't support more than {}x)", name, maxFakeFrames); + return std::format("{} Doesn't support more than {}x", name, maxFakeFrames); else if (fakeFramesCount == 0) - return std::format(" ({} off)", name); + return std::format("{} off", name); - return std::format(" ({} x{})", name, fakeFramesCount + 1); + return std::format("{} x{}", name, fakeFramesCount + 1); }; const FGNvngxReplacement activeNvngxFg = state.activeFgNvngx; @@ -2420,15 +2777,53 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) const auto overlayType = config->FpsOverlayType.value_or_default(); const bool hasFeature = currentFeature && !currentFeature->IsFrozen(); - // Prepare Line 1 - std::string featurePart; - std::string fpsPart; + // Lines are made of labels (names, separators, info) and values, each with its own color and size + struct OverlayPart + { + 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) + { + if (!text.empty()) + parts.push_back({ std::move(text), false }); + + return *this; + } + + // The id keeps the value's widest width across frames + OverlayLine& Value(const char* id, std::string text) + { + auto padding = OverlayValuePadding(id, text); + parts.push_back({ std::move(text), true, 0.0f, std::move(padding) }); + return *this; + } + }; + + const bool compact = config->FpsOverlayCompactLabels.value_or_default(); + auto L = [compact](const char* full, const char* shorter) { return std::string(compact ? shorter : full); }; + + std::string featureText; if (hasFeature) { const bool usesDx12CompatLayer = currentFeature->IsWithDx12(); - featurePart = StrFmt(" | %s -> %s%s", ApiUpscalerInputName(state.currentInputApiName).c_str(), + featureText = StrFmt("%s -> %s%s", ApiUpscalerInputName(state.currentInputApiName).c_str(), NameWithVersion(currentFeature).c_str(), usesDx12CompatLayer ? " w/Dx12" : ""); } @@ -2436,215 +2831,474 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) if (fg != nullptr && fg->IsActive() && !fg->IsPaused()) interpolatedFrameCount = fg->GetInterpolatedFrameCount(); - if (interpolatedFrameCount) + // The values that change every frame, as their average over the update interval so they can be read + struct ShownTimes { - const double baseFps = frameRate / (double) (interpolatedFrameCount + 1); - - switch (overlayType) - { - case FpsOverlay_JustFPS: - fpsPart = StrFmt("%6.1f/%5.1f ", frameRate, baseFps); - break; - - case FpsOverlay_Simple: - fpsPart = StrFmt("FPS: %6.1f/%5.1f, %7.2f ms", frameRate, baseFps, frameTime); - break; - - default: - fpsPart = StrFmt("FPS: %6.1f/%5.1f, Avg: %6.1f", frameRate, baseFps, 1000.0f / averageFrameTime); - break; - } - } - else - { - switch (overlayType) - { - case FpsOverlay_JustFPS: - fpsPart = StrFmt("%6.1f ", frameRate); - break; - - case FpsOverlay_Simple: - fpsPart = StrFmt("FPS: %6.1f, %7.2f ms", frameRate, frameTime); - break; - - default: - fpsPart = StrFmt("FPS: %6.1f, Avg: %6.1f", frameRate, 1000.0f / averageFrameTime); - break; - } - } - - struct OverlayLine - { - std::string text; - const char* graphId = nullptr; - RingBuffer* graphData = nullptr; - float graphMax = 0.0f; + double frameRate = 0.0; + double frameTime = 0.0; + double averageFrameTime = 0.0; + double lastFrameTime = 0.0; + double lastUpscalerTime = 0.0; + double averageUpscalerTime = 0.0; }; - std::vector lines; + static ShownTimes shown; + static ShownTimes summed; + static uint32_t summedFrames = 0; + static double shownAt = 0.0; - if (overlayType == FpsOverlay_JustFPS) - lines.push_back({ fpsPart }); + summed.frameRate += frameRate; + summed.frameTime += frameTime; + summed.averageFrameTime += averageFrameTime; + summed.lastFrameTime += state.frameTimes.empty() ? 0.0 : state.frameTimes.back(); + summed.lastUpscalerTime += state.upscaleTimes.empty() ? 0.0 : state.upscaleTimes.back(); + summed.averageUpscalerTime += averageUpscalerFT; + summedFrames++; + + const float updateIntervalMs = config->FpsOverlayUpdateInterval.value_or_default(); + + if (updateIntervalMs < 1.0f || shownAt <= 0.001 || now < shownAt || now - shownAt >= updateIntervalMs) + { + const double frames = static_cast(summedFrames); + shown = { summed.frameRate / frames, summed.frameTime / frames, + summed.averageFrameTime / frames, summed.lastFrameTime / frames, + summed.lastUpscalerTime / frames, summed.averageUpscalerTime / frames }; + + summed = {}; + summedFrames = 0; + shownAt = now; + } + + // With frame generation also the base frame rate + const std::string fpsValue = + interpolatedFrameCount + ? 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; + + const auto fpsRow = OverlayFpsRow(overlayType); + + if (fpsRow == FpsOverlayFpsRow_JustFps) + { + rows[OverlayRow_Fps] = OverlayLine().Value("fps", fpsValue).Label(" "); + } else - lines.push_back( - { StrFmt("%s | %s%s%s", api.c_str(), fpsPart.c_str(), fgText.c_str(), featurePart.c_str()) }); + { + OverlayLine line; + line.Label(api + L(" | FPS: ", " | FPS ")).Value("fps", fpsValue); - if (overlayType >= FpsOverlay_Simple && hasFeature && config->FpsOverlayShowRatio.value_or_default() && - currentFeature->RenderWidth() > 0) + if (fpsRow == FpsOverlayFpsRow_FrameTime) + line.Label(", ").Value("fpsFrameTime", StrFmt("%.2f ms", shown.frameTime)); + else + line.Label(L(", Avg: ", " Avg ")) + .Value("fpsAverage", StrFmt("%.1f", 1000.0 / shown.averageFrameTime)); + + if (!fgText.empty()) + line.Label(" (" + fgText + ")"); + + if (!featureText.empty()) + line.Label(" | " + featureText); + + rows[OverlayRow_Fps] = std::move(line); + } + + if (hasFeature && currentFeature->RenderWidth() > 0) { const float ratio = (float) currentFeature->TargetWidth() / (float) currentFeature->RenderWidth(); - lines.push_back({ StrFmt("Upscale: %ux%u -> %ux%u (%.2fx)", currentFeature->RenderWidth(), - currentFeature->RenderHeight(), currentFeature->TargetWidth(), - currentFeature->TargetHeight(), ratio) }); + OverlayLine line; + line.Label(L("Upscale: ", "UPS ")) + .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 { StrFmt("Frame Time: %7.2f ms, Avg: %7.2f ms", state.frameTimes.back(), - averageFrameTime) }; + OverlayLine line; + line.graphId = id; + line.graphData = data; + line.graphMax = max; - if (overlayType >= FpsOverlay_DetailedGraph) + 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("upscalerTimeAverage", StrFmt("%.2f ms", shown.averageUpscalerTime)); + + rows[OverlayRow_UpscalerTimeGraph] = graphRow("##UpscalerFrameTimeGraph", &gUpscalerTimes, 20.0f); + + if (gotData && timingData.timeRange.has_value()) + { + rows[OverlayRow_LowLatency] = OverlayLine(); + rows[OverlayRow_LowLatency]->lowLatency = true; + } + + std::vector lines; + for (const auto& entry : overlayRows) + { + if (entry.shown && rows[entry.row].has_value()) { - line.graphId = "##FrameTimeGraph"; - line.graphData = &gFrameTimes; - line.graphMax = 66.6f; + lines.push_back(std::move(rows[entry.row].value())); + lines.back().row = entry.row; + } + } + + ImFont* font = ImGui::GetFont(); + const float basePx = ImGui::GetFontSize(); + const float labelPx = std::round(basePx * config->FpsOverlayLabelScale.value_or_default()); + const float valuePx = std::round(basePx * config->FpsOverlayValueScale.value_or_default()); + + const ImU32 labelColor = ImGui::GetColorU32(ImGuiCol_Text); + const ImU32 valueColor = config->OverlaysUseTheme.value_or_default() + ? labelColor + : ImGui::ColorConvertFloat4ToU32(toneMapColor( + RgbToColor(config->FpsOverlayValueColor.value_or_default()))); + + // A stronger shadow is thicker and darker + const auto shadowType = config->FpsOverlayShadowType.value_or_default(); + const float shadowStrength = config->FpsOverlayShadowStrength.value_or_default(); + const float shadowSize = std::max(1.0f, std::round(basePx / 14.0f * (0.5f + shadowStrength * 1.5f))); + auto shadowVec = toneMapColor(RgbToColor(config->FpsOverlayShadowColor.value_or_default())); + shadowVec.w = 0.35f + 0.65f * shadowStrength; + const ImU32 shadowColor = ImGui::ColorConvertFloat4ToU32(shadowVec); + + // The outline is a filled circle of opaque copies around the text, translucent ones would add up unevenly + std::vector outlineOffsets; + + if (shadowType == FpsOverlayShadow_Outline) + { + const int radius = static_cast(shadowSize); + const int limit = radius * radius + radius; + + for (int dy = -radius; dy <= radius; dy++) + { + for (int dx = -radius; dx <= radius; dx++) + { + if ((dx != 0 || dy != 0) && dx * dx + dy * dy <= limit) + outlineOffsets.emplace_back(static_cast(dx), static_cast(dy)); + } } - lines.push_back(std::move(line)); + shadowVec.w = 1.0f; } - if (overlayType >= FpsOverlay_Full) - { - OverlayLine line { StrFmt("Upscaler Time: %7.2f ms, Avg: %7.2f ms", state.upscaleTimes.back(), - averageUpscalerFT) }; + const ImU32 outlineColor = ImGui::ColorConvertFloat4ToU32(shadowVec); - if (overlayType >= FpsOverlay_FullGraph) + auto partPx = [&](const OverlayPart& part) { return part.value ? valuePx : labelPx; }; + + // Digits of the values take the widest digit's width, so changing numbers don't shift with fonts whose + // digits differ in width + float digitWidth = 0.0f; + for (char digit = '0'; digit <= '9'; digit++) + 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 and each number after its padding. The part's width. + auto layoutPart = [&](const OverlayPart& part, auto&& place) + { + const float px = partPx(part); + 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'; }; + + while (text < end) { - line.graphId = "##UpscalerFrameTimeGraph"; - line.graphData = &gUpscalerTimes; - line.graphMax = 20.0f; + 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; + text++; + continue; + } + + const char* runEnd = text; + while (runEnd < end && !isDigit(runEnd)) + runEnd++; + + place(text, runEnd, x); + x += font->CalcTextSizeA(px, FLT_MAX, 0.0f, text, runEnd).x; + text = runEnd; } - lines.push_back(std::move(line)); - } + return x; + }; + + auto measureLine = [&](OverlayLine& line) + { + ImVec2 size { 0.0f, 0.0f }; + + for (auto& part : line.parts) + { + const float px = partPx(part); + part.width = layoutPart(part, [](const char*, const char*, float) {}); + size.x += part.width; + size.y = std::max(size.y, px); + } + + line.size = size; + }; + + for (auto& line : lines) + measureLine(line); + + // On whole pixels, the shadows of a line under all its text so they don't cover the next part + auto drawText = [&](ImDrawList* drawList, float px, ImVec2 pos, ImU32 color, const char* text, + const char* end, bool shadowPass) + { + pos = ImVec2(std::round(pos.x), std::round(pos.y)); + + if (!shadowPass) + { + drawList->AddText(font, px, pos, color, text, end); + } + else if (shadowType == FpsOverlayShadow_Shadow) + { + for (float d = 1.0f; d <= shadowSize; d++) + drawList->AddText(font, px, ImVec2(pos.x + d, pos.y + d), shadowColor, text, end); + } + else if (shadowType == FpsOverlayShadow_Outline) + { + for (const auto& offset : outlineOffsets) + drawList->AddText(font, px, pos + offset, outlineColor, text, end); + } + }; + + 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); + + const float maxAscent = std::max(lineAscent(line), sharedAscent); + + auto drawList = ImGui::GetWindowDrawList(); + const auto start = ImGui::GetCursorScreenPos(); + + for (const bool shadowPass : { true, false }) + { + if (shadowPass && shadowType == FpsOverlayShadow_Off) + continue; + + auto pos = start; + + for (const auto& part : line.parts) + { + const float px = partPx(part); + const float y = pos.y + maxAscent - font->GetFontBaked(px)->Ascent; + const ImU32 color = part.value ? valueColor : labelColor; + + layoutPart(part, [&](const char* text, const char* end, float x) + { drawText(drawList, px, ImVec2(pos.x + x, y), color, text, end, shadowPass); }); + + pos.x += part.width; + } + } + + ImGui::Dummy(ImVec2(line.size.x, line.size.y + maxAscent - lineAscent(line))); + }; const bool horizontal = config->FpsOverlayHorizontal.value_or_default(); + const float graphHeight = config->FpsOverlayGraphHeight.value_or_default(); ImVec2 plotSize; if (horizontal) { - plotSize = { fpsScale * 150, fpsScale * 16 }; + plotSize = { fpsScale * 150, fpsScale * 16 * graphHeight }; } else { // Graphs match the widest text line auto textWidth = 0.0f; for (const auto& line : lines) - textWidth = std::max(textWidth, ImGui::CalcTextSize(line.text.c_str()).x); + textWidth = std::max(textWidth, line.size.x); - plotSize = { std::max(textWidth, fpsScale * 300.0f), fpsScale * 30 }; + plotSize = { std::max(textWidth, fpsScale * 300.0f), fpsScale * 30 * graphHeight }; + } + + // Rows are apart by the row spacing, by default as ImGui::Spacing() puts them + const float itemSpacingY = ImGui::GetStyle().ItemSpacing.y; + const float rowSpacing = config->FpsOverlayRowSpacing.has_value() + ? config->FpsOverlayRowSpacing.value() * fpsScale + : itemSpacingY * 2.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 for (size_t i = 0; i < lines.size(); i++) { - const auto& line = lines[i]; + 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); - ImGui::Text(" | "); - ImGui::SameLine(0.0f, 0.0f); + + if (!attached) + { + drawLine(sepLine); + ImGui::SameLine(0.0f, 0.0f); + } } else { - ImGui::Spacing(); + ImGui::Dummy(ImVec2(0.0f, attached ? itemSpacingY : rowSpacing)); } } - ImGui::TextUnformatted(line.text.c_str()); - if (line.graphData != nullptr) { - if (horizontal) - ImGui::SameLine(0.0f, 0.0f); - 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); } - } - - 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) + else if (line.lowLatency) { - gotData = InputCommon::get_timing_data(timingData); - previousPoll = now; + drawLowLatency(); } - - if (gotData && timingData.timeRange.has_value()) + else { - 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)); + drawLine(line); } } + + ImGui::PopStyleVar(); + + if (gOverlayFont != nullptr) + ImGui::PopFont(); + else + ImGui::PopFontSize(); } // Restore the style @@ -2656,39 +3310,30 @@ void MenuCommon::RenderPerformanceOverlay(RenderMenuContext& ctx) // Get size for postioning overlaySize = ImGui::GetWindowSize(); - if (config->UseHQFont.value_or_default()) - ImGui::PopFontSize(); - ImGui::End(); + ImGui::PopStyleVar(); // Rounding + if (stylePushed) ImGui::PopStyleVar(7); - // Left / Right - if (config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_TopLeft || - config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_BottomLeft) - { - overlayPosition.x = 0; - } - else - { - overlayPosition.x = io.DisplaySize.x - overlaySize.x; - } + // 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 } }; - // Top / Bottom - if (config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_TopLeft || - config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_TopRight) - { - overlayPosition.y = 0; - } - else - { - // Prevent overlapping with splash message - if (!config->DisableSplash.value_or_default() && now > splashStart && now < splashLimit) - overlayPosition.y = io.DisplaySize.y - overlaySize.y - splashSize.y; - else - overlayPosition.y = io.DisplaySize.y - overlaySize.y; - } + 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; + if (alignY > 0.5f && !config->DisableSplash.value_or_default() && now > splashStart && now < splashLimit) + freeHeight -= splashSize.y; + + overlayPosition.x = std::round(std::max(0.0f, io.DisplaySize.x - overlaySize.x) * alignX); + overlayPosition.y = std::round(std::max(0.0f, freeHeight) * alignY); } } @@ -7280,6 +7925,13 @@ void MenuCommon::RenderThemeSettings(RenderMenuContext& ctx) ApplyThemeStyle(); } + auto rounding = config->MenuRounding.value_or_default(); + 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 this rounding + } + if (_bgBlurTexture != ImTextureID_Invalid) { ConfigCheckbox("Background Blur", config->MenuBlur, @@ -7328,14 +7980,8 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) if (ImGui::Checkbox("Horizontal", &fpsHorizontal)) config->FpsOverlayHorizontal = fpsHorizontal; - ImGui::SameLine(0.0f, 6.0f); - - bool fpsShowRatio = config->FpsOverlayShowRatio.value_or_default(); - if (ImGui::Checkbox("Upscale Ratio", &fpsShowRatio)) - config->FpsOverlayShowRatio = fpsShowRatio; - ShowTooltip("Shows the current render -> output resolution and upscale ratio in the overlay"); - - const char* fpsPosition[] = { "Top Left", "Top Right", "Bottom Left", "Bottom Right" }; + const char* fpsPosition[] = { "Top Left", "Top Right", "Bottom Left", "Bottom Right", + "Top Center", "Bottom Center", "Custom" }; const char* selectedPosition = fpsPosition[config->FpsOverlayPosition.value_or_default()]; if (ImGui::BeginCombo("Overlay Position", selectedPosition)) @@ -7349,43 +7995,470 @@ void MenuCommon::RenderFpsOverlaySettings(RenderMenuContext& ctx) ImGui::EndCombo(); } - const char* fpsType[] = { "Just FPS", "Simple", "Detailed", "Detailed + Graph", - "Full", "Full + Graph", "Reflex timings" }; - const char* selectedType = fpsType[config->FpsOverlayType.value_or_default()]; + if (config->FpsOverlayPosition.value_or_default() == FpsOverlayPos_Custom) + { + float posX = config->FpsOverlayPosX.value_or_default(); + if (ImGui::SliderFloat("Horizontal Position", &posX, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp)) + config->FpsOverlayPosX = posX; - if (ImGui::BeginCombo("Overlay Type", selectedType)) + float posY = config->FpsOverlayPosY.value_or_default(); + 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", "Low latency timings", + "Custom" + }; + static_assert(std::size(fpsType) == FpsOverlay_COUNT); + const auto currentType = config->FpsOverlayType.value_or_default(); + + 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."); - float fpsAlpha = config->FpsOverlayAlpha.value_or_default(); - if (ImGui::SliderFloat("Background Alpha", &fpsAlpha, 0.0f, 1.0f, "%.2f")) - config->FpsOverlayAlpha = fpsAlpha; + // 0 is the menu's scale, then 0.5 to 3.0 in 0.1 steps + int currentIndex = config->FpsScale.has_value() + ? std::clamp((int) std::lround(config->FpsScale.value() * 10.0f) - 4, 1, 26) + : 0; + auto scaleText = currentIndex == 0 ? StrFmt("Same as menu (%.1f)", ctx.menuResScale) + : StrFmt("%.1f", 0.4f + currentIndex / 10.0f); - const char* options[] = { "Same as menu", "0.5", "0.6", "0.7", "0.8", "0.9", "1.0", "1.1", "1.2", - "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "2.0" }; - int currentIndex = std::max(((int) (config->FpsScale.value_or(0.0f) * 10.0f)) - 4, 0); - float values[] = { 0.0f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.1f, 1.2f, - 1.3f, 1.4f, 1.5f, 1.6f, 1.7f, 1.8f, 1.9f, 2.0f }; - - if (ImGui::SliderInt("Scale", ¤tIndex, 0, IM_ARRAYSIZE(options) - 1, options[currentIndex], - ImGuiSliderFlags_ClampOnInput)) + if (ImGui::SliderInt("Scale", ¤tIndex, 0, 26, scaleText.c_str(), ImGuiSliderFlags_ClampOnInput)) { if (currentIndex == 0) config->FpsScale.reset(); else - config->FpsScale = values[currentIndex]; + config->FpsScale = 0.4f + currentIndex / 10.0f; + } + ShowTooltip("Size of the FPS overlay, independent from the menu scale unless set to Same as menu"); + + auto optionCheckbox = [&](const char* label, CustomOptional& setting, const char* tip) + { + bool value = setting.value_or_default(); + if (ImGui::Checkbox(label, &value)) + setting = value; + + ShowTooltip(tip); + }; + + auto statusText = [](const std::string& text) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", text.c_str()); + ImGui::PopStyleColor(); + }; + + // Which rows are shown and their order, from top to bottom (left to right when horizontal) + if (ImGui::TreeNode("Rows")) + { + const bool custom = currentType == FpsOverlay_Custom; + auto rows = OverlayRows(currentType); + bool rowsChanged = false; + + if (!custom) + { + statusText(StrFmt("The rows of %s, set Overlay Type to Custom to change them.", fpsType[currentType])); + } + + ImGui::BeginDisabled(!custom); + + if (ImGui::BeginTable("overlayRows", 2, ImGuiTableFlags_SizingFixedFit)) + { + ImGui::TableSetupColumn("Order"); + ImGui::TableSetupColumn("Row", ImGuiTableColumnFlags_WidthStretch); + + // 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++) + { + const int row = shownRows[i].row; + ScopedID rowId { overlayRowKeys[row] }; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGui::BeginDisabled(i == 0); + if (ImGui::ArrowButton("##Up", ImGuiDir_Up)) + { + std::swap(rows[i], rows[i - 1]); + rowsChanged = true; + } + ImGui::EndDisabled(); + + ImGui::SameLine(0.0f, 2.0f); + + ImGui::BeginDisabled(i + 1 == shownRows.size()); + if (ImGui::ArrowButton("##Down", ImGuiDir_Down)) + { + std::swap(rows[i], rows[i + 1]); + rowsChanged = true; + } + ImGui::EndDisabled(); + + ImGui::TableNextColumn(); + + // A click here means no row moved this frame + bool rowOn = shownRows[i].shown; + if (ImGui::Checkbox(overlayRowNames[row], &rowOn)) + { + rows[i].shown = rowOn; + rowsChanged = true; + } + + ShowTooltip(overlayRowTips[row]); + } + + ImGui::EndTable(); + } + + 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(); + + // -1 is the default spacing + float rowSpacing = config->FpsOverlayRowSpacing.value_or(-1.0f); + if (ImGui::SliderFloat("Row Spacing", &rowSpacing, -1.0f, 32.0f, + config->FpsOverlayRowSpacing.has_value() ? "%.0f px" : "Default", + ImGuiSliderFlags_AlwaysClamp)) + { + if (rowSpacing < -0.5f) + config->FpsOverlayRowSpacing.reset(); + else + config->FpsOverlayRowSpacing = std::round(std::max(rowSpacing, 0.0f)); + } + + if (ImGui::Button(custom ? "Reset Rows & Spacing" : "Reset Spacing")) + { + 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(); } - bool useTheme = config->OverlaysUseTheme.value_or_default(); - if (ImGui::Checkbox("Use Theme Colors", &useTheme)) - config->OverlaysUseTheme = useTheme; + if (ImGui::TreeNode("Appearance")) + { + auto scaleSlider = [&](const char* label, CustomOptional& setting) + { + float value = setting.value_or_default(); + if (ImGui::SliderFloat(label, &value, 0.5f, 2.0f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) + 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, + "Shorter labels for a narrower overlay: UPS, FT, UT"); + + float fpsAlpha = config->FpsOverlayAlpha.value_or_default(); + if (ImGui::SliderFloat("Background Alpha", &fpsAlpha, 0.0f, 1.0f, "%.2f")) + config->FpsOverlayAlpha = fpsAlpha; + + float graphHeight = config->FpsOverlayGraphHeight.value_or_default(); + if (ImGui::SliderFloat("Graph Height", &graphHeight, 0.5f, 3.0f, "%.1fx", ImGuiSliderFlags_AlwaysClamp)) + config->FpsOverlayGraphHeight = graphHeight; + + 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)) + { + config->FpsOverlayRounding = std::round(rounding); + } + + SectionTitle("Text"); + + // Checked once, not every frame: fonts this Windows doesn't have aren't offered + static const std::vector overlayFontExists = []() + { + std::vector exists; + exists.reserve(IM_ARRAYSIZE(overlayFonts)); + for (const auto& [name, file] : overlayFonts) + exists.push_back(std::filesystem::exists(OverlayFontPath(file))); + return exists; + }(); + + auto fontName = config->FpsOverlayFont.value_or(""); + std::string selectedFont = fontName.empty() ? "Menu font" : fontName; + + for (const auto& [name, file] : overlayFonts) + { + if (_stricmp(file, fontName.c_str()) == 0) + selectedFont = name; + } + + if (ImGui::BeginCombo("Font", selectedFont.c_str())) + { + if (ImGui::Selectable("Menu font", fontName.empty())) + config->FpsOverlayFont.reset(); + + for (size_t i = 0; i < IM_ARRAYSIZE(overlayFonts); ++i) + { + if (!overlayFontExists[i]) + continue; + + const auto& [name, file] = overlayFonts[i]; + if (ImGui::Selectable(name, _stricmp(file, fontName.c_str()) == 0)) + config->FpsOverlayFont = std::string(file); + } + + ImGui::EndCombo(); + } + ShowTooltip("Choose a Windows system font or enter any custom .ttf/.otf file below"); + + char customFontBuf[260] {}; + std::snprintf(customFontBuf, sizeof(customFontBuf), "%s", fontName.c_str()); + if (ImGui::InputText("Custom Font File", customFontBuf, sizeof(customFontBuf), + ImGuiInputTextFlags_EnterReturnsTrue)) + { + std::string entered(customFontBuf); + if (entered.empty()) + config->FpsOverlayFont.reset(); + else + config->FpsOverlayFont = entered; + } + ShowTooltip("A file in C:\\Windows\\Fonts or a full path, Enter applies it"); + + // 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", + ImGuiSliderFlags_AlwaysClamp)) + { + if (fontSizeValue < 8.0f) + config->FpsOverlayFontSize.reset(); + else + config->FpsOverlayFontSize = std::round(fontSizeValue); + } + ShowTooltip("Before the overlay's scale, the far left is the menu's font size"); + + float letterSpacing = config->FpsOverlayLetterSpacing.value_or_default(); + if (ImGui::SliderFloat("Letter Spacing", &letterSpacing, -2.0f, 8.0f, "%.1f px", + ImGuiSliderFlags_AlwaysClamp)) + { + config->FpsOverlayLetterSpacing = letterSpacing; + } + ShowTooltip("Extra horizontal tracking in pixels between characters"); + + scaleSlider("Label Size", config->FpsOverlayLabelScale); + ShowTooltip("Names like FPS: and Avg:"); + + scaleSlider("Value Size", config->FpsOverlayValueScale); + ShowTooltip("The numbers"); + + float updateInterval = config->FpsOverlayUpdateInterval.value_or_default(); + if (ImGui::SliderFloat("Update Interval", &updateInterval, 0.0f, 1000.0f, + updateInterval < 1.0f ? "Every frame" : "%.0f ms", ImGuiSliderFlags_AlwaysClamp)) + { + config->FpsOverlayUpdateInterval = std::round(updateInterval / 10.0f) * 10.0f; + } + ShowTooltip("How often FPS and times change, averaged in between. Higher is easier to read."); + + const char* shadowTypes[] = { "Off", "Shadow", "Outline" }; + int shadowType = config->FpsOverlayShadowType.value_or_default(); + if (ImGui::Combo("Text Shadow", &shadowType, shadowTypes, IM_ARRAYSIZE(shadowTypes))) + config->FpsOverlayShadowType = (FpsOverlayShadow) shadowType; + + ImGui::BeginDisabled(shadowType == FpsOverlayShadow_Off); + float shadowStrength = config->FpsOverlayShadowStrength.value_or_default(); + if (ImGui::SliderFloat("Shadow Strength", &shadowStrength, 0.0f, 1.0f, "%.2f", + ImGuiSliderFlags_AlwaysClamp)) + { + config->FpsOverlayShadowStrength = shadowStrength; + } + ShowTooltip("Thickness and darkness of the shadow, thickness of the outline"); + ImGui::EndDisabled(); + + 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", &useTheme)) + config->OverlaysUseTheme = useTheme; + + ShowTooltip("The menu theme's colors for the labels, values, graph and background"); + + ImGui::BeginDisabled(useTheme); + colorEdit("Labels", config->FpsOverlayLabelColor); + colorEdit("Values", config->FpsOverlayValueColor); + colorEdit("Graph", config->FpsOverlayGraphColor); + colorEdit("Background", config->FpsOverlayBGColor); + ImGui::EndDisabled(); + + ImGui::BeginDisabled(shadowType == FpsOverlayShadow_Off); + colorEdit("Shadow", config->FpsOverlayShadowColor); + ImGui::EndDisabled(); + + ImGui::Spacing(); + + if (ImGui::Button("Reset Appearance")) + { + config->FpsOverlayCompactLabels.reset(); + config->FpsOverlayFont.reset(); + config->FpsOverlayFontSize.reset(); + config->FpsOverlayLetterSpacing.reset(); + config->FpsOverlayUpdateInterval.reset(); + config->FpsOverlayLabelColor.reset(); + config->FpsOverlayValueColor.reset(); + config->FpsOverlayLabelScale.reset(); + config->FpsOverlayValueScale.reset(); + config->FpsOverlayGraphColor.reset(); + config->FpsOverlayBGColor.reset(); + config->FpsOverlayShadowType.reset(); + config->FpsOverlayShadowStrength.reset(); + config->FpsOverlayShadowColor.reset(); + config->FpsOverlayGraphHeight.reset(); + config->FpsOverlayRounding.reset(); + config->FpsOverlayAlpha.reset(); + } + + ImGui::TreePop(); + } + + // The overlay's settings as a code, kept outside the ini across updates and shared with others + if (ImGui::TreeNode("Share Settings")) + { + static std::string settingsCodeStatus; + + if (ImGui::Button("Copy Code")) + { + ImGui::SetClipboardText(config->ExportOverlaySettings().c_str()); + settingsCodeStatus = "Copied to the clipboard"; + } + + ShowTooltip("Copies your overlay settings as a code to keep or share"); + + ImGui::SameLine(); + + if (ImGui::Button("Paste Code")) + { + const char* clipboard = ImGui::GetClipboardText(); + + if (clipboard != nullptr && config->ImportOverlaySettings(clipboard)) + settingsCodeStatus = "Applied, Save Settings to keep it"; + else + settingsCodeStatus = "No FPS overlay code in the clipboard"; + } + + ShowTooltip("Applies a copied code. Settings it doesn't list go back to their defaults."); + + if (!settingsCodeStatus.empty()) + statusText(settingsCodeStatus); + + ImGui::TreePop(); + } } } @@ -9137,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 | @@ -9269,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); @@ -9411,6 +10486,11 @@ void MenuCommon::Shutdown() ImGui::DestroyContext(); + // The font was in the destroyed context's atlas + gOverlayFont = nullptr; + gOverlayFontName.clear(); + gOverlayLetterSpacing = 0.0f; + _handle = nullptr; _isInited = false; _isVisible = false;