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