From b6fdcb621caaaeac53d1f6b2e685d4d7a9b3a044 Mon Sep 17 00:00:00 2001 From: Bert Peers Date: Fri, 17 Jan 2020 00:21:17 +0000 Subject: [PATCH] Bug 1605283 - Improve support for invalidation debugging and testing r=gw Optionally serialize N frames into a circular memory buffer, and save them as part of wr-capture into tilecache/. The new tile_view utility reads the folder and converts the frames to svg for easier visualization, with a few extra features: - color code each primitive based on which slice it is on; - highlight new or moving primitives in red (brute force diff); - print all invalidated tiles at the top and the invalidation reason; - overlay the tile quadtree to visualize splitting & merging; - HTML and JS wrappers for animation playback, timeline scrubbing; Positioning of the tiles on the screen is a bit broken still; especially during scrolling and "special" pages (like about:config). Interning info is not used yet. Differential Revision: https://phabricator.services.mozilla.com/D59975 --HG-- extra : moz-landing-system : lando --- gfx/thebes/gfxPlatform.cpp | 2 + gfx/webrender_bindings/WebRenderAPI.cpp | 4 +- gfx/wr/Cargo.lock | 10 + gfx/wr/Cargo.toml | 1 + gfx/wr/tileview/Cargo.toml | 16 + gfx/wr/tileview/src/main.rs | 404 ++++++++++++++++++++++++ gfx/wr/tileview/src/tilecache.js | 145 +++++++++ gfx/wr/webrender/src/frame_builder.rs | 8 + gfx/wr/webrender/src/lib.rs | 2 + gfx/wr/webrender/src/picture.rs | 216 ++++++++++++- gfx/wr/webrender/src/prim_store/mod.rs | 7 +- gfx/wr/webrender/src/render_backend.rs | 13 +- gfx/wr/webrender_api/src/api.rs | 4 + modules/libpref/init/all.js | 1 + 14 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 gfx/wr/tileview/Cargo.toml create mode 100644 gfx/wr/tileview/src/main.rs create mode 100644 gfx/wr/tileview/src/tilecache.js diff --git a/gfx/thebes/gfxPlatform.cpp b/gfx/thebes/gfxPlatform.cpp index 04e19dd6ec96..973ad9b9f1f4 100644 --- a/gfx/thebes/gfxPlatform.cpp +++ b/gfx/thebes/gfxPlatform.cpp @@ -590,6 +590,8 @@ static void WebRenderDebugPrefChangeCallback(const char* aPrefName, void*) { GFX_WEBRENDER_DEBUG(".texture-cache.clear-evicted", wr::DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) GFX_WEBRENDER_DEBUG(".picture-caching", wr::DebugFlags::PICTURE_CACHING_DBG) + GFX_WEBRENDER_DEBUG(".tile-cache-logging", + wr::DebugFlags::TILE_CACHE_LOGGING_DBG) GFX_WEBRENDER_DEBUG(".primitives", wr::DebugFlags::PRIMITIVE_DBG) // Bit 18 is for the zoom display, which requires the mouse position and thus // currently only works in wrench. diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp index c7e47b54f1b7..fa10cff71c29 100644 --- a/gfx/webrender_bindings/WebRenderAPI.cpp +++ b/gfx/webrender_bindings/WebRenderAPI.cpp @@ -620,7 +620,9 @@ void WebRenderAPI::WaitFlushed() { } void WebRenderAPI::Capture() { - uint8_t bits = 3; // TODO: get from JavaScript + // see CaptureBits + // SCENE | FRAME | TILE_CACHE + uint8_t bits = 7; // TODO: get from JavaScript const char* path = "wr-capture"; // TODO: get from JavaScript wr_api_capture(mDocHandle, path, bits); } diff --git a/gfx/wr/Cargo.lock b/gfx/wr/Cargo.lock index f1797bb28344..0eb95ee858f7 100644 --- a/gfx/wr/Cargo.lock +++ b/gfx/wr/Cargo.lock @@ -1630,6 +1630,16 @@ dependencies = [ "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tileview" +version = "0.1.0" +dependencies = [ + "ron 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "webrender 0.60.0", + "webrender_api 0.60.0", +] + [[package]] name = "time" version = "0.1.40" diff --git a/gfx/wr/Cargo.toml b/gfx/wr/Cargo.toml index 4e9541d7dd08..bb6b204412a2 100644 --- a/gfx/wr/Cargo.toml +++ b/gfx/wr/Cargo.toml @@ -6,6 +6,7 @@ members = [ "webrender_api", "wrench", "example-compositor/compositor", + "tileview", ] [profile.release] diff --git a/gfx/wr/tileview/Cargo.toml b/gfx/wr/tileview/Cargo.toml new file mode 100644 index 000000000000..1bed00f5a9eb --- /dev/null +++ b/gfx/wr/tileview/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tileview" +version = "0.1.0" +authors = ["Bert Peers "] +license = "MPL-2.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# api = { version = "0.60.0", path = "../webrender_api", package = "webrender_api" } +ron = "0.1.5" +webrender = {path = "../webrender", features=["capture","replay","debugger","png","profiler","no_static_freetype", "leak_checks"]} +webrender_api = {path = "../webrender_api", features=["serialize","deserialize"]} +# api = {path = "../webrender_api", features=["serialize","deserialize"]} +serde = {version = "1.0", features = ["derive"] } diff --git a/gfx/wr/tileview/src/main.rs b/gfx/wr/tileview/src/main.rs new file mode 100644 index 000000000000..1147fb04e9f0 --- /dev/null +++ b/gfx/wr/tileview/src/main.rs @@ -0,0 +1,404 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset}; +use webrender::{TileSerializer, TileCacheInstanceSerializer}; +use serde::Deserialize; +//use ron::de::from_reader; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::ffi::OsString; + +#[derive(Deserialize)] +pub struct Slice { + pub x: f32, + pub y: f32, + pub tile_cache: TileCacheInstanceSerializer +} + +// invalidation reason CSS colors +static CSS_FRACTIONAL_OFFSET: &str = "fill:#4040c0;fill-opacity:0.1;"; +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;"; + +fn tile_node_to_svg(node: &TileNode, x: f32, y: f32) -> String +{ + match &node.kind { + TileNodeKind::Leaf { .. } => { + format!("\n", + (node.rect.origin.x + x) as i32, + (node.rect.origin.y + y) as i32, + node.rect.size.width, node.rect.size.height) + }, + TileNodeKind::Node { children } => { + children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, x, y) ) + } + } +} + +fn tile_to_svg(key: TileOffset, + tile: &TileSerializer, + slice: &Slice, + prev_tile: Option<&TileSerializer>, + tile_stroke: &str, + prim_class: &str, + invalidation_report: &mut String, + svg_width: &mut i32, svg_height: &mut i32 ) -> String +{ + let mut svg = format!("\n\n", key.x, key.y, slice.x, slice.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::NoTexture) => CSS_NO_TEXTURE.to_string(), + Some(InvalidationReason::NoSurface) => CSS_NO_SURFACE.to_string(), + Some(InvalidationReason::PrimCount) => CSS_PRIM_COUNT.to_string(), + Some(InvalidationReason::Content { prim_compare_result } ) => { + let _foo = prim_compare_result; + CSS_CONTENT.to_string() //TODO do something with the compare result + } + 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, + (color.g * 255.0) as u8, + (color.b * 255.0) as u8 ); + format!("fill:rgb({},{},{});fill-opacity:0.3;", rgb.0, rgb.1, rgb.2) + } + None => "fill:none;".to_string() + } + } + }; + + //let tile_style = format!("{}{}", tile_fill, tile_stroke); + let tile_style = format!("{}stroke:none;", tile_fill); + + let title = match tile.invalidation_reason { + Some(_) => format!("slice {} tile ({},{}) - {:?}", + slice.tile_cache.slice, key.x, key.y, + tile.invalidation_reason), + None => String::new() + }; + + if let Some(reason) = tile.invalidation_reason { + invalidation_report.push_str( + &format!("\nslice {} tile key ({},{}) invalidated: {:?}\n", + slice.tile_cache.slice, key.x, key.y, reason)); + } + + svg = format!(r#"{}"#, + svg, + //TODO --bpe are these in local space or screen space? + tile.rect.origin.x, + tile.rect.origin.y, + tile.rect.size.width, + tile.rect.size.height, + tile_style); + + svg = format!("{}\n\n\n{}\n", + svg, + //tile_node_to_svg(&tile.root, tile.rect.origin.x, tile.rect.origin.y)); + tile_node_to_svg(&tile.root, 0.0, 0.0)); + + 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\n"; + + svg = format!("{}\n\t", + svg, + prim_class); + + for prim in &tile.current_descriptor.prims { + let rect = prim.prim_clip_rect; + //TODO proper positioning of prims, especially when scrolling + // this version seems closest, but introduces gaps (eg in about:config) + let x = (rect.x + slice.x) as i32; + let y = (rect.y + slice.y) as i32; + // this version is .. interesting: when scrolling, nothing moves in about:config, + // instead the searchbox shifts down.. hmm.. + //let x = rect.x as i32; + //let y = rect.y as i32; + let w = rect.w as i32; + let h = rect.h as i32; + + let style = + if let Some(prev_tile) = prev_tile { + // when this O(n^2) gets too slow, stop brute-forcing and use a set or something + if prev_tile.current_descriptor.prims.iter().find(|&prim| prim.prim_clip_rect == rect).is_some() { + "" + } else { + "class=\"svg_changed_prim\" " + } + } else { + "class=\"svg_changed_prim\" " + }; + + svg = format!(r#"{}"#, + svg, + x, y, w, h, + style); + + svg += "\n\t"; + } + + svg += "\n\n"; + + // nearly invisible, all we want is the toolip really + let style = "style=\"fill-opacity:0.001;"; + svg += &format!("{}<\u{2f}rect>", + tile.rect.origin.x, + tile.rect.origin.y, + tile.rect.size.width, + tile.rect.size.height, + style, + tile_stroke, + title); + + svg +} + +fn slices_to_svg(slices: &[Slice], prev_slices: Option>, + svg_width: &mut i32, svg_height: &mut i32, + max_slice_index: &mut usize) -> String +{ + let svg_begin = "\n"; + + let mut svg = String::new(); + let mut invalidation_report = String::new(); + + 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); + svg.push_str(&format!("\n\n", + tile_cache.slice)); + + //let tile_stroke = "stroke:grey;stroke-width:1;".to_string(); + let tile_stroke = "stroke:none;".to_string(); + + let mut prev_slice = None; + if let Some(prev) = &prev_slices { + for prev_search in prev { + if prev_search.tile_cache.slice == tile_cache.slice { + prev_slice = Some(prev_search); + break; + } + } + } + + 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(&format!("{}\n", invalidation_report)); + + format!(r#"{}"#, + svg_begin, + svg_width, + svg_height) + + "\n" + + "\n" + + &svg + + "\n\n" +} + +fn write_html(output_dir: &Path, svg_files: &[String]) { + let html_head = "\n\ + \n\ + \n\ + \n\ + \n\ + \n" + .to_string(); + + let html_body = "\n" + .to_string(); + + + let mut script = "\n\n\n", script); + //TODO this requires copying the js file from somewhere? + script = format!("{}\n\n", script); + + + let html_end = "\n\ + \n" + .to_string(); + + let html_body = format!( + "{}\n\ + \n\ + \n\ +
\n\ +
{}
\n\ +
Spacebar to Play
\n\ +
Use Left/Right to Step
\n\ + +
", + html_body, + svg_files[0], + svg_files[0], + svg_files[0], + svg_files.len() ); + + 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) { + let mut css = ".tile_svg {\n\ + position: fixed;\n\ + }\n\n\n\ + .svg_invalidated {\n\ + fill: white;\n\ + font-family:monospace;\n\ + }\n\n\n\ + #svg_ui_overlay {\n\ + position:absolute;\n\ + right:0; \n\ + top:0; \n\ + z-index:70; \n\ + color: rgb(255,255,100);\n\ + font-family:monospace;\n\ + background-color: #404040a0;\n\ + }\n\n\n\ + .svg_quadtree {\n\ + fill: none;\n\ + stroke-width: 1;\n\ + stroke: orange;\n\ + }\n\n\n\ + .svg_changed_prim {\n\ + stroke: red;\n\ + stroke-width: 2.0;\n\ + }\n\n\n\ + #svg_ui_slider {\n\ + width:90%;\n\ + }\n\n".to_string(); + + 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\ + #{} {{\n\ + fill: {};\n\ + fill-opacity: 0.03;\n\ + stroke-width: 0.8;\n\ + stroke: {};\n\ + }}\n\n", + css, + prim_class, + //rgb, + "none", + 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(); +} + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() != 3 { + println!("Usage: tileview input_dir output_dir"); + println!(" where input_dir is a tile_cache folder inside a wr-capture."); + 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 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:: = Vec::new(); + let mut prev_slices = None; + + for entry in &entries { + if entry.path().is_dir() { + continue; + } + print!("processing {:?}\t", entry.path()); + let f = File::open(entry.path()).unwrap(); + let slices: Vec = match ron::de::from_reader(f) { + Ok(data) => { data } + Err(e) => { + println!("ERROR: failed to deserialize {:?}\n{:?}", entry.path(), e); + prev_slices = None; + continue; + } + }; + + let svg = slices_to_svg(&slices, prev_slices, &mut svg_width, &mut svg_height, &mut max_slice_index); + + 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(); + + print!("\r"); + prev_slices = Some(slices); + } + + write_html(output_dir, &svg_files); + write_css(output_dir, max_slice_index); + + println!("OK. For now, manually copy tilecache.js to the output folder please. "); +} diff --git a/gfx/wr/tileview/src/tilecache.js b/gfx/wr/tileview/src/tilecache.js new file mode 100644 index 000000000000..1e1d323c0a82 --- /dev/null +++ b/gfx/wr/tileview/src/tilecache.js @@ -0,0 +1,145 @@ +// current SVG file for scrubbing and playback +var svg_index = 0; + +// double buffered s each holding an SVG file +var backbuffer; +var frontbuffer; + + +// timer for animation +var svg_timer; +var is_playing = false; + +function toggle_play() { + if( is_playing ) { + is_playing = false; + clearInterval(svg_timer); + document.getElementById("text_spacebar").innerHTML = + 'Spacebar to play'; + } else { + is_playing = true; + svg_timer = setInterval(on_tick, 100); + document.getElementById("text_spacebar").innerHTML = + 'Playing (Spacebar to stop)'; + function on_tick() { + if( svg_index + 1 == svg_files.length ) { + toggle_play(); + } else { + go_to_svg(svg_index+1); + } + } + } +} + +function toggle_quadtree() { + var quad_groups = document.getElementsByClassName("svg_quadtree") + var i; + for (i = 0; i < quad_groups.length; i++) { + if( quad_groups[i].style.display == "none" ) + quad_groups[i].style.display = "block"; + else + quad_groups[i].style.display = "none"; + } +} + +// try to block repeated keypressed from causing flickering +// when they land between go_to_svg returning and onload +// firing. +var is_loading = false; + +function go_to_svg(index) { + if( index >= svg_files.length || + index < 0 || + is_loading ) { + return; + } + + is_loading = true; + svg_index = index; + + var slider = document.getElementById('frame_slider'); + // won't recurse thanks to is_loading + slider.value = svg_index; + + backbuffer.onload = function(){ + + document.getElementById("text_frame_counter").innerHTML = + svg_files[svg_index]; + + backbuffer.style.display = ''; + frontbuffer.style.display = 'none'; + + var t = frontbuffer; + frontbuffer = backbuffer; + backbuffer = t; + is_loading = false; + } + backbuffer.setAttribute('data', svg_files[svg_index]); + + // also see https://stackoverflow.com/a/29915275 +} + +function load() { + window.addEventListener('keypress', handle_keyboard_shortcut); + window.addEventListener('keydown', handle_keydown); + + frontbuffer = document.getElementById("svg_container0"); + backbuffer = document.getElementById("svg_container1"); + backbuffer.style.display='none'; + + var slider = document.getElementById('frame_slider'); + slider.oninput = function() { + if( is_playing ) { + toggle_play(); + } + go_to_svg(this.value); + } +} + +function handle_keyboard_shortcut(event) { + switch (event.charCode) { + case 32: // ' ' + toggle_play(); + break; + case 113: // 'q' + toggle_quadtree(); + break; + /* + case 49: // "1" key + document.getElementById("radio1").checked = true; + show_image(1); + break; + case 50: // "2" key + document.getElementById("radio2").checked = true; + show_image(2); + break; + case 100: // "d" key + document.getElementById("differences").click(); + break; + case 112: // "p" key + shift_images(-1); + break; + case 110: // "n" key + shift_images(1); + break; + */ + } +} + +function handle_keydown(event) { + switch (event.keyCode) { + case 37: // left arrow + go_to_svg(svg_index-1); + event.preventDefault(); + break; + case 38: // up arrow + break; + case 39: // right arrow + go_to_svg(svg_index+1); + event.preventDefault(); + break; + case 40: // down arrow + break; + } +} + diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs index f1fecfde5bb8..cd02e500285d 100644 --- a/gfx/wr/webrender/src/frame_builder.rs +++ b/gfx/wr/webrender/src/frame_builder.rs @@ -15,6 +15,7 @@ use crate::gpu_types::TransformData; use crate::internal_types::{FastHashMap, PlaneSplitter, SavedTargetIndex}; use crate::picture::{PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex, RecordedDirtyRegion}; use crate::picture::{RetainedTiles, TileCacheInstance, DirtyRegion, SurfaceRenderTasks, SubpixelMode}; +use crate::picture::{TileCacheLogger}; use crate::prim_store::{SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer}; use crate::prim_store::{DeferredResolve, PrimitiveVisibilityMask}; use crate::profiler::{FrameProfileCounters, TextureCacheProfileCounters, ResourceProfileCounters}; @@ -240,6 +241,7 @@ impl FrameBuilder { debug_flags: DebugFlags, texture_cache_profile: &mut TextureCacheProfileCounters, composite_state: &mut CompositeState, + tile_cache_logger: &mut TileCacheLogger, ) -> Option { profile_scope!("cull"); @@ -411,9 +413,12 @@ impl FrameBuilder { &mut frame_state, &frame_context, scratch, + tile_cache_logger ) .unwrap(); + tile_cache_logger.advance(); + { profile_marker!("PreparePrims"); @@ -425,6 +430,7 @@ impl FrameBuilder { &mut frame_state, data_stores, scratch, + tile_cache_logger, ); } @@ -465,6 +471,7 @@ impl FrameBuilder { scratch: &mut PrimitiveScratchBuffer, render_task_counters: &mut RenderTaskGraphCounters, debug_flags: DebugFlags, + tile_cache_logger: &mut TileCacheLogger, ) -> Frame { profile_scope!("build"); profile_marker!("BuildFrame"); @@ -534,6 +541,7 @@ impl FrameBuilder { debug_flags, &mut resource_profile.texture_cache, &mut composite_state, + tile_cache_logger, ); let mut passes; diff --git a/gfx/wr/webrender/src/lib.rs b/gfx/wr/webrender/src/lib.rs index 23f7ae2804c3..91545b7dcf1e 100644 --- a/gfx/wr/webrender/src/lib.rs +++ b/gfx/wr/webrender/src/lib.rs @@ -221,3 +221,5 @@ 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::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset}; diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs index d315c50fa1f3..1f6d288d855b 100644 --- a/gfx/wr/webrender/src/picture.rs +++ b/gfx/wr/webrender/src/picture.rs @@ -107,6 +107,15 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use crate::texture_cache::TextureCacheHandle; use crate::util::{TransformedRectKind, MatrixHelpers, MaxRect, scale_factors, VecHelper, RectHelpers}; use crate::filterdata::{FilterDataHandle}; +#[cfg(feature = "capture")] +use ron; + +#[cfg(feature = "capture")] +use std::fs::File; +#[cfg(feature = "capture")] +use std::io::prelude::*; +#[cfg(feature = "capture")] +use std::path::PathBuf; /// Specify whether a surface allows subpixel AA text rendering. #[derive(Debug, Copy, Clone, PartialEq)] @@ -300,7 +309,7 @@ fn clampf(value: f32, low: f32, high: f32) -> f32 { /// An index into the prims array in a TileDescriptor. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct PrimitiveDependencyIndex(u32); +pub struct PrimitiveDependencyIndex(u32); /// Information about the state of an opacity binding. #[derive(Debug)] @@ -313,6 +322,8 @@ pub struct OpacityBindingInfo { /// Information stored in a tile descriptor for an opacity binding. #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] pub enum OpacityBinding { Value(f32), Binding(PropertyBindingId), @@ -532,8 +543,10 @@ impl TileSurface { /// 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)] -enum PrimitiveCompareResult { +pub enum PrimitiveCompareResult { /// Primitives match Equal, /// Something in the PrimitiveDescriptor was different @@ -549,8 +562,10 @@ enum PrimitiveCompareResult { } /// Debugging information about why a tile was invalidated -#[derive(Debug)] -enum InvalidationReason { +#[derive(Debug,Copy,Clone)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum InvalidationReason { /// The fractional offset changed FractionalOffset, /// The background color changed @@ -570,6 +585,29 @@ enum InvalidationReason { }, } +/// A minimal subset of Tile for debug capturing +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileSerializer { + pub rect: PictureRect, + pub current_descriptor: TileDescriptor, + pub fract_offset: PictureVector2D, + pub id: TileId, + pub root: TileNode, + pub background_color: Option, + pub invalidation_reason: Option +} + +/// A minimal subset of TileCacheInstance for debug capturing +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileCacheInstanceSerializer { + pub slice: usize, + pub tiles: FastHashMap, + pub background_color: Option, + pub fract_offset: PictureVector2D, +} + /// Information about a cached tile. pub struct Tile { /// The current world rect of this tile. @@ -1023,6 +1061,8 @@ impl Tile { /// Defines a key that uniquely identifies a primitive instance. #[derive(Debug, 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, @@ -1031,7 +1071,7 @@ pub struct PrimitiveDescriptor { /// 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. - prim_clip_rect: RectangleKey, + pub prim_clip_rect: RectangleKey, /// The number of extra dependencies that this primitive has. transform_dep_count: u8, image_dep_count: u8, @@ -1153,6 +1193,9 @@ impl<'a, T> CompareHelper<'a, T> where T: PartialEq { /// Uniquely describes the content of this tile, in a way that can be /// (reasonably) efficiently hashed and compared. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] pub struct TileDescriptor { /// List of primitive instance unique identifiers. The uid is guaranteed /// to uniquely describe the content of the primitive template, while @@ -1222,7 +1265,7 @@ impl TileDescriptor { pt.new_level("images".to_string()); for info in &self.images { pt.new_level(format!("key={:?}", info.key)); - pt.new_level(format!("generation={:?}", info.generation)); + pt.add_item(format!("generation={:?}", info.generation)); pt.end_level(); } pt.end_level(); @@ -1419,6 +1462,107 @@ impl BackdropInfo { } } +#[derive(Clone)] +pub struct TileCacheLoggerSlice { + pub serialized_slice: String, + pub local_to_world_transform: DeviceRect +} + +/// Log tile cache activity whenever anything happens in take_context. +pub struct TileCacheLogger { + /// next write pointer + pub write_index : usize, + /// ron serialization of tile caches; + /// each frame consists of slices, one per take_context call + pub frames: Vec> +} + +impl TileCacheLogger { + pub fn new( + num_frames: usize + ) -> Self { + let mut frames = Vec::with_capacity(num_frames); + let empty_element = Vec::new(); + frames.resize(num_frames, empty_element); + TileCacheLogger { + write_index: 0, + frames + } + } + + pub fn is_enabled(&self) -> bool { + !self.frames.is_empty() + } + + #[cfg(feature = "capture")] + pub fn add(&mut self, serialized_slice: String, local_to_world_transform: DeviceRect) { + if !self.is_enabled() { + return; + } + self.frames[self.write_index].push( + TileCacheLoggerSlice { + serialized_slice, + local_to_world_transform }); + } + + /// see if anything was written in this frame, and if so, + /// advance the write index in a circular way and clear the + /// recorded string. + pub fn advance(&mut self) { + if !self.is_enabled() || self.frames[self.write_index].is_empty() { + return; + } + self.write_index = self.write_index + 1; + if self.write_index >= self.frames.len() { + self.write_index = 0; + } + self.frames[self.write_index] = Vec::new(); + } + + #[cfg(feature = "capture")] + pub fn save_capture( + &self, root: &PathBuf + ) { + if !self.is_enabled() { + return; + } + use std::fs; + + info!("saving tile cache log"); + let path_tile_cache = root.join("tile_cache"); + if !path_tile_cache.is_dir() { + fs::create_dir(&path_tile_cache).unwrap(); + } + + let mut files_written = 0; + for ix in 0..self.frames.len() { + // ...and start with write_index, since that's the oldest entry + // that we're about to overwrite. However when we get to + // save_capture, we've add()ed entries but haven't advance()d yet, + // so the actual oldest entry is write_index + 1 + let index = (self.write_index + 1 + ix) % self.frames.len(); + if self.frames[index].is_empty() { + continue; + } + + let filename = path_tile_cache.join(format!("frame{:05}.ron", files_written)); + files_written = files_written + 1; + let mut output = File::create(filename).unwrap(); + output.write_all(b"[\n").unwrap(); + for item in &self.frames[index] { + output.write_all(format!("( x: {}, y: {},\n", + item.local_to_world_transform.origin.x, + item.local_to_world_transform.origin.y) + .as_bytes()).unwrap(); + output.write_all(b"tile_cache:\n").unwrap(); + output.write_all(item.serialized_slice.as_bytes()).unwrap(); + output.write_all(b"\n),\n").unwrap(); + } + output.write_all(b"]\n").unwrap(); + } + } +} + /// Represents a cache of tiles that make up a picture primitives. pub struct TileCacheInstance { /// Index of the tile cache / slice for this frame builder. It's determined @@ -3275,6 +3419,7 @@ impl PicturePrimitive { frame_state: &mut FrameBuildingState, frame_context: &FrameBuildingContext, scratch: &mut PrimitiveScratchBuffer, + tile_cache_logger: &mut TileCacheLogger, ) -> Option<(PictureContext, PictureState, PrimitiveList)> { if !self.is_visible() { return None; @@ -3338,6 +3483,7 @@ impl PicturePrimitive { } }; + let unclipped = match self.raster_config { Some(ref raster_config) => { let pic_rect = PictureRect::from_untyped(&self.precise_local_rect.to_untyped()); @@ -3926,10 +4072,46 @@ impl PicturePrimitive { root, ); } + + unclipped } - None => {} + None => DeviceRect::zero() }; + #[cfg(feature = "capture")] + { + if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) { + if let Some(ref tile_cache) = self.tile_cache + { + // extract just the fields that we're interested in + let mut tile_cache_tiny = TileCacheInstanceSerializer { + slice: tile_cache.slice, + tiles: FastHashMap::default(), + background_color: tile_cache.background_color, + fract_offset: tile_cache.fract_offset + }; + for (key, tile) in &tile_cache.tiles { + tile_cache_tiny.tiles.insert(*key, TileSerializer { + rect: tile.rect, + current_descriptor: tile.current_descriptor.clone(), + fract_offset: tile.fract_offset, + id: tile.id, + root: tile.root.clone(), + background_color: tile.background_color, + invalidation_reason: tile.invalidation_reason.clone() + }); + } + let text = ron::ser::to_string_pretty(&tile_cache_tiny, Default::default()).unwrap(); + tile_cache_logger.add(text, unclipped); + } + } + } + #[cfg(not(feature = "capture"))] + { + let _tile_cache_logger = tile_cache_logger; // unused variable fix + let _unclipped = unclipped; + } + let state = PictureState { //TODO: check for MAX_CACHE_SIZE here? map_local_to_pic, @@ -4682,6 +4864,8 @@ struct PrimitiveComparisonKey { /// Information stored an image dependency #[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] struct ImageDependency { key: ImageKey, generation: ImageGeneration, @@ -4830,15 +5014,22 @@ impl<'a> PrimitiveComparer<'a> { } /// Details for a node in a quadtree that tracks dirty rects for a tile. -enum TileNodeKind { +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TileNodeKind { Leaf { /// The index buffer of primitives that affected this tile previous frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] prev_indices: Vec, /// The index buffer of primitives that affect this tile on this frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] curr_indices: Vec, /// A bitset of which of the last 64 frames have been dirty for this leaf. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] dirty_tracker: u64, /// The number of frames since this node split or merged. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] frames_since_modified: usize, }, Node { @@ -4855,11 +5046,14 @@ enum TileModification { } /// A node in the dirty rect tracking quadtree. -struct TileNode { +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileNode { /// Leaf or internal node - kind: TileNodeKind, + pub kind: TileNodeKind, /// Rect of this node in the same space as the tile cache picture - rect: PictureRect, + pub rect: PictureRect, } impl TileNode { diff --git a/gfx/wr/webrender/src/prim_store/mod.rs b/gfx/wr/webrender/src/prim_store/mod.rs index e90922dff5d2..99ba0311aa16 100644 --- a/gfx/wr/webrender/src/prim_store/mod.rs +++ b/gfx/wr/webrender/src/prim_store/mod.rs @@ -27,7 +27,7 @@ use crate::image::{Repetition}; use crate::intern; use crate::internal_types::PlaneSplitAnchor; use malloc_size_of::MallocSizeOf; -use crate::picture::{PictureCompositeMode, PicturePrimitive, ClusterFlags}; +use crate::picture::{PictureCompositeMode, PicturePrimitive, ClusterFlags, TileCacheLogger}; use crate::picture::{PrimitiveList, RecordedDirtyRegion, SurfaceIndex, RetainedTiles, RasterConfig}; use crate::prim_store::backdrop::BackdropDataHandle; use crate::prim_store::borders::{ImageBorderDataHandle, NormalBorderDataHandle}; @@ -2656,6 +2656,7 @@ impl PrimitiveStore { plane_split_anchor: PlaneSplitAnchor, data_stores: &mut DataStores, scratch: &mut PrimitiveScratchBuffer, + tile_cache_log: &mut TileCacheLogger, ) -> bool { // If we have dependencies, we need to prepare them first, in order // to know the actual rect of this primitive. @@ -2681,6 +2682,7 @@ impl PrimitiveStore { frame_state, frame_context, scratch, + tile_cache_log, ) { Some(info) => Some(info), None => { @@ -2724,6 +2726,7 @@ impl PrimitiveStore { frame_state, data_stores, scratch, + tile_cache_log, ); // Restore the dependencies (borrow check dance) @@ -2789,6 +2792,7 @@ impl PrimitiveStore { frame_state: &mut FrameBuildingState, data_stores: &mut DataStores, scratch: &mut PrimitiveScratchBuffer, + tile_cache_log: &mut TileCacheLogger, ) { for (cluster_index, cluster) in prim_list.clusters.iter_mut().enumerate() { pic_state.map_local_to_pic.set_target_spatial_node( @@ -2834,6 +2838,7 @@ impl PrimitiveStore { plane_split_anchor, data_stores, scratch, + tile_cache_log, ) { frame_state.profile_counters.visible_primitives.inc(); } diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs index 690a167cfe8f..f997299570f7 100644 --- a/gfx/wr/webrender/src/render_backend.rs +++ b/gfx/wr/webrender/src/render_backend.rs @@ -31,7 +31,7 @@ use crate::hit_test::{HitTest, HitTester}; use crate::intern::DataStore; use crate::internal_types::{DebugOutput, FastHashMap, FastHashSet, RenderedDocument, ResultMsg}; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; -use crate::picture::RetainedTiles; +use crate::picture::{RetainedTiles, TileCacheLogger}; use crate::prim_store::{PrimitiveScratchBuffer, PrimitiveInstance}; use crate::prim_store::{PrimitiveInstanceKind, PrimTemplateCommonData}; use crate::prim_store::interned::*; @@ -540,6 +540,7 @@ impl Document { gpu_cache: &mut GpuCache, resource_profile: &mut ResourceProfileCounters, debug_flags: DebugFlags, + tile_cache_logger: &mut TileCacheLogger, ) -> RenderedDocument { let accumulated_scale_factor = self.view.accumulated_scale_factor(); let pan = self.view.pan.to_f32() / accumulated_scale_factor; @@ -566,6 +567,7 @@ impl Document { &mut self.scratch, &mut self.render_task_counters, debug_flags, + tile_cache_logger, ); self.hit_tester = Some(self.scene.create_hit_tester(&self.data_stores.clip)); frame @@ -712,6 +714,7 @@ pub struct RenderBackend { notifier: Box, recorder: Option>, logrecorder: Option>, + tile_cache_logger: TileCacheLogger, sampler: Option>, size_of_ops: Option, debug_flags: DebugFlags, @@ -754,6 +757,7 @@ impl RenderBackend { notifier, recorder, logrecorder: None, + tile_cache_logger: TileCacheLogger::new(500usize), sampler, size_of_ops, debug_flags, @@ -1529,6 +1533,7 @@ impl RenderBackend { &mut self.gpu_cache, &mut profile_counters.resources, self.debug_flags, + &mut self.tile_cache_logger, ); debug!("generated frame for document {:?} with {} passes", @@ -1706,6 +1711,7 @@ impl RenderBackend { &mut self.gpu_cache, &mut profile_counters.resources, self.debug_flags, + &mut self.tile_cache_logger, ); // After we rendered the frames, there are pending updates to both // GPU cache and resources. Instead of serializing them, we are going to make sure @@ -1753,6 +1759,11 @@ impl RenderBackend { debug!("\tresource cache"); let (resources, deferred) = self.resource_cache.save_capture(&config.root); + if config.bits.contains(CaptureBits::TILE_CACHE) { + debug!("\ttile cache"); + self.tile_cache_logger.save_capture(&config.root); + } + info!("\tbackend"); let backend = PlainRenderBackend { default_device_pixel_ratio: self.default_device_pixel_ratio, diff --git a/gfx/wr/webrender_api/src/api.rs b/gfx/wr/webrender_api/src/api.rs index f8387be0206e..5f1dfaeea204 100644 --- a/gfx/wr/webrender_api/src/api.rs +++ b/gfx/wr/webrender_api/src/api.rs @@ -910,6 +910,8 @@ bitflags!{ const SCENE = 0x1; /// const FRAME = 0x2; + /// + const TILE_CACHE = 0x4; } } @@ -1411,6 +1413,8 @@ bitflags! { const DISABLE_PICTURE_CACHING = 1 << 27; /// If set, dump picture cache invalidation debug to console. const INVALIDATION_DBG = 1 << 28; + /// Log tile cache to memory for later saving as part of wr-capture + const TILE_CACHE_LOGGING_DBG = 1 << 29; } } diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index c9b5da5292e2..3502f79004be 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -684,6 +684,7 @@ pref("gfx.webrender.debug.new-scene-indicator", false); pref("gfx.webrender.debug.show-overdraw", false); pref("gfx.webrender.debug.slow-frame-indicator", false); pref("gfx.webrender.debug.picture-caching", false); +pref("gfx.webrender.debug.tile-cache-logging", false); pref("gfx.webrender.debug.primitives", false); pref("gfx.webrender.debug.small-screen", false); pref("gfx.webrender.debug.obscure-images", false);