mod.rs (20851B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 mod debug_flags; 6 mod profiler; 7 mod shell; 8 mod textures; 9 mod composite_view; 10 mod draw_calls; 11 mod timeline; 12 13 use eframe::egui; 14 use webrender_api::{DebugFlags, RenderCommandInfo}; 15 use webrender_api::debugger::{DebuggerMessage, DebuggerTextureContent, ProfileCounterId, CompositorDebugInfo}; 16 use crate::{command, net}; 17 use std::collections::{HashMap, BTreeMap, VecDeque}; 18 use std::fs; 19 use std::io::Write; 20 use std::sync::mpsc; 21 22 use profiler::Graph; 23 24 #[allow(dead_code)] 25 enum ApplicationEvent { 26 RunCommand(String), 27 NetworkEvent(net::NetworkEvent), 28 } 29 30 struct LoggedFrame { 31 pub render_commands: Option<Vec<RenderCommandInfo>>, 32 } 33 34 struct FrameLog { 35 pub frames: VecDeque<LoggedFrame>, 36 pub enabled: bool, 37 pub frames_end: usize, 38 } 39 40 impl FrameLog { 41 pub fn new() -> Self { 42 FrameLog { 43 frames: VecDeque::with_capacity(100), 44 enabled: false, 45 frames_end: 0, 46 } 47 } 48 49 pub fn first_frame_index(&self) -> usize { 50 self.frames_end - self.frames.len() 51 } 52 53 pub fn last_frame_index(&self) -> usize { 54 self.frames_end.max(1) - 1 55 } 56 57 pub fn frame(&self, idx: usize) -> Option<&LoggedFrame> { 58 let i = idx - self.first_frame_index(); 59 if i < self.frames.len() { 60 return Some(&self.frames[i]); 61 } 62 63 None 64 } 65 } 66 67 struct DataModel { 68 is_connected: bool, 69 debug_flags: DebugFlags, 70 cmd: String, 71 log: Vec<String>, 72 documents: Vec<Document>, 73 preview_doc_index: Option<usize>, 74 profile_graphs: HashMap<ProfileCounterId, Graph>, 75 frame_log: FrameLog, 76 timeline: timeline::Timeline, 77 } 78 79 impl DataModel { 80 fn new() -> Self { 81 DataModel { 82 is_connected: false, 83 debug_flags: DebugFlags::empty(), 84 cmd: String::new(), 85 log: Vec::new(), 86 documents: Vec::new(), 87 preview_doc_index: None, 88 profile_graphs: HashMap::new(), 89 frame_log: FrameLog::new(), 90 timeline: timeline::Timeline::new(), 91 } 92 } 93 } 94 95 #[derive(serde::Serialize, serde::Deserialize)] 96 pub enum Tool { 97 DebugFlags, 98 Profiler, 99 Shell, 100 Documents, 101 Preview, 102 DrawCalls, 103 Timeline, 104 } 105 106 impl egui_tiles::Behavior<Tool> for Gui { 107 fn tab_title_for_pane(&mut self, tool: &Tool) -> egui::WidgetText { 108 let title = match tool { 109 Tool::DebugFlags => { "Debug flags" } 110 Tool::Profiler => { "Profiler" } 111 Tool::Shell => { "Shell" } 112 Tool::Documents => { "Documents" } 113 Tool::Preview => { "Preview" } 114 Tool::DrawCalls => { "Draw calls" } 115 Tool::Timeline => { "Timeline" } 116 }; 117 118 title.into() 119 } 120 121 fn pane_ui(&mut self, ui: &mut egui::Ui, _id: egui_tiles::TileId, tool: &mut Tool) -> egui_tiles::UiResponse { 122 // Add a bit of margin around the panes, otherwise their content hugs the 123 // border in an awkward way. There may be a setting somewhere in egui_tiles 124 // rather than doing it manually. 125 egui::Frame::new().inner_margin(egui::Margin::symmetric(5, 5)).show(ui, |ui| { 126 match tool { 127 Tool::Documents => { do_documents_ui(self, ui); } 128 Tool::Preview => { do_preview_ui(self, ui); } 129 Tool::DebugFlags => { debug_flags::ui(self, ui); } 130 Tool::Profiler => { profiler::ui(self, ui); } 131 Tool::Shell => { shell::ui(self, ui); } 132 Tool::DrawCalls => { draw_calls::ui(self, ui); } 133 Tool::Timeline => { timeline::ui(self, ui); } 134 } 135 }); 136 137 Default::default() 138 } 139 140 fn simplification_options(&self) -> egui_tiles::SimplificationOptions { 141 let mut options = egui_tiles::SimplificationOptions::default(); 142 options.all_panes_must_have_tabs = true; 143 options 144 } 145 146 fn tab_title_spacing(&self, _visuals: &egui::Visuals) -> f32 { 147 50.0 148 } 149 150 fn gap_width(&self, _style: &egui::Style) -> f32 { 151 2.0 152 } 153 154 fn tab_outline_stroke( 155 &self, 156 _visuals: &egui::Visuals, 157 _tiles: &egui_tiles::Tiles<Tool>, 158 _tile_id: egui_tiles::TileId, 159 _state: &egui_tiles::TabState, 160 ) -> egui::Stroke { 161 egui::Stroke::NONE 162 } 163 } 164 165 pub struct Gui { 166 data_model: DataModel, 167 net: net::HttpConnection, 168 cmd_list: command::CommandList, 169 cmd_history: Vec<String>, 170 cmd_history_index: usize, 171 doc_id: usize, 172 event_receiver: mpsc::Receiver<ApplicationEvent>, 173 ui_tiles: Option<egui_tiles::Tree<Tool>>, 174 } 175 176 impl Gui { 177 pub fn new(host: &str, cmd_list: command::CommandList) -> Self { 178 let net = net::HttpConnection::new(host); 179 let data_model = DataModel::new(); 180 181 let (event_sender, event_receiver) = mpsc::channel(); 182 183 // Spawn network event thread 184 let host_clone = host.to_string(); 185 std::thread::spawn(move || { 186 net::NetworkEventStream::spawn(&host_clone, move |event| { 187 let _ = event_sender.send(ApplicationEvent::NetworkEvent(event)); 188 }); 189 }); 190 191 // Try to load saved ui_tiles state, or create default layout 192 let save = fs::read_to_string(config_path()) 193 .ok() 194 .and_then(|content| { 195 serde_json::from_str::<GuiSavedState>(&content) 196 .map_err(|e| { 197 eprintln!("Failed to deserialize ui_tiles state: {}", e); 198 e 199 }) 200 .ok() 201 }).and_then(|save| { 202 if save.version == GuiSavedState::VERSION { 203 Some(save) 204 } else { 205 None 206 } 207 }).unwrap_or_else(|| { 208 // Create default layout 209 let mut tiles = egui_tiles::Tiles::default(); 210 let tabs = vec![ 211 tiles.insert_pane(Tool::Profiler), 212 tiles.insert_pane(Tool::Preview), 213 tiles.insert_pane(Tool::DrawCalls), 214 ]; 215 let side = vec![ 216 tiles.insert_pane(Tool::DebugFlags), 217 tiles.insert_pane(Tool::Documents), 218 ]; 219 let side = tiles.insert_vertical_tile(side); 220 let main_tile = tiles.insert_tab_tile(tabs); 221 let main_and_side = vec![ 222 side, 223 main_tile, 224 ]; 225 let shell_and_timeline = vec![ 226 tiles.insert_pane(Tool::Shell), 227 tiles.insert_pane(Tool::Timeline), 228 ]; 229 let shell_and_timeline = tiles.insert_tab_tile(shell_and_timeline); 230 let main_and_side = tiles.insert_horizontal_tile(main_and_side); 231 let root = tiles.insert_vertical_tile(vec![ 232 main_and_side, 233 shell_and_timeline, 234 ]); 235 236 GuiSavedState { 237 version: GuiSavedState::VERSION, 238 cmd_history: Vec::new(), 239 ui_tiles: egui_tiles::Tree::new("WR debugger", root, tiles), 240 } 241 }); 242 243 Gui { 244 data_model, 245 net, 246 cmd_list, 247 cmd_history: save.cmd_history, 248 cmd_history_index: 0, 249 doc_id: 0, 250 event_receiver, 251 ui_tiles: Some(save.ui_tiles), 252 } 253 } 254 255 pub fn run(self) { 256 let native_options = eframe::NativeOptions { 257 viewport: egui::ViewportBuilder::default() 258 .with_inner_size([1280.0, 720.0]) 259 .with_title("WebRender Debug UI"), 260 ..Default::default() 261 }; 262 263 let _ = eframe::run_native( 264 "WebRender Debug UI", 265 native_options, 266 Box::new(|cc| { 267 // Load fonts 268 let mut fonts = egui::FontDefinitions::default(); 269 270 fonts.font_data.insert( 271 "FiraSans".to_owned(), 272 egui::FontData::from_static(include_bytes!("../../res/FiraSans-Regular.ttf")).into(), 273 ); 274 fonts.font_data.insert( 275 "FiraCode".to_owned(), 276 egui::FontData::from_static(include_bytes!("../../res/FiraCode-Regular.ttf")).into(), 277 ); 278 279 fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap() 280 .insert(0, "FiraSans".to_owned()); 281 fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap() 282 .insert(0, "FiraCode".to_owned()); 283 284 cc.egui_ctx.set_fonts(fonts); 285 286 // Load layout settings if available 287 if let Ok(_ini) = fs::read_to_string("default-layout.ini") { 288 // egui uses a different state persistence mechanism 289 // This would need to be adapted for egui's state system 290 } 291 292 Ok(Box::new(self)) 293 }), 294 ); 295 } 296 } 297 298 impl eframe::App for Gui { 299 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 300 // Process pending events 301 while let Ok(event) = self.event_receiver.try_recv() { 302 match event { 303 ApplicationEvent::RunCommand(cmd_name) => { 304 self.handle_command(&cmd_name); 305 } 306 ApplicationEvent::NetworkEvent(net_event) => { 307 self.handle_network_event(net_event); 308 } 309 } 310 } 311 312 textures::prepare(self, ctx); 313 314 // Main menu bar 315 egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { 316 egui::MenuBar::new().ui(ui, |ui| { 317 ui.menu_button("File", |ui| { 318 if ui.button("Exit").clicked() { 319 ctx.send_viewport_cmd(egui::ViewportCommand::Close); 320 } 321 }); 322 ui.menu_button("Help", |ui| { 323 ui.label("About"); 324 }); 325 326 // Connection status on the right 327 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 328 let (msg, text_color, bg_color) = if self.data_model.is_connected { 329 ("Connected", egui::Color32::from_rgb(48, 48, 48), egui::Color32::from_rgb(0, 255, 0)) 330 } else { 331 ("Disconnected", egui::Color32::WHITE, egui::Color32::from_rgb(255, 0, 0)) 332 }; 333 334 // TODO: measure the text instead. 335 let mut area = ui.max_rect(); 336 area.min.x = area.max.x - 85.0; 337 area.max.x += 10.0; 338 339 ui.painter().rect_filled(area, 0, bg_color); 340 341 let label = egui::Label::new( 342 egui::RichText::new(msg).color(text_color) 343 ); 344 ui.add(label).on_hover_text("Connection status"); 345 }); 346 }); 347 }); 348 349 let mut ui_tiles = self.ui_tiles.take().unwrap(); 350 egui::CentralPanel::default().show(ctx, |ui| { 351 ui.spacing_mut().window_margin = egui::Margin::ZERO; 352 ui_tiles.ui(self, ui); 353 }); 354 self.ui_tiles = Some(ui_tiles); 355 } 356 } 357 358 impl Gui { 359 fn handle_command(&mut self, cmd_name: &str) { 360 match self.cmd_list.get_mut(cmd_name) { 361 Some(cmd) => { 362 let mut ctx = command::CommandContext::new( 363 BTreeMap::new(), 364 &mut self.net, 365 ); 366 let output = cmd.run(&mut ctx); 367 368 match output { 369 command::CommandOutput::Log(msg) => { 370 self.data_model.log.push(msg); 371 } 372 command::CommandOutput::Err(msg) => { 373 self.data_model.log.push(msg); 374 } 375 command::CommandOutput::TextDocument { title, content } => { 376 let title = format!("{} [id {}]", title, self.doc_id); 377 self.doc_id += 1; 378 self.data_model.preview_doc_index = Some(self.data_model.documents.len()); 379 self.data_model.documents.push( 380 Document { 381 title, 382 kind: DocumentKind::Text { 383 content, 384 } 385 } 386 ); 387 } 388 command::CommandOutput::SerdeDocument { kind, ref content } => { 389 let title = format!("Compositor [id {}]", self.doc_id); 390 self.doc_id += 1; 391 self.data_model.preview_doc_index = Some(self.data_model.documents.len()); 392 393 let kind = match kind.as_str() { 394 "composite-view" => { 395 let info = serde_json::from_str(content).unwrap(); 396 DocumentKind::Compositor { 397 info, 398 } 399 } 400 _ => { 401 unreachable!("unknown content"); 402 } 403 }; 404 405 self.data_model.documents.push( 406 Document { 407 title, 408 kind, 409 } 410 ); 411 } 412 command::CommandOutput::Textures(textures) => { 413 textures::add_textures(self, textures); 414 } 415 } 416 } 417 None => { 418 self.data_model.log.push( 419 format!("Unknown command '{}'", cmd_name) 420 ); 421 } 422 } 423 } 424 425 fn handle_network_event(&mut self, event: net::NetworkEvent) { 426 match event { 427 net::NetworkEvent::Connected => { 428 self.data_model.is_connected = true; 429 } 430 net::NetworkEvent::Disconnected => { 431 self.data_model.is_connected = false; 432 } 433 net::NetworkEvent::Message(msg) => { 434 match msg { 435 DebuggerMessage::SetDebugFlags(info) => { 436 self.data_model.debug_flags = info.flags; 437 } 438 DebuggerMessage::InitProfileCounters(info) => { 439 let selected_counters = [ 440 "Frame building", 441 "Renderer", 442 ]; 443 444 for counter in info.counters { 445 if selected_counters.contains(&counter.name.as_str()) { 446 println!("Add profile counter {:?}", counter.name); 447 self.data_model.profile_graphs.insert( 448 counter.id, 449 Graph::new(&counter.name, 512), 450 ); 451 } 452 } 453 } 454 DebuggerMessage::UpdateFrameLog(info) => { 455 if let Some(updates) = info.profile_counters { 456 for counter in &updates { 457 if let Some(graph) = self.data_model 458 .profile_graphs 459 .get_mut(&counter.id) { 460 graph.push(counter.value); 461 } 462 } 463 } 464 let frame_log = &mut self.data_model.frame_log; 465 if frame_log.frames.len() == frame_log.frames.capacity() { 466 frame_log.frames.pop_front(); 467 } 468 frame_log.frames.push_back(LoggedFrame { 469 render_commands: info.render_commands.clone(), 470 }); 471 frame_log.frames_end += 1; 472 let first = frame_log.first_frame_index(); 473 let last = frame_log.last_frame_index(); 474 475 if self.data_model.timeline.current_frame < first { 476 self.data_model.timeline.current_frame = first; 477 } 478 479 if self.data_model.timeline.current_frame >= last - 1 { 480 self.data_model.timeline.current_frame = last; 481 } 482 } 483 } 484 } 485 } 486 } 487 } 488 489 impl Drop for Gui { 490 fn drop(&mut self) { 491 // Serialize and save UI state 492 const MAX_HISTORY_SAVED: usize = 50; 493 let hist_len = self.cmd_history.len(); 494 let range = if hist_len > MAX_HISTORY_SAVED { 495 hist_len - MAX_HISTORY_SAVED..hist_len 496 } else { 497 0..hist_len 498 }; 499 500 let save = GuiSavedState { 501 version: GuiSavedState::VERSION, 502 ui_tiles: self.ui_tiles.take().unwrap(), 503 cmd_history: self.cmd_history.drain(range).collect(), 504 }; 505 506 match serde_json::to_string(&save) { 507 Ok(serialized) => { 508 if let Err(e) = fs::File::create(config_path()) 509 .and_then(|mut file| file.write_all(serialized.as_bytes())) 510 { 511 eprintln!("Failed to save ui_tiles state: {}", e); 512 } 513 } 514 Err(e) => { 515 eprintln!("Failed to serialize ui_tiles: {}", e); 516 } 517 } 518 } 519 } 520 521 pub enum DocumentKind { 522 Text { 523 content: String, 524 }, 525 Compositor { 526 info: CompositorDebugInfo, 527 }, 528 Texture { 529 content: DebuggerTextureContent, 530 handle: Option<egui::TextureHandle>, 531 } 532 } 533 534 pub struct Document { 535 pub title: String, 536 pub kind: DocumentKind, 537 } 538 539 fn do_documents_ui(app: &mut Gui, ui: &mut egui::Ui) { 540 let width = ui.available_width(); 541 for (i, doc) in app.data_model.documents.iter().enumerate() { 542 if let DocumentKind::Texture { .. } = doc.kind { 543 // Handle textures separately below. 544 continue; 545 } 546 547 let item = egui::Button::selectable( 548 app.data_model.preview_doc_index == Some(i), 549 &doc.title, 550 ).min_size(egui::vec2(width, 20.0)); 551 552 if ui.add(item).clicked() { 553 app.data_model.preview_doc_index = Some(i); 554 } 555 } 556 557 textures::texture_list_ui(app, ui); 558 } 559 560 fn do_preview_ui(app: &mut Gui, ui: &mut egui::Ui) { 561 if let Some(idx) = app.data_model.preview_doc_index { 562 if idx >= app.data_model.documents.len() { 563 app.data_model.preview_doc_index = None; 564 return; 565 } 566 567 let doc = &app.data_model.documents[idx]; 568 569 match &doc.kind { 570 DocumentKind::Text { content } => { 571 egui::ScrollArea::both().show(ui, |ui| { 572 ui.label(egui::RichText::new(content).monospace()); 573 }); 574 } 575 DocumentKind::Compositor { .. } => { 576 // We need to handle compositor separately due to borrow checker 577 if let Some(Document { kind: DocumentKind::Compositor { info }, .. }) = 578 app.data_model.documents.get_mut(idx) { 579 composite_view::ui(ui, info); 580 } 581 } 582 DocumentKind::Texture { content, handle } => { 583 if let Some(handle) = handle { 584 textures::texture_viewer_ui(ui, &content, &handle); 585 } 586 } 587 } 588 } 589 } 590 591 592 // TODO: It would be better to use confy::load/store instead of loading 593 // files manually but for some reason serialization of the ui tiles panics 594 // in confy. 595 fn config_path() -> std::path::PathBuf { 596 confy::get_configuration_file_path("wr_debugger", None).unwrap() 597 } 598 599 #[derive(serde::Serialize, serde::Deserialize)] 600 struct GuiSavedState { 601 version: u32, 602 cmd_history: Vec<String>, 603 ui_tiles: egui_tiles::Tree<Tool>, 604 } 605 606 impl GuiSavedState { 607 /// Update this number to reset the configuration. This ensures that new 608 /// panels are added. 609 const VERSION: u32 = 2; 610 }