| author | Bert Peers <bpeers@mozilla.com> |
| Mon, 10 Feb 2020 17:35:05 +0000 | |
| changeset 513061 | 813768d074e3accc36dc3313d0bffb658dcc562d |
| parent 513060 | da808bb758190e48123fc4c389ba8f676695918f |
| child 513062 | 42d8559f9f4fd842d8e0244b64a12e678f83c0ab |
| push id | 37109 |
| push user | ncsoregi@mozilla.com |
| push date | Mon, 10 Feb 2020 21:33:47 +0000 |
| treeherder | mozilla-central@813768d074e3 [default view] [failures only] |
| perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
| reviewers | gw |
| bugs | 1605283 |
| milestone | 75.0a1 |
| first release with | nightly linux32
813768d074e3
/
75.0a1
/
20200210213347
/
files
nightly linux64
813768d074e3
/
75.0a1
/
20200210213347
/
files
nightly mac
813768d074e3
/
75.0a1
/
20200210213347
/
files
nightly win32
813768d074e3
/
75.0a1
/
20200210213347
/
files
nightly win64
813768d074e3
/
75.0a1
/
20200210213347
/
files
|
| last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
| releases | nightly linux32
75.0a1
/
20200210213347
/
pushlog to previous
nightly linux64
75.0a1
/
20200210213347
/
pushlog to previous
nightly mac
75.0a1
/
20200210213347
/
pushlog to previous
nightly win32
75.0a1
/
20200210213347
/
pushlog to previous
nightly win64
75.0a1
/
20200210213347
/
pushlog to previous
|
--- a/gfx/wr/tileview/src/main.rs +++ b/gfx/wr/tileview/src/main.rs @@ -1,22 +1,33 @@ /* 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/. */ +/// Command line tool to convert logged tile cache files into a visualization. +/// +/// Steps to use this: +/// 1. enable webrender; enable gfx.webrender.debug.tile-cache-logging +/// 2. take a capture using ctrl-shift-3 +/// if all is well, there will be a .../wr-capture/tilecache folder with *.ron files +/// 3. run tileview with that folder as the first parameter and some empty output folder as the +/// 2nd: +/// cargo run --release -- /foo/bar/wr-capture/tilecache /tmp/tilecache +/// 4. open /tmp/tilecache/index.html + use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset}; use webrender::{TileSerializer, TileCacheInstanceSerializer, TileCacheLoggerUpdateLists}; +use webrender::{PrimitiveCompareResultDetail, CompareHelperResult, UpdateKind, ItemUid}; use serde::Deserialize; -//use ron::de::from_reader; use std::fs::File; use std::io::prelude::*; use std::path::Path; use std::ffi::OsString; -use webrender::api::enumerate_interners; -use webrender::UpdateKind; +use std::collections::HashMap; +use webrender::api::{enumerate_interners, ColorF}; use euclid::{Rect, Transform3D}; use webrender_api::units::{PicturePoint, PictureSize, PicturePixel, WorldPixel}; static RES_JAVASCRIPT: &'static str = include_str!("tilecache.js"); static RES_BASE_CSS: &'static str = include_str!("tilecache_base.css"); #[derive(Deserialize)] pub struct Slice { @@ -29,58 +40,66 @@ static CSS_FRACTIONAL_OFFSET: &str static CSS_BACKGROUND_COLOR: &str = "fill:#10c070;fill-opacity:0.1;"; static CSS_SURFACE_OPACITY_CHANNEL: &str = "fill:#c040c0;fill-opacity:0.1;"; static CSS_NO_TEXTURE: &str = "fill:#c04040;fill-opacity:0.1;"; static CSS_NO_SURFACE: &str = "fill:#40c040;fill-opacity:0.1;"; static CSS_PRIM_COUNT: &str = "fill:#40f0f0;fill-opacity:0.1;"; static CSS_CONTENT: &str = "fill:#f04040;fill-opacity:0.1;"; static CSS_COMPOSITOR_KIND_CHANGED: &str = "fill:#f0c070;fill-opacity:0.1;"; -fn tile_node_to_svg(node: &TileNode, transform: &Transform3D<f32, PicturePixel, WorldPixel>) -> String +// parameters to tweak the SVG generation +struct SvgSettings { + pub scale: f32, + pub x: f32, + pub y: f32, +} + +fn tile_node_to_svg(node: &TileNode, + transform: &Transform3D<f32, PicturePixel, WorldPixel>, + svg_settings: &SvgSettings) -> String { match &node.kind { TileNodeKind::Leaf { .. } => { let rect_world = transform.transform_rect(&node.rect).unwrap(); format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" />\n", - rect_world.origin.x, - rect_world.origin.y, - rect_world.size.width, - rect_world.size.height) + rect_world.origin.x * svg_settings.scale + svg_settings.x, + rect_world.origin.y * svg_settings.scale + svg_settings.y, + rect_world.size.width * svg_settings.scale, + rect_world.size.height * svg_settings.scale) }, TileNodeKind::Node { children } => { - children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform) ) + children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform, svg_settings) ) } } } fn tile_to_svg(key: TileOffset, tile: &TileSerializer, slice: &Slice, prev_tile: Option<&TileSerializer>, + itemuid_to_string: &HashMap<ItemUid, String>, tile_stroke: &str, prim_class: &str, invalidation_report: &mut String, - svg_width: &mut i32, svg_height: &mut i32 ) -> String + svg_width: &mut i32, svg_height: &mut i32, + svg_settings: &SvgSettings) -> String { let mut svg = format!("\n<!-- tile key {},{} ; -->\n", key.x, key.y); let tile_fill = match tile.invalidation_reason { - Some(InvalidationReason::FractionalOffset) => CSS_FRACTIONAL_OFFSET.to_string(), - Some(InvalidationReason::BackgroundColor) => CSS_BACKGROUND_COLOR.to_string(), - Some(InvalidationReason::SurfaceOpacityChanged) => CSS_SURFACE_OPACITY_CHANNEL.to_string(), + Some(InvalidationReason::FractionalOffset { .. }) => CSS_FRACTIONAL_OFFSET.to_string(), + Some(InvalidationReason::BackgroundColor { .. }) => CSS_BACKGROUND_COLOR.to_string(), + Some(InvalidationReason::SurfaceOpacityChanged { .. }) => CSS_SURFACE_OPACITY_CHANNEL.to_string(), Some(InvalidationReason::NoTexture) => CSS_NO_TEXTURE.to_string(), Some(InvalidationReason::NoSurface) => CSS_NO_SURFACE.to_string(), - Some(InvalidationReason::PrimCount) => CSS_PRIM_COUNT.to_string(), + Some(InvalidationReason::PrimCount { .. }) => CSS_PRIM_COUNT.to_string(), Some(InvalidationReason::CompositorKindChanged) => CSS_COMPOSITOR_KIND_CHANGED.to_string(), - Some(InvalidationReason::Content { prim_compare_result } ) => { - let _foo = prim_compare_result; - CSS_CONTENT.to_string() //TODO do something with the compare result - } + Some(InvalidationReason::Content { .. } ) => CSS_CONTENT.to_string(), None => { let mut background = tile.background_color; if background.is_none() { background = slice.tile_cache.background_color } match background { Some(color) => { let rgb = ( (color.r * 255.0) as u8, @@ -98,33 +117,165 @@ fn tile_to_svg(key: TileOffset, let title = match tile.invalidation_reason { Some(_) => format!("<title>slice {} tile ({},{}) - {:?}</title>", slice.tile_cache.slice, key.x, key.y, tile.invalidation_reason), None => String::new() }; - if let Some(reason) = tile.invalidation_reason { + if let Some(reason) = &tile.invalidation_reason { invalidation_report.push_str( - &format!("\n<tspan x=\"0\" dy=\"16px\">slice {} tile key ({},{}) invalidated: {:?}</tspan>\n", - slice.tile_cache.slice, key.x, key.y, reason)); + &format!("<div class=\"subheader\">slice {} key ({},{})</div><div class=\"data\">", + slice.tile_cache.slice, + key.x, key.y)); + + // go through most reasons individually so we can print something nicer than + // the default debug formatting of old and new: + match reason { + InvalidationReason::FractionalOffset { old, new } => { + invalidation_report.push_str( + &format!("<b>FractionalOffset</b> changed from ({},{}) to ({},{})", + old.x, old.y, new.x, new.y)); + }, + InvalidationReason::BackgroundColor { old, new } => { + fn to_str(c: &Option<ColorF>) -> String { + if let Some(c) = c { + format!("({},{},{},{})", c.r, c.g, c.b, c.a) + } else { + "none".to_string() + } + } + + invalidation_report.push_str( + &format!("<b>BackGroundColor</b> changed from {} to {}", + to_str(old), to_str(new))); + }, + InvalidationReason::SurfaceOpacityChanged { became_opaque } => { + invalidation_report.push_str( + &format!("<b>SurfaceOpacityChanged</b> changed from {} to {}", + !became_opaque, became_opaque)); + }, + InvalidationReason::PrimCount { old, new } => { + // diff the lists to find removed and added ItemUids, + // and convert them to strings to pretty-print what changed: + let old = old.as_ref().unwrap(); + let new = new.as_ref().unwrap(); + let removed = old.iter() + .filter(|i| !new.contains(i)) + .fold(String::new(), + |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..." + + &itemuid_to_string.get(i).unwrap_or(&String::new()) + + "</li>\n"); + let added = new.iter() + .filter(|i| !old.contains(i)) + .fold(String::new(), + |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..." + + &itemuid_to_string.get(i).unwrap_or(&String::new()) + + "</li>\n"); + invalidation_report.push_str( + &format!("<b>PrimCount</b> changed from {} to {}:<br/>\ + removed:<ul>{}</ul> + added:<ul>{}</ul>", + old.len(), new.len(), + removed, added)); + }, + InvalidationReason::Content { prim_compare_result, prim_compare_result_detail } => { + let _ = prim_compare_result; + match prim_compare_result_detail { + Some(PrimitiveCompareResultDetail::Descriptor { old, new }) => { + if old.prim_uid == new.prim_uid { + // if the prim uid hasn't changed then try to print something useful + invalidation_report.push_str( + &format!("<b>Content: Descriptor</b> changed for uid {}<br/>", + old.prim_uid.get_uid())); + let mut changes = String::new(); + if old.origin != new.origin { + changes += &format!("<li><b>origin</b> changed from ({},{}) to ({},{})</li>", + old.origin.x, old.origin.y, + new.origin.x, new.origin.y); + } + if old.prim_clip_rect != new.prim_clip_rect { + changes += &format!("<li><b>prim_clip_rect</b> changed from {}x{} at ({},{})", + old.prim_clip_rect.w, + old.prim_clip_rect.h, + old.prim_clip_rect.x, + old.prim_clip_rect.y); + changes += &format!(" to {}x{} at ({},{})</li>", + new.prim_clip_rect.w, + new.prim_clip_rect.h, + new.prim_clip_rect.x, + new.prim_clip_rect.y); + } + invalidation_report.push_str( + &format!("<ul>{}<li>Item: {}</li></ul>", + changes, + &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new()))); + } else { + // .. if prim UIDs have changed, just dump both items and descriptors. + invalidation_report.push_str( + &format!("<b>Content: Descriptor</b> changed; old uid {}, new uid {}:<br/>", + old.prim_uid.get_uid(), + new.prim_uid.get_uid())); + invalidation_report.push_str( + &format!("old:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>", + old, + &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new()))); + invalidation_report.push_str( + &format!("new:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>", + new, + &itemuid_to_string.get(&new.prim_uid).unwrap_or(&String::new()))); + } + }, + Some(PrimitiveCompareResultDetail::Clip { detail }) => { + match detail { + CompareHelperResult::Count { prev_count, curr_count } => { + invalidation_report.push_str( + &format!("<b>Content: Clip</b> count changed from {} to {}<br/>", + prev_count, curr_count )); + }, + CompareHelperResult::NotEqual { prev, curr } => { + invalidation_report.push_str( + &format!("<b>Content: Clip</b> ItemUids changed from {} to {}:<br/>", + prev.get_uid(), curr.get_uid() )); + invalidation_report.push_str( + &format!("old:<ul><li>{}</li></ul>", + &itemuid_to_string.get(&prev).unwrap_or(&String::new()))); + invalidation_report.push_str( + &format!("new:<ul><li>{}</li></ul>", + &itemuid_to_string.get(&curr).unwrap_or(&String::new()))); + }, + reason => { + invalidation_report.push_str(&format!("{:?}", reason)); + }, + } + }, + reason => { + invalidation_report.push_str(&format!("{:?}", reason)); + }, + } + }, + reason => { + invalidation_report.push_str(&format!("{:?}", reason)); + }, + } + invalidation_report.push_str("</div>\n"); } svg = format!(r#"{}<rect x="{}" y="{}" width="{}" height="{}" style="{}" ></rect>"#, svg, - tile.rect.origin.x, - tile.rect.origin.y, - tile.rect.size.width, - tile.rect.size.height, + tile.rect.origin.x * svg_settings.scale + svg_settings.x, + tile.rect.origin.y * svg_settings.scale + svg_settings.y, + tile.rect.size.width * svg_settings.scale, + tile.rect.size.height * svg_settings.scale, tile_style); svg = format!("{}\n\n<g class=\"svg_quadtree\">\n{}</g>\n", svg, - tile_node_to_svg(&tile.root, &slice.transform)); + tile_node_to_svg(&tile.root, &slice.transform, svg_settings)); let right = (tile.rect.origin.x + tile.rect.size.width) as i32; let bottom = (tile.rect.origin.y + tile.rect.size.height) as i32; *svg_width = if right > *svg_width { right } else { *svg_width }; *svg_height = if bottom > *svg_height { bottom } else { *svg_height }; svg += "\n<!-- primitives -->\n"; @@ -152,50 +303,52 @@ fn tile_to_svg(key: TileOffset, } else { "class=\"svg_changed_prim\" " } } else { "class=\"svg_changed_prim\" " }; svg += &format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" {}/>", - rect_world.origin.x, - rect_world.origin.y, - rect_world.size.width, - rect_world.size.height, + rect_world.origin.x * svg_settings.scale + svg_settings.x, + rect_world.origin.y * svg_settings.scale + svg_settings.y, + rect_world.size.width * svg_settings.scale, + rect_world.size.height * svg_settings.scale, style); svg += "\n\t"; } svg += "\n</g>\n"; // nearly invisible, all we want is the toolip really let style = "style=\"fill-opacity:0.001;"; svg += &format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" {}{}\" >{}<\u{2f}rect>", - tile.rect.origin.x, - tile.rect.origin.y, - tile.rect.size.width, - tile.rect.size.height, + tile.rect.origin.x * svg_settings.scale + svg_settings.x, + tile.rect.origin.y * svg_settings.scale + svg_settings.y, + tile.rect.size.width * svg_settings.scale, + tile.rect.size.height * svg_settings.scale, style, tile_stroke, title); svg } fn slices_to_svg(slices: &[Slice], prev_slices: Option<Vec<Slice>>, + itemuid_to_string: &HashMap<ItemUid, String>, svg_width: &mut i32, svg_height: &mut i32, - max_slice_index: &mut usize) -> String + max_slice_index: &mut usize, + svg_settings: &SvgSettings) -> (String, String) { let svg_begin = "<?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache_base.css\" ?>\n\ <?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache.css\" ?>\n"; let mut svg = String::new(); - let mut invalidation_report = String::new(); + let mut invalidation_report = "<div class=\"header\">Invalidation</div>\n".to_string(); for slice in slices { let tile_cache = &slice.tile_cache; *max_slice_index = if tile_cache.slice > *max_slice_index { tile_cache.slice } else { *max_slice_index }; let prim_class = format!("tile_slice{}", tile_cache.slice); //println!("slice {}", tile_cache.slice); @@ -216,34 +369,36 @@ fn slices_to_svg(slices: &[Slice], prev_ } for (key, tile) in &tile_cache.tiles { let mut prev_tile = None; if let Some(prev) = prev_slice { prev_tile = prev.tile_cache.tiles.get(key); } - //println!("fofs {:?}", tile.fract_offset); - //println!("id {:?}", tile.id); - //println!("invr {:?}", tile.invalidation_reason); - svg.push_str(&tile_to_svg(*key, &tile, &slice, prev_tile, &tile_stroke, &prim_class, - &mut invalidation_report, svg_width, svg_height)); + svg.push_str(&tile_to_svg(*key, &tile, &slice, prev_tile, + itemuid_to_string, + &tile_stroke, &prim_class, + &mut invalidation_report, + svg_width, svg_height, svg_settings)); } } - svg.push_str(&format!("<text x=\"0\" y=\"-8px\" class=\"svg_invalidated\">{}</text>\n", invalidation_report)); - - format!(r#"{}<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" >"#, - svg_begin, - svg_width, - svg_height) + ( + format!("{}<svg version=\"1.1\" baseProfile=\"full\" xmlns=\"http://www.w3.org/2000/svg\" \ + width=\"{}\" height=\"{}\" >", + svg_begin, + svg_width, + svg_height) + "\n" + "<rect fill=\"black\" width=\"100%\" height=\"100%\"/>\n" + &svg - + "\n</svg>\n" + + "\n</svg>\n", + invalidation_report + ) } fn write_html(output_dir: &Path, svg_files: &[String], intern_files: &[String]) { let html_head = "<!DOCTYPE html>\n\ <html>\n\ <head>\n\ <meta charset=\"UTF-8\">\n\ <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\ @@ -264,17 +419,16 @@ fn write_html(output_dir: &Path, svg_fil script = format!("{}];\n\n", script); script = format!("{}var intern_files = [\n", script); for intern_file in intern_files { script = format!("{} \"{}\",\n", script, intern_file); } script = format!("{}];\n</script>\n\n", script); - //TODO this requires copying the js file from somewhere? script = format!("{}<script src=\"tilecache.js\" type=\"text/javascript\"></script>\n\n", script); let html_end = "</body>\n\ </html>\n" .to_string(); let html_body = format!( @@ -305,142 +459,169 @@ fn write_html(output_dir: &Path, svg_fil let html = format!("{}{}{}{}", html_head, html_body, script, html_end); let output_file = output_dir.join("index.html"); let mut html_output = File::create(output_file).unwrap(); html_output.write_all(html.as_bytes()).unwrap(); } -fn write_css(output_dir: &Path, max_slice_index: usize) { +fn write_css(output_dir: &Path, max_slice_index: usize, svg_settings: &SvgSettings) { let mut css = String::new(); for ix in 0..max_slice_index + 1 { let color = ( ix % 7 ) + 1; let rgb = format!("rgb({},{},{})", if color & 2 != 0 { 205 } else { 90 }, if color & 4 != 0 { 205 } else { 90 }, if color & 1 != 0 { 225 } else { 90 }); let prim_class = format!("tile_slice{}", ix); css += &format!("#{} {{\n\ fill: {};\n\ fill-opacity: 0.03;\n\ - stroke-width: 0.8;\n\ + stroke-width: {};\n\ stroke: {};\n\ }}\n\n", prim_class, //rgb, "none", + 0.8 * svg_settings.scale, rgb); } let output_file = output_dir.join("tilecache.css"); let mut css_output = File::create(output_file).unwrap(); css_output.write_all(css.as_bytes()).unwrap(); } macro_rules! updatelist_to_html_macro { ( $( $name:ident: $ty:ty, )+ ) => { - fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists) -> String { + fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists, + invalidation_report: String) -> String + { let mut html = "\ <!DOCTYPE html>\n\ <html> <head> <meta charset=\"UTF-8\">\n\ <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\ <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\ - </head> <body>\n".to_string(); + </head> <body>\n\ + <div class=\"datasheet\">\n".to_string(); + html += &invalidation_report; + + html += "<div class=\"header\">Interning</div>\n"; $( - html += &format!("<div class=\"intern_header\">{}</div>\n<div class=\"intern_data\">\n", + html += &format!("<div class=\"subheader\">{}</div>\n<div class=\"intern data\">\n", stringify!($name)); - let mut insert_count = 0; - for update in &update_lists.$name.1.updates { - match update.kind { - UpdateKind::Insert => { - html += &format!("<div class=\"insert\">{} {}</div>\n", - update.index, - format!("({:?})", update_lists.$name.1.data[insert_count])); - insert_count = insert_count + 1; - } - _ => { - html += &format!("<div class=\"remove\">{}</div>\n", - update.index); - } - }; + for list in &update_lists.$name.1 { + let mut insert_count = 0; + for update in &list.updates { + match update.kind { + UpdateKind::Insert => { + html += &format!("<div class=\"insert\"><b>{}</b> {}</div>\n", + update.uid.get_uid(), + format!("({:?})", list.data[insert_count])); + insert_count = insert_count + 1; + } + _ => { + html += &format!("<div class=\"remove\"><b>{}</b></div>\n", + update.uid.get_uid()); + } + }; + } } html += "</div><br/>\n"; )+ - html += "</body> </html>\n"; + html += "</div> </body> </html>\n"; html } } } enumerate_interners!(updatelist_to_html_macro); fn write_tile_cache_visualizer_svg(entry: &std::fs::DirEntry, output_dir: &Path, slices: &[Slice], prev_slices: Option<Vec<Slice>>, + itemuid_to_string: &HashMap<ItemUid, String>, svg_width: &mut i32, svg_height: &mut i32, max_slice_index: &mut usize, - svg_files: &mut Vec::<String>) + svg_files: &mut Vec::<String>, + svg_settings: &SvgSettings) -> String { - let svg = slices_to_svg(&slices, prev_slices, svg_width, svg_height, max_slice_index); + let (svg, invalidation_report) = slices_to_svg(&slices, prev_slices, + itemuid_to_string, + svg_width, svg_height, + max_slice_index, + svg_settings); let mut output_filename = OsString::from(entry.path().file_name().unwrap()); output_filename.push(".svg"); svg_files.push(output_filename.to_string_lossy().to_string()); output_filename = output_dir.join(output_filename).into_os_string(); let mut svg_output = File::create(output_filename).unwrap(); svg_output.write_all(svg.as_bytes()).unwrap(); + + invalidation_report } fn write_update_list_html(entry: &std::fs::DirEntry, output_dir: &Path, update_lists: &TileCacheLoggerUpdateLists, - html_files: &mut Vec::<String>) + html_files: &mut Vec::<String>, + invalidation_report: String) { - let html = updatelist_to_html(update_lists); + let html = updatelist_to_html(update_lists, invalidation_report); let mut output_filename = OsString::from(entry.path().file_name().unwrap()); output_filename.push(".html"); html_files.push(output_filename.to_string_lossy().to_string()); output_filename = output_dir.join(output_filename).into_os_string(); let mut html_output = File::create(output_filename).unwrap(); html_output.write_all(html.as_bytes()).unwrap(); } fn main() { let args: Vec<String> = std::env::args().collect(); - if args.len() != 3 { - println!("Usage: tileview input_dir output_dir"); + if args.len() < 3 { + println!("Usage: tileview input_dir output_dir [scale [x y]]"); println!(" where input_dir is a tile_cache folder inside a wr-capture."); + println!(" Scale is an optional scaling factor to compensate for high-DPI."); + println!(" X, Y is an optional offset to shift the entire SVG by."); println!("\nexample: cargo run c:/Users/me/AppData/Local/wr-capture.6/tile_cache/ c:/temp/tilecache/"); std::process::exit(1); } let input_dir = Path::new(&args[1]); let output_dir = Path::new(&args[2]); std::fs::create_dir_all(output_dir).unwrap(); + let scale = if args.len() >= 4 { args[3].parse::<f32>().unwrap() } else { 1.0 }; + let x = if args.len() >= 6 { args[4].parse::<f32>().unwrap() } else { 0.0 }; // >= 6, requires X and Y + let y = if args.len() >= 6 { args[5].parse::<f32>().unwrap() } else { 0.0 }; + let svg_settings = SvgSettings { scale, x, y }; + let mut svg_width = 100i32; let mut svg_height = 100i32; let mut max_slice_index = 0; let mut entries: Vec<_> = std::fs::read_dir(input_dir).unwrap() //.map(|r| r.unwrap()) .filter_map(|r| r.ok()) .collect(); entries.sort_by_key(|dir| dir.path()); let mut svg_files: Vec::<String> = Vec::new(); let mut intern_files: Vec::<String> = Vec::new(); let mut prev_slices = None; + let mut itemuid_to_string = HashMap::default(); + for entry in &entries { if entry.path().is_dir() { continue; } print!("processing {:?}\t", entry.path()); let file_data = std::fs::read_to_string(entry.path()).unwrap(); let chunks: Vec<_> = file_data.split("// @@@ chunk @@@").collect(); let slices: Vec<Slice> = match ron::de::from_str(&chunks[0]) { @@ -448,30 +629,34 @@ fn main() { Err(e) => { println!("ERROR: failed to deserialize slicesg {:?}\n{:?}", entry.path(), e); prev_slices = None; continue; } }; let mut update_lists = TileCacheLoggerUpdateLists::new(); update_lists.from_ron(&chunks[1]); + update_lists.insert_in_lookup(&mut itemuid_to_string); - write_tile_cache_visualizer_svg(&entry, &output_dir, - &slices, prev_slices, - &mut svg_width, &mut svg_height, - &mut max_slice_index, - &mut svg_files); + let invalidation_report = write_tile_cache_visualizer_svg( + &entry, &output_dir, + &slices, prev_slices, + &itemuid_to_string, + &mut svg_width, &mut svg_height, + &mut max_slice_index, + &mut svg_files, + &svg_settings); write_update_list_html(&entry, &output_dir, &update_lists, - &mut intern_files); + &mut intern_files, invalidation_report); print!("\r"); prev_slices = Some(slices); } write_html(output_dir, &svg_files, &intern_files); - write_css(output_dir, max_slice_index); + write_css(output_dir, max_slice_index, &svg_settings); std::fs::write(output_dir.join("tilecache.js"), RES_JAVASCRIPT).unwrap(); std::fs::write(output_dir.join("tilecache_base.css"), RES_BASE_CSS).unwrap(); println!("\n"); }
--- a/gfx/wr/tileview/src/tilecache_base.css +++ b/gfx/wr/tileview/src/tilecache_base.css @@ -8,62 +8,77 @@ width: 100%; height: 100%; color: orange; border: 0px; overflow: auto; background: white; } -.intern_header { +.datasheet .header { + color: white; + font-family: Arial; + font-weight: bold; + font-size: 150%; + line-height: 200%; + background-color: grey; + margin-top: 15px; + margin-bottom: 5px; + padding-left: 10px; +} + +.datasheet .subheader { color: blue; font-family: Arial; font-weight: bold; line-height: 200%; background-color: lightgrey; margin-top: 5px; margin-bottom: 5px; } -.intern_data { +.datasheet .data { font-family: monospace; - font-size: small; + margin-bottom: 5px; } -.intern_data .insert:nth-child(even) { +.datasheet .data *:nth-child(even) { background: #FFFFFF; } -.intern_data .insert:nth-child(odd) { +.datasheet .data *:nth-child(odd) { background: #EFEFEF; } -.intern_data .insert { - color: #008000; +.datasheet .data .insert { + color: #006000; } -.intern_data .remove { - color: #800000; +.datasheet .data .remove { + color: #600000; } +.datasheet .data .change { + color: #000060; +} .split { position: fixed; z-index: 1; top: 0; padding-top: 14px; } .left { left: 0; } .right { right: 0; - width: 25%; + width: 30%; height: 90%; } #svg_ui_overlay { position:absolute; right:0; top:0; z-index:70;
--- a/gfx/wr/webrender/src/intern.rs +++ b/gfx/wr/webrender/src/intern.rs @@ -120,16 +120,17 @@ pub enum UpdateKind { Remove, } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct Update { pub index: usize, + pub uid: ItemUid, pub kind: UpdateKind, } pub trait InternDebug { fn on_interned(&self, _uid: ItemUid) {} } /// The data store lives in the frame builder thread. It @@ -256,28 +257,31 @@ impl<I: Internable> Interner<I> { // We need to intern a new data item. First, find out // if there is a spare slot in the free-list that we // can use. Otherwise, append to the end of the list. let index = match self.free_list.pop() { Some(index) => index, None => self.local_data.len(), }; + let uid = ItemUid::next_uid(); + // Add a pending update to insert the new data. self.updates.push(Update { index, + uid, kind: UpdateKind::Insert, }); self.update_data.alloc().init(data.clone()); // Generate a handle for access via the data store. let handle = Handle { index: index as u32, epoch: self.current_epoch, - uid: ItemUid::next_uid(), + uid, _marker: PhantomData, }; #[cfg(debug_assertions)] data.on_interned(handle.uid); // Store this handle so the next time it is // interned, it gets re-used. @@ -311,16 +315,17 @@ impl<I: Internable> Interner<I> { if handle.epoch.0 + 10 < current_epoch { // To expire an item: // - Add index to the free-list for re-use. // - Add an update to the data store to invalidate this slow. // - Remove from the hash map. free_list.push(handle.index as usize); updates.push(Update { index: handle.index as usize, + uid: handle.uid, kind: UpdateKind::Remove, }); return false; } true }); let updates = UpdateList {
--- a/gfx/wr/webrender/src/lib.rs +++ b/gfx/wr/webrender/src/lib.rs @@ -216,11 +216,12 @@ pub use crate::renderer::{ GraphicsApiInfo, PipelineInfo, Renderer, RendererError, RendererOptions, RenderResults, RendererStats, SceneBuilderHooks, ThreadListener, ShaderPrecacheFlags, MAX_VERTEX_TEXTURE_WIDTH, }; pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle}; pub use crate::shade::{Shaders, WrShaders}; pub use api as webrender_api; pub use webrender_build::shader::ProgramSourceDigest; -pub use crate::picture::{TileDescriptor, TileId, InvalidationReason, PrimitiveCompareResult}; +pub use crate::picture::{TileDescriptor, TileId, InvalidationReason}; +pub use crate::picture::{PrimitiveCompareResult, PrimitiveCompareResultDetail, CompareHelperResult}; pub use crate::picture::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset, TileCacheLoggerUpdateLists}; -pub use crate::intern::{Update,UpdateKind}; +pub use crate::intern::{Update, UpdateKind, ItemUid};
--- a/gfx/wr/webrender/src/picture.rs +++ b/gfx/wr/webrender/src/picture.rs @@ -108,16 +108,18 @@ use crate::texture_cache::TextureCacheHa use crate::util::{MaxRect, scale_factors, VecHelper, RectHelpers}; use crate::filterdata::{FilterDataHandle}; #[cfg(any(feature = "capture", feature = "replay"))] use ron; #[cfg(feature = "capture")] use crate::scene_builder_thread::InternerUpdates; #[cfg(any(feature = "capture", feature = "replay"))] use crate::intern::{Internable, UpdateList}; +#[cfg(any(feature = "replay"))] +use crate::intern::{UpdateKind}; #[cfg(any(feature = "capture", feature = "replay"))] use api::{ClipIntern, FilterDataIntern, PrimitiveKeyKind}; #[cfg(any(feature = "capture", feature = "replay"))] use crate::prim_store::backdrop::Backdrop; #[cfg(any(feature = "capture", feature = "replay"))] use crate::prim_store::borders::{ImageBorder, NormalBorderPrim}; #[cfg(any(feature = "capture", feature = "replay"))] use crate::prim_store::gradient::{LinearGradient, RadialGradient}; @@ -133,16 +135,20 @@ use crate::prim_store::text_run::TextRun #[cfg(feature = "capture")] use std::fs::File; #[cfg(feature = "capture")] use std::io::prelude::*; #[cfg(feature = "capture")] use std::path::PathBuf; use crate::scene_building::{SliceFlags}; +#[cfg(feature = "replay")] +// used by tileview so don't use an internal_types FastHashMap +use std::collections::HashMap; + /// Specify whether a surface allows subpixel AA text rendering. #[derive(Debug, Copy, Clone, PartialEq)] pub enum SubpixelMode { /// This surface allows subpixel AA text Allow, /// Subpixel AA text cannot be drawn on this surface Deny, } @@ -350,29 +356,31 @@ fn clamp(value: i32, low: i32, high: i32 } fn clampf(value: f32, low: f32, high: f32) -> f32 { value.max(low).min(high) } /// An index into the prims array in a TileDescriptor. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PrimitiveDependencyIndex(u32); +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct PrimitiveDependencyIndex(pub u32); /// Information about the state of an opacity binding. #[derive(Debug)] pub struct OpacityBindingInfo { /// The current value retrieved from dynamic scene properties. value: f32, /// True if it was changed (or is new) since the last frame build. changed: bool, } /// Information stored in a tile descriptor for an opacity binding. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Copy)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub enum OpacityBinding { Value(f32), Binding(PropertyBindingId), } impl From<PropertyBinding<f32>> for OpacityBinding { @@ -580,16 +588,42 @@ impl TileSurface { match *self { TileSurface::Color { .. } => "Color", TileSurface::Texture { .. } => "Texture", TileSurface::Clear => "Clear", } } } +/// Optional extra information returned by is_same when +/// logging is enabled. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum CompareHelperResult<T> { + /// Primitives match + Equal, + /// Counts differ + Count { + prev_count: u8, + curr_count: u8, + }, + /// Sentinel + Sentinel, + /// Two items are not equal + NotEqual { + prev: T, + curr: T, + }, + /// User callback returned true on item + PredicateTrue { + curr: T + }, +} + /// The result of a primitive dependency comparison. Size is a u8 /// since this is a hot path in the code, and keeping the data small /// is a performance win. #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[repr(u8)] pub enum PrimitiveCompareResult { @@ -602,37 +636,80 @@ pub enum PrimitiveCompareResult { /// The value of the transform changed Transform, /// An image dependency was dirty Image, /// The value of an opacity binding changed OpacityBinding, } +/// A more detailed version of PrimitiveCompareResult used when +/// debug logging is enabled. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum PrimitiveCompareResultDetail { + /// Primitives match + Equal, + /// Something in the PrimitiveDescriptor was different + Descriptor { + old: PrimitiveDescriptor, + new: PrimitiveDescriptor, + }, + /// The clip node content or spatial node changed + Clip { + detail: CompareHelperResult<ItemUid>, + }, + /// The value of the transform changed + Transform { + detail: CompareHelperResult<SpatialNodeIndex>, + }, + /// An image dependency was dirty + Image { + detail: CompareHelperResult<ImageDependency>, + }, + /// The value of an opacity binding changed + OpacityBinding { + detail: CompareHelperResult<OpacityBinding>, + } +} + /// Debugging information about why a tile was invalidated -#[derive(Debug,Copy,Clone)] +#[derive(Debug,Clone)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub enum InvalidationReason { /// The fractional offset changed - FractionalOffset, + FractionalOffset { + old: PictureVector2D, + new: PictureVector2D, + }, /// The background color changed - BackgroundColor, + BackgroundColor { + old: Option<ColorF>, + new: Option<ColorF>, + }, /// The opaque state of the backing native surface changed - SurfaceOpacityChanged, + SurfaceOpacityChanged{ + became_opaque: bool + }, /// There was no backing texture (evicted or never rendered) NoTexture, /// There was no backing native surface (never rendered, or recreated) NoSurface, /// The primitive count in the dependency list was different - PrimCount, + PrimCount { + old: Option<Vec<ItemUid>>, + new: Option<Vec<ItemUid>>, + }, /// The content of one of the primitives was different Content { /// What changed in the primitive that was different prim_compare_result: PrimitiveCompareResult, + prim_compare_result_detail: Option<PrimitiveCompareResultDetail>, }, // The compositor type changed CompositorKindChanged, } /// A minimal subset of Tile for debug capturing #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] @@ -742,16 +819,17 @@ impl Tile { } /// Check if the content of the previous and current tile descriptors match fn update_dirty_rects( &mut self, ctx: &TilePostUpdateContext, state: &mut TilePostUpdateState, invalidation_reason: &mut Option<InvalidationReason>, + frame_context: &FrameVisibilityContext, ) -> PictureRect { let mut prim_comparer = PrimitiveComparer::new( &self.prev_descriptor, &self.current_descriptor, state.resource_cache, ctx.spatial_nodes, ctx.opacity_bindings, ); @@ -759,38 +837,41 @@ impl Tile { let mut dirty_rect = PictureRect::zero(); self.root.update_dirty_rects( &self.prev_descriptor.prims, &self.current_descriptor.prims, &mut prim_comparer, &mut dirty_rect, state.compare_cache, invalidation_reason, + frame_context, ); dirty_rect } /// Invalidate a tile based on change in content. This /// must be called even if the tile is not currently /// visible on screen. We might be able to improve this /// later by changing how ComparableVec is used. fn update_content_validity( &mut self, ctx: &TilePostUpdateContext, state: &mut TilePostUpdateState, + frame_context: &FrameVisibilityContext, ) { // Check if the contents of the primitives, clips, and // other dependencies are the same. state.compare_cache.clear(); let mut invalidation_reason = None; let dirty_rect = self.update_dirty_rects( ctx, state, &mut invalidation_reason, + frame_context, ); if !dirty_rect.is_empty() { self.invalidate( Some(dirty_rect), invalidation_reason.expect("bug: no invalidation_reason"), ); } } @@ -859,22 +940,26 @@ impl Tile { return; } // Determine if the fractional offset of the transform is different this frame // from the currently cached tile set. let fract_changed = (self.fract_offset.x - ctx.fract_offset.x).abs() > 0.01 || (self.fract_offset.y - ctx.fract_offset.y).abs() > 0.01; if fract_changed { - self.invalidate(None, InvalidationReason::FractionalOffset); + self.invalidate(None, InvalidationReason::FractionalOffset { + old: self.fract_offset, + new: ctx.fract_offset }); self.fract_offset = ctx.fract_offset; } if ctx.background_color != self.background_color { - self.invalidate(None, InvalidationReason::BackgroundColor); + self.invalidate(None, InvalidationReason::BackgroundColor { + old: self.background_color, + new: ctx.background_color }); self.background_color = ctx.background_color; } // Clear any dependencies so that when we rebuild them we // can compare if the tile has the same content. mem::swap( &mut self.current_descriptor, &mut self.prev_descriptor, @@ -965,26 +1050,27 @@ impl Tile { } /// Called during tile cache instance post_update. Allows invalidation and dirty /// rect calculation after primitive dependencies have been updated. fn post_update( &mut self, ctx: &TilePostUpdateContext, state: &mut TilePostUpdateState, + frame_context: &FrameVisibilityContext, ) -> bool { // If tile is not visible, just early out from here - we don't update dependencies // so don't want to invalidate, merge, split etc. The tile won't need to be drawn // (and thus updated / invalidated) until it is on screen again. if !self.is_visible { return false; } // Invalidate the tile based on the content changing. - self.update_content_validity(ctx, state); + self.update_content_validity(ctx, state, frame_context); // If there are no primitives there is no need to draw or cache it. if self.current_descriptor.prims.is_empty() { // If there is a native compositor surface allocated for this (now empty) tile // it must be freed here, otherwise the stale tile with previous contents will // be composited. If the tile subsequently gets new primitives added to it, the // surface will be re-allocated when it's added to the composite draw list. if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() { @@ -1121,24 +1207,24 @@ impl Tile { // Store the current surface backing info for use during batching. self.surface = Some(surface); true } } /// Defines a key that uniquely identifies a primitive instance. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct PrimitiveDescriptor { /// Uniquely identifies the content of the primitive template. - prim_uid: ItemUid, + pub prim_uid: ItemUid, /// The origin in world space of this primitive. - origin: PointKey, + pub origin: PointKey, /// The clip rect for this primitive. Included here in /// dependencies since there is no entry in the clip chain /// dependencies for the local clip rect. pub prim_clip_rect: RectangleKey, /// The number of extra dependencies that this primitive has. transform_dep_count: u8, image_dep_count: u8, opacity_binding_dep_count: u8, @@ -1173,24 +1259,24 @@ impl PartialEq for PrimitiveDescriptor { return false; } true } } /// A small helper to compare two arrays of primitive dependencies. -struct CompareHelper<'a, T> { +struct CompareHelper<'a, T> where T: Copy { offset_curr: usize, offset_prev: usize, curr_items: &'a [T], prev_items: &'a [T], } -impl<'a, T> CompareHelper<'a, T> where T: PartialEq { +impl<'a, T> CompareHelper<'a, T> where T: Copy + PartialEq { /// Construct a new compare helper for a current / previous set of dependency information. fn new( prev_items: &'a [T], curr_items: &'a [T], ) -> Self { CompareHelper { offset_curr: 0, offset_prev: 0, @@ -1207,47 +1293,56 @@ impl<'a, T> CompareHelper<'a, T> where T /// Test if two sections of the dependency arrays are the same, by checking both /// item equality, and a user closure to see if the content of the item changed. fn is_same<F>( &self, prev_count: u8, curr_count: u8, f: F, + opt_detail: Option<&mut CompareHelperResult<T>>, ) -> bool where F: Fn(&T) -> bool { // If the number of items is different, trivial reject. if prev_count != curr_count { + if let Some(detail) = opt_detail { *detail = CompareHelperResult::Count{ prev_count, curr_count }; } return false; } // If both counts are 0, then no need to check these dependencies. if curr_count == 0 { + if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; } return true; } // If both counts are u8::MAX, this is a sentinel that we can't compare these // deps, so just trivial reject. if curr_count as usize == MAX_PRIM_SUB_DEPS { + if let Some(detail) = opt_detail { *detail = CompareHelperResult::Sentinel; } return false; } let end_prev = self.offset_prev + prev_count as usize; let end_curr = self.offset_curr + curr_count as usize; let curr_items = &self.curr_items[self.offset_curr .. end_curr]; let prev_items = &self.prev_items[self.offset_prev .. end_prev]; for (curr, prev) in curr_items.iter().zip(prev_items.iter()) { if prev != curr { + if let Some(detail) = opt_detail { + *detail = CompareHelperResult::NotEqual{ prev: *prev, curr: *curr }; + } return false; } if f(curr) { + if let Some(detail) = opt_detail { *detail = CompareHelperResult::PredicateTrue{ curr: *curr }; } return false; } } + if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; } true } // Advance the prev dependency array by a given amount fn advance_prev(&mut self, count: u8) { self.offset_prev += count as usize; } @@ -1547,80 +1642,110 @@ macro_rules! declare_tile_cache_logger_u $( /// Generate storage, one per interner. /// the tuple is a workaround to avoid the need for multiple /// fields that start with $name (macro concatenation). /// the string is .ron serialized updatelist at capture time; /// the updates is the list of DataStore updates (avoid UpdateList /// due to Default() requirements on the Keys) reconstructed at /// load time. - pub $name: (String, UpdateList<<$ty as Internable>::Key>), + pub $name: (Vec<String>, Vec<UpdateList<<$ty as Internable>::Key>>), )+ } impl TileCacheLoggerUpdateLists { pub fn new() -> Self { TileCacheLoggerUpdateLists { $( - $name : ( String::new(), UpdateList{ updates: Vec::new(), data: Vec::new()} ), + $name : ( Vec::new(), Vec::new() ), )+ } } /// serialize all interners in updates to .ron #[cfg(feature = "capture")] fn serialize_updates( &mut self, updates: &InternerUpdates ) { $( - self.$name.0 = ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap(); + self.$name.0.push(ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap()); )+ } fn is_empty(&self) -> bool { $( if !self.$name.0.is_empty() { return false; } )+ true } #[cfg(feature = "capture")] fn to_ron(&self) -> String { let mut serializer = TileCacheLoggerUpdateListsSerializer { ron_string: Vec::new() }; $( serializer.ron_string.push( - if self.$name.0.is_empty() { - "( updates: [], data: [] )".to_string() - } else { - self.$name.0.clone() - }); + ron::ser::to_string_pretty(&self.$name.0, Default::default()).unwrap()); )+ ron::ser::to_string_pretty(&serializer, Default::default()).unwrap() } #[cfg(feature = "replay")] pub fn from_ron(&mut self, text: &str) { let serializer : TileCacheLoggerUpdateListsSerializer = match ron::de::from_str(&text) { Ok(data) => { data } Err(e) => { println!("ERROR: failed to deserialize updatelist: {:?}\n{:?}", &text, e); return; } }; let mut index = 0; $( - self.$name.1 = ron::de::from_str(&serializer.ron_string[index]).unwrap(); + let ron_lists : Vec<String> = ron::de::from_str(&serializer.ron_string[index]).unwrap(); + self.$name.1 = ron_lists.iter() + .map( |list| ron::de::from_str(&list).unwrap() ) + .collect(); index = index + 1; )+ // error: value assigned to `index` is never read let _ = index; } + + /// helper method to add a stringified version of all interned keys into + /// a lookup table based on ItemUid. Use strings as a form of type erasure + /// so all UpdateLists can go into a single map. + /// Then during analysis, when we see an invalidation reason due to + /// "ItemUid such and such was added to the tile primitive list", the lookup + /// allows mapping that back into something readable. + #[cfg(feature = "replay")] + pub fn insert_in_lookup( + &mut self, + itemuid_to_string: &mut HashMap<ItemUid, String>) + { + $( + { + for list in &self.$name.1 { + let mut insert_count = 0; + for update in &list.updates { + match update.kind { + UpdateKind::Insert => { + itemuid_to_string.insert( + update.uid, + format!("{:?}", list.data[insert_count])); + insert_count = insert_count + 1; + }, + _ => {} + } + } + } + } + )+ + } } } } #[cfg(any(feature = "capture", feature = "replay"))] enumerate_interners!(declare_tile_cache_logger_updatelists); #[cfg(not(any(feature = "capture", feature = "replay")))] @@ -2757,17 +2882,17 @@ impl TileCacheInstance { composite_state: frame_state.composite_state, compare_cache: &mut self.compare_cache, }; // Step through each tile and invalidate if the dependencies have changed. Determine // the current opacity setting and whether it's changed. let mut tile_cache_is_opaque = true; for (key, tile) in self.tiles.iter_mut() { - if tile.post_update(&ctx, &mut state) { + if tile.post_update(&ctx, &mut state, frame_context) { self.tiles_to_draw.push(*key); tile_cache_is_opaque &= tile.is_opaque; } } // If opacity changed, the native compositor surface and all tiles get invalidated. // (this does nothing if not using native compositor mode). // TODO(gw): This property probably changes very rarely, so it is OK to invalidate @@ -2780,17 +2905,18 @@ impl TileCacheInstance { // Since the native surface will be destroyed, need to clear the compositor tile // handle for all tiles. This means the tiles will be reallocated on demand // when the tiles are added to render tasks. for tile in self.tiles.values_mut() { if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { *id = None; } // Invalidate the entire tile to force a redraw. - tile.invalidate(None, InvalidationReason::SurfaceOpacityChanged); + tile.invalidate(None, InvalidationReason::SurfaceOpacityChanged { + became_opaque: tile_cache_is_opaque }); } // Destroy the compositor surface. It will be reallocated with the correct // opacity flag when render tasks are generated for tiles. frame_state.resource_cache.destroy_compositor_surface(native_surface_id); } self.is_opaque = tile_cache_is_opaque; } @@ -5123,17 +5249,17 @@ struct PrimitiveComparisonKey { prev_index: PrimitiveDependencyIndex, curr_index: PrimitiveDependencyIndex, } /// Information stored an image dependency #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] -struct ImageDependency { +pub struct ImageDependency { key: ImageKey, generation: ImageGeneration, } /// A helper struct to compare a primitive and all its sub-dependencies. struct PrimitiveComparer<'a> { clip_comparer: CompareHelper<'a, ItemUid>, transform_comparer: CompareHelper<'a, SpatialNodeIndex>, @@ -5204,75 +5330,97 @@ impl<'a> PrimitiveComparer<'a> { self.opacity_comparer.advance_curr(prim.opacity_binding_dep_count); } /// Check if two primitive descriptors are the same. fn compare_prim( &mut self, prev: &PrimitiveDescriptor, curr: &PrimitiveDescriptor, + opt_detail: Option<&mut PrimitiveCompareResultDetail>, ) -> PrimitiveCompareResult { let resource_cache = self.resource_cache; let spatial_nodes = self.spatial_nodes; let opacity_bindings = self.opacity_bindings; // Check equality of the PrimitiveDescriptor if prev != curr { + if let Some(detail) = opt_detail { + *detail = PrimitiveCompareResultDetail::Descriptor{ old: *prev, new: *curr }; + } return PrimitiveCompareResult::Descriptor; } // Check if any of the clips this prim has are different. + let mut clip_result = CompareHelperResult::Equal; if !self.clip_comparer.is_same( prev.clip_dep_count, curr.clip_dep_count, |_| { false - } + }, + if opt_detail.is_some() { Some(&mut clip_result) } else { None } ) { + if let Some(detail) = opt_detail { *detail = PrimitiveCompareResultDetail::Clip{ detail: clip_result }; } return PrimitiveCompareResult::Clip; } // Check if any of the transforms this prim has are different. + let mut transform_result = CompareHelperResult::Equal; if !self.transform_comparer.is_same( prev.transform_dep_count, curr.transform_dep_count, |curr| { spatial_nodes[curr].changed + }, + if opt_detail.is_some() { Some(&mut transform_result) } else { None }, + ) { + if let Some(detail) = opt_detail { + *detail = PrimitiveCompareResultDetail::Transform{ detail: transform_result }; } - ) { return PrimitiveCompareResult::Transform; } // Check if any of the images this prim has are different. + let mut image_result = CompareHelperResult::Equal; if !self.image_comparer.is_same( prev.image_dep_count, curr.image_dep_count, |curr| { resource_cache.get_image_generation(curr.key) != curr.generation + }, + if opt_detail.is_some() { Some(&mut image_result) } else { None }, + ) { + if let Some(detail) = opt_detail { + *detail = PrimitiveCompareResultDetail::Image{ detail: image_result }; } - ) { return PrimitiveCompareResult::Image; } // Check if any of the opacity bindings this prim has are different. + let mut bind_result = CompareHelperResult::Equal; if !self.opacity_comparer.is_same( prev.opacity_binding_dep_count, curr.opacity_binding_dep_count, |curr| { if let OpacityBinding::Binding(id) = curr { if opacity_bindings .get(id) .map_or(true, |info| info.changed) { return true; } } false + }, + if opt_detail.is_some() { Some(&mut bind_result) } else { None }, + ) { + if let Some(detail) = opt_detail { + *detail = PrimitiveCompareResultDetail::OpacityBinding{ detail: bind_result }; } - ) { return PrimitiveCompareResult::OpacityBinding; } PrimitiveCompareResult::Equal } } /// Details for a node in a quadtree that tracks dirty rects for a tile. @@ -5621,27 +5769,29 @@ impl TileNode { fn update_dirty_rects( &mut self, prev_prims: &[PrimitiveDescriptor], curr_prims: &[PrimitiveDescriptor], prim_comparer: &mut PrimitiveComparer, dirty_rect: &mut PictureRect, compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, invalidation_reason: &mut Option<InvalidationReason>, + frame_context: &FrameVisibilityContext, ) { match self.kind { TileNodeKind::Node { ref mut children, .. } => { for child in children.iter_mut() { child.update_dirty_rects( prev_prims, curr_prims, prim_comparer, dirty_rect, compare_cache, invalidation_reason, + frame_context, ); } } TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => { // If the index buffers are of different length, they must be different if prev_indices.len() == curr_indices.len() { let mut prev_i0 = 0; let mut prev_i1 = 0; @@ -5663,42 +5813,80 @@ impl TileNode { // Compare the primitives, caching the result in a hash map // to save comparisons in other tree nodes. let key = PrimitiveComparisonKey { prev_index: *prev_index, curr_index: *curr_index, }; + #[cfg(any(feature = "capture", feature = "replay"))] + let mut compare_detail = PrimitiveCompareResultDetail::Equal; + #[cfg(any(feature = "capture", feature = "replay"))] + let prim_compare_result_detail = + if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) { + Some(&mut compare_detail) + } else { + None + }; + + #[cfg(not(any(feature = "capture", feature = "replay")))] + let compare_detail = PrimitiveCompareResultDetail::Equal; + #[cfg(not(any(feature = "capture", feature = "replay")))] + let prim_compare_result_detail = None; + let prim_compare_result = *compare_cache .entry(key) .or_insert_with(|| { let prev = &prev_prims[i0]; let curr = &curr_prims[i1]; - prim_comparer.compare_prim(prev, curr) + prim_comparer.compare_prim(prev, curr, prim_compare_result_detail) }); // If not the same, mark this node as dirty and update the dirty rect if prim_compare_result != PrimitiveCompareResult::Equal { if invalidation_reason.is_none() { *invalidation_reason = Some(InvalidationReason::Content { prim_compare_result, + prim_compare_result_detail: Some(compare_detail) }); } *dirty_rect = self.rect.union(dirty_rect); *dirty_tracker = *dirty_tracker | 1; break; } prev_i0 = i0; prev_i1 = i1; } } else { if invalidation_reason.is_none() { - *invalidation_reason = Some(InvalidationReason::PrimCount); + // if and only if tile logging is enabled, do the expensive step of + // converting indices back to ItemUids and allocating old and new vectors + // to store them in. + #[cfg(any(feature = "capture", feature = "replay"))] + { + if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) { + let old = prev_indices.iter().map( |i| prev_prims[i.0 as usize].prim_uid ).collect(); + let new = curr_indices.iter().map( |i| curr_prims[i.0 as usize].prim_uid ).collect(); + *invalidation_reason = Some(InvalidationReason::PrimCount { + old: Some(old), + new: Some(new) }); + } else { + *invalidation_reason = Some(InvalidationReason::PrimCount { + old: None, + new: None }); + } + } + #[cfg(not(any(feature = "capture", feature = "replay")))] + { + *invalidation_reason = Some(InvalidationReason::PrimCount { + old: None, + new: None }); + } } *dirty_rect = self.rect.union(dirty_rect); *dirty_tracker = *dirty_tracker | 1; } } } } }
--- a/gfx/wr/webrender/src/prim_store/mod.rs +++ b/gfx/wr/webrender/src/prim_store/mod.rs @@ -607,17 +607,17 @@ impl From<WorldVector2D> for VectorKey { y: vec.y, } } } /// A hashable point for using as a key during primitive interning. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] -#[derive(Debug, Clone, MallocSizeOf, PartialEq)] +#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)] pub struct PointKey { pub x: f32, pub y: f32, } impl Eq for PointKey {} impl hash::Hash for PointKey {