@@ -7,8 +7,7 @@ use bevy::ecs::schedule::{
7
7
} ;
8
8
use bevy:: ecs:: system:: { System , SystemInput } ;
9
9
use bevy:: reflect:: Reflect ;
10
- use bevy:: utils:: HashSet ;
11
- use bevy:: utils:: hashbrown:: HashMap ;
10
+ use bevy:: utils:: hashbrown:: { HashMap , HashSet } ;
12
11
use dot_writer:: { Attributes , DotWriter } ;
13
12
14
13
#[ derive( Reflect , Debug , Clone ) ]
@@ -76,7 +75,7 @@ impl ReflectSystem {
76
75
}
77
76
}
78
77
79
- #[ derive( Reflect , Clone , Debug , PartialEq , Eq , Hash ) ]
78
+ #[ derive( Reflect , Clone , Debug , PartialEq , Eq , Hash , PartialOrd , Ord ) ]
80
79
#[ reflect( opaque) ]
81
80
pub ( crate ) struct ReflectNodeId ( pub NodeId ) ;
82
81
@@ -158,7 +157,11 @@ impl ReflectSystemSet {
158
157
/// Renders a schedule to a dot graph using the optimized schedule.
159
158
pub fn schedule_to_dot_graph ( schedule : & Schedule ) -> String {
160
159
let graph = schedule_to_reflect_graph ( schedule) ;
160
+ reflect_graph_to_dot ( graph)
161
+ }
161
162
163
+ /// Renders a reflectable system graph to a dot graph
164
+ pub fn reflect_graph_to_dot ( graph : ReflectSystemGraph ) -> String {
162
165
// create a dot graph with:
163
166
// - hierarchy represented by red composition arrows
164
167
// - dependencies represented by blue arrows
@@ -191,7 +194,6 @@ pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
191
194
}
192
195
}
193
196
194
- // go through hierarchy edges
195
197
// go through hierarchy edges
196
198
for edge in graph. hierarchy {
197
199
let from = node_id_map. get ( & edge. from ) . cloned ( ) . unwrap_or_else ( || {
@@ -321,6 +323,132 @@ pub struct ReflectSystemGraph {
321
323
hierarchy : Vec < Edge > ,
322
324
}
323
325
326
+ impl ReflectSystemGraph {
327
+ /// Sorts the graph nodes and edges.
328
+ ///
329
+ /// Useful if you want a stable order and deterministic graphs.
330
+ pub fn sort ( & mut self ) {
331
+ // sort everything in this graph
332
+ self . nodes . sort_by_key ( |node| match node {
333
+ ReflectSystemGraphNode :: System ( system) => system. node_id . 0 ,
334
+ ReflectSystemGraphNode :: SystemSet ( system_set) => system_set. node_id . 0 ,
335
+ } ) ;
336
+
337
+ self . dependencies . sort ( ) ;
338
+
339
+ self . hierarchy . sort ( ) ;
340
+ }
341
+
342
+ /// removes the set and bridges the edges connecting to it
343
+ fn absorb_set ( & mut self , node_id : NodeId ) {
344
+ // collect hierarchy parents and children in one pass
345
+ let mut hierarchy_parents = Vec :: new ( ) ;
346
+ let mut hierarchy_children = Vec :: new ( ) ;
347
+ // the relation ship expressed by edges is "is child of"
348
+ for edge in & self . hierarchy {
349
+ // these are children
350
+ if edge. to . 0 == node_id {
351
+ hierarchy_children. push ( edge. from . clone ( ) ) ;
352
+ }
353
+ // these are parents
354
+ if edge. from . 0 == node_id {
355
+ hierarchy_parents. push ( edge. to . clone ( ) ) ;
356
+ }
357
+ }
358
+
359
+ //
360
+ let mut dependencies = Vec :: new ( ) ;
361
+ let mut dependents = Vec :: new ( ) ;
362
+ // the relationship expressed is "runs before" i.e. "is depended on by"
363
+ for edge in & self . dependencies {
364
+ if edge. to . 0 == node_id {
365
+ dependencies. push ( edge. from . clone ( ) ) ;
366
+ }
367
+ if edge. from . 0 == node_id {
368
+ dependents. push ( edge. to . clone ( ) ) ;
369
+ }
370
+ }
371
+
372
+ let mut new_hierarchy_edges =
373
+ HashSet :: with_capacity ( hierarchy_parents. len ( ) * hierarchy_children. len ( ) ) ;
374
+ let mut new_dependency_edges =
375
+ HashSet :: with_capacity ( dependencies. len ( ) * dependents. len ( ) ) ;
376
+
377
+ // each parent, becomes a parent to the sets children
378
+ for parent in hierarchy_parents. iter ( ) {
379
+ for child in hierarchy_children. iter ( ) {
380
+ new_hierarchy_edges. insert ( Edge {
381
+ from : child. clone ( ) ,
382
+ to : parent. clone ( ) ,
383
+ } ) ;
384
+ }
385
+ }
386
+
387
+ for child in hierarchy_parents. iter ( ) {
388
+ // bridge dependencies
389
+ for dependency in dependencies. iter ( ) {
390
+ new_dependency_edges. insert ( Edge {
391
+ from : dependency. clone ( ) ,
392
+ to : child. clone ( ) ,
393
+ } ) ;
394
+ }
395
+
396
+ for dependent in dependents. iter ( ) {
397
+ new_dependency_edges. insert ( Edge {
398
+ from : child. clone ( ) ,
399
+ to : dependent. clone ( ) ,
400
+ } ) ;
401
+ }
402
+ }
403
+
404
+
405
+ // remove any edges involving the set to absorb
406
+ self . hierarchy
407
+ . retain ( |edge| edge. from . 0 != node_id && edge. to . 0 != node_id) ;
408
+ self . dependencies
409
+ . retain ( |edge| edge. from . 0 != node_id && edge. to . 0 != node_id) ;
410
+
411
+ // remove the set from nodes
412
+ self . nodes . retain ( |node| match node {
413
+ ReflectSystemGraphNode :: SystemSet ( system_set) => system_set. node_id . 0 != node_id,
414
+ _ => true ,
415
+ } ) ;
416
+
417
+ // add new bridging edges
418
+ self . hierarchy . extend ( new_hierarchy_edges) ;
419
+ self . dependencies . extend ( new_dependency_edges) ;
420
+ }
421
+
422
+ /// type system sets, are not really important to us, for all intents and purposes
423
+ /// they are one and the same as the underlying systems
424
+ /// Adapter and pipe systems might have multiple default system sets attached, but we want all them gone from the graph.
425
+ ///
426
+ /// Inlines every type system set into its children, replacing anything pointing to those sets by edges to every system contained in the set
427
+ pub fn absorb_type_system_sets ( & mut self ) {
428
+
429
+ let type_sets = self
430
+ . nodes
431
+ . iter ( )
432
+ . filter_map ( |node| match node {
433
+ ReflectSystemGraphNode :: SystemSet ( system_set) => {
434
+ if system_set. type_id . is_some ( ) {
435
+ Some ( system_set. node_id . 0 )
436
+ } else {
437
+ None
438
+ }
439
+ }
440
+ _ => None ,
441
+ } )
442
+ . collect :: < Vec < _ > > ( ) ;
443
+
444
+ // yes this is very inefficient, no this isn't a big hot path, these graphs mostly serve a debugging role.
445
+ for node_id in type_sets {
446
+ self . absorb_set ( node_id) ;
447
+ }
448
+
449
+ }
450
+ }
451
+
324
452
/// A node in the reflectable system graph
325
453
#[ derive( Reflect ) ]
326
454
pub enum ReflectSystemGraphNode {
@@ -331,10 +459,83 @@ pub enum ReflectSystemGraphNode {
331
459
}
332
460
333
461
/// An edge in the graph
334
- #[ derive( Reflect ) ]
462
+ #[ derive( Reflect , PartialEq , Eq , PartialOrd , Ord , Hash ) ]
335
463
pub struct Edge {
336
464
/// The id of the node this edge is coming from
337
465
from : ReflectNodeId ,
338
466
/// The id of the node this edge is going to
339
467
to : ReflectNodeId ,
340
468
}
469
+
470
+ #[ cfg( test) ]
471
+ mod test {
472
+ use bevy:: {
473
+ app:: Update ,
474
+ ecs:: {
475
+ schedule:: { IntoSystemConfigs , IntoSystemSetConfigs } ,
476
+ world:: World ,
477
+ } ,
478
+ } ;
479
+
480
+ use super :: * ;
481
+
482
+ fn system_a ( ) { }
483
+
484
+ fn system_b ( ) { }
485
+
486
+ fn system_c ( ) { }
487
+
488
+ fn system_d ( ) { }
489
+
490
+ fn system_e ( ) { }
491
+
492
+ const BLESS_MODE : bool = false ;
493
+
494
+ #[ test]
495
+ fn test_graph_is_as_expected ( ) {
496
+ // create empty schedule and graph it
497
+
498
+ let mut schedule = Schedule :: new ( Update ) ;
499
+
500
+ #[ derive( SystemSet , Hash , PartialEq , Eq , PartialOrd , Ord , Debug , Clone , Copy ) ]
501
+ enum SystemSet {
502
+ SystemSetG ,
503
+ SystemSetH ,
504
+ }
505
+
506
+ // add a few systems that depend on each other, some via system sets
507
+
508
+ schedule
509
+ . add_systems ( system_a)
510
+ . add_systems ( system_b. before ( system_a) )
511
+ . add_systems ( system_c. after ( system_b) . before ( SystemSet :: SystemSetH ) )
512
+ . add_systems ( system_d. in_set ( SystemSet :: SystemSetG ) )
513
+ . add_systems ( system_e. in_set ( SystemSet :: SystemSetH ) )
514
+ . configure_sets ( SystemSet :: SystemSetG . after ( SystemSet :: SystemSetH ) ) ;
515
+ let mut world = World :: new ( ) ;
516
+ schedule. initialize ( & mut world) . unwrap ( ) ;
517
+
518
+ let mut graph = schedule_to_reflect_graph ( & schedule) ;
519
+ graph. absorb_type_system_sets ( ) ;
520
+ graph. sort ( ) ;
521
+ let dot = reflect_graph_to_dot ( graph) ;
522
+
523
+ let normalize = |s : & str | {
524
+ // trim each line individually from the start, and replace " = " with "=" to deal with formatters
525
+ let lines: Vec < & str > = s. lines ( ) . map ( |line| line. trim_start ( ) ) . collect ( ) ;
526
+ lines. join ( "\n " ) . replace ( " = " , "" ) . replace ( ";" , "" ) . replace ( "," , "" ) . trim ( ) . to_string ( )
527
+ } ;
528
+
529
+ // check that the dot graph is as expected
530
+ // the expected file is found next to the src/lib.rs folder
531
+ let expected = include_str ! ( "../test_graph.dot" ) ;
532
+ let expected_path = manifest_dir_macros:: file_path!( "test_graph.dot" ) ;
533
+
534
+ if BLESS_MODE {
535
+ std:: fs:: write ( expected_path, normalize ( & dot) ) . unwrap ( ) ;
536
+ panic ! ( "Bless mode is active" ) ;
537
+ } else {
538
+ pretty_assertions:: assert_eq!( normalize( & dot) , normalize( expected) ) ;
539
+ }
540
+ }
541
+ }
0 commit comments