diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 992a52999c..b502c15f7f 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -57,6 +57,8 @@ struct UserSettings { ConfigVar enableFullscreen; ConfigVar enableVsync; ConfigVar lockAspectRatio; + ConfigVar enableFpsOverlay; + ConfigVar fpsOverlayCorner; } video; struct { diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 0f9e4ffd4e..5b07aeda2a 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -20,18 +20,22 @@ body { pointer-events: none; } +fps, toast { position: absolute; + border: 1dp #92875B; + backdrop-filter: blur(5dp); + box-shadow: 0 0 15dp 3dp; + background-color: rgba(21, 22, 16, 80%); +} + +toast { top: 40dp; right: 40dp; display: flex; flex-flow: column; border-radius: 14dp; overflow: hidden; - border: 1dp #92875B; - backdrop-filter: blur(5dp); - box-shadow: 0 0 15dp 3dp; - background-color: rgba(21, 22, 16, 80%); filter: opacity(0); transform: scale(0.9); transform-origin: center; @@ -186,6 +190,41 @@ icon.warning { decorator: text("" center center); } +fps { + display: none; + z-index: 99; + font-size: 18dp; + font-weight: bold; + padding: 9dp 12dp; + border-radius: 7dp; + pointer-events: none; + white-space: nowrap; +} + +fps[open] { + display: block; +} + +fps[corner=tl] { + top: 12dp; + left: 12dp; +} + +fps[corner=tr] { + top: 12dp; + right: 12dp; +} + +fps[corner=bl] { + bottom: 12dp; + left: 12dp; +} + +fps[corner=br] { + bottom: 12dp; + right: 12dp; +} + logo { position: absolute; width: 100dp; diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 09d1ca6e81..370c48896c 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -276,16 +276,6 @@ namespace dusk { m_menuGame.draw(); m_menuTools.draw(); - const auto fpsLabel = - fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate); - const auto fpsSize = - ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size()); - ImGui::SetCursorPosX( - ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - - ImGui::GetStyle().DisplaySafeAreaPadding.x - - fpsSize.x - ImGui::GetStyle().ItemSpacing.x)); - ImGuiStringViewText(fpsLabel); - ImGui::EndMainMenuBar(); } ImGui::PopStyleColor(); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index b3e0e13cd3..56d5038aa6 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -8,6 +8,8 @@ UserSettings g_userSettings = { .enableFullscreen {"video.enableFullscreen", false}, .enableVsync {"video.enableVsync", true}, .lockAspectRatio {"video.lockAspectRatio", false}, + .enableFpsOverlay {"game.enableFpsOverlay", false}, + .fpsOverlayCorner {"game.fpsOverlayCorner", 0}, }, .audio = { @@ -128,6 +130,8 @@ void registerSettings() { Register(g_userSettings.video.enableFullscreen); Register(g_userSettings.video.enableVsync); Register(g_userSettings.video.lockAspectRatio); + Register(g_userSettings.video.enableFpsOverlay); + Register(g_userSettings.video.fpsOverlayCorner); // Audio Register(g_userSettings.audio.masterVolume); diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 713a2151b1..0ce0b904ac 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -6,6 +6,7 @@ #include "window.hpp" #include +#include #include #include @@ -19,6 +20,7 @@ const Rml::String kDocumentSource = R"RML( + )RML"; @@ -31,6 +33,8 @@ constexpr std::array, 3> kAutoSaveLayers{{ constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500); +constexpr std::array kFpsCorners = { "tl", "tr", "bl", "br" }; + Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { if (toast.type == "autosave") { auto* logo = append(parent, "logo"); @@ -153,7 +157,46 @@ void remove_element(Rml::Element*& elem) noexcept { } // namespace +// https://vplesko.com/posts/how_to_implement_an_fps_counter.html +void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) { + if (perfFreq == 0) { + outFps = 0.f; + return; + } + + const Uint64 curr = SDL_GetPerformanceCounter(); + if (!mFpsHavePrevCounter) { + mFpsPrevCounter = curr; + mFpsHavePrevCounter = true; + outFps = 0.f; + return; + } + + const Uint64 processingTicks = curr - mFpsPrevCounter; + mFpsPrevCounter = curr; + + mFpsFrameEvents.push_back({curr, processingTicks}); + mFpsSumTicks += processingTicks; + + while (!mFpsFrameEvents.empty() && mFpsFrameEvents.front().endCounter + perfFreq < curr) { + mFpsSumTicks -= mFpsFrameEvents.front().processingTicks; + mFpsFrameEvents.pop_front(); + } + + const auto n = mFpsFrameEvents.size(); + if (n == 0 || mFpsSumTicks == 0) { + outFps = 0.f; + return; + } + + const double avgSeconds = + static_cast(mFpsSumTicks) / static_cast(n) / static_cast(perfFreq); + outFps = static_cast(1.0 / avgSeconds); +} + Overlay::Overlay() : Document(kDocumentSource) { + mFpsCounter = mDocument->GetElementById("fps"); + listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); }); listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) { if (event.GetTargetElement() == mCurrentToast) { @@ -187,6 +230,33 @@ void Overlay::update() { return; } + if (mFpsCounter != nullptr) { + if (getSettings().video.enableFpsOverlay.getValue()) { + const int idx = getSettings().video.fpsOverlayCorner.getValue(); + mFpsCounter->SetAttribute("open", ""); + mFpsCounter->SetAttribute("corner", kFpsCorners[idx]); + + const Uint64 perfFreq = SDL_GetPerformanceFrequency(); + float fps = 0.f; + advance_fps_counter(fps, perfFreq); + + const Uint64 now = SDL_GetPerformanceCounter(); + // Limit updates to twice per second + const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 || + static_cast(now - mFpsLastUpdate) >= 0.5 * static_cast(perfFreq); + if (refreshLabel) { + mFpsLastUpdate = now; + mFpsCounter->SetInnerRML(escape(fmt::format("{:.0f} FPS", fps))); + } + } else { + mFpsCounter->RemoveAttribute("open"); + mFpsFrameEvents.clear(); + mFpsSumTicks = 0; + mFpsHavePrevCounter = false; + mFpsLastUpdate = 0; + } + } + const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 && PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr && dynamic_cast(top_document()) == nullptr && diff --git a/src/dusk/ui/overlay.hpp b/src/dusk/ui/overlay.hpp index 6a29ca26e4..5c1433c51b 100644 --- a/src/dusk/ui/overlay.hpp +++ b/src/dusk/ui/overlay.hpp @@ -3,6 +3,7 @@ #include "document.hpp" #include +#include namespace dusk::ui { @@ -16,11 +17,25 @@ public: protected: bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; + Rml::Element* mFpsCounter = nullptr; Rml::Element* mCurrentToast = nullptr; Rml::Element* mControllerWarning = nullptr; Rml::Element* mMenuNotification = nullptr; clock::time_point mCurrentToastStartTime; clock::time_point mMenuNotificationStartTime; + + struct FpsFrameEvent { + Uint64 endCounter; + Uint64 processingTicks; + }; + + std::deque mFpsFrameEvents; + Uint64 mFpsSumTicks = 0; + bool mFpsHavePrevCounter = false; + Uint64 mFpsPrevCounter = 0; + Uint64 mFpsLastUpdate = 0; + + void advance_fps_counter(float& outFps, Uint64 perfFreq); }; } // namespace dusk::ui diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 6a3961e40e..92cff4c411 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -33,6 +33,13 @@ constexpr std::array kCardFileTypes = { "GCI Folder", }; +constexpr std::array kFpsOverlayCornerNames = { + "Top Left", + "Top Right", + "Bottom Left", + "Bottom Right", +}; + bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) { if (backend == "auto") { outBackend = BACKEND_AUTO; @@ -430,7 +437,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }); } - add_tab("Graphics", [this](Rml::Element* content) { + add_tab("Video", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); @@ -472,6 +479,40 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .key = "Pause on Focus Lost", .isDisabled = [] { return IsMobile; }, }); + config_bool_select(leftPane, rightPane, getSettings().video.enableFpsOverlay, + { + .key = "Enable FPS Counter", + .helpText = "Display the current framerate in a corner of the screen while playing.", + }); + leftPane.register_control(leftPane.add_select_button({ + .key = "FPS Counter Display Corner", + .getValue = + [] { + const int corner = getSettings().video.fpsOverlayCorner.getValue(); + return Rml::String{kFpsOverlayCornerNames[corner]}; + }, + .isDisabled = [] { return !getSettings().video.enableFpsOverlay.getValue(); }, + }), + rightPane, [](Pane& pane) { + for (int i = 0; i < static_cast(kFpsOverlayCornerNames.size()); ++i) { + pane + .add_button({ + .text = kFpsOverlayCornerNames[i], + .isSelected = + [i] { + return std::clamp( + getSettings().video.fpsOverlayCorner.getValue(), 0, + 3) == i; + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().video.fpsOverlayCorner.setValue(i); + config::Save(); + }); + } + pane.add_rml("
Choose which corner the framerate counter displays in."); + }); leftPane.add_section("Resolution"); graphics_tuner_control(*this, leftPane, rightPane,