#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ViewportStatsConfig {
pub window_size: usize,
pub visible: bool,
pub draw_call_warn_threshold: u32,
}
impl Default for ViewportStatsConfig {
fn default() -> Self {
Self {
window_size: 60,
visible: true,
draw_call_warn_threshold: 1000,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FrameStats {
pub frame_time_ms: f32,
pub draw_calls: u32,
pub vertex_count: u64,
pub triangle_count: u64,
}
#[derive(Debug, Clone)]
pub struct ViewportStats {
pub config: ViewportStatsConfig,
history: Vec<FrameStats>,
total_frames: u64,
peak_ms: f32,
}
pub type RollingAvg = (f32, u32, u64, u64);
#[allow(dead_code)]
pub fn default_viewport_stats_config() -> ViewportStatsConfig {
ViewportStatsConfig::default()
}
#[allow(dead_code)]
pub fn new_viewport_stats(config: ViewportStatsConfig) -> ViewportStats {
ViewportStats {
config,
history: Vec::new(),
total_frames: 0,
peak_ms: 0.0,
}
}
#[allow(dead_code)]
pub fn record_frame(mut stats: ViewportStats, frame: FrameStats) -> ViewportStats {
if frame.frame_time_ms > stats.peak_ms {
stats.peak_ms = frame.frame_time_ms;
}
stats.total_frames += 1;
stats.history.push(frame);
let win = stats.config.window_size;
if stats.history.len() > win {
let excess = stats.history.len() - win;
stats.history.drain(..excess);
}
stats
}
#[allow(dead_code)]
pub fn frame_time_ms(stats: &ViewportStats) -> f32 {
stats.history.last().map(|f| f.frame_time_ms).unwrap_or(0.0)
}
#[allow(dead_code)]
pub fn draw_call_count(stats: &ViewportStats) -> u32 {
stats.history.last().map(|f| f.draw_calls).unwrap_or(0)
}
#[allow(dead_code)]
pub fn vertex_count_stat(stats: &ViewportStats) -> u64 {
stats.history.last().map(|f| f.vertex_count).unwrap_or(0)
}
#[allow(dead_code)]
pub fn triangle_count_stat(stats: &ViewportStats) -> u64 {
stats.history.last().map(|f| f.triangle_count).unwrap_or(0)
}
#[allow(dead_code)]
pub fn stats_rolling_average(stats: &ViewportStats) -> RollingAvg {
let n = stats.history.len();
if n == 0 {
return (0.0, 0, 0, 0);
}
let n_f = n as f32;
let n_u = n as u64;
let sum_ms: f32 = stats.history.iter().map(|f| f.frame_time_ms).sum();
let sum_dc: u64 = stats.history.iter().map(|f| f.draw_calls as u64).sum();
let sum_vc: u64 = stats.history.iter().map(|f| f.vertex_count).sum();
let sum_tc: u64 = stats.history.iter().map(|f| f.triangle_count).sum();
(sum_ms / n_f, (sum_dc / n_u) as u32, sum_vc / n_u, sum_tc / n_u)
}
#[allow(dead_code)]
pub fn stats_to_text(stats: &ViewportStats) -> String {
let (avg_ms, avg_dc, avg_vc, avg_tc) = stats_rolling_average(stats);
format!(
"Frame: {:.2}ms DrawCalls: {} Verts: {} Tris: {} Avg: {:.2}ms/{}/{}/{} Peak: {:.2}ms Frames: {}",
frame_time_ms(stats),
draw_call_count(stats),
vertex_count_stat(stats),
triangle_count_stat(stats),
avg_ms,
avg_dc,
avg_vc,
avg_tc,
stats.peak_ms,
stats.total_frames,
)
}
#[allow(dead_code)]
pub fn stats_to_json(stats: &ViewportStats) -> String {
let (avg_ms, avg_dc, avg_vc, avg_tc) = stats_rolling_average(stats);
format!(
"{{\"frame_time_ms\":{:.4},\"draw_calls\":{},\"vertex_count\":{},\"triangle_count\":{},\
\"avg_frame_time_ms\":{:.4},\"avg_draw_calls\":{},\"avg_vertex_count\":{},\
\"avg_triangle_count\":{},\"peak_frame_time_ms\":{:.4},\"total_frames\":{}}}",
frame_time_ms(stats),
draw_call_count(stats),
vertex_count_stat(stats),
triangle_count_stat(stats),
avg_ms,
avg_dc,
avg_vc,
avg_tc,
stats.peak_ms,
stats.total_frames,
)
}
#[allow(dead_code)]
pub fn reset_stats(mut stats: ViewportStats) -> ViewportStats {
stats.history.clear();
stats.total_frames = 0;
stats.peak_ms = 0.0;
stats
}
#[allow(dead_code)]
pub fn peak_frame_time_ms(stats: &ViewportStats) -> f32 {
stats.peak_ms
}
#[allow(dead_code)]
pub fn stats_visible(stats: &ViewportStats) -> bool {
stats.config.visible
}
#[cfg(test)]
mod tests {
use super::*;
fn make_frame(ms: f32, dc: u32, vc: u64, tc: u64) -> FrameStats {
FrameStats {
frame_time_ms: ms,
draw_calls: dc,
vertex_count: vc,
triangle_count: tc,
}
}
fn filled_stats() -> ViewportStats {
let cfg = default_viewport_stats_config();
let s = new_viewport_stats(cfg);
let s = record_frame(s, make_frame(16.0, 100, 10000, 5000));
let s = record_frame(s, make_frame(18.0, 120, 12000, 6000));
record_frame(s, make_frame(14.0, 80, 8000, 4000))
}
#[test]
fn default_config_fields() {
let cfg = default_viewport_stats_config();
assert_eq!(cfg.window_size, 60);
assert!(cfg.visible);
assert_eq!(cfg.draw_call_warn_threshold, 1000);
}
#[test]
fn new_stats_has_no_history() {
let s = new_viewport_stats(default_viewport_stats_config());
assert_eq!(s.history.len(), 0);
assert_eq!(s.total_frames, 0);
}
#[test]
fn record_frame_increments_total() {
let s = new_viewport_stats(default_viewport_stats_config());
let s = record_frame(s, make_frame(16.0, 50, 1000, 500));
assert_eq!(s.total_frames, 1);
}
#[test]
fn record_frame_updates_history() {
let s = filled_stats();
assert_eq!(s.history.len(), 3);
}
#[test]
fn frame_time_ms_returns_last() {
let s = filled_stats();
assert!((frame_time_ms(&s) - 14.0).abs() < 1e-4);
}
#[test]
fn draw_call_count_returns_last() {
let s = filled_stats();
assert_eq!(draw_call_count(&s), 80);
}
#[test]
fn vertex_count_stat_returns_last() {
let s = filled_stats();
assert_eq!(vertex_count_stat(&s), 8000);
}
#[test]
fn triangle_count_stat_returns_last() {
let s = filled_stats();
assert_eq!(triangle_count_stat(&s), 4000);
}
#[test]
fn rolling_average_frame_time() {
let s = filled_stats();
let (avg_ms, _, _, _) = stats_rolling_average(&s);
let expected = (16.0 + 18.0 + 14.0) / 3.0;
assert!((avg_ms - expected).abs() < 0.01);
}
#[test]
fn rolling_average_empty_returns_zeros() {
let s = new_viewport_stats(default_viewport_stats_config());
let avg = stats_rolling_average(&s);
assert_eq!(avg, (0.0, 0, 0, 0));
}
#[test]
fn stats_to_text_contains_ms() {
let s = filled_stats();
let t = stats_to_text(&s);
assert!(t.contains("ms"));
}
#[test]
fn stats_to_json_is_valid_json_start() {
let s = filled_stats();
let j = stats_to_json(&s);
assert!(j.starts_with('{'));
assert!(j.ends_with('}'));
}
#[test]
fn reset_clears_history() {
let s = filled_stats();
let s = reset_stats(s);
assert_eq!(s.history.len(), 0);
assert_eq!(s.total_frames, 0);
assert!((s.peak_ms).abs() < 1e-6);
}
#[test]
fn peak_frame_time_ms_tracks_max() {
let s = filled_stats();
assert!((peak_frame_time_ms(&s) - 18.0).abs() < 1e-4);
}
#[test]
fn stats_visible_reflects_config() {
let mut cfg = default_viewport_stats_config();
cfg.visible = false;
let s = new_viewport_stats(cfg);
assert!(!stats_visible(&s));
}
#[test]
fn window_size_limits_history() {
let mut cfg = default_viewport_stats_config();
cfg.window_size = 2;
let s = new_viewport_stats(cfg);
let s = record_frame(s, make_frame(10.0, 1, 100, 50));
let s = record_frame(s, make_frame(11.0, 2, 200, 100));
let s = record_frame(s, make_frame(12.0, 3, 300, 150));
assert_eq!(s.history.len(), 2);
assert!((s.history[0].frame_time_ms - 11.0).abs() < 1e-4);
}
#[test]
fn frame_stats_default_is_zero() {
let f = FrameStats::default();
assert_eq!(f.draw_calls, 0);
assert_eq!(f.vertex_count, 0);
assert_eq!(f.triangle_count, 0);
assert!((f.frame_time_ms).abs() < 1e-6);
}
}