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