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
This commit is contained in:
Bert Peers 2020-01-17 00:21:17 +00:00
parent b66b7fa491
commit b6fdcb621c
14 changed files with 819 additions and 14 deletions

View File

@ -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.

View File

@ -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);
}

10
gfx/wr/Cargo.lock generated
View File

@ -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"

View File

@ -6,6 +6,7 @@ members = [
"webrender_api",
"wrench",
"example-compositor/compositor",
"tileview",
]
[profile.release]

View File

@ -0,0 +1,16 @@
[package]
name = "tileview"
version = "0.1.0"
authors = ["Bert Peers <bpeers@mozilla.com>"]
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"] }

404
gfx/wr/tileview/src/main.rs Normal file
View File

@ -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!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" />\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<!-- tile key {},{} ; slice x {} y {}-->\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!("<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 {
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));
}
svg = format!(r#"{}<rect x="{}" y="{}" width="{}" height="{}" style="{}" ></rect>"#,
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<g class=\"svg_quadtree\">\n{}</g>\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<!-- primitives -->\n";
svg = format!("{}<g id=\"{}\">\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#"{}<rect x="{}" y="{}" width="{}" height="{}" {}/>"#,
svg,
x, y, w, h,
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,
style,
tile_stroke,
title);
svg
}
fn slices_to_svg(slices: &[Slice], prev_slices: Option<Vec<Slice>>,
svg_width: &mut i32, svg_height: &mut i32,
max_slice_index: &mut usize) -> String
{
let svg_begin = "<?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();
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<!-- tile_cache slice {} -->\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!("<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)
+ "\n"
+ "<rect fill=\"black\" width=\"100%\" height=\"100%\"/>\n"
+ &svg
+ "\n</svg>\n"
}
fn write_html(output_dir: &Path, svg_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.css\"></link>\n\
</head>\n"
.to_string();
let html_body = "<body bgcolor=\"#000000\" onload=\"load()\">\n"
.to_string();
let mut script = "\n<script>\n".to_string();
script = format!("{}var svg_files = [\n", script);
for svg_file in svg_files {
script = format!("{} \"{}\",\n", script, svg_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!(
"{}\n\
<object id=\"svg_container0\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
<object id=\"svg_container1\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
<div id=\"svg_ui_overlay\">\n\
<div id=\"text_frame_counter\">{}</div>\n\
<div id=\"text_spacebar\">Spacebar to Play</div>\n\
<div>Use Left/Right to Step</div>\n\
<input id=\"frame_slider\" type=\"range\" min=\"0\" max=\"{}\" value=\"0\" class=\"svg_ui_slider\" />
</div>",
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<String> = 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::<String> = 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<Slice> = 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. ");
}

View File

@ -0,0 +1,145 @@
// current SVG file for scrubbing and playback
var svg_index = 0;
// double buffered <object>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;
}
}

View File

@ -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<RenderTaskId> {
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;

View File

@ -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};

View File

@ -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<ColorF>,
pub invalidation_reason: Option<InvalidationReason>
}
/// 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<TileOffset, TileSerializer>,
pub background_color: Option<ColorF>,
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<Vec<TileCacheLoggerSlice>>
}
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<PrimitiveDependencyIndex>,
/// The index buffer of primitives that affect this tile on this frame
#[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
curr_indices: Vec<PrimitiveDependencyIndex>,
/// 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 {

View File

@ -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();
}

View File

@ -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<dyn RenderNotifier>,
recorder: Option<Box<dyn ApiRecordingReceiver>>,
logrecorder: Option<Box<LogRecorder>>,
tile_cache_logger: TileCacheLogger,
sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
size_of_ops: Option<MallocSizeOfOps>,
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,

View File

@ -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;
}
}

View File

@ -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);