mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-19 03:54:23 +02:00
fix windows tui frame timing bug
This commit is contained in:
@@ -33,6 +33,7 @@ add_library(prism_tui_analysis STATIC
|
|||||||
src/cli.cpp
|
src/cli.cpp
|
||||||
src/dashboard_layout.cpp
|
src/dashboard_layout.cpp
|
||||||
src/display_model.cpp
|
src/display_model.cpp
|
||||||
|
src/frame_pacing.cpp
|
||||||
src/frame_rate_meter.cpp
|
src/frame_rate_meter.cpp
|
||||||
src/meter_display_model.cpp
|
src/meter_display_model.cpp
|
||||||
src/output_selection.cpp
|
src/output_selection.cpp
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -3,6 +3,7 @@
|
|||||||
#include "analysis_pipeline.h"
|
#include "analysis_pipeline.h"
|
||||||
#include "dashboard_layout.h"
|
#include "dashboard_layout.h"
|
||||||
#include "display_model.h"
|
#include "display_model.h"
|
||||||
|
#include "frame_pacing.h"
|
||||||
#include "frame_rate_meter.h"
|
#include "frame_rate_meter.h"
|
||||||
#include "meter_display_model.h"
|
#include "meter_display_model.h"
|
||||||
#include "output_selection.h"
|
#include "output_selection.h"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/event.hpp>
|
#include <ftxui/component/event.hpp>
|
||||||
|
#include <ftxui/component/loop.hpp>
|
||||||
#include <ftxui/component/screen_interactive.hpp>
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
#include <ftxui/dom/canvas.hpp>
|
#include <ftxui/dom/canvas.hpp>
|
||||||
#include <ftxui/dom/elements.hpp>
|
#include <ftxui/dom/elements.hpp>
|
||||||
@@ -23,12 +25,14 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@@ -2446,8 +2450,30 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
std::atomic<bool> resetRequested{false};
|
std::atomic<bool> resetRequested{false};
|
||||||
std::atomic<bool> redrawQueued{false};
|
std::atomic<bool> redrawQueued{false};
|
||||||
std::atomic<uint64_t> outputListRequested{0};
|
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;
|
std::exception_ptr workerError;
|
||||||
auto exitLoop = screen.ExitLoopClosure();
|
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([&]() {
|
std::thread worker([&]() {
|
||||||
try {
|
try {
|
||||||
@@ -2475,6 +2501,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
if (signalRequested != 0) {
|
if (signalRequested != 0) {
|
||||||
running.store(false);
|
running.store(false);
|
||||||
exitLoop();
|
exitLoop();
|
||||||
|
notifyScreenLoop();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const uint64_t requestedOutputListSerial =
|
const uint64_t requestedOutputListSerial =
|
||||||
@@ -2485,9 +2512,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
requestedOutputListSerial,
|
requestedOutputListSerial,
|
||||||
capture->listOutputDevices(),
|
capture->listOutputDevices(),
|
||||||
});
|
});
|
||||||
if (running.load() && !redrawQueued.exchange(true)) {
|
queueRedraw();
|
||||||
screen.PostEvent(Event::Custom);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OutputSwitchRequest outputSwitchRequest =
|
const OutputSwitchRequest outputSwitchRequest =
|
||||||
@@ -2514,9 +2539,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
outcome.started,
|
outcome.started,
|
||||||
outcome.error,
|
outcome.error,
|
||||||
});
|
});
|
||||||
if (running.load() && !redrawQueued.exchange(true)) {
|
queueRedraw();
|
||||||
screen.PostEvent(Event::Custom);
|
|
||||||
}
|
|
||||||
if (!outcome.captureRunning) {
|
if (!outcome.captureRunning) {
|
||||||
throw std::runtime_error(outcome.error);
|
throw std::runtime_error(outcome.error);
|
||||||
}
|
}
|
||||||
@@ -2581,13 +2604,13 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
: activeStarted.deviceLabel;
|
: activeStarted.deviceLabel;
|
||||||
next.captureOverrun = captureOverrun;
|
next.captureOverrun = captureOverrun;
|
||||||
frameStore.publish(std::move(next));
|
frameStore.publish(std::move(next));
|
||||||
if (running.load() && !redrawQueued.exchange(true)) {
|
queueRedraw();
|
||||||
screen.PostEvent(Event::Custom);
|
nextFrameAt = advanceFrameDeadline(
|
||||||
}
|
nextFrameAt,
|
||||||
nextFrameAt = now +
|
now,
|
||||||
displayFrameInterval(effectiveRefreshRate(
|
displayFrameInterval(effectiveRefreshRate(
|
||||||
appliedSettings.refreshRate,
|
appliedSettings.refreshRate,
|
||||||
appliedSettings.terminalCompatibility));
|
appliedSettings.terminalCompatibility)));
|
||||||
}
|
}
|
||||||
std::this_thread::sleep_for(kCapturePollInterval);
|
std::this_thread::sleep_for(kCapturePollInterval);
|
||||||
}
|
}
|
||||||
@@ -2595,6 +2618,7 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
workerError = std::current_exception();
|
workerError = std::current_exception();
|
||||||
if (running.exchange(false)) {
|
if (running.exchange(false)) {
|
||||||
exitLoop();
|
exitLoop();
|
||||||
|
notifyScreenLoop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3528,7 +3552,26 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
|
|
||||||
std::exception_ptr screenError;
|
std::exception_ptr screenError;
|
||||||
try {
|
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);
|
screen.Loop(component);
|
||||||
|
#endif
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
screenError = std::current_exception();
|
screenError = std::current_exception();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
#include "dashboard_layout.h"
|
#include "dashboard_layout.h"
|
||||||
#include "display_model.h"
|
#include "display_model.h"
|
||||||
|
#include "frame_pacing.h"
|
||||||
#include "frame_rate_meter.h"
|
#include "frame_rate_meter.h"
|
||||||
#include "meter_display_model.h"
|
#include "meter_display_model.h"
|
||||||
#include "output_selection.h"
|
#include "output_selection.h"
|
||||||
@@ -1119,6 +1120,52 @@ void testFrameRateMeter() {
|
|||||||
"render telemetry should recover safely from a regressing clock");
|
"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
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -1134,6 +1181,7 @@ int main() {
|
|||||||
testPitchReadoutResponse();
|
testPitchReadoutResponse();
|
||||||
testScrollingHistory();
|
testScrollingHistory();
|
||||||
testPipelineAndFakeCapture();
|
testPipelineAndFakeCapture();
|
||||||
|
testFramePacing();
|
||||||
testFrameRateMeter();
|
testFrameRateMeter();
|
||||||
testThreadSafeSnapshots();
|
testThreadSafeSnapshots();
|
||||||
std::cout << "Prism TUI tests passed\n";
|
std::cout << "Prism TUI tests passed\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user