fix windows tui frame timing bug

This commit is contained in:
Boof2015
2026-08-16 00:40:38 -04:00
parent 10fca00c41
commit 90cde23f4d
5 changed files with 137 additions and 11 deletions
+1
View File
@@ -33,6 +33,7 @@ add_library(prism_tui_analysis STATIC
src/cli.cpp
src/dashboard_layout.cpp
src/display_model.cpp
src/frame_pacing.cpp
src/frame_rate_meter.cpp
src/meter_display_model.cpp
src/output_selection.cpp
+20
View File
@@ -0,0 +1,20 @@
#include "frame_pacing.h"
namespace Prism::Tui {
std::chrono::steady_clock::time_point advanceFrameDeadline(
std::chrono::steady_clock::time_point currentDeadline,
std::chrono::steady_clock::time_point renderedAt,
std::chrono::steady_clock::duration frameInterval) {
if (frameInterval <= std::chrono::steady_clock::duration::zero()) {
return renderedAt;
}
const auto elapsed = renderedAt - currentDeadline;
const auto intervalsToAdvance = elapsed >= decltype(elapsed)::zero()
? elapsed / frameInterval + 1
: 1;
return currentDeadline + frameInterval * intervalsToAdvance;
}
} // namespace Prism::Tui
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <chrono>
namespace Prism::Tui {
// Preserve the cadence anchor while skipping missed slots so late frames never
// create a catch-up backlog.
std::chrono::steady_clock::time_point advanceFrameDeadline(
std::chrono::steady_clock::time_point currentDeadline,
std::chrono::steady_clock::time_point renderedAt,
std::chrono::steady_clock::duration frameInterval);
} // namespace Prism::Tui
+54 -11
View File
@@ -3,6 +3,7 @@
#include "analysis_pipeline.h"
#include "dashboard_layout.h"
#include "display_model.h"
#include "frame_pacing.h"
#include "frame_rate_meter.h"
#include "meter_display_model.h"
#include "output_selection.h"
@@ -14,6 +15,7 @@
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/component/loop.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/canvas.hpp>
#include <ftxui/dom/elements.hpp>
@@ -23,12 +25,14 @@
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <csignal>
#include <cstdio>
#include <exception>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <optional>
#include <sstream>
#include <stdexcept>
@@ -2446,8 +2450,30 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
std::atomic<bool> resetRequested{false};
std::atomic<bool> redrawQueued{false};
std::atomic<uint64_t> outputListRequested{0};
#if defined(_WIN32)
std::mutex screenWakeMutex;
std::condition_variable screenWakeCondition;
uint64_t screenWakeSerial = 0;
#endif
std::exception_ptr workerError;
auto exitLoop = screen.ExitLoopClosure();
#if defined(_WIN32)
const auto notifyScreenLoop = [&]() {
{
std::lock_guard<std::mutex> lock(screenWakeMutex);
++screenWakeSerial;
}
screenWakeCondition.notify_one();
};
#else
const auto notifyScreenLoop = []() {};
#endif
const auto queueRedraw = [&]() {
if (running.load() && !redrawQueued.exchange(true)) {
screen.PostEvent(Event::Custom);
notifyScreenLoop();
}
};
std::thread worker([&]() {
try {
@@ -2475,6 +2501,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
if (signalRequested != 0) {
running.store(false);
exitLoop();
notifyScreenLoop();
break;
}
const uint64_t requestedOutputListSerial =
@@ -2485,9 +2512,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
requestedOutputListSerial,
capture->listOutputDevices(),
});
if (running.load() && !redrawQueued.exchange(true)) {
screen.PostEvent(Event::Custom);
}
queueRedraw();
}
const OutputSwitchRequest outputSwitchRequest =
@@ -2514,9 +2539,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
outcome.started,
outcome.error,
});
if (running.load() && !redrawQueued.exchange(true)) {
screen.PostEvent(Event::Custom);
}
queueRedraw();
if (!outcome.captureRunning) {
throw std::runtime_error(outcome.error);
}
@@ -2581,13 +2604,13 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
: activeStarted.deviceLabel;
next.captureOverrun = captureOverrun;
frameStore.publish(std::move(next));
if (running.load() && !redrawQueued.exchange(true)) {
screen.PostEvent(Event::Custom);
}
nextFrameAt = now +
queueRedraw();
nextFrameAt = advanceFrameDeadline(
nextFrameAt,
now,
displayFrameInterval(effectiveRefreshRate(
appliedSettings.refreshRate,
appliedSettings.terminalCompatibility));
appliedSettings.terminalCompatibility)));
}
std::this_thread::sleep_for(kCapturePollInterval);
}
@@ -2595,6 +2618,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
workerError = std::current_exception();
if (running.exchange(false)) {
exitLoop();
notifyScreenLoop();
}
}
});
@@ -3528,7 +3552,26 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
std::exception_ptr screenError;
try {
#if defined(_WIN32)
// FTXUI 7's blocking loop enforces its 60 FPS ceiling with sleep_for.
// The default Windows timer resolution rounds that sleep to about 31 ms,
// capping redraws near 32 FPS. The capture worker already paces and
// coalesces redraws, so park this loop until that worker queues one.
Loop loop(&screen, component);
uint64_t handledWakeSerial = 0;
while (!loop.HasQuitted()) {
loop.RunOnce();
if (loop.HasQuitted()) break;
std::unique_lock<std::mutex> lock(screenWakeMutex);
screenWakeCondition.wait(lock, [&]() {
return screenWakeSerial != handledWakeSerial || !running.load();
});
handledWakeSerial = screenWakeSerial;
}
#else
screen.Loop(component);
#endif
} catch (...) {
screenError = std::current_exception();
}
+48
View File
@@ -2,6 +2,7 @@
#include "cli.h"
#include "dashboard_layout.h"
#include "display_model.h"
#include "frame_pacing.h"
#include "frame_rate_meter.h"
#include "meter_display_model.h"
#include "output_selection.h"
@@ -1119,6 +1120,52 @@ void testFrameRateMeter() {
"render telemetry should recover safely from a regressing clock");
}
void testFramePacing() {
using Clock = std::chrono::steady_clock;
using namespace std::chrono_literals;
const Clock::time_point origin{};
const auto interval = 16ms;
require(Prism::Tui::advanceFrameDeadline(origin, origin, interval) ==
origin + interval,
"an on-time frame should preserve the original cadence");
require(Prism::Tui::advanceFrameDeadline(origin, origin + 1ms, interval) ==
origin + interval,
"a slightly late frame should not shift the original cadence");
require(Prism::Tui::advanceFrameDeadline(origin, origin + 51ms, interval) ==
origin + 64ms,
"a substantially late frame should skip every missed deadline");
const auto afterLongPause = Prism::Tui::advanceFrameDeadline(
origin, origin + 24h, interval);
require(afterLongPause > origin + 24h &&
afterLongPause <= origin + 24h + interval,
"a long pause should advance directly to the first future deadline");
const auto simulateCoarseWindowsTimer = [&](int framesPerSecond) {
const auto frameInterval = std::chrono::microseconds(
1000000 / framesPerSecond);
auto deadline = origin;
size_t frames = 0;
for (auto wakeAt = origin;
wakeAt < origin + 1s;
wakeAt += 15625us) {
if (wakeAt < deadline) continue;
++frames;
deadline = Prism::Tui::advanceFrameDeadline(
deadline, wakeAt, frameInterval);
}
return frames;
};
require(simulateCoarseWindowsTimer(30) == 30,
"coarse Windows wakeups should preserve a 30 FPS cadence");
require(simulateCoarseWindowsTimer(60) == 60,
"coarse Windows wakeups should preserve a 60 FPS cadence");
require(simulateCoarseWindowsTimer(120) == 64,
"120 FPS should remain bounded by the available coarse wakeups");
}
} // namespace
int main() {
@@ -1134,6 +1181,7 @@ int main() {
testPitchReadoutResponse();
testScrollingHistory();
testPipelineAndFakeCapture();
testFramePacing();
testFrameRateMeter();
testThreadSafeSnapshots();
std::cout << "Prism TUI tests passed\n";