gled 2.29.0

gled is an application for creating animations and effects on artnet or dmx installations
//! Frame pacing and frame statistics of the render thread.

use std::time::{Duration, Instant};
use tracing::debug;

/// The OS sleep ends this much before the frame deadline; the rest is spent
/// yielding, because sleeps overshoot by tens to hundreds of microseconds.
const SLEEP_MARGIN: Duration = Duration::from_micros(300);
/// How often frame statistics are logged and reported.
const REPORT_INTERVAL: Duration = Duration::from_secs(1);
/// Startup frames are much slower than steady-state frames (pipeline creation),
/// so they are left out of the worst-case frame time.
const WARMUP_FRAMES: u64 = 10;

/// Paces the render loop to the configured frame rate limit.
#[derive(Debug)]
pub(super) struct FramePacer {
    last_frame: Instant,
}

impl FramePacer {
    pub(super) fn new() -> Self {
        Self {
            last_frame: Instant::now(),
        }
    }

    /// Block until `1 / fps_limit` has passed since the previous call returned.
    /// A late frame starts the next interval from now, so there is no catch-up
    /// burst. An infinite limit does not wait at all; the GPU then paces the
    /// loop through the frames-in-flight limit.
    pub(super) fn wait_for_next_frame(&mut self, fps_limit: f32) {
        if fps_limit.is_infinite() {
            self.last_frame = Instant::now();
            return;
        }
        let interval = Duration::from_secs_f32(1.0 / fps_limit.max(1.0));
        let deadline = self.last_frame + interval;

        if let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
            if remaining > SLEEP_MARGIN {
                std::thread::sleep(remaining - SLEEP_MARGIN);
            }
            while Instant::now() < deadline {
                std::thread::yield_now();
            }
        }

        self.last_frame = Instant::now();
    }
}

/// Average frame rate and worst frame time of the render thread.
#[derive(Debug)]
pub(super) struct FrameStats {
    window_start: Instant,
    window_frames: u32,
    last_frame: Instant,
    max_frame: Duration,
    total_frames: u64,
}

impl FrameStats {
    pub(super) fn new() -> Self {
        let now = Instant::now();
        Self {
            window_start: now,
            window_frames: 0,
            last_frame: now,
            max_frame: Duration::ZERO,
            total_frames: 0,
        }
    }

    /// Record a rendered frame. Once per `REPORT_INTERVAL` this logs the
    /// `frame_stats` line (parsed by `tests/fps.rs`) and returns the average fps.
    pub(super) fn record_frame(&mut self) -> Option<f32> {
        let now = Instant::now();
        self.total_frames += 1;
        if self.total_frames > WARMUP_FRAMES {
            self.max_frame = self.max_frame.max(now - self.last_frame);
        }
        self.last_frame = now;
        self.window_frames += 1;

        let elapsed = now - self.window_start;
        if elapsed < REPORT_INTERVAL {
            return None;
        }

        let fps = self.window_frames as f32 / elapsed.as_secs_f32();
        let max_frame_ms = self.max_frame.as_secs_f32() * 1000.0;
        debug!("frame_stats: fps={fps:.1} max_frame_ms={max_frame_ms:.3}");

        self.window_start = now;
        self.window_frames = 0;
        self.max_frame = Duration::ZERO;

        Some(fps)
    }
}