Skip to main content

zencodec_coroutine/
decoder.rs

1//! `IncrementalDecode` over a coroutine.
2
3use alloc::boxed::Box;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::future::Future;
7use core::marker::PhantomData;
8use core::pin::Pin;
9use core::task::{Context, Poll, Waker};
10
11use zencodec::ImageInfo;
12use zencodec::decode::{
13    BoundaryAction, BoundaryId, ControlError, ControlledIncrementalDecode, ControlledProgress,
14    DecodeBoundary, DecodeRowSink, Event, IncrementalDecode, Input, InputRequest, MetadataUpdate,
15    OutputInfo, Progress, SinkError, StepError,
16};
17use zencodec::source::AccessPattern;
18use zenpixels::PixelDescriptor;
19
20use crate::context::{CoroutineContext, CoroutineError, Shared, SharedCell};
21
22/// Default cap on fed-but-unread input, in bytes.
23pub const DEFAULT_INPUT_CAP: usize = 16 * 1024 * 1024;
24
25/// A boxed decode future, for decoders whose concrete future type is not
26/// nameable (every `async` block).
27pub type BoxFuture<E> = Pin<Box<dyn Future<Output = Result<OutputInfo, E>> + Send + 'static>>;
28
29/// A [`CoroutineDecoder`] over a [`BoxFuture`]: the type to name in a
30/// codec's `DecodeJob::dyn_incremental_decoder` or a struct field.
31pub type BoxedCoroutineDecoder<E> = CoroutineDecoder<BoxFuture<E>, E>;
32
33enum State {
34    Running,
35    /// The future returned while rows or metadata it deposited were still
36    /// undelivered (it polled `write_rows` by hand and did not await it):
37    /// they go out first, then `Done`.
38    Finishing(OutputInfo),
39    Done(OutputInfo),
40    Failed,
41}
42
43/// An [`IncrementalDecode`] whose logic is an `async` block.
44///
45/// Build one with [`new`](Self::new) from a closure that receives the
46/// [`CoroutineContext`] and returns the decode future. The future's error
47/// type is the codec's; it must be constructible from [`CoroutineError`]
48/// so the context's `?` operator works.
49pub struct CoroutineDecoder<F, E> {
50    shared: SharedCell,
51    future: Pin<Box<F>>,
52    state: State,
53    begun: bool,
54    /// The sink's `accepts_provisional`, mirrored here when `begin` ran so
55    /// the `Rows` arm does not take the shared lock just to read it.
56    accepts_provisional: bool,
57    access: AccessPattern,
58    need: Vec<InputRequest>,
59    cached_info: Option<ImageInfo>,
60    details: Option<zencodec::decode::DecodeDetails>,
61    cached_info_generation: u64,
62    /// A native batch may exceed the sink's capacity for one progress event.
63    /// Keep its allocation until all strips have been delivered; do not poll
64    /// the producer while any of its rows remain unpublished.
65    pending_rows: Option<(crate::context::PendingRows, u32)>,
66    _err: PhantomData<fn() -> E>,
67}
68
69// Stack-only cleanup: no extra Arc clone or lock on a successful step.
70struct DetailsCleanup<'a, F, E>(&'a mut CoroutineDecoder<F, E>);
71impl<F, E> Drop for DetailsCleanup<'_, F, E> {
72    fn drop(&mut self) {
73        if matches!(self.0.state, State::Failed) {
74            let pending = self.0.shared.lock().details.take();
75            drop(pending);
76            self.0.details = None;
77        }
78    }
79}
80
81impl<F, E> CoroutineDecoder<F, E>
82where
83    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
84    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
85{
86    /// Create a decoder with the default input cap and
87    /// [`AccessPattern::RandomAccess`].
88    pub fn new(init: impl FnOnce(CoroutineContext) -> F) -> Self {
89        Self::with_options(DEFAULT_INPUT_CAP, AccessPattern::RandomAccess, init)
90    }
91
92    /// Create a decoder requiring explicit interpretation decisions. No sink
93    /// effects are allowed until `ctx.interpretation(...).await` has received
94    /// adoption of final facts. Serve this through `step_controlled`; legacy
95    /// `step` refuses before polling the future.
96    pub fn controlled(
97        input_cap: usize,
98        access: AccessPattern,
99        init: impl FnOnce(CoroutineContext) -> F,
100    ) -> Self {
101        let decoder = Self::with_options(input_cap, access, init);
102        decoder.shared.lock().controlled = true;
103        decoder
104    }
105
106    /// Controlled decoding with admitted input payload, range index and row
107    /// staging. Admission runs before `init` is called. The input index holds at
108    /// most 64 disjoint spans; excess fragmentation is an error, never eviction.
109    /// Native workspace, image metadata, request bookkeeping and sink allocations
110    /// are outside this scoped allowance. Reuse the same budget in native code
111    /// to admit its additional owned buffers.
112    pub fn controlled_with_storage(
113        input_cap: usize,
114        access: AccessPattern,
115        storage: zencodec::storage::StorageBudget,
116        init: impl FnOnce(CoroutineContext) -> F,
117    ) -> Result<Self, CoroutineError> {
118        let mut shared = Shared::with_storage(input_cap, storage)?;
119        shared.controlled = true;
120        Ok(Self::from_shared(shared, access, init))
121    }
122
123    /// Create a decoder that keeps at most `input_cap` unread bytes and
124    /// declares `access` to drivers.
125    pub fn with_options(
126        input_cap: usize,
127        access: AccessPattern,
128        init: impl FnOnce(CoroutineContext) -> F,
129    ) -> Self {
130        Self::from_shared(Shared::new(input_cap), access, init)
131    }
132
133    fn from_shared(
134        state: Shared,
135        access: AccessPattern,
136        init: impl FnOnce(CoroutineContext) -> F,
137    ) -> Self {
138        let shared = Arc::new(spin::Mutex::new(state));
139        let future = Box::pin(init(CoroutineContext::new(Arc::clone(&shared))));
140        Self {
141            shared,
142            future,
143            state: State::Running,
144            begun: false,
145            accepts_provisional: false,
146            access,
147            need: Vec::new(),
148            cached_info: None,
149            details: None,
150            cached_info_generation: 0,
151            pending_rows: None,
152            _err: PhantomData,
153        }
154    }
155
156    /// Unread input currently retained, in bytes.
157    pub fn retained_input(&self) -> usize {
158        self.shared.lock().input.retained()
159    }
160}
161
162impl<E> CoroutineDecoder<BoxFuture<E>, E>
163where
164    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
165{
166    /// Like [`new`](Self::new), boxing the future so the decoder type is
167    /// [`BoxedCoroutineDecoder`] regardless of the `async` block.
168    pub fn boxed<Fut>(init: impl FnOnce(CoroutineContext) -> Fut) -> Self
169    where
170        Fut: Future<Output = Result<OutputInfo, E>> + Send + 'static,
171    {
172        Self::new(|ctx| Box::pin(init(ctx)) as BoxFuture<E>)
173    }
174
175    /// Like [`with_options`](Self::with_options), boxing the future.
176    pub fn boxed_with_options<Fut>(
177        input_cap: usize,
178        access: AccessPattern,
179        init: impl FnOnce(CoroutineContext) -> Fut,
180    ) -> Self
181    where
182        Fut: Future<Output = Result<OutputInfo, E>> + Send + 'static,
183    {
184        Self::with_options(input_cap, access, |ctx| Box::pin(init(ctx)) as BoxFuture<E>)
185    }
186}
187
188impl<F, E> CoroutineDecoder<F, E>
189where
190    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
191    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
192{
193    /// Copy into the store the bytes of `input[from..]` that the
194    /// outstanding reads need, and report how far into `input` that took
195    /// (a prefix: needed bytes plus any unneeded bytes before them).
196    fn absorb(
197        shared: &mut Shared,
198        input: &Input<'_>,
199        from: usize,
200    ) -> Result<usize, CoroutineError> {
201        if let Some(t) = input.total_len {
202            shared.input.set_total(t);
203        }
204        let front = input
205            .offset
206            .checked_add(from as u64)
207            .ok_or(CoroutineError::InputRangeOverflow)?;
208        input
209            .offset
210            .checked_add(input.bytes.len() as u64)
211            .ok_or(CoroutineError::InputRangeOverflow)?;
212        let end = input.end();
213        if front >= end {
214            return Ok(from);
215        }
216        let mut taken_to = front;
217        let needs = core::mem::take(&mut shared.need);
218        for r in &needs {
219            let (lo, hi, sequential) = match *r {
220                InputRequest::AtLeast { offset, .. } => (offset, end, true),
221                InputRequest::Range(range) => (range.offset, range.end(), false),
222                _ => continue,
223            };
224            let lo = lo.max(front);
225            let hi = hi.min(end);
226            if lo >= hi {
227                continue;
228            }
229            let slice = &input.bytes[(lo - input.offset) as usize..(hi - input.offset) as usize];
230            let stored = shared.input.feed(lo, slice, false)?;
231            let reached = lo + stored as u64;
232            if stored == slice.len() || sequential {
233                taken_to = taken_to.max(reached);
234            } else {
235                // The store is full: take only up to where storing stopped.
236                taken_to = taken_to.max(reached);
237                break;
238            }
239        }
240        shared.need = needs;
241        Ok((taken_to - input.offset) as usize)
242    }
243
244    fn begin_if_ready(&mut self, sink: &mut dyn DecodeRowSink) -> Result<(), StepError<E>> {
245        self.check_interpretation()?;
246        if self.begun {
247            return Ok(());
248        }
249        let (dims, desc) = {
250            let shared = self.shared.lock();
251            (
252                shared.info.as_ref().map(|i| (i.width, i.height)),
253                shared.descriptor,
254            )
255        };
256        if let (Some((w, h)), Some(desc)) = (dims, desc) {
257            sink.begin(w, h, desc).map_err(StepError::Sink)?;
258            let accepts = sink.accepts_provisional();
259            self.shared.lock().accepts_provisional = accepts;
260            self.accepts_provisional = accepts;
261            self.begun = true;
262        }
263        Ok(())
264    }
265
266    fn check_interpretation(&self) -> Result<(), StepError<E>> {
267        let shared = self.shared.lock();
268        if let Some(error) = shared.control_failure {
269            return Err(StepError::Control(error));
270        }
271        if shared.controlled && (shared.boundary.is_some() || shared.adopted.is_none()) {
272            return Err(StepError::Control(ControlError::OutputBeforeInterpretation));
273        }
274        Ok(())
275    }
276}
277
278impl<F, E> CoroutineDecoder<F, E>
279where
280    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
281    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
282{
283    fn with_details_cleanup<T>(&mut self, work: impl FnOnce(&mut Self) -> T) -> T {
284        let guard = DetailsCleanup(self);
285        work(&mut *guard.0)
286    }
287
288    fn advance(
289        &mut self,
290        input: Input<'_>,
291        sink: &mut dyn DecodeRowSink,
292        pulse: &dyn zencodec::Pulse,
293    ) -> Result<ControlledProgress, StepError<E>> {
294        match &self.state {
295            State::Done(info) => return Ok(Progress::new(0, Event::Done(info.clone())).into()),
296            State::Failed => return Err(StepError::Finished),
297            State::Running | State::Finishing(_) => {}
298        }
299        if let Err(reason) = pulse.check() {
300            self.pending_rows = None;
301            self.shared.lock().cancelled = Some(reason);
302            self.state = State::Failed;
303            return Err(StepError::Cancelled(reason));
304        }
305        if let Some(boundary) = self.shared.lock().boundary {
306            return Ok(ControlledProgress::Suspended {
307                consumed: 0,
308                boundary,
309            });
310        }
311        if let Some((rows, offset)) = self.pending_rows.take() {
312            return self.deliver_rows(rows, offset, 0, sink);
313        }
314        if let State::Finishing(info) = &self.state {
315            return self.finish(sink, info.clone(), 0).map(Into::into);
316        }
317        self.state = State::Failed;
318        let mut consumed = Self::absorb(&mut self.shared.lock(), &input, 0)
319            .map_err(|e| StepError::Codec(E::from(e)))?;
320        self.state = State::Running;
321        let waker = Waker::noop();
322        let mut cx = Context::from_waker(waker);
323        loop {
324            // A caught panic never permits polling the half-mutated future again.
325            self.state = State::Failed;
326            let polled = self.future.as_mut().poll(&mut cx);
327            self.state = State::Running;
328            if let Err(reason) = pulse.check() {
329                self.shared.lock().cancelled = Some(reason);
330                self.state = State::Failed;
331                return Err(StepError::Cancelled(reason));
332            }
333            let event = {
334                let mut shared = self.shared.lock();
335                if shared.info_generation != self.cached_info_generation {
336                    self.cached_info = shared.info.clone();
337                    self.cached_info_generation = shared.info_generation;
338                }
339                if let Some(error) = shared.control_failure {
340                    self.state = State::Failed;
341                    return Err(StepError::Control(error));
342                }
343                if let Some(boundary) = shared.boundary {
344                    if shared.rows.is_some() {
345                        self.state = State::Failed;
346                        return Err(StepError::Control(ControlError::OutputBeforeInterpretation));
347                    }
348                    Pending::Boundary(boundary)
349                } else if let Some(rows) = shared.rows.take() {
350                    Pending::Rows(rows)
351                } else if let Some(update) = shared.metadata.take() {
352                    Pending::Metadata(update)
353                } else if !shared.need.is_empty() {
354                    Pending::Need
355                } else {
356                    Pending::Stalled
357                }
358            };
359            match polled {
360                Poll::Ready(Ok(info)) => {
361                    if matches!(event, Pending::Boundary(_)) {
362                        self.state = State::Failed;
363                        return Err(StepError::Control(ControlError::InconsistentFacts));
364                    }
365                    if matches!(event, Pending::Rows(_) | Pending::Metadata(_)) {
366                        // Deliver what was deposited before finishing; the
367                        // future is complete and must not be polled again.
368                        self.state = State::Finishing(info);
369                        self.need.clear();
370                    } else {
371                        return self.finish(sink, info, consumed).map(Into::into);
372                    }
373                }
374                Poll::Ready(Err(e)) => {
375                    self.state = State::Failed;
376                    self.need.clear();
377                    return Err(StepError::Codec(e));
378                }
379                Poll::Pending => {}
380            }
381            match event {
382                Pending::Boundary(boundary) => {
383                    self.need.clear();
384                    return Ok(ControlledProgress::Suspended { consumed, boundary });
385                }
386                Pending::Rows(rows) => {
387                    let inconsistent = {
388                        let shared = self.shared.lock();
389                        shared.controlled
390                            && (shared.descriptor != Some(rows.descriptor)
391                                || shared
392                                    .info
393                                    .as_ref()
394                                    .is_none_or(|info| info.width != rows.width))
395                    };
396                    if inconsistent {
397                        self.state = State::Failed;
398                        return Err(StepError::Control(ControlError::InconsistentFacts));
399                    }
400                    if let Err(e) = self.begin_if_ready(sink) {
401                        self.state = State::Failed;
402                        return Err(e);
403                    }
404                    if !self.begun {
405                        self.state = State::Failed;
406                        return Err(StepError::Codec(E::from(CoroutineError::RowsBeforeHeader)));
407                    }
408                    return self.deliver_rows(rows, 0, consumed, sink);
409                }
410                Pending::Metadata(update) => {
411                    let controlled = self.shared.lock().controlled;
412                    if !controlled
413                        && update.is_header_ready()
414                        && let Err(e) = self.begin_if_ready(sink)
415                    {
416                        self.state = State::Failed;
417                        return Err(e);
418                    }
419                    return Ok(Progress::new(consumed, Event::Metadata(update)).into());
420                }
421                Pending::Need => {
422                    // The same input may hold what the coroutine now asks
423                    // for: take it and poll again rather than send the
424                    // driver back for bytes it already offered.
425                    let mut shared = self.shared.lock();
426                    self.state = State::Failed;
427                    let more = Self::absorb(&mut shared, &input, consumed)
428                        .map_err(|e| StepError::Codec(E::from(e)))?;
429                    self.state = State::Running;
430                    if more > consumed {
431                        consumed = more;
432                        continue;
433                    }
434                    self.need.clear();
435                    self.need.extend_from_slice(&shared.need);
436                    return Ok(Progress::need(consumed).into());
437                }
438                Pending::Stalled => {
439                    self.state = State::Failed;
440                    return Err(StepError::Codec(E::from(CoroutineError::Stalled)));
441                }
442            }
443        }
444    }
445}
446
447impl<F, E> IncrementalDecode for CoroutineDecoder<F, E>
448where
449    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
450    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
451{
452    type Error = E;
453
454    fn step(
455        &mut self,
456        input: Input<'_>,
457        sink: &mut dyn DecodeRowSink,
458        pulse: Option<&dyn zencodec::Pulse>,
459    ) -> Result<Progress, StepError<E>> {
460        self.with_details_cleanup(|this| {
461            let pulse = pulse.unwrap_or(&zencodec::NoPulse);
462            if matches!(this.state, State::Failed) {
463                return Err(StepError::Finished);
464            }
465            if this.shared.lock().controlled {
466                this.state = State::Failed;
467                return Err(StepError::Control(ControlError::UnhandledBoundary));
468            }
469            match this.advance(input, sink, pulse)? {
470                ControlledProgress::Decode(progress) => Ok(progress),
471                ControlledProgress::Suspended { .. } => {
472                    this.state = State::Failed;
473                    Err(StepError::Control(ControlError::UnhandledBoundary))
474                }
475            }
476        })
477    }
478
479    fn requested(&self) -> &[InputRequest] {
480        match self.state {
481            State::Running => &self.need,
482            _ => &[],
483        }
484    }
485
486    fn info(&self) -> Option<&ImageInfo> {
487        self.details
488            .as_ref()
489            .map(|d| &d.info)
490            .or(self.cached_info.as_ref())
491    }
492
493    fn take_details(&mut self) -> Option<zencodec::decode::DecodeDetails> {
494        if !matches!(self.state, State::Done(_)) {
495            return None;
496        }
497        let details = self.details.take();
498        if details.is_some() {
499            self.cached_info = None;
500        }
501        details
502    }
503
504    fn output_descriptor(&self) -> Option<PixelDescriptor> {
505        self.shared.lock().descriptor
506    }
507
508    fn access_pattern(&self) -> AccessPattern {
509        self.access
510    }
511}
512
513impl<F, E> ControlledIncrementalDecode for CoroutineDecoder<F, E>
514where
515    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
516    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
517{
518    fn step_controlled(
519        &mut self,
520        input: Input<'_>,
521        sink: &mut dyn DecodeRowSink,
522        pulse: Option<&dyn zencodec::Pulse>,
523    ) -> Result<ControlledProgress, StepError<E>> {
524        self.with_details_cleanup(|this| {
525            let pulse = pulse.unwrap_or(&zencodec::NoPulse);
526            if matches!(this.state, State::Failed) {
527                return Err(StepError::Finished);
528            }
529            if !this.shared.lock().controlled {
530                this.state = State::Failed;
531                return Err(StepError::Control(ControlError::NotControlled));
532            }
533            this.advance(input, sink, pulse)
534        })
535    }
536
537    fn resume(
538        &mut self,
539        id: BoundaryId,
540        action: BoundaryAction,
541        pulse: Option<&dyn zencodec::Pulse>,
542    ) -> Result<(), StepError<E>> {
543        self.with_details_cleanup(|this| {
544            let pulse = pulse.unwrap_or(&zencodec::NoPulse);
545            let state = core::mem::replace(&mut this.state, State::Failed);
546            if !matches!(state, State::Running) {
547                return Err(StepError::Finished);
548            }
549            pulse.check()?;
550            let mut shared = this.shared.lock();
551            if !shared.controlled {
552                return Err(StepError::Control(ControlError::NotControlled));
553            }
554            if let Some(error) = shared.control_failure {
555                return Err(StepError::Control(error));
556            }
557            let Some(boundary) = shared.boundary.take() else {
558                return Err(StepError::Control(ControlError::InvalidReply));
559            };
560            if boundary.id() != id || !action.permits(boundary.facts()) || shared.reply.is_some() {
561                return Err(StepError::Control(ControlError::InvalidReply));
562            }
563            let facts = boundary.facts();
564            if facts.descriptor != shared.descriptor
565                || shared
566                    .info
567                    .as_ref()
568                    .is_none_or(|info| info.width != facts.width || info.height != facts.height)
569            {
570                return Err(StepError::Control(ControlError::InconsistentFacts));
571            }
572            if action == BoundaryAction::AdoptInterpretation {
573                shared.adopted = Some(facts);
574            }
575            shared.reply = Some((id, action));
576            this.state = State::Running;
577            Ok(())
578        })
579    }
580}
581
582enum Pending {
583    Boundary(DecodeBoundary),
584    Rows(crate::context::PendingRows),
585    Metadata(MetadataUpdate),
586    Need,
587    Stalled,
588}
589
590/// Route a batch to the sink: final rows through `provide_next_buffer`,
591/// provisional rows through `provide_pass_buffer`, and only to a sink that
592/// accepts them. The coroutine API (`accepts_provisional` on the context)
593/// lets a decoder skip provisional passes nobody wants; sending one anyway
594/// is an error rather than a silent drop.
595fn copy_rows(
596    sink: &mut dyn DecodeRowSink,
597    rows: &crate::context::PendingRows,
598    offset: u32,
599    count: u32,
600    accepts_provisional: bool,
601) -> Result<(), SinkError> {
602    if !rows.meta.is_final && !accepts_provisional {
603        return Err("decoder wrote provisional rows to a sink that does not accept them".into());
604    }
605    let pixels = zenpixels::PixelSlice::new(
606        rows.data.as_slice(),
607        rows.width,
608        rows.rows,
609        rows.stride,
610        rows.descriptor,
611    )?;
612    rows.meta
613        .y
614        .checked_add(rows.rows)
615        .ok_or_else(|| SinkError::from("copied rows exceed the row coordinate range"))?;
616    if count == 0 {
617        return Ok(());
618    }
619    let mut meta = rows.meta;
620    meta.y += offset;
621    let mut destination = if meta.is_final {
622        sink.provide_next_buffer(meta.y, count, rows.width, rows.descriptor)?
623    } else {
624        sink.provide_pass_buffer(meta, count, rows.width, rows.descriptor)?
625    };
626    if destination.width() != rows.width
627        || destination.rows() != count
628        || destination.descriptor() != rows.descriptor
629    {
630        return Err("sink returned a buffer with a different geometry or descriptor".into());
631    }
632    for row in 0..count {
633        destination
634            .row_mut(row)
635            .copy_from_slice(pixels.row(offset + row));
636    }
637    Ok(())
638}
639
640impl<F, E> CoroutineDecoder<F, E>
641where
642    F: Future<Output = Result<OutputInfo, E>> + Send + 'static,
643    E: core::error::Error + Send + Sync + From<CoroutineError> + 'static,
644{
645    fn deliver_rows(
646        &mut self,
647        rows: crate::context::PendingRows,
648        offset: u32,
649        consumed: usize,
650        sink: &mut dyn DecodeRowSink,
651    ) -> Result<ControlledProgress, StepError<E>> {
652        // Keep terminal behavior even if a sink callback unwinds. The batch is
653        // local until copying succeeds, so failure releases it immediately.
654        let state = core::mem::replace(&mut self.state, State::Failed);
655        self.check_interpretation()?;
656        let count = sink.max_buffer_rows().get().min(rows.rows - offset);
657        copy_rows(sink, &rows, offset, count, self.accepts_provisional).map_err(StepError::Sink)?;
658        let mut meta = rows.meta;
659        meta.y += offset;
660        if offset + count < rows.rows {
661            self.pending_rows = Some((rows, offset + count));
662        } else {
663            self.shared.lock().spare = rows.data;
664        }
665        self.state = state;
666        self.need.clear();
667        Ok(Progress::new(consumed, Event::Rows { meta, rows: count }).into())
668    }
669
670    /// The future completed with `info`: begin if the header never was,
671    /// finish the sink, and report `Done`.
672    fn finish(
673        &mut self,
674        sink: &mut dyn DecodeRowSink,
675        info: OutputInfo,
676        consumed: usize,
677    ) -> Result<Progress, StepError<E>> {
678        // Final sink callbacks may unwind. A caught panic must never resume a
679        // completed future or expose its pending details as successful output.
680        self.state = State::Failed;
681        let inconsistent = {
682            let shared = self.shared.lock();
683            shared.details.as_ref().is_some_and(|details| {
684                details.info.width != info.width
685                    || details.info.height != info.height
686                    || (shared.adopted.is_some()
687                        && shared.info.as_ref().is_some_and(|old| {
688                            old.orientation != details.info.orientation
689                                || old.source_color != details.info.source_color
690                        }))
691            }) || (shared.controlled
692                && shared.adopted.is_some_and(|facts| {
693                    facts.width != info.width
694                        || facts.height != info.height
695                        || facts.descriptor != Some(info.native_format)
696                }))
697        };
698        if inconsistent {
699            self.state = State::Failed;
700            return Err(StepError::Control(ControlError::InconsistentFacts));
701        }
702        if let Err(e) = self.begin_if_ready(sink) {
703            self.state = State::Failed;
704            return Err(e);
705        }
706        if self.begun
707            && let Err(e) = sink.finish()
708        {
709            self.state = State::Failed;
710            return Err(StepError::Sink(e));
711        }
712        self.state = State::Done(info.clone());
713        self.need.clear();
714        let mut shared = self.shared.lock();
715        shared.input.clear();
716        shared.spare = Default::default();
717        self.details = shared.details.take();
718        if self.details.is_some() {
719            // The owned final result supersedes earlier metadata snapshots.
720            self.cached_info = None;
721            shared.info = None;
722        }
723        Ok(Progress::new(consumed, Event::Done(info)))
724    }
725}