this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

:memo: Crate and image rendering sections done

+462 -121
+1
.gitignore
··· 17 17 out.png 18 18 stems_data/ 19 19 street 20 + timings.log
+6
Justfile
··· 29 29 30 30 paper: 31 31 just 32 + # just analyze_times disabled because it needs manual adjustements in the render loop pipeline diagram 32 33 ./shapemaker examples dna-analysis-machine --resolution 1920 paper/dna-analysis-machine.png 33 34 ./shapemaker examples shapeshed --resolution 1920 paper/shapeshed.svg 34 35 ./shapemaker examples colors-shed --resolution 1920 paper/colorshed.svg ··· 40 41 #!/usr/bin/env bash 41 42 cd examples/gallery 42 43 ./fill.rb 44 + 45 + analyze_times: 46 + just 47 + rm timings.log 48 + python script/debug-performance.py
+22
paper/bibliography.yaml
··· 117 117 url: 118 118 value: https://www.xnview.com/fr/ 119 119 date: 2025-03-23 120 + 121 + rustcrates: 122 + title: The Rust Programming Language § 7.1 Packages and Crates 123 + type: book 124 + author: 125 + - Steve Klabnik 126 + - Carol Nichols 127 + - Chris Krycho 128 + url: 129 + value: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html 130 + date: 2025-03-23 131 + 132 + rusttraits: 133 + title: "The Rust Programming Language § 10.2 Traits: Defining Shared Behavior" 134 + type: book 135 + author: 136 + - Steve Klabnik 137 + - Carol Nichols 138 + - Chris Krycho 139 + url: 140 + value: https://doc.rust-lang.org/book/ch10-02-traits.html 141 + date: 2025-03-23
paper/main.pdf

This is a binary file and will not be displayed.

+222 -56
paper/main.typ
··· 13 13 caption: caption, 14 14 ) 15 15 16 - #let diagram(caption: "", content) = figure( 16 + #let diagram(caption: "", size: 100%, content) = figure( 17 17 caption: caption, 18 18 kind: image, 19 - content, 19 + scale(size, content, reflow: true), 20 + ) 21 + 22 + #let codesnippet(caption: "", content, lang: "rust") = block( 23 + inset: 2em, 24 + fill: luma(230), 25 + radius: 4pt, 26 + width: 100%, 27 + breakable: false, 28 + raw( 29 + lang: lang, 30 + content, 31 + ), 20 32 ) 21 33 22 34 #show link: underline ··· 213 225 214 226 Bien évidemment, surtout s'il s'agit d'une vidéo synchronisée à sa bande son, il ne suffit pas de générer une frame aléatoire chaque seconde. Il faut pouvoir _réagit à des moments et rythmes clés du morceau_. 215 227 228 + 216 229 = Une _crate_ Rust avec un API sympathique 217 230 231 + Pour implémenter cette génération, il faut donner donc un moyen à l'artiste de décrire sa procédure de génération. 232 + 233 + Ainsi, Shapemaker est une bibliothèque réutilisable, ou _crate_ dans l'écosystème Rust @rustcrates. 234 + 235 + La création d'un procédé de génération est conceptualisée par un canvas, composé de une ou plusieurs couches ou _layers_ d'objets. Ces objets sont _colorés_ (possèdent une information sur la manière dont il faut les remplir: bleu solide, hachures cyan, etc.), et peuvent également subir des filtres et transformations #footnote[Avec un peu de recul, le terme d'objet texturé est plus approprié, mais le code n'a pas encore changé]. Ils sont aussi _placés_ dans l'espace du canvas: le canvas possède une information de _région_, un intervalle 2D de points valables. Les objets se placent dans cette région, en stockant dans leur structure les coordonnées de _points_ marquant leur positionnement dans l'espace (coins pour un #raw(lang: "rust", "Object::Rectangle")) 236 + 218 237 #diagram( 219 - caption: [Pipeline], 220 - scale(80%, reflow: true)[ 221 - ```dot 222 - digraph G { 223 - rankdir="LR"; 224 - compound=true; 225 - node[shape="record"]; 238 + caption: [Modèle objet du Canvas], 239 + size: 90%, 240 + ```dot 241 + digraph { 242 + // rankdir="LR"; 243 + node [shape="record"]; 226 244 227 - subgraph cluster_0 { 228 - label = "Render loop" 229 - style = "filled" 230 - color = "#f0f0f0" 245 + Canvas -> Layer [label="1+"] 246 + region2 [label="Region"] 247 + Layer -> region2 248 + Canvas -> Region [label=".world_region"] 249 + point2 [label="Point"] 250 + Region -> point2 [label="RegionIterator"] 251 + Layer -> ColoredObject [label="0+"] 252 + Object -> "Object::Rectangle,\nObject::Circle,\n…" -> Point 253 + ColoredObject -> Object 254 + ColoredObject -> Fill 255 + ColoredObject -> Transform 256 + ColoredObject -> Filter 257 + Fill -> "Fill::Solid,\nFill::Hatches,\n…" -> Color 258 + Transform -> "Transform::Rotate,\nTransform::Translate,\n…" 259 + Filter -> "Filter::Blur,\nFilter::Glow,\n…" 260 + } 261 + ```, 262 + ) 263 + 264 + Ce modèle mental permet de travailler plus efficacement car il est bien plus proche de la manière dont on a tendance à penser l'art visuel: sur Illustrator par exemple, ce sont des objets, organisés en plusieurs couches, qui possèdent des attributs dictant leur remplissage. 265 + 266 + Les concepts de transformations et de filtres sont également très proche de ce qu'on peut retrouver dans des logiciels de création d'images raster, comme Photoshop. 267 + 268 + 269 + 270 + #grid( 271 + columns: (1fr, 1fr), 272 + gutter: 1em, 273 + [ 274 + La bibliothèque fournit une grande quantité de fonctions utiles pour redimensionner des régions, en prendre le milieu. 275 + 276 + La partie purement géométrique de la bibliothèque, définissans `Point`, `Region` et leurs opérations utiles associées (itérer les points d'une région, calculer le milieu d'une région, etc.), sont regroupées dans `shapemaker::geometry`. 277 + 278 + Les définitions des objets et de tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`) sont regroupées dans `shapemaker::objects`. 279 + 280 + Il y a également `shapemaker::random` qui regroupe des fonctions de génération aléatoire, permettant d'introduire facilement et de manière plus ou moins granulaire, une part d'aléatoire dans le processus de génération: `Region.random_point()`, `Color::random()`, etc. 281 + 282 + Enfin, `shapemaker::rendering` implémente le rendu d'un canvas et de tout ce qu'il contient en SVG 283 + ], 284 + diagram( 285 + caption: [Dépendances entre les modules de la bibliothèque], 286 + size: 60%, 287 + raw( 288 + lang: "mermaid", 289 + cut-between( 290 + it => it == "```mermaid", 291 + it => it == "```", 292 + read("../src/README.md"), 293 + ), 294 + ), 295 + ), 296 + ) 231 297 232 - 233 - // Create a more circular arrangement using rank constraints 234 - { rank=same; "next frame"; rasterize; } 235 - { rank=same; hooks; "render to SVG"; } 236 - { rank=same; canvas; } 237 - 238 - // Set specific weights to encourage circular layout 239 - "next frame" -> hooks [weight=2]; 240 - hooks -> canvas [weight=2]; 241 - canvas -> "render to SVG" [weight=2]; 242 - "render to SVG" -> rasterize [weight=2]; 243 - rasterize -> "next frame" [weight=2]; 244 - 245 - // Add some balancing invisible edges 246 - "next frame" -> canvas [style=invis, weight=0.5]; 247 - hooks -> "render to SVG" [style=invis, weight=0.5]; 248 - canvas -> rasterize [style=invis, weight=0.5]; 249 - "render to SVG" -> "next frame" [style=invis, weight=0.5]; 250 - rasterize -> hooks [style=invis, weight=0.5]; 251 - } 252 298 253 - syncdata[label="sync data"]; 254 299 255 - audioin[label="stems .wav + BPM"] 256 - midi[label="MIDI export"] 257 - flp[label=".flp project file"] 300 + = Rendu en images 258 301 259 - midi -> syncdata 260 - audioin -> syncdata 261 - flp -> syncdata 302 + Maintenant que l'on a cette structure, il est bien évidemment essentiel de pouvoir la rendre en un fichier image exploitable, en PNG par exemple. 262 303 263 - syncdata -> "next frame" 304 + L'idée est d'exploiter le standard SVG et tout l'écosystème existant autour pour éviter d'avoir à ré-implémenter un moteur de rasterisation à la main: SVG possède déjà énormément de fonctionnalités, et faire ainsi nous permet de fournir un "escape hatch" et de fournir à Shapemaker des fragments de code SVG pour des cas spécifiques que la bibliothèque ne couvrirait pas, à travers `Object::RawSVG`, qui prend en argument un arbre SVG brut. 264 305 265 - usercode[label="user code"]; 266 - usercode -> hooks 306 + Ce processus de rendu est réalisé via l'implémentation d'un trait, une sorte d'équivalent des interfaces dans les langages orientés objet @rusttraits: 267 307 268 - "rasterize" -> "video encoder" 269 - syncdata -> audio -> "video encoder" 270 - } 271 - ``` 272 - ] 308 + #codesnippet( 309 + lang: "rust", 310 + cut-around( 311 + it => it.trim().starts-with("pub trait SVGRenderable"), 312 + it => it == "}", 313 + read("../src/rendering/renderable.rs"), 314 + ), 273 315 ) 274 316 317 + Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`: 318 + 319 + / Canvas: rendu de toutes ses `Layer`, en prenant garde à les ordonner correctement pour que les premières couches soit déssinées par dessus les dernières 320 + / Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>") 321 + / ColoredObject: rendu de l'`Object` qu'il contient, en appliquant les transformations et filtres 322 + / Object: dépend de la variante: `Object::Rectangle` est rendu comme un #raw(lang: "svg", "<rect>"), `Object::Circle` est rendu comme un #raw(lang: "svg", "<circle>"), etc. 323 + / Fill: dépend de la variante: simple attribut SVG `fill` pour `Fill::Solid`, utilisation de #raw(lang: "svg", "<pattern>") pour `Fill::Hatches`, etc. 324 + / Transform: attribut SVG `transform` 325 + / Filter: définition d'un #raw(lang: "svg", "<filter>") avec les attributs correspondants 326 + / Color: utilise le `ColorMapping` donné pour réaliser sa variante en une valeur de couleur SVG (notation hexadécimale) 327 + 275 328 #diagram( 276 - caption: [Organisation des sous-modules], 277 - raw( 278 - lang: "mermaid", 279 - cut-between( 280 - it => it == "```mermaid", 281 - it => it == "```", 282 - read("../src/README.md"), 283 - ), 329 + caption: [Objets rendables en SVG], 330 + size: 60%, 331 + ```dot 332 + digraph { 333 + // rankdir="LR"; 334 + node [shape="record", style="filled", fillcolor="#e0e000"]; 335 + 336 + Canvas -> Layer 337 + region2 [label="Region", style="solid"] 338 + Layer -> region2 339 + Canvas -> Region 340 + point2 [label="Point", style="solid"] 341 + Region -> point2 342 + Layer -> ColoredObject 343 + Point[style="solid"] 344 + Object -> "Object::Rectangle,\nObject::Circle,\n…" -> Point 345 + ColoredObject -> Object 346 + ColoredObject -> Fill 347 + ColoredObject -> Transform 348 + ColoredObject -> Filter 349 + Fill -> "Fill::Solid,\nFill::Hatches,\n…" -> Color 350 + Transform -> "Transform::Rotate,\nTransform::Translate,\n…" 351 + Filter -> "Filter::Blur,\nFilter::Glow,\n…" 352 + } 353 + ```, 354 + ) 355 + Les arguments `cell_size` et `object_sizes` permettent de réaliser en valeur concrètes (pixels) les valeurs de taille abstraites: la distance unitaire entre deux points est définie par `cell_size`, et les tailles des objets, qui, par choix, n'est pas contrôlable finement, sont définies par `object_sizes`. 356 + 357 + #codesnippet( 358 + lang: "rust", 359 + cut-around( 360 + it => it.trim().starts-with("pub struct ObjectSizes"), 361 + it => it == "}", 362 + read("../src/graphics/objects.rs"), 284 363 ), 285 364 ) 286 365 287 366 288 367 = Render loop et hooks 368 + 369 + #diagram( 370 + caption: [Pipeline], 371 + size: 60%, 372 + ```dot 373 + digraph G { 374 + rankdir="LR"; 375 + compound=true; 376 + node[shape="record"]; 377 + 378 + subgraph cluster_0 { 379 + label = "Render loop" 380 + style = "filled" 381 + color = "#f0f0f0" 382 + 383 + // Set specific weights to encourage circular layout 384 + "next frame" -> hooks [weight=2, label="Trigger"]; 385 + hooks -> canvas [weight=2, label="Modify"]; 386 + canvas -> frame [weight=2, label="Render"]; 387 + frame -> "next frame" [weight=2]; 388 + } 389 + 390 + syncdata[label="sync data"]; 391 + 392 + audioin[label="stems .wav + BPM"] 393 + midi[label="MIDI export"] 394 + flp[label=".flp project file"] 395 + 396 + midi -> syncdata 397 + audioin -> syncdata 398 + flp -> syncdata 399 + 400 + syncdata -> "next frame" 401 + 402 + usercode[label="user code"]; 403 + usercode -> hooks [label="Specifies"] 404 + 405 + frame -> video 406 + syncdata -> audio -> video 407 + } 408 + ```, 409 + ) 410 + 411 + 289 412 290 413 = Sources de synchronisation 291 414 ··· 450 573 ) 451 574 452 575 = Performance 576 + 577 + #grid( 578 + columns: (auto, auto), 579 + diagram( 580 + caption: [Détail de la boucle de rendu], 581 + scale(90%, reflow: true)[ 582 + ```dot 583 + digraph G { 584 + compound=true; 585 + node[shape="record"]; 586 + 587 + hooks -> canvas [label="Modify"]; 588 + subgraph cluster_tosvg { 589 + label = "SVG string rendering [0.2ms]" 590 + canvas -> render_to_svg [label="0.1ms"] 591 + "render_to_svg" -> stringify_svg [label="0.1ms"] 592 + } 593 + subgraph cluster_rasterize { 594 + label = "Encode frame [167ms]" 595 + stringify_svg -> "svg string" 596 + "svg string" -> "usvg tree" [label="48ms"] 597 + "usvg tree" -> pixmap [label="11ms"] 598 + pixmap -> "hwc frame" [label="108ms"] 599 + } 600 + } 601 + ``` 602 + ], 603 + ), 604 + [ 605 + // #figure(caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes", csvtable(read("../results.csv"), columns: (auto, auto, auto), inset: 10pt)) 606 + #figure( 607 + caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes", 608 + table( 609 + columns: 3, 610 + inset: 0.75em, 611 + [*Tâche*], [*Durée [ms]*], [*\#*], 612 + ..csv("../results.csv").slice(1).flatten() 613 + ), 614 + ) 615 + 616 + Comme on peut le remarquer, il y a un gain de performance assez conséquent de possible si l'on parvient à utiliser usvg, non seulement pour la rastérisation, mais également pour la construction de l'arbre SVG: sur une boule de rendu de 167 ms, *on passe 29% du temps à parser un arbre SVG sérialisé, alors que l'on vient de construire cette arbre*. 617 + ], 618 + ) 453 619 454 620 = Conclusion 455 621
+11
results.csv
··· 1 + Tâche,Durée [ms],# 2 + render_to_svg,0.127,150 3 + stringify_svg,0.135,150 4 + create_pixmap,0.251,150 5 + setup_encoder,5.160,1 6 + usvg_tree_to_pixmap,10.823,150 7 + svg_to_usvg_tree,47.558,150 8 + pixmap_to_hwc_frame,107.686,150 9 + load_midi_notes,119.540,1 10 + load_fonts,148.610,1 11 + encode_frame,167.082,150
+95
script/debug-performance.py
··· 1 + from subprocess import run 2 + from pathlib import Path 3 + from rich.table import Table 4 + from rich.console import Console 5 + import os 6 + 7 + ignored_tasks = ["render_frames", "render", "sync_audio_with", "run", "canvas_from_cli"] 8 + 9 + 10 + def avg(numbers: list[float]): 11 + return sum(numbers) / len(numbers) 12 + 13 + 14 + if not Path("timings.log").exists(): 15 + result = run( 16 + ["just", "example-video", "out.mp4", "--duration 5"], 17 + capture_output=True, 18 + env=os.environ | {"RUST_LOG": "debug"}, 19 + ) 20 + 21 + Path("timings.log").write_bytes(result.stdout + result.stderr) 22 + 23 + timings = [ 24 + line.split(" took ") 25 + for line in Path("timings.log").read_text().splitlines() 26 + if " took " in line 27 + ] 28 + 29 + 30 + def parse_duration(duration_string: str) -> float: 31 + if " " in duration_string: 32 + return sum(parse_duration(part) for part in duration_string.split(" ")) 33 + try: 34 + figure = float(duration_string.strip("msµ")) 35 + except ValueError: 36 + return None 37 + 38 + if "µs" in duration_string: 39 + return figure * 1e-3 40 + if "ms" in duration_string: 41 + return figure 42 + if "s" in duration_string: 43 + return figure * 1e3 44 + else: 45 + return figure 46 + 47 + raise ValueError(f"Duration string {duration_string!r} has unsupported unit") 48 + 49 + 50 + timings = [ 51 + (label.split("] ")[1], parse_duration(timing)) 52 + for label, timing in timings 53 + if parse_duration(timing) 54 + ] 55 + 56 + per_function: dict[str, list[float]] = {function: [] for function, _ in timings} 57 + 58 + for function, timing in timings: 59 + per_function[function].append(timing) 60 + 61 + averages: list[tuple[str, float, int]] = [ 62 + (function, avg(timings), len(timings)) for function, timings in per_function.items() 63 + ] 64 + 65 + averages.sort(key=lambda item: item[1]) 66 + 67 + formatted_results = [ 68 + [function, f"{timing:.3f}", f"{count}"] for function, timing, count in averages 69 + ] 70 + 71 + 72 + def to_csv(lists: list[list[str]]): 73 + return "\r\n".join( 74 + ",".join(cell.replace(",", "_") for cell in line) for line in lists 75 + ) 76 + 77 + 78 + table = Table("task", "time [ms]", "count") 79 + for row in formatted_results: 80 + table.add_row(*row) 81 + Console().print(table) 82 + 83 + Path("results.csv").write_text( 84 + to_csv( 85 + [ 86 + ["Tâche", "Durée [ms]", "#"], 87 + *[ 88 + [task, *things] 89 + for task, *things in formatted_results 90 + if task not in ignored_tasks 91 + ], 92 + ] 93 + ), 94 + encoding="utf8", 95 + )
+2 -2
src/cli/mod.rs
··· 2 2 3 3 use crate::{Canvas, ColorMapping}; 4 4 use docopt::Docopt; 5 - use measure_time::info_time; 5 + use measure_time::debug_time; 6 6 use serde::Deserialize; 7 7 8 8 const USAGE: &str = " ··· 67 67 } 68 68 69 69 pub fn canvas_from_cli(args: &Args) -> Canvas { 70 - info_time!("canvas_from_cli"); 70 + debug_time!("canvas_from_cli"); 71 71 let mut canvas = Canvas::new(vec![]); 72 72 canvas.colormap = load_colormap(args); 73 73 set_canvas_settings_from_args(args, &mut canvas);
+5 -5
src/graphics/canvas.rs
··· 3 3 use std::{collections::HashMap, ops::Range, sync::Arc}; 4 4 5 5 use itertools::Itertools as _; 6 - use measure_time::info_time; 6 + use measure_time::debug_time; 7 7 8 8 use crate::{ 9 9 fonts::{load_fonts, FontOptions}, ··· 219 219 return Ok(()); 220 220 } 221 221 222 - info_time!("load_fonts"); 222 + debug_time!("load_fonts"); 223 223 let usvg = load_fonts(&self.font_options)?; 224 224 self.fontdb = Some(usvg.fontdb); 225 225 Ok(()) ··· 259 259 resolution: u32, 260 260 previous_frame_at: Option<&str>, 261 261 ) -> anyhow::Result<()> { 262 - info_time!("render_to_png"); 262 + debug_time!("render_to_png"); 263 263 let (width, height) = self.resolution_to_size(resolution); 264 264 if let Some(previous_frame_at) = previous_frame_at { 265 265 match self.render_to_pixmap(width, height)? { ··· 281 281 } 282 282 283 283 fn pixmap_to_png_data(pixmap: tiny_skia::Pixmap) -> anyhow::Result<Vec<u8>> { 284 - info_time!("\tpixmap_to_png_data"); 284 + debug_time!("\tpixmap_to_png_data"); 285 285 Ok(pixmap.encode_png()?) 286 286 } 287 287 288 288 fn write_png_data(data: Vec<u8>, at: &str) -> anyhow::Result<()> { 289 - info_time!("\twrite_png_data"); 289 + debug_time!("\twrite_png_data"); 290 290 std::fs::write(at, data)?; 291 291 Ok(()) 292 292 }
+2 -2
src/main.rs
··· 2 2 #[cfg(feature = "vst")] 3 3 #[cfg(feature = "mp4")] 4 4 use env_logger; 5 - use measure_time::info_time; 5 + use measure_time::debug_time; 6 6 use shapemaker::{ 7 7 cli::{canvas_from_cli, cli_args}, 8 8 *, ··· 18 18 } 19 19 20 20 pub fn run(args: cli::Args) -> Result<()> { 21 - info_time!("run"); 21 + debug_time!("run"); 22 22 let mut canvas = canvas_from_cli(&args); 23 23 24 24 if args.cmd_examples {
+7 -4
src/random/canvas.rs
··· 23 23 ) -> Layer { 24 24 let mut objects: HashMap<String, ColoredObject> = HashMap::new(); 25 25 for i in 0..count { 26 - let object = 27 - Object::random_curve_within(region, self.object_sizes.default_line_width); 26 + let object = Object::random_curve_within( 27 + region, 28 + self.object_sizes.default_line_width, 29 + ); 28 30 objects.insert( 29 31 format!("{}#{}", layer_name, i), 30 32 ColoredObject::from(( ··· 53 55 object_counts: impl SampleRange<usize>, 54 56 ) -> Layer { 55 57 let number_of_objects = rand::thread_rng().gen_range(object_counts); 56 - self.n_random_curves_within(layer_name, region, number_of_objects) 58 + self.n_random_curves_within(region, number_of_objects, layer_name) 57 59 } 58 60 59 61 pub fn random_layer_within(&self, name: &str, region: &Region) -> Layer { 60 62 let mut objects: HashMap<String, ColoredObject> = HashMap::new(); 61 - let number_of_objects = rand::thread_rng().gen_range(self.objects_count_range.clone()); 63 + let number_of_objects = 64 + rand::thread_rng().gen_range(self.objects_count_range.clone()); 62 65 for i in 0..number_of_objects { 63 66 let object = Object::random( 64 67 region,
+20 -10
src/rendering/canvas.rs
··· 1 1 use super::renderable::SVGRenderable; 2 2 use crate::graphics::canvas::Canvas; 3 - use measure_time::{debug_time, info_time}; 3 + use measure_time::debug_time; 4 4 use resvg::usvg; 5 5 use std::sync::Arc; 6 6 ··· 44 44 } 45 45 46 46 for pattern_fill in self.unique_pattern_fills() { 47 - if let Some(patterndef) = pattern_fill.pattern_definition(&self.colormap) { 47 + if let Some(patterndef) = 48 + pattern_fill.pattern_definition(&self.colormap) 49 + { 48 50 defs = defs.add(patterndef) 49 51 } 50 52 } ··· 73 75 height: u32, 74 76 contents: &str, 75 77 ) -> anyhow::Result<tiny_skia::Pixmap> { 76 - info_time!("svg_to_pixmap"); 77 - 78 78 let mut pixmap = self.create_pixmap(width, height); 79 79 80 80 let parsed_svg = &svg_to_usvg_tree(contents, &self.fontdb)?; ··· 90 90 height: u32, 91 91 ) -> anyhow::Result<tiny_skia::Pixmap> { 92 92 let svg_contents = self 93 - .render_to_svg(self.colormap.clone(), self.cell_size, self.object_sizes, "")? 93 + .render_to_svg( 94 + self.colormap.clone(), 95 + self.cell_size, 96 + self.object_sizes, 97 + "", 98 + )? 94 99 .to_string(); 95 100 self.svg_to_pixmap(width, height, &svg_contents) 96 101 } ··· 101 106 width: u32, 102 107 height: u32, 103 108 ) -> anyhow::Result<Option<tiny_skia::Pixmap>> { 104 - info_time!("render_to_pixmap"); 109 + debug_time!("render_to_pixmap"); 105 110 106 111 self.load_fonts()?; 107 112 108 113 let new_svg_contents = self 109 - .render_to_svg(self.colormap.clone(), self.cell_size, self.object_sizes, "")? 114 + .render_to_svg( 115 + self.colormap.clone(), 116 + self.cell_size, 117 + self.object_sizes, 118 + "", 119 + )? 110 120 .to_string(); 111 121 if let Some(cached_svg) = &self.png_render_cache { 112 122 if *cached_svg == new_svg_contents { ··· 129 139 mut pixmap_mut: tiny_skia::PixmapMut<'_>, 130 140 parsed_svg: &resvg::usvg::Tree, 131 141 ) { 132 - info_time!("usvg_tree_to_pixmap"); 142 + debug_time!("usvg_tree_to_pixmap"); 133 143 resvg::render( 134 144 parsed_svg, 135 145 tiny_skia::Transform::from_scale( ··· 141 151 } 142 152 143 153 fn create_pixmap(&self, width: u32, height: u32) -> tiny_skia::Pixmap { 144 - info_time!("create_pixmap"); 154 + debug_time!("create_pixmap"); 145 155 tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap") 146 156 } 147 157 } ··· 150 160 svg: &str, 151 161 fontdb: &Option<Arc<usvg::fontdb::Database>>, 152 162 ) -> anyhow::Result<resvg::usvg::Tree> { 153 - info_time!("svg_to_usvg_tree"); 163 + debug_time!("svg_to_usvg_tree"); 154 164 Ok(resvg::usvg::Tree::from_str( 155 165 svg, 156 166 &match fontdb {
-5
src/rendering/layer.rs
··· 1 - use measure_time::debug_time; 2 - 3 1 use crate::Layer; 4 - 5 2 use super::renderable::SVGRenderable; 6 3 7 4 impl SVGRenderable for Layer { ··· 12 9 object_sizes: crate::graphics::objects::ObjectSizes, 13 10 id: &str, 14 11 ) -> anyhow::Result<svg::node::element::Element> { 15 - debug_time!("render_to_svg"); 16 - 17 12 let mut layer_group = svg::node::element::Group::new() 18 13 .set("class", "layer") 19 14 .set("data-layer", self.name.clone());
+7
src/rendering/mod.rs
··· 7 7 pub mod renderable; 8 8 pub mod transform; 9 9 10 + use measure_time::debug_time; 10 11 pub use renderable::{CSSRenderable, SVGAttributesRenderable, SVGRenderable}; 12 + 13 + pub fn stringify_svg(element: svg::node::element::Element) -> String { 14 + debug_time!("stringify_svg"); 15 + 16 + return element.to_string(); 17 + }
+2
src/synchronization/midi.rs
··· 3 3 use crate::ui::{Log, MaybeProgressBar}; 4 4 use indicatif::ProgressBar; 5 5 use itertools::Itertools; 6 + use measure_time::debug_time; 6 7 use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; 7 8 use std::{collections::HashMap, fmt::Debug, path::PathBuf}; 8 9 ··· 144 145 source: &PathBuf, 145 146 progressbar: Option<&ProgressBar>, 146 147 ) -> (Now, HashMap<String, Vec<Note>>) { 148 + debug_time!("load_midi_notes"); 147 149 // Read midi file using midly 148 150 if let Some(pb) = progressbar { 149 151 pb.set_length(1);
+58 -35
src/video/encoding.rs
··· 1 1 extern crate ffmpeg_next as ffmpeg; 2 2 use super::{context::Context, engine::milliseconds_to_timestamp, Video}; 3 + use crate::rendering::stringify_svg; 3 4 use crate::{ui::Log, Canvas, SVGRenderable}; 4 5 use anyhow::Result; 5 6 use indicatif::ProgressIterator; 6 7 use itertools::Itertools; 7 - use measure_time::{debug_time, info_time}; 8 + use measure_time::debug_time; 8 9 use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 9 10 use rayon::{iter::IndexedParallelIterator, slice::ParallelSliceMut}; 10 11 use std::{ ··· 16 17 use video_rs::Time; 17 18 18 19 impl Canvas { 19 - pub fn render_to_hwc_frame(&mut self, resolution: u32) -> anyhow::Result<video_rs::Frame> { 20 + pub fn render_to_hwc_frame( 21 + &mut self, 22 + resolution: u32, 23 + ) -> anyhow::Result<video_rs::Frame> { 20 24 let (width, height) = self.resolution_to_size(resolution); 21 25 let pixmap = self.render_to_pixmap_no_cache(width, height)?; 22 26 self.pixmap_to_hwc_frame(resolution, &pixmap) ··· 27 31 resolution: u32, 28 32 pixmap: &tiny_skia::Pixmap, 29 33 ) -> anyhow::Result<video_rs::Frame> { 30 - info_time!("pixmap_to_hwc_frame"); 34 + debug_time!("pixmap_to_hwc_frame"); 31 35 let (width, height) = self.resolution_to_size(resolution); 32 36 let (width, height) = (width as usize, height as usize); 33 37 let mut data = vec![0u8; height * width * 3]; ··· 38 42 let x = index % width; 39 43 let y = index / width; 40 44 41 - let pixel = pixmap 42 - .pixel(x as u32, y as u32) 43 - .unwrap_or_else(|| panic!("No pixel found at x, y = {x}, {y}")); 45 + let pixel = 46 + pixmap.pixel(x as u32, y as u32).unwrap_or_else(|| { 47 + panic!("No pixel found at x, y = {x}, {y}") 48 + }); 44 49 45 50 chunk[0] = pixel.red(); 46 51 chunk[1] = pixel.green(); ··· 53 58 54 59 impl<AdditionalContext: Default> Video<AdditionalContext> { 55 60 fn setup_encoder(&mut self, output_path: &str) -> anyhow::Result<()> { 56 - let (width, height) = self.initial_canvas.resolution_to_size(self.resolution); 61 + debug_time!("setup_encoder"); 62 + let (width, height) = 63 + self.initial_canvas.resolution_to_size(self.resolution); 57 64 58 65 self.encoder = Some(Arc::new(Mutex::new( 59 66 video_rs::Encoder::new( ··· 71 78 } 72 79 73 80 pub fn render_frames(&mut self) -> Result<usize> { 81 + debug_time!("render_frames"); 74 82 let mut written_frames_count: usize = 0; 75 83 let mut context = Context { 76 84 frame: 0, ··· 102 110 .progress_with(self.progress_bar.clone()) 103 111 { 104 112 context.ms += 1_usize; 105 - context.timestamp = milliseconds_to_timestamp(context.ms).to_string(); 106 - context.beat_fractional = (context.bpm * context.ms) as f32 / (1000.0 * 60.0); 113 + context.timestamp = 114 + milliseconds_to_timestamp(context.ms).to_string(); 115 + context.beat_fractional = 116 + (context.bpm * context.ms) as f32 / (1000.0 * 60.0); 107 117 context.beat = context.beat_fractional as usize; 108 118 context.frame = self.fps * context.ms / 1000; 109 119 ··· 119 129 120 130 if context.marker().starts_with(':') { 121 131 let marker_text = context.marker(); 122 - let commandline = marker_text.trim_start_matches(':').to_string(); 132 + let commandline = 133 + marker_text.trim_start_matches(':').to_string(); 123 134 124 135 for command in &self.commands { 125 136 if commandline.starts_with(&command.name) { ··· 166 177 } 167 178 168 179 if context.frame != previous_rendered_frame { 169 - debug_time!("compute_frame"); 170 180 frames_to_encode.push(( 171 181 Time::from_secs_f64(context.ms as f64 * 1e-3), 172 - canvas 173 - .render_to_svg( 174 - canvas.colormap.clone(), 175 - canvas.cell_size, 176 - canvas.object_sizes, 177 - "", 178 - )? 179 - .to_string(), 182 + stringify_svg(canvas.render_to_svg( 183 + canvas.colormap.clone(), 184 + canvas.cell_size, 185 + canvas.object_sizes, 186 + "", 187 + )?), 180 188 )); 181 189 182 190 written_frames_count += 1; ··· 199 207 let pb = self.progress_bar.clone(); 200 208 let canvas = self.initial_canvas.clone(); 201 209 frames_to_encode.par_iter().for_each(|(time, svg)| { 202 - let (width, height) = canvas.resolution_to_size(resolution); 203 - let pixmap = canvas 204 - .svg_to_pixmap(width, height, svg) 205 - .expect("Failed to render frame"); 206 - 207 - let frame = canvas 208 - .pixmap_to_hwc_frame(resolution, &pixmap) 209 - .expect("Failed to convert pixmap to frame"); 210 - 211 - hwc_frames_send 212 - .send((*time, frame)) 213 - .expect("Failed to send frame"); 210 + encode_frame(&hwc_frames_send, resolution, &canvas, time, svg); 214 211 215 212 pb.inc(1); 216 213 }); ··· 221 218 self.progress_bar.set_length(frames_to_encode.len() as u64); 222 219 self.progress_bar.set_message("Encoding"); 223 220 224 - for (time, frame) in hwc_frames_receive 225 - .iter() 226 - .sorted_by(|(a, _), (b, _)| a.as_secs_f64().total_cmp(&b.as_secs_f64())) 221 + for (time, frame) in 222 + hwc_frames_receive.iter().sorted_by(|(a, _), (b, _)| { 223 + a.as_secs_f64().total_cmp(&b.as_secs_f64()) 224 + }) 227 225 { 228 226 self.encoder 229 227 .as_mut() ··· 242 240 } 243 241 244 242 pub fn render(&mut self, output_file: String) -> Result<()> { 245 - info_time!("render"); 243 + debug_time!("render"); 246 244 247 245 // create_dir_all(self.frames_output_directory)?; 248 246 // remove_dir_all(self.frames_output_directory)?; ··· 281 279 todo!("Look into https://github.com/zmwangx/rust-ffmpeg/blob/master/examples/transcode-x264.rs and maybe contribute to video-rs (see https://github.com/oddity-ai/video-rs/issues/44)"); 282 280 } 283 281 } 282 + 283 + fn encode_frame( 284 + hwc_frames_send: &std::sync::mpsc::Sender<( 285 + Time, 286 + ndarray::ArrayBase<ndarray::OwnedRepr<u8>, ndarray::Dim<[usize; 3]>>, 287 + )>, 288 + resolution: u32, 289 + canvas: &Canvas, 290 + time: &Time, 291 + svg: &String, 292 + ) { 293 + debug_time!("encode_frame"); 294 + let (width, height) = canvas.resolution_to_size(resolution); 295 + let pixmap = canvas 296 + .svg_to_pixmap(width, height, svg) 297 + .expect("Failed to render frame"); 298 + 299 + let frame = canvas 300 + .pixmap_to_hwc_frame(resolution, &pixmap) 301 + .expect("Failed to convert pixmap to frame"); 302 + 303 + hwc_frames_send 304 + .send((*time, frame)) 305 + .expect("Failed to send frame"); 306 + }
+2 -2
src/video/engine.rs
··· 8 8 use anyhow::Result; 9 9 use chrono::{DateTime, NaiveDateTime}; 10 10 use indicatif::ProgressBar; 11 - use measure_time::info_time; 11 + use measure_time::debug_time; 12 12 #[allow(unused)] 13 13 use std::sync::{Arc, Mutex}; 14 14 use std::{fmt::Formatter, panic, path::PathBuf}; ··· 109 109 } 110 110 111 111 pub fn sync_audio_with(self, sync_data_path: &str) -> Self { 112 - info_time!("sync_audio_with"); 112 + debug_time!("sync_audio_with"); 113 113 if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") { 114 114 let loader = MidiSynchronizer::new(sync_data_path); 115 115 let syncdata = loader.load(Some(&self.progress_bar));