Skip to main content

zencodec_io/
tokio_io.rs

1//! Async drivers for tokio (and any executor): decode with no thread
2//! blocked on I/O and no `async` in codec code.
3//!
4//! [`decode_async`] keeps the decoder, the sink and the input window in the
5//! async task. It awaits the [`AsyncSource`] for bytes, then hands one
6//! decode batch (steps until the decoder needs input or finishes) to a
7//! [`Compute`] pool, and awaits that. While the client uploads, nothing
8//! occupies a thread; CPU work runs on the pool in batches as large as the
9//! input on hand. A source returns whatever bytes have arrived, so rows
10//! come out while the upload is still running.
11//!
12//! [`transcode_async`] adds an encoder: rows go through an
13//! [`EncoderSink`](crate::EncoderSink) and encoded bytes are written to an
14//! `AsyncWrite` after every batch. The decoder and encoder stay alive while
15//! a write waits; their raw input and intermediate storage are not bounded
16//! by the encoded-output queue. Choose this coupling for configurations
17//! whose retained state and upstream production are bounded.
18//!
19//! [`pull_decode_on_thread`] runs a pull-native decoder (one written against
20//! [`DecodeJob::push_decoder_from_source`](zencodec::decode::DecodeJob::push_decoder_from_source))
21//! on one pool thread, with a [`DynReadAt`] whose reads block on the same
22//! [`AsyncSource`]. Such a codec blocks on reads by design, so it holds a
23//! thread for the whole decode.
24//!
25//! # Cancellation
26//!
27//! Two mechanisms, for the two places a decode can be waiting:
28//!
29//! - **Inside a compute batch**, the [`SharedPulse`] is checked at every
30//!   step, so a long batch stops within one step of the pulse tripping.
31//!   The pulse is also checked before each batch starts.
32//! - **While awaiting the source**, cancel the way tokio code always does:
33//!   drop the future, or race it in `tokio::select!` against your
34//!   cancellation token or a timeout. Nothing runs between batches, so the
35//!   decoder and sink are dropped at once. A batch already on the pool
36//!   finishes it (bounded by the input on hand) unless the pulse also trips.
37//!
38//! [`pull_decode_on_thread`] differs: the decode thread re-checks the pulse
39//! while it waits for a read, returns [`AsyncDriveError::Cancelled`], and the
40//! async side stops waiting on the source when the thread finishes.
41
42use alloc::boxed::Box;
43use alloc::vec;
44use alloc::vec::Vec;
45use core::fmt;
46use core::future::Future;
47
48use enough::{Stop, StopReason};
49use how_far::{Pulse, SharedPulse};
50use tokio::sync::{mpsc, oneshot};
51use zencodec::decode::{
52    BatchMeta, BoxedError, ByteRange, DecodeRowSink, Event, IncrementalDecode, InputRequest,
53    OutputInfo, SinkError, StepError,
54};
55use zencodec::encode::Encoder;
56use zencodec::how_far::NoPulse;
57use zencodec::source::{DynReadAt, SourceError};
58use zenpixels::{PixelBuffer, PixelDescriptor, PixelSliceMut};
59
60use crate::admission::{self, InputError};
61use crate::drive::{DriveOptions, Progressor};
62use crate::window::{ProtocolViolation, Window, check_need, read_target};
63
64// ───────────────────────────── sources and compute ─────────────────────────────
65
66/// Where the async drivers get bytes: a socket, an HTTP body, an object
67/// store with range requests.
68// `len` is async and fallible; an `is_empty` would only wrap it.
69#[allow(clippy::len_without_is_empty)]
70pub trait AsyncSource: Send {
71    /// Read up to `out.len()` bytes at `offset` into `out` and return the
72    /// count. Like `AsyncRead`, a read may return fewer bytes than asked:
73    /// whatever has arrived, so a decoder can start on it. Zero means the
74    /// source ends at `offset`.
75    fn read_at(
76        &mut self,
77        offset: u64,
78        out: &mut [u8],
79    ) -> impl Future<Output = Result<usize, BoxedError>> + Send;
80
81    /// Fill each buffer from its offset. Sources that can serve ranges
82    /// concurrently (HTTP/2, object stores) override this to overlap them;
83    /// the default reads them one after another.
84    fn read_ranges(
85        &mut self,
86        reqs: &mut [(u64, &mut [u8])],
87    ) -> impl Future<Output = Result<(), BoxedError>> + Send {
88        async move {
89            for (offset, out) in reqs.iter_mut() {
90                read_full(self, *offset, out).await?;
91            }
92            Ok(())
93        }
94    }
95
96    /// Total length if known without reading ahead.
97    fn len_hint(&self) -> Option<u64> {
98        None
99    }
100
101    /// Total length. Sequential sources can only answer once they reached
102    /// the end; they return an error before that.
103    fn len(&mut self) -> impl Future<Output = Result<u64, BoxedError>> + Send;
104}
105
106/// Fill `out` from `offset`, reading until it is full; the source ending
107/// first is [`SourceError::UnexpectedEof`].
108async fn read_full<S: AsyncSource + ?Sized>(
109    source: &mut S,
110    offset: u64,
111    out: &mut [u8],
112) -> Result<(), BoxedError> {
113    let mut filled = 0;
114    while filled < out.len() {
115        let n = source
116            .read_at(offset + filled as u64, &mut out[filled..])
117            .await?;
118        if n == 0 {
119            return Err(Box::new(SourceError::UnexpectedEof {
120                offset,
121                len: out.len() as u64,
122                total: Some(offset + filled as u64),
123            }));
124        }
125        filled += n;
126    }
127    Ok(())
128}
129
130/// An in-memory [`AsyncSource`] (tests, already-buffered bodies).
131#[derive(Debug, Clone)]
132pub struct BytesSource(pub alloc::sync::Arc<[u8]>);
133
134impl AsyncSource for BytesSource {
135    async fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, BoxedError> {
136        let start = (offset as usize).min(self.0.len());
137        let n = (self.0.len() - start).min(out.len());
138        out[..n].copy_from_slice(&self.0[start..start + n]);
139        Ok(n)
140    }
141
142    fn len_hint(&self) -> Option<u64> {
143        Some(self.0.len() as u64)
144    }
145
146    async fn len(&mut self) -> Result<u64, BoxedError> {
147        Ok(self.0.len() as u64)
148    }
149}
150
151/// A sequential [`AsyncSource`] over any `AsyncRead` (an HTTP body, a
152/// socket). Reads ahead of the stream position skip forward; reads behind
153/// it fail with [`SourceError::BeyondWindow`], and the length is known only
154/// once the stream has ended.
155#[derive(Debug)]
156pub struct AsyncReadSource<R> {
157    reader: R,
158    pos: u64,
159    eof: bool,
160}
161
162impl<R> AsyncReadSource<R> {
163    /// Wrap `reader`, positioned at offset 0.
164    pub fn new(reader: R) -> Self {
165        Self {
166            reader,
167            pos: 0,
168            eof: false,
169        }
170    }
171}
172
173impl<R: tokio::io::AsyncRead + Unpin + Send> AsyncSource for AsyncReadSource<R> {
174    async fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, BoxedError> {
175        use tokio::io::AsyncReadExt;
176        if offset < self.pos {
177            return Err(Box::new(SourceError::BeyondWindow {
178                offset,
179                oldest_retained: self.pos,
180            }));
181        }
182        let mut skip = [0u8; 4096];
183        while self.pos < offset && !self.eof {
184            let want = ((offset - self.pos) as usize).min(skip.len());
185            let n = self.reader.read(&mut skip[..want]).await?;
186            if n == 0 {
187                self.eof = true;
188            }
189            self.pos += n as u64;
190        }
191        if self.eof || out.is_empty() {
192            return Ok(0);
193        }
194        // One read: whatever has arrived, so decoding starts on it.
195        let n = self.reader.read(out).await?;
196        if n == 0 {
197            self.eof = true;
198        }
199        self.pos += n as u64;
200        Ok(n)
201    }
202
203    fn len_hint(&self) -> Option<u64> {
204        self.eof.then_some(self.pos)
205    }
206
207    async fn len(&mut self) -> Result<u64, BoxedError> {
208        if self.eof {
209            Ok(self.pos)
210        } else {
211            Err("a sequential stream cannot report its length before it ends".into())
212        }
213    }
214}
215
216/// Work handed to a [`Compute`].
217pub type Job = Box<dyn FnOnce() + Send + 'static>;
218
219/// Where decode batches run. Implemented for any `Fn(Job)`:
220/// `|job| rayon::spawn(job)`, `|job| { tokio::task::spawn_blocking(job); }`,
221/// or [`Inline`] to run on the calling task (single-threaded hosts, tiny
222/// images).
223pub trait Compute: Send + Sync {
224    /// Run `job` somewhere; the driver awaits its result.
225    fn spawn(&self, job: Job);
226
227    /// Whether `spawn` would run the job right here, on the calling task.
228    /// The drivers then call the batch directly, without boxing it or
229    /// creating a channel for its result. Default `false`.
230    fn runs_inline(&self) -> bool {
231        false
232    }
233}
234
235impl<F: Fn(Job) + Send + Sync> Compute for F {
236    fn spawn(&self, job: Job) {
237        self(job)
238    }
239}
240
241/// Runs each batch on the calling task. For single-threaded hosts (WASM)
242/// and small images, where moving work to another thread costs more than it
243/// saves; it blocks the executor for one batch.
244#[derive(Debug, Clone, Copy, Default)]
245pub struct Inline;
246
247impl Compute for Inline {
248    fn spawn(&self, job: Job) {
249        job()
250    }
251
252    fn runs_inline(&self) -> bool {
253        true
254    }
255}
256
257/// Run `f` on `compute` and await its result.
258///
259/// Spawns (or, for an inline compute, runs) before returning, and the
260/// returned future holds only the result or its receiver, so the caller's
261/// future stays `Send` whatever `compute` is.
262fn run_on<C: Compute + ?Sized, T: Send + 'static>(
263    compute: &C,
264    f: impl FnOnce() -> T + Send + 'static,
265) -> impl Future<Output = Result<T, AsyncDriveError<core::convert::Infallible>>> + Send + 'static {
266    let (done, rx) = if compute.runs_inline() {
267        (Some(f()), None)
268    } else {
269        let (tx, rx) = oneshot::channel();
270        compute.spawn(Box::new(move || {
271            let _ = tx.send(f());
272        }));
273        (None, Some(rx))
274    };
275    async move {
276        match (done, rx) {
277            (Some(v), _) => Ok(v),
278            (None, Some(rx)) => rx.await.map_err(|_| AsyncDriveError::Disconnected),
279            (None, None) => Err(AsyncDriveError::Disconnected),
280        }
281    }
282}
283
284// ───────────────────────────── errors ─────────────────────────────
285
286/// Failure of an async drive.
287#[derive(Debug)]
288#[non_exhaustive]
289pub enum AsyncDriveError<E> {
290    /// The decoder failed, the sink failed, or the pulse tripped inside a batch.
291    Step(StepError<E>),
292    /// The source failed.
293    Source(BoxedError),
294    /// Writing encoded output failed.
295    Write(std::io::Error),
296    /// The decoder broke the protocol.
297    Protocol(ProtocolViolation),
298    /// Driver-owned input could not be admitted or allocated.
299    Input(InputError),
300    /// The pulse tripped while the driver was waiting.
301    Cancelled(StopReason),
302    /// A compute job was dropped without a result (its pool shut down or
303    /// it panicked).
304    Disconnected,
305}
306
307/// Lift a codec-free error into any codec error type.
308fn widen<F>(e: AsyncDriveError<core::convert::Infallible>) -> AsyncDriveError<F> {
309    {
310        match e {
311            AsyncDriveError::Step(s) => match s {
312                StepError::Codec(never) => match never {},
313                StepError::Sink(e) => AsyncDriveError::Step(StepError::Sink(e)),
314                StepError::Cancelled(r) => AsyncDriveError::Step(StepError::Cancelled(r)),
315                StepError::Finished => AsyncDriveError::Step(StepError::Finished),
316                _ => AsyncDriveError::Disconnected,
317            },
318            AsyncDriveError::Source(e) => AsyncDriveError::Source(e),
319            AsyncDriveError::Write(e) => AsyncDriveError::Write(e),
320            AsyncDriveError::Protocol(p) => AsyncDriveError::Protocol(p),
321            AsyncDriveError::Input(e) => AsyncDriveError::Input(e),
322            AsyncDriveError::Cancelled(r) => AsyncDriveError::Cancelled(r),
323            AsyncDriveError::Disconnected => AsyncDriveError::Disconnected,
324        }
325    }
326}
327
328impl<E: fmt::Display> fmt::Display for AsyncDriveError<E> {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        match self {
331            Self::Step(e) => write!(f, "{e}"),
332            Self::Source(e) => write!(f, "source: {e}"),
333            Self::Write(e) => write!(f, "writing output: {e}"),
334            Self::Protocol(e) => write!(f, "protocol: {e}"),
335            Self::Input(e) => write!(f, "input: {e}"),
336            Self::Cancelled(r) => write!(f, "cancelled: {r:?}"),
337            Self::Disconnected => write!(f, "compute job dropped without a result"),
338        }
339    }
340}
341
342impl<E: core::error::Error + 'static> core::error::Error for AsyncDriveError<E> {
343    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
344        match self {
345            Self::Step(e) => Some(e),
346            Self::Source(e) => Some(e.as_ref()),
347            Self::Write(e) => Some(e),
348            Self::Protocol(e) => Some(e),
349            Self::Input(e) => Some(e),
350            _ => None,
351        }
352    }
353}
354
355// ───────────────────────────── resumable decode ─────────────────────────────
356
357/// What one compute batch hands back.
358struct Batch<D: IncrementalDecode, S> {
359    decoder: D,
360    sink: S,
361    window: Window,
362    outcome: Result<BatchEnd, BatchFail<D>>,
363}
364
365enum BatchEnd {
366    /// The decoder's `requested()` holds what it needs.
367    Need,
368    Yield,
369    Done(OutputInfo),
370}
371
372enum BatchFail<D: IncrementalDecode> {
373    Step(StepError<D::Error>),
374    Protocol(ProtocolViolation),
375}
376
377/// Step until the decoder needs input or finishes.
378fn run_batch<D, S>(
379    mut decoder: D,
380    mut sink: S,
381    mut window: Window,
382    pulse: Option<SharedPulse>,
383    quota: crate::WorkQuota,
384) -> Batch<D, S>
385where
386    D: IncrementalDecode,
387    S: DecodeRowSink,
388{
389    let pulse_ref: Option<&dyn Pulse> = pulse.as_ref().map(|p| p as &dyn Pulse);
390    let mut progress = Progressor::new(pulse_ref);
391    let mut steps = 0usize;
392    let outcome = loop {
393        if steps == quota.max_steps() {
394            break Ok(BatchEnd::Yield);
395        }
396        let input = window.input();
397        let p = match decoder.step(input, &mut sink, pulse_ref) {
398            Ok(p) => p,
399            Err(e) => break Err(BatchFail::Step(e)),
400        };
401        steps += 1;
402        progress.observe(&decoder, &p.event);
403        if p.event == Event::Need
404            && let Err(v) = check_need(decoder.requested(), &input, p.consumed)
405        {
406            break Err(BatchFail::Protocol(v));
407        }
408        window.consume(p.consumed);
409        match p.event {
410            Event::Need => break Ok(BatchEnd::Need),
411            Event::Done(info) => break Ok(BatchEnd::Done(info)),
412            _ => continue,
413        }
414    };
415    Batch {
416        decoder,
417        sink,
418        window,
419        outcome,
420    }
421}
422
423/// Satisfy `requests` into `window` from `source`. Range requests are
424/// coalesced and read with one `read_ranges` call; the first one's bytes
425/// become the window, later ones wait in `fetched`.
426// Written out rather than `async fn` so the `Send` bound is proven here,
427// for every type parameter: with `async fn`, rustc's higher-ranked
428// lifetime check rejects `tokio::spawn` of the caller's future.
429#[allow(clippy::manual_async_fn)]
430fn fill<'a, Src: AsyncSource + ?Sized, E: 'a>(
431    source: &'a mut Src,
432    window: &'a mut Window,
433    fetched: &'a mut Vec<(u64, Vec<u8>)>,
434    requests: &'a [InputRequest],
435    opts: DriveOptions,
436) -> impl Future<Output = Result<(), AsyncDriveError<E>>> + Send + 'a {
437    async move {
438        let limits = opts.limits;
439        let request = admission::select(requests, limits).map_err(AsyncDriveError::Input)?;
440        match request {
441            InputRequest::AtLeast { offset, min_len } => {
442                window.position(offset, fetched);
443                // Read until the request is met (or the source ends), asking
444                // for up to `read_size` more each time; a network source returns
445                // what has arrived, so the decoder steps as soon as it can.
446                let target = read_target(min_len, opts.read_size)
447                    .min(limits.max_input_bytes())
448                    .min(usize::try_from(u64::MAX - offset).unwrap_or(usize::MAX));
449                admission::make_room(window, fetched, target, limits);
450                while window.len() < min_len.max(1) && window.total != Some(window.end()) {
451                    limits.reading().map_err(AsyncDriveError::Input)?;
452                    let end = window.end();
453                    let want = (target - window.len().min(target))
454                        .min(usize::try_from(u64::MAX - end).unwrap_or(usize::MAX));
455                    let spare = window
456                        .try_spare(
457                            want,
458                            limits.max_input_bytes() - admission::capacity(fetched),
459                        )
460                        .map_err(AsyncDriveError::Input)?;
461                    let n = source
462                        .read_at(end, spare)
463                        .await
464                        .map_err(AsyncDriveError::Source)?;
465                    window.commit(n);
466                    if n == 0 {
467                        window.total = Some(end);
468                    }
469                }
470            }
471            InputRequest::Range(r) => {
472                if r.offset >= window.base() && r.end() <= window.end() {
473                    window.consume((r.offset - window.base()) as usize);
474                    return Ok(());
475                }
476                if let Some(i) = fetched
477                    .iter()
478                    .position(|(off, buf)| r.offset >= *off && r.end() <= off + buf.len() as u64)
479                {
480                    let (off, buf) = fetched.swap_remove(i);
481                    window.adopt(off, buf);
482                    window.consume((r.offset - off) as usize);
483                    return Ok(());
484                }
485                if window.position(r.offset, fetched) {
486                    let missing = (r.end() - window.end()) as usize;
487                    let offset = window.end();
488                    limits.reading().map_err(AsyncDriveError::Input)?;
489                    admission::make_room(window, fetched, r.length as usize, limits);
490                    let spare = window
491                        .try_spare(
492                            missing,
493                            limits.max_input_bytes() - admission::capacity(fetched),
494                        )
495                        .map_err(AsyncDriveError::Input)?;
496                    read_full(source, offset, spare)
497                        .await
498                        .map_err(AsyncDriveError::Source)?;
499                    window.commit(missing);
500                    return Ok(());
501                }
502                let mut coalesced = Vec::new();
503                admission::plan(r, requests, opts.max_gap, limits, &mut coalesced)
504                    .map_err(AsyncDriveError::Input)?;
505                window.release();
506                let mut pending = core::mem::take(fetched);
507                admission::allocate_ranges(&coalesced, limits, &mut pending)
508                    .map_err(AsyncDriveError::Input)?;
509                {
510                    let mut reqs = Vec::new();
511                    admission::reserve(&mut reqs, pending.len()).map_err(AsyncDriveError::Input)?;
512                    reqs.extend(pending.iter_mut().map(|(o, b)| (*o, &mut b[..])));
513                    for batch in reqs.chunks_mut(limits.max_in_flight()) {
514                        source
515                            .read_ranges(batch)
516                            .await
517                            .map_err(AsyncDriveError::Source)?;
518                    }
519                }
520                // Cancellation drops pending storage without publishing zeros.
521                *fetched = pending;
522                let i = fetched
523                    .iter()
524                    .position(|(off, buf)| r.offset >= *off && r.end() <= off + buf.len() as u64)
525                    .expect("coalesced ranges cover every request");
526                let (off, buf) = fetched.swap_remove(i);
527                window.adopt(off, buf);
528                window.consume((r.offset - off) as usize);
529            }
530            InputRequest::Len => {
531                if window.total.is_none() {
532                    limits.reading().map_err(AsyncDriveError::Input)?;
533                    window.total = Some(source.len().await.map_err(AsyncDriveError::Source)?);
534                }
535            }
536            _ => return Err(AsyncDriveError::Protocol(ProtocolViolation::EmptyRequest)),
537        }
538        Ok(())
539    }
540}
541
542/// Drives any [`ByteStepper`](crate::ByteStepper) from an [`AsyncSource`]:
543/// the async twin of [`ReadAtCursor`](crate::ReadAtCursor).
544///
545/// Each `next` steps the stage on the calling task and awaits the source
546/// only when the stage needs bytes, so a container demuxer (cheap per step)
547/// streams packets from a socket or an object store without holding a
548/// thread during I/O and without becoming async itself. Range requests are
549/// coalesced and fetched with one [`AsyncSource::read_ranges`] per round.
550/// CPU-heavy stages (image decoders) use [`decode_async`], which moves the
551/// work off the async runtime.
552#[derive(Debug)]
553pub struct AsyncCursor {
554    opts: DriveOptions,
555    window: Window,
556    fetched: Vec<(u64, Vec<u8>)>,
557    started: bool,
558    steps_since_yield: usize,
559}
560
561impl AsyncCursor {
562    /// A cursor with the given read sizes.
563    pub fn new(opts: DriveOptions) -> Self {
564        Self {
565            opts,
566            window: Window::new(),
567            fetched: Vec::new(),
568            started: false,
569            steps_since_yield: 0,
570        }
571    }
572
573    /// Retained driver-owned payload capacity, excluding source caches and outputs.
574    pub fn retained_input_capacity(&self) -> usize {
575        self.window
576            .capacity()
577            .saturating_add(admission::capacity(&self.fetched))
578    }
579
580    /// Step `stage` until it produces an output, awaiting `source` for every
581    /// request. Pass the same source on every call.
582    pub async fn next<S, Src>(
583        &mut self,
584        source: &mut Src,
585        stage: &mut S,
586        pulse: Option<&dyn zencodec::Pulse>,
587    ) -> Result<crate::Yield<S::Output>, crate::DriveFailure<S::Error, BoxedError>>
588    where
589        S: crate::ByteStepper + ?Sized,
590        Src: AsyncSource + ?Sized,
591    {
592        use crate::{DriveFailure, Step, Yield};
593        if !self.started {
594            self.started = true;
595            self.window.total = source.len_hint();
596        }
597        loop {
598            if self.steps_since_yield == self.opts.work_quota.max_steps() {
599                crate::work_quota::yield_once().await;
600                self.steps_since_yield = 0;
601            }
602            let input = self.window.input();
603            let p = stage.step(input, pulse).map_err(DriveFailure::Step)?;
604            self.steps_since_yield += 1;
605            if matches!(p.step, Step::Need) {
606                check_need(stage.requested(), &input, p.consumed)
607                    .map_err(DriveFailure::Protocol)?;
608            }
609            self.window.consume(p.consumed);
610            match p.step {
611                Step::Output(o) => return Ok(Yield::Output(o)),
612                Step::Finished(o) => return Ok(Yield::Finished(o)),
613                Step::Need => {}
614            }
615            fill::<_, core::convert::Infallible>(
616                source,
617                &mut self.window,
618                &mut self.fetched,
619                stage.requested(),
620                self.opts,
621            )
622            .await
623            .map_err(|e| match e {
624                AsyncDriveError::Source(e) => DriveFailure::Source(e),
625                AsyncDriveError::Protocol(p) => DriveFailure::Protocol(p),
626                AsyncDriveError::Input(e) => DriveFailure::Input(e),
627                // `fill` reports only source and protocol failures.
628                _ => DriveFailure::Protocol(ProtocolViolation::EmptyRequest),
629            })?;
630        }
631    }
632}
633
634/// Write every byte `enc` has pending to `w`, then consume it: the async
635/// twin of [`write_pending`](crate::write_pending). Returns how many bytes
636/// were written.
637///
638/// Encoding is CPU work, so where `push_rows` runs is the caller's choice
639/// (a blocking thread, a compute pool); this only moves the bytes. Between
640/// pushes, awaiting it is the backpressure point: no more rows go in until
641/// the transport took the last batch. This also keeps the encoder's raw and
642/// intermediate state alive during the wait. For an admitted buffered job,
643/// completing into separately budgeted encoded storage can release that state
644/// earlier. On an error nothing is consumed and the transport holds an unknown
645/// prefix: abort it.
646pub async fn write_pending_async<E, W>(enc: &mut E, w: &mut W) -> std::io::Result<usize>
647where
648    E: Encoder,
649    W: tokio::io::AsyncWrite + Unpin + ?Sized,
650{
651    use tokio::io::AsyncWriteExt as _;
652    let n = enc.pending_output().len();
653    if n > 0 {
654        w.write_all(enc.pending_output()).await?;
655        enc.consume_output(n);
656    }
657    Ok(n)
658}
659
660/// Decode from an async source with no thread blocked on I/O. See the
661/// [module documentation](self).
662///
663/// Returns the output info and the sink. `pulse` cancels at the next step
664/// inside a batch and between batches, and receives the row total and
665/// per-strip progress.
666// Written out rather than `async fn` so the `Send` bound is proven here,
667// for every type parameter: with `async fn`, rustc's higher-ranked
668// lifetime check rejects `tokio::spawn` of the caller's future.
669#[allow(clippy::manual_async_fn)]
670pub fn decode_async<'a, D, S, Src, C>(
671    decoder: D,
672    sink: S,
673    source: &'a mut Src,
674    compute: C,
675    opts: DriveOptions,
676    pulse: Option<SharedPulse>,
677) -> impl Future<Output = Result<(OutputInfo, S), AsyncDriveError<D::Error>>> + Send + 'a
678where
679    D: IncrementalDecode + Send + 'static,
680    S: DecodeRowSink + Send + 'static,
681    Src: AsyncSource + ?Sized,
682    C: Compute + 'a,
683{
684    async move {
685        let mut state = Some((decoder, sink, Window::new()));
686        if let Some((_, _, w)) = state.as_mut() {
687            w.total = source.len_hint();
688        }
689        let mut fetched = Vec::new();
690        let mut requests: Vec<InputRequest> = Vec::new();
691        loop {
692            if let Some(p) = &pulse
693                && let Err(r) = p.check()
694            {
695                return Err(AsyncDriveError::Cancelled(r));
696            }
697            let (decoder, sink, window) = state.take().expect("restored after every batch");
698            let pulse_for_batch = pulse.clone();
699            let batch = run_on(&compute, move || {
700                run_batch(decoder, sink, window, pulse_for_batch, opts.work_quota)
701            })
702            .await
703            .map_err(widen)?;
704            let Batch {
705                decoder,
706                sink,
707                mut window,
708                outcome,
709            } = batch;
710            match outcome {
711                Ok(BatchEnd::Done(info)) => return Ok((info, sink)),
712                Ok(BatchEnd::Yield) => crate::work_quota::yield_once().await,
713                Ok(BatchEnd::Need) => {
714                    // Copied into a reused list: the decoder is not `Sync`, so it
715                    // cannot be borrowed across the reads.
716                    admission::copy_requests(decoder.requested(), opts.limits, &mut requests)
717                        .map_err(AsyncDriveError::Input)?;
718                    fill(source, &mut window, &mut fetched, &requests, opts).await?;
719                }
720                Err(BatchFail::Step(e)) => return Err(AsyncDriveError::Step(e)),
721                Err(BatchFail::Protocol(v)) => return Err(AsyncDriveError::Protocol(v)),
722            }
723            state = Some((decoder, sink, window));
724        }
725    }
726}
727
728/// Decode from an async source straight into a streaming encoder, writing
729/// encoded bytes to `out` after every batch. This does not establish an
730/// end-to-end memory bound: codec-owned raw/intermediate state and caller-owned
731/// input can remain live while the output is stalled. The current driver also
732/// drains pending output before finalizing the encoder. Select an operation
733/// strategy based on the actual configuration's retention and release behavior.
734///
735/// The encoder is told the image size through [`Encoder::begin`] when the
736/// decoder reaches its header; an encoder that answers with a streaming
737/// [`OutputTiming`](zencodec::encode::OutputTiming) then emits bytes as rows
738/// arrive, and one that answers
739/// [`AT_FINISH`](zencodec::encode::OutputTiming::AT_FINISH) writes
740/// everything at the end. Returns the number of bytes written.
741// Written out rather than `async fn` so the `Send` bound is proven here,
742// for every type parameter: with `async fn`, rustc's higher-ranked
743// lifetime check rejects `tokio::spawn` of the caller's future.
744#[allow(clippy::manual_async_fn)]
745pub fn transcode_async<'a, D, E, Src, W, C>(
746    decoder: D,
747    encoder: E,
748    source: &'a mut Src,
749    out: &'a mut W,
750    compute: C,
751    opts: DriveOptions,
752    pulse: Option<SharedPulse>,
753) -> impl Future<Output = Result<u64, AsyncDriveError<D::Error>>> + Send + 'a
754where
755    D: IncrementalDecode + Send + 'static,
756    E: Encoder + Send + 'static,
757    E::Error: Send + Sync,
758    Src: AsyncSource + ?Sized,
759    W: tokio::io::AsyncWrite + Unpin + Send + ?Sized,
760    C: Compute + 'a,
761{
762    async move {
763        use tokio::io::AsyncWriteExt;
764        let mut written = 0u64;
765        let mut state = Some((decoder, crate::EncoderSink::new(encoder), Window::new()));
766        if let Some((_, _, w)) = state.as_mut() {
767            w.total = source.len_hint();
768        }
769        let mut fetched = Vec::new();
770        let mut requests: Vec<InputRequest> = Vec::new();
771        loop {
772            if let Some(p) = &pulse
773                && let Err(r) = p.check()
774            {
775                return Err(AsyncDriveError::Cancelled(r));
776            }
777            let (decoder, sink, window) = state.take().expect("restored after every batch");
778            let pulse_for_batch = pulse.clone();
779            let batch = run_on(&compute, move || {
780                run_batch(decoder, sink, window, pulse_for_batch, opts.work_quota)
781            })
782            .await
783            .map_err(widen)?;
784            let Batch {
785                decoder,
786                mut sink,
787                mut window,
788                outcome,
789            } = batch;
790            let pending = sink.pending_output().len();
791            if pending > 0 && matches!(outcome, Ok(BatchEnd::Need | BatchEnd::Done(_))) {
792                out.write_all(sink.pending_output())
793                    .await
794                    .map_err(AsyncDriveError::Write)?;
795                sink.consume_output(pending);
796                written += pending as u64;
797            }
798            match outcome {
799                Ok(BatchEnd::Yield) => crate::work_quota::yield_once().await,
800                Ok(BatchEnd::Done(_)) => {
801                    // Finalization can perform most of a buffered encoder's CPU
802                    // work. Keep it on the same executor as row processing.
803                    let rest = run_on(&compute, move || sink.finish())
804                        .await
805                        .map_err(widen)?
806                        .map_err(|e| AsyncDriveError::Step(StepError::Sink(Box::new(e))))?;
807                    out.write_all(rest.data())
808                        .await
809                        .map_err(AsyncDriveError::Write)?;
810                    out.flush().await.map_err(AsyncDriveError::Write)?;
811                    return Ok(written + rest.data().len() as u64);
812                }
813                Ok(BatchEnd::Need) => {
814                    // Copied into a reused list: the decoder is not `Sync`, so it
815                    // cannot be borrowed across the reads.
816                    admission::copy_requests(decoder.requested(), opts.limits, &mut requests)
817                        .map_err(AsyncDriveError::Input)?;
818                    fill(source, &mut window, &mut fetched, &requests, opts).await?;
819                }
820                Err(BatchFail::Step(e)) => return Err(AsyncDriveError::Step(e)),
821                Err(BatchFail::Protocol(v)) => return Err(AsyncDriveError::Protocol(v)),
822            }
823            state = Some((decoder, sink, window));
824        }
825    }
826}
827
828// ───────────────────────────── pull-native bridge ─────────────────────────────
829
830/// Outcome of one non-blocking attempt in [`wait_with_stop`].
831enum Attempt<T> {
832    Ready(T),
833    Pending,
834    Closed,
835}
836
837/// Block until `attempt` is ready, re-checking `pulse` between attempts.
838///
839/// Without a pulse the caller uses the channel's own blocking call; with
840/// one, a short spin then a sleep that doubles from 50 µs to 2 ms bounds
841/// both the cancellation latency and the idle wakeups.
842fn wait_with_stop<T>(
843    pulse: Option<&dyn zencodec::Pulse>,
844    mut attempt: impl FnMut() -> Attempt<T>,
845) -> Result<T, AsyncDriveError<core::convert::Infallible>> {
846    let mut sleep = core::time::Duration::from_micros(50);
847    let mut spins = 0u32;
848    loop {
849        match attempt() {
850            Attempt::Ready(v) => return Ok(v),
851            Attempt::Closed => return Err(AsyncDriveError::Disconnected),
852            Attempt::Pending => {}
853        }
854        if let Some(s) = pulse
855            && let Err(reason) = s.check()
856        {
857            return Err(AsyncDriveError::Cancelled(reason));
858        }
859        if spins < 64 {
860            spins += 1;
861            core::hint::spin_loop();
862        } else {
863            std::thread::sleep(sleep);
864            sleep = (sleep * 2).min(core::time::Duration::from_millis(2));
865        }
866    }
867}
868
869/// A read the pull-native decoder is blocked on.
870enum PullRequest {
871    ReadAt { offset: u64, len: usize },
872    Ranges(Vec<ByteRange>),
873    Len,
874}
875
876enum PullReply {
877    Bytes(Vec<u8>),
878    Ranges(Vec<Vec<u8>>),
879    Len(u64),
880    Failed(BoxedError),
881}
882
883struct Links {
884    requests: mpsc::Sender<PullRequest>,
885    replies: mpsc::Receiver<PullReply>,
886    pulse: Option<SharedPulse>,
887}
888
889impl Links {
890    fn ask(
891        &mut self,
892        req: PullRequest,
893    ) -> Result<PullReply, AsyncDriveError<core::convert::Infallible>> {
894        let pulse: &dyn zencodec::Pulse = match &self.pulse {
895            Some(p) => p,
896            None => &NoPulse,
897        };
898        pulse.check().map_err(AsyncDriveError::Cancelled)?;
899        if self.pulse.is_none() {
900            self.requests
901                .blocking_send(req)
902                .map_err(|_| AsyncDriveError::Disconnected)?;
903            return self
904                .replies
905                .blocking_recv()
906                .ok_or(AsyncDriveError::Disconnected);
907        }
908        let mut slot = Some(req);
909        let tx = &self.requests;
910        wait_with_stop(Some(pulse), || {
911            let Some(r) = slot.take() else {
912                return Attempt::Ready(());
913            };
914            match tx.try_send(r) {
915                Ok(()) => Attempt::Ready(()),
916                Err(mpsc::error::TrySendError::Full(r)) => {
917                    slot = Some(r);
918                    Attempt::Pending
919                }
920                Err(mpsc::error::TrySendError::Closed(_)) => Attempt::Closed,
921            }
922        })?;
923        let rx = &mut self.replies;
924        wait_with_stop(Some(pulse), || match rx.try_recv() {
925            Ok(reply) => Attempt::Ready(reply),
926            Err(mpsc::error::TryRecvError::Empty) => Attempt::Pending,
927            Err(mpsc::error::TryRecvError::Disconnected) => Attempt::Closed,
928        })
929    }
930}
931
932/// [`DynReadAt`] whose reads block on the async side's [`AsyncSource`].
933struct ChannelReadAt {
934    links: Links,
935    len: Option<u64>,
936}
937
938fn bridge_err(e: AsyncDriveError<core::convert::Infallible>) -> BoxedError {
939    Box::new(e.into_infallible_display())
940}
941
942impl AsyncDriveError<core::convert::Infallible> {
943    fn into_infallible_display(self) -> BridgeError {
944        BridgeError(self)
945    }
946}
947
948/// An [`AsyncDriveError`] carried through a pull-native codec's error.
949#[derive(Debug)]
950pub struct BridgeError(pub AsyncDriveError<core::convert::Infallible>);
951
952impl fmt::Display for BridgeError {
953    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
954        fmt::Display::fmt(&self.0, f)
955    }
956}
957
958impl core::error::Error for BridgeError {
959    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
960        core::error::Error::source(&self.0)
961    }
962}
963
964impl DynReadAt for ChannelReadAt {
965    fn len(&mut self) -> Result<u64, BoxedError> {
966        if let Some(len) = self.len {
967            return Ok(len);
968        }
969        match self.links.ask(PullRequest::Len).map_err(bridge_err)? {
970            PullReply::Len(len) => {
971                self.len = Some(len);
972                Ok(len)
973            }
974            PullReply::Failed(e) => Err(e),
975            _ => Err("pull bridge: unexpected reply".into()),
976        }
977    }
978
979    fn len_hint(&self) -> Option<u64> {
980        self.len
981    }
982
983    fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, BoxedError> {
984        match self
985            .links
986            .ask(PullRequest::ReadAt {
987                offset,
988                len: out.len(),
989            })
990            .map_err(bridge_err)?
991        {
992            PullReply::Bytes(b) => {
993                out[..b.len()].copy_from_slice(&b);
994                if b.len() < out.len() {
995                    self.len = Some(offset + b.len() as u64);
996                }
997                Ok(b.len())
998            }
999            PullReply::Failed(e) => Err(e),
1000            _ => Err("pull bridge: unexpected reply".into()),
1001        }
1002    }
1003
1004    fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), BoxedError> {
1005        let n = self.read_at(offset, out)?;
1006        if n < out.len() {
1007            return Err(Box::new(SourceError::UnexpectedEof {
1008                offset,
1009                len: out.len() as u64,
1010                total: self.len,
1011            }));
1012        }
1013        Ok(())
1014    }
1015
1016    fn read_many_at(&mut self, reqs: &mut [(u64, &mut [u8])]) -> Result<(), BoxedError> {
1017        let ranges = reqs
1018            .iter()
1019            .map(|(o, b)| ByteRange::new(*o, b.len() as u64))
1020            .collect();
1021        match self
1022            .links
1023            .ask(PullRequest::Ranges(ranges))
1024            .map_err(bridge_err)?
1025        {
1026            PullReply::Ranges(bufs) => {
1027                for ((_, out), b) in reqs.iter_mut().zip(bufs) {
1028                    out.copy_from_slice(&b);
1029                }
1030                Ok(())
1031            }
1032            PullReply::Failed(e) => Err(e),
1033            _ => Err("pull bridge: unexpected reply".into()),
1034        }
1035    }
1036}
1037
1038/// Run a pull-native decode on one `compute` thread. `run` receives a
1039/// [`DynReadAt`] whose reads block on `source`, so a codec written against
1040/// [`push_decoder_from_source`](zencodec::decode::DecodeJob::push_decoder_from_source)
1041/// streams from an async transport, with `read_many_at` becoming one
1042/// concurrent [`AsyncSource::read_ranges`].
1043///
1044/// `pulse` cancels at the next read: each read checks it, and the wait for
1045/// the reply re-checks it. Row progress is whatever the sink observes; the
1046/// bridge cannot see rows written by code it does not drive.
1047// Written out rather than `async fn` so the `Send` bound is proven here,
1048// for every type parameter: with `async fn`, rustc's higher-ranked
1049// lifetime check rejects `tokio::spawn` of the caller's future.
1050#[allow(clippy::manual_async_fn)]
1051pub fn pull_decode_on_thread<'a, R, S, Src, C>(
1052    run: R,
1053    mut sink: S,
1054    source: &'a mut Src,
1055    compute: C,
1056    pulse: Option<SharedPulse>,
1057) -> impl Future<Output = Result<(OutputInfo, S), AsyncDriveError<BoxedError>>> + Send + 'a
1058where
1059    R: FnOnce(&mut dyn DynReadAt, &mut dyn DecodeRowSink) -> Result<OutputInfo, BoxedError>
1060        + Send
1061        + 'static,
1062    S: DecodeRowSink + Send + 'static,
1063    Src: AsyncSource + ?Sized,
1064    C: Compute + 'a,
1065{
1066    async move {
1067        let (req_tx, mut req_rx) = mpsc::channel(1);
1068        let (rep_tx, rep_rx) = mpsc::channel(1);
1069        let (done_tx, mut done_rx) = oneshot::channel();
1070        let mut reader = ChannelReadAt {
1071            links: Links {
1072                requests: req_tx,
1073                replies: rep_rx,
1074                pulse,
1075            },
1076            len: source.len_hint(),
1077        };
1078        compute.spawn(Box::new(move || {
1079            let result = run(&mut reader, &mut sink);
1080            drop(reader);
1081            let _ = done_tx.send(result.map(|info| (info, sink)));
1082        }));
1083        let finish = |done: Result<
1084            Result<(OutputInfo, S), BoxedError>,
1085            oneshot::error::RecvError,
1086        >| match done {
1087            Ok(Ok(v)) => Ok(v),
1088            Ok(Err(e)) => match e.downcast::<BridgeError>() {
1089                Ok(b) => Err(widen(b.0)),
1090                Err(e) => Err(AsyncDriveError::Step(StepError::Codec(e))),
1091            },
1092            Err(_) => Err(AsyncDriveError::Disconnected),
1093        };
1094        let mut requests_open = true;
1095        loop {
1096            // Wait for the decode to finish or ask for bytes. Serving a request
1097            // also races the decode finishing: a decode cancelled by its pulse
1098            // while the source hangs returns without waiting for the source.
1099            let req = tokio::select! {
1100                biased;
1101                done = &mut done_rx => return finish(done),
1102                req = req_rx.recv(), if requests_open => req,
1103            };
1104            let Some(req) = req else {
1105                requests_open = false;
1106                continue;
1107            };
1108            let reply = tokio::select! {
1109                biased;
1110                done = &mut done_rx => return finish(done),
1111                reply = serve_pull(source, req) => reply,
1112            };
1113            // The decode thread waits for this reply; if it has gone, the next
1114            // loop turn collects its result.
1115            let _ = rep_tx.send(reply).await;
1116        }
1117    }
1118}
1119
1120/// Answer one blocking read from the pull-native decode thread.
1121async fn serve_pull<Src: AsyncSource + ?Sized>(source: &mut Src, req: PullRequest) -> PullReply {
1122    match req {
1123        PullRequest::ReadAt { offset, len } => {
1124            // `DynReadAt::read_at` is short only at the end: read until full
1125            // or the source ends.
1126            let mut buf = vec![0u8; len];
1127            let mut filled = 0;
1128            while filled < len {
1129                match source
1130                    .read_at(offset + filled as u64, &mut buf[filled..])
1131                    .await
1132                {
1133                    Ok(0) => break,
1134                    Ok(n) => filled += n,
1135                    Err(e) => return PullReply::Failed(e),
1136                }
1137            }
1138            buf.truncate(filled);
1139            PullReply::Bytes(buf)
1140        }
1141        PullRequest::Ranges(ranges) => {
1142            let mut bufs: Vec<Vec<u8>> = ranges
1143                .iter()
1144                .map(|r| vec![0u8; r.length as usize])
1145                .collect();
1146            let result = {
1147                let mut reqs: Vec<(u64, &mut [u8])> = ranges
1148                    .iter()
1149                    .zip(bufs.iter_mut())
1150                    .map(|(r, b)| (r.offset, &mut b[..]))
1151                    .collect();
1152                source.read_ranges(&mut reqs).await
1153            };
1154            match result {
1155                Ok(()) => PullReply::Ranges(bufs),
1156                Err(e) => PullReply::Failed(e),
1157            }
1158        }
1159        PullRequest::Len => match source.len().await {
1160            Ok(n) => PullReply::Len(n),
1161            Err(e) => PullReply::Failed(e),
1162        },
1163    }
1164}
1165
1166// ───────────────────────────── strips to the async side ─────────────────────────────
1167
1168/// What a [`ChannelRowSink`] sends to the async side.
1169#[derive(Debug)]
1170#[non_exhaustive]
1171pub enum StripEvent {
1172    /// The decoder announced the output geometry.
1173    Begin {
1174        /// Output width.
1175        width: u32,
1176        /// Output height.
1177        height: u32,
1178        /// Output pixel format.
1179        descriptor: PixelDescriptor,
1180    },
1181    /// One finished strip.
1182    Strip {
1183        /// Row position, pass and finality.
1184        meta: BatchMeta,
1185        /// The rows.
1186        pixels: PixelBuffer,
1187    },
1188    /// The decoder finished.
1189    Finish,
1190}
1191
1192/// A sink that streams finished strips to the async side over a bounded
1193/// channel. Each strip is an owned [`PixelBuffer`]; `provide_*` hands the
1194/// decoder a fresh strip and ships the previous one, blocking the decode
1195/// thread while the channel is full, so a slow consumer throttles the
1196/// decoder instead of the decoder filling memory.
1197///
1198/// It blocks, so it belongs on a thread outside the runtime: a [`Compute`]
1199/// such as `spawn_blocking` or a rayon pool, or [`pull_decode_on_thread`].
1200/// Under [`Inline`] the batch runs on a runtime worker, where tokio refuses
1201/// to block.
1202pub struct ChannelRowSink {
1203    tx: mpsc::Sender<StripEvent>,
1204    current: Option<(BatchMeta, PixelBuffer)>,
1205    provisional: bool,
1206    pulse: Option<SharedPulse>,
1207}
1208
1209impl fmt::Debug for ChannelRowSink {
1210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1211        f.debug_struct("ChannelRowSink")
1212            .field("provisional", &self.provisional)
1213            .field("pulse", &self.pulse.is_some())
1214            .finish_non_exhaustive()
1215    }
1216}
1217
1218impl ChannelRowSink {
1219    /// A sink sending into `tx`. `provisional` says whether progressive
1220    /// passes should be forwarded as they arrive.
1221    pub fn new(tx: mpsc::Sender<StripEvent>, provisional: bool) -> Self {
1222        Self {
1223            tx,
1224            current: None,
1225            provisional,
1226            pulse: None,
1227        }
1228    }
1229
1230    /// Re-check `pulse` while blocked on a full channel, so a cancelled
1231    /// decode is not held hostage by a consumer that stopped consuming.
1232    pub fn with_pulse(mut self, pulse: SharedPulse) -> Self {
1233        self.pulse = Some(pulse);
1234        self
1235    }
1236
1237    fn send(&self, event: StripEvent) -> Result<(), SinkError> {
1238        match &self.pulse {
1239            None => self
1240                .tx
1241                .blocking_send(event)
1242                .map_err(|_| SinkError::from("strip consumer went away")),
1243            Some(pulse) => {
1244                let pulse: &dyn zencodec::Pulse = pulse;
1245                let mut slot = Some(event);
1246                wait_with_stop(Some(pulse), || {
1247                    let Some(ev) = slot.take() else {
1248                        return Attempt::Ready(());
1249                    };
1250                    match self.tx.try_send(ev) {
1251                        Ok(()) => Attempt::Ready(()),
1252                        Err(mpsc::error::TrySendError::Full(ev)) => {
1253                            slot = Some(ev);
1254                            Attempt::Pending
1255                        }
1256                        Err(mpsc::error::TrySendError::Closed(_)) => Attempt::Closed,
1257                    }
1258                })
1259                .map_err(|e| SinkError::from(alloc::format!("{e}")))
1260            }
1261        }
1262    }
1263
1264    fn ship(&mut self) -> Result<(), SinkError> {
1265        if let Some((meta, pixels)) = self.current.take() {
1266            self.send(StripEvent::Strip { meta, pixels })?;
1267        }
1268        Ok(())
1269    }
1270
1271    fn strip(
1272        &mut self,
1273        meta: BatchMeta,
1274        height: u32,
1275        width: u32,
1276        descriptor: PixelDescriptor,
1277    ) -> Result<PixelSliceMut<'_>, SinkError> {
1278        self.ship()?;
1279        let (_, pixels) = self
1280            .current
1281            .insert((meta, PixelBuffer::new(width, height, descriptor)));
1282        Ok(pixels.as_slice_mut())
1283    }
1284}
1285
1286impl DecodeRowSink for ChannelRowSink {
1287    fn begin(
1288        &mut self,
1289        width: u32,
1290        height: u32,
1291        descriptor: PixelDescriptor,
1292    ) -> Result<(), SinkError> {
1293        self.send(StripEvent::Begin {
1294            width,
1295            height,
1296            descriptor,
1297        })
1298    }
1299
1300    fn provide_next_buffer(
1301        &mut self,
1302        y: u32,
1303        height: u32,
1304        width: u32,
1305        descriptor: PixelDescriptor,
1306    ) -> Result<PixelSliceMut<'_>, SinkError> {
1307        self.strip(BatchMeta::final_rows(y), height, width, descriptor)
1308    }
1309
1310    fn accepts_provisional(&self) -> bool {
1311        self.provisional
1312    }
1313
1314    fn provide_pass_buffer(
1315        &mut self,
1316        meta: BatchMeta,
1317        height: u32,
1318        width: u32,
1319        descriptor: PixelDescriptor,
1320    ) -> Result<PixelSliceMut<'_>, SinkError> {
1321        self.strip(meta, height, width, descriptor)
1322    }
1323
1324    fn finish(&mut self) -> Result<(), SinkError> {
1325        self.ship()?;
1326        self.send(StripEvent::Finish)
1327    }
1328}