summaryrefslogtreecommitdiffstats
path: root/gfx/wr/tileview
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /gfx/wr/tileview
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--gfx/wr/tileview/Cargo.toml15
-rw-r--r--gfx/wr/tileview/src/main.rs724
-rw-r--r--gfx/wr/tileview/src/tilecache.js187
-rw-r--r--gfx/wr/tileview/src/tilecache_base.css109
4 files changed, 1035 insertions, 0 deletions
diff --git a/gfx/wr/tileview/Cargo.toml b/gfx/wr/tileview/Cargo.toml
new file mode 100644
index 0000000000..e9ea9af13e
--- /dev/null
+++ b/gfx/wr/tileview/Cargo.toml
@@ -0,0 +1,15 @@
+[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]
+ron = "0.6.2"
+serde = {version = "1.0.88", features = ["derive"] }
+webrender = {path = "../webrender", features=["capture","replay","debugger","png","profiler","no_static_freetype", "leak_checks"]}
+webrender_api = {path = "../webrender_api", features=["serialize","deserialize"]}
+euclid = { version = "0.22.0", features = ["serde"] }
diff --git a/gfx/wr/tileview/src/main.rs b/gfx/wr/tileview/src/main.rs
new file mode 100644
index 0000000000..d3290260ca
--- /dev/null
+++ b/gfx/wr/tileview/src/main.rs
@@ -0,0 +1,724 @@
+/* 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
+///
+/// Note: accurate interning info requires that the circular buffer doesn't wrap around.
+/// So for best results, use this workflow:
+/// a. start up blank browser; in about:config enable logging; close browser
+/// b. start new browser, quickly load the repro
+/// c. capture.
+///
+/// If that's tricky, you can also just throw more memory at it: in render_backend.rs,
+/// increase the buffer size here: 'TileCacheLogger::new(500usize)'
+///
+/// Note: some features don't work when opening index.html directly due to cross-scripting
+/// protections. Instead use a HTTP server:
+/// python -m SimpleHTTPServer 8000
+
+
+use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset};
+use webrender::{TileSerializer, TileCacheInstanceSerializer, TileCacheLoggerUpdateLists};
+use webrender::{PrimitiveCompareResultDetail, CompareHelperResult, ItemUid};
+use serde::Deserialize;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::Path;
+use std::ffi::OsString;
+use std::collections::HashMap;
+use webrender::enumerate_interners;
+use webrender::api::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 {
+ pub transform: Transform3D<f32, PicturePixel, WorldPixel>,
+ 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;";
+static CSS_COMPOSITOR_KIND_CHANGED: &str = "fill:#f0c070;fill-opacity:0.1;";
+static CSS_VALID_RECT_CHANGED: &str = "fill:#ff00ff;fill-opacity:0.1;";
+static CSS_SCALE_CHANGED: &str = "fill:#ff80ff;fill-opacity:0.1;";
+
+// 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.outer_transformed_rect(&node.rect.to_rect()).unwrap();
+ format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" />\n",
+ 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, 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,
+ 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::NoTexture) => CSS_NO_TEXTURE.to_string(),
+ Some(InvalidationReason::NoSurface) => CSS_NO_SURFACE.to_string(),
+ Some(InvalidationReason::PrimCount { .. }) => CSS_PRIM_COUNT.to_string(),
+ Some(InvalidationReason::CompositorKindChanged) => CSS_COMPOSITOR_KIND_CHANGED.to_string(),
+ Some(InvalidationReason::Content { .. } ) => CSS_CONTENT.to_string(),
+ Some(InvalidationReason::ValidRectChanged) => CSS_VALID_RECT_CHANGED.to_string(),
+ Some(InvalidationReason::ScaleChanged) => CSS_SCALE_CHANGED.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,
+ (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!("<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.prim_clip_box != new.prim_clip_box {
+ changes += &format!("<li><b>prim_clip_rect</b> changed from {},{} -> {},{}",
+ old.prim_clip_box.min.x,
+ old.prim_clip_box.min.y,
+ old.prim_clip_box.max.x,
+ old.prim_clip_box.max.y);
+ changes += &format!(" to {},{} -> {},{}</li>",
+ new.prim_clip_box.min.x,
+ new.prim_clip_box.min.y,
+ new.prim_clip_box.max.x,
+ new.prim_clip_box.max.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>"#,
+ 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",
+ 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";
+
+ svg += &format!("<g id=\"{}\">\n\t", prim_class);
+
+
+ let rect_visual_id = Rect {
+ origin: tile.rect.origin,
+ size: PictureSize::new(1.0, 1.0)
+ };
+ let rect_visual_id_world = slice.transform.outer_transformed_rect(&rect_visual_id).unwrap();
+ svg += &format!("\n<text class=\"svg_tile_visual_id\" x=\"{}\" y=\"{}\">{},{} ({})</text>",
+ rect_visual_id_world.origin.x * svg_settings.scale + svg_settings.x,
+ (rect_visual_id_world.origin.y + 110.0) * svg_settings.scale + svg_settings.y,
+ key.x, key.y, slice.tile_cache.slice);
+
+
+ for prim in &tile.current_descriptor.prims {
+ let rect = prim.prim_clip_box;
+
+ // the transform could also be part of the CSS, let the browser do it;
+ // might be a bit faster and also enable actual 3D transforms.
+ let rect_pixel = Rect {
+ origin: PicturePoint::new(rect.min.x, rect.min.y),
+ size: PictureSize::new(rect.max.x - rect.min.x, rect.max.y - rect.min.y),
+ };
+ let rect_world = slice.transform.outer_transformed_rect(&rect_pixel).unwrap();
+
+ 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_box == rect).is_some() {
+ ""
+ } 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 * 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 * 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,
+ 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 = "<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 };
+
+ invalidation_report.push_str(&format!("<div id=\"invalidation_slice{}\">\n", tile_cache.slice));
+
+ let prim_class = format!("tile_slice{}", tile_cache.slice);
+
+ svg += &format!("\n<g id=\"tile_slice{}_everything\">", tile_cache.slice);
+
+ //println!("slice {}", tile_cache.slice);
+ svg += &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);
+ }
+
+ svg += &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 += "\n</g>";
+
+ invalidation_report.push_str("</div>\n");
+ }
+
+ (
+ 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",
+ invalidation_report
+ )
+}
+
+fn write_html(output_dir: &Path, max_slice_index: usize, 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\
+ <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\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);
+
+ script = format!("{}<script src=\"tilecache.js\" type=\"text/javascript\"></script>\n\n", script);
+
+
+ let html_end = "</body>\n\
+ </html>\n"
+ .to_string();
+
+ let mut html_slices_form =
+ "\n<form id=\"slicecontrols\">\n\
+ Slice\n".to_string();
+
+ for ix in 0..max_slice_index + 1 {
+ html_slices_form +=
+ &format!(
+ "<input id=\"slice_toggle{}\" \
+ type=\"checkbox\" \
+ onchange=\"update_slice_visibility({})\" \
+ checked=\"checked\" />\n\
+ <label for=\"slice_toggle{}\">{}</label>\n",
+ ix,
+ max_slice_index + 1,
+ ix,
+ ix );
+ }
+
+ html_slices_form += "<form>\n";
+
+ let html_body = format!(
+ "{}\n\
+ <div class=\"split left\">\n\
+ <div>\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>\n\
+ </div>\n\
+ \n\
+ <div class=\"split right\">\n\
+ <iframe width=\"100%\" id=\"intern\" src=\"{}\"></iframe>\n\
+ </div>\n\
+ \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],
+ intern_files[0],
+ svg_files[0],
+ svg_files.len(),
+ html_slices_form );
+
+ 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, 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: {};\n\
+ stroke: {};\n\
+ }}\n\n",
+ prim_class,
+ //rgb,
+ "none",
+ 0.8 * svg_settings.scale,
+ rgb);
+ }
+
+ css += &format!(".svg_tile_visual_id {{\n\
+ font: {}px sans-serif;\n\
+ fill: rgb(50,50,50);\n\
+ }}\n\n",
+ 150.0 * svg_settings.scale);
+
+ 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,
+ 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\
+ <div class=\"datasheet\">\n".to_string();
+
+ html += &invalidation_report;
+
+ html += "<div class=\"header\">Interning</div>\n";
+ $(
+ html += &format!("<div class=\"subheader\">{}</div>\n<div class=\"intern data\">\n",
+ stringify!($name));
+ for list in &update_lists.$name.1 {
+ for insertion in &list.insertions {
+ html += &format!("<div class=\"insert\"><b>{}</b> {}</div>\n",
+ insertion.uid.get_uid(),
+ format!("({:?})", insertion.value));
+ }
+
+ for removal in &list.removals {
+ html += &format!("<div class=\"remove\"><b>{}</b></div>\n",
+ removal.uid.get_uid());
+ }
+ }
+ html += "</div><br/>\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_settings: &SvgSettings) -> String
+{
+ 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>,
+ invalidation_report: String)
+{
+ 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 [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()
+ .filter_map(|r| r.ok())
+ .collect();
+ // auto-fix a missing 'tile_cache' postfix on the input path -- easy to do when copy-pasting a
+ // path to a wr-capture; there should at least be a frame00000.ron...
+ let frame00000 = entries.iter().find(|&entry| entry.path().ends_with("frame00000.ron"));
+ // ... and if not, try again with 'tile_cache' appended to the input folder
+ if frame00000.is_none() {
+ let new_path = input_dir.join("tile_cache");
+ entries = std::fs::read_dir(new_path).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]) {
+ Ok(data) => { data }
+ 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);
+
+ 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, invalidation_report);
+
+ print!("\r");
+ prev_slices = Some(slices);
+ }
+
+ write_html(output_dir, max_slice_index, &svg_files, &intern_files);
+ 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");
+}
diff --git a/gfx/wr/tileview/src/tilecache.js b/gfx/wr/tileview/src/tilecache.js
new file mode 100644
index 0000000000..0ea3d98af5
--- /dev/null
+++ b/gfx/wr/tileview/src/tilecache.js
@@ -0,0 +1,187 @@
+// 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";
+ }
+}
+
+function update_slice_visibility(max_slice) {
+ let content = frontbuffer.contentDocument;
+ update_slice_visibility_for_content(content, max_slice);
+}
+
+function update_slice_visibility_for_content(content, max_slice) {
+
+ intern = document.getElementById('intern').contentDocument;
+
+ for (let slice = 0; slice != max_slice; ++slice) {
+ var cbox_name = "slice_toggle" + slice;
+ let cbox = document.getElementById(cbox_name);
+ if( !cbox )
+ continue;
+ let checked = cbox.checked;
+ if (content) { // might fail due to cross scripting -- use SimpleHTTPServer
+ var id = "tile_slice" + slice + "_everything";
+ var group = content.getElementById(id);
+ if (group) {
+ if (checked)
+ group.style.display = "block";
+ else
+ group.style.display = "none";
+ }
+ }
+ if (intern) {
+ var id = "invalidation_slice" + slice;
+ var div = intern.getElementById(id);
+ if (div) {
+ if (checked)
+ div.style.display = "block";
+ else
+ div.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];
+
+ let content = backbuffer.contentDocument;
+ update_slice_visibility_for_content(content, 20);
+
+ backbuffer.style.display = '';
+ frontbuffer.style.display = 'none';
+
+ var t = frontbuffer;
+ frontbuffer = backbuffer;
+ backbuffer = t;
+ is_loading = false;
+ }
+ document.getElementById('intern').src = intern_files[svg_index];
+ 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/tileview/src/tilecache_base.css b/gfx/wr/tileview/src/tilecache_base.css
new file mode 100644
index 0000000000..5b6e994ad8
--- /dev/null
+++ b/gfx/wr/tileview/src/tilecache_base.css
@@ -0,0 +1,109 @@
+.tile_svg {
+ float: left;
+}
+
+#intern {
+ position:relative;
+ top:60px;
+ width: 100%;
+ height: 100%;
+ color: orange;
+ border: 0px;
+ overflow: auto;
+ background: white;
+}
+
+.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;
+}
+
+.datasheet .data {
+ font-family: monospace;
+ margin-bottom: 5px;
+}
+
+.datasheet .data *:nth-child(even) {
+ background: #FFFFFF;
+}
+.datasheet .data *:nth-child(odd) {
+ background: #EFEFEF;
+}
+
+.datasheet .data .insert {
+ color: #006000;
+}
+
+.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: 30%;
+ height: 90%;
+}
+
+#svg_ui_overlay {
+ position:absolute;
+ right:0;
+ top:0;
+ z-index:70;
+ color: rgb(255,255,100);
+ font-family:monospace;
+ background-color: #404040a0;
+}
+
+#svg_ui_slider {
+ width:90%;
+}
+
+.svg_invalidated {
+ fill: white;
+ font-family:monospace;
+}
+
+.svg_quadtree {
+ fill: none;
+ stroke-width: 1;
+ stroke: orange;
+}
+
+.svg_changed_prim {
+ stroke: red;
+ stroke-width: 2.0;
+}
+