1use crate::{AccessPattern, DecodeCapability, Image, Node, NodeId, PixelsError, Region, Result};
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum TileShape {
24 Strip {
29 rows: u32,
31 },
32 Square {
37 size: u32,
39 },
40}
41
42impl TileShape {
43 pub const DEFAULT_STRIP_ROWS: u32 = 64;
45 pub const DEFAULT_SQUARE_SIZE: u32 = 128;
47
48 #[must_use]
53 pub fn tiles(self, region: Region) -> Vec<Region> {
54 if region.is_empty() {
55 return Vec::new();
56 }
57 let (tile_width, tile_height) = match self {
58 Self::Strip { rows } => (region.width, rows.max(1)),
59 Self::Square { size } => (size.max(1), size.max(1)),
60 };
61 let mut tiles = Vec::new();
62 let mut y = region.y;
63 while u64::from(y) < region.bottom() {
64 let height = tile_height.min((region.bottom() - u64::from(y)) as u32);
65 let mut x = region.x;
66 while u64::from(x) < region.right() {
67 let width = tile_width.min((region.right() - u64::from(x)) as u32);
68 tiles.push(Region::new(x, y, width, height));
69 x = x.saturating_add(width);
70 }
71 y = y.saturating_add(height);
72 }
73 tiles
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[non_exhaustive]
80pub struct NodePlan {
81 pub shape: TileShape,
83 pub materialize: bool,
90 pub cacheable: bool,
99}
100
101#[derive(Debug, Clone)]
103pub struct Plan {
104 nodes: HashMap<NodeId, NodePlan>,
105 output_tiles: Vec<Region>,
106 root: NodeId,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub struct PlanOptions {
113 pub strip_rows: u32,
115 pub square_size: u32,
117}
118
119impl PlanOptions {
120 #[must_use]
125 pub const fn with_strip_rows(mut self, rows: u32) -> Self {
126 self.strip_rows = rows;
127 self
128 }
129
130 #[must_use]
132 pub const fn with_square_size(mut self, size: u32) -> Self {
133 self.square_size = size;
134 self
135 }
136}
137
138impl Default for PlanOptions {
139 fn default() -> Self {
140 Self {
141 strip_rows: TileShape::DEFAULT_STRIP_ROWS,
142 square_size: TileShape::DEFAULT_SQUARE_SIZE,
143 }
144 }
145}
146
147impl Plan {
148 pub fn build(image: &Image, options: PlanOptions) -> Result<Self> {
157 let root = Arc::clone(image.node());
158 let order = topological_order(&root);
159
160 let spatial_consumers = spatial_consumers(&order);
164 let mut nodes = HashMap::with_capacity(order.len());
165 for node in &order {
166 let is_spatial = node
167 .op()
168 .is_some_and(|op| op.access_pattern() == AccessPattern::Spatial);
169 let feeds_spatial = spatial_consumers.contains(&node.id());
170 let shape = if is_spatial || feeds_spatial {
171 TileShape::Square {
172 size: options.square_size,
173 }
174 } else {
175 TileShape::Strip {
176 rows: options.strip_rows,
177 }
178 };
179 nodes.insert(
180 node.id(),
181 NodePlan {
182 shape,
183 materialize: false,
184 cacheable: false,
186 },
187 );
188 }
189
190 let root_shape = nodes
192 .get(&root.id())
193 .ok_or_else(|| PixelsError::graph("root node missing from plan"))?
194 .shape;
195 let output_tiles = root_shape.tiles(root.descriptor().region());
196
197 let demand = demand_sequences(&root, &output_tiles)?;
200
201 let fan_out = fan_out(&order);
205 for node in &order {
206 let shared = fan_out.get(&node.id()).copied().unwrap_or(0) > 1;
207 let overlapping = spatial_consumers.contains(&node.id());
208 if let Some(plan) = nodes.get_mut(&node.id()) {
209 plan.cacheable = shared || overlapping;
210 }
211 }
212
213 let forward_only = forward_only_nodes(&order);
216 for node in &order {
217 let Some(regions) = demand.get(&node.id()) else {
218 continue;
219 };
220 if is_forward_monotonic(regions) || !forward_only.contains(&node.id()) {
221 continue;
222 }
223 if let Some(plan) = nodes.get_mut(&node.id()) {
224 plan.materialize = true;
225 }
226 }
227
228 Ok(Self {
229 nodes,
230 output_tiles,
231 root: root.id(),
232 })
233 }
234
235 #[must_use]
237 pub fn node(&self, id: NodeId) -> Option<NodePlan> {
238 self.nodes.get(&id).copied()
239 }
240
241 #[must_use]
243 pub fn output_tiles(&self) -> &[Region] {
244 &self.output_tiles
245 }
246
247 #[must_use]
249 pub const fn root(&self) -> NodeId {
250 self.root
251 }
252
253 #[must_use]
255 pub fn len(&self) -> usize {
256 self.nodes.len()
257 }
258
259 #[must_use]
261 pub fn is_empty(&self) -> bool {
262 self.nodes.is_empty()
263 }
264
265 #[must_use]
271 pub fn materializes(&self) -> bool {
272 self.nodes.values().any(|plan| plan.materialize)
273 }
274}
275
276fn topological_order(root: &Arc<Node>) -> Vec<Arc<Node>> {
278 enum Step {
280 Visit(Arc<Node>),
281 Emit(Arc<Node>),
282 }
283 let mut seen = HashSet::new();
284 let mut order = Vec::new();
285 let mut stack = vec![Step::Visit(Arc::clone(root))];
286 while let Some(step) = stack.pop() {
287 match step {
288 Step::Visit(node) => {
289 if seen.contains(&node.id()) {
290 continue;
291 }
292 stack.push(Step::Emit(Arc::clone(&node)));
293 for input in node.inputs() {
294 stack.push(Step::Visit(Arc::clone(input)));
295 }
296 }
297 Step::Emit(node) => {
298 if seen.insert(node.id()) {
299 order.push(node);
300 }
301 }
302 }
303 }
304 order
305}
306
307fn fan_out(order: &[Arc<Node>]) -> HashMap<NodeId, usize> {
309 let mut counts: HashMap<NodeId, usize> = HashMap::new();
310 for node in order {
311 for input in node.inputs() {
312 *counts.entry(input.id()).or_default() += 1;
313 }
314 }
315 counts
316}
317
318fn spatial_consumers(order: &[Arc<Node>]) -> HashSet<NodeId> {
320 let mut feeding = HashSet::new();
321 for node in order {
322 let spatial = node
323 .op()
324 .is_some_and(|op| op.access_pattern() == AccessPattern::Spatial);
325 if spatial {
326 for input in node.inputs() {
327 feeding.insert(input.id());
328 }
329 }
330 }
331 feeding
332}
333
334fn forward_only_nodes(order: &[Arc<Node>]) -> HashSet<NodeId> {
339 let mut forward_only = HashSet::new();
340 for node in order {
342 let constrained = match node.producer() {
343 Some(producer) => producer.capability() == DecodeCapability::Sequential,
344 None => node
345 .inputs()
346 .iter()
347 .any(|input| forward_only.contains(&input.id())),
348 };
349 if constrained {
350 forward_only.insert(node.id());
351 }
352 }
353 forward_only
354}
355
356fn demand_sequences(
358 root: &Arc<Node>,
359 output_tiles: &[Region],
360) -> Result<HashMap<NodeId, Vec<Region>>> {
361 let mut sequences: HashMap<NodeId, Vec<Region>> = HashMap::new();
362 for tile in output_tiles {
363 let mut stack = vec![(Arc::clone(root), *tile)];
364 while let Some((node, region)) = stack.pop() {
365 sequences.entry(node.id()).or_default().push(region);
366 let Some(op) = node.op() else { continue };
367 let descriptors: Vec<_> = node.inputs().iter().map(|n| n.descriptor()).collect();
368 let requested = op.input_regions(region, &descriptors)?;
369 if requested.len() != node.inputs().len() {
370 return Err(PixelsError::graph(format!(
371 "op `{}` requested {} input region(s) for {} input(s)",
372 op.name(),
373 requested.len(),
374 node.inputs().len()
375 )));
376 }
377 for (input, region) in node.inputs().iter().zip(requested) {
378 stack.push((Arc::clone(input), region));
379 }
380 }
381 }
382 Ok(sequences)
383}
384
385fn is_forward_monotonic(regions: &[Region]) -> bool {
391 regions.windows(2).all(|pair| {
392 let (Some(previous), Some(next)) = (pair.first(), pair.get(1)) else {
393 return true;
394 };
395 next.y >= previous.y
396 })
397}
398
399#[cfg(test)]
400#[allow(
401 clippy::unwrap_used,
402 clippy::indexing_slicing,
403 clippy::panic,
404 reason = "tests operate on known-good values and assert shapes directly"
405)]
406mod tests {
407 use super::*;
408 use crate::testing::{ConstantOp, CountingProducer};
409 use crate::{
410 BufferSource, Format, ImageDescriptor, Op, PixelFormat, Producer, Tile, TileBuf, TileMut,
411 };
412
413 #[derive(Debug)]
415 struct Source {
416 descriptor: ImageDescriptor,
417 capability: DecodeCapability,
418 }
419
420 impl Producer for Source {
421 fn name(&self) -> &'static str {
422 "test-source"
423 }
424 fn descriptor(&self) -> ImageDescriptor {
425 self.descriptor
426 }
427 fn capability(&self) -> DecodeCapability {
428 self.capability
429 }
430 fn produce(&self, _: Region, _: &mut TileMut<'_>) -> Result<()> {
431 Ok(())
432 }
433 }
434
435 fn image(width: u32, height: u32, capability: DecodeCapability) -> Image {
436 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
437 Image::from_producer(
438 Arc::new(Source {
439 descriptor,
440 capability,
441 }),
442 Format::Raw,
443 )
444 }
445
446 #[derive(Debug)]
448 struct Reverse;
449 impl Op for Reverse {
450 fn name(&self) -> &'static str {
451 "reverse"
452 }
453 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
454 inputs
455 .first()
456 .copied()
457 .ok_or_else(|| PixelsError::graph("no input"))
458 }
459 fn input_regions(&self, out: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
460 let input = inputs
461 .first()
462 .ok_or_else(|| PixelsError::graph("no input"))?;
463 let y = (u64::from(input.height) - out.bottom()) as u32;
464 Ok(vec![Region::new(out.x, y, out.width, out.height)])
465 }
466 fn compute(&self, _: &[Tile<'_>], _: &mut TileMut<'_>) -> Result<()> {
467 Ok(())
468 }
469 }
470
471 #[derive(Debug)]
473 struct Blur;
474 impl Op for Blur {
475 fn name(&self) -> &'static str {
476 "blur"
477 }
478 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
479 inputs
480 .first()
481 .copied()
482 .ok_or_else(|| PixelsError::graph("no input"))
483 }
484 fn input_regions(&self, out: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
485 let input = inputs
486 .first()
487 .ok_or_else(|| PixelsError::graph("no input"))?;
488 Ok(vec![Region::new(
490 out.x.saturating_sub(1),
491 out.y.saturating_sub(1),
492 (out.width + 2).min(input.width),
493 (out.height + 2).min(input.height),
494 )])
495 }
496 fn access_pattern(&self) -> AccessPattern {
497 AccessPattern::Spatial
498 }
499 fn compute(&self, _: &[Tile<'_>], _: &mut TileMut<'_>) -> Result<()> {
500 Ok(())
501 }
502 }
503
504 #[test]
507 fn strips_cover_the_region_exactly_once() {
508 let region = Region::from_size(10, 25);
509 let tiles = TileShape::Strip { rows: 10 }.tiles(region);
510 assert_eq!(tiles.len(), 3);
511 assert_eq!(tiles[0], Region::new(0, 0, 10, 10));
512 assert_eq!(tiles[1], Region::new(0, 10, 10, 10));
513 assert_eq!(tiles[2], Region::new(0, 20, 10, 5), "final strip is short");
514 assert_eq!(
515 tiles.iter().map(|t| t.pixel_count()).sum::<u64>(),
516 region.pixel_count()
517 );
518 }
519
520 #[test]
521 fn squares_cover_the_region_exactly_once() {
522 let region = Region::from_size(10, 10);
523 let tiles = TileShape::Square { size: 4 }.tiles(region);
524 assert_eq!(tiles.len(), 9, "3x3 grid of tiles over a 10x10 image");
525 assert_eq!(tiles[0], Region::new(0, 0, 4, 4));
526 assert_eq!(tiles[2], Region::new(8, 0, 2, 4), "right edge is narrow");
527 assert_eq!(tiles[8], Region::new(8, 8, 2, 2), "corner is small");
528 assert_eq!(
529 tiles.iter().map(|t| t.pixel_count()).sum::<u64>(),
530 region.pixel_count()
531 );
532 }
533
534 #[test]
535 fn tiles_are_produced_top_to_bottom() {
536 let tiles = TileShape::Square { size: 4 }.tiles(Region::from_size(8, 8));
537 let ys: Vec<u32> = tiles.iter().map(|t| t.y).collect();
538 assert!(
539 ys.windows(2).all(|w| w[0] <= w[1]),
540 "rows must not go backwards"
541 );
542 }
543
544 #[test]
545 fn splitting_handles_degenerate_inputs() {
546 assert!(TileShape::Strip { rows: 4 }.tiles(Region::EMPTY).is_empty());
547 assert_eq!(
549 TileShape::Strip { rows: 0 }
550 .tiles(Region::from_size(2, 2))
551 .len(),
552 2
553 );
554 assert_eq!(
555 TileShape::Square { size: 0 }
556 .tiles(Region::from_size(2, 2))
557 .len(),
558 4
559 );
560 assert_eq!(
562 TileShape::Strip { rows: 999 }
563 .tiles(Region::from_size(4, 4))
564 .len(),
565 1
566 );
567 }
568
569 #[test]
572 fn a_sequential_pipeline_moves_strips() {
573 let image = image(64, 64, DecodeCapability::Regions)
574 .apply(Arc::new(ConstantOp::new(1)))
575 .unwrap();
576 let plan = Plan::build(&image, PlanOptions::default()).unwrap();
577 for node in [image.node().id(), image.node().inputs()[0].id()] {
578 assert!(
579 matches!(plan.node(node).unwrap().shape, TileShape::Strip { .. }),
580 "sequential nodes should move strips"
581 );
582 }
583 }
584
585 #[test]
586 fn a_spatial_op_switches_its_segment_to_squares() {
587 let source = image(64, 64, DecodeCapability::Regions);
589 let blurred = source.apply(Arc::new(Blur)).unwrap();
590 let plan = Plan::build(&blurred, PlanOptions::default()).unwrap();
591 assert!(matches!(
592 plan.node(blurred.node().id()).unwrap().shape,
593 TileShape::Square { .. }
594 ));
595 assert!(
596 matches!(
597 plan.node(source.node().id()).unwrap().shape,
598 TileShape::Square { .. }
599 ),
600 "the node feeding a spatial op must deliver squares"
601 );
602 }
603
604 #[test]
605 fn sequential_nodes_above_a_spatial_op_stay_strips() {
606 let source = image(64, 64, DecodeCapability::Regions);
608 let blurred = source.apply(Arc::new(Blur)).unwrap();
609 let after = blurred.apply(Arc::new(ConstantOp::new(1))).unwrap();
610 let plan = Plan::build(&after, PlanOptions::default()).unwrap();
611 assert!(
612 matches!(
613 plan.node(after.node().id()).unwrap().shape,
614 TileShape::Strip { .. }
615 ),
616 "a sequential consumer keeps strips"
617 );
618 }
619
620 #[test]
621 fn plan_options_choose_the_tile_dimensions() {
622 let image = image(100, 100, DecodeCapability::Regions);
623 let options = PlanOptions {
624 strip_rows: 25,
625 square_size: 10,
626 };
627 let plan = Plan::build(&image, options).unwrap();
628 assert_eq!(
629 plan.node(image.node().id()).unwrap().shape,
630 TileShape::Strip { rows: 25 }
631 );
632 assert_eq!(plan.output_tiles().len(), 4);
633 }
634
635 #[test]
638 fn a_forward_pipeline_never_materializes() {
639 let image = image(256, 256, DecodeCapability::Sequential)
640 .apply(Arc::new(ConstantOp::new(1)))
641 .unwrap();
642 let plan = Plan::build(&image, PlanOptions::default()).unwrap();
643 assert!(!plan.materializes(), "forward demand needs no buffer");
644 }
645
646 #[test]
647 fn reversal_over_a_sequential_source_materializes() {
648 let source = image(256, 256, DecodeCapability::Sequential);
650 let reversed = source.apply(Arc::new(Reverse)).unwrap();
651 let plan = Plan::build(&reversed, PlanOptions::default()).unwrap();
652 assert!(
653 plan.materializes(),
654 "reversal over a forward-only source must buffer"
655 );
656 assert!(
657 plan.node(source.node().id()).unwrap().materialize,
658 "the buffer belongs at the source being read backwards"
659 );
660 assert!(
661 !plan.node(reversed.node().id()).unwrap().materialize,
662 "the reversing node itself is pulled in output order"
663 );
664 }
665
666 #[test]
667 fn reversal_over_a_random_access_source_streams() {
668 let source = image(256, 256, DecodeCapability::Regions);
671 let reversed = source.apply(Arc::new(Reverse)).unwrap();
672 let plan = Plan::build(&reversed, PlanOptions::default()).unwrap();
673 assert!(
674 !plan.materializes(),
675 "a random-access source serves bands in any order"
676 );
677 }
678
679 #[test]
680 fn a_memory_buffer_source_streams_under_reversal() {
681 let descriptor = ImageDescriptor::new(32, 32, PixelFormat::Gray8).unwrap();
683 let buffer = Arc::new(TileBuf::for_image(&descriptor).unwrap());
684 let source = BufferSource::new(descriptor, buffer).unwrap();
685 let image = Image::from_producer(Arc::new(source), Format::Raw)
686 .apply(Arc::new(Reverse))
687 .unwrap();
688 let plan = Plan::build(&image, PlanOptions::default()).unwrap();
689 assert!(!plan.materializes());
690 }
691
692 #[test]
693 fn the_constraint_propagates_down_a_chain() {
694 let source = image(128, 128, DecodeCapability::Sequential);
696 let image = source
697 .apply(Arc::new(ConstantOp::new(1)))
698 .unwrap()
699 .apply(Arc::new(ConstantOp::new(2)))
700 .unwrap()
701 .apply(Arc::new(Reverse))
702 .unwrap();
703 let plan = Plan::build(&image, PlanOptions::default()).unwrap();
704 assert!(plan.materializes());
705 }
706
707 #[test]
708 fn a_single_tile_pipeline_is_trivially_forward() {
709 let source = image(8, 8, DecodeCapability::Sequential);
711 let reversed = source.apply(Arc::new(Reverse)).unwrap();
712 let options = PlanOptions {
713 strip_rows: 64,
714 square_size: 128,
715 };
716 let plan = Plan::build(&reversed, options).unwrap();
717 assert_eq!(plan.output_tiles().len(), 1);
718 assert!(
719 !plan.materializes(),
720 "a single whole-image tile never reverses"
721 );
722 }
723
724 #[test]
725 fn monotonicity_allows_repeats_and_overlap() {
726 assert!(is_forward_monotonic(&[]));
728 assert!(is_forward_monotonic(&[Region::new(0, 0, 4, 4)]));
729 assert!(is_forward_monotonic(&[
730 Region::new(0, 0, 4, 4),
731 Region::new(0, 4, 4, 4)
732 ]));
733 assert!(is_forward_monotonic(&[
734 Region::new(0, 0, 4, 4),
735 Region::new(0, 0, 4, 4)
736 ]));
737 assert!(is_forward_monotonic(&[
738 Region::new(0, 0, 4, 8),
739 Region::new(0, 4, 4, 8)
740 ]));
741 assert!(!is_forward_monotonic(&[
742 Region::new(0, 4, 4, 4),
743 Region::new(0, 0, 4, 4)
744 ]));
745 }
746
747 #[test]
750 fn a_linear_pipeline_caches_nothing() {
751 let source = image(64, 64, DecodeCapability::Regions);
755 let a = source.apply(Arc::new(ConstantOp::new(1))).unwrap();
756 let b = a.apply(Arc::new(ConstantOp::new(2))).unwrap();
757 let plan = Plan::build(&b, PlanOptions::default()).unwrap();
758 for node in [source.node().id(), a.node().id(), b.node().id()] {
759 assert!(
760 !plan.node(node).unwrap().cacheable,
761 "a linear chain cached a tile"
762 );
763 }
764 }
765
766 #[test]
767 fn a_shared_prefix_is_cacheable() {
768 let source = image(32, 32, DecodeCapability::Regions);
771 let base = source.apply(Arc::new(ConstantOp::new(1))).unwrap();
772 let left = base.apply(Arc::new(ConstantOp::new(2))).unwrap();
773 let right = base.apply(Arc::new(ConstantOp::new(3))).unwrap();
774 let joined =
775 Image::combine(&[left.clone(), right], Arc::new(crate::testing::SumOp)).unwrap();
776 let plan = Plan::build(&joined, PlanOptions::default()).unwrap();
777 assert!(
778 plan.node(base.node().id()).unwrap().cacheable,
779 "shared prefix not cached"
780 );
781 assert!(
782 !plan.node(left.node().id()).unwrap().cacheable,
783 "single consumer cached"
784 );
785 assert!(
786 !plan.node(joined.node().id()).unwrap().cacheable,
787 "the root has no consumer"
788 );
789 }
790
791 #[test]
792 fn a_node_feeding_a_spatial_op_is_cacheable() {
793 let source = image(64, 64, DecodeCapability::Regions);
796 let blurred = source.apply(Arc::new(Blur)).unwrap();
797 let plan = Plan::build(&blurred, PlanOptions::default()).unwrap();
798 assert!(plan.node(source.node().id()).unwrap().cacheable);
799 }
800
801 #[test]
802 fn the_plan_covers_every_node_once() {
803 let source = image(32, 32, DecodeCapability::Regions);
804 let a = source.apply(Arc::new(ConstantOp::new(1))).unwrap();
805 let b = a.apply(Arc::new(ConstantOp::new(2))).unwrap();
806 let plan = Plan::build(&b, PlanOptions::default()).unwrap();
807 assert_eq!(plan.len(), 3);
808 assert!(!plan.is_empty());
809 assert_eq!(plan.root(), b.node().id());
810 assert!(plan.node(source.node().id()).is_some());
811 }
812
813 #[test]
814 fn a_shared_prefix_is_planned_once() {
815 let source = image(32, 32, DecodeCapability::Regions);
816 let base = source.apply(Arc::new(ConstantOp::new(1))).unwrap();
817 let left = base.apply(Arc::new(ConstantOp::new(2))).unwrap();
818 let right = base.apply(Arc::new(ConstantOp::new(3))).unwrap();
819 let joined = Image::combine(&[left, right], Arc::new(crate::testing::SumOp)).unwrap();
820 let plan = Plan::build(&joined, PlanOptions::default()).unwrap();
821 assert_eq!(plan.len(), 5);
823 }
824
825 #[test]
826 fn output_tiles_tile_the_whole_image() {
827 let image = image(100, 70, DecodeCapability::Regions);
828 let plan = Plan::build(
829 &image,
830 PlanOptions {
831 strip_rows: 16,
832 square_size: 128,
833 },
834 )
835 .unwrap();
836 let covered: u64 = plan.output_tiles().iter().map(|t| t.pixel_count()).sum();
837 assert_eq!(covered, 100 * 70);
838 assert_eq!(plan.output_tiles().len(), 5, "70 rows in strips of 16");
839 }
840
841 #[test]
842 fn planning_touches_no_pixels() {
843 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Gray8).unwrap();
844 let producer = Arc::new(CountingProducer::new(descriptor));
845 let image = Image::from_producer(Arc::clone(&producer) as Arc<dyn Producer>, Format::Raw)
846 .apply(Arc::new(ConstantOp::new(1)))
847 .unwrap();
848 let _plan = Plan::build(&image, PlanOptions::default()).unwrap();
849 assert_eq!(producer.produce_calls(), 0, "planning is pure analysis");
850 }
851
852 #[test]
853 fn an_op_rejecting_a_planned_region_fails_the_plan() {
854 #[derive(Debug)]
856 struct Refuses;
857 impl Op for Refuses {
858 fn name(&self) -> &'static str {
859 "refuses"
860 }
861 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
862 inputs
863 .first()
864 .copied()
865 .ok_or_else(|| PixelsError::graph("no input"))
866 }
867 fn input_regions(&self, _: Region, _: &[ImageDescriptor]) -> Result<Vec<Region>> {
868 Err(PixelsError::invalid_argument("output", "never satisfiable"))
869 }
870 fn compute(&self, _: &[Tile<'_>], _: &mut TileMut<'_>) -> Result<()> {
871 Ok(())
872 }
873 }
874 let image = image(32, 32, DecodeCapability::Regions)
875 .apply(Arc::new(Refuses))
876 .unwrap();
877 let err = Plan::build(&image, PlanOptions::default()).unwrap_err();
878 assert_eq!(err.code(), crate::ErrorCode::InvalidArgument);
879 }
880}