Skip to main content

j2k_native/j2c/decode/
workspace.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Reusable decoder workspace ownership, policy, and diagnostics.
4
5use super::{DecompositionStorage, OutputRegion, TileDecodeContext};
6#[cfg(feature = "parallel")]
7use crate::{HtCodeBlockDecodeWorkspace, J2kCodeBlockDecodeWorkspace};
8#[cfg(feature = "parallel")]
9use alloc::vec::Vec;
10
11/// A worker's slot in a retained bank of code-block decode workspaces.
12#[cfg(feature = "parallel")]
13pub(in crate::j2c::decode) struct TaskWorkspace<W> {
14    pub(in crate::j2c::decode) workspace: W,
15    // Componentwise maxima make orthogonal edge shapes reusable without growth.
16    pub(in crate::j2c::decode) prepared_width: u32,
17    pub(in crate::j2c::decode) prepared_height: u32,
18}
19
20#[cfg(feature = "parallel")]
21pub(in crate::j2c::decode) type HtTaskWorkspace = TaskWorkspace<HtCodeBlockDecodeWorkspace>;
22
23/// A code-block decode workspace that a retained bank keeps per worker.
24#[cfg(feature = "parallel")]
25pub(in crate::j2c::decode) trait TaskScratch: Default + Send {
26    /// The bytes budgeted for blocks of up to `width` x `height`.
27    fn planned_bytes(width: u32, height: u32) -> crate::Result<usize>;
28    fn prepare_for(&mut self, width: u32, height: u32) -> crate::Result<()>;
29    fn owned_bytes(&self) -> crate::Result<usize>;
30}
31
32#[cfg(feature = "parallel")]
33impl TaskScratch for HtCodeBlockDecodeWorkspace {
34    fn planned_bytes(width: u32, height: u32) -> crate::Result<usize> {
35        crate::j2c::ht_block_decode::ht_decode_workspace_bytes(width, height)
36    }
37
38    fn prepare_for(&mut self, width: u32, height: u32) -> crate::Result<()> {
39        self.reserve(width, height)
40    }
41
42    fn owned_bytes(&self) -> crate::Result<usize> {
43        self.allocated_bytes()
44    }
45}
46
47#[cfg(feature = "parallel")]
48impl TaskScratch for J2kCodeBlockDecodeWorkspace {
49    fn planned_bytes(width: u32, height: u32) -> crate::Result<usize> {
50        crate::j2c::bitplane::classic_decode_workspace_bytes(width, height)
51    }
52
53    fn prepare_for(&mut self, width: u32, height: u32) -> crate::Result<()> {
54        self.prepare(width, height)
55    }
56
57    fn owned_bytes(&self) -> crate::Result<usize> {
58        self.allocated_bytes()
59    }
60}
61
62#[cfg(feature = "parallel")]
63pub(in crate::j2c::decode) fn task_workspace_bytes<W: TaskScratch>(
64    slots: &Vec<TaskWorkspace<W>>,
65) -> crate::Result<usize> {
66    let mut bytes = slots
67        .capacity()
68        .checked_mul(core::mem::size_of::<TaskWorkspace<W>>())
69        .ok_or(crate::ValidationError::ImageTooLarge)?;
70    for slot in slots {
71        bytes = bytes
72            .checked_add(slot.workspace.owned_bytes()?)
73            .ok_or(crate::ValidationError::ImageTooLarge)?;
74    }
75    Ok(bytes)
76}
77
78/// CPU parallelism policy for native JPEG 2000 decode.
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
80pub enum CpuDecodeParallelism {
81    /// Allow concurrent tile decode and internal code-block parallelism.
82    #[default]
83    Auto,
84    /// Keep code-block decode serial for callers that already parallelize tiles.
85    Serial,
86}
87
88/// Observable counters and retained ownership for a reusable decoder workspace.
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct DecoderWorkspaceStats {
91    decode_calls: u64,
92    component_owner_reuses: u64,
93    tier1_owner_reuses: u64,
94    idwt_owner_reuses: u64,
95    scratch_capacity_retries: u64,
96    retained_component_bytes: usize,
97    retained_tier1_bytes: usize,
98    retained_idwt_bytes: usize,
99}
100
101impl DecoderWorkspaceStats {
102    /// Number of native image decode calls made with this workspace.
103    #[must_use]
104    pub const fn decode_calls(self) -> u64 {
105        self.decode_calls
106    }
107
108    /// Number of calls that began with reusable decoded-component owners.
109    #[must_use]
110    pub const fn component_owner_reuses(self) -> u64 {
111        self.component_owner_reuses
112    }
113
114    /// Number of calls that began with reusable Tier-1 allocations.
115    #[must_use]
116    pub const fn tier1_owner_reuses(self) -> u64 {
117        self.tier1_owner_reuses
118    }
119
120    /// Number of calls that began with reusable IDWT allocations.
121    #[must_use]
122    pub const fn idwt_owner_reuses(self) -> u64 {
123        self.idwt_owner_reuses
124    }
125
126    /// Number of retained-scratch evictions followed by a fresh retry.
127    #[must_use]
128    pub const fn scratch_capacity_retries(self) -> u64 {
129        self.scratch_capacity_retries
130    }
131
132    /// Retained component-owner capacity after the most recent completed core decode.
133    #[must_use]
134    pub const fn retained_component_bytes(self) -> usize {
135        self.retained_component_bytes
136    }
137
138    /// Retained classic and HT Tier-1 capacity after the most recent call.
139    #[must_use]
140    pub const fn retained_tier1_bytes(self) -> usize {
141        self.retained_tier1_bytes
142    }
143
144    /// Retained floating-point and exact-integer IDWT capacity after the most recent call.
145    #[must_use]
146    pub const fn retained_idwt_bytes(self) -> usize {
147        self.retained_idwt_bytes
148    }
149
150    /// Total retained lifetime-free decode scratch after the most recent call.
151    #[must_use]
152    pub const fn retained_scratch_bytes(self) -> usize {
153        self.retained_tier1_bytes
154            .saturating_add(self.retained_idwt_bytes)
155    }
156}
157
158/// Lifetime-free allocation owner that can be moved between borrowing decoder contexts.
159///
160/// Parsed packet and tile graphs remain in [`DecoderContext`] and are always
161/// released before this value is recovered. Decoded component, Tier-1, and
162/// IDWT allocations are retained for reuse with unrelated encoded inputs.
163#[derive(Default)]
164pub struct DecoderWorkspace {
165    tile_decode_context: TileDecodeContext,
166    pub(super) cpu_decode_parallelism: CpuDecodeParallelism,
167    stats: DecoderWorkspaceStats,
168}
169
170impl DecoderWorkspace {
171    /// Return reuse counters and retained allocation sizes.
172    #[must_use]
173    pub const fn stats(&self) -> DecoderWorkspaceStats {
174        self.stats
175    }
176}
177
178/// A decoder context for decoding JPEG2000 images.
179pub struct DecoderContext<'a> {
180    pub(crate) tile_decode_context: TileDecodeContext,
181    pub(crate) storage: DecompositionStorage<'a>,
182    #[cfg(test)]
183    pub(crate) direct_plan_code_blocks: usize,
184    pub(super) cpu_decode_parallelism: CpuDecodeParallelism,
185    /// Whether region decodes may use the row-streamed region IDWT. On by
186    /// default; it applies only to region decodes without a hook, of images
187    /// without palette, component-definition or sYCC/CIELab post-processing.
188    /// Tests turn it off to compare against the whole-window IDWT.
189    pub(crate) line_region_idwt: bool,
190    workspace_stats: DecoderWorkspaceStats,
191}
192
193impl Default for DecoderContext<'_> {
194    fn default() -> Self {
195        Self {
196            tile_decode_context: TileDecodeContext::default(),
197            storage: DecompositionStorage::default(),
198            #[cfg(test)]
199            direct_plan_code_blocks: 0,
200            cpu_decode_parallelism: CpuDecodeParallelism::Auto,
201            line_region_idwt: true,
202            workspace_stats: DecoderWorkspaceStats::default(),
203        }
204    }
205}
206
207impl DecoderContext<'_> {
208    /// Create a borrowing decoder context from a lifetime-free reusable workspace.
209    #[must_use]
210    pub fn from_workspace(workspace: DecoderWorkspace) -> Self {
211        Self {
212            tile_decode_context: workspace.tile_decode_context,
213            storage: DecompositionStorage::default(),
214            #[cfg(test)]
215            direct_plan_code_blocks: 0,
216            cpu_decode_parallelism: workspace.cpu_decode_parallelism,
217            line_region_idwt: true,
218            workspace_stats: workspace.stats,
219        }
220    }
221
222    /// Release input-borrowing graph owners and recover the reusable workspace.
223    #[must_use]
224    pub fn into_workspace(mut self) -> DecoderWorkspace {
225        self.storage.release_all_allocations();
226        DecoderWorkspace {
227            tile_decode_context: self.tile_decode_context,
228            cpu_decode_parallelism: self.cpu_decode_parallelism,
229            stats: self.workspace_stats,
230        }
231    }
232
233    /// Return reuse counters for this context's lifetime-free workspace state.
234    #[must_use]
235    pub const fn workspace_stats(&self) -> DecoderWorkspaceStats {
236        self.workspace_stats
237    }
238
239    pub(super) fn record_decode_start(
240        &mut self,
241        retained_component_bytes: usize,
242        retained_tier1_bytes: usize,
243        retained_idwt_bytes: usize,
244    ) {
245        self.workspace_stats.decode_calls = self.workspace_stats.decode_calls.saturating_add(1);
246        if retained_component_bytes != 0 {
247            self.workspace_stats.component_owner_reuses = self
248                .workspace_stats
249                .component_owner_reuses
250                .saturating_add(1);
251        }
252        if retained_tier1_bytes != 0 {
253            self.workspace_stats.tier1_owner_reuses =
254                self.workspace_stats.tier1_owner_reuses.saturating_add(1);
255        }
256        if retained_idwt_bytes != 0 {
257            self.workspace_stats.idwt_owner_reuses =
258                self.workspace_stats.idwt_owner_reuses.saturating_add(1);
259        }
260    }
261
262    pub(super) fn record_scratch_capacity_retry(&mut self) {
263        self.workspace_stats.scratch_capacity_retries = self
264            .workspace_stats
265            .scratch_capacity_retries
266            .saturating_add(1);
267    }
268
269    pub(super) fn record_decode_complete(
270        &mut self,
271        retained_component_bytes: usize,
272        retained_tier1_bytes: usize,
273        retained_idwt_bytes: usize,
274    ) {
275        self.workspace_stats.retained_component_bytes = retained_component_bytes;
276        self.workspace_stats.retained_tier1_bytes = retained_tier1_bytes;
277        self.workspace_stats.retained_idwt_bytes = retained_idwt_bytes;
278    }
279
280    pub(crate) fn release_reusable_allocations(&mut self) {
281        self.tile_decode_context.release_all_allocations();
282        self.storage.release_all_allocations();
283    }
284
285    pub(crate) fn set_output_region(&mut self, output_region: Option<(u32, u32, u32, u32)>) {
286        self.tile_decode_context.output_region = output_region.map(OutputRegion::from_tuple);
287    }
288
289    pub(crate) fn set_round_irreversible_output(&mut self, enabled: bool) {
290        self.tile_decode_context.round_irreversible_output = enabled;
291    }
292
293    pub(crate) fn set_defer_mct(&mut self, enabled: bool) {
294        self.tile_decode_context.defer_mct = enabled;
295    }
296
297    /// Return the native CPU decode parallelism policy.
298    #[must_use]
299    pub fn cpu_decode_parallelism(&self) -> CpuDecodeParallelism {
300        self.cpu_decode_parallelism
301    }
302
303    /// Set the native CPU decode parallelism policy.
304    pub fn set_cpu_decode_parallelism(&mut self, parallelism: CpuDecodeParallelism) {
305        self.cpu_decode_parallelism = parallelism;
306    }
307
308    /// Turn the row-streamed region IDWT on or off; it is on by default and
309    /// lowers region-decode memory without changing output.
310    #[cfg(test)]
311    pub(crate) fn set_line_region_idwt(&mut self, enabled: bool) {
312        self.line_region_idwt = enabled;
313    }
314
315    /// Allow the current call to use the row-streamed region IDWT.
316    pub(crate) fn permit_line_region_idwt(&mut self, permitted: bool) {
317        self.tile_decode_context.line_region_idwt_permitted = permitted;
318    }
319}