Add game-frame-synced flip toggle + multi-series colored perf graphs
Frame sync: new "Sync flip to game frames" toggle in the Video mirror panel (Hooked source only -- WGC frames are delivered by the compositor at monitor refresh and don't carry the game's true present cadence, so it's disabled there). When on, the main loop waits for the hook's next published frame (its generation bump) before rendering and presents with sync interval 0, so the tool flips in lockstep with the game instead of vsync. The wait pumps messages to stay responsive and times out after 200 ms so a paused/stalled game can't hang the overlay. timeBeginPeriod(1) keeps the wait's Sleep(1) granular; links winmm. render_frame() gained a sync_interval parameter (default 1 = vsync). Perf graphs: the old graphs drew the tool's frametime and FPS as single same-color lines. Replaced with a custom multi-series plotter (ImDrawList polylines) that overlays Tool (blue), Game present (green), and Hook publish (orange) -- or Tool + WGC capture in WGC mode -- in distinct colors with a colored legend, for both an FPS (0-144) and a frametime (0-33 ms) view. Game/hook rates come from an EdgeRate tracker that measures the instantaneous rate the moment each counter advances, so the lines have real per-frame resolution rather than 0.5 s stair-steps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,10 +5,13 @@
|
||||
// (Injection panel), spoofs the game's focus, and mirrors the game's window into
|
||||
// this window via Windows Graphics Capture (Video mirror panel).
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <timeapi.h>
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
|
||||
#include "imgui.h"
|
||||
@@ -71,6 +74,41 @@ void draw_overlay_hidden_hint(double seconds_hidden)
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// Frame-sync: block until the injected hook publishes a new frame (its generation
|
||||
// bumps) or a short timeout elapses, pumping window messages so the window stays
|
||||
// responsive while we wait. Updates `last_gen` to the generation we should treat as
|
||||
// just-presented. Returns false only if the app is quitting (WM_QUIT seen mid-wait).
|
||||
bool wait_for_hooked_frame(coop::D3D11Window& window, coop::InjectionPanel& injection, std::uint32_t& last_gen)
|
||||
{
|
||||
LARGE_INTEGER freq{}, start{};
|
||||
QueryPerformanceFrequency(&freq);
|
||||
QueryPerformanceCounter(&start);
|
||||
constexpr double kTimeoutMs = 200.0; // present anyway if the game stalls / is paused
|
||||
for (;;)
|
||||
{
|
||||
const std::uint32_t gen = injection.video_share().generation;
|
||||
if (gen != last_gen)
|
||||
{
|
||||
last_gen = gen;
|
||||
return true;
|
||||
}
|
||||
LARGE_INTEGER now{};
|
||||
QueryPerformanceCounter(&now);
|
||||
const double elapsed =
|
||||
static_cast<double>(now.QuadPart - start.QuadPart) * 1000.0 / static_cast<double>(freq.QuadPart);
|
||||
if (elapsed >= kTimeoutMs)
|
||||
{
|
||||
last_gen = gen;
|
||||
return true;
|
||||
}
|
||||
if (!window.pump_messages())
|
||||
{
|
||||
return false; // WM_QUIT
|
||||
}
|
||||
Sleep(1); // yield ~1 ms (timeBeginPeriod(1) keeps this granular) instead of busy-spinning
|
||||
}
|
||||
}
|
||||
|
||||
int run()
|
||||
{
|
||||
coop::D3D11Window window;
|
||||
@@ -121,8 +159,21 @@ int run()
|
||||
std::uint16_t last_rumble_l[coop::kMaxPads] = {};
|
||||
std::uint16_t last_rumble_r[coop::kMaxPads] = {};
|
||||
|
||||
// Frame-sync: the hook generation we last presented (so we wait for the next one).
|
||||
std::uint32_t last_synced_gen = 0;
|
||||
|
||||
while (window.pump_messages())
|
||||
{
|
||||
// When the operator enabled "Sync flip to game frames" (Hooked source), pace the
|
||||
// whole iteration to the game: wait for the next published frame before rendering,
|
||||
// then present without vsync so the flip lands in lockstep with the game.
|
||||
if (capture.frame_sync_active())
|
||||
{
|
||||
if (!wait_for_hooked_frame(window, injection, last_synced_gen))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef COOP_WITH_STEAM
|
||||
// Switch the active input backend to match the Controllers-panel toggle.
|
||||
const bool want_steam = controllers.steam_input_requested();
|
||||
@@ -228,11 +279,16 @@ int run()
|
||||
const auto dst_w = static_cast<std::uint32_t>(client.right - client.left);
|
||||
const auto dst_h = static_cast<std::uint32_t>(client.bottom - client.top);
|
||||
|
||||
// Mirrored frame first (the window background), ImGui overlay on top.
|
||||
window.render_frame([&]() {
|
||||
capture.render(window.context(), dst_w, dst_h);
|
||||
imgui.end_frame();
|
||||
});
|
||||
// Mirrored frame first (the window background), ImGui overlay on top. When
|
||||
// frame-syncing, present immediately (interval 0) since the wait above already
|
||||
// paced us to the game; otherwise vsync to the monitor.
|
||||
const UINT sync_interval = capture.frame_sync_active() ? 0u : 1u;
|
||||
window.render_frame(
|
||||
[&]() {
|
||||
capture.render(window.context(), dst_w, dst_h);
|
||||
imgui.end_frame();
|
||||
},
|
||||
sync_interval);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -245,7 +301,11 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
|
||||
// WGC requires an initialized apartment; multi-threaded suits the
|
||||
// free-threaded frame pool.
|
||||
winrt::init_apartment(winrt::apartment_type::multi_threaded);
|
||||
// 1 ms timer resolution so the frame-sync wait's Sleep(1) is actually ~1 ms (the
|
||||
// default ~15 ms granularity would cap the synced present rate and add jitter).
|
||||
timeBeginPeriod(1);
|
||||
const int result = run();
|
||||
timeEndPeriod(1);
|
||||
winrt::uninit_apartment();
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user