germterm 0.4.0

A lightweight high-performance terminal graphics framework!
Documentation
use std::thread::sleep;
use std::time::{Duration, Instant};

pub struct FpsLimiter {
    target_frametime: Duration,
    next_frame_timestamp: Instant,
    poll_interval_sec: Duration,
    spin_reserve_sec: Duration,
}

impl FpsLimiter {
    pub fn new(fps_limit: u32, poll_interval_sec: f32, spin_reserve_sec: f32) -> Self {
        let target_frametime: Duration = calc_target_frametime(fps_limit as f32);

        Self {
            target_frametime,
            next_frame_timestamp: Instant::now()
                .checked_add(target_frametime)
                .unwrap_or_else(Instant::now),
            poll_interval_sec: Duration::from_secs_f32(poll_interval_sec),
            spin_reserve_sec: Duration::from_secs_f32(spin_reserve_sec),
        }
    }
}

pub fn limit_fps(fps_limiter: &mut FpsLimiter, value: u32) {
    let target_frametime: Duration = calc_target_frametime(value as f32);

    fps_limiter.target_frametime = target_frametime;
    fps_limiter.next_frame_timestamp = Instant::now()
        .checked_add(target_frametime)
        .unwrap_or_else(Instant::now);
}

pub fn wait_for_next_frame(fps_limiter: &mut FpsLimiter) -> f32 {
    if fps_limiter.target_frametime == Duration::ZERO {
        let delta_time: f32 = calc_delta_time(fps_limiter.next_frame_timestamp, Duration::ZERO);
        fps_limiter.next_frame_timestamp = Instant::now();
        return delta_time;
    }

    while Instant::now()
        .checked_add(fps_limiter.spin_reserve_sec)
        .unwrap_or_else(Instant::now)
        < fps_limiter.next_frame_timestamp
    {
        let remaining = fps_limiter
            .next_frame_timestamp
            .saturating_duration_since(Instant::now())
            .saturating_sub(fps_limiter.spin_reserve_sec);
        sleep(fps_limiter.poll_interval_sec.min(remaining));
    }

    // Busy wait at the end for precision
    while Instant::now() < fps_limiter.next_frame_timestamp {
        std::hint::spin_loop();
    }

    let delta_time: f32 = calc_delta_time(
        fps_limiter.next_frame_timestamp,
        fps_limiter.target_frametime,
    );

    let frame_is_late: bool = Instant::now() > fps_limiter.next_frame_timestamp;
    fps_limiter.next_frame_timestamp = if frame_is_late {
        Instant::now() + fps_limiter.target_frametime
    } else {
        fps_limiter
            .next_frame_timestamp
            .checked_add(fps_limiter.target_frametime)
            .unwrap_or(Instant::now())
    };

    delta_time
}

fn calc_target_frametime(target_fps: f32) -> Duration {
    let fps_is_uncapped: bool = target_fps == 0.0;

    if fps_is_uncapped {
        Duration::ZERO
    } else {
        Duration::from_secs_f32(1.0 / target_fps)
    }
}

fn calc_delta_time(next_frame_timestamp: Instant, target_frametime: Duration) -> f32 {
    let now = Instant::now();
    let prev_frame = if target_frametime == Duration::ZERO {
        // Uncapped FPS => measure delta since last timestamp
        next_frame_timestamp
    } else {
        next_frame_timestamp
            .checked_sub(target_frametime)
            .unwrap_or(next_frame_timestamp)
    };
    now.checked_duration_since(prev_frame)
        .unwrap_or(Duration::ZERO)
        .as_secs_f32()
}