From 843d5d388e7fa9444bee1b559401fb5db02832e1 Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Mon, 23 Sep 2024 12:43:51 -0300 Subject: [PATCH] Frame graph + Precise 60 fps timing (#998) * video info: add frame graph Toggle advanced info with CTRL+F10. Also fixed imgui using gamepad for nav in wrong situations * 60fps! Implemented a timer that accumulates the time spent sleeping and sleeps for the remaining time. Also measure entire PresentThread time instead of just the time spent in Flip. * sceKernelGettimeofday: replace chrono by win32 api. Better performance bb uses this function too much. Consuming almost 30% of cpu time --- src/common/thread.cpp | 34 +++++ src/common/thread.h | 14 +++ src/core/libraries/kernel/time_management.cpp | 19 ++- src/core/libraries/videoout/driver.cpp | 18 ++- src/core/libraries/videoout/driver.h | 2 +- src/imgui/layer/video_info.cpp | 117 +++++++++++++++++- src/imgui/layer/video_info.h | 1 - src/imgui/renderer/imgui_impl_sdl3.cpp | 4 +- 8 files changed, 183 insertions(+), 26 deletions(-) diff --git a/src/common/thread.cpp b/src/common/thread.cpp index d1b225472..46df68c38 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -3,10 +3,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include "common/error.h" #include "common/logging/log.h" #include "common/thread.h" +#include "ntapi.h" #ifdef __APPLE__ #include #include @@ -102,6 +104,16 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { SetThreadPriority(handle, windows_priority); } +static void AccurateSleep(std::chrono::nanoseconds duration) { + LARGE_INTEGER interval{ + .QuadPart = -1 * (duration.count() / 100u), + }; + HANDLE timer = ::CreateWaitableTimer(NULL, TRUE, NULL); + SetWaitableTimer(timer, &interval, 0, NULL, NULL, 0); + WaitForSingleObject(timer, INFINITE); + ::CloseHandle(timer); +} + #else void SetCurrentThreadPriority(ThreadPriority new_priority) { @@ -122,6 +134,10 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { pthread_setschedparam(this_thread, scheduling_type, ¶ms); } +static void AccurateSleep(std::chrono::nanoseconds duration) { + std::this_thread::sleep_for(duration); +} + #endif #ifdef _MSC_VER @@ -164,4 +180,22 @@ void SetCurrentThreadName(const char*) { #endif +AccurateTimer::AccurateTimer(std::chrono::nanoseconds target_interval) + : target_interval(target_interval) {} + +void AccurateTimer::Start() { + auto begin_sleep = std::chrono::high_resolution_clock::now(); + if (total_wait.count() > 0) { + AccurateSleep(total_wait); + } + start_time = std::chrono::high_resolution_clock::now(); + total_wait -= std::chrono::duration_cast(start_time - begin_sleep); +} + +void AccurateTimer::End() { + auto now = std::chrono::high_resolution_clock::now(); + total_wait += + target_interval - std::chrono::duration_cast(now - start_time); +} + } // namespace Common diff --git a/src/common/thread.h b/src/common/thread.h index 3ee60c72f..fd962f8e5 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -23,4 +23,18 @@ void SetCurrentThreadPriority(ThreadPriority new_priority); void SetCurrentThreadName(const char* name); +class AccurateTimer { + std::chrono::nanoseconds target_interval{}; + std::chrono::nanoseconds total_wait{}; + + std::chrono::high_resolution_clock::time_point start_time; + +public: + explicit AccurateTimer(std::chrono::nanoseconds target_interval); + + void Start(); + + void End(); +}; + } // namespace Common diff --git a/src/core/libraries/kernel/time_management.cpp b/src/core/libraries/kernel/time_management.cpp index 7a6ba4f62..5e5e0ef27 100644 --- a/src/core/libraries/kernel/time_management.cpp +++ b/src/core/libraries/kernel/time_management.cpp @@ -147,13 +147,20 @@ int PS4_SYSV_ABI sceKernelGettimeofday(OrbisKernelTimeval* tp) { } #ifdef _WIN64 - auto now = std::chrono::system_clock::now(); - auto duration = now.time_since_epoch(); - auto seconds = std::chrono::duration_cast(duration); - auto microsecs = std::chrono::duration_cast(duration - seconds); + FILETIME filetime; + GetSystemTimeAsFileTime(&filetime); - tp->tv_sec = seconds.count(); - tp->tv_usec = microsecs.count(); + constexpr u64 UNIX_TIME_START = 0x295E9648864000; + constexpr u64 TICKS_PER_SECOND = 1000000; + + u64 ticks = filetime.dwHighDateTime; + ticks <<= 32; + ticks |= filetime.dwLowDateTime; + ticks /= 10; + ticks -= UNIX_TIME_START; + + tp->tv_sec = ticks / TICKS_PER_SECOND; + tp->tv_usec = ticks % TICKS_PER_SECOND; #else timeval tv; gettimeofday(&tv, nullptr); diff --git a/src/core/libraries/videoout/driver.cpp b/src/core/libraries/videoout/driver.cpp index f04fb505d..fa7577907 100644 --- a/src/core/libraries/videoout/driver.cpp +++ b/src/core/libraries/videoout/driver.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include "common/assert.h" @@ -160,9 +161,7 @@ int VideoOutDriver::UnregisterBuffers(VideoOutPort* port, s32 attributeIndex) { return ORBIS_OK; } -std::chrono::microseconds VideoOutDriver::Flip(const Request& req) { - const auto start = std::chrono::high_resolution_clock::now(); - +void VideoOutDriver::Flip(const Request& req) { // Whatever the game is rendering show splash if it is active if (!renderer->ShowSplash(req.frame)) { // Present the frame. @@ -198,9 +197,6 @@ std::chrono::microseconds VideoOutDriver::Flip(const Request& req) { port->buffer_labels[req.index] = 0; port->SignalVoLabel(); } - - const auto end = std::chrono::high_resolution_clock::now(); - return std::chrono::duration_cast(end - start); } void VideoOutDriver::DrawBlankFrame() { @@ -267,6 +263,8 @@ void VideoOutDriver::PresentThread(std::stop_token token) { Common::SetCurrentThreadName("PresentThread"); Common::SetCurrentThreadRealtime(vblank_period); + Common::AccurateTimer timer{vblank_period}; + const auto receive_request = [this] -> Request { std::scoped_lock lk{mutex}; if (!requests.empty()) { @@ -279,20 +277,18 @@ void VideoOutDriver::PresentThread(std::stop_token token) { auto delay = std::chrono::microseconds{0}; while (!token.stop_requested()) { - // Sleep for most of the vblank duration. - std::this_thread::sleep_for(vblank_period - delay); + timer.Start(); // Check if it's time to take a request. auto& vblank_status = main_port.vblank_status; if (vblank_status.count % (main_port.flip_rate + 1) == 0) { const auto request = receive_request(); if (!request) { - delay = std::chrono::microseconds{0}; if (!main_port.is_open) { DrawBlankFrame(); } } else { - delay = Flip(request); + Flip(request); FRAME_END; } } @@ -313,6 +309,8 @@ void VideoOutDriver::PresentThread(std::stop_token token) { Kernel::SceKernelEvent::Filter::VideoOut, nullptr); } } + + timer.End(); } } diff --git a/src/core/libraries/videoout/driver.h b/src/core/libraries/videoout/driver.h index 141294bfd..2e478b9ee 100644 --- a/src/core/libraries/videoout/driver.h +++ b/src/core/libraries/videoout/driver.h @@ -101,7 +101,7 @@ private: } }; - std::chrono::microseconds Flip(const Request& req); + void Flip(const Request& req); void DrawBlankFrame(); // Used when there is no flip request to keep ImGui up to date void SubmitFlipInternal(VideoOutPort* port, s32 index, s64 flip_arg, bool is_eop = false); void PresentThread(std::stop_token token); diff --git a/src/imgui/layer/video_info.cpp b/src/imgui/layer/video_info.cpp index bf30f8701..55cfaf895 100644 --- a/src/imgui/layer/video_info.cpp +++ b/src/imgui/layer/video_info.cpp @@ -2,16 +2,121 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include + +#include "common/config.h" +#include "common/types.h" +#include "imgui_internal.h" #include "video_info.h" -void ImGui::Layers::VideoInfo::Draw() { - const ImGuiIO& io = GetIO(); +using namespace ImGui; - m_show = IsKeyPressed(ImGuiKey_F10, false) ^ m_show; +struct FrameInfo { + u32 num; + float delta; +}; - if (m_show) { - if (Begin("Video Info", 0, ImGuiWindowFlags_NoNav)) { - Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); +static bool show = false; +static bool show_advanced = false; + +static u32 current_frame = 0; +constexpr float TARGET_FPS = 60.0f; +constexpr u32 FRAME_BUFFER_SIZE = 1024; +constexpr float BAR_WIDTH_MULT = 1.4f; +constexpr float BAR_HEIGHT_MULT = 1.25f; +constexpr float FRAME_GRAPH_PADDING_Y = 3.0f; +static std::array frame_list; +static float frame_graph_height = 50.0f; + +static void DrawSimple() { + const auto io = GetIO(); + Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); +} + +static void DrawAdvanced() { + const auto& ctx = *GetCurrentContext(); + const auto& io = ctx.IO; + const auto& window = *ctx.CurrentWindow; + auto& draw_list = *window.DrawList; + + Text("Frame time: %.3f ms (%.1f FPS)", io.DeltaTime * 1000.0f, io.Framerate); + + SeparatorText("Frame graph"); + const float full_width = GetContentRegionAvail().x; + { // Frame graph - inspired by + // https://asawicki.info/news_1758_an_idea_for_visualization_of_frame_times + auto pos = GetCursorScreenPos(); + const ImVec2 size{full_width, frame_graph_height + FRAME_GRAPH_PADDING_Y * 2.0f}; + ItemSize(size); + if (!ItemAdd({pos, pos + size}, GetID("FrameGraph"))) { + return; + } + + float target_dt = 1.0f / (TARGET_FPS * (float)Config::vblankDiv()); + float cur_pos_x = pos.x + full_width; + pos.y += FRAME_GRAPH_PADDING_Y; + const float final_pos_y = pos.y + frame_graph_height; + + draw_list.AddRectFilled({pos.x, pos.y - FRAME_GRAPH_PADDING_Y}, + {pos.x + full_width, final_pos_y + FRAME_GRAPH_PADDING_Y}, + IM_COL32(0x33, 0x33, 0x33, 0xFF)); + draw_list.PushClipRect({pos.x, pos.y}, {pos.x + full_width, final_pos_y}, true); + for (u32 i = 0; i < FRAME_BUFFER_SIZE; ++i) { + const auto& frame_info = frame_list[(current_frame - i) % FRAME_BUFFER_SIZE]; + const float dt_factor = target_dt / frame_info.delta; + + const float width = std::ceil(BAR_WIDTH_MULT / dt_factor); + const float height = + std::min(std::log2(BAR_HEIGHT_MULT / dt_factor) / 3.0f, 1.0f) * frame_graph_height; + + ImU32 color; + if (dt_factor >= 0.95f) { // BLUE + color = IM_COL32(0x33, 0x33, 0xFF, 0xFF); + } else if (dt_factor >= 0.5f) { // GREEN <> YELLOW + float t = 1.0f - (dt_factor - 0.5f) * 2.0f; + int r = (int)(0xFF * t); + color = IM_COL32(r, 0xFF, 0, 0xFF); + } else { // YELLOW <> RED + float t = dt_factor * 2.0f; + int g = (int)(0xFF * t); + color = IM_COL32(0xFF, g, 0, 0xFF); + } + draw_list.AddRectFilled({cur_pos_x - width, final_pos_y - height}, + {cur_pos_x, final_pos_y}, color); + cur_pos_x -= width; + if (cur_pos_x < width) { + break; + } + } + draw_list.PopClipRect(); + } +} + +void Layers::VideoInfo::Draw() { + const auto io = GetIO(); + + const FrameInfo frame_info{ + .num = ++current_frame, + .delta = io.DeltaTime, + }; + frame_list[current_frame % FRAME_BUFFER_SIZE] = frame_info; + + if (IsKeyPressed(ImGuiKey_F10, false)) { + const bool changed_ctrl = io.KeyCtrl != show_advanced; + show_advanced = io.KeyCtrl; + show = changed_ctrl || !show; + } + + if (show) { + if (show_advanced) { + if (Begin("Video debug info", &show, 0)) { + DrawAdvanced(); + } + } else { + if (Begin("Video Info", nullptr, + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize)) { + DrawSimple(); + } } End(); } diff --git a/src/imgui/layer/video_info.h b/src/imgui/layer/video_info.h index 8eec972a8..8a8af554e 100644 --- a/src/imgui/layer/video_info.h +++ b/src/imgui/layer/video_info.h @@ -11,7 +11,6 @@ class RendererVulkan; namespace ImGui::Layers { class VideoInfo : public Layer { - bool m_show = false; ::Vulkan::RendererVulkan* renderer{}; public: diff --git a/src/imgui/renderer/imgui_impl_sdl3.cpp b/src/imgui/renderer/imgui_impl_sdl3.cpp index 2a7d801e4..bb194bff7 100644 --- a/src/imgui/renderer/imgui_impl_sdl3.cpp +++ b/src/imgui/renderer/imgui_impl_sdl3.cpp @@ -703,8 +703,8 @@ static void UpdateGamepads() { const int thumb_dead_zone = 8000; // SDL_gamepad.h suggests using this value. UpdateGamepadButton(bd, io, ImGuiKey_GamepadStart, SDL_GAMEPAD_BUTTON_START); UpdateGamepadButton(bd, io, ImGuiKey_GamepadBack, SDL_GAMEPAD_BUTTON_BACK); - UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceLeft, - SDL_GAMEPAD_BUTTON_WEST); // Xbox X, PS Square + /*UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceLeft, + SDL_GAMEPAD_BUTTON_WEST); // Xbox X, PS Square*/ // Disable to avoid menu toggle UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceRight, SDL_GAMEPAD_BUTTON_EAST); // Xbox B, PS Circle UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceUp,