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