macroquad_profiler/
lib.rs

1use macroquad::{experimental::collections::storage, telemetry};
2
3use macroquad::prelude::*;
4
5use macroquad::ui::{hash, root_ui, widgets::Window, Ui};
6
7pub struct ProfilerState {
8    fps_buffer: Vec<f32>,
9    frames_buffer: Vec<telemetry::Frame>,
10    selected_frame: Option<telemetry::Frame>,
11    profiler_window_opened: bool,
12    paused: bool,
13}
14
15pub struct ProfilerParams {
16    pub fps_counter_pos: Vec2,
17}
18
19impl Default for ProfilerParams {
20    fn default() -> ProfilerParams {
21        ProfilerParams {
22            fps_counter_pos: vec2(10., 10.),
23        }
24    }
25}
26
27const FPS_BUFFER_CAPACITY: usize = 100;
28const FRAMES_BUFFER_CAPACITY: usize = 400;
29
30fn profiler_window(ui: &mut Ui, state: &mut ProfilerState) {
31    fn zone_ui(ui: &mut Ui, zone: &telemetry::Zone, n: usize) {
32        let label = format!(
33            "{}: {:.4}ms {:.1}(1/t)",
34            zone.name,
35            zone.duration * 1000.0,
36            1.0 / zone.duration
37        );
38        if zone.children.len() != 0 {
39            ui.tree_node(hash!(hash!(), n), &label, |ui| {
40                for (m, zone) in zone.children.iter().enumerate() {
41                    zone_ui(ui, zone, n * 1000 + m + 1);
42                }
43            });
44        } else {
45            ui.label(None, &label);
46        }
47    }
48
49    let mut canvas = ui.canvas();
50    let w = 515.0;
51    let h = 40.0;
52    let pos = canvas.request_space(vec2(w, h));
53
54    let rect = Rect::new(pos.x, pos.y, w, h);
55    canvas.rect(rect, Color::new(0.5, 0.5, 0.5, 1.0), None);
56
57    let (mouse_x, mouse_y) = mouse_position();
58
59    let mut selected_frame = None;
60
61    // select the slowest frame among the ones close to the mouse cursor
62    if rect.contains(vec2(mouse_x, mouse_y)) && state.frames_buffer.len() >= 1 {
63        let x = ((mouse_x - pos.x - 2.) / w * FRAMES_BUFFER_CAPACITY as f32) as i32;
64
65        let min = clamp(x - 2, 0, state.frames_buffer.len() as i32 - 1) as usize;
66        let max = clamp(x + 3, 0, state.frames_buffer.len() as i32 - 1) as usize;
67
68        selected_frame = state.frames_buffer[min..max]
69            .iter()
70            .enumerate()
71            .max_by(|(_, a), (_, b)| a.full_frame_time.partial_cmp(&b.full_frame_time).unwrap())
72            .map(|(n, _)| n + min);
73    }
74
75    if let Some(frame) = selected_frame {
76        if is_mouse_button_down(MouseButton::Left) {
77            state.selected_frame = state.frames_buffer[frame].try_clone();
78        }
79    }
80    for (n, frame) in state.frames_buffer.iter().enumerate() {
81        let x = n as f32 / FRAMES_BUFFER_CAPACITY as f32 * (w - 2.);
82        let selected = selected_frame.map_or(false, |selected| n == selected);
83        let color = if selected {
84            Color::new(1.0, 1.0, 0.0, 1.0)
85        } else if frame.full_frame_time < 1.0 / 58.0 {
86            Color::new(0.6, 0.6, 1.0, 1.0)
87        } else if frame.full_frame_time < 1.0 / 25.0 {
88            Color::new(0.3, 0.3, 0.8, 1.0)
89        } else {
90            Color::new(0.2, 0.2, 0.6, 1.0)
91        };
92        let t = macroquad::math::clamp(frame.full_frame_time * 1000.0, 0.0, h);
93
94        canvas.line(
95            vec2(pos.x + x + 2., pos.y + h - 1.0),
96            vec2(pos.x + x + 2., pos.y + h - t),
97            color,
98        );
99    }
100
101    if let Some(frame) = state
102        .selected_frame
103        .as_ref()
104        .or_else(|| state.frames_buffer.get(0))
105    {
106        ui.label(
107            None,
108            &format!(
109                "Full frame time: {:.3}ms {:.1}(1/t)",
110                frame.full_frame_time * 1000.0,
111                (1.0 / frame.full_frame_time)
112            ),
113        );
114    }
115
116    if state.paused {
117        if ui.button(None, "resume") {
118            state.paused = false;
119        }
120    } else {
121        if ui.button(None, "pause") {
122            state.paused = true;
123        }
124    }
125    if state.selected_frame.is_some() {
126        ui.same_line(100.0);
127        if ui.button(None, "deselect frame") {
128            state.selected_frame = None;
129        }
130    }
131
132    let frame = state
133        .selected_frame
134        .as_ref()
135        .or_else(|| state.frames_buffer.get(0));
136
137    ui.separator();
138    ui.group(hash!(), vec2(355., 300.), |ui| {
139        if let Some(frame) = frame {
140            for (n, zone) in frame.zones.iter().enumerate() {
141                zone_ui(ui, zone, n + 1);
142            }
143        }
144    });
145    ui.group(hash!(), vec2(153., 300.), |ui| {
146        let queries = telemetry::gpu_queries();
147
148        for query in queries {
149            let t = query.1 as f64 / 1_000_000_000.0;
150            ui.label(
151                None,
152                &format!("{}: {:.3}ms {:.1}(1/t)", query.0, t * 1000.0, 1.0 / t),
153            );
154        }
155    });
156    if ui.button(None, "sample gpu") {
157        telemetry::sample_gpu_queries();
158    }
159}
160
161pub fn profiler(params: ProfilerParams) {
162    telemetry::pause_gl_capture();
163    if storage::try_get::<ProfilerState>().is_none() {
164        storage::store(ProfilerState {
165            fps_buffer: vec![],
166            frames_buffer: vec![],
167            profiler_window_opened: false,
168            selected_frame: None,
169            paused: false,
170        })
171    }
172    let mut state = storage::get_mut::<ProfilerState>();
173
174    let frame = telemetry::frame();
175
176    if state.paused == false && state.profiler_window_opened {
177        state.frames_buffer.insert(0, frame);
178    }
179    let time = get_frame_time();
180    state.fps_buffer.insert(0, time);
181
182    state.fps_buffer.truncate(FPS_BUFFER_CAPACITY);
183    state.frames_buffer.truncate(FRAMES_BUFFER_CAPACITY);
184
185    push_camera_state();
186    set_default_camera();
187    let mut sum = 0.0;
188    for (x, time) in state.fps_buffer.iter().enumerate() {
189        draw_line(
190            x as f32 + params.fps_counter_pos.x,
191            params.fps_counter_pos.y + 100.0,
192            x as f32 + params.fps_counter_pos.x,
193            params.fps_counter_pos.y + 100.0 - (time * 2000.0).min(100.0),
194            1.0,
195            BLUE,
196        );
197        sum += time;
198    }
199
200    let selectable_rect = Rect::new(
201        params.fps_counter_pos.x,
202        params.fps_counter_pos.y + 40.0,
203        100.0,
204        100.0,
205    );
206
207    if selectable_rect.contains(mouse_position().into()) {
208        draw_rectangle(
209            selectable_rect.x,
210            selectable_rect.y,
211            100.0,
212            100.0,
213            Color::new(1.0, 1.0, 1.0, 0.4),
214        );
215        if is_mouse_button_pressed(MouseButton::Left) {
216            state.profiler_window_opened ^= true;
217            if state.profiler_window_opened {
218                telemetry::enable();
219            } else {
220                telemetry::disable();
221            }
222        }
223    }
224
225    draw_text(
226        &format!("{:.1}", 1.0 / (sum / state.fps_buffer.len() as f32)),
227        params.fps_counter_pos.x,
228        params.fps_counter_pos.y + 100.0,
229        30.0,
230        WHITE,
231    );
232
233    if state.profiler_window_opened {
234        Window::new(
235            hash!(),
236            vec2(params.fps_counter_pos.x, params.fps_counter_pos.y + 150.0),
237            vec2(525., 450.),
238        )
239        .ui(&mut *root_ui(), |ui| {
240            let tab = ui.tabbar(
241                hash!(),
242                vec2(300.0, 20.0),
243                &["profiler", "scene", "frame", "log"],
244            );
245
246            match tab {
247                0 => profiler_window(ui, &mut state),
248                1 => ui.label(
249                    None,
250                    &format!(
251                        "scene allocated memory: {:.1} kb",
252                        (telemetry::scene_allocated_memory() as f32) / 1000.0
253                    ),
254                ),
255                2 => {
256                    let drawcalls = telemetry::drawcalls();
257                    ui.label(None, &format!("Draw calls: {}", drawcalls.len()));
258                    for telemetry::DrawCallTelemetry { indices_count, .. } in &drawcalls {
259                        ui.same_line(0.0);
260
261                        ui.label(None, &format!("{}", indices_count));
262                        ui.same_line(0.0);
263                    }
264                    ui.label(None, " ");
265
266                    for telemetry::DrawCallTelemetry {
267                        indices_count,
268                        texture,
269                    } in &drawcalls
270                    {
271                        ui.label(None, &format!("{}", *indices_count));
272                        ui.same_line(0.0);
273                        ui.texture(Texture2D::from_miniquad_texture(*texture), 100., 100.0);
274                        ui.same_line(0.0);
275                    }
276                    ui.label(None, " ");
277
278                    if ui.button(None, "Capture frame") {
279                        telemetry::capture_frame();
280                    }
281                }
282                3 => {
283                    for label in telemetry::strings() {
284                        ui.label(None, &label);
285                    }
286                }
287                _ => unreachable!(),
288            }
289        });
290    }
291    pop_camera_state();
292
293    telemetry::resume_gl_capture();
294}