#include "graphics/presentation/performanceOverlay.h" #include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/hostStats.h" #include "graphics/host_gpu/graphicContext.h" #include "imgui.h" #include "kernel/memory.h" #include #include #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { using Clock = std::chrono::steady_clock; using Counter = Common::DebugCounters::Counter; using Peak = Common::DebugCounters::Peak; constexpr auto REFRESH_INTERVAL = std::chrono::seconds(1); constexpr size_t FRAME_HISTORY = 120; constexpr float FRAME_GRAPH_MS = 50.0f; constexpr float FRAME_TIME_GOOD = 17.5f; constexpr float FRAME_TIME_MEDIUM = 34.0f; constexpr size_t COUNTER_COUNT = static_cast(Counter::Count); // Tables as wide as their columns; the default fills the window, which an auto-resizing // window then never shrinks. constexpr ImGuiTableFlags TABLE_FLAGS = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX; constexpr ImU32 GOOD = IM_COL32(96, 220, 120, 255); constexpr ImU32 MEDIUM = IM_COL32(230, 195, 60, 255); constexpr ImU32 BAD = IM_COL32(235, 80, 70, 255); constexpr ImU32 NEUTRAL = IM_COL32(160, 160, 160, 255); struct FrameWindow { std::array intervals_ms {}; size_t next = 0; size_t count = 0; uint32_t flips = 0; float max_ms = 0.0f; uint32_t guest_width = 0; uint32_t guest_height = 0; Clock::time_point last_flip = {}; Clock::time_point window_start = {}; }; // Shown values, recomputed once per refresh interval. struct Snapshot { double fps = 0.0; double frame_ms = 0.0; double max_frame_ms = 0.0; double low_fps = 0.0; // From the 99th percentile. double stalled_s = 0.0; uint32_t guest_width = 0; uint32_t guest_height = 0; std::array rates {}; std::array totals {}; uint64_t shader_compile_peak_ns = 0; uint64_t pipeline_create_peak_ns = 0; Common::HostStats host; std::optional device_memory; uint64_t device_budget = 0; std::array heaps {}; uint32_t heap_count = 0; std::array history {}; size_t history_count = 0; Clock::time_point sample_time {}; LibKernel::Memory::DebugStats guest_memory; }; std::mutex g_frame_mutex; FrameWindow g_frames; std::atomic g_generation {0}; std::atomic g_details {false}; std::atomic g_revision {0}; std::mutex g_host_mutex; Common::HostStats g_host_stats; uint64_t g_host_generation = 0; std::condition_variable_any g_sampler_wait; std::jthread g_sampler; void SetOverlayEnabled(bool enabled) { { std::scoped_lock lock(g_host_mutex); if (enabled) { std::scoped_lock frame_lock(g_frame_mutex); g_frames = {}; g_generation.fetch_add(1, std::memory_order_release); } Common::DebugCounters::SetEnabled(enabled); } g_sampler_wait.notify_all(); } bool GuestStalled(Clock::time_point now) { std::scoped_lock lock(g_frame_mutex); return now - g_frames.last_flip >= REFRESH_INTERVAL; } // Host statistics can block for milliseconds (IOKit, thread enumeration), so sample them off // the present path. While the guest stops flipping (loading screens), ask for one repaint per // interval. void SamplerThread(const std::stop_token& stop) { std::optional sampler; uint64_t sampled_generation = 0; std::unique_lock lock(g_host_mutex); while (!stop.stop_requested()) { g_sampler_wait.wait(lock, stop, [] { return Common::DebugCounters::Enabled(); }); if (stop.stop_requested()) { break; } const auto generation = g_generation.load(std::memory_order_acquire); lock.unlock(); if (!sampler || generation != sampled_generation) { sampler.emplace(); sampled_generation = generation; } auto stats = sampler->Sample(); lock.lock(); g_host_stats = std::move(stats); g_host_generation = generation; if (Common::DebugCounters::Enabled() && GuestStalled(Clock::now())) { g_revision.fetch_add(1, std::memory_order_acq_rel); } g_sampler_wait.wait_for(lock, stop, REFRESH_INTERVAL, [generation] { return !Common::DebugCounters::Enabled() || generation != g_generation.load(std::memory_order_acquire); }); } } void EnsureSampler() { static std::once_flag once; std::call_once(once, [] { g_sampler = std::jthread(SamplerThread); }); } ImU32 FrameTimeColor(double ms) { return ms <= FRAME_TIME_GOOD ? GOOD : ms <= FRAME_TIME_MEDIUM ? MEDIUM : BAD; } // Green below `warn`, yellow below `bad`, red above. ImU32 LevelColor(double value, double warn, double bad) { return value < warn ? GOOD : value < bad ? MEDIUM : BAD; } ImVec4 Color(ImU32 color) { return ImGui::ColorConvertU32ToFloat4(color); } std::string Bytes(uint64_t bytes) { constexpr double GIB = 1024.0 * 1024.0 * 1024.0; constexpr double MIB = 1024.0 * 1024.0; char buffer[32]; if (static_cast(bytes) >= GIB) { std::snprintf(buffer, sizeof(buffer), "%.2f GB", static_cast(bytes) / GIB); } else { std::snprintf(buffer, sizeof(buffer), "%.0f MB", static_cast(bytes) / MIB); } return buffer; } std::string Bytes(std::optional bytes) { return bytes ? Bytes(*bytes) : "n/a"; } std::string Throughput(double bytes_per_second) { char buffer[32]; std::snprintf(buffer, sizeof(buffer), "%.1f MB/s", bytes_per_second / (1024.0 * 1024.0)); return buffer; } std::string Percent(std::optional value) { if (!value) { return "n/a"; } char buffer[16]; std::snprintf(buffer, sizeof(buffer), "%.0f%%", *value); return buffer; } void Row(const char* label, const std::string& value, std::optional color = std::nullopt) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextDisabled("%s", label); ImGui::TableNextColumn(); if (color) { ImGui::TextColored(Color(*color), "%s", value.c_str()); } else { ImGui::TextUnformatted(value.c_str()); } } [[gnu::format(printf, 1, 2)]] std::string Fmt(const char* format, ...) { char buffer[128]; va_list args; va_start(args, format); std::vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); return buffer; } bool BeginSection(const char* title) { ImGui::SeparatorText(title); return ImGui::BeginTable(title, 2, TABLE_FLAGS); } void DrawFrameGraph(const std::array& history, size_t count, float width, float height) { const ImVec2 origin = ImGui::GetCursorScreenPos(); auto* draw = ImGui::GetWindowDrawList(); draw->AddRectFilled(origin, {origin.x + width, origin.y + height}, IM_COL32(0, 0, 0, 90)); const auto y_of = [&](float ms) { return origin.y + height - std::min(ms, FRAME_GRAPH_MS) / FRAME_GRAPH_MS * height; }; for (const float reference: {1000.0f / 60.0f, 1000.0f / 30.0f}) { draw->AddLine({origin.x, y_of(reference)}, {origin.x + width, y_of(reference)}, IM_COL32(255, 255, 255, 45)); } const float bar = width / static_cast(FRAME_HISTORY); for (size_t i = 0; i < count; i++) { const float ms = history[i]; const float x = origin.x + width - static_cast(count - i) * bar; draw->AddRectFilled({x, y_of(ms)}, {x + std::max(bar - 1.0f, 1.0f), origin.y + height}, FrameTimeColor(ms)); } ImGui::Dummy({width, height}); } const char* PressureName(Common::MemoryPressure pressure) { switch (pressure) { case Common::MemoryPressure::Normal: return "Normal"; case Common::MemoryPressure::Warning: return "Warning"; case Common::MemoryPressure::Critical: return "Critical"; case Common::MemoryPressure::Unknown: break; } return "n/a"; } ImU32 PressureColor(Common::MemoryPressure pressure) { switch (pressure) { case Common::MemoryPressure::Normal: return GOOD; case Common::MemoryPressure::Warning: return MEDIUM; case Common::MemoryPressure::Critical: return BAD; case Common::MemoryPressure::Unknown: break; } return NEUTRAL; } const char* ThermalName(Common::ThermalPressure thermal) { switch (thermal) { case Common::ThermalPressure::Nominal: return "Nominal"; case Common::ThermalPressure::Moderate: return "Moderate"; case Common::ThermalPressure::Heavy: return "Heavy (throttling)"; case Common::ThermalPressure::Critical: return "Critical (throttling)"; case Common::ThermalPressure::Unknown: break; } return "n/a"; } ImU32 ThermalColor(Common::ThermalPressure thermal) { switch (thermal) { case Common::ThermalPressure::Nominal: return GOOD; case Common::ThermalPressure::Moderate: return MEDIUM; case Common::ThermalPressure::Heavy: case Common::ThermalPressure::Critical: return BAD; case Common::ThermalPressure::Unknown: break; } return NEUTRAL; } const char* PresentModeName(Config::PresentMode mode) { switch (mode) { case Config::PresentMode::Fifo: return "FIFO"; case Config::PresentMode::Mailbox: return "Mailbox"; case Config::PresentMode::Immediate: return "Immediate"; } return "?"; } double Rate(const Snapshot& shown, Counter counter) { return shown.rates[static_cast(counter)]; } uint64_t Total(const Snapshot& shown, Counter counter) { return shown.totals[static_cast(counter)]; } void TransferRow(const Snapshot& shown, const char* label, Counter upload, Counter download) { Row(label, Throughput(Rate(shown, upload)) + " up / " + Throughput(Rate(shown, download)) + " down"); } void GpuActivityRows(const Snapshot& shown) { const auto draws = Rate(shown, Counter::Draws); Row("Draw calls", Fmt("%.0f/frame %.0f/s", draws / std::max(shown.fps, 1.0), draws)); // Includes present pacing waits, so a high value alone is not a stall. Row("CPU waiting on GPU", Fmt("%.0f ms/s (%.0f waits/s)", Rate(shown, Counter::GpuWaitNs) / 1e6, Rate(shown, Counter::GpuWaits))); } void CompilationRows(const Snapshot& shown) { const auto row = [&](const char* label, Counter count_counter, Counter duration_counter, uint64_t peak_ns) { const auto count = Total(shown, count_counter); Row(label, Fmt("%llu (avg %.1f ms, peak %.1f ms)", static_cast(count), count != 0 ? static_cast(Total(shown, duration_counter)) / 1e6 / static_cast(count) : 0.0, static_cast(peak_ns) / 1e6), Rate(shown, count_counter) > 0.0 ? MEDIUM : GOOD); }; row("Shader recompiler", Counter::ShadersCompiled, Counter::ShaderCompileNs, shown.shader_compile_peak_ns); row("Pipeline compile", Counter::PipelinesCreated, Counter::PipelineCreateNs, shown.pipeline_create_peak_ns); } void Refresh(Snapshot& shown, const GraphicContext& graphics, Clock::time_point now, uint64_t generation) { const double seconds = shown.sample_time == Clock::time_point {} ? 0.0 : std::chrono::duration(now - shown.sample_time).count(); for (size_t i = 0; i < COUNTER_COUNT; i++) { const auto value = Common::DebugCounters::Get(static_cast(i)); shown.rates[i] = seconds > 0.0 ? static_cast(value - shown.totals[i]) / seconds : 0.0; shown.totals[i] = value; } shown.shader_compile_peak_ns = Common::DebugCounters::Get(Peak::ShaderCompileNs); shown.pipeline_create_peak_ns = Common::DebugCounters::Get(Peak::PipelineCreateNs); shown.sample_time = now; { std::scoped_lock lock(g_host_mutex); if (g_host_generation == generation) { shown.host = g_host_stats; } } shown.heap_count = graphics.GetHeapUsage(shown.heaps); shown.device_memory = graphics.CanReportMemoryUsage() ? std::optional(0) : std::nullopt; shown.device_budget = 0; const bool discrete = graphics.GetPhysicalDeviceProperties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu; for (uint32_t i = 0; i < shown.heap_count; i++) { const auto& heap = shown.heaps[i]; if (shown.device_memory && (!discrete || heap.device_local)) { *shown.device_memory += heap.usage; shown.device_budget += heap.budget; } } if (g_details.load(std::memory_order_acquire)) { shown.guest_memory = LibKernel::Memory::GetDebugStats(); } } void DrawCompact(const Snapshot& shown) { if (!ImGui::BeginTable("##performance", 2, TABLE_FLAGS)) { return; } const auto& host = shown.host; if (shown.max_frame_ms > 0.0) { Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms)); } Row("CPU", host.process_cpu_percent ? Fmt("%.0f%% of %u threads", *host.process_cpu_percent, host.logical_cores) : std::string("n/a")); Row("GPU", Percent(host.gpu_percent)); GpuActivityRows(shown); CompilationRows(shown); if (shown.device_memory) { Row("GPU memory", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget)); } TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes, Counter::TextureDownloadBytes); TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes, Counter::BufferDownloadBytes); Row("Emulator RAM", Bytes(host.process_memory)); if (host.system_memory_used && host.system_memory_total) { Row("System RAM", Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total)); } ImGui::EndTable(); } void DrawFramesAndCpu(const Snapshot& shown, vk::Extent2D extent) { const auto& host = shown.host; if (BeginSection("Frames")) { if (shown.low_fps > 0.0) { Row("1% low", Fmt("%.1f FPS", shown.low_fps), FrameTimeColor(1000.0 / shown.low_fps)); } Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms)); Row("Guest output", shown.guest_width != 0 ? Fmt("%ux%u", shown.guest_width, shown.guest_height) : std::string("n/a")); Row("Window", Fmt("%ux%u %s %u Hz", extent.width, extent.height, PresentModeName(Config::GetPresentMode()), Config::GetVblankFrequency())); Row("Flips", Fmt("%.0f/s requested", Rate(shown, Counter::FlipsRequested))); const auto rejected = Rate(shown, Counter::FlipsRejected); Row("Flip queue full", Fmt("%.0f/s", rejected), rejected > 0.0 ? BAD : GOOD); const auto late = Rate(shown, Counter::LateVblanks); Row("Late vblanks", Fmt("%.0f/s", late), LevelColor(late, 1.0, 10.0)); Row("Swapchain rebuilds", Fmt("%llu (dropped %llu)", static_cast(Total(shown, Counter::SwapchainRecreations)), static_cast(Total(shown, Counter::DroppedPresents)))); ImGui::EndTable(); } if (BeginSection("CPU")) { Row("Emulator", host.process_cpu_percent ? Fmt("%.0f%% of %u threads (%.1f cores)", *host.process_cpu_percent, host.logical_cores, *host.process_cpu_percent * host.logical_cores / 100.0) : std::string("n/a")); Row("Machine", Percent(host.system_cpu_percent), host.system_cpu_percent ? std::optional(LevelColor(*host.system_cpu_percent, 70, 90)) : std::nullopt); if (host.thread_count) { Row("Host threads", Fmt("%u", *host.thread_count)); } for (const auto& thread: host.busiest_threads) { Row(Fmt(" %.24s", thread.name.c_str()).c_str(), Fmt("%.0f%%", thread.cpu_percent), LevelColor(thread.cpu_percent, 70, 95)); } if (host.context_switches_per_s) { Row("Context switches", Fmt("%.0f/s", *host.context_switches_per_s)); } if (host.page_faults_per_s) { Row("Page faults", Fmt("%.0f/s (%.0f from disk)", *host.page_faults_per_s, host.page_ins_per_s.value_or(0.0))); } Row("Thermal", ThermalName(host.thermal_pressure), ThermalColor(host.thermal_pressure)); const auto emulated = Rate(shown, Counter::EmulatedInstructions); Row("Emulated instrs", Fmt("%.0f/s", emulated), LevelColor(emulated, 1000, 100000)); const auto unresolved = Rate(shown, Counter::UnresolvedImportCalls); Row("Missing HLE calls", Fmt("%.0f/s (%llu total)", unresolved, static_cast(Total(shown, Counter::UnresolvedImportCalls))), unresolved > 0.0 ? MEDIUM : GOOD); ImGui::EndTable(); } } void DrawGpu(const Snapshot& shown, const GraphicContext& graphics) { const auto& host = shown.host; if (BeginSection("GPU")) { Row("Device", host.gpu_model.empty() ? std::string(graphics.GetPhysicalDeviceProperties().deviceName.data()) : host.gpu_cores ? Fmt("%s (%u cores)", host.gpu_model.c_str(), *host.gpu_cores) : host.gpu_model); Row("Utilization", Percent(host.gpu_percent), host.gpu_percent ? std::optional(LevelColor(*host.gpu_percent, 80, 95)) : std::nullopt); if (host.gpu_renderer_percent || host.gpu_tiler_percent) { Row("Renderer / tiler", Percent(host.gpu_renderer_percent) + " / " + Percent(host.gpu_tiler_percent)); } if (host.gpu_memory_in_use) { Row("Driver memory", Bytes(*host.gpu_memory_in_use) + " in use / " + Bytes(host.gpu_memory_allocated) + " allocated"); } if (host.gpu_recoveries) { Row("GPU resets", Fmt("%llu", static_cast(*host.gpu_recoveries)), *host.gpu_recoveries > 0 ? BAD : GOOD); } const double fps = std::max(shown.fps, 1.0); GpuActivityRows(shown); Row("Instances", Fmt("%.0f/frame", Rate(shown, Counter::DrawInstances) / fps)); Row("Indirect draws", Fmt("%.0f/s", Rate(shown, Counter::IndirectDraws))); Row("Dispatches", Fmt("%.0f/frame %.0f/s", Rate(shown, Counter::Dispatches) / fps, Rate(shown, Counter::Dispatches))); Row("Queue submits", Fmt("%.0f/s", Rate(shown, Counter::QueueSubmits))); const auto faults = Rate(shown, Counter::GpuFaults); Row("Write-tracking faults", Fmt("%.0f/s", faults), LevelColor(faults, 20000, 100000)); CompilationRows(shown); ImGui::EndTable(); } if (BeginSection("GPU memory")) { if (shown.device_memory) { const double share = shown.device_budget != 0 ? static_cast(*shown.device_memory) / static_cast(shown.device_budget) * 100.0 : 0.0; Row("Vulkan budget", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget), LevelColor(share, 80, 95)); } for (size_t heap = 0; heap < shown.heap_count; heap++) { const auto& usage = shown.heaps[heap]; Row(Fmt(" Heap %zu%s", heap, usage.device_local ? " (device)" : "").c_str(), Bytes(usage.usage) + " / " + Bytes(usage.budget) + Fmt(" %u allocs", usage.allocations)); } TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes, Counter::TextureDownloadBytes); Row("Texture evictions", Fmt("%.0f/s", Rate(shown, Counter::TextureEvictions))); TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes, Counter::BufferDownloadBytes); Row("Buffer evictions", Fmt("%.0f/s", Rate(shown, Counter::BufferEvictions))); ImGui::EndTable(); } } void DrawMemoryAndIo(const Snapshot& shown) { const auto& host = shown.host; const auto& guest = shown.guest_memory; if (BeginSection("PS5 memory")) { const auto direct_used = guest.direct_allocated + guest.pooled_allocated; Row("Direct", Bytes(direct_used) + " / " + Bytes(guest.direct_total), LevelColor(guest.direct_total != 0 ? static_cast(direct_used) / static_cast(guest.direct_total) * 100.0 : 0.0, 85, 97)); Row(" Mapped", Bytes(guest.direct_mapped)); Row(" Pool", Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)"); Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total)); Row("Extended", Bytes(guest.automatic_allocated)); // Past the pool size the guest sees no page-table entries available. const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries); Row("Page table (2 MB)", Fmt("CPU %llu GPU %llu / %llu", static_cast(guest.cpu_page_entries), static_cast(guest.gpu_page_entries), static_cast(guest.page_entries_total)), entries > guest.page_entries_total ? BAD : entries * 10 > guest.page_entries_total * 9 ? MEDIUM : GOOD); ImGui::EndTable(); } if (BeginSection("Emulator process")) { Row("Footprint", Bytes(host.process_memory)); Row("Resident", Bytes(host.process_resident)); if (host.process_compressed) { Row("Compressed / swapped", Bytes(*host.process_compressed), *host.process_compressed > (1ull << 30u) ? MEDIUM : GOOD); } Row("File-backed", Bytes(host.process_file_backed)); Row("Address space", Bytes(host.process_virtual)); ImGui::EndTable(); } if (BeginSection("System memory")) { if (host.system_memory_used && host.system_memory_total) { Row("Used", Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total)); } if (host.system_wired) { Row("Wired", Bytes(*host.system_wired)); } if (host.system_compressed) { Row("Compressed", Bytes(*host.system_compressed)); } Row("Cached files", Bytes(host.system_cached)); Row("Free", Bytes(host.system_free)); if (host.swap_used) { Row("Swap", Bytes(*host.swap_used) + " / " + Bytes(host.swap_total), *host.swap_used > (4ull << 30u) ? MEDIUM : GOOD); } Row("Pressure", PressureName(host.memory_pressure), PressureColor(host.memory_pressure)); ImGui::EndTable(); } if (BeginSection("I/O & audio")) { Row("Disk read / write", Throughput(host.disk_read_per_s.value_or(0.0)) + " / " + Throughput(host.disk_write_per_s.value_or(0.0))); Row("Guest file read / write", Throughput(Rate(shown, Counter::GuestFileReadBytes)) + " / " + Throughput(Rate(shown, Counter::GuestFileWriteBytes))); const auto underruns = Rate(shown, Counter::AudioUnderruns); Row("Audio underruns", Fmt("%.0f/s (%llu total)", underruns, static_cast(Total(shown, Counter::AudioUnderruns))), underruns > 0.0 ? BAD : GOOD); ImGui::EndTable(); } } } // namespace void PerformanceOverlayRecordFlip(uint32_t guest_width, uint32_t guest_height) noexcept { if (!Common::DebugCounters::Enabled()) { return; } std::scoped_lock lock(g_frame_mutex); const auto now = Clock::now(); if (g_frames.window_start == Clock::time_point {}) { g_frames.window_start = now; } auto& frames = g_frames; if (frames.last_flip != Clock::time_point {}) { const float ms = std::chrono::duration(now - frames.last_flip).count(); frames.intervals_ms[frames.next] = ms; frames.next = (frames.next + 1) % FRAME_HISTORY; frames.count = std::min(frames.count + 1, FRAME_HISTORY); frames.max_ms = std::max(frames.max_ms, ms); } frames.last_flip = now; frames.guest_width = guest_width; frames.guest_height = guest_height; frames.flips++; } bool PerformanceOverlayEnabled() noexcept { return Common::DebugCounters::Enabled(); } void TogglePerformanceOverlay() noexcept { SetOverlayEnabled(!Common::DebugCounters::Enabled()); g_revision.fetch_add(1, std::memory_order_acq_rel); } void TogglePerformanceOverlayDetails() noexcept { if (Common::DebugCounters::Enabled()) { g_details.store(!g_details.load(std::memory_order_acquire), std::memory_order_release); } else { g_details.store(true, std::memory_order_release); SetOverlayEnabled(true); } g_revision.fetch_add(1, std::memory_order_acq_rel); } uint64_t PerformanceOverlayRevision() noexcept { return g_revision.load(std::memory_order_acquire); } float DrawPerformanceOverlay(const GraphicContext& graphics, vk::Extent2D extent) { if (!PerformanceOverlayEnabled()) { return 0.0f; } EnsureSampler(); static Snapshot shown; static uint64_t shown_generation = 0; static bool shown_details = false; const auto generation = g_generation.load(std::memory_order_acquire); const bool details = g_details.load(std::memory_order_acquire); const auto now = Clock::now(); if (generation != shown_generation) { shown = {}; shown_generation = generation; } if (shown.sample_time == Clock::time_point {} || now - shown.sample_time >= REFRESH_INTERVAL || details != shown_details) { { std::scoped_lock lock(g_frame_mutex); auto& frames = g_frames; shown.history_count = frames.count; for (size_t i = 0; i < frames.count; i++) { shown.history[i] = frames.intervals_ms[(frames.next + FRAME_HISTORY - frames.count + i) % FRAME_HISTORY]; } if (frames.window_start == Clock::time_point {}) { frames.window_start = now; } const double seconds = std::chrono::duration(now - frames.window_start).count(); shown.fps = seconds > 0.0 ? frames.flips / seconds : 0.0; shown.frame_ms = frames.flips > 0 ? seconds * 1000.0 / frames.flips : 0.0; shown.max_frame_ms = frames.max_ms; shown.stalled_s = frames.last_flip == Clock::time_point {} ? 0.0 : std::chrono::duration(now - frames.last_flip).count(); shown.guest_width = frames.guest_width; shown.guest_height = frames.guest_height; frames.flips = 0; frames.max_ms = 0.0f; frames.window_start = now; } shown_details = details; if (shown.history_count > 0) { auto sorted = shown.history; const auto end = sorted.begin() + shown.history_count; const auto percentile = sorted.begin() + shown.history_count * 99 / 100; std::nth_element(sorted.begin(), percentile, end); shown.low_fps = *percentile > 0.0f ? 1000.0 / *percentile : 0.0; } Refresh(shown, graphics, now, generation); } const float scale = std::clamp(std::min(static_cast(extent.width) / 1920.0f, static_cast(extent.height) / 1080.0f), 0.75f, 1.5f); ImGui::SetNextWindowPos({static_cast(extent.width) - 12.0f * scale, 12.0f * scale}, ImGuiCond_Always, {1.0f, 0.0f}); ImGui::SetNextWindowBgAlpha(details ? 0.82f : 0.72f); constexpr ImGuiWindowFlags FLAGS = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_AlwaysAutoResize; ImGui::PushFont(nullptr, (details ? 13.0f : 15.0f) * scale); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * scale); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {10.0f * scale, 8.0f * scale}); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8.0f * scale, 2.0f * scale}); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {4.0f * scale, 1.0f * scale}); if (ImGui::Begin("##kyty_performance_overlay", nullptr, FLAGS)) { if (shown.fps > 0.0) { ImGui::TextColored(Color(FrameTimeColor(shown.frame_ms)), "%.1f FPS %.1f ms", shown.fps, shown.frame_ms); } else if (shown.stalled_s >= 1.0) { ImGui::TextColored(Color(BAD), "No frames for %.0f s", shown.stalled_s); } else { ImGui::TextDisabled("Waiting for frames"); } DrawFrameGraph(shown.history, shown.history_count, (details ? 330.0f : 260.0f) * scale, 46.0f * scale); if (details) { // Three columns keep the detailed panel within a 720p window. if (ImGui::BeginTable("##details", 3, TABLE_FLAGS)) { ImGui::TableNextRow(); ImGui::TableNextColumn(); DrawFramesAndCpu(shown, extent); ImGui::TableNextColumn(); DrawGpu(shown, graphics); ImGui::TableNextColumn(); DrawMemoryAndIo(shown); ImGui::EndTable(); } } else { DrawCompact(shown); } #if defined(__APPLE__) ImGui::TextDisabled(details ? "Cmd+Shift+P compact Cmd+P hide" : "Cmd+Shift+P details Cmd+P hide"); #else ImGui::TextDisabled(details ? "Shift+F2 compact F2 hide" : "Shift+F2 details F2 hide"); #endif } const float bottom = ImGui::GetWindowPos().y + ImGui::GetWindowSize().y; ImGui::End(); ImGui::PopStyleVar(4); ImGui::PopFont(); return bottom; } } // namespace Libs::Graphics