Skip to main content

zencodec/
operation.rs

1//! Stored operation requirements, checked automatically when executed.
2//!
3//! These requests are reusable configuration. Building one performs no codec
4//! work. Execution uses the registered template and checked job primitives;
5//! required policies and fidelity targets cannot fall back to legacy setters.
6//! Pixel conversion is an explicit pipeline operation, not an implicit retry.
7
8use crate::{
9    At, CodecSet, CodecSetError, DecodeOutput, DecodePolicy, EncodeOutput, EncodePolicy,
10    ImageFormat, encode::Fidelity,
11};
12use alloc::borrow::Cow;
13use alloc::boxed::Box;
14use alloc::vec::Vec;
15use core::fmt;
16use zenpixels::{PixelDescriptor, PixelSlice};
17
18mod binding;
19mod collection;
20mod conversion;
21mod decoding;
22mod encoding_route;
23mod frame;
24mod image;
25mod policy;
26mod scheduling;
27mod transcode;
28mod transition;
29pub use binding::{BoundPolicy, OperationPolicy};
30pub use collection::{CheckedCollection, CollectionOperationError, CollectionStage};
31pub use conversion::{ImageConversionOffer, resolve_image_conversion};
32pub use encoding_route::{
33    EncodeEvidence, EncodeRouteFacts, EncodeRouteOffer, InputAcquisition, ReplayEvidence,
34    resolve_encode_route,
35};
36pub use frame::{FrameFacts, PublicationOffer, resolve_frame};
37pub use image::{ImageFacts, resolve_image};
38pub use policy::{
39    BoundaryOffer, Choice, ChoiceId, Choices, Decision, DecisionCode, DecisionContext,
40    DecisionRecord, Decline, EncodeFacts, EncodeOffer, OperationReport, PolicyFailure,
41    PolicyRequirement, PolicyResolver, PolicySet, Question, RequirementEvidence, Selection,
42    UnmetRequirements, resolve_boundary,
43};
44pub use scheduling::{
45    OutputSchedule, OutputScheduleFacts, OutputScheduleOffer, OutputSource, resolve_output_schedule,
46};
47pub use transcode::{MetadataTransfer, TranscodeOffer, resolve_transcode};
48pub use transition::{
49    DestinationFacts, FrameConversionOffer, FrameRoute, FrameRouteFacts, FrameRouteOffer,
50    FrameRouteState, StagingFacts, StagingKind, resolve_frame_route,
51};
52
53/// Successful output and the decisions that selected its permitted contract.
54/// Dereferences to the ordinary codec output; reports never replace errors for
55/// a violated requirement.
56#[derive(Debug)]
57pub struct Outcome<T> {
58    output: T,
59    report: OperationReport,
60}
61impl<T> Outcome<T> {
62    /// Assemble an adapter's completed output and bounded decision record.
63    /// This container does not independently validate the adapter's postconditions.
64    pub const fn from_parts(output: T, report: OperationReport) -> Self {
65        Self { output, report }
66    }
67    /// Inspect the ordinary codec output.
68    pub const fn output(&self) -> &T {
69        &self.output
70    }
71    /// Take the ordinary codec output, discarding its decision report.
72    pub fn into_output(self) -> T {
73        self.output
74    }
75    /// Take both the ordinary output and its decision report.
76    pub fn into_parts(self) -> (T, OperationReport) {
77        (self.output, self.report)
78    }
79    /// Decisions taken before producing this output.
80    pub const fn report(&self) -> &OperationReport {
81        &self.report
82    }
83}
84impl<T> core::ops::Deref for Outcome<T> {
85    type Target = T;
86    fn deref(&self) -> &T {
87        &self.output
88    }
89}
90
91/// Why one permitted fidelity route could not be prepared. No encode was run.
92#[derive(Debug)]
93pub struct RouteRejection {
94    /// Index in the original preference list.
95    pub preference_index: usize,
96    /// Requested fidelity mode or target.
97    pub fidelity: Option<Fidelity>,
98    /// The native configuration refusal.
99    pub cause: CodecSetError,
100}
101
102/// A checked operation failure. Native causes remain in the source chain.
103#[derive(Debug)]
104#[non_exhaustive]
105pub enum OperationError {
106    /// Codec registration, configuration or execution failed.
107    Codec(CodecSetError),
108    /// The stored preference list is empty or exceeds the bounded planner.
109    AlternativeCount {
110        /// Supplied number of alternatives.
111        count: usize,
112        /// Maximum alternatives accepted by this implementation.
113        limit: usize,
114    },
115    /// A malformed target is an invalid request, not an excuse to choose another.
116    InvalidFidelity {
117        /// Index of the invalid target.
118        preference_index: usize,
119        /// Target validation failure.
120        cause: crate::encode::FidelityTargetError,
121    },
122    /// No permitted encoder route passed configuration validation.
123    NoEncodeRoute(Vec<RouteRejection>),
124    /// Bounded planner storage could not be reserved.
125    Allocation,
126    /// Cancellation observed outside an application policy question.
127    Stopped(enough::StopReason),
128    /// Policy callback, decline, invalid choice or cancellation failure.
129    Policy(PolicyFailure),
130    /// The selected implementation failed; its decision report survives.
131    Execution {
132        /// Native codec failure.
133        cause: Box<CodecSetError>,
134        /// Decisions that preceded this failure.
135        report: Box<OperationReport>,
136    },
137    /// The decoder could not produce the required descriptor without conversion.
138    OutputDescriptor {
139        /// Required output interpretation and layout.
140        requested: PixelDescriptor,
141        /// The decoder's actual output interpretation and layout.
142        actual: PixelDescriptor,
143    },
144}
145
146impl fmt::Display for OperationError {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            Self::Codec(e) => write!(f, "operation: {e}"),
150            Self::AlternativeCount { count, limit } => {
151                write!(f, "request has {count} alternatives; expected 1..={limit}")
152            }
153            Self::InvalidFidelity {
154                preference_index,
155                cause,
156            } => write!(f, "fidelity alternative {preference_index}: {cause}"),
157            Self::NoEncodeRoute(rejections) => write!(
158                f,
159                "none of {} permitted encoder routes could be prepared",
160                rejections.len()
161            ),
162            Self::Allocation => f.write_str("operation planning allocation failed"),
163            Self::Stopped(reason) => write!(f, "operation stopped: {reason:?}"),
164            Self::Policy(e) => e.fmt(f),
165            Self::Execution { .. } => {
166                f.write_str("selected encoder failed; inspect the decision report and native cause")
167            }
168            Self::OutputDescriptor { requested, actual } => write!(
169                f,
170                "required output {requested:?}; decoder produced {actual:?}"
171            ),
172        }
173    }
174}
175impl core::error::Error for OperationError {
176    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
177        match self {
178            Self::Codec(e) => Some(e),
179            Self::InvalidFidelity { cause, .. } => Some(cause),
180            Self::NoEncodeRoute(rejections) => rejections
181                .first()
182                .map(|r| &r.cause as &dyn core::error::Error),
183            Self::Policy(e) => Some(e),
184            Self::Execution { cause, .. } => Some(cause.as_ref()),
185            _ => None,
186        }
187    }
188}
189
190#[derive(Clone, Debug, Default)]
191enum Output {
192    #[default]
193    Native,
194    Prefer(Vec<PixelDescriptor>),
195    Require(PixelDescriptor),
196}
197
198/// Reusable requirements for decoding one image into owned pixels.
199///
200/// Set-level controls (limits, stop/pulse, policy) are inherited. An explicitly
201/// supplied policy replaces the set's policy and is checked before the decoder
202/// receives input. Resource limits keep their codec-defined enforcement scope;
203/// this request does not turn an estimate into a whole-operation memory bound.
204#[derive(Clone, Debug, Default)]
205pub struct DecodeRequest {
206    format: Option<ImageFormat>,
207    policy: Option<DecodePolicy>,
208    output: Output,
209}
210
211impl DecodeRequest {
212    /// Detect the registered input format and request its native output.
213    pub const fn new() -> Self {
214        Self {
215            format: None,
216            policy: None,
217            output: Output::Native,
218        }
219    }
220
221    /// Decode as this format instead of detecting it.
222    pub const fn with_format(mut self, format: ImageFormat) -> Self {
223        self.format = Some(format);
224        self
225    }
226
227    /// Record a required policy, replacing the inherited set policy.
228    pub const fn with_policy(mut self, policy: DecodePolicy) -> Self {
229        self.policy = Some(policy);
230        self
231    }
232
233    /// Require this output descriptor. An unsupported result is an error;
234    /// conversion or lower precision is never selected implicitly.
235    ///
236    /// The one-shot path verifies the actual result after native decoding, so
237    /// refusal can follow a full raster allocation. For controlled execution,
238    /// [`Self::start_controlled`] checks the final interpretation before sink
239    /// effects; this is not a bound on native decoding workspace.
240    pub fn with_output(mut self, descriptor: PixelDescriptor) -> Self {
241        self.output = Output::Require(descriptor);
242        self
243    }
244
245    /// Prefer these descriptors in order, permitting the decoder's native
246    /// fallback. Inspect the returned pixels for the selected descriptor.
247    pub fn preferring_output(mut self, descriptors: impl Into<Vec<PixelDescriptor>>) -> Self {
248        self.output = Output::Prefer(descriptors.into());
249        self
250    }
251
252    /// Start controlled decoding through the same registered templates and
253    /// checked requirements as this request's one-shot path. Format detection
254    /// consumes no prefix bytes. A required descriptor is checked at final
255    /// interpretation, before policy adoption or sink effects.
256    ///
257    /// Serve the returned session with `zencodec_io::PolicyDecoder` and any
258    /// existing transport driver. Unsupported controlled execution refuses;
259    /// there is no automatic fallback to ordinary incremental decoding. A known
260    /// format is checked immediately, before the driver consumes any input.
261    #[track_caller]
262    pub fn start_controlled(
263        &self,
264        codecs: &CodecSet,
265        storage: crate::storage::StorageBudget,
266    ) -> Result<Box<crate::decode::DynControlledDecoder>, At<OperationError>> {
267        let session = decoding::CheckedDecoder::new(self.clone(), codecs.clone(), storage)
268            .map_err(|e| At::wrap(e).at_named("start controlled decode"))?;
269        Ok(crate::decode::erase_controlled(session))
270    }
271
272    /// Validate configuration and decode. No capability-query choreography is
273    /// required. Failures retain a location trace and the native codec cause.
274    #[track_caller]
275    pub fn decode(
276        &self,
277        codecs: &CodecSet,
278        input: &[u8],
279    ) -> Result<Outcome<DecodeOutput>, At<OperationError>> {
280        let run = || -> Result<DecodeOutput, OperationError> {
281            codecs
282                .operation_pulse()
283                .check()
284                .map_err(OperationError::Stopped)?;
285            let format = self
286                .format
287                .or_else(|| codecs.detect(input))
288                .ok_or(OperationError::Codec(CodecSetError::UnrecognizedFormat))?;
289            let preferred = match &self.output {
290                Output::Native => &[][..],
291                Output::Prefer(list) => list,
292                Output::Require(descriptor) => core::slice::from_ref(descriptor),
293            };
294            let job = codecs
295                .checked_decode_job(format, self.policy)
296                .map_err(OperationError::Codec)?;
297            let decoder = job
298                .into_decoder(Cow::Borrowed(input), preferred)
299                .map_err(|e| OperationError::Codec(e.into()))?;
300            let output = decoder
301                .decode()
302                .map_err(|e| OperationError::Codec(e.into()))?;
303            codecs
304                .operation_pulse()
305                .check()
306                .map_err(OperationError::Stopped)?;
307            if let Output::Require(requested) = self.output {
308                let actual = output.pixels().descriptor();
309                if actual != requested {
310                    return Err(OperationError::OutputDescriptor { requested, actual });
311                }
312            }
313            Ok(output)
314        };
315        run()
316            .map(|output| Outcome {
317                output,
318                report: OperationReport::default(),
319            })
320            .map_err(|e| At::wrap(e).at_named("checked image decode"))
321    }
322}
323
324/// A checked encoder job retained for a pipeline's joint decision.
325///
326/// Preparing it checks required fidelity and encode policy and stamps the
327/// registry's limits/cancellation. It does not construct an encoder, read
328/// pixels, validate output metadata, or establish a native memory bound.
329/// Ordinary callers use `EncodeRequest::encode`; adapters can retain this
330/// same object alongside a conversion plan until their resolver chooses it.
331pub struct PreparedEncode {
332    offer: EncodeOffer,
333    job: Box<dyn crate::encode::DynEncodeJob>,
334}
335impl fmt::Debug for PreparedEncode {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        f.debug_struct("PreparedEncode")
338            .field("offer", &self.offer)
339            .finish_non_exhaustive()
340    }
341}
342impl PreparedEncode {
343    /// Configuration confirmed during preparation.
344    pub const fn offer(&self) -> EncodeOffer {
345        self.offer
346    }
347
348    /// Transfer the exact prepared job to an adapter. Metadata/carrier
349    /// validation and any further configuration are that adapter's obligation.
350    /// Replacing its checked policy or fidelity invalidates the original offer.
351    pub fn into_job(self) -> Box<dyn crate::encode::DynEncodeJob> {
352        self.job
353    }
354}
355
356/// A prepared encoder job with compile-time thread-transfer capability.
357///
358/// The same checked requirements and offer as `PreparedEncode`, retaining Send
359/// until native encoder construction. Created by `EncodeRequest::prepare_send`.
360pub struct PreparedSendEncode {
361    offer: EncodeOffer,
362    job: Box<dyn crate::encode::DynEncodeJob + Send>,
363}
364impl fmt::Debug for PreparedSendEncode {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        f.debug_struct("PreparedSendEncode")
367            .field("offer", &self.offer)
368            .finish_non_exhaustive()
369    }
370}
371impl PreparedSendEncode {
372    /// Confirmed configuration, with the original preference index.
373    pub const fn offer(&self) -> EncodeOffer {
374        self.offer
375    }
376    /// Transfer the same job, retaining its Send guarantee.
377    pub fn into_job(self) -> Box<dyn crate::encode::DynEncodeJob + Send> {
378        self.job
379    }
380}
381
382/// Reusable requirements for encoding pixels with a registered template.
383///
384/// A fidelity target is checked at configuration time. This confirms the
385/// requested mode/quality selection, not a perceptual score or that arbitrary
386/// inputs satisfy a backend's content-dependent lossless restrictions.
387#[derive(Clone, Debug)]
388pub struct EncodeRequest {
389    format: ImageFormat,
390    fidelity: FidelityRequest,
391    policy: Option<EncodePolicy>,
392    policy_defaults: EncodePolicy,
393    preset: PolicySet,
394    resolution: Option<crate::Resolution>,
395    orientation_mode: Option<crate::encode::EncodeOrientation>,
396}
397
398#[derive(Clone, Debug)]
399enum FidelityRequest {
400    Template,
401    Required(Fidelity),
402    Alternatives(Vec<Fidelity>),
403}
404
405impl EncodeRequest {
406    /// Use the registered encoder template, inheriting the set's controls.
407    pub const fn new(format: ImageFormat) -> Self {
408        Self {
409            format,
410            fidelity: FidelityRequest::Template,
411            policy: None,
412            policy_defaults: EncodePolicy::none(),
413            preset: PolicySet::new(),
414            resolution: None,
415            orientation_mode: None,
416        }
417    }
418
419    /// Require this fidelity target; unsupported, substituted and unreported
420    /// targets fail before encoding pixels.
421    pub fn with_fidelity(mut self, fidelity: Fidelity) -> Self {
422        self.fidelity = FidelityRequest::Required(fidelity);
423        self
424    }
425
426    /// Permit only these fidelity targets, in preference order. Invalid values
427    /// refuse the whole request; unsupported values may be omitted from offers.
428    /// There is no encode retry after the resolver selects a prepared job.
429    pub fn preferring_fidelity(mut self, targets: impl Into<Vec<Fidelity>>) -> Self {
430        self.fidelity = FidelityRequest::Alternatives(targets.into());
431        self
432    }
433
434    /// Configure the built-in policy used by ordinary execution and delegation.
435    /// During migration, these requirements and the policy argument
436    /// both apply. A bound policy supplies preferences in place of these.
437    #[deprecated(note = "Pass PolicySet or BoundPolicy as the operation policy argument")]
438    pub const fn with_policy_set(mut self, preset: PolicySet) -> Self {
439        self.preset = preset;
440        self
441    }
442
443    /// Require exact physical density, or an equivalent unknown-unit aspect.
444    /// No implicit rounding, unit loss, resize or orientation transform is allowed.
445    /// Validation occurs during preparation, before the callback or pixel encoding.
446    pub const fn with_resolution(mut self, resolution: crate::Resolution) -> Self {
447        self.resolution = Some(resolution);
448        self
449    }
450
451    /// Require stored or baked orientation through the configured job.
452    /// Preparation refuses unsupported codecs before invoking the resolver or
453    /// encoding pixels. Metadata still supplies the intrinsic orientation.
454    /// This is not a memory bound or an implicit conversion between formats.
455    pub const fn with_orientation_mode(mut self, mode: crate::encode::EncodeOrientation) -> Self {
456        self.orientation_mode = Some(mode);
457        self
458    }
459
460    /// Record a required policy, replacing the inherited set policy.
461    pub const fn with_policy(mut self, policy: EncodePolicy) -> Self {
462        self.policy = Some(policy);
463        self
464    }
465
466    /// Supply adapter defaults for unspecified policy fields.
467    ///
468    /// Explicit fields in the request's policy (or the inherited set policy)
469    /// win, including `Some(false)`. Filled fields go through the same checked
470    /// setter as requirements; an unsupported default refuses preparation.
471    /// An all-`None` default leaves the registered template unchanged.
472    pub const fn with_policy_defaults(mut self, defaults: EncodePolicy) -> Self {
473        self.policy_defaults = defaults;
474        self
475    }
476
477    fn effective_policy(&self, codecs: &CodecSet) -> Option<EncodePolicy> {
478        let required = self.policy.or(codecs.operation_encode_policy());
479        if self.policy_defaults == EncodePolicy::none() {
480            return required;
481        }
482        let mut policy = required.unwrap_or_else(EncodePolicy::none);
483        policy.color = policy.color.or(self.policy_defaults.color);
484        policy.embed_icc = policy.embed_icc.or(self.policy_defaults.embed_icc);
485        policy.embed_exif = policy.embed_exif.or(self.policy_defaults.embed_exif);
486        policy.embed_xmp = policy.embed_xmp.or(self.policy_defaults.embed_xmp);
487        policy.strict = policy.strict.or(self.policy_defaults.strict);
488        Some(policy)
489    }
490
491    /// Prepare permitted configurations without constructing an encoder or
492    /// touching pixels. Adapter API for coupling conversion, metadata and
493    /// encode into one question. All malformed targets reject the request;
494    /// unsupported alternatives are omitted. An empty result is an error.
495    /// The returned jobs retain original preference indices and must be the
496    /// objects executed after selection. This method never calls a resolver.
497    #[track_caller]
498    pub fn prepare(&self, codecs: &CodecSet) -> Result<Vec<PreparedEncode>, At<OperationError>> {
499        self.prepare_with(codecs, |offer| {
500            let mut job = codecs.checked_encode_job(
501                self.format,
502                offer.fidelity,
503                self.effective_policy(codecs),
504            )?;
505            if let Some(mode) = self.orientation_mode {
506                job.try_set_orientation_mode(mode)?;
507            }
508            if let Some(resolution) = offer.resolution {
509                job.try_set_resolution(resolution)?;
510            }
511            Ok(PreparedEncode { offer, job })
512        })
513    }
514
515    /// Prepare transferable jobs for an adapter that holds them across awaits.
516    /// The selected registry entry must use `CodecSet::with_send_encoder`.
517    /// Refusal occurs during preparation, before the adapter consumes input.
518    #[track_caller]
519    pub fn prepare_send(
520        &self,
521        codecs: &CodecSet,
522    ) -> Result<Vec<PreparedSendEncode>, At<OperationError>> {
523        self.prepare_with(codecs, |offer| {
524            let mut job = codecs.checked_send_encode_job(
525                self.format,
526                offer.fidelity,
527                self.effective_policy(codecs),
528            )?;
529            if let Some(mode) = self.orientation_mode {
530                job.try_set_orientation_mode(mode)?;
531            }
532            if let Some(resolution) = offer.resolution {
533                job.try_set_resolution(resolution)?;
534            }
535            Ok(PreparedSendEncode { offer, job })
536        })
537    }
538
539    fn prepare_with<T>(
540        &self,
541        codecs: &CodecSet,
542        mut create: impl FnMut(EncodeOffer) -> Result<T, CodecSetError>,
543    ) -> Result<Vec<T>, At<OperationError>> {
544        let failure = |error| At::wrap(error).at_named("prepare encode routes");
545        if let Some(resolution) = self.resolution {
546            resolution.validate().map_err(|error| {
547                failure(OperationError::Codec(CodecSetError::Codec(Box::new(error))))
548            })?;
549        }
550        let targets = match &self.fidelity {
551            FidelityRequest::Template => None,
552            FidelityRequest::Required(target) => Some(core::slice::from_ref(target)),
553            FidelityRequest::Alternatives(targets) => Some(targets.as_slice()),
554        };
555        let count = targets.map_or(1, <[_]>::len);
556        codecs
557            .operation_pulse()
558            .check()
559            .map_err(|reason| failure(OperationError::Stopped(reason)))?;
560        if count == 0 || count > policy::MAX_OFFERS {
561            return Err(failure(OperationError::AlternativeCount {
562                count,
563                limit: policy::MAX_OFFERS,
564            }));
565        }
566        if let Some(targets) = targets {
567            for (index, target) in targets.iter().enumerate() {
568                target.validate_target().map_err(|cause| {
569                    failure(OperationError::InvalidFidelity {
570                        preference_index: index,
571                        cause,
572                    })
573                })?;
574            }
575        }
576        let mut jobs = Vec::new();
577        let mut rejections = Vec::new();
578        jobs.try_reserve_exact(count)
579            .map_err(|_| failure(OperationError::Allocation))?;
580        rejections
581            .try_reserve_exact(count)
582            .map_err(|_| failure(OperationError::Allocation))?;
583        for index in 0..count {
584            codecs
585                .operation_pulse()
586                .check()
587                .map_err(|reason| failure(OperationError::Stopped(reason)))?;
588            let fidelity = targets.map(|targets| targets[index]);
589            match create(EncodeOffer {
590                format: self.format,
591                fidelity,
592                preference_index: index,
593                resolution: self.resolution,
594            }) {
595                Ok(job) => jobs.push(job),
596                Err(cause) => rejections.push(RouteRejection {
597                    preference_index: index,
598                    fidelity,
599                    cause,
600                }),
601            }
602        }
603        if jobs.is_empty() {
604            return Err(failure(OperationError::NoEncodeRoute(rejections)));
605        }
606        Ok(jobs)
607    }
608
609    /// Validate configuration and encode, preserving strided pixel input and
610    /// native errors. The stored request and registered template are reusable.
611    #[track_caller]
612    pub fn encode(
613        &self,
614        codecs: &CodecSet,
615        pixels: PixelSlice<'_>,
616    ) -> Result<Outcome<EncodeOutput>, At<OperationError>> {
617        let mut delegate = |_: Question<'_>| -> Result<Decision, At<core::convert::Infallible>> {
618            Ok(Decision::UsePreset)
619        };
620        self.encode_with(codecs, pixels, &mut delegate)
621    }
622
623    /// Execute with one application resolver. Only prepared jobs are offered;
624    /// callback failures retain their concrete `At<E>` in the error chain.
625    /// Callback success cannot authorize a stale or fabricated choice.
626    #[track_caller]
627    pub fn encode_with<R: OperationPolicy + ?Sized>(
628        &self,
629        codecs: &CodecSet,
630        pixels: PixelSlice<'_>,
631        resolver: &mut R,
632    ) -> Result<Outcome<EncodeOutput>, At<OperationError>> {
633        let mut jobs = self.prepare(codecs)?;
634        let mut offers = Vec::new();
635        offers
636            .try_reserve_exact(jobs.len())
637            .map_err(|_| At::wrap(OperationError::Allocation).at())?;
638        offers.extend(jobs.iter().map(PreparedEncode::offer));
639        let facts = EncodeFacts {
640            width: pixels.width(),
641            height: pixels.rows(),
642            descriptor: pixels.descriptor(),
643        };
644        let (selected, record) = policy::choose_encode(
645            facts,
646            &offers,
647            resolver,
648            self.preset,
649            codecs.operation_pulse(),
650        )
651        .map_err(|error| {
652            error
653                .map_error(OperationError::Policy)
654                .at_named("resolve encode route")
655        })?;
656        let mut report = OperationReport::default();
657        report.record(record);
658        let job = jobs.swap_remove(selected).into_job();
659        // Release unselected jobs before starting the chosen codec. The chosen
660        // configuration is exactly the prepared object described by its offer.
661        drop(jobs);
662        let output = job.encode(pixels).map_err(|cause| {
663            At::wrap(OperationError::Execution {
664                cause: Box::new(cause.into()),
665                report: Box::new(report.clone()),
666            })
667            .at_named("execute selected encoder")
668        })?;
669        Ok(Outcome { output, report })
670    }
671}