Skip to main content

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}