we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement layer tree and damage tracking for minimal repaints

Add a retained layer tree system that tracks layout box state between frames
and computes damage rectangles for partial repainting. The system compares
box fingerprints (position, color, opacity, text content, borders) across
frames to identify which screen regions changed, merges nearby damage rects
to reduce draw calls, and skips rendering entirely when nothing changed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+818 -1
+764
crates/render/src/layer.rs
··· 1 + //! Retained layer tree and damage tracking for minimal repaints. 2 + //! 3 + //! The layer tree maintains a persistent record of layout box state between 4 + //! frames. Each frame, it compares new layout boxes against previous state and 5 + //! computes damage rectangles — screen regions that need repainting. 6 + //! 7 + //! The GPU and software renderers can use these damage rects as scissor regions 8 + //! to avoid full-screen repaints when only small areas changed (e.g. cursor 9 + //! blink, hover effects). 10 + 11 + use std::collections::HashMap; 12 + 13 + use we_css::values::Color; 14 + use we_dom::NodeId; 15 + use we_layout::{LayoutBox, LayoutTree, Rect}; 16 + use we_style::computed::{Overflow, Position, Visibility}; 17 + 18 + use crate::{build_display_list_with_page_scroll, node_id_from_box_type, DisplayList, ScrollState}; 19 + 20 + // --------------------------------------------------------------------------- 21 + // Damage rectangles 22 + // --------------------------------------------------------------------------- 23 + 24 + /// An axis-aligned damage rectangle in screen coordinates. 25 + #[derive(Debug, Clone, Copy, PartialEq)] 26 + pub struct DamageRect { 27 + pub x: f32, 28 + pub y: f32, 29 + pub width: f32, 30 + pub height: f32, 31 + } 32 + 33 + impl DamageRect { 34 + pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { 35 + Self { 36 + x, 37 + y, 38 + width, 39 + height, 40 + } 41 + } 42 + 43 + /// Create a damage rect from a layout `Rect`. 44 + pub fn from_rect(r: Rect) -> Self { 45 + Self { 46 + x: r.x, 47 + y: r.y, 48 + width: r.width, 49 + height: r.height, 50 + } 51 + } 52 + 53 + /// Union of two damage rects: the smallest rect containing both. 54 + pub fn union(self, other: DamageRect) -> DamageRect { 55 + let x0 = self.x.min(other.x); 56 + let y0 = self.y.min(other.y); 57 + let x1 = (self.x + self.width).max(other.x + other.width); 58 + let y1 = (self.y + self.height).max(other.y + other.height); 59 + DamageRect { 60 + x: x0, 61 + y: y0, 62 + width: x1 - x0, 63 + height: y1 - y0, 64 + } 65 + } 66 + 67 + /// Returns true if this rect has zero or negative area. 68 + pub fn is_empty(&self) -> bool { 69 + self.width <= 0.0 || self.height <= 0.0 70 + } 71 + 72 + /// Returns true if this rect intersects with another. 73 + pub fn intersects(&self, other: &DamageRect) -> bool { 74 + self.x < other.x + other.width 75 + && self.x + self.width > other.x 76 + && self.y < other.y + other.height 77 + && self.y + self.height > other.y 78 + } 79 + 80 + /// Expand this rect by a margin on all sides. Used for merging nearby rects. 81 + pub fn expand(&self, margin: f32) -> DamageRect { 82 + DamageRect { 83 + x: self.x - margin, 84 + y: self.y - margin, 85 + width: self.width + margin * 2.0, 86 + height: self.height + margin * 2.0, 87 + } 88 + } 89 + } 90 + 91 + /// Merge a list of damage rects, coalescing nearby or overlapping ones to 92 + /// reduce draw calls. Two rects are merged if they overlap or are within 93 + /// `merge_threshold` pixels of each other. 94 + pub fn merge_damage_rects(rects: &[DamageRect], merge_threshold: f32) -> Vec<DamageRect> { 95 + if rects.is_empty() { 96 + return Vec::new(); 97 + } 98 + 99 + let mut merged: Vec<DamageRect> = Vec::new(); 100 + 101 + for &rect in rects { 102 + if rect.is_empty() { 103 + continue; 104 + } 105 + 106 + let mut did_merge = false; 107 + for existing in &mut merged { 108 + if existing.expand(merge_threshold).intersects(&rect) { 109 + *existing = existing.union(rect); 110 + did_merge = true; 111 + break; 112 + } 113 + } 114 + if !did_merge { 115 + merged.push(rect); 116 + } 117 + } 118 + 119 + // Second pass: merge any newly-overlapping rects created by the first pass. 120 + let mut stable = false; 121 + while !stable { 122 + stable = true; 123 + let mut i = 0; 124 + while i < merged.len() { 125 + let mut j = i + 1; 126 + while j < merged.len() { 127 + if merged[i].expand(merge_threshold).intersects(&merged[j]) { 128 + let b = merged.remove(j); 129 + merged[i] = merged[i].union(b); 130 + stable = false; 131 + } else { 132 + j += 1; 133 + } 134 + } 135 + i += 1; 136 + } 137 + } 138 + 139 + merged 140 + } 141 + 142 + // --------------------------------------------------------------------------- 143 + // Box fingerprint (for change detection) 144 + // --------------------------------------------------------------------------- 145 + 146 + /// A lightweight fingerprint of a layout box's paint-relevant properties. 147 + /// Used for fast comparison to detect changes between frames. 148 + #[derive(Debug, Clone, PartialEq)] 149 + struct BoxFingerprint { 150 + rect: Rect, 151 + background_color: Color, 152 + color: Color, 153 + opacity: f32, 154 + visibility: Visibility, 155 + overflow: Overflow, 156 + position: Position, 157 + z_index: Option<i32>, 158 + border_widths: [u32; 4], // stored as f32 bit patterns 159 + border_colors: [Color; 4], 160 + text_hash: u64, 161 + child_count: usize, 162 + content_height_bits: u32, 163 + } 164 + 165 + impl BoxFingerprint { 166 + fn from_layout_box(b: &LayoutBox) -> Self { 167 + let text_hash = b.lines.iter().fold(0u64, |acc, line| { 168 + let mut h = acc; 169 + for byte in line.text.as_bytes() { 170 + h = h.wrapping_mul(31).wrapping_add(*byte as u64); 171 + } 172 + h = h.wrapping_mul(31).wrapping_add(line.x.to_bits() as u64); 173 + h = h.wrapping_mul(31).wrapping_add(line.y.to_bits() as u64); 174 + h 175 + }); 176 + 177 + BoxFingerprint { 178 + rect: b.rect, 179 + background_color: b.background_color, 180 + color: b.color, 181 + opacity: b.opacity, 182 + visibility: b.visibility, 183 + overflow: b.overflow, 184 + position: b.position, 185 + z_index: b.z_index, 186 + border_widths: [ 187 + b.border.top.to_bits(), 188 + b.border.right.to_bits(), 189 + b.border.bottom.to_bits(), 190 + b.border.left.to_bits(), 191 + ], 192 + border_colors: b.border_colors, 193 + text_hash, 194 + child_count: b.children.len(), 195 + content_height_bits: b.content_height.to_bits(), 196 + } 197 + } 198 + 199 + /// Compute a damage rect from this fingerprint (border box approximation). 200 + fn damage_rect(&self) -> DamageRect { 201 + let bt = f32::from_bits(self.border_widths[0]); 202 + let br = f32::from_bits(self.border_widths[1]); 203 + let bb = f32::from_bits(self.border_widths[2]); 204 + let bl = f32::from_bits(self.border_widths[3]); 205 + 206 + DamageRect { 207 + x: self.rect.x - bl, 208 + y: self.rect.y - bt, 209 + width: self.rect.width + bl + br, 210 + height: self.rect.height + bt + bb, 211 + } 212 + } 213 + } 214 + 215 + // --------------------------------------------------------------------------- 216 + // Retained layer tree 217 + // --------------------------------------------------------------------------- 218 + 219 + /// Retained layer tree that persists between frames and tracks damage. 220 + /// 221 + /// On each frame, call [`update`] with the new layout tree. The layer tree 222 + /// compares the new layout against the previous frame's snapshot and produces 223 + /// a set of damage rects indicating which screen regions need repainting. 224 + /// 225 + /// For the first frame or after a full invalidation, [`damage_rects`] returns 226 + /// `None`, indicating that the entire viewport should be repainted. 227 + pub struct LayerTree { 228 + /// Previous frame's box fingerprints, keyed by NodeId. 229 + prev_fingerprints: HashMap<NodeId, BoxFingerprint>, 230 + /// Collected damage rects from the most recent update. 231 + damage: Option<Vec<DamageRect>>, 232 + /// Viewport dimensions. 233 + viewport_width: f32, 234 + viewport_height: f32, 235 + /// Whether this is the first frame (no previous state). 236 + first_frame: bool, 237 + /// The display list from the most recent frame. 238 + cached_display_list: DisplayList, 239 + /// Repaint metrics for debugging/testing. 240 + metrics: RepaintMetrics, 241 + } 242 + 243 + /// Metrics about what was repainted in the last frame. 244 + #[derive(Debug, Clone, Default)] 245 + pub struct RepaintMetrics { 246 + /// Number of damage rects produced. 247 + pub damage_rect_count: usize, 248 + /// Total area of damage rects (in px^2). 249 + pub damage_area: f32, 250 + /// Whether a full repaint was performed. 251 + pub full_repaint: bool, 252 + /// Number of boxes that changed between frames. 253 + pub changed_box_count: usize, 254 + } 255 + 256 + impl LayerTree { 257 + /// Create a new layer tree for the given viewport size. 258 + pub fn new(viewport_width: f32, viewport_height: f32) -> Self { 259 + Self { 260 + prev_fingerprints: HashMap::new(), 261 + damage: None, 262 + viewport_width, 263 + viewport_height, 264 + first_frame: true, 265 + cached_display_list: Vec::new(), 266 + metrics: RepaintMetrics::default(), 267 + } 268 + } 269 + 270 + /// Resize the viewport. Triggers a full repaint on the next frame. 271 + pub fn resize(&mut self, width: f32, height: f32) { 272 + if (self.viewport_width - width).abs() > 0.001 273 + || (self.viewport_height - height).abs() > 0.001 274 + { 275 + self.viewport_width = width; 276 + self.viewport_height = height; 277 + self.invalidate(); 278 + } 279 + } 280 + 281 + /// Force a full repaint on the next frame. 282 + pub fn invalidate(&mut self) { 283 + self.first_frame = true; 284 + self.damage = None; 285 + } 286 + 287 + /// Update the layer tree with a new layout. Computes damage rects by 288 + /// comparing the new layout against the previous frame's state. 289 + /// 290 + /// After calling this, use [`damage_rects`] to query which regions need 291 + /// repainting, and [`display_list`] to get the full display list. 292 + pub fn update(&mut self, tree: &LayoutTree, page_scroll_y: f32, scroll_state: &ScrollState) { 293 + // Always rebuild the full display list. 294 + let new_list = build_display_list_with_page_scroll(tree, page_scroll_y, scroll_state); 295 + self.cached_display_list = new_list; 296 + 297 + if self.first_frame { 298 + let mut new_fingerprints = HashMap::new(); 299 + collect_fingerprints(&tree.root, &mut new_fingerprints); 300 + self.prev_fingerprints = new_fingerprints; 301 + self.first_frame = false; 302 + self.damage = None; 303 + self.metrics = RepaintMetrics { 304 + damage_rect_count: 0, 305 + damage_area: 0.0, 306 + full_repaint: true, 307 + changed_box_count: 0, 308 + }; 309 + return; 310 + } 311 + 312 + let mut new_fingerprints = HashMap::new(); 313 + collect_fingerprints(&tree.root, &mut new_fingerprints); 314 + 315 + let mut damage_rects = Vec::new(); 316 + let mut changed_count = 0; 317 + 318 + // Changed or new boxes. 319 + for (node_id, new_fp) in &new_fingerprints { 320 + match self.prev_fingerprints.get(node_id) { 321 + Some(old_fp) if old_fp == new_fp => {} 322 + Some(old_fp) => { 323 + changed_count += 1; 324 + let old_d = old_fp.damage_rect(); 325 + let new_d = new_fp.damage_rect(); 326 + if !old_d.is_empty() { 327 + damage_rects.push(old_d); 328 + } 329 + if !new_d.is_empty() { 330 + damage_rects.push(new_d); 331 + } 332 + } 333 + None => { 334 + changed_count += 1; 335 + let d = new_fp.damage_rect(); 336 + if !d.is_empty() { 337 + damage_rects.push(d); 338 + } 339 + } 340 + } 341 + } 342 + 343 + // Removed boxes. 344 + for (node_id, old_fp) in &self.prev_fingerprints { 345 + if !new_fingerprints.contains_key(node_id) { 346 + changed_count += 1; 347 + let d = old_fp.damage_rect(); 348 + if !d.is_empty() { 349 + damage_rects.push(d); 350 + } 351 + } 352 + } 353 + 354 + let merged = merge_damage_rects(&damage_rects, 16.0); 355 + let total_area: f32 = merged.iter().map(|r| r.width * r.height).sum(); 356 + 357 + self.metrics = RepaintMetrics { 358 + damage_rect_count: merged.len(), 359 + damage_area: total_area, 360 + full_repaint: false, 361 + changed_box_count: changed_count, 362 + }; 363 + 364 + let viewport_area = self.viewport_width * self.viewport_height; 365 + if viewport_area > 0.0 && total_area > viewport_area * 0.75 { 366 + self.metrics.full_repaint = true; 367 + self.damage = None; 368 + } else if merged.is_empty() { 369 + self.damage = Some(Vec::new()); 370 + } else { 371 + self.damage = Some(merged); 372 + } 373 + 374 + self.prev_fingerprints = new_fingerprints; 375 + } 376 + 377 + /// Get the damage rects from the most recent update. 378 + /// 379 + /// Returns `None` if a full repaint is needed (first frame, resize, or 380 + /// damage covers most of the viewport). Returns `Some(empty)` if nothing 381 + /// changed. Returns `Some(rects)` for partial damage. 382 + pub fn damage_rects(&self) -> Option<&[DamageRect]> { 383 + self.damage.as_deref() 384 + } 385 + 386 + /// Get the full display list from the most recent update. 387 + pub fn display_list(&self) -> &DisplayList { 388 + &self.cached_display_list 389 + } 390 + 391 + /// Get repaint metrics from the most recent frame. 392 + pub fn metrics(&self) -> &RepaintMetrics { 393 + &self.metrics 394 + } 395 + 396 + /// Check if any repaint is needed. Returns `false` only when zero damage. 397 + pub fn needs_repaint(&self) -> bool { 398 + match &self.damage { 399 + None => true, 400 + Some(rects) => !rects.is_empty(), 401 + } 402 + } 403 + } 404 + 405 + // --------------------------------------------------------------------------- 406 + // Helpers 407 + // --------------------------------------------------------------------------- 408 + 409 + /// Collect box fingerprints from the layout tree, keyed by NodeId. 410 + fn collect_fingerprints(b: &LayoutBox, out: &mut HashMap<NodeId, BoxFingerprint>) { 411 + if let Some(id) = node_id_from_box_type(&b.box_type) { 412 + out.insert(id, BoxFingerprint::from_layout_box(b)); 413 + } 414 + for child in &b.children { 415 + collect_fingerprints(child, out); 416 + } 417 + } 418 + 419 + // --------------------------------------------------------------------------- 420 + // Tests 421 + // --------------------------------------------------------------------------- 422 + 423 + #[cfg(test)] 424 + mod tests { 425 + use super::*; 426 + 427 + // -- DamageRect tests --------------------------------------------------- 428 + 429 + #[test] 430 + fn damage_rect_union() { 431 + let a = DamageRect::new(10.0, 10.0, 20.0, 20.0); 432 + let b = DamageRect::new(25.0, 25.0, 20.0, 20.0); 433 + let u = a.union(b); 434 + assert_eq!(u.x, 10.0); 435 + assert_eq!(u.y, 10.0); 436 + assert_eq!(u.width, 35.0); 437 + assert_eq!(u.height, 35.0); 438 + } 439 + 440 + #[test] 441 + fn damage_rect_union_overlapping() { 442 + let a = DamageRect::new(0.0, 0.0, 50.0, 50.0); 443 + let b = DamageRect::new(10.0, 10.0, 20.0, 20.0); 444 + let u = a.union(b); 445 + assert_eq!(u.x, 0.0); 446 + assert_eq!(u.y, 0.0); 447 + assert_eq!(u.width, 50.0); 448 + assert_eq!(u.height, 50.0); 449 + } 450 + 451 + #[test] 452 + fn damage_rect_is_empty() { 453 + assert!(DamageRect::new(0.0, 0.0, 0.0, 10.0).is_empty()); 454 + assert!(DamageRect::new(0.0, 0.0, 10.0, 0.0).is_empty()); 455 + assert!(DamageRect::new(0.0, 0.0, -1.0, 10.0).is_empty()); 456 + assert!(!DamageRect::new(0.0, 0.0, 1.0, 1.0).is_empty()); 457 + } 458 + 459 + #[test] 460 + fn damage_rect_intersects() { 461 + let a = DamageRect::new(0.0, 0.0, 20.0, 20.0); 462 + let b = DamageRect::new(10.0, 10.0, 20.0, 20.0); 463 + assert!(a.intersects(&b)); 464 + 465 + let c = DamageRect::new(50.0, 50.0, 10.0, 10.0); 466 + assert!(!a.intersects(&c)); 467 + } 468 + 469 + #[test] 470 + fn damage_rect_expand() { 471 + let r = DamageRect::new(10.0, 10.0, 20.0, 20.0); 472 + let e = r.expand(5.0); 473 + assert_eq!(e.x, 5.0); 474 + assert_eq!(e.y, 5.0); 475 + assert_eq!(e.width, 30.0); 476 + assert_eq!(e.height, 30.0); 477 + } 478 + 479 + #[test] 480 + fn damage_rect_from_layout_rect() { 481 + let r = Rect { 482 + x: 5.0, 483 + y: 10.0, 484 + width: 100.0, 485 + height: 200.0, 486 + }; 487 + let d = DamageRect::from_rect(r); 488 + assert_eq!(d.x, 5.0); 489 + assert_eq!(d.y, 10.0); 490 + assert_eq!(d.width, 100.0); 491 + assert_eq!(d.height, 200.0); 492 + } 493 + 494 + // -- merge_damage_rects tests ------------------------------------------- 495 + 496 + #[test] 497 + fn merge_empty_rects() { 498 + let result = merge_damage_rects(&[], 16.0); 499 + assert!(result.is_empty()); 500 + } 501 + 502 + #[test] 503 + fn merge_single_rect() { 504 + let rects = vec![DamageRect::new(10.0, 10.0, 50.0, 50.0)]; 505 + let result = merge_damage_rects(&rects, 16.0); 506 + assert_eq!(result.len(), 1); 507 + assert_eq!(result[0].x, 10.0); 508 + } 509 + 510 + #[test] 511 + fn merge_overlapping_rects() { 512 + let rects = vec![ 513 + DamageRect::new(0.0, 0.0, 30.0, 30.0), 514 + DamageRect::new(20.0, 20.0, 30.0, 30.0), 515 + ]; 516 + let result = merge_damage_rects(&rects, 0.0); 517 + assert_eq!(result.len(), 1); 518 + assert_eq!(result[0].x, 0.0); 519 + assert_eq!(result[0].y, 0.0); 520 + assert_eq!(result[0].width, 50.0); 521 + assert_eq!(result[0].height, 50.0); 522 + } 523 + 524 + #[test] 525 + fn merge_nearby_rects() { 526 + let rects = vec![ 527 + DamageRect::new(0.0, 0.0, 20.0, 20.0), 528 + DamageRect::new(30.0, 0.0, 20.0, 20.0), 529 + ]; 530 + let result = merge_damage_rects(&rects, 16.0); 531 + assert_eq!(result.len(), 1); 532 + } 533 + 534 + #[test] 535 + fn merge_distant_rects_stay_separate() { 536 + let rects = vec![ 537 + DamageRect::new(0.0, 0.0, 20.0, 20.0), 538 + DamageRect::new(200.0, 200.0, 20.0, 20.0), 539 + ]; 540 + let result = merge_damage_rects(&rects, 16.0); 541 + assert_eq!(result.len(), 2); 542 + } 543 + 544 + #[test] 545 + fn merge_skips_empty_rects() { 546 + let rects = vec![ 547 + DamageRect::new(10.0, 10.0, 0.0, 0.0), 548 + DamageRect::new(20.0, 20.0, 30.0, 30.0), 549 + ]; 550 + let result = merge_damage_rects(&rects, 16.0); 551 + assert_eq!(result.len(), 1); 552 + assert_eq!(result[0].x, 20.0); 553 + } 554 + 555 + #[test] 556 + fn merge_chain_of_nearby_rects() { 557 + let rects = vec![ 558 + DamageRect::new(0.0, 0.0, 20.0, 20.0), 559 + DamageRect::new(30.0, 0.0, 20.0, 20.0), 560 + DamageRect::new(60.0, 0.0, 20.0, 20.0), 561 + ]; 562 + let result = merge_damage_rects(&rects, 16.0); 563 + assert_eq!(result.len(), 1); 564 + } 565 + 566 + // -- LayerTree tests (using real HTML parsing) -------------------------- 567 + 568 + use we_dom::Document; 569 + use we_style::computed::{extract_stylesheets, resolve_styles}; 570 + use we_text::font::Font; 571 + 572 + fn test_font() -> Font { 573 + let paths = [ 574 + "/System/Library/Fonts/Geneva.ttf", 575 + "/System/Library/Fonts/Monaco.ttf", 576 + ]; 577 + for path in &paths { 578 + let p = std::path::Path::new(path); 579 + if p.exists() { 580 + return Font::from_file(p).expect("failed to parse font"); 581 + } 582 + } 583 + panic!("no test font found"); 584 + } 585 + 586 + fn layout_html(html: &str) -> LayoutTree { 587 + let font = test_font(); 588 + let doc = we_html::parse_html(html); 589 + let sheets = extract_stylesheets(&doc); 590 + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 591 + we_layout::layout( 592 + &styled, 593 + &doc, 594 + 800.0, 595 + 600.0, 596 + &font, 597 + &std::collections::HashMap::new(), 598 + ) 599 + } 600 + 601 + #[test] 602 + fn layer_tree_first_frame_full_repaint() { 603 + let lt = LayerTree::new(800.0, 600.0); 604 + assert!(lt.damage_rects().is_none()); 605 + assert!(lt.needs_repaint()); 606 + } 607 + 608 + #[test] 609 + fn layer_tree_resize_triggers_full_repaint() { 610 + let mut lt = LayerTree::new(800.0, 600.0); 611 + lt.first_frame = false; 612 + lt.damage = Some(Vec::new()); 613 + 614 + lt.resize(1024.0, 768.0); 615 + assert!(lt.damage_rects().is_none()); 616 + assert!(lt.needs_repaint()); 617 + } 618 + 619 + #[test] 620 + fn layer_tree_invalidate_triggers_full_repaint() { 621 + let mut lt = LayerTree::new(800.0, 600.0); 622 + lt.first_frame = false; 623 + lt.damage = Some(Vec::new()); 624 + 625 + lt.invalidate(); 626 + assert!(lt.damage_rects().is_none()); 627 + } 628 + 629 + #[test] 630 + fn layer_tree_identical_layout_no_damage() { 631 + let mut lt = LayerTree::new(800.0, 600.0); 632 + let scroll_state = HashMap::new(); 633 + 634 + let tree1 = layout_html("<html><body><p>Hello</p></body></html>"); 635 + lt.update(&tree1, 0.0, &scroll_state); 636 + assert!(lt.metrics().full_repaint); 637 + 638 + let tree2 = layout_html("<html><body><p>Hello</p></body></html>"); 639 + lt.update(&tree2, 0.0, &scroll_state); 640 + assert!(!lt.metrics().full_repaint); 641 + assert_eq!(lt.metrics().changed_box_count, 0); 642 + assert!(!lt.needs_repaint()); 643 + } 644 + 645 + #[test] 646 + fn layer_tree_detects_text_change() { 647 + let mut lt = LayerTree::new(800.0, 600.0); 648 + let scroll_state = HashMap::new(); 649 + 650 + let tree1 = layout_html("<html><body><p>Hello</p></body></html>"); 651 + lt.update(&tree1, 0.0, &scroll_state); 652 + 653 + let tree2 = layout_html("<html><body><p>World</p></body></html>"); 654 + lt.update(&tree2, 0.0, &scroll_state); 655 + 656 + assert!(lt.needs_repaint()); 657 + assert!(lt.metrics().changed_box_count >= 1); 658 + } 659 + 660 + #[test] 661 + fn layer_tree_detects_style_change() { 662 + let mut lt = LayerTree::new(800.0, 600.0); 663 + let scroll_state = HashMap::new(); 664 + 665 + let tree1 = layout_html( 666 + "<html><body><div style=\"background: white; width: 100px; height: 100px\">A</div></body></html>", 667 + ); 668 + lt.update(&tree1, 0.0, &scroll_state); 669 + 670 + let tree2 = layout_html( 671 + "<html><body><div style=\"background: red; width: 100px; height: 100px\">A</div></body></html>", 672 + ); 673 + lt.update(&tree2, 0.0, &scroll_state); 674 + 675 + assert!(lt.needs_repaint()); 676 + assert!(lt.metrics().changed_box_count >= 1); 677 + } 678 + 679 + #[test] 680 + fn layer_tree_detects_added_element() { 681 + let mut lt = LayerTree::new(800.0, 600.0); 682 + let scroll_state = HashMap::new(); 683 + 684 + let tree1 = layout_html("<html><body><p>One</p></body></html>"); 685 + lt.update(&tree1, 0.0, &scroll_state); 686 + 687 + let tree2 = layout_html("<html><body><p>One</p><p>Two</p></body></html>"); 688 + lt.update(&tree2, 0.0, &scroll_state); 689 + 690 + assert!(lt.needs_repaint()); 691 + assert!(lt.metrics().changed_box_count >= 1); 692 + } 693 + 694 + #[test] 695 + fn layer_tree_detects_removed_element() { 696 + let mut lt = LayerTree::new(800.0, 600.0); 697 + let scroll_state = HashMap::new(); 698 + 699 + let tree1 = layout_html("<html><body><p>One</p><p>Two</p></body></html>"); 700 + lt.update(&tree1, 0.0, &scroll_state); 701 + 702 + let tree2 = layout_html("<html><body><p>One</p></body></html>"); 703 + lt.update(&tree2, 0.0, &scroll_state); 704 + 705 + assert!(lt.needs_repaint()); 706 + assert!(lt.metrics().changed_box_count >= 1); 707 + } 708 + 709 + #[test] 710 + fn layer_tree_display_list_always_available() { 711 + let mut lt = LayerTree::new(800.0, 600.0); 712 + let scroll_state = HashMap::new(); 713 + 714 + let tree = layout_html("<html><body><p>Test</p></body></html>"); 715 + lt.update(&tree, 0.0, &scroll_state); 716 + 717 + assert!(!lt.display_list().is_empty()); 718 + } 719 + 720 + #[test] 721 + fn layer_tree_damage_covers_changed_region() { 722 + let mut lt = LayerTree::new(800.0, 600.0); 723 + let scroll_state = HashMap::new(); 724 + 725 + // Two distinct divs at known positions. 726 + let tree1 = layout_html(concat!( 727 + "<html><body>", 728 + "<div style=\"width:50px;height:50px;background:blue\">A</div>", 729 + "<div style=\"width:50px;height:50px;background:green\">B</div>", 730 + "</body></html>" 731 + )); 732 + lt.update(&tree1, 0.0, &scroll_state); 733 + 734 + // Change only the second div's background. 735 + let tree2 = layout_html(concat!( 736 + "<html><body>", 737 + "<div style=\"width:50px;height:50px;background:blue\">A</div>", 738 + "<div style=\"width:50px;height:50px;background:red\">B</div>", 739 + "</body></html>" 740 + )); 741 + lt.update(&tree2, 0.0, &scroll_state); 742 + 743 + assert!(lt.needs_repaint()); 744 + let rects = lt.damage_rects(); 745 + // Should have damage, not a full repaint for this small change. 746 + assert!(rects.is_some()); 747 + } 748 + 749 + #[test] 750 + fn layer_tree_metrics_accurate_on_no_change() { 751 + let mut lt = LayerTree::new(800.0, 600.0); 752 + let scroll_state = HashMap::new(); 753 + 754 + let tree = layout_html("<html><body>Static</body></html>"); 755 + lt.update(&tree, 0.0, &scroll_state); 756 + assert!(lt.metrics().full_repaint); 757 + 758 + let tree2 = layout_html("<html><body>Static</body></html>"); 759 + lt.update(&tree2, 0.0, &scroll_state); 760 + assert!(!lt.metrics().full_repaint); 761 + assert_eq!(lt.metrics().damage_rect_count, 0); 762 + assert_eq!(lt.metrics().damage_area, 0.0); 763 + } 764 + }
+54 -1
crates/render/src/lib.rs
··· 5 5 6 6 pub mod atlas; 7 7 pub mod gpu; 8 + pub mod layer; 8 9 9 10 use std::collections::HashMap; 10 11 ··· 422 423 } 423 424 424 425 /// Extract the NodeId from a BoxType, if it has one. 425 - fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> { 426 + pub(crate) fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> { 426 427 match box_type { 427 428 BoxType::Block(id) | BoxType::Inline(id) => Some(*id), 428 429 BoxType::TextRun { node, .. } => Some(*node), ··· 1231 1232 if let RenderBackend::Metal(gpu) = self { 1232 1233 gpu.clear_image_cache(); 1233 1234 } 1235 + } 1236 + 1237 + /// Render using damage tracking from a [`layer::LayerTree`]. 1238 + /// 1239 + /// If the layer tree indicates no repaint is needed (zero damage), this 1240 + /// skips rendering entirely and returns `false`. Otherwise renders the 1241 + /// full display list and returns `true`. 1242 + /// 1243 + /// For the Metal backend, this delegates to [`render_metal`]. For the 1244 + /// software backend, this delegates to [`render_software`]. In both 1245 + /// cases the layer tree's cached display list is used. 1246 + /// 1247 + /// Future optimization: use damage rects as scissor regions to limit 1248 + /// GPU/software rendering to only the damaged areas. 1249 + #[allow(clippy::too_many_arguments)] 1250 + pub fn render_with_layer_tree( 1251 + &mut self, 1252 + layer_tree: &layer::LayerTree, 1253 + font: &Font, 1254 + images: &HashMap<NodeId, &Image>, 1255 + metal_layer: Option<&MetalLayer>, 1256 + queue: Option<&CommandQueue>, 1257 + clear_color: ClearColor, 1258 + viewport_width: f32, 1259 + viewport_height: f32, 1260 + ) -> bool { 1261 + if !layer_tree.needs_repaint() { 1262 + return false; 1263 + } 1264 + 1265 + let display_list = layer_tree.display_list(); 1266 + 1267 + match self { 1268 + RenderBackend::Metal(gpu) => { 1269 + if let (Some(layer), Some(q)) = (metal_layer, queue) { 1270 + gpu.render( 1271 + display_list, 1272 + font, 1273 + images, 1274 + layer, 1275 + q, 1276 + clear_color, 1277 + viewport_width, 1278 + viewport_height, 1279 + ); 1280 + } 1281 + } 1282 + RenderBackend::Software(renderer) => { 1283 + renderer.paint_display_list(display_list, font, images); 1284 + } 1285 + } 1286 + true 1234 1287 } 1235 1288 } 1236 1289