tessera_ui/renderer/
reorder.rs

1//! Command reordering utilities to improve batching and reduce GPU barriers.
2//! ## Usage Sort command streams before submission to minimize pipeline flushes.
3
4use std::{
5    any::TypeId,
6    collections::{BinaryHeap, HashMap},
7};
8
9use petgraph::{
10    graph::{DiGraph, NodeIndex},
11    visit::IntoNodeIdentifiers,
12};
13
14use crate::{
15    px::{Px, PxPosition, PxRect, PxSize},
16    renderer::command::{BarrierRequirement, Command},
17};
18
19/// Instruction category for sorting.
20/// The order of the variants is important as it defines the priority.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub(crate) enum InstructionCategory {
23    /// Low priority, can be batched together.
24    ContinuationDraw,
25    /// Medium priority, requires a barrier.
26    BarrierDraw,
27    /// High priority, must be executed before barrier draws that depend on it.
28    Compute,
29    /// A state-changing command that acts as a reordering fence.
30    StateChange,
31}
32
33/// A wrapper for a command with additional information for sorting.
34pub(crate) struct InstructionInfo {
35    pub(crate) original_index: usize,
36    pub(crate) command: Command,
37    pub(crate) type_id: TypeId,
38    pub(crate) size: PxSize,
39    pub(crate) position: PxPosition,
40    pub(crate) category: InstructionCategory,
41    pub(crate) rect: PxRect,
42}
43
44impl InstructionInfo {
45    /// Creates a new `InstructionInfo` from a command and its context.
46    ///
47    /// It calculates the instruction category and the bounding rectangle.
48    pub(crate) fn new(
49        (command, type_id, size, position): (Command, TypeId, PxSize, PxPosition),
50        original_index: usize,
51    ) -> Self {
52        let (category, rect) = match &command {
53            Command::Compute(command) => {
54                // Compute commands should have proper scoping based on their barrier requirement
55                // instead of always using global scope
56                let barrier_req = command.barrier();
57                let rect = match barrier_req {
58                    BarrierRequirement::Global => PxRect {
59                        x: Px(0),
60                        y: Px(0),
61                        width: Px(i32::MAX),
62                        height: Px(i32::MAX),
63                    },
64                    BarrierRequirement::PaddedLocal(_) => component_dependency_rect(position, size),
65                    BarrierRequirement::Absolute(rect) => rect,
66                };
67                (InstructionCategory::Compute, rect)
68            }
69            Command::Draw(draw_command) => {
70                let barrier = draw_command.barrier();
71                let category = if barrier.is_some() {
72                    InstructionCategory::BarrierDraw
73                } else {
74                    InstructionCategory::ContinuationDraw
75                };
76
77                let rect = match barrier {
78                    Some(BarrierRequirement::Global) => PxRect {
79                        x: Px(0),
80                        y: Px(0),
81                        width: Px(i32::MAX),
82                        height: Px(i32::MAX),
83                    },
84                    Some(BarrierRequirement::PaddedLocal(_)) => {
85                        component_dependency_rect(position, size)
86                    }
87                    Some(BarrierRequirement::Absolute(rect)) => rect,
88                    None => PxRect {
89                        x: position.x,
90                        y: position.y,
91                        width: size.width,
92                        height: size.height,
93                    },
94                };
95                (category, rect)
96            }
97            Command::ClipPush(rect) => (InstructionCategory::StateChange, *rect),
98            Command::ClipPop => (
99                InstructionCategory::StateChange,
100                PxRect {
101                    x: position.x,
102                    y: position.y,
103                    width: Px::ZERO,
104                    height: Px::ZERO,
105                },
106            ),
107        };
108
109        Self {
110            original_index,
111            command,
112            type_id,
113            size,
114            position,
115            category,
116            rect,
117        }
118    }
119}
120
121fn component_dependency_rect(position: PxPosition, size: PxSize) -> PxRect {
122    PxRect {
123        x: position.x.max(Px::ZERO),
124        y: position.y.max(Px::ZERO),
125        width: size.width,
126        height: size.height,
127    }
128}
129
130/// A node in the priority queue for topological sorting.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132struct PriorityNode {
133    category: InstructionCategory,
134    type_id: TypeId,
135    original_index: usize,
136    node_index: NodeIndex,
137    batch_potential: usize,
138}
139
140impl Ord for PriorityNode {
141    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
142        // This is the core heuristic for optimal batching:
143        // 1. Higher category is always higher priority.
144        // 2. For the same category, nodes with smaller batch potential are prioritized.
145        //    This helps to get "lonely" nodes out of the way, clearing the path for
146        //    larger batches to be processed contiguously.
147        // 3. The original index is used as a final tie-breaker for stability.
148        self.category
149            .cmp(&other.category)
150            .then_with(|| other.batch_potential.cmp(&self.batch_potential))
151            .then_with(|| other.original_index.cmp(&self.original_index))
152    }
153}
154
155impl PartialOrd for PriorityNode {
156    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
157        Some(self.cmp(other))
158    }
159}
160
161pub(crate) fn reorder_instructions(
162    commands: impl IntoIterator<Item = (Command, TypeId, PxSize, PxPosition)>,
163) -> Vec<(Command, TypeId, PxSize, PxPosition)> {
164    let instructions: Vec<InstructionInfo> = commands
165        .into_iter()
166        .enumerate()
167        .map(|(i, cmd)| InstructionInfo::new(cmd, i))
168        .collect();
169
170    if instructions.is_empty() {
171        return vec![];
172    }
173
174    let mut potentials = HashMap::new();
175    for info in &instructions {
176        *potentials.entry((info.category, info.type_id)).or_insert(0) += 1;
177    }
178
179    let graph = build_dependency_graph(&instructions);
180
181    let sorted_node_indices = priority_topological_sort(&graph, &instructions, &potentials);
182
183    let mut sorted_instructions = Vec::with_capacity(instructions.len());
184    let mut original_infos: Vec<_> = instructions.into_iter().map(Some).collect();
185
186    for node_index in sorted_node_indices {
187        let original_index = node_index.index();
188        if let Some(info) = original_infos[original_index].take() {
189            sorted_instructions.push((info.command, info.type_id, info.size, info.position));
190        }
191    }
192
193    sorted_instructions
194}
195
196fn priority_topological_sort(
197    graph: &DiGraph<(), ()>,
198    instructions: &[InstructionInfo],
199    potentials: &HashMap<(InstructionCategory, TypeId), usize>,
200) -> Vec<NodeIndex> {
201    let mut in_degree = vec![0; graph.node_count()];
202    for edge in graph.raw_edges() {
203        in_degree[edge.target().index()] += 1;
204    }
205
206    let mut ready_queue = BinaryHeap::new();
207    for node_index in graph.node_identifiers() {
208        if in_degree[node_index.index()] == 0 {
209            let info = &instructions[node_index.index()];
210            ready_queue.push(PriorityNode {
211                category: info.category,
212                type_id: info.type_id,
213                original_index: info.original_index,
214                node_index,
215                batch_potential: potentials[&(info.category, info.type_id)],
216            });
217        }
218    }
219
220    let mut sorted_list = Vec::with_capacity(instructions.len());
221    let mut last_type_id: Option<TypeId> = None;
222    while !ready_queue.is_empty() {
223        let highest_category = ready_queue.peek().map(|node| node.category);
224
225        let mut selected: Option<PriorityNode> = None;
226        if let (Some(last_type), Some(high_cat)) = (last_type_id, highest_category) {
227            let mut deferred = Vec::new();
228            while let Some(node) = ready_queue.pop() {
229                if node.category == high_cat && node.type_id == last_type {
230                    selected = Some(node);
231                    break;
232                }
233                deferred.push(node);
234            }
235            for node in deferred {
236                ready_queue.push(node);
237            }
238        }
239
240        let priority_node = selected.unwrap_or_else(|| {
241            ready_queue
242                .pop()
243                .expect("ready_queue should not be empty while sorting instructions")
244        });
245        let u = priority_node.node_index;
246        sorted_list.push(u);
247        match priority_node.category {
248            InstructionCategory::StateChange => last_type_id = None,
249            _ => last_type_id = Some(priority_node.type_id),
250        }
251
252        for v in graph.neighbors(u) {
253            in_degree[v.index()] -= 1;
254            if in_degree[v.index()] == 0 {
255                let info = &instructions[v.index()];
256                ready_queue.push(PriorityNode {
257                    category: info.category,
258                    type_id: info.type_id,
259                    original_index: info.original_index,
260                    node_index: v,
261                    batch_potential: potentials[&(info.category, info.type_id)],
262                });
263            }
264        }
265    }
266
267    if sorted_list.len() != instructions.len() {
268        // This indicates a cycle in the graph, which should not happen
269        // in a well-formed UI command stream.
270        // Fallback to original order.
271        return (0..instructions.len()).map(NodeIndex::new).collect();
272    }
273
274    sorted_list
275}
276
277fn build_dependency_graph(instructions: &[InstructionInfo]) -> DiGraph<(), ()> {
278    let mut graph = DiGraph::new();
279    let node_indices: Vec<NodeIndex> = instructions.iter().map(|_| graph.add_node(())).collect();
280
281    for i in 0..instructions.len() {
282        for j in 0..instructions.len() {
283            if i == j {
284                continue;
285            }
286
287            let inst_i = &instructions[i];
288            let inst_j = &instructions[j];
289
290            // Rule 0: State changes act as fences.
291            // If one of two commands is a state change, their relative order must be preserved.
292            if inst_i.original_index < inst_j.original_index
293                && (inst_i.category == InstructionCategory::StateChange
294                    || inst_j.category == InstructionCategory::StateChange)
295            {
296                graph.add_edge(node_indices[i], node_indices[j], ());
297            }
298
299            // Rule 1: Explicit dependency (Compute -> BarrierDraw)
300            // If inst_j is a BarrierDraw and inst_i is a Compute that appeared
301            // earlier, then j depends on i.
302            if inst_i.category == InstructionCategory::Compute
303                && inst_j.category == InstructionCategory::BarrierDraw
304                && inst_i.original_index < inst_j.original_index
305            {
306                graph.add_edge(node_indices[i], node_indices[j], ());
307            }
308
309            // Rule 2: Implicit dependency (Overlapping Draws)
310            // If both are draw commands and their original order matters (j came after i)
311            // and their rectangles are not orthogonal (i.e., they might overlap),
312            // then j depends on i to maintain painter's algorithm.
313            if (inst_i.category == InstructionCategory::BarrierDraw
314                || inst_i.category == InstructionCategory::ContinuationDraw)
315                && (inst_j.category == InstructionCategory::BarrierDraw
316                    || inst_j.category == InstructionCategory::ContinuationDraw)
317                && inst_i.original_index < inst_j.original_index
318                && !inst_i.rect.is_orthogonal(&inst_j.rect)
319            {
320                graph.add_edge(node_indices[i], node_indices[j], ());
321            }
322
323            // Rule 3: Implicit dependency (Draw -> Compute)
324            // If inst_j is a Compute command and inst_i is a Draw command that
325            // appeared earlier, and their areas are not orthogonal, then j depends on i.
326            if (inst_i.category == InstructionCategory::BarrierDraw
327                || inst_i.category == InstructionCategory::ContinuationDraw)
328                && inst_j.category == InstructionCategory::Compute
329                && inst_i.original_index < inst_j.original_index
330                && !inst_i.rect.is_orthogonal(&inst_j.rect)
331            {
332                graph.add_edge(node_indices[i], node_indices[j], ());
333            }
334        }
335    }
336
337    graph
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::{
344        px::{Px, PxPosition, PxRect, PxSize},
345        renderer::{
346            BarrierRequirement, command::Command, compute::ComputeCommand, drawer::DrawCommand,
347        },
348    };
349    use std::any::TypeId;
350    use std::fmt::Debug;
351
352    #[derive(Debug, PartialEq, Clone)]
353    struct MockDrawCommand {
354        barrier_req: Option<BarrierRequirement>,
355    }
356
357    impl DrawCommand for MockDrawCommand {
358        fn barrier(&self) -> Option<BarrierRequirement> {
359            self.barrier_req
360        }
361    }
362
363    #[derive(Debug, PartialEq, Clone)]
364    struct MockDrawCommand2 {
365        barrier_req: Option<BarrierRequirement>,
366    }
367
368    impl DrawCommand for MockDrawCommand2 {
369        fn barrier(&self) -> Option<BarrierRequirement> {
370            self.barrier_req
371        }
372    }
373
374    #[derive(Debug, PartialEq, Clone)]
375    struct MockComputeCommand {
376        barrier_req: BarrierRequirement,
377    }
378
379    impl ComputeCommand for MockComputeCommand {
380        fn barrier(&self) -> BarrierRequirement {
381            self.barrier_req
382        }
383    }
384
385    #[derive(Debug, PartialEq, Clone)]
386    struct MockComputeCommand2 {
387        barrier_req: BarrierRequirement,
388    }
389
390    impl ComputeCommand for MockComputeCommand2 {
391        fn barrier(&self) -> BarrierRequirement {
392            self.barrier_req
393        }
394    }
395
396    // --- Helper Functions ---
397
398    fn create_cmd(
399        pos: PxPosition,
400        barrier_req: Option<BarrierRequirement>,
401        is_compute: bool,
402    ) -> (Command, TypeId, PxSize, PxPosition) {
403        let size = PxSize::new(Px(10), Px(10));
404        if is_compute {
405            let cmd = MockComputeCommand {
406                barrier_req: barrier_req.unwrap_or(BarrierRequirement::Global),
407            };
408            (
409                Command::Compute(Box::new(cmd)),
410                TypeId::of::<MockComputeCommand>(),
411                size,
412                pos,
413            )
414        } else {
415            let cmd = MockDrawCommand { barrier_req };
416            (
417                Command::Draw(Box::new(cmd)),
418                TypeId::of::<MockDrawCommand>(),
419                size,
420                pos,
421            )
422        }
423    }
424
425    fn create_cmd2(
426        pos: PxPosition,
427        barrier_req: Option<BarrierRequirement>,
428        is_compute: bool,
429    ) -> (Command, TypeId, PxSize, PxPosition) {
430        let size = PxSize::new(Px(10), Px(10));
431        if is_compute {
432            let cmd = MockComputeCommand2 {
433                barrier_req: barrier_req.unwrap_or(BarrierRequirement::Global),
434            };
435            (
436                Command::Compute(Box::new(cmd)),
437                TypeId::of::<MockComputeCommand2>(),
438                size,
439                pos,
440            )
441        } else {
442            let cmd = MockDrawCommand2 { barrier_req };
443            (
444                Command::Draw(Box::new(cmd)),
445                TypeId::of::<MockDrawCommand2>(),
446                size,
447                pos,
448            )
449        }
450    }
451
452    fn get_positions(commands: &[(Command, TypeId, PxSize, PxPosition)]) -> Vec<PxPosition> {
453        commands.iter().map(|(_, _, _, pos)| *pos).collect()
454    }
455
456    #[test]
457    fn test_empty_instructions() {
458        let commands = vec![];
459        let reordered = reorder_instructions(commands);
460        assert!(reordered.is_empty());
461    }
462
463    #[test]
464    fn test_no_dependencies_preserves_order() {
465        let commands = vec![
466            create_cmd(PxPosition::new(Px(0), Px(0)), None, false), // 0
467            create_cmd(PxPosition::new(Px(20), Px(0)), None, false), // 1
468        ];
469        let original_positions = get_positions(&commands);
470        let reordered = reorder_instructions(commands);
471        let reordered_positions = get_positions(&reordered);
472        assert_eq!(reordered_positions, original_positions);
473    }
474
475    #[test]
476    fn test_compute_before_barrier_preserves_order() {
477        let commands = vec![
478            create_cmd(
479                PxPosition::new(Px(0), Px(0)),
480                Some(BarrierRequirement::Global),
481                true,
482            ), // 0: Compute
483            create_cmd(
484                PxPosition::new(Px(20), Px(20)),
485                Some(BarrierRequirement::Global),
486                false,
487            ), // 1: BarrierDraw
488        ];
489        let original_positions = get_positions(&commands);
490        let reordered = reorder_instructions(commands);
491        let reordered_positions = get_positions(&reordered);
492        assert_eq!(reordered_positions, original_positions);
493    }
494
495    #[test]
496    fn test_opt() {
497        // Test case 1: No dependencies, test batching
498        let commands = vec![
499            create_cmd(PxPosition::new(Px(0), Px(0)), None, false), // 0 (T1)
500            create_cmd2(PxPosition::new(Px(10), Px(10)), None, false), // 1 (T2)
501            create_cmd(PxPosition::new(Px(20), Px(20)), None, false), // 2 (T1)
502        ];
503        let reordered = reorder_instructions(commands);
504        let reordered_positions = get_positions(&reordered);
505
506        // Potentials: T1 -> 2, T2 -> 1.
507        // T2 has lower potential, so it's prioritized.
508        // Expected order: [1, 0, 2]
509        let expected_positions = vec![
510            PxPosition::new(Px(10), Px(10)), // 1
511            PxPosition::new(Px(0), Px(0)),   // 0
512            PxPosition::new(Px(20), Px(20)), // 2
513        ];
514        assert_eq!(reordered_positions, expected_positions);
515
516        // Test case 2: With dependencies, test batching
517        let commands = vec![
518            create_cmd(PxPosition::new(Px(0), Px(0)), None, false), // 0 (T1)
519            create_cmd2(PxPosition::new(Px(10), Px(10)), None, false), // 1 (T2)
520            create_cmd(PxPosition::new(Px(5), Px(5)), None, false), // 2 (T1)
521        ];
522        let reordered = reorder_instructions(commands);
523        let reordered_positions = get_positions(&reordered);
524
525        // Potentials: T1 -> 2, T2 -> 1.
526        // Dependencies: 2 > 0, 2 > 1.
527        // Initial ready queue: [0, 1].
528        // Node 1 has lower potential (1 vs 2), so it's prioritized.
529        // Expected order: [1, 0, 2]
530        let expected_positions = vec![
531            PxPosition::new(Px(10), Px(10)), // Cmd 1
532            PxPosition::new(Px(0), Px(0)),   // Cmd 0
533            PxPosition::new(Px(5), Px(5)),   // Cmd 2
534        ];
535        assert_eq!(expected_positions, reordered_positions);
536    }
537
538    #[test]
539    fn compute_draw_pairs_remain_grouped_with_local_barrier() {
540        let padding = BarrierRequirement::uniform_padding_local(Px::new(10));
541
542        let commands = vec![
543            create_cmd(PxPosition::new(Px(0), Px(0)), Some(padding), true),
544            create_cmd(PxPosition::new(Px(0), Px(0)), Some(padding), false),
545            create_cmd(PxPosition::new(Px(200), Px(0)), Some(padding), true),
546            create_cmd(PxPosition::new(Px(200), Px(0)), Some(padding), false),
547        ];
548
549        let reordered = reorder_instructions(commands);
550        let kinds: Vec<&'static str> = reordered
551            .iter()
552            .map(|(cmd, _, _, _)| match cmd {
553                Command::Compute(_) => "C",
554                Command::Draw(_) => "D",
555                _ => panic!("unexpected command variant"),
556            })
557            .collect();
558
559        assert_eq!(kinds, vec!["C", "C", "D", "D"]);
560    }
561
562    #[test]
563    fn test_overlapping_draw_preserves_order() {
564        let commands = vec![
565            create_cmd(PxPosition::new(Px(0), Px(0)), None, false), // 0
566            create_cmd(PxPosition::new(Px(5), Px(5)), None, false), // 1 (overlaps with 0)
567        ];
568        let original_positions = get_positions(&commands);
569        let reordered = reorder_instructions(commands);
570        let reordered_positions = get_positions(&reordered);
571        assert_eq!(reordered_positions, original_positions);
572    }
573
574    #[test]
575    fn test_draw_before_overlapping_compute_preserves_order() {
576        let commands = vec![
577            create_cmd(
578                PxPosition::new(Px(0), Px(0)),
579                Some(BarrierRequirement::Global),
580                false,
581            ), // 0: BarrierDraw
582            create_cmd(
583                PxPosition::new(Px(20), Px(20)),
584                Some(BarrierRequirement::Global),
585                true,
586            ), // 1: Compute
587        ];
588        let original_positions = get_positions(&commands);
589        let reordered = reorder_instructions(commands);
590        let reordered_positions = get_positions(&reordered);
591        assert_eq!(reordered_positions, original_positions);
592    }
593
594    #[test]
595    fn test_clip_state_change_acts_as_fence() {
596        let clip_rect = PxRect::new(Px(0), Px(0), Px(50), Px(50));
597        let clip_size = PxSize::new(Px(50), Px(50));
598        let commands = vec![
599            (
600                Command::ClipPush(clip_rect),
601                TypeId::of::<Command>(),
602                clip_size,
603                PxPosition::new(Px(0), Px(0)),
604            ),
605            create_cmd(
606                PxPosition::new(Px(5), Px(5)),
607                Some(BarrierRequirement::Global),
608                false,
609            ),
610            (
611                Command::ClipPop,
612                TypeId::of::<Command>(),
613                clip_size,
614                PxPosition::new(Px(0), Px(0)),
615            ),
616            create_cmd2(
617                PxPosition::new(Px(60), Px(60)),
618                Some(BarrierRequirement::Global),
619                false,
620            ),
621        ];
622        let original_positions = get_positions(&commands);
623        let reordered = reorder_instructions(commands);
624        let reordered_positions = get_positions(&reordered);
625        assert_eq!(reordered_positions, original_positions);
626    }
627
628    #[test]
629    fn test_padded_local_overlap_prevents_reorder() {
630        let commands = vec![
631            create_cmd(
632                PxPosition::new(Px(100), Px(100)),
633                Some(BarrierRequirement::uniform_padding_local(Px(30))),
634                false,
635            ),
636            create_cmd(
637                PxPosition::new(Px(70), Px(70)),
638                Some(BarrierRequirement::Global),
639                false,
640            ),
641        ];
642        let original_positions = get_positions(&commands);
643        let reordered = reorder_instructions(commands);
644        let reordered_positions = get_positions(&reordered);
645        assert_eq!(reordered_positions, original_positions);
646    }
647
648    #[test]
649    fn test_mixed_categories_with_state_fences() {
650        let clip_rect = PxRect::new(Px(0), Px(0), Px(400), Px(400));
651        let clip_size = PxSize::new(Px(400), Px(400));
652        let commands = vec![
653            (
654                Command::ClipPush(clip_rect),
655                TypeId::of::<Command>(),
656                clip_size,
657                PxPosition::ZERO,
658            ),
659            create_cmd(
660                PxPosition::new(Px(10), Px(10)),
661                Some(BarrierRequirement::Absolute(PxRect::new(
662                    Px(10),
663                    Px(10),
664                    Px(20),
665                    Px(20),
666                ))),
667                false,
668            ),
669            create_cmd2(PxPosition::new(Px(220), Px(20)), None, false),
670            create_cmd(
671                PxPosition::new(Px(150), Px(150)),
672                Some(BarrierRequirement::Absolute(PxRect::new(
673                    Px(150),
674                    Px(150),
675                    Px(30),
676                    Px(30),
677                ))),
678                true,
679            ),
680            create_cmd2(
681                PxPosition::new(Px(155), Px(155)),
682                Some(BarrierRequirement::Absolute(PxRect::new(
683                    Px(150),
684                    Px(150),
685                    Px(30),
686                    Px(30),
687                ))),
688                false,
689            ),
690            create_cmd(PxPosition::new(Px(260), Px(30)), None, false),
691            create_cmd2(
692                PxPosition::new(Px(300), Px(60)),
693                Some(BarrierRequirement::Absolute(PxRect::new(
694                    Px(300),
695                    Px(60),
696                    Px(25),
697                    Px(25),
698                ))),
699                true,
700            ),
701            (
702                Command::ClipPop,
703                TypeId::of::<Command>(),
704                clip_size,
705                PxPosition::ZERO,
706            ),
707        ];
708
709        let reordered = reorder_instructions(commands.clone());
710        let reordered_positions = get_positions(&reordered);
711        let expected_positions = vec![
712            PxPosition::ZERO,
713            PxPosition::new(Px(150), Px(150)),
714            PxPosition::new(Px(300), Px(60)),
715            PxPosition::new(Px(10), Px(10)),
716            PxPosition::new(Px(155), Px(155)),
717            PxPosition::new(Px(220), Px(20)),
718            PxPosition::new(Px(260), Px(30)),
719            PxPosition::ZERO,
720        ];
721        assert_eq!(reordered_positions, expected_positions);
722    }
723
724    #[test]
725    fn test_randomized_sequences_preserve_dependencies() {
726        struct Lcg(u64);
727        impl Lcg {
728            fn new(seed: u64) -> Self {
729                Self(seed)
730            }
731            fn next_u32(&mut self) -> u32 {
732                // ANSI C LCG constants
733                self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
734                (self.0 >> 32) as u32
735            }
736            fn next_range(&mut self, upper: u32) -> u32 {
737                if upper == 0 {
738                    0
739                } else {
740                    self.next_u32() % upper
741                }
742            }
743            fn next_bool(&mut self) -> bool {
744                self.next_u32() & 1 == 1
745            }
746        }
747
748        fn random_barrier(rng: &mut Lcg, allow_none: bool) -> Option<BarrierRequirement> {
749            let roll = rng.next_range(if allow_none { 4 } else { 3 });
750            match roll {
751                0 if allow_none => None,
752                0 | 3 => Some(BarrierRequirement::Global),
753                1 => {
754                    let padding = Px(rng.next_range(20) as i32);
755                    Some(BarrierRequirement::uniform_padding_local(padding))
756                }
757                _ => {
758                    let x = Px(rng.next_range(240) as i32);
759                    let y = Px(rng.next_range(240) as i32);
760                    let width = Px(10 + rng.next_range(80) as i32);
761                    let height = Px(10 + rng.next_range(80) as i32);
762                    Some(BarrierRequirement::Absolute(PxRect::new(
763                        x, y, width, height,
764                    )))
765                }
766            }
767        }
768
769        fn random_commands(seed: u64) -> Vec<(Command, TypeId, PxSize, PxPosition)> {
770            let mut rng = Lcg::new(seed);
771            let mut commands = Vec::new();
772            let len = 5 + rng.next_range(6) as usize; // 5..10 commands before balancing clip stack
773            let mut clip_depth = 0usize;
774
775            for _ in 0..len {
776                match rng.next_range(6) {
777                    0 => {
778                        // Continuation draw of type 1
779                        let pos = PxPosition::new(
780                            Px(rng.next_range(256) as i32),
781                            Px(rng.next_range(256) as i32),
782                        );
783                        commands.push(create_cmd(pos, None, false));
784                    }
785                    1 => {
786                        // Continuation draw of type 2 (different TypeId)
787                        let pos = PxPosition::new(
788                            Px(rng.next_range(256) as i32),
789                            Px(rng.next_range(256) as i32),
790                        );
791                        commands.push(create_cmd2(pos, None, false));
792                    }
793                    2 => {
794                        // Barrier draw
795                        let pos = PxPosition::new(
796                            Px(rng.next_range(256) as i32),
797                            Px(rng.next_range(256) as i32),
798                        );
799                        let command = if rng.next_bool() {
800                            create_cmd(pos, random_barrier(&mut rng, false), false)
801                        } else {
802                            create_cmd2(pos, random_barrier(&mut rng, false), false)
803                        };
804                        commands.push(command);
805                    }
806                    3 => {
807                        // Compute command
808                        let pos = PxPosition::new(
809                            Px(rng.next_range(256) as i32),
810                            Px(rng.next_range(256) as i32),
811                        );
812                        let command = if rng.next_bool() {
813                            create_cmd(pos, random_barrier(&mut rng, false), true)
814                        } else {
815                            create_cmd2(pos, random_barrier(&mut rng, false), true)
816                        };
817                        commands.push(command);
818                    }
819                    4 => {
820                        // Clip push
821                        let width = Px(50 + rng.next_range(150) as i32);
822                        let height = Px(50 + rng.next_range(150) as i32);
823                        let rect = PxRect::new(
824                            Px(rng.next_range(200) as i32),
825                            Px(rng.next_range(200) as i32),
826                            width,
827                            height,
828                        );
829                        let size = PxSize::new(width, height);
830                        commands.push((
831                            Command::ClipPush(rect),
832                            TypeId::of::<Command>(),
833                            size,
834                            PxPosition::new(rect.x, rect.y),
835                        ));
836                        clip_depth += 1;
837                    }
838                    _ => {
839                        // Clip pop if possible, otherwise fallback to draw
840                        if clip_depth > 0 {
841                            clip_depth -= 1;
842                            commands.push((
843                                Command::ClipPop,
844                                TypeId::of::<Command>(),
845                                PxSize::new(Px(0), Px(0)),
846                                PxPosition::new(Px(0), Px(0)),
847                            ));
848                        } else {
849                            let pos = PxPosition::new(
850                                Px(rng.next_range(256) as i32),
851                                Px(rng.next_range(256) as i32),
852                            );
853                            commands.push(create_cmd(pos, None, false));
854                        }
855                    }
856                }
857            }
858
859            while clip_depth > 0 {
860                clip_depth -= 1;
861                commands.push((
862                    Command::ClipPop,
863                    TypeId::of::<Command>(),
864                    PxSize::new(Px(0), Px(0)),
865                    PxPosition::new(Px(0), Px(0)),
866                ));
867            }
868
869            commands
870        }
871
872        for seed in 0..50 {
873            let commands = random_commands(seed);
874            let reordered = reorder_instructions(commands.clone());
875
876            // Rebuild instruction metadata to validate ordering properties.
877            let instructions: Vec<InstructionInfo> = commands
878                .iter()
879                .cloned()
880                .enumerate()
881                .map(|(i, cmd)| InstructionInfo::new(cmd, i))
882                .collect();
883
884            let mut potentials = HashMap::new();
885            for info in &instructions {
886                *potentials.entry((info.category, info.type_id)).or_insert(0) += 1;
887            }
888
889            let graph = build_dependency_graph(&instructions);
890            let sorted_indices = priority_topological_sort(&graph, &instructions, &potentials);
891            let expected: Vec<_> = sorted_indices
892                .iter()
893                .map(|idx| commands[idx.index()].clone())
894                .collect();
895
896            assert_eq!(reordered.len(), expected.len());
897            for (idx, (expected_item, reordered_item)) in
898                expected.iter().zip(&reordered).enumerate()
899            {
900                assert_eq!(
901                    expected_item.1, reordered_item.1,
902                    "TypeId mismatch at position {idx} for seed {seed}"
903                );
904                assert_eq!(
905                    expected_item.2, reordered_item.2,
906                    "Size mismatch at position {idx} for seed {seed}"
907                );
908                assert_eq!(
909                    expected_item.3, reordered_item.3,
910                    "Position mismatch at position {idx} for seed {seed}"
911                );
912            }
913
914            // Validate the ordering is a legal topological sort.
915            let mut position_by_original = vec![0usize; instructions.len()];
916            for (order, node_idx) in sorted_indices.iter().enumerate() {
917                position_by_original[node_idx.index()] = order;
918            }
919
920            for edge in graph.raw_edges() {
921                let src = edge.source().index();
922                let dst = edge.target().index();
923                assert!(
924                    position_by_original[src] < position_by_original[dst],
925                    "edge {:?}->{:?} violated for seed {seed}",
926                    src,
927                    dst
928                );
929            }
930        }
931    }
932
933    #[test]
934    fn test_batches_cards_across_orthogonal_gap() {
935        let card1 = create_cmd(PxPosition::new(Px(0), Px(0)), None, false);
936        let text1 = create_cmd2(
937            PxPosition::new(Px(0), Px(0)),
938            None, // overlaps card1
939            false,
940        );
941        let card2 = create_cmd(PxPosition::new(Px(200), Px(0)), None, false);
942        let text2 = create_cmd2(
943            PxPosition::new(Px(200), Px(0)),
944            None, // overlaps card2
945            false,
946        );
947
948        let commands = vec![card1, text1, card2, text2];
949
950        let reordered = reorder_instructions(commands);
951        let reordered_positions = get_positions(&reordered);
952        let expected_positions = vec![
953            PxPosition::new(Px(0), Px(0)),
954            PxPosition::new(Px(200), Px(0)),
955            PxPosition::new(Px(0), Px(0)),
956            PxPosition::new(Px(200), Px(0)),
957        ];
958        assert_eq!(reordered_positions, expected_positions);
959    }
960
961    #[test]
962    fn test_reorder_based_on_priority_with_no_overlap() {
963        let commands = vec![
964            create_cmd(
965                PxPosition::new(Px(0), Px(0)),
966                Some(BarrierRequirement::Absolute(PxRect::new(
967                    Px(0),
968                    Px(0),
969                    Px(10),
970                    Px(10),
971                ))), // rect A
972                false, // BarrierDraw
973            ), // 0
974            create_cmd(
975                PxPosition::new(Px(100), Px(100)),
976                Some(BarrierRequirement::Absolute(PxRect::new(
977                    Px(100),
978                    Px(100),
979                    Px(10),
980                    Px(10),
981                ))), // rect B
982                true, // Compute
983            ), // 1
984            create_cmd(PxPosition::new(Px(200), Px(200)), None, false), // 2: ContinuationDraw
985        ];
986        let original_positions = get_positions(&commands);
987        // No dependencies as all rects are orthogonal.
988        // Priority: Compute (1) > BarrierDraw (0) > ContinuationDraw (2)
989        let reordered = reorder_instructions(commands);
990        let reordered_positions = get_positions(&reordered);
991
992        let expected_positions = vec![
993            original_positions[1], // Compute
994            original_positions[0], // BarrierDraw
995            original_positions[2], // ContinuationDraw
996        ];
997        assert_eq!(reordered_positions, expected_positions);
998    }
999
1000    #[test]
1001    fn test_compute_commands_of_same_type_stay_contiguous() {
1002        let commands = vec![
1003            create_cmd(
1004                PxPosition::new(Px(0), Px(0)),
1005                Some(BarrierRequirement::Absolute(PxRect::new(
1006                    Px(0),
1007                    Px(0),
1008                    Px(30),
1009                    Px(30),
1010                ))),
1011                true,
1012            ),
1013            create_cmd2(PxPosition::new(Px(150), Px(0)), None, false),
1014            create_cmd2(
1015                PxPosition::new(Px(60), Px(0)),
1016                Some(BarrierRequirement::Absolute(PxRect::new(
1017                    Px(60),
1018                    Px(0),
1019                    Px(30),
1020                    Px(30),
1021                ))),
1022                true,
1023            ),
1024            create_cmd(
1025                PxPosition::new(Px(120), Px(0)),
1026                Some(BarrierRequirement::Absolute(PxRect::new(
1027                    Px(120),
1028                    Px(0),
1029                    Px(30),
1030                    Px(30),
1031                ))),
1032                true,
1033            ),
1034        ];
1035
1036        let reordered = reorder_instructions(commands);
1037
1038        let mut compute_type_sequence = Vec::new();
1039        let mut saw_non_compute = false;
1040        for (command, type_id, _, _) in &reordered {
1041            match command {
1042                Command::Compute(_) => {
1043                    assert!(
1044                        !saw_non_compute,
1045                        "Compute command appeared after non-compute commands"
1046                    );
1047                    compute_type_sequence.push(*type_id);
1048                }
1049                _ => saw_non_compute = true,
1050            }
1051        }
1052
1053        assert!(
1054            compute_type_sequence.len() >= 2,
1055            "Expected multiple compute commands to verify grouping"
1056        );
1057
1058        let mut type_positions: std::collections::HashMap<TypeId, Vec<usize>> =
1059            std::collections::HashMap::new();
1060        for (idx, ty) in compute_type_sequence.iter().enumerate() {
1061            type_positions.entry(*ty).or_default().push(idx);
1062        }
1063
1064        for positions in type_positions.values() {
1065            let first = positions[0];
1066            let last = *positions
1067                .last()
1068                .expect("compute positions should not be empty");
1069            assert_eq!(
1070                last - first + 1,
1071                positions.len(),
1072                "Compute type span is not contiguous"
1073            );
1074        }
1075    }
1076
1077    #[test]
1078    fn test_complex_reordering_with_dependencies() {
1079        let commands = vec![
1080            // 0: Compute. Must run first.
1081            create_cmd(
1082                PxPosition::new(Px(0), Px(0)),
1083                Some(BarrierRequirement::Global),
1084                true,
1085            ),
1086            // 1: BarrierDraw. Depends on 0. Orthogonal to 4.
1087            create_cmd(
1088                PxPosition::new(Px(50), Px(50)),
1089                Some(BarrierRequirement::Absolute(PxRect::new(
1090                    Px(50),
1091                    Px(50),
1092                    Px(10),
1093                    Px(10),
1094                ))),
1095                false,
1096            ),
1097            // 2: ContinuationDraw. Overlaps with 3.
1098            create_cmd(PxPosition::new(Px(200), Px(200)), None, false),
1099            // 3: ContinuationDraw.
1100            create_cmd(PxPosition::new(Px(205), Px(205)), None, false),
1101            // 4: BarrierDraw. Depends on 0. Orthogonal to 1.
1102            create_cmd(
1103                PxPosition::new(Px(80), Px(80)),
1104                Some(BarrierRequirement::Absolute(PxRect::new(
1105                    Px(80),
1106                    Px(80),
1107                    Px(10),
1108                    Px(10),
1109                ))),
1110                false,
1111            ),
1112        ];
1113        let original_positions = get_positions(&commands);
1114
1115        // Dependencies:
1116        // 0 -> 1 (Compute -> Barrier)
1117        // 0 -> 4 (Compute -> Barrier)
1118        // 2 -> 3 (Overlapping Draw)
1119        // Potentials: Compute:1, BarrierDraw:2, ContinuationDraw:2
1120        // All categories have different potentials, so batching heuristic won't apply across categories.
1121        // Ready queue starts with [0(C), 2(CD)] -> Prio sort -> [0, 2]
1122        // 1. Pop 0. Result: [0]. Add 1, 4 to queue. Queue: [1(BD), 4(BD), 2(CD)]. Prio sort: [1,4,2]
1123        // 2. Pop 1. Result: [0, 1].
1124        // 3. Pop 4. Result: [0, 1, 4].
1125        // 4. Pop 2. Result: [0, 1, 4, 2]. Add 3 to queue. Queue: [3]
1126        // 5. Pop 3. Result: [0, 1, 4, 2, 3].
1127        let reordered = reorder_instructions(commands);
1128        let reordered_positions = get_positions(&reordered);
1129        let expected_positions = vec![
1130            original_positions[0],
1131            original_positions[1],
1132            original_positions[4],
1133            original_positions[2],
1134            original_positions[3],
1135        ];
1136        assert_eq!(reordered_positions, expected_positions);
1137    }
1138
1139    #[test]
1140    fn test_blur_batching_with_large_sampling_padding() {
1141        // Scenario: 5 orthogonal glass components with 50px blur radius
1142        // Each has 75px sampling padding and relies on component bounds for collisions
1143        // They should be able to batch together despite large sampling areas
1144
1145        let blur_commands = vec![
1146            create_cmd(
1147                PxPosition::new(Px(0), Px(0)),
1148                Some(BarrierRequirement::uniform_padding_local(Px(75))),
1149                true, // Compute command
1150            ),
1151            create_cmd(
1152                PxPosition::new(Px(200), Px(0)),
1153                Some(BarrierRequirement::uniform_padding_local(Px(75))),
1154                true,
1155            ),
1156            create_cmd(
1157                PxPosition::new(Px(400), Px(0)),
1158                Some(BarrierRequirement::uniform_padding_local(Px(75))),
1159                true,
1160            ),
1161            create_cmd(
1162                PxPosition::new(Px(600), Px(0)),
1163                Some(BarrierRequirement::uniform_padding_local(Px(75))),
1164                true,
1165            ),
1166            create_cmd(
1167                PxPosition::new(Px(800), Px(0)),
1168                Some(BarrierRequirement::uniform_padding_local(Px(75))),
1169                true,
1170            ),
1171        ];
1172
1173        let reordered = reorder_instructions(blur_commands);
1174
1175        // All compute commands should remain in order (no dependencies)
1176        // And they should all be consecutive (no reordering needed)
1177        let kinds: Vec<&'static str> = reordered
1178            .iter()
1179            .map(|(cmd, _, _, _)| match cmd {
1180                Command::Compute(_) => "C",
1181                Command::Draw(_) => "D",
1182                _ => panic!("unexpected command variant"),
1183            })
1184            .collect();
1185
1186        assert_eq!(kinds, vec!["C", "C", "C", "C", "C"]);
1187
1188        // Verify they are orthogonal based on component bounds (not sampling boxes)
1189        let positions = get_positions(&reordered);
1190        assert_eq!(positions.len(), 5);
1191
1192        // Each component is at x=0,200,400,600,800 with size 10x10
1193        // Component bounds are 10x10 (no padding), so they don't overlap
1194        // Sampling boxes would be 85x85 (10 + 75*2) centered on each position,
1195        // which would overlap, but that shouldn't prevent batching
1196    }
1197}