terminal compatibility modes and fps counter

This commit is contained in:
Boof2015
2026-08-15 19:55:15 -04:00
parent b691178d65
commit 0763d84d84
8 changed files with 312 additions and 16 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_rate_meter.cpp
src/meter_display_model.cpp
src/output_selection.cpp
src/profile_library.cpp
+46
View File
@@ -0,0 +1,46 @@
#include "frame_rate_meter.h"
#include <algorithm>
#include <cmath>
namespace Prism::Tui {
FrameRateMeter::FrameRateMeter(double windowSeconds)
: windowSeconds_(
std::isfinite(windowSeconds)
? std::clamp(windowSeconds, 0.25, 5.0)
: 1.0) {}
double FrameRateMeter::record(double timestampSeconds) {
if (!std::isfinite(timestampSeconds)) return framesPerSecond_;
if (!timestamps_.empty() && timestampSeconds <= timestamps_.back()) {
if (timestampSeconds < timestamps_.back()) reset();
return framesPerSecond_;
}
timestamps_.push_back(timestampSeconds);
const double cutoff = timestampSeconds - windowSeconds_;
while (timestamps_.size() > 2 && timestamps_[1] <= cutoff) {
timestamps_.pop_front();
}
if (timestamps_.size() < 2) {
framesPerSecond_ = 0.0;
return framesPerSecond_;
}
const double elapsed = timestamps_.back() - timestamps_.front();
const double minimumSampleWindow = std::min(0.25, windowSeconds_);
if (elapsed < minimumSampleWindow) return framesPerSecond_;
framesPerSecond_ = elapsed > 0.0
? static_cast<double>(timestamps_.size() - 1) / elapsed
: framesPerSecond_;
return framesPerSecond_;
}
void FrameRateMeter::reset() {
timestamps_.clear();
framesPerSecond_ = 0.0;
}
} // namespace Prism::Tui
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <cstddef>
#include <deque>
namespace Prism::Tui {
class FrameRateMeter {
public:
explicit FrameRateMeter(double windowSeconds = 1.0);
double record(double timestampSeconds);
double framesPerSecond() const { return framesPerSecond_; }
void reset();
private:
double windowSeconds_ = 1.0;
double framesPerSecond_ = 0.0;
std::deque<double> timestamps_;
};
} // namespace Prism::Tui
+4
View File
@@ -430,6 +430,8 @@ bool profileSettingsEqual(const TuiSettings& left,
TuiSettings normalizedLeft = normalizeSettings(left);
TuiSettings normalizedRight = normalizeSettings(right);
normalizedLeft.refreshRate = normalizedRight.refreshRate;
normalizedLeft.terminalCompatibility =
normalizedRight.terminalCompatibility;
return normalizedLeft == normalizedRight;
}
@@ -437,6 +439,8 @@ TuiSettings applyProfileSettings(const TuiSettings& profile,
const TuiSettings& working) {
TuiSettings applied = normalizeSettings(profile);
applied.refreshRate = normalizeSettings(working).refreshRate;
applied.terminalCompatibility =
normalizeSettings(working).terminalCompatibility;
return applied;
}
+118 -6
View File
@@ -3,6 +3,7 @@
#include "analysis_pipeline.h"
#include "dashboard_layout.h"
#include "display_model.h"
#include "frame_rate_meter.h"
#include "meter_display_model.h"
#include "output_selection.h"
#include "profile_library.h"
@@ -200,9 +201,12 @@ struct InterfaceState {
uint64_t appliedOutputSwitchSerial = 0;
uint64_t outputListSerial = 0;
uint64_t appliedOutputListSerial = 0;
double renderedFramesPerSecond = 0.0;
};
const TuiTheme* renderTheme = nullptr;
TerminalCompatibilityMode renderTerminalCompatibility =
TerminalCompatibilityMode::Modern;
const TuiTheme& palette() {
static const TuiTheme fallback = defaultTuiTheme();
@@ -210,7 +214,83 @@ const TuiTheme& palette() {
}
ftxui::Color terminalColor(const ThemeColor& color) {
return ftxui::Color::RGB(color.red, color.green, color.blue);
using Color = ftxui::Color;
if (renderTerminalCompatibility == TerminalCompatibilityMode::Modern) {
return Color::RGB(color.red, color.green, color.blue);
}
if (renderTerminalCompatibility == TerminalCompatibilityMode::Compatible) {
constexpr std::array<int, 6> levels = {0, 95, 135, 175, 215, 255};
const auto nearestLevel = [&](int channel) {
size_t nearest = 0;
int nearestDistance = 256;
for (size_t index = 0; index < levels.size(); ++index) {
const int distance = std::abs(channel - levels[index]);
if (distance < nearestDistance) {
nearest = index;
nearestDistance = distance;
}
}
return nearest;
};
const size_t red = nearestLevel(color.red);
const size_t green = nearestLevel(color.green);
const size_t blue = nearestLevel(color.blue);
const int cubeRed = levels[red];
const int cubeGreen = levels[green];
const int cubeBlue = levels[blue];
const int cubeDistance =
(static_cast<int>(color.red) - cubeRed) *
(static_cast<int>(color.red) - cubeRed) +
(static_cast<int>(color.green) - cubeGreen) *
(static_cast<int>(color.green) - cubeGreen) +
(static_cast<int>(color.blue) - cubeBlue) *
(static_cast<int>(color.blue) - cubeBlue);
const int average = (
static_cast<int>(color.red) + color.green + color.blue) / 3;
const int grayIndex = std::clamp(
static_cast<int>(std::lround((average - 8) / 10.0)), 0, 23);
const int gray = 8 + grayIndex * 10;
const int grayDistance =
(static_cast<int>(color.red) - gray) *
(static_cast<int>(color.red) - gray) +
(static_cast<int>(color.green) - gray) *
(static_cast<int>(color.green) - gray) +
(static_cast<int>(color.blue) - gray) *
(static_cast<int>(color.blue) - gray);
const int index = grayDistance < cubeDistance
? 232 + grayIndex
: 16 + static_cast<int>(red * 36 + green * 6 + blue);
return Color(static_cast<Color::Palette256>(index));
}
static constexpr std::array<std::array<int, 3>, 16> ansi = {{
{{0, 0, 0}}, {{205, 0, 0}}, {{0, 205, 0}}, {{205, 205, 0}},
{{0, 0, 238}}, {{205, 0, 205}}, {{0, 205, 205}}, {{229, 229, 229}},
{{127, 127, 127}}, {{255, 0, 0}}, {{0, 255, 0}}, {{255, 255, 0}},
{{92, 92, 255}}, {{255, 0, 255}}, {{0, 255, 255}}, {{255, 255, 255}},
}};
size_t nearest = 0;
int nearestDistance = 3 * 256 * 256;
for (size_t index = 0; index < ansi.size(); ++index) {
const int red = static_cast<int>(color.red) - ansi[index][0];
const int green = static_cast<int>(color.green) - ansi[index][1];
const int blue = static_cast<int>(color.blue) - ansi[index][2];
const int distance = red * red + green * green + blue * blue;
if (distance < nearestDistance) {
nearest = index;
nearestDistance = distance;
}
}
return Color(static_cast<Color::Palette16>(nearest));
}
ftxui::Color terminalRgb(int red, int green, int blue) {
return terminalColor({
static_cast<uint8_t>(std::clamp(red, 0, 255)),
static_cast<uint8_t>(std::clamp(green, 0, 255)),
static_cast<uint8_t>(std::clamp(blue, 0, 255)),
});
}
void fillCanvasBackground(ftxui::Canvas& surface,
@@ -268,6 +348,7 @@ const size_t& settingsSelection(const InterfaceState& state) {
std::string makeCaptureStatus(const DisplayFrame& frame,
const TuiSettings& settings,
double renderedFramesPerSecond,
bool compact) {
std::ostringstream sampleRate;
const double kilohertz = frame.sampleRate / 1000.0;
@@ -287,6 +368,19 @@ std::string makeCaptureStatus(const DisplayFrame& frame,
if (frame.captureOverrun) {
footer += " • capture overrun";
}
if (!compact) {
const int target = effectiveRefreshRate(
settings.refreshRate, settings.terminalCompatibility);
footer += "";
if (renderedFramesPerSecond > 0.0 &&
std::isfinite(renderedFramesPerSecond)) {
footer += std::to_string(static_cast<int>(
std::lround(renderedFramesPerSecond)));
} else {
footer += "--";
}
footer += "/" + std::to_string(target) + " FPS";
}
return footer;
}
@@ -1185,7 +1279,7 @@ ftxui::Color interpolateColor(const ThemeColor& from,
const ThemeColor& to,
float amount) {
const float t = std::clamp(amount, 0.0f, 1.0f);
return ftxui::Color::RGB(
return terminalRgb(
static_cast<uint8_t>(std::lround(from.red +
(static_cast<int>(to.red) - from.red) * t)),
static_cast<uint8_t>(std::lround(from.green +
@@ -1448,7 +1542,7 @@ ftxui::Color waveformBandColor(const float* summary, bool multiband) {
weights[band] * static_cast<float>(value)));
}
}
return ftxui::Color::RGB(
return terminalRgb(
std::clamp(mixed[0], 0, 255),
std::clamp(mixed[1], 0, 255),
std::clamp(mixed[2], 0, 255));
@@ -1706,7 +1800,11 @@ ftxui::Element renderFooter(const DisplayFrame& frame,
" • o outputs • p profiles • s settings • q quit"
: "Tab focus • Enter " + enterAction +
" • o outputs • p profiles • s settings • l edit layout • r reset • q quit";
auto status = text(makeCaptureStatus(frame, state.settings, compact)) | dim;
auto status = text(makeCaptureStatus(
frame,
state.settings,
state.renderedFramesPerSecond,
compact)) | dim;
if (frame.captureOverrun) {
status = status | color(terminalColor(palette().danger));
}
@@ -2395,7 +2493,12 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
const TuiSettings requestedSettings = settingsStore.read();
if (requestedSettings != appliedSettings) {
const bool refreshChanged =
requestedSettings.refreshRate != appliedSettings.refreshRate;
effectiveRefreshRate(
requestedSettings.refreshRate,
requestedSettings.terminalCompatibility) !=
effectiveRefreshRate(
appliedSettings.refreshRate,
appliedSettings.terminalCompatibility);
pipeline->setInputTrimDb(requestedSettings.inputTrimDb);
pipeline->setSpectrumTilt(
requestedSettings.spectrumTiltDbPerOctave);
@@ -2446,7 +2549,9 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
screen.PostEvent(Event::Custom);
}
nextFrameAt = now +
displayFrameInterval(appliedSettings.refreshRate);
displayFrameInterval(effectiveRefreshRate(
appliedSettings.refreshRate,
appliedSettings.terminalCompatibility));
}
std::this_thread::sleep_for(kCapturePollInterval);
}
@@ -2458,8 +2563,15 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
}
});
FrameRateMeter frameRateMeter;
auto renderer = Renderer([&]() {
renderTheme = &interfaceState.theme;
renderTerminalCompatibility =
interfaceState.settings.terminalCompatibility;
const double renderedAt = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count();
interfaceState.renderedFramesPerSecond =
frameRateMeter.record(renderedAt);
return renderFrame(
frameStore.read(), screen.dimx(), screen.dimy(), interfaceState) |
color(terminalColor(interfaceState.theme.text)) |
+61 -4
View File
@@ -15,6 +15,7 @@ namespace {
const std::vector<SettingDescriptor> kAppearanceSettings = {
{SettingId::Theme, "Theme", "Uses Prism .iro files from the shared Prism Themes folder."},
{SettingId::TerminalCompatibility, "Terminal mode", "Modern uses truecolor; Compatible uses 256 colors at up to 60 FPS; Safe uses ANSI colors at 30 FPS."},
};
const std::vector<SettingDescriptor> kGeneralSettings = {
@@ -153,6 +154,15 @@ std::string serializeWaveformMode(WaveformMode mode) {
return mode == WaveformMode::Stereo ? "stereo" : "mono";
}
std::string serializeTerminalCompatibility(TerminalCompatibilityMode mode) {
switch (mode) {
case TerminalCompatibilityMode::Modern: return "modern";
case TerminalCompatibilityMode::Compatible: return "compatible";
case TerminalCompatibilityMode::Safe: return "safe";
}
return "modern";
}
bool parseBool(const std::string& value, bool fallback) {
if (value == "true" || value == "1" || value == "on") return true;
if (value == "false" || value == "0" || value == "off") return false;
@@ -259,6 +269,15 @@ WaveformMode parseWaveformMode(const std::string& value, WaveformMode fallback)
return fallback;
}
TerminalCompatibilityMode parseTerminalCompatibility(
const std::string& value,
TerminalCompatibilityMode fallback) {
if (value == "modern") return TerminalCompatibilityMode::Modern;
if (value == "compatible") return TerminalCompatibilityMode::Compatible;
if (value == "safe") return TerminalCompatibilityMode::Safe;
return fallback;
}
const char* environmentValue(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] != '\0' ? value : nullptr;
@@ -298,6 +317,30 @@ const char* waveformModeName(WaveformMode mode) {
return mode == WaveformMode::Stereo ? "Stereo" : "Mono";
}
const char* terminalCompatibilityName(TerminalCompatibilityMode mode) {
switch (mode) {
case TerminalCompatibilityMode::Modern: return "Modern";
case TerminalCompatibilityMode::Compatible: return "Compatible";
case TerminalCompatibilityMode::Safe: return "Safe";
}
return "Modern";
}
int effectiveRefreshRate(int requestedRate, TerminalCompatibilityMode mode) {
const int normalized = requestedRate <= 30
? 30
: requestedRate <= 60 ? 60 : 120;
switch (mode) {
case TerminalCompatibilityMode::Modern:
return normalized;
case TerminalCompatibilityMode::Compatible:
return std::min(normalized, 60);
case TerminalCompatibilityMode::Safe:
return 30;
}
return normalized;
}
TuiSettings normalizeSettings(TuiSettings settings) {
settings.themeId.erase(std::remove_if(
settings.themeId.begin(), settings.themeId.end(), [](char character) {
@@ -329,6 +372,7 @@ TuiSettings normalizeSettings(TuiSettings settings) {
bool operator==(const TuiSettings& left, const TuiSettings& right) {
return left.themeId == right.themeId &&
left.terminalCompatibility == right.terminalCompatibility &&
left.inputTrimDb == right.inputTrimDb &&
left.refreshRate == right.refreshRate &&
left.rackLayout == right.rackLayout &&
@@ -428,6 +472,8 @@ std::string settingValue(const TuiSettings& settings, SettingId setting) {
switch (setting) {
case SettingId::Theme:
return settings.themeId;
case SettingId::TerminalCompatibility:
return terminalCompatibilityName(settings.terminalCompatibility);
case SettingId::InputTrim:
return (settings.inputTrimDb > 0.0f ? "+" : "") +
trimFloat(settings.inputTrimDb, 1) + " dB";
@@ -502,6 +548,13 @@ bool adjustSetting(TuiSettings& settings, SettingId setting, int direction) {
switch (setting) {
case SettingId::Theme:
return false;
case SettingId::TerminalCompatibility: {
int value = static_cast<int>(settings.terminalCompatibility);
value = (value + (direction > 0 ? 1 : 2)) % 3;
settings.terminalCompatibility =
static_cast<TerminalCompatibilityMode>(value);
break;
}
case SettingId::InputTrim:
settings.inputTrimDb += direction > 0 ? 0.5f : -0.5f;
break;
@@ -634,6 +687,7 @@ bool resetSetting(TuiSettings& settings, SettingId setting) {
const TuiSettings before = settings;
switch (setting) {
case SettingId::Theme: settings.themeId = defaults.themeId; break;
case SettingId::TerminalCompatibility: settings.terminalCompatibility = defaults.terminalCompatibility; break;
case SettingId::InputTrim: settings.inputTrimDb = defaults.inputTrimDb; break;
case SettingId::RefreshRate: settings.refreshRate = defaults.refreshRate; break;
case SettingId::SpectrumPeakReadout: settings.spectrumPeakReadout = defaults.spectrumPeakReadout; break;
@@ -704,6 +758,7 @@ TuiSettings parseSettingsText(const std::string& text,
const std::string key = line.substr(0, separator);
const std::string value = line.substr(separator + 1);
if (key == "theme_id") settings.themeId = value;
else if (key == "terminal_mode") settings.terminalCompatibility = parseTerminalCompatibility(value, settings.terminalCompatibility);
else if (key == "input_trim_db") settings.inputTrimDb = parseFloat(value, settings.inputTrimDb);
else if (key == "refresh_rate") settings.refreshRate = parseInt(value, settings.refreshRate);
else if (key == "rack_layout") settings.rackLayout = parseRackLayout(value, settings.rackLayout);
@@ -738,12 +793,14 @@ std::string serializeSettingsText(const TuiSettings& rawSettings,
bool includeRefreshRate) {
const TuiSettings settings = normalizeSettings(rawSettings);
std::ostringstream output;
output << "theme_id=" << settings.themeId << '\n'
<< "input_trim_db=" << settings.inputTrimDb << '\n';
output << "theme_id=" << settings.themeId << '\n';
if (includeRefreshRate) {
output << "refresh_rate=" << settings.refreshRate << '\n';
output << "terminal_mode="
<< serializeTerminalCompatibility(settings.terminalCompatibility) << '\n'
<< "refresh_rate=" << settings.refreshRate << '\n';
}
output << "rack_layout=" << serializeRackLayout(settings.rackLayout) << '\n'
output << "input_trim_db=" << settings.inputTrimDb << '\n'
<< "rack_layout=" << serializeRackLayout(settings.rackLayout) << '\n'
<< "spectrum_peak=" << (settings.spectrumPeakReadout ? "true" : "false") << '\n'
<< "spectrum_tilt=" << settings.spectrumTiltDbPerOctave << '\n'
<< "osc_pitch_lock=" << (settings.oscilloscopePitchLock ? "true" : "false") << '\n'
+6
View File
@@ -25,6 +25,7 @@ enum class SettingsPage {
enum class SettingId {
Theme,
TerminalCompatibility,
InputTrim,
RefreshRate,
SpectrumPeakReadout,
@@ -63,9 +64,12 @@ enum class SpectrogramClarity { Classic, Sharp, Sharper };
enum class SpectrogramScale { Mel, Logarithmic, Linear };
enum class SpectrogramOrientation { Horizontal, Vertical };
enum class WaveformMode { Mono, Stereo };
enum class TerminalCompatibilityMode { Modern, Compatible, Safe };
struct TuiSettings {
std::string themeId = "Default";
TerminalCompatibilityMode terminalCompatibility =
TerminalCompatibilityMode::Modern;
float inputTrimDb = 0.0f;
int refreshRate = 60;
RackLayout rackLayout = defaultRackLayout();
@@ -119,6 +123,8 @@ const char* spectrogramClarityName(SpectrogramClarity clarity);
const char* spectrogramScaleName(SpectrogramScale scale);
const char* spectrogramOrientationName(SpectrogramOrientation orientation);
const char* waveformModeName(WaveformMode mode);
const char* terminalCompatibilityName(TerminalCompatibilityMode mode);
int effectiveRefreshRate(int requestedRate, TerminalCompatibilityMode mode);
std::filesystem::path defaultSettingsPath();
TuiSettings parseSettingsText(const std::string& text,
+54 -6
View File
@@ -2,6 +2,7 @@
#include "cli.h"
#include "dashboard_layout.h"
#include "display_model.h"
#include "frame_rate_meter.h"
#include "meter_display_model.h"
#include "output_selection.h"
#include "profile_library.h"
@@ -549,10 +550,29 @@ void testSettingsModelAndPersistence() {
const auto pages = Prism::Tui::settingsPages();
require(pages.size() == 9 &&
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::Appearance).size() == 1 &&
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::Appearance).size() == 2 &&
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::General).size() == 2,
"settings should expose shallow category pages");
Prism::Tui::TuiSettings adjusted;
require(Prism::Tui::adjustSetting(
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
adjusted.terminalCompatibility ==
Prism::Tui::TerminalCompatibilityMode::Compatible &&
Prism::Tui::effectiveRefreshRate(
120, adjusted.terminalCompatibility) == 60,
"compatible terminals should use the 256-color mode with a 60 FPS cap");
require(Prism::Tui::adjustSetting(
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
adjusted.terminalCompatibility ==
Prism::Tui::TerminalCompatibilityMode::Safe &&
Prism::Tui::effectiveRefreshRate(
120, adjusted.terminalCompatibility) == 30,
"safe terminals should use the ANSI mode with a 30 FPS cap");
require(Prism::Tui::adjustSetting(
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
adjusted.terminalCompatibility ==
Prism::Tui::TerminalCompatibilityMode::Modern,
"terminal compatibility should cycle back to modern truecolor");
require(Prism::Tui::adjustSetting(
adjusted, Prism::Tui::SettingId::RefreshRate, 1) &&
adjusted.refreshRate == 120 &&
@@ -623,6 +643,8 @@ void testSettingsModelAndPersistence() {
adjusted.vectorscopeMode = Prism::Tui::VectorscopeMode::PolarBipolar;
adjusted.vectorscopeDetail = Prism::Tui::VectorscopeDetail::Maximum;
adjusted.themeId = "Test Theme";
adjusted.terminalCompatibility =
Prism::Tui::TerminalCompatibilityMode::Compatible;
std::string error;
require(Prism::Tui::saveSettings(adjusted, settingsPath, &error),
"settings should persist to a TUI-specific configuration file");
@@ -650,6 +672,8 @@ void testProfileLibrary() {
Prism::Tui::TuiSettings settings;
settings.themeId = "Test Theme";
settings.terminalCompatibility =
Prism::Tui::TerminalCompatibilityMode::Safe;
settings.inputTrimDb = 4.0f;
settings.refreshRate = 30;
settings.spectrogramContrast = 1.7f;
@@ -676,20 +700,26 @@ void testProfileLibrary() {
foundProfileFile = true;
require(text.find("format=prism-tui-profile") != std::string::npos &&
text.find("theme_id=Test Theme") != std::string::npos &&
text.find("refresh_rate=") == std::string::npos,
".prsmt files should retain themes while excluding global refresh settings");
text.find("refresh_rate=") == std::string::npos &&
text.find("terminal_mode=") == std::string::npos,
".prsmt files should retain themes while excluding terminal runtime settings");
}
require(foundProfileFile,
"saved TUI profiles should use the .prsmt extension");
Prism::Tui::TuiSettings differentRefresh = settings;
differentRefresh.refreshRate = 60;
differentRefresh.terminalCompatibility =
Prism::Tui::TerminalCompatibilityMode::Modern;
require(Prism::Tui::profileSettingsEqual(settings, differentRefresh),
"refresh rate should not make an active profile dirty");
"terminal runtime settings should not make an active profile dirty");
const auto applied = Prism::Tui::applyProfileSettings(
settings, differentRefresh);
require(applied.refreshRate == 60 && applied.inputTrimDb == 4.0f,
"loading a profile should preserve global refresh rate while applying trim");
require(applied.refreshRate == 60 &&
applied.terminalCompatibility ==
Prism::Tui::TerminalCompatibilityMode::Modern &&
applied.inputTrimDb == 4.0f,
"loading a profile should preserve terminal runtime settings while applying trim");
settings.waveformMultiband = true;
require(library.overwrite(profileId, settings, &error) &&
@@ -1032,6 +1062,23 @@ void testThreadSafeSnapshots() {
"immutable display snapshots should publish safely across threads");
}
void testFrameRateMeter() {
Prism::Tui::FrameRateMeter meter;
require(meter.record(0.0) == 0.0 && meter.record(0.01) == 0.0,
"render telemetry should wait for a stable sample window");
meter.reset();
for (int frame = 0; frame <= 60; ++frame) {
meter.record(static_cast<double>(frame) / 60.0);
}
require(std::abs(meter.framesPerSecond() - 60.0) < 0.1,
"render telemetry should measure a deterministic 60 FPS cadence");
meter.reset();
require(meter.framesPerSecond() == 0.0 && meter.record(5.0) == 0.0,
"render telemetry should reset without inventing an initial frame rate");
require(meter.record(4.0) == 0.0,
"render telemetry should recover safely from a regressing clock");
}
} // namespace
int main() {
@@ -1047,6 +1094,7 @@ int main() {
testPitchReadoutResponse();
testScrollingHistory();
testPipelineAndFakeCapture();
testFrameRateMeter();
testThreadSafeSnapshots();
std::cout << "Prism TUI tests passed\n";
return 0;