Skip to main content

j2k_core/
batch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use core::num::NonZeroUsize;
4
5use crate::{backend::BackendRequest, pixel::PixelFormat, scale::Downscale, types::Rect};
6
7mod allocation;
8mod collection;
9
10#[doc(hidden)]
11pub use allocation::{
12    checked_batch_count_product, checked_batch_count_sum, try_batch_reserve_for_push,
13    try_batch_reserve_to, BatchAllocationBudget, BatchAllocationRequest,
14};
15pub use collection::{
16    try_collect_indexed_batch_results, try_collect_ordered_batch_results,
17    try_collect_ordered_batch_results_with_limits,
18};
19
20/// Worker configuration for CPU tile batches.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct TileBatchOptions {
23    /// Worker count. `None` asks the codec crate to use available parallelism.
24    pub workers: Option<NonZeroUsize>,
25}
26
27impl TileBatchOptions {
28    /// Construct tile-batch options with an optional fixed worker count.
29    #[must_use]
30    pub const fn new(workers: Option<NonZeroUsize>) -> Self {
31        Self { workers }
32    }
33}
34
35/// Indexed result produced by one tile-batch worker.
36#[doc(hidden)]
37pub type IndexedBatchResult<T, E> = (usize, Result<T, E>);
38
39/// One ordered batch result slot written by exactly one worker.
40#[doc(hidden)]
41pub type BatchResultSlot<T, E> = Option<Result<T, E>>;
42
43/// One full-tile decode request.
44pub struct TileDecodeJob<'i, 'o> {
45    /// Compressed tile bytes.
46    pub input: &'i [u8],
47    /// Caller-owned output buffer for this tile.
48    pub out: &'o mut [u8],
49    /// Distance in bytes between output rows.
50    pub stride: usize,
51}
52
53/// One region tile decode request.
54pub struct TileRegionDecodeJob<'i, 'o> {
55    /// Compressed tile bytes.
56    pub input: &'i [u8],
57    /// Caller-owned output buffer for this tile.
58    pub out: &'o mut [u8],
59    /// Distance in bytes between output rows.
60    pub stride: usize,
61    /// Region of interest in source-image coordinates.
62    pub roi: Rect,
63}
64
65/// One scaled tile decode request.
66pub struct TileScaledDecodeJob<'i, 'o> {
67    /// Compressed tile bytes.
68    pub input: &'i [u8],
69    /// Caller-owned output buffer for this tile.
70    pub out: &'o mut [u8],
71    /// Distance in bytes between output rows.
72    pub stride: usize,
73    /// Downscale factor applied to the full-tile decode.
74    pub scale: Downscale,
75}
76
77/// One region+scaled tile decode request.
78pub struct TileRegionScaledDecodeJob<'i, 'o> {
79    /// Compressed tile bytes.
80    pub input: &'i [u8],
81    /// Caller-owned output buffer for this tile.
82    pub out: &'o mut [u8],
83    /// Distance in bytes between output rows.
84    pub stride: usize,
85    /// Region of interest in source-image coordinates.
86    pub roi: Rect,
87    /// Downscale factor applied to the region decode.
88    pub scale: Downscale,
89}
90
91/// One region+scaled tile device decode request.
92pub struct TileRegionScaledDeviceDecodeRequest<'i> {
93    /// Compressed tile bytes.
94    pub input: &'i [u8],
95    /// Pixel format requested for the decoded surface.
96    pub fmt: PixelFormat,
97    /// Region of interest in source-image coordinates.
98    pub roi: Rect,
99    /// Downscale factor applied to the region decode.
100    pub scale: Downscale,
101    /// Backend requested for the returned surface.
102    pub backend: BackendRequest,
103}
104
105/// Error returned by tile batches, annotated with the failing input index.
106#[derive(Debug)]
107pub struct TileBatchError<E> {
108    /// Index of the first failing tile in input order.
109    pub index: usize,
110    /// Decode error reported for that tile.
111    pub source: E,
112}
113
114impl<E: core::fmt::Display> core::fmt::Display for TileBatchError<E> {
115    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        write!(f, "tile {} decode failed: {}", self.index, self.source)
117    }
118}
119
120impl<E: core::error::Error + 'static> core::error::Error for TileBatchError<E> {
121    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
122        Some(&self.source)
123    }
124}
125
126/// Failure in batch scheduling, allocation, or worker-result collection.
127///
128/// These failures are deliberately separate from [`TileBatchError`]: no tile
129/// index exists for an allocator failure or an internal scheduler invariant.
130#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
131#[non_exhaustive]
132pub enum BatchInfrastructureError {
133    /// An internal planning boundary was invoked without any submitted jobs.
134    #[error("batch plan requires at least one job")]
135    EmptyBatchPlan,
136    /// Batch-owned metadata would exceed the operation's host-memory budget.
137    #[error("{what} is too large: requested {requested} bytes, cap {cap}")]
138    AllocationTooLarge {
139        /// Name of the batch-owned allocation or live set.
140        what: &'static str,
141        /// Requested live byte count, saturated to `usize::MAX` on overflow.
142        requested: usize,
143        /// Maximum permitted live byte count.
144        cap: usize,
145    },
146    /// The host allocator rejected an otherwise cap-valid reservation.
147    #[error("host allocation failed for {bytes} bytes while allocating {what}")]
148    HostAllocationFailed {
149        /// Name of the batch-owned allocation.
150        what: &'static str,
151        /// Requested host byte count.
152        bytes: usize,
153    },
154    /// The host could not create a requested scoped worker.
155    #[error("failed to spawn batch worker {worker}")]
156    WorkerSpawnFailed {
157        /// Zero-based worker index in the planned batch.
158        worker: usize,
159    },
160    /// A worker unwound before completing its assigned jobs.
161    #[error("batch worker {worker} panicked")]
162    WorkerPanicked {
163        /// Zero-based worker index in the planned batch.
164        worker: usize,
165    },
166    /// Planned work referenced a worker slot that does not exist.
167    #[error("batch worker slot {worker} is outside retained slot count {available}")]
168    WorkerSlotMissing {
169        /// Missing zero-based worker slot.
170        worker: usize,
171        /// Number of worker slots available to the scheduler.
172        available: usize,
173    },
174    /// A worker managed by a shared parallel runtime unwound.
175    #[error("parallel batch worker panicked")]
176    ParallelWorkerPanicked,
177    /// Shared batch state was poisoned by an earlier unwind.
178    #[error("batch scheduler state was poisoned")]
179    SchedulerPoisoned,
180    /// A worker reported an index outside the submitted job range.
181    #[error("batch result index {index} is outside job count {job_count}")]
182    ResultIndexOutOfBounds {
183        /// Invalid worker-reported index.
184        index: usize,
185        /// Number of submitted jobs.
186        job_count: usize,
187    },
188    /// More than one worker result claimed the same job index.
189    #[error("batch result index {index} was reported more than once")]
190    DuplicateResult {
191        /// Duplicated job index.
192        index: usize,
193    },
194    /// No worker result was produced for a submitted job.
195    #[error("batch worker result missing for job {index}")]
196    MissingResult {
197        /// Missing job index.
198        index: usize,
199    },
200    /// Collector state contradicted a result kind it had just inspected.
201    #[error("batch result {index} changed kind during ordered collection")]
202    ResultKindMismatch {
203        /// Job index whose result kind contradicted the inspected state.
204        index: usize,
205    },
206}
207
208/// Error returned by a fallible batch boundary.
209///
210/// `Tile` identifies an input-specific codec failure. `Infrastructure`
211/// identifies failures for which assigning a tile index would be misleading.
212#[derive(Debug)]
213#[non_exhaustive]
214pub enum BatchDecodeError<E> {
215    /// The first codec failure in caller input order.
216    Tile(TileBatchError<E>),
217    /// Allocation, scheduling, or collection failed independently of a tile.
218    Infrastructure(BatchInfrastructureError),
219}
220
221impl<E> BatchDecodeError<E> {
222    /// Return the indexed codec failure when this is a tile-specific error.
223    #[must_use]
224    pub const fn tile_error(&self) -> Option<&TileBatchError<E>> {
225        match self {
226            Self::Tile(error) => Some(error),
227            Self::Infrastructure(_) => None,
228        }
229    }
230
231    /// Return the infrastructure failure when no tile index applies.
232    #[must_use]
233    pub const fn infrastructure_error(&self) -> Option<&BatchInfrastructureError> {
234        match self {
235            Self::Tile(_) => None,
236            Self::Infrastructure(error) => Some(error),
237        }
238    }
239}
240
241impl<E: core::fmt::Display> core::fmt::Display for BatchDecodeError<E> {
242    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
243        match self {
244            Self::Tile(error) => error.fmt(f),
245            Self::Infrastructure(error) => error.fmt(f),
246        }
247    }
248}
249
250impl<E: core::error::Error + 'static> core::error::Error for BatchDecodeError<E> {
251    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
252        match self {
253            Self::Tile(error) => Some(error),
254            Self::Infrastructure(error) => Some(error),
255        }
256    }
257}
258
259impl<E> From<BatchInfrastructureError> for BatchDecodeError<E> {
260    fn from(error: BatchInfrastructureError) -> Self {
261        Self::Infrastructure(error)
262    }
263}
264
265impl<E> From<TileBatchError<E>> for BatchDecodeError<E> {
266    fn from(error: TileBatchError<E>) -> Self {
267        Self::Tile(error)
268    }
269}
270
271/// Resolve the number of CPU workers for a tile batch.
272///
273/// `available_workers` should be the host's available parallelism. Passing
274/// `0` is accepted and treated as one available worker.
275#[doc(hidden)]
276pub fn tile_batch_worker_count(
277    batch_size: usize,
278    options: TileBatchOptions,
279    available_workers: usize,
280) -> usize {
281    if batch_size <= 1 {
282        return 1;
283    }
284    let workers = options.workers.map_or(available_workers, NonZeroUsize::get);
285    workers.max(1).min(batch_size)
286}