mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Added F3 debug overlay: FPS counters, frametime graphs
FPS/frametime counters (avg 0.5s), two fixed-time-interval frametime graphs (fps-independent, spikes preserved), present-wait metric (rex/ui/present_stats.h). Guest stats provider wired in OnCreateDialogs. Removed the old inline F3-only build watermark (moves to a standalone always-on drawer in a later commit).
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "ac6_native_graphics.h"
|
||||
#include "ac6_native_graphics_overlay.h"
|
||||
#include "render_hooks.h"
|
||||
#include "generated/ac6recomp_config.h"
|
||||
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_backend);
|
||||
@@ -78,6 +79,17 @@ class Ac6recompApp : public rex::ReXApp {
|
||||
native_graphics_status_dialog_ =
|
||||
std::make_unique<ac6::graphics::NativeGraphicsStatusDialog>(drawer);
|
||||
native_graphics_status_dialog_->Show();
|
||||
|
||||
// Feed the F3 debug overlay the game's own frame stats so its guest
|
||||
// frametime graph and counter reflect the real sim cadence (SDK cannot
|
||||
// reach into the AC6 app layer, so it takes them through this callback).
|
||||
// Must be here, not OnPostSetup: the debug overlay is created just before
|
||||
// OnCreateDialogs, but OnPostSetup runs BEFORE the overlay exists, so a
|
||||
// provider set there is silently dropped.
|
||||
SetGuestFrameStats([]() -> rex::ui::FrameStats {
|
||||
const ::ac6::FrameStats s = ::ac6::GetFrameStats();
|
||||
return rex::ui::FrameStats{s.frame_time_ms, s.fps, s.frame_count};
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -39,6 +39,44 @@ class DebugOverlayDialog : public ImGuiDialog {
|
||||
private:
|
||||
bool visible_ = false;
|
||||
FrameStatsProvider stats_provider_;
|
||||
|
||||
// Frametime graph + throttled counter state.
|
||||
static constexpr int kHistory = 240;
|
||||
static constexpr double kCounterRefreshSeconds = 0.5;
|
||||
|
||||
// Ring buffers of frame times in milliseconds. Host = the presenter/paint
|
||||
// cadence (what an external tool like RTSS would measure); guest = the
|
||||
// game's own per-frame sim delta (where framerate-dependent judder lives).
|
||||
float host_ms_history_[kHistory] = {};
|
||||
float guest_ms_history_[kHistory] = {};
|
||||
int host_history_pos_ = 0;
|
||||
int guest_history_pos_ = 0;
|
||||
uint64_t last_guest_frame_count_ = 0;
|
||||
|
||||
// Graph samples are pushed at a FIXED TIME interval (not per frame), so the
|
||||
// graphs scroll at a consistent rate regardless of fps. kHistory samples then
|
||||
// cover a fixed time window (240 * 1/60 s = ~4 s). Each sample is the max
|
||||
// frametime since the last sample, so spikes between samples are preserved.
|
||||
static constexpr double kGraphSampleSeconds = 1.0 / 60.0;
|
||||
double graph_sample_accum_s_ = 0.0;
|
||||
float graph_host_ms_max_ = 0.0f;
|
||||
float graph_guest_ms_max_ = 0.0f;
|
||||
|
||||
// Counter averaging window (refreshed every kCounterRefreshSeconds so the
|
||||
// numbers are readable instead of flickering every frame).
|
||||
double counter_window_s_ = 0.0;
|
||||
int host_frames_in_window_ = 0;
|
||||
uint64_t guest_frames_at_window_start_ = 0;
|
||||
double window_guest_ms_min_ = 1.0e9;
|
||||
double window_guest_ms_max_ = 0.0;
|
||||
|
||||
// Held (displayed) counter values.
|
||||
double disp_host_fps_ = 0.0;
|
||||
double disp_host_ms_ = 0.0;
|
||||
double disp_guest_fps_ = 0.0;
|
||||
double disp_guest_ms_ = 0.0;
|
||||
double disp_guest_ms_min_ = 0.0;
|
||||
double disp_guest_ms_max_ = 0.0;
|
||||
};
|
||||
|
||||
} // namespace rex::ui
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file rex/ui/present_stats.h
|
||||
* @brief Lightweight presenter timing readouts for the debug overlay.
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Project Gracemeria
|
||||
* @license BSD 3-Clause License
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace rex::ui {
|
||||
|
||||
// Duration (ms) of the presenter's last swap-chain frame-latency wait - how long
|
||||
// painting blocked on the GPU/display being ready to accept a new frame. A high,
|
||||
// steady value means GPU-bound (the display/GPU is the bottleneck); ~0 means the
|
||||
// pacing/limiter is the bottleneck (CPU-side), which is the healthy case. This is
|
||||
// the analog of UnleashedRecomp's "Present Wait" profiler.
|
||||
double GetLastPresentWaitMs();
|
||||
void SetLastPresentWaitMs(double ms);
|
||||
|
||||
} // namespace rex::ui
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/math.h>
|
||||
#include <rex/ui/present_stats.h>
|
||||
#include <native/ui/d3d12/d3d12_presenter.h>
|
||||
#include <native/ui/d3d12/d3d12_provider.h>
|
||||
#include <native/ui/d3d12/d3d12_util.h>
|
||||
@@ -585,8 +587,12 @@ void D3D12Presenter::PaintContext::DestroySwapChain() {
|
||||
|
||||
Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(bool execute_ui_drawers) {
|
||||
if (paint_context_.HasFrameLatencyWaitableObject()) {
|
||||
auto wait_begin = std::chrono::steady_clock::now();
|
||||
DWORD wait_result =
|
||||
WaitForSingleObjectEx(paint_context_.swap_chain_latency_waitable_object, 1000, FALSE);
|
||||
ui::SetLastPresentWaitMs(
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - wait_begin)
|
||||
.count());
|
||||
if (wait_result == WAIT_FAILED) {
|
||||
REXLOG_WARN("D3D12Presenter: Waiting for the swap chain frame latency object failed");
|
||||
} else if (wait_result == WAIT_TIMEOUT) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/ui/present_stats.h>
|
||||
#include <native/ui/presenter.h>
|
||||
#include <native/ui/window.h>
|
||||
|
||||
@@ -28,6 +29,16 @@ REXCVAR_DEFINE_BOOL(host_present_from_non_ui_thread, true, "UI/Presenter",
|
||||
REXCVAR_DEFINE_BOOL(present_letterbox, true, "UI/Presenter",
|
||||
"Enable letterboxing for non-native aspect ratios");
|
||||
|
||||
namespace rex::ui {
|
||||
namespace {
|
||||
std::atomic<double> g_last_present_wait_ms{0.0};
|
||||
} // namespace
|
||||
double GetLastPresentWaitMs() { return g_last_present_wait_ms.load(std::memory_order_relaxed); }
|
||||
void SetLastPresentWaitMs(double ms) {
|
||||
g_last_present_wait_ms.store(ms, std::memory_order_relaxed);
|
||||
}
|
||||
} // namespace rex::ui
|
||||
|
||||
REXCVAR_DEFINE_INT32(present_safe_area_x, 90, "UI/Presenter",
|
||||
"Horizontal safe area percentage (0-100)")
|
||||
.range(0, 100);
|
||||
|
||||
+105
-27
@@ -11,9 +11,13 @@
|
||||
*/
|
||||
#include <rex/ui/overlay/debug_overlay.h>
|
||||
#include <rex/ui/keybinds.h>
|
||||
#include <rex/ui/present_stats.h>
|
||||
#include <rex/version.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace rex::ui {
|
||||
|
||||
DebugOverlayDialog::DebugOverlayDialog(ImGuiDrawer* imgui_drawer, FrameStatsProvider stats_provider)
|
||||
@@ -29,36 +33,110 @@ void DebugOverlayDialog::OnDraw(ImGuiIO& io) {
|
||||
if (!visible_)
|
||||
return;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(220, 60), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowBgAlpha(0.5f);
|
||||
if (ImGui::Begin("Debug##overlay", nullptr, ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::Text("Host: %.1f FPS (%.2f ms)", io.Framerate, 1000.0f / io.Framerate);
|
||||
if (stats_provider_) {
|
||||
auto stats = stats_provider_();
|
||||
if (stats.frame_count > 0) {
|
||||
ImGui::Text("Guest: %.1f FPS (%.2f ms)", stats.fps, stats.frame_time_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
// --- Sample this host frame --------------------------------------------
|
||||
const double host_ms = io.DeltaTime * 1000.0;
|
||||
|
||||
// Build stamp watermark -- centered near bottom of screen
|
||||
auto text_size = ImGui::CalcTextSize(REXGLUE_BUILD_STAMP);
|
||||
float padding = ImGui::GetStyle().WindowPadding.x * 2.0f;
|
||||
float bottom_offset = io.DisplaySize.y * 0.03f;
|
||||
ImGui::SetNextWindowPos(ImVec2((io.DisplaySize.x - text_size.x - padding) * 0.5f,
|
||||
io.DisplaySize.y - text_size.y - bottom_offset));
|
||||
ImGui::SetNextWindowSize(ImVec2(0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.5f));
|
||||
if (ImGui::Begin("##watermark", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground |
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextUnformatted(REXGLUE_BUILD_STAMP);
|
||||
FrameStats stats;
|
||||
bool have_guest = false;
|
||||
if (stats_provider_) {
|
||||
stats = stats_provider_();
|
||||
have_guest = stats.frame_count > 0;
|
||||
}
|
||||
|
||||
// Counter spread metric: track guest frametime extremes per ACTUAL guest frame.
|
||||
if (have_guest && stats.frame_count != last_guest_frame_count_) {
|
||||
last_guest_frame_count_ = stats.frame_count;
|
||||
window_guest_ms_min_ = (std::min)(window_guest_ms_min_, stats.frame_time_ms);
|
||||
window_guest_ms_max_ = (std::max)(window_guest_ms_max_, stats.frame_time_ms);
|
||||
}
|
||||
|
||||
// Push graph samples at a FIXED TIME interval (kGraphSampleSeconds), not per
|
||||
// frame, so both graphs scroll at a consistent rate regardless of fps. Each
|
||||
// sample carries the max frametime since the last sample (spikes preserved).
|
||||
graph_host_ms_max_ = (std::max)(graph_host_ms_max_, float(host_ms));
|
||||
if (have_guest) {
|
||||
graph_guest_ms_max_ = (std::max)(graph_guest_ms_max_, float(stats.frame_time_ms));
|
||||
}
|
||||
graph_sample_accum_s_ += io.DeltaTime;
|
||||
if (graph_sample_accum_s_ >= kGraphSampleSeconds) {
|
||||
bool first = true;
|
||||
while (graph_sample_accum_s_ >= kGraphSampleSeconds) {
|
||||
graph_sample_accum_s_ -= kGraphSampleSeconds;
|
||||
// First push uses the accumulated max; extra pushes (a frame longer than
|
||||
// several sample intervals - a hitch) repeat the current frametime so the
|
||||
// graph fills the gap at the right time width instead of dropping to zero.
|
||||
host_ms_history_[host_history_pos_] = first ? graph_host_ms_max_ : float(host_ms);
|
||||
host_history_pos_ = (host_history_pos_ + 1) % kHistory;
|
||||
guest_ms_history_[guest_history_pos_] =
|
||||
first ? graph_guest_ms_max_ : (have_guest ? float(stats.frame_time_ms) : 0.0f);
|
||||
guest_history_pos_ = (guest_history_pos_ + 1) % kHistory;
|
||||
first = false;
|
||||
}
|
||||
graph_host_ms_max_ = 0.0f;
|
||||
graph_guest_ms_max_ = 0.0f;
|
||||
}
|
||||
|
||||
// --- Throttled counters (averaged over kCounterRefreshSeconds) ----------
|
||||
counter_window_s_ += io.DeltaTime;
|
||||
++host_frames_in_window_;
|
||||
if (counter_window_s_ >= kCounterRefreshSeconds) {
|
||||
disp_host_fps_ = host_frames_in_window_ / counter_window_s_;
|
||||
disp_host_ms_ = disp_host_fps_ > 0.0 ? 1000.0 / disp_host_fps_ : 0.0;
|
||||
if (have_guest) {
|
||||
const double guest_frames = double(stats.frame_count - guest_frames_at_window_start_);
|
||||
disp_guest_fps_ = guest_frames / counter_window_s_;
|
||||
disp_guest_ms_ = disp_guest_fps_ > 0.0 ? 1000.0 / disp_guest_fps_ : 0.0;
|
||||
disp_guest_ms_min_ = window_guest_ms_min_ < 1.0e8 ? window_guest_ms_min_ : 0.0;
|
||||
disp_guest_ms_max_ = window_guest_ms_max_;
|
||||
}
|
||||
counter_window_s_ = 0.0;
|
||||
host_frames_in_window_ = 0;
|
||||
guest_frames_at_window_start_ = have_guest ? stats.frame_count : 0;
|
||||
window_guest_ms_min_ = 1.0e9;
|
||||
window_guest_ms_max_ = 0.0;
|
||||
}
|
||||
|
||||
// --- Draw ---------------------------------------------------------------
|
||||
ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(320, 0), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowBgAlpha(0.55f);
|
||||
if (ImGui::Begin("Frame timing##overlay", nullptr, ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::Text("Host: %6.1f FPS %5.2f ms", disp_host_fps_, disp_host_ms_);
|
||||
// Host = the presenter's paint/present rate = what an external tool like RTSS
|
||||
// measures. It is DECOUPLED from the guest game rate below (the presenter
|
||||
// paints on the UI thread at ~monitor refresh), so Host >> Guest is expected
|
||||
// and is why RTSS reads the present rate, not the game fps. Present-wait = how
|
||||
// long paint blocked on the GPU/display (high = GPU-bound; ~0 = pacing-bound).
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(" wait %.2f ms", GetLastPresentWaitMs());
|
||||
if (have_guest) {
|
||||
ImGui::Text("Guest: %6.1f FPS %5.2f ms", disp_guest_fps_, disp_guest_ms_);
|
||||
// Spread over the window is the numeric shake metric: a wide min/max at a
|
||||
// steady average fps is exactly the uneven-pacing / aircraft-shake signal.
|
||||
ImGui::TextDisabled("guest ft min %.2f max %.2f spread %.2f ms", disp_guest_ms_min_,
|
||||
disp_guest_ms_max_, disp_guest_ms_max_ - disp_guest_ms_min_);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Both graphs share a fixed 0-40ms scale so host vs guest jaggedness is
|
||||
// directly comparable. Flat host + jagged guest == sim-dt jitter (the shake
|
||||
// is in the game's timing, not the presenter); jagged both == real delivery
|
||||
// unevenness (look at the pacing/limiter).
|
||||
char overlay[48];
|
||||
std::snprintf(overlay, sizeof(overlay), "host %.2f ms (now)", host_ms);
|
||||
ImGui::PlotLines("##host_ft", host_ms_history_, kHistory, host_history_pos_, overlay, 0.0f,
|
||||
40.0f, ImVec2(0, 55));
|
||||
if (have_guest) {
|
||||
std::snprintf(overlay, sizeof(overlay), "guest %.2f ms (now)", stats.frame_time_ms);
|
||||
ImGui::PlotLines("##guest_ft", guest_ms_history_, kHistory, guest_history_pos_, overlay,
|
||||
0.0f, 40.0f, ImVec2(0, 55));
|
||||
}
|
||||
ImGui::TextDisabled("graphs 0-40 ms, ~%.0fs window (max/%.0fms) | counters avg %.1fs",
|
||||
kHistory * kGraphSampleSeconds, kGraphSampleSeconds * 1000.0,
|
||||
kCounterRefreshSeconds);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
} // namespace rex::ui
|
||||
|
||||
Reference in New Issue
Block a user