Program Listing for File fps_readout.hpp#
↰ Return to documentation for file (src/video/fps_readout.hpp)
#pragma once
namespace mosaic {
// Decides what a camera's "achievable frame rate" readout should say, given
// the camera's own measured rate plus the exposure/frame-rate settings the
// user currently has selected.
//
// Pure, Qt-free and hardware-free so it can be unit-tested directly — the
// same split already used for rms_quality_for()/pose_tracking_quality_for():
// this decides *what is true*, and CameraCardW decides how to word and colour
// it.
//
// The one thing this deliberately never does is predict a frame rate from the
// exposure time and present it as fact. Exposure is only one of three limits
// (sensor readout time and GigE bandwidth are the others), and on this rig
// bandwidth is the binding constraint on at least one camera — so a
// client-side 1/exposure figure would be confidently wrong exactly when it
// matters most. The camera's own ResultingFrameRate already accounts for all
// three, so a real measurement is always preferred; the exposure figure is
// only ever offered as an upper *bound*, clearly distinguished from a
// measurement (see FpsReadoutKind::ExposureCeiling).
// A camera counts as unable to sustain its configured rate below this
// fraction of it. Shared with VideoGrabber::refresh_achievable_fps()'s own
// "requested X fps but the camera can only sustain ~Y" log warning, so the
// on-screen readout and the log can never disagree about what counts as a
// shortfall.
inline constexpr double k_fps_shortfall_factor = 0.9;
// Smallest change in a camera's measured rate worth telling the UI about.
// refresh_achievable_fps() re-measures every ~2s per camera and a stable
// camera's reading jitters in the third decimal place, so announcing every
// re-read would repaint every card's readout continuously to no purpose.
inline constexpr double k_fps_change_epsilon = 0.05;
// A measurement within this fraction of the configured rate counts as "at the
// cap": the camera is delivering exactly what was asked for, so the configured
// rate — not exposure, sensor readout or bandwidth — is what binds. Relative
// rather than absolute so it holds at 5 fps and at 200 fps alike; the real
// acA1920-25gc cameras on this rig report 14.9999 against a configured 15.
inline constexpr double k_fps_at_cap_tolerance = 0.01;
enum class FpsReadoutKind {
// No trustworthy measurement yet: the camera isn't open, or it opened so
// recently that its own reading is still inside the warm-up window (see
// is_achievable_fps_measurement_warmed_up()). Exposure is on auto, so
// there's no honest upper bound to offer either — the camera picks the
// exposure itself and we can't know it until it runs.
AwaitingMeasurement,
// No measurement, but exposure is manual, so the exposure time alone
// imposes a hard ceiling: a sensor cannot produce frames faster than it
// exposes them. An upper bound, never a prediction — the real rate is
// usually lower once readout and bandwidth are accounted for.
ExposureCeiling,
// The camera's own measured ResultingFrameRate.
Measured,
};
// Which constraint is actually holding the rate down. Knowing the number
// alone isn't enough to act on it: a camera pinned to 15 fps reports 15 fps
// no matter how the exposure control is moved, which reads as a dead widget
// unless the UI says *why*.
enum class FpsLimit {
// Nothing can be said: no measurement to classify, no configured rate to
// compare one against, or a measurement running *faster* than the
// configured rate — in which case that rate is demonstrably not capping
// anything.
Unknown,
// The camera is hitting the rate it was pinned to. Exposure is not the
// binding constraint, and moving it will change nothing until it crosses
// exposureCrossoverUs.
ConfiguredRate,
// Measurably below the configured rate: exposure, sensor readout time or
// link bandwidth is binding instead. Which of the three, only the camera
// knows — ResultingFrameRate is a single number.
Other,
};
struct FpsReadout {
FpsReadoutKind kind = FpsReadoutKind::AwaitingMeasurement;
// Frames per second for Measured/ExposureCeiling; -1.0 for
// AwaitingMeasurement.
double fps = -1.0;
// True when `fps` falls meaningfully short of the configured frame rate.
// Always false when the user hasn't pinned a frame rate (specifyFps), and
// always false for AwaitingMeasurement — there is nothing to compare.
bool belowConfigured = false;
// Which constraint is holding `fps` down. Only ever classified for
// Measured readouts with a configured rate to compare against;
// ExposureCeiling stays Unknown because its own wording already names
// exposure as the limit.
FpsLimit limitedBy = FpsLimit::Unknown;
// The exposure time, in microseconds, above which exposure alone would
// push the rate below the configured cap: 1e6 / configuredFps. This is
// the answer to "why does moving the exposure control do nothing?" — it
// does nothing until it passes this value. Only meaningful when
// limitedBy == ConfiguredRate; -1.0 otherwise.
//
// A slight over-estimate by design: ResultingFrameRate also charges
// sensor readout time, so the real crossover sits a little below this.
// Callers should present it as approximate.
double exposureCrossoverUs = -1.0;
};
// @param measuredFps VideoGrabber::achievable_fps() — <= 0 means "no
// trustworthy reading yet", which is its documented
// sentinel, not an error.
// @param specifyFps CameraParameters::specifyFps — whether the user has
// pinned a target frame rate at all.
// @param configuredFps CameraParameters::fps.
// @param manualExposure True iff CameraParameters::exposureAuto == "Off".
// Under "Once"/"Continuous" the camera chooses its own
// exposure, so exposureTimeUs says nothing about what
// it will actually use.
// @param exposureTimeUs CameraParameters::exposureTimeUs.
[[nodiscard]] FpsReadout compute_fps_readout(double measuredFps, bool specifyFps,
double configuredFps, bool manualExposure,
double exposureTimeUs);
} // namespace mosaic