1use alloc::format;
16use alloc::string::{String, ToString};
17use core::fmt::Write as _;
18
19use crate::caps::Caps;
20use crate::graph::{Edge, Graph, NodeId, NodeKind, ValidatedGraph};
21use crate::link::LinkPolicy;
22use crate::memory::MemoryDomainKind;
23
24#[derive(Debug, Default, Clone, Copy)]
32pub struct DotAnnotations<'a> {
33 pub edge_caps: Option<&'a [Caps]>,
36 pub edge_memory: Option<&'a [MemoryDomainKind]>,
41}
42
43impl<E> ValidatedGraph<E> {
44 pub fn to_dot(
54 &self,
55 title: &str,
56 label: impl Fn(NodeId) -> Option<String>,
57 ann: &DotAnnotations<'_>,
58 ) -> String {
59 render(
60 title,
61 self.node_count(),
62 |n| self.kind(n),
63 self.edges(),
64 label,
65 ann,
66 )
67 }
68}
69
70impl<E> Graph<E> {
71 pub fn to_dot(
77 &self,
78 title: &str,
79 label: impl Fn(NodeId) -> Option<String>,
80 ann: &DotAnnotations<'_>,
81 ) -> String {
82 render(
83 title,
84 self.node_count(),
85 |n| self.node_kind(n).expect("node id in range"),
86 self.edges(),
87 label,
88 ann,
89 )
90 }
91}
92
93fn render(
97 title: &str,
98 node_count: usize,
99 kind_of: impl Fn(NodeId) -> NodeKind,
100 edges: &[Edge],
101 label: impl Fn(NodeId) -> Option<String>,
102 ann: &DotAnnotations<'_>,
103) -> String {
104 let mut s = String::new();
105 let _ = writeln!(s, "digraph \"{}\" {{", escape(title));
106 s.push_str(" rankdir=LR;\n");
107 s.push_str(" node [fontname=\"monospace\", fontsize=10];\n");
108 s.push_str(" edge [fontname=\"monospace\", fontsize=9];\n");
109
110 for i in 0..node_count {
112 let node = NodeId(i as u32);
113 let kind = kind_of(node);
114 let name = label(node).unwrap_or_else(|| kind_label(kind).to_string());
115 let _ = writeln!(
116 s,
117 " n{i} [label=\"{}\"{}];",
118 escape(&name),
119 node_style(kind)
120 );
121 }
122
123 s.push('\n');
124
125 for (id, e) in edges.iter().enumerate() {
127 let (src, dst) = (e.src.node.0, e.dst.node.0);
128 let domain = ann.edge_memory.and_then(|m| m.get(id).copied());
129 let label = edge_label(ann.edge_caps.and_then(|c| c.get(id)), domain, e.policy);
130 let mut attrs = String::new();
131 if !label.is_empty() {
132 let _ = write!(attrs, "label=\"{}\"", escape(&label));
133 }
134 if matches!(domain, Some(d) if d != MemoryDomainKind::System) {
137 if !attrs.is_empty() {
138 attrs.push_str(", ");
139 }
140 attrs.push_str("color=\"#b58900\", penwidth=2");
141 }
142 let pads = pad_labels(e.src.index, e.dst.index);
145 if !pads.is_empty() {
146 if !attrs.is_empty() {
147 attrs.push_str(", ");
148 }
149 attrs.push_str(&pads);
150 }
151 if attrs.is_empty() {
152 let _ = writeln!(s, " n{src} -> n{dst};");
153 } else {
154 let _ = writeln!(s, " n{src} -> n{dst} [{attrs}];");
155 }
156 }
157
158 s.push_str("}\n");
159 s
160}
161
162pub fn kind_label(kind: NodeKind) -> &'static str {
166 match kind {
167 NodeKind::Source => "source",
168 NodeKind::Transform => "transform",
169 NodeKind::Sink => "sink",
170 NodeKind::Tee(_) => "tee",
171 NodeKind::Muxer(_) => "mux",
172 NodeKind::FaninSink(_) => "fanin-sink",
173 NodeKind::FanoutSrc(_) => "fanout-src",
174 }
175}
176
177fn node_style(kind: NodeKind) -> &'static str {
181 match kind {
182 NodeKind::Source => ", shape=box, style=\"rounded,filled\", fillcolor=\"#cde8cd\"",
183 NodeKind::Sink => ", shape=box, style=\"rounded,filled\", fillcolor=\"#f0cdcd\"",
184 NodeKind::Transform => ", shape=box, style=\"rounded,filled\", fillcolor=\"#cddcf0\"",
185 NodeKind::Tee(_) => ", shape=diamond, style=filled, fillcolor=\"#f0e8cd\"",
186 NodeKind::Muxer(_) => ", shape=trapezium, style=filled, fillcolor=\"#f0e8cd\"",
187 NodeKind::FaninSink(_) => ", shape=trapezium, style=filled, fillcolor=\"#f0cdcd\"",
188 NodeKind::FanoutSrc(_) => ", shape=invtrapezium, style=filled, fillcolor=\"#f0cdcd\"",
189 }
190}
191
192fn edge_label(caps: Option<&Caps>, domain: Option<MemoryDomainKind>, policy: LinkPolicy) -> String {
196 let mut lines: alloc::vec::Vec<String> = alloc::vec::Vec::new();
197 if let Some(c) = caps {
198 lines.push(c.to_gst_string());
199 }
200 if let Some(d) = domain {
201 if d != MemoryDomainKind::System {
202 lines.push(format!("memory:{d:?}"));
203 }
204 }
205 if policy != LinkPolicy::Block {
206 lines.push(format!("[{policy:?}]"));
207 }
208 lines.join("\\n")
210}
211
212fn pad_labels(src_index: u8, dst_index: u8) -> String {
215 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
216 if src_index != 0 {
217 parts.push(format!("taillabel=\"{src_index}\""));
218 }
219 if dst_index != 0 {
220 parts.push(format!("headlabel=\"{dst_index}\""));
221 }
222 parts.join(", ")
223}
224
225fn escape(s: &str) -> String {
227 let mut out = String::with_capacity(s.len());
228 for c in s.chars() {
229 match c {
230 '\\' => out.push_str("\\\\"),
231 '"' => out.push_str("\\\""),
232 _ => out.push(c),
233 }
234 }
235 out
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::caps::{Caps, Dim, Rate, VideoCodec};
242 use crate::graph::Graph;
243 use crate::link::LinkPolicy;
244
245 type G = Graph<&'static str>;
246
247 fn h264(w: u32, h: u32) -> Caps {
248 Caps::CompressedVideo {
249 codec: VideoCodec::H264,
250 width: Dim::Fixed(w),
251 height: Dim::Fixed(h),
252 framerate: Rate::Fixed(30 << 16),
253 }
254 }
255
256 #[test]
257 fn linear_chain_renders_nodes_and_caps_labelled_edges() {
258 let mut g = G::new();
259 let src = g.add_source("rtspsrc");
260 let tx = g.add_transform("h264parse");
261 let sink = g.add_sink("fakesink");
262 g.link(src, tx).unwrap();
263 g.link(tx, sink).unwrap();
264 let v = g.finish().unwrap();
265
266 let caps = [h264(1920, 1080), h264(1920, 1080)];
267 let dot = v.to_dot(
268 "pipeline",
269 |n| v.element(n).map(|e| (*e).to_string()),
270 &DotAnnotations {
271 edge_caps: Some(&caps),
272 edge_memory: None,
273 },
274 );
275
276 assert!(dot.starts_with("digraph \"pipeline\" {"));
277 assert!(dot.trim_end().ends_with('}'));
278 assert!(dot.contains("label=\"rtspsrc\""));
280 assert!(dot.contains("label=\"h264parse\""));
281 assert!(dot.contains("label=\"fakesink\""));
282 assert!(dot.contains("n0 -> n1"));
284 assert!(dot.contains("n1 -> n2"));
285 assert!(
286 dot.contains("video/x-h264"),
287 "edge caps should be labelled: {dot}"
288 );
289 assert!(dot.contains("fillcolor=\"#cde8cd\"")); assert!(dot.contains("fillcolor=\"#f0cdcd\"")); }
293
294 #[test]
295 fn structural_nodes_fall_back_to_kind_and_pads_are_labelled() {
296 let mut g = G::new();
297 let src = g.add_source("src");
298 let tee = g.add_tee(2);
299 let a = g.add_sink("a");
300 let b = g.add_sink("b");
301 g.link(src, tee.input()).unwrap();
302 g.link(tee.out(0), a).unwrap();
303 g.link_with(tee.out(1), b, LinkPolicy::DropOldest).unwrap();
305 let v = g.finish().unwrap();
306
307 let dot = v.to_dot(
309 "fanout",
310 |n| v.element(n).map(|e| (*e).to_string()),
311 &DotAnnotations::default(),
312 );
313 assert!(
314 dot.contains("label=\"tee\""),
315 "tee uses kind fallback: {dot}"
316 );
317 assert!(dot.contains("shape=diamond"));
318 assert!(
320 dot.contains("taillabel=\"1\""),
321 "tee branch pad index: {dot}"
322 );
323 assert!(
324 dot.contains("[DropOldest]"),
325 "non-default policy shown: {dot}"
326 );
327 }
328
329 #[test]
330 fn gpu_memory_edge_is_marked() {
331 let mut g = G::new();
332 let src = g.add_source("nvdec");
333 let sink = g.add_sink("nvenc");
334 g.link(src, sink).unwrap();
335 let v = g.finish().unwrap();
336
337 let mem = [MemoryDomainKind::Cuda];
338 let dot = v.to_dot(
339 "gpu",
340 |n| v.element(n).map(|e| (*e).to_string()),
341 &DotAnnotations {
342 edge_caps: None,
343 edge_memory: Some(&mem),
344 },
345 );
346 assert!(dot.contains("memory:Cuda"), "CUDA domain labelled: {dot}");
347 assert!(dot.contains("penwidth=2"), "GPU link drawn bold: {dot}");
348 let sys = [MemoryDomainKind::System];
350 let dot2 = v.to_dot(
351 "sys",
352 |_| None,
353 &DotAnnotations {
354 edge_caps: None,
355 edge_memory: Some(&sys),
356 },
357 );
358 assert!(
359 !dot2.contains("memory:"),
360 "System domain is not labelled: {dot2}"
361 );
362 }
363
364 #[test]
365 fn title_and_names_are_escaped() {
366 let mut g = G::new();
367 let src = g.add_source("a\"b");
368 let sink = g.add_sink("sink");
369 g.link(src, sink).unwrap();
370 let v = g.finish().unwrap();
371 let dot = v.to_dot(
372 "t\"t",
373 |n| v.element(n).map(|e| (*e).to_string()),
374 &DotAnnotations::default(),
375 );
376 assert!(
377 dot.contains("digraph \"t\\\"t\""),
378 "title quote escaped: {dot}"
379 );
380 assert!(
381 dot.contains("label=\"a\\\"b\""),
382 "name quote escaped: {dot}"
383 );
384 }
385}