otf_pixels_core/op.rs
1//! The operation trait and the pixel producers that feed graphs.
2
3use crate::{ImageDescriptor, Region, Result, Tile, TileMut};
4use core::fmt;
5
6/// The tile **shape** an op wants its input delivered in.
7///
8/// The scheduler negotiates tile shapes from this declaration (ADR-0003):
9/// runs of [`Sequential`] ops move full-width strips, matching how codecs
10/// produce and consume rows, while [`Spatial`] segments switch to square tiles
11/// to bound redundant border work. A rolling line-cache is inserted at the
12/// seam between the two. Declaring [`Spatial`] when [`Sequential`] would do
13/// costs throughput; declaring [`Sequential`] when the op actually reads
14/// neighbours is a correctness bug.
15///
16/// # Shape, not order
17///
18/// This says nothing about the *order* tiles are produced in. An op that
19/// mirrors vertically reads no neighbours and wants full-width strips, so it
20/// is [`Sequential`] — even though it consumes those strips bottom-up.
21///
22/// Order is not declared at all: the scheduler derives it from
23/// [`Op::input_regions`] and inserts a materialization buffer only where a
24/// non-forward demand sequence meets a forward-only source (ADR-0009). Keeping
25/// order out of this enum is deliberate — it is a property of the *seam*
26/// between an op and its upstream, not of the op, so the same op streams or
27/// buffers depending on what it is connected to.
28///
29/// [`Sequential`]: AccessPattern::Sequential
30/// [`Spatial`]: AccessPattern::Spatial
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum AccessPattern {
34 /// Each output pixel depends on input pixels from a single row.
35 ///
36 /// Wants full-width strips. Pointwise ops (`modulate`, `flatten`, channel
37 /// extraction) and row-preserving geometry remaps (`crop`, `flop`, `flip`)
38 /// are sequential — including `flip`, which reverses row *order* but still
39 /// reads one input row per output row.
40 Sequential,
41 /// Output pixels depend on a neighbourhood spanning multiple input rows.
42 ///
43 /// Wants square tiles. Convolution and resize with filter support are
44 /// spatial.
45 Spatial,
46}
47
48/// A node in the op graph.
49///
50/// Ops are immutable, shared (`Arc<dyn Op>`), and evaluated concurrently, hence
51/// `Send + Sync`. An op holds its own parameters; the graph supplies the input
52/// descriptors, which is what lets one op instance be evaluated against
53/// different input shapes.
54///
55/// # Relationship to ARCHITECTURE §Layer 3
56///
57/// [`Op::input_regions`] and [`Op::output_descriptor`] take the input
58/// descriptors explicitly, where ARCHITECTURE writes them in shorthand as
59/// `input_region(out_region)` and `output_descriptor()`. The semantics are
60/// unchanged — an op still cannot know its input shapes without being told
61/// them, and passing them keeps ops free of duplicated graph state.
62pub trait Op: Send + Sync + fmt::Debug {
63 /// A short, stable name for this op, used in diagnostics.
64 fn name(&self) -> &'static str;
65
66 /// The number of inputs this op consumes.
67 fn arity(&self) -> usize {
68 1
69 }
70
71 /// Compute the output shape from the input shapes.
72 ///
73 /// This runs at **graph-build time**, not evaluation time: descriptors flow
74 /// forward as the graph is chained, which is what makes
75 /// [`Image::metadata`] free of pixel work (SPEC §Guarantees 3).
76 ///
77 /// [`Image::metadata`]: crate::Image::metadata
78 ///
79 /// # Errors
80 ///
81 /// Returns [`PixelsError::InvalidArgument`] if the op's parameters are
82 /// incompatible with these inputs (for example a crop outside the image),
83 /// or [`PixelsError::Unsupported`] if the op cannot handle the input pixel
84 /// format.
85 ///
86 /// [`PixelsError::InvalidArgument`]: crate::PixelsError::InvalidArgument
87 /// [`PixelsError::Unsupported`]: crate::PixelsError::Unsupported
88 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor>;
89
90 /// A copy of this op fit for a graph rebuilt at a **reduced input
91 /// resolution**, or `None` if this op must not be.
92 ///
93 /// This is what licenses shrink-on-load: a JPEG can be decoded at 1/8 and
94 /// fed to `resize` with the same result. Returning `Some` asserts two
95 /// separate things, which is why they are one method — an op that got only
96 /// the first right would be silently wrong:
97 ///
98 /// 1. **The op means the same thing** against a smaller input. Three kinds
99 /// do not, and all three still produce a correctly-shaped image:
100 /// ops carrying coordinates in input pixels (`crop(1000, 1000, ..)`
101 /// names a different part of a source eight times smaller), ops
102 /// carrying a distance in input pixels (a 3x3 convolution over a 1/8
103 /// decode is eight times the blur relative to content), and ops whose
104 /// second input is a separate image that would not be reduced with it.
105 /// 2. **The returned instance carries no state bound to the old input.**
106 /// Ops may memoize tables keyed to the shape they first saw — `resize`
107 /// builds its filter weights that way — and reusing such an instance
108 /// against a new shape is at best an error and at worst a resample
109 /// against the wrong scale.
110 ///
111 /// The default is `None`, so an op is presumed unsafe until it says
112 /// otherwise. Declaring it wrongly does not corrupt memory or change the
113 /// output *shape*; it silently changes the picture, which is worse, so the
114 /// conservative default is the right one.
115 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
116 None
117 }
118
119 /// The input regions needed to produce `output`.
120 ///
121 /// This is the inverse mapping that demand propagation walks backwards
122 /// (ARCHITECTURE §Layer 4). The returned vector has one region per input,
123 /// in input order. A pointwise op returns `output` unchanged; a 5×5
124 /// convolution returns it grown by 2px; a resize returns the scaled region
125 /// plus filter support.
126 ///
127 /// Returned regions must be clamped to their input's bounds — an op asking
128 /// for pixels outside its input is a defect, and edge handling
129 /// (clamp, reflect, …) is the op's own business.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if `output` is not a region this op can produce.
134 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>>;
135
136 /// How this op reads its inputs; drives tile negotiation (ADR-0003).
137 ///
138 /// Defaults to [`AccessPattern::Sequential`], the safe-and-fast case for
139 /// pointwise ops. Override it for anything reading a neighbourhood.
140 fn access_pattern(&self) -> AccessPattern {
141 AccessPattern::Sequential
142 }
143
144 /// Fill `output` from `inputs` — the kernel entry point.
145 ///
146 /// `inputs` holds one tile per input, covering exactly the regions
147 /// [`Op::input_regions`] asked for. `output` covers the region that was
148 /// passed to it.
149 ///
150 /// Per ADR-0002 the pixel format is a runtime value here: dispatch **once**
151 /// on it into a monomorphized kernel (see [`dispatch_sample!`]) rather than
152 /// branching per pixel.
153 ///
154 /// [`dispatch_sample!`]: crate::dispatch_sample
155 ///
156 /// # Errors
157 ///
158 /// Returns an error if the supplied tiles do not match what
159 /// [`Op::input_regions`] requested, or if the pixel format is one this op
160 /// does not implement.
161 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()>;
162}
163
164/// A source of pixels at the root of a graph.
165///
166/// Producers sit where decoders meet the graph. `produce` takes `&self` because
167/// graph nodes are shared across threads; a producer wrapping a single-pass
168/// streaming decoder therefore owns whatever interior mutability it needs (see
169/// [`DecodedSource`]).
170///
171/// [`DecodedSource`]: crate::DecodedSource
172pub trait Producer: Send + Sync + fmt::Debug {
173 /// A short, stable name for this producer, used in diagnostics.
174 fn name(&self) -> &'static str;
175
176 /// The shape of the image this producer yields.
177 ///
178 /// Known from the header alone; answering this must not decode pixels.
179 fn descriptor(&self) -> ImageDescriptor;
180
181 /// Whether this producer can serve arbitrary regions, or only forward ones.
182 ///
183 /// This is the upstream half of ADR-0009's seam analysis: a producer that
184 /// can only go forward forces the scheduler to materialize whenever demand
185 /// is not forward-monotonic, while one serving arbitrary regions lets the
186 /// same pipeline stream.
187 ///
188 /// Defaults to [`DecodeCapability::Sequential`], the conservative answer —
189 /// over-declaring it costs a buffer, under-declaring it is a correctness
190 /// bug.
191 ///
192 /// [`DecodeCapability::Sequential`]: crate::DecodeCapability::Sequential
193 fn capability(&self) -> crate::DecodeCapability {
194 crate::DecodeCapability::Sequential
195 }
196
197 /// Fill `output` with the pixels of `region`.
198 ///
199 /// `region` is always within `descriptor().region()`.
200 ///
201 /// # Errors
202 ///
203 /// Returns [`PixelsError::Malformed`] on invalid input bytes,
204 /// [`PixelsError::Io`] on source failure, or
205 /// [`PixelsError::InvalidArgument`] if `output` does not cover `region`.
206 ///
207 /// [`PixelsError::Malformed`]: crate::PixelsError::Malformed
208 /// [`PixelsError::Io`]: crate::PixelsError::Io
209 /// [`PixelsError::InvalidArgument`]: crate::PixelsError::InvalidArgument
210 fn produce(&self, region: Region, output: &mut TileMut<'_>) -> Result<()>;
211
212 /// What this producer would emit if asked for `target` or larger, when it
213 /// can reach that size more cheaply than by producing full resolution.
214 ///
215 /// **Pure**: nothing is committed, and calling this must not change what
216 /// the producer subsequently emits. The planner asks first, checks the
217 /// whole graph still holds, and only then calls [`Producer::reduce_to`] —
218 /// so a producer that reduced itself here would corrupt pipelines the
219 /// planner went on to reject.
220 ///
221 /// The returned descriptor is never smaller than `target` in either axis:
222 /// decoding below the requested size and enlarging afterwards would
223 /// discard detail and then invent it back.
224 ///
225 /// `None` — the default — means this producer has only one resolution.
226 fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
227 let _ = target;
228 None
229 }
230
231 /// Commit to emitting `descriptor`, which [`Producer::reduced_descriptor`]
232 /// must have returned.
233 ///
234 /// # Errors
235 ///
236 /// Returns [`PixelsError::Unsupported`] if this producer cannot reduce, or
237 /// [`PixelsError::InvalidArgument`] if pixels have already been produced —
238 /// the resolution is fixed from the first [`Producer::produce`] onward,
239 /// because rows already emitted cannot be retracted.
240 ///
241 /// [`PixelsError::Unsupported`]: crate::PixelsError::Unsupported
242 /// [`PixelsError::InvalidArgument`]: crate::PixelsError::InvalidArgument
243 fn reduce_to(&self, descriptor: ImageDescriptor) -> Result<()> {
244 let _ = descriptor;
245 Err(crate::PixelsError::unsupported(format!(
246 "producer `{}` has only one resolution",
247 self.name()
248 )))
249 }
250}