1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 /// Command line tool to convert logged tile cache files into a visualization.
6 ///
7 /// Steps to use this:
8 /// 1. enable webrender; enable gfx.webrender.debug.tile-cache-logging
9 /// 2. take a capture using ctrl-shift-3
10 /// if all is well, there will be a .../wr-capture/tilecache folder with *.ron files
11 /// 3. run tileview with that folder as the first parameter and some empty output folder as the
12 /// 2nd:
13 /// cargo run --release -- /foo/bar/wr-capture/tilecache /tmp/tilecache
14 /// 4. open /tmp/tilecache/index.html
15 ///
16 /// Note: accurate interning info requires that the circular buffer doesn't wrap around.
17 /// So for best results, use this workflow:
18 /// a. start up blank browser; in about:config enable logging; close browser
19 /// b. start new browser, quickly load the repro
20 /// c. capture.
21 ///
22 /// If that's tricky, you can also just throw more memory at it: in render_backend.rs,
23 /// increase the buffer size here: 'TileCacheLogger::new(500usize)'
24 ///
25 /// Note: some features don't work when opening index.html directly due to cross-scripting
26 /// protections. Instead use a HTTP server:
27 /// python -m SimpleHTTPServer 8000
28
29
30 use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset};
31 use webrender::{TileSerializer, TileCacheInstanceSerializer, TileCacheLoggerUpdateLists};
32 use webrender::{PrimitiveCompareResultDetail, CompareHelperResult, ItemUid};
33 use serde::Deserialize;
34 use std::fs::File;
35 use std::io::prelude::*;
36 use std::path::Path;
37 use std::ffi::OsString;
38 use std::collections::HashMap;
39 use webrender::enumerate_interners;
40 use webrender::api::ColorF;
41 use euclid::{Rect, Transform3D};
42 use webrender_api::units::{PicturePoint, PictureSize, PicturePixel, WorldPixel};
43
44 static RES_JAVASCRIPT: &'static str = include_str!("tilecache.js");
45 static RES_BASE_CSS: &'static str = include_str!("tilecache_base.css");
46
47 #[derive(Deserialize)]
48 pub struct Slice {
49 pub transform: Transform3D<f32, PicturePixel, WorldPixel>,
50 pub tile_cache: TileCacheInstanceSerializer
51 }
52
53 // invalidation reason CSS colors
54 static CSS_BACKGROUND_COLOR: &str = "fill:#10c070;fill-opacity:0.1;";
55 static CSS_SURFACE_OPACITY_CHANNEL: &str = "fill:#c040c0;fill-opacity:0.1;";
56 static CSS_NO_TEXTURE: &str = "fill:#c04040;fill-opacity:0.1;";
57 static CSS_NO_SURFACE: &str = "fill:#40c040;fill-opacity:0.1;";
58 static CSS_PRIM_COUNT: &str = "fill:#40f0f0;fill-opacity:0.1;";
59 static CSS_CONTENT: &str = "fill:#f04040;fill-opacity:0.1;";
60 static CSS_COMPOSITOR_KIND_CHANGED: &str = "fill:#f0c070;fill-opacity:0.1;";
61 static CSS_VALID_RECT_CHANGED: &str = "fill:#ff00ff;fill-opacity:0.1;";
62 static CSS_SCALE_CHANGED: &str = "fill:#ff80ff;fill-opacity:0.1;";
63
64 // parameters to tweak the SVG generation
65 struct SvgSettings {
66 pub scale: f32,
67 pub x: f32,
68 pub y: f32,
69 }
70
tile_node_to_svg(node: &TileNode, transform: &Transform3D<f32, PicturePixel, WorldPixel>, svg_settings: &SvgSettings) -> String71 fn tile_node_to_svg(node: &TileNode,
72 transform: &Transform3D<f32, PicturePixel, WorldPixel>,
73 svg_settings: &SvgSettings) -> String
74 {
75 match &node.kind {
76 TileNodeKind::Leaf { .. } => {
77 let rect_world = transform.outer_transformed_rect(&node.rect.to_rect()).unwrap();
78 format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" />\n",
79 rect_world.origin.x * svg_settings.scale + svg_settings.x,
80 rect_world.origin.y * svg_settings.scale + svg_settings.y,
81 rect_world.size.width * svg_settings.scale,
82 rect_world.size.height * svg_settings.scale)
83 },
84 TileNodeKind::Node { children } => {
85 children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform, svg_settings) )
86 }
87 }
88 }
89
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) -> String90 fn tile_to_svg(key: TileOffset,
91 tile: &TileSerializer,
92 slice: &Slice,
93 prev_tile: Option<&TileSerializer>,
94 itemuid_to_string: &HashMap<ItemUid, String>,
95 tile_stroke: &str,
96 prim_class: &str,
97 invalidation_report: &mut String,
98 svg_width: &mut i32, svg_height: &mut i32,
99 svg_settings: &SvgSettings) -> String
100 {
101 let mut svg = format!("\n<!-- tile key {},{} ; -->\n", key.x, key.y);
102
103
104 let tile_fill =
105 match tile.invalidation_reason {
106 Some(InvalidationReason::BackgroundColor { .. }) => CSS_BACKGROUND_COLOR.to_string(),
107 Some(InvalidationReason::SurfaceOpacityChanged { .. }) => CSS_SURFACE_OPACITY_CHANNEL.to_string(),
108 Some(InvalidationReason::NoTexture) => CSS_NO_TEXTURE.to_string(),
109 Some(InvalidationReason::NoSurface) => CSS_NO_SURFACE.to_string(),
110 Some(InvalidationReason::PrimCount { .. }) => CSS_PRIM_COUNT.to_string(),
111 Some(InvalidationReason::CompositorKindChanged) => CSS_COMPOSITOR_KIND_CHANGED.to_string(),
112 Some(InvalidationReason::Content { .. } ) => CSS_CONTENT.to_string(),
113 Some(InvalidationReason::ValidRectChanged) => CSS_VALID_RECT_CHANGED.to_string(),
114 Some(InvalidationReason::ScaleChanged) => CSS_SCALE_CHANGED.to_string(),
115 None => {
116 let mut background = tile.background_color;
117 if background.is_none() {
118 background = slice.tile_cache.background_color
119 }
120 match background {
121 Some(color) => {
122 let rgb = ( (color.r * 255.0) as u8,
123 (color.g * 255.0) as u8,
124 (color.b * 255.0) as u8 );
125 format!("fill:rgb({},{},{});fill-opacity:0.3;", rgb.0, rgb.1, rgb.2)
126 }
127 None => "fill:none;".to_string()
128 }
129 }
130 };
131
132 //let tile_style = format!("{}{}", tile_fill, tile_stroke);
133 let tile_style = format!("{}stroke:none;", tile_fill);
134
135 let title = match tile.invalidation_reason {
136 Some(_) => format!("<title>slice {} tile ({},{}) - {:?}</title>",
137 slice.tile_cache.slice, key.x, key.y,
138 tile.invalidation_reason),
139 None => String::new()
140 };
141
142 if let Some(reason) = &tile.invalidation_reason {
143 invalidation_report.push_str(
144 &format!("<div class=\"subheader\">slice {} key ({},{})</div><div class=\"data\">",
145 slice.tile_cache.slice,
146 key.x, key.y));
147
148 // go through most reasons individually so we can print something nicer than
149 // the default debug formatting of old and new:
150 match reason {
151 InvalidationReason::BackgroundColor { old, new } => {
152 fn to_str(c: &Option<ColorF>) -> String {
153 if let Some(c) = c {
154 format!("({},{},{},{})", c.r, c.g, c.b, c.a)
155 } else {
156 "none".to_string()
157 }
158 }
159
160 invalidation_report.push_str(
161 &format!("<b>BackGroundColor</b> changed from {} to {}",
162 to_str(old), to_str(new)));
163 },
164 InvalidationReason::SurfaceOpacityChanged { became_opaque } => {
165 invalidation_report.push_str(
166 &format!("<b>SurfaceOpacityChanged</b> changed from {} to {}",
167 !became_opaque, became_opaque));
168 },
169 InvalidationReason::PrimCount { old, new } => {
170 // diff the lists to find removed and added ItemUids,
171 // and convert them to strings to pretty-print what changed:
172 let old = old.as_ref().unwrap();
173 let new = new.as_ref().unwrap();
174 let removed = old.iter()
175 .filter(|i| !new.contains(i))
176 .fold(String::new(),
177 |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..."
178 + &itemuid_to_string.get(i).unwrap_or(&String::new())
179 + "</li>\n");
180 let added = new.iter()
181 .filter(|i| !old.contains(i))
182 .fold(String::new(),
183 |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..."
184 + &itemuid_to_string.get(i).unwrap_or(&String::new())
185 + "</li>\n");
186 invalidation_report.push_str(
187 &format!("<b>PrimCount</b> changed from {} to {}:<br/>\
188 removed:<ul>{}</ul>
189 added:<ul>{}</ul>",
190 old.len(), new.len(),
191 removed, added));
192 },
193 InvalidationReason::Content { prim_compare_result, prim_compare_result_detail } => {
194 let _ = prim_compare_result;
195 match prim_compare_result_detail {
196 Some(PrimitiveCompareResultDetail::Descriptor { old, new }) => {
197 if old.prim_uid == new.prim_uid {
198 // if the prim uid hasn't changed then try to print something useful
199 invalidation_report.push_str(
200 &format!("<b>Content: Descriptor</b> changed for uid {}<br/>",
201 old.prim_uid.get_uid()));
202 let mut changes = String::new();
203 if old.prim_clip_box != new.prim_clip_box {
204 changes += &format!("<li><b>prim_clip_rect</b> changed from {},{} -> {},{}",
205 old.prim_clip_box.min.x,
206 old.prim_clip_box.min.y,
207 old.prim_clip_box.max.x,
208 old.prim_clip_box.max.y);
209 changes += &format!(" to {},{} -> {},{}</li>",
210 new.prim_clip_box.min.x,
211 new.prim_clip_box.min.y,
212 new.prim_clip_box.max.x,
213 new.prim_clip_box.max.y);
214 }
215 invalidation_report.push_str(
216 &format!("<ul>{}<li>Item: {}</li></ul>",
217 changes,
218 &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new())));
219 } else {
220 // .. if prim UIDs have changed, just dump both items and descriptors.
221 invalidation_report.push_str(
222 &format!("<b>Content: Descriptor</b> changed; old uid {}, new uid {}:<br/>",
223 old.prim_uid.get_uid(),
224 new.prim_uid.get_uid()));
225 invalidation_report.push_str(
226 &format!("old:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>",
227 old,
228 &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new())));
229 invalidation_report.push_str(
230 &format!("new:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>",
231 new,
232 &itemuid_to_string.get(&new.prim_uid).unwrap_or(&String::new())));
233 }
234 },
235 Some(PrimitiveCompareResultDetail::Clip { detail }) => {
236 match detail {
237 CompareHelperResult::Count { prev_count, curr_count } => {
238 invalidation_report.push_str(
239 &format!("<b>Content: Clip</b> count changed from {} to {}<br/>",
240 prev_count, curr_count ));
241 },
242 CompareHelperResult::NotEqual { prev, curr } => {
243 invalidation_report.push_str(
244 &format!("<b>Content: Clip</b> ItemUids changed from {} to {}:<br/>",
245 prev.get_uid(), curr.get_uid() ));
246 invalidation_report.push_str(
247 &format!("old:<ul><li>{}</li></ul>",
248 &itemuid_to_string.get(&prev).unwrap_or(&String::new())));
249 invalidation_report.push_str(
250 &format!("new:<ul><li>{}</li></ul>",
251 &itemuid_to_string.get(&curr).unwrap_or(&String::new())));
252 },
253 reason => {
254 invalidation_report.push_str(&format!("{:?}", reason));
255 },
256 }
257 },
258 reason => {
259 invalidation_report.push_str(&format!("{:?}", reason));
260 },
261 }
262 },
263 reason => {
264 invalidation_report.push_str(&format!("{:?}", reason));
265 },
266 }
267 invalidation_report.push_str("</div>\n");
268 }
269
270 svg += &format!(r#"<rect x="{}" y="{}" width="{}" height="{}" style="{}" ></rect>"#,
271 tile.rect.min.x * svg_settings.scale + svg_settings.x,
272 tile.rect.min.y * svg_settings.scale + svg_settings.y,
273 tile.rect.width() * svg_settings.scale,
274 tile.rect.height() * svg_settings.scale,
275 tile_style);
276
277 svg += &format!("\n\n<g class=\"svg_quadtree\">\n{}</g>\n",
278 tile_node_to_svg(&tile.root, &slice.transform, svg_settings));
279
280 let right = tile.rect.max.x as i32;
281 let bottom = tile.rect.max.y as i32;
282
283 *svg_width = if right > *svg_width { right } else { *svg_width };
284 *svg_height = if bottom > *svg_height { bottom } else { *svg_height };
285
286 svg += "\n<!-- primitives -->\n";
287
288 svg += &format!("<g id=\"{}\">\n\t", prim_class);
289
290
291 let rect_visual_id = Rect {
292 origin: tile.rect.min,
293 size: PictureSize::new(1.0, 1.0)
294 };
295 let rect_visual_id_world = slice.transform.outer_transformed_rect(&rect_visual_id).unwrap();
296 svg += &format!("\n<text class=\"svg_tile_visual_id\" x=\"{}\" y=\"{}\">{},{} ({})</text>",
297 rect_visual_id_world.origin.x * svg_settings.scale + svg_settings.x,
298 (rect_visual_id_world.origin.y + 110.0) * svg_settings.scale + svg_settings.y,
299 key.x, key.y, slice.tile_cache.slice);
300
301
302 for prim in &tile.current_descriptor.prims {
303 let rect = prim.prim_clip_box;
304
305 // the transform could also be part of the CSS, let the browser do it;
306 // might be a bit faster and also enable actual 3D transforms.
307 let rect_pixel = Rect {
308 origin: PicturePoint::new(rect.min.x, rect.min.y),
309 size: PictureSize::new(rect.max.x - rect.min.x, rect.max.y - rect.min.y),
310 };
311 let rect_world = slice.transform.outer_transformed_rect(&rect_pixel).unwrap();
312
313 let style =
314 if let Some(prev_tile) = prev_tile {
315 // when this O(n^2) gets too slow, stop brute-forcing and use a set or something
316 if prev_tile.current_descriptor.prims.iter().find(|&prim| prim.prim_clip_box == rect).is_some() {
317 ""
318 } else {
319 "class=\"svg_changed_prim\" "
320 }
321 } else {
322 "class=\"svg_changed_prim\" "
323 };
324
325 svg += &format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" {}/>",
326 rect_world.origin.x * svg_settings.scale + svg_settings.x,
327 rect_world.origin.y * svg_settings.scale + svg_settings.y,
328 rect_world.size.width * svg_settings.scale,
329 rect_world.size.height * svg_settings.scale,
330 style);
331
332 svg += "\n\t";
333 }
334
335 svg += "\n</g>\n";
336
337 // nearly invisible, all we want is the toolip really
338 let style = "style=\"fill-opacity:0.001;";
339 svg += &format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" {}{}\" >{}<\u{2f}rect>",
340 tile.rect.min.x * svg_settings.scale + svg_settings.x,
341 tile.rect.min.y * svg_settings.scale + svg_settings.y,
342 tile.rect.width() * svg_settings.scale,
343 tile.rect.height() * svg_settings.scale,
344 style,
345 tile_stroke,
346 title);
347
348 svg
349 }
350
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)351 fn slices_to_svg(slices: &[Slice], prev_slices: Option<Vec<Slice>>,
352 itemuid_to_string: &HashMap<ItemUid, String>,
353 svg_width: &mut i32, svg_height: &mut i32,
354 max_slice_index: &mut usize,
355 svg_settings: &SvgSettings) -> (String, String)
356 {
357 let svg_begin = "<?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache_base.css\" ?>\n\
358 <?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache.css\" ?>\n";
359
360 let mut svg = String::new();
361 let mut invalidation_report = "<div class=\"header\">Invalidation</div>\n".to_string();
362
363 for slice in slices {
364 let tile_cache = &slice.tile_cache;
365 *max_slice_index = if tile_cache.slice > *max_slice_index { tile_cache.slice } else { *max_slice_index };
366
367 invalidation_report.push_str(&format!("<div id=\"invalidation_slice{}\">\n", tile_cache.slice));
368
369 let prim_class = format!("tile_slice{}", tile_cache.slice);
370
371 svg += &format!("\n<g id=\"tile_slice{}_everything\">", tile_cache.slice);
372
373 //println!("slice {}", tile_cache.slice);
374 svg += &format!("\n<!-- tile_cache slice {} -->\n",
375 tile_cache.slice);
376
377 //let tile_stroke = "stroke:grey;stroke-width:1;".to_string();
378 let tile_stroke = "stroke:none;".to_string();
379
380 let mut prev_slice = None;
381 if let Some(prev) = &prev_slices {
382 for prev_search in prev {
383 if prev_search.tile_cache.slice == tile_cache.slice {
384 prev_slice = Some(prev_search);
385 break;
386 }
387 }
388 }
389
390 for (key, tile) in &tile_cache.tiles {
391 let mut prev_tile = None;
392 if let Some(prev) = prev_slice {
393 prev_tile = prev.tile_cache.tiles.get(key);
394 }
395
396 svg += &tile_to_svg(*key, &tile, &slice, prev_tile,
397 itemuid_to_string,
398 &tile_stroke, &prim_class,
399 &mut invalidation_report,
400 svg_width, svg_height, svg_settings);
401 }
402
403 svg += "\n</g>";
404
405 invalidation_report.push_str("</div>\n");
406 }
407
408 (
409 format!("{}<svg version=\"1.1\" baseProfile=\"full\" xmlns=\"http://www.w3.org/2000/svg\" \
410 width=\"{}\" height=\"{}\" >",
411 svg_begin,
412 svg_width,
413 svg_height)
414 + "\n"
415 + "<rect fill=\"black\" width=\"100%\" height=\"100%\"/>\n"
416 + &svg
417 + "\n</svg>\n",
418 invalidation_report
419 )
420 }
421
write_html(output_dir: &Path, max_slice_index: usize, svg_files: &[String], intern_files: &[String])422 fn write_html(output_dir: &Path, max_slice_index: usize, svg_files: &[String], intern_files: &[String]) {
423 let html_head = "<!DOCTYPE html>\n\
424 <html>\n\
425 <head>\n\
426 <meta charset=\"UTF-8\">\n\
427 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
428 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\
429 </head>\n"
430 .to_string();
431
432 let html_body = "<body bgcolor=\"#000000\" onload=\"load()\">\n"
433 .to_string();
434
435
436 let mut script = "\n<script>\n".to_string();
437
438 script = format!("{}var svg_files = [\n", script);
439 for svg_file in svg_files {
440 script = format!("{} \"{}\",\n", script, svg_file);
441 }
442 script = format!("{}];\n\n", script);
443
444 script = format!("{}var intern_files = [\n", script);
445 for intern_file in intern_files {
446 script = format!("{} \"{}\",\n", script, intern_file);
447 }
448 script = format!("{}];\n</script>\n\n", script);
449
450 script = format!("{}<script src=\"tilecache.js\" type=\"text/javascript\"></script>\n\n", script);
451
452
453 let html_end = "</body>\n\
454 </html>\n"
455 .to_string();
456
457 let mut html_slices_form =
458 "\n<form id=\"slicecontrols\">\n\
459 Slice\n".to_string();
460
461 for ix in 0..max_slice_index + 1 {
462 html_slices_form +=
463 &format!(
464 "<input id=\"slice_toggle{}\" \
465 type=\"checkbox\" \
466 onchange=\"update_slice_visibility({})\" \
467 checked=\"checked\" />\n\
468 <label for=\"slice_toggle{}\">{}</label>\n",
469 ix,
470 max_slice_index + 1,
471 ix,
472 ix );
473 }
474
475 html_slices_form += "<form>\n";
476
477 let html_body = format!(
478 "{}\n\
479 <div class=\"split left\">\n\
480 <div>\n\
481 <object id=\"svg_container0\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
482 <object id=\"svg_container1\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
483 </div>\n\
484 </div>\n\
485 \n\
486 <div class=\"split right\">\n\
487 <iframe width=\"100%\" id=\"intern\" src=\"{}\"></iframe>\n\
488 </div>\n\
489 \n\
490 <div id=\"svg_ui_overlay\">\n\
491 <div id=\"text_frame_counter\">{}</div>\n\
492 <div id=\"text_spacebar\">Spacebar to Play</div>\n\
493 <div>Use Left/Right to Step</div>\n\
494 <input id=\"frame_slider\" type=\"range\" min=\"0\" max=\"{}\" value=\"0\" class=\"svg_ui_slider\" />
495 {}
496 </div>",
497 html_body,
498 svg_files[0],
499 svg_files[0],
500 intern_files[0],
501 svg_files[0],
502 svg_files.len(),
503 html_slices_form );
504
505 let html = format!("{}{}{}{}", html_head, html_body, script, html_end);
506
507 let output_file = output_dir.join("index.html");
508 let mut html_output = File::create(output_file).unwrap();
509 html_output.write_all(html.as_bytes()).unwrap();
510 }
511
write_css(output_dir: &Path, max_slice_index: usize, svg_settings: &SvgSettings)512 fn write_css(output_dir: &Path, max_slice_index: usize, svg_settings: &SvgSettings) {
513 let mut css = String::new();
514
515 for ix in 0..max_slice_index + 1 {
516 let color = ( ix % 7 ) + 1;
517 let rgb = format!("rgb({},{},{})",
518 if color & 2 != 0 { 205 } else { 90 },
519 if color & 4 != 0 { 205 } else { 90 },
520 if color & 1 != 0 { 225 } else { 90 });
521
522 let prim_class = format!("tile_slice{}", ix);
523
524 css += &format!("#{} {{\n\
525 fill: {};\n\
526 fill-opacity: 0.03;\n\
527 stroke-width: {};\n\
528 stroke: {};\n\
529 }}\n\n",
530 prim_class,
531 //rgb,
532 "none",
533 0.8 * svg_settings.scale,
534 rgb);
535 }
536
537 css += &format!(".svg_tile_visual_id {{\n\
538 font: {}px sans-serif;\n\
539 fill: rgb(50,50,50);\n\
540 }}\n\n",
541 150.0 * svg_settings.scale);
542
543 let output_file = output_dir.join("tilecache.css");
544 let mut css_output = File::create(output_file).unwrap();
545 css_output.write_all(css.as_bytes()).unwrap();
546 }
547
548 macro_rules! updatelist_to_html_macro {
549 ( $( $name:ident: $ty:ty, )+ ) => {
550 fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists,
551 invalidation_report: String) -> String
552 {
553 let mut html = "\
554 <!DOCTYPE html>\n\
555 <html> <head> <meta charset=\"UTF-8\">\n\
556 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
557 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\
558 </head> <body>\n\
559 <div class=\"datasheet\">\n".to_string();
560
561 html += &invalidation_report;
562
563 html += "<div class=\"header\">Interning</div>\n";
564 $(
565 html += &format!("<div class=\"subheader\">{}</div>\n<div class=\"intern data\">\n",
566 stringify!($name));
567 for list in &update_lists.$name.1 {
568 for insertion in &list.insertions {
569 html += &format!("<div class=\"insert\"><b>{}</b> {}</div>\n",
570 insertion.uid.get_uid(),
571 format!("({:?})", insertion.value));
572 }
573
574 for removal in &list.removals {
575 html += &format!("<div class=\"remove\"><b>{}</b></div>\n",
576 removal.uid.get_uid());
577 }
578 }
579 html += "</div><br/>\n";
580 )+
581 html += "</div> </body> </html>\n";
582 html
583 }
584 }
585 }
586 enumerate_interners!(updatelist_to_html_macro);
587
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) -> String588 fn write_tile_cache_visualizer_svg(entry: &std::fs::DirEntry, output_dir: &Path,
589 slices: &[Slice], prev_slices: Option<Vec<Slice>>,
590 itemuid_to_string: &HashMap<ItemUid, String>,
591 svg_width: &mut i32, svg_height: &mut i32,
592 max_slice_index: &mut usize,
593 svg_files: &mut Vec::<String>,
594 svg_settings: &SvgSettings) -> String
595 {
596 let (svg, invalidation_report) = slices_to_svg(&slices, prev_slices,
597 itemuid_to_string,
598 svg_width, svg_height,
599 max_slice_index,
600 svg_settings);
601
602 let mut output_filename = OsString::from(entry.path().file_name().unwrap());
603 output_filename.push(".svg");
604 svg_files.push(output_filename.to_string_lossy().to_string());
605
606 output_filename = output_dir.join(output_filename).into_os_string();
607 let mut svg_output = File::create(output_filename).unwrap();
608 svg_output.write_all(svg.as_bytes()).unwrap();
609
610 invalidation_report
611 }
612
write_update_list_html(entry: &std::fs::DirEntry, output_dir: &Path, update_lists: &TileCacheLoggerUpdateLists, html_files: &mut Vec::<String>, invalidation_report: String)613 fn write_update_list_html(entry: &std::fs::DirEntry, output_dir: &Path,
614 update_lists: &TileCacheLoggerUpdateLists,
615 html_files: &mut Vec::<String>,
616 invalidation_report: String)
617 {
618 let html = updatelist_to_html(update_lists, invalidation_report);
619
620 let mut output_filename = OsString::from(entry.path().file_name().unwrap());
621 output_filename.push(".html");
622 html_files.push(output_filename.to_string_lossy().to_string());
623
624 output_filename = output_dir.join(output_filename).into_os_string();
625 let mut html_output = File::create(output_filename).unwrap();
626 html_output.write_all(html.as_bytes()).unwrap();
627 }
628
main()629 fn main() {
630 let args: Vec<String> = std::env::args().collect();
631
632 if args.len() < 3 {
633 println!("Usage: tileview input_dir output_dir [scale [x y]]");
634 println!(" where input_dir is a tile_cache folder inside a wr-capture.");
635 println!(" Scale is an optional scaling factor to compensate for high-DPI.");
636 println!(" X, Y is an optional offset to shift the entire SVG by.");
637 println!("\nexample: cargo run c:/Users/me/AppData/Local/wr-capture.6/tile_cache/ c:/temp/tilecache/");
638 std::process::exit(1);
639 }
640
641 let input_dir = Path::new(&args[1]);
642 let output_dir = Path::new(&args[2]);
643 std::fs::create_dir_all(output_dir).unwrap();
644
645 let scale = if args.len() >= 4 { args[3].parse::<f32>().unwrap() } else { 1.0 };
646 let x = if args.len() >= 6 { args[4].parse::<f32>().unwrap() } else { 0.0 }; // >= 6, requires X and Y
647 let y = if args.len() >= 6 { args[5].parse::<f32>().unwrap() } else { 0.0 };
648 let svg_settings = SvgSettings { scale, x, y };
649
650 let mut svg_width = 100i32;
651 let mut svg_height = 100i32;
652 let mut max_slice_index = 0;
653
654 let mut entries: Vec<_> = std::fs::read_dir(input_dir).unwrap()
655 .filter_map(|r| r.ok())
656 .collect();
657 // auto-fix a missing 'tile_cache' postfix on the input path -- easy to do when copy-pasting a
658 // path to a wr-capture; there should at least be a frame00000.ron...
659 let frame00000 = entries.iter().find(|&entry| entry.path().ends_with("frame00000.ron"));
660 // ... and if not, try again with 'tile_cache' appended to the input folder
661 if frame00000.is_none() {
662 let new_path = input_dir.join("tile_cache");
663 entries = std::fs::read_dir(new_path).unwrap()
664 .filter_map(|r| r.ok())
665 .collect();
666 }
667 entries.sort_by_key(|dir| dir.path());
668
669 let mut svg_files: Vec::<String> = Vec::new();
670 let mut intern_files: Vec::<String> = Vec::new();
671 let mut prev_slices = None;
672
673 let mut itemuid_to_string = HashMap::default();
674
675 for entry in &entries {
676 if entry.path().is_dir() {
677 continue;
678 }
679 print!("processing {:?}\t", entry.path());
680 let file_data = std::fs::read_to_string(entry.path()).unwrap();
681 let chunks: Vec<_> = file_data.split("// @@@ chunk @@@").collect();
682 let slices: Vec<Slice> = match ron::de::from_str(&chunks[0]) {
683 Ok(data) => { data }
684 Err(e) => {
685 println!("ERROR: failed to deserialize slicesg {:?}\n{:?}", entry.path(), e);
686 prev_slices = None;
687 continue;
688 }
689 };
690 let mut update_lists = TileCacheLoggerUpdateLists::new();
691 update_lists.from_ron(&chunks[1]);
692 update_lists.insert_in_lookup(&mut itemuid_to_string);
693
694 let invalidation_report = write_tile_cache_visualizer_svg(
695 &entry, &output_dir,
696 &slices, prev_slices,
697 &itemuid_to_string,
698 &mut svg_width, &mut svg_height,
699 &mut max_slice_index,
700 &mut svg_files,
701 &svg_settings);
702
703 write_update_list_html(&entry, &output_dir, &update_lists,
704 &mut intern_files, invalidation_report);
705
706 print!("\r");
707 prev_slices = Some(slices);
708 }
709
710 write_html(output_dir, max_slice_index, &svg_files, &intern_files);
711 write_css(output_dir, max_slice_index, &svg_settings);
712
713 std::fs::write(output_dir.join("tilecache.js"), RES_JAVASCRIPT).unwrap();
714 std::fs::write(output_dir.join("tilecache_base.css"), RES_BASE_CSS).unwrap();
715
716 println!("\n");
717 }
718