commit 538ed294babe504cf5e08528c99b339519288831
parent 9793fb44a87cf3806de26314c18a525eb47e495c
Author: Nicolas Silva <nical@fastmail.com>
Date: Tue, 9 Dec 2025 08:19:08 +0000
Bug 1998182 - Add a basic timeline widget to WR's debugger. r=gw
Differential Revision: https://phabricator.services.mozilla.com/D271377
Diffstat:
3 files changed, 202 insertions(+), 10 deletions(-)
diff --git a/gfx/wr/wrshell/src/gui/draw_calls.rs b/gfx/wr/wrshell/src/gui/draw_calls.rs
@@ -10,8 +10,7 @@ pub fn ui(app: &mut Gui, ui: &mut egui::Ui) {
app.net.post_with_content("render-cmd-log", &app.data_model.frame_log.enabled).ok();
}
- // TODO: select the frame using a timeline.
- if let Some(frame) = app.data_model.frame_log.frames.back() {
+ if let Some(frame) = app.data_model.frame_log.frame(app.data_model.timeline.current_frame) {
if let Some(cmds) = &frame.render_commands {
draw_calls_ui(cmds, ui);
}
diff --git a/gfx/wr/wrshell/src/gui/mod.rs b/gfx/wr/wrshell/src/gui/mod.rs
@@ -8,6 +8,7 @@ mod shell;
mod textures;
mod composite_view;
mod draw_calls;
+mod timeline;
use eframe::egui;
use webrender_api::{DebugFlags, RenderCommandInfo};
@@ -33,6 +34,7 @@ struct LoggedFrame {
struct FrameLog {
pub frames: VecDeque<LoggedFrame>,
pub enabled: bool,
+ pub frames_end: usize,
}
impl FrameLog {
@@ -40,8 +42,26 @@ impl FrameLog {
FrameLog {
frames: VecDeque::with_capacity(100),
enabled: false,
+ frames_end: 0,
}
}
+
+ pub fn first_frame_index(&self) -> usize {
+ self.frames_end - self.frames.len()
+ }
+
+ pub fn last_frame_index(&self) -> usize {
+ self.frames_end.max(1) - 1
+ }
+
+ pub fn frame(&self, idx: usize) -> Option<&LoggedFrame> {
+ let i = idx - self.first_frame_index();
+ if i < self.frames.len() {
+ return Some(&self.frames[i]);
+ }
+
+ None
+ }
}
struct DataModel {
@@ -53,6 +73,7 @@ struct DataModel {
preview_doc_index: Option<usize>,
profile_graphs: HashMap<ProfileCounterId, Graph>,
frame_log: FrameLog,
+ timeline: timeline::Timeline,
}
impl DataModel {
@@ -66,6 +87,7 @@ impl DataModel {
preview_doc_index: None,
profile_graphs: HashMap::new(),
frame_log: FrameLog::new(),
+ timeline: timeline::Timeline::new(),
}
}
}
@@ -78,6 +100,7 @@ pub enum Tool {
Documents,
Preview,
DrawCalls,
+ Timeline,
}
impl egui_tiles::Behavior<Tool> for Gui {
@@ -89,6 +112,7 @@ impl egui_tiles::Behavior<Tool> for Gui {
Tool::Documents => { "Documents" }
Tool::Preview => { "Preview" }
Tool::DrawCalls => { "Draw calls" }
+ Tool::Timeline => { "Timeline" }
};
title.into()
@@ -106,6 +130,7 @@ impl egui_tiles::Behavior<Tool> for Gui {
Tool::Profiler => { profiler::ui(self, ui); }
Tool::Shell => { shell::ui(self, ui); }
Tool::DrawCalls => { draw_calls::ui(self, ui); }
+ Tool::Timeline => { timeline::ui(self, ui); }
}
});
@@ -197,12 +222,16 @@ impl Gui {
side,
main_tile,
];
- let main_and_side = tiles.insert_horizontal_tile(main_and_side);
- let v = vec![
- main_and_side,
+ let shell_and_timeline = vec![
tiles.insert_pane(Tool::Shell),
+ tiles.insert_pane(Tool::Timeline),
];
- let root = tiles.insert_vertical_tile(v);
+ let shell_and_timeline = tiles.insert_tab_tile(shell_and_timeline);
+ let main_and_side = tiles.insert_horizontal_tile(main_and_side);
+ let root = tiles.insert_vertical_tile(vec![
+ main_and_side,
+ shell_and_timeline,
+ ]);
GuiSavedState {
version: GuiSavedState::VERSION,
@@ -432,12 +461,24 @@ impl Gui {
}
}
}
- if self.data_model.frame_log.frames.len() == self.data_model.frame_log.frames.capacity() {
- self.data_model.frame_log.frames.pop_front();
+ let frame_log = &mut self.data_model.frame_log;
+ if frame_log.frames.len() == frame_log.frames.capacity() {
+ frame_log.frames.pop_front();
}
- self.data_model.frame_log.frames.push_back(LoggedFrame {
+ frame_log.frames.push_back(LoggedFrame {
render_commands: info.render_commands.clone(),
});
+ frame_log.frames_end += 1;
+ let first = frame_log.first_frame_index();
+ let last = frame_log.last_frame_index();
+
+ if self.data_model.timeline.current_frame < first {
+ self.data_model.timeline.current_frame = first;
+ }
+
+ if self.data_model.timeline.current_frame >= last - 1 {
+ self.data_model.timeline.current_frame = last;
+ }
}
}
}
@@ -565,5 +606,5 @@ struct GuiSavedState {
impl GuiSavedState {
/// Update this number to reset the configuration. This ensures that new
/// panels are added.
- const VERSION: u32 = 1;
+ const VERSION: u32 = 2;
}
diff --git a/gfx/wr/wrshell/src/gui/timeline.rs b/gfx/wr/wrshell/src/gui/timeline.rs
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::Gui;
+
+pub struct Timeline {
+ pub current_frame: usize,
+}
+
+impl Timeline {
+ pub fn new() -> Self {
+ Timeline {
+ current_frame: 0,
+ }
+ }
+}
+
+pub fn ui(app: &mut Gui, ui: &mut egui::Ui) {
+ let area = ui.max_rect();
+ let mut top_area = area;
+ let mut bottom_area = area;
+ top_area.max.y -= 20.0;
+ bottom_area.min.y = top_area.max.y;
+
+ ui.scope_builder(egui::UiBuilder::new().max_rect(bottom_area), |ui| {
+ show_timeline(
+ bottom_area,
+ &mut app.data_model.timeline.current_frame,
+ app.data_model.frame_log.first_frame_index(),
+ app.data_model.frame_log.last_frame_index(),
+ ui,
+ );
+ });
+}
+
+pub fn show_timeline(
+ rect: egui::Rect,
+ current_frame: &mut usize,
+ first_frame: usize,
+ last_frame: usize,
+ ui: &mut egui::Ui,
+) {
+ ui.horizontal(|ui| {
+ let style = ui.style().clone();
+ let radius = style.visuals.widgets.inactive.corner_radius;
+
+ let button_size = egui::Vec2 { x: 20.0, y: rect.height() };
+ let prev = egui::Button::new("←")
+ .min_size(button_size)
+ .corner_radius(egui::CornerRadius { nw: radius.nw, sw: radius.sw, ne: 0, se: 0 });
+ let next = egui::Button::new("→")
+ .min_size(button_size)
+ .corner_radius(egui::CornerRadius { ne: radius.ne, se: radius.se, nw: 0, sw: 0 });
+
+ let spacing = ui.spacing().item_spacing.x;
+ ui.spacing_mut().item_spacing.x = 1.0;
+
+ let prev_clicked = ui.add(prev).clicked();
+
+ ui.spacing_mut().item_spacing.x = spacing;
+
+ let next_clicked = ui.add(next).clicked();
+
+ if prev_clicked {
+ *current_frame = (*current_frame).max(first_frame + 1) - 1
+ }
+
+ if next_clicked {
+ *current_frame = (*current_frame + 1).min(last_frame)
+ }
+
+ let min = ui.cursor().min;
+ let max = rect.max;
+ let tl_rect = egui::Rect { min, max };
+ let size = max - min;
+
+ let sense = egui::Sense::CLICK
+ | egui::Sense::HOVER
+ | egui::Sense::FOCUSABLE
+ | egui::Sense::DRAG;
+ let response = ui.allocate_response(size, sense);
+
+ let num_frames = last_frame - first_frame + 1;
+ let n = num_frames as f32;
+
+ let y0 = min.y;
+ let y1 = min.y + size.y;
+
+ let background = style.visuals.widgets.inactive.bg_fill;
+ let border = style.visuals.widgets.inactive.bg_stroke;
+ let separator = egui::Stroke { width: 1.0, color: style.visuals.panel_fill };
+ ui.painter().rect(tl_rect, radius, background, border, egui::StrokeKind::Inside);
+
+ let mut hovered_frame = None;
+ let mut prev_x = min.x;
+ for i in 0..num_frames {
+ let x = min.x + (i + 1) as f32 * size.x / n;
+
+ let frame_rect = egui::Rect {
+ min: egui::Pos2 { x: prev_x, y: y0 },
+ max: egui::Pos2 { x, y: y1 },
+ };
+
+ if ui.rect_contains_pointer(frame_rect) {
+ hovered_frame = Some((i, frame_rect))
+ }
+
+ if i != num_frames - 1 {
+ ui.painter().vline(x, egui::Rangef { min: y0, max: y1 }, separator);
+ }
+ prev_x = x;
+ }
+
+ let selected_cell_idx = *current_frame - first_frame;
+
+ if num_frames > 1 {
+ let x0 = min.x + selected_cell_idx as f32 * size.x / n;
+ let x1 = min.x + (selected_cell_idx + 1) as f32 * size.x / n;
+ let selected_frame_rect = egui::Rect {
+ min: egui::Pos2 { x: x0, y: y0 },
+ max: egui::Pos2 { x: x1, y: y1 },
+ };
+ ui.painter().rect(
+ selected_frame_rect,
+ 0u8,
+ style.visuals.widgets.active.bg_fill,
+ style.visuals.widgets.active.bg_stroke,
+ egui::StrokeKind::Inside,
+ );
+ }
+
+ if num_frames > 0 {
+ if let Some((idx, frame_rect)) = hovered_frame {
+ ui.painter().rect(
+ frame_rect, 0u8,
+ if idx == selected_cell_idx {
+ style.visuals.widgets.active.bg_fill
+ } else {
+ style.visuals.widgets.hovered.bg_fill
+ },
+ style.visuals.widgets.hovered.bg_stroke,
+ egui::StrokeKind::Inside,
+ );
+
+ if response.clicked() || response.is_pointer_button_down_on() {
+ *current_frame = first_frame + idx;
+ }
+ }
+ }
+ });
+}