Skip to main content

signinum_j2k_native/
direct_cpu.rs

1use alloc::vec::Vec;
2
3use crate::error::{bail, DecodingError, Result};
4use crate::j2c::idwt;
5use crate::math::{floor_f32, round_f32};
6use crate::{
7    decode_ht_code_block_scalar, decode_j2k_code_block_scalar, HtCodeBlockDecodeJob,
8    HtOwnedSubBandPlan, J2kCodeBlockDecodeJob, J2kDirectBandId, J2kDirectColorPlan,
9    J2kDirectGrayscalePlan, J2kDirectGrayscaleStep, J2kDirectIdwtStep, J2kDirectStoreStep,
10    J2kIdwtBand, J2kOwnedSubBandPlan, J2kRect, J2kSingleDecompositionIdwtJob, J2kWaveletTransform,
11};
12
13/// Adapter reusable scratch for executing direct J2K RGB plans on the CPU.
14#[derive(Debug, Default)]
15pub struct J2kDirectCpuScratch {
16    component_band_sets: Vec<DirectComponentBandScratch>,
17    component_planes: Vec<DirectComponentPlane>,
18}
19
20impl J2kDirectCpuScratch {
21    /// Create empty direct-plan CPU scratch.
22    #[must_use]
23    pub const fn new() -> Self {
24        Self {
25            component_band_sets: Vec::new(),
26            component_planes: Vec::new(),
27        }
28    }
29
30    /// Release retained scratch allocations.
31    pub fn clear(&mut self) {
32        self.component_band_sets.clear();
33        self.component_planes.clear();
34    }
35
36    fn prepare_component_scratch(&mut self, component_count: usize) {
37        while self.component_band_sets.len() < component_count {
38            self.component_band_sets
39                .push(DirectComponentBandScratch::default());
40        }
41        while self.component_planes.len() < component_count {
42            self.component_planes.push(DirectComponentPlane::default());
43        }
44    }
45
46    #[cfg(test)]
47    fn allocation_profile_for_tests(&self) -> DirectScratchAllocationProfile {
48        let band_buffers = self
49            .component_band_sets
50            .iter()
51            .map(|component| component.bands.len())
52            .sum();
53        let band_sample_len = self
54            .component_band_sets
55            .iter()
56            .flat_map(|component| component.bands.iter())
57            .map(|band| band.coefficients.len())
58            .sum();
59        let band_sample_capacity = self
60            .component_band_sets
61            .iter()
62            .flat_map(|component| component.bands.iter())
63            .map(|band| band.coefficients.capacity())
64            .sum();
65        let component_sample_len = self
66            .component_planes
67            .iter()
68            .map(|plane| plane.samples.len())
69            .sum();
70        let component_sample_capacity = self
71            .component_planes
72            .iter()
73            .map(|plane| plane.samples.capacity())
74            .sum();
75        DirectScratchAllocationProfile {
76            component_band_sets: self.component_band_sets.len(),
77            component_planes: self.component_planes.len(),
78            band_buffers,
79            band_sample_len,
80            band_sample_capacity,
81            component_sample_len,
82            component_sample_capacity,
83        }
84    }
85}
86
87#[cfg(test)]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89struct DirectScratchAllocationProfile {
90    component_band_sets: usize,
91    component_planes: usize,
92    band_buffers: usize,
93    band_sample_len: usize,
94    band_sample_capacity: usize,
95    component_sample_len: usize,
96    component_sample_capacity: usize,
97}
98
99#[derive(Debug, Default)]
100struct DirectComponentBandScratch {
101    bands: Vec<DirectCpuBand>,
102    active_len: usize,
103}
104
105impl DirectComponentBandScratch {
106    fn reset(&mut self) {
107        self.active_len = 0;
108    }
109
110    fn active(&self) -> &[DirectCpuBand] {
111        &self.bands[..self.active_len]
112    }
113
114    fn prepare_band(&mut self, band_id: J2kDirectBandId, rect: J2kRect, len: usize) -> usize {
115        let index = self.active_len;
116        if index == self.bands.len() {
117            self.bands.push(DirectCpuBand::empty());
118        }
119        let band = &mut self.bands[index];
120        band.band_id = band_id;
121        band.rect = rect;
122        resize_and_zero(&mut band.coefficients, len);
123        self.active_len += 1;
124        index
125    }
126}
127
128#[derive(Debug)]
129struct DirectCpuBand {
130    band_id: J2kDirectBandId,
131    rect: J2kRect,
132    coefficients: Vec<f32>,
133}
134
135impl DirectCpuBand {
136    const fn empty() -> Self {
137        Self {
138            band_id: 0,
139            rect: J2kRect {
140                x0: 0,
141                y0: 0,
142                x1: 0,
143                y1: 0,
144            },
145            coefficients: Vec::new(),
146        }
147    }
148}
149
150#[derive(Debug, Default)]
151struct DirectComponentPlane {
152    width: u32,
153    height: u32,
154    samples: Vec<f32>,
155}
156
157/// Execute a adapter direct RGB plan on the CPU and write an RGB8 output region.
158pub fn execute_direct_color_plan_rgb8_into(
159    plan: &J2kDirectColorPlan,
160    output_region: J2kRect,
161    scratch: &mut J2kDirectCpuScratch,
162    out: &mut [u8],
163    stride: usize,
164) -> Result<()> {
165    execute_direct_color_plan_u8_into(
166        plan,
167        output_region,
168        scratch,
169        out,
170        stride,
171        DirectColorU8Output::Rgb8,
172    )
173}
174
175/// Execute a adapter direct RGB plan on the CPU and write an RGBA8 output region.
176pub fn execute_direct_color_plan_rgba8_into(
177    plan: &J2kDirectColorPlan,
178    output_region: J2kRect,
179    scratch: &mut J2kDirectCpuScratch,
180    out: &mut [u8],
181    stride: usize,
182) -> Result<()> {
183    execute_direct_color_plan_u8_into(
184        plan,
185        output_region,
186        scratch,
187        out,
188        stride,
189        DirectColorU8Output::Rgba8,
190    )
191}
192
193#[derive(Clone, Copy)]
194enum DirectColorU8Output {
195    Rgb8,
196    Rgba8,
197}
198
199impl DirectColorU8Output {
200    const fn bytes_per_pixel(self) -> usize {
201        match self {
202            Self::Rgb8 => 3,
203            Self::Rgba8 => 4,
204        }
205    }
206}
207
208fn execute_direct_color_plan_u8_into(
209    plan: &J2kDirectColorPlan,
210    output_region: J2kRect,
211    scratch: &mut J2kDirectCpuScratch,
212    out: &mut [u8],
213    stride: usize,
214    output: DirectColorU8Output,
215) -> Result<()> {
216    if plan.component_plans.len() != 3 {
217        bail!(DecodingError::UnsupportedFeature(
218            "direct CPU color plan requires three components"
219        ));
220    }
221    validate_output_region(plan, output_region, out.len(), stride, output)?;
222
223    scratch.prepare_component_scratch(plan.component_plans.len());
224    for (component_index, component_plan) in plan.component_plans.iter().enumerate() {
225        let band_scratch = &mut scratch.component_band_sets[component_index];
226        let plane = &mut scratch.component_planes[component_index];
227        execute_component_plan(component_plan, band_scratch, plane)?;
228    }
229
230    let [plane0, plane1, plane2, ..] = scratch.component_planes.as_mut_slice() else {
231        bail!(DecodingError::CodeBlockDecodeFailure);
232    };
233    if plan.mct {
234        apply_inverse_mct(plan.transform, plan.bit_depths, plane0, plane1, plane2)?;
235    }
236    write_rgb8_region(
237        [plane0, plane1, plane2],
238        plan.bit_depths,
239        output_region,
240        out,
241        stride,
242        output,
243    )
244}
245
246fn execute_component_plan(
247    plan: &J2kDirectGrayscalePlan,
248    bands: &mut DirectComponentBandScratch,
249    output: &mut DirectComponentPlane,
250) -> Result<()> {
251    bands.reset();
252    let mut output_written = false;
253
254    for step in &plan.steps {
255        match step {
256            J2kDirectGrayscaleStep::ClassicSubBand(sub_band) => {
257                execute_classic_sub_band(sub_band, bands)?;
258            }
259            J2kDirectGrayscaleStep::HtSubBand(sub_band) => {
260                execute_ht_sub_band(sub_band, bands)?;
261            }
262            J2kDirectGrayscaleStep::Idwt(step) => {
263                execute_idwt_step(step, bands)?;
264            }
265            J2kDirectGrayscaleStep::Store(store) => {
266                store_component(store, bands.active(), output, &mut output_written)?;
267            }
268        }
269    }
270
271    if output_written {
272        Ok(())
273    } else {
274        Err(DecodingError::CodeBlockDecodeFailure.into())
275    }
276}
277
278fn execute_classic_sub_band(
279    plan: &J2kOwnedSubBandPlan,
280    bands: &mut DirectComponentBandScratch,
281) -> Result<()> {
282    let required_len = checked_area(plan.width, plan.height)?;
283    let band_index = bands.prepare_band(plan.band_id, plan.rect, required_len);
284    let output = &mut bands.bands[band_index].coefficients;
285    let sub_band_width =
286        usize::try_from(plan.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
287
288    for job in &plan.jobs {
289        let base_idx = checked_block_base(job.output_x, job.output_y, sub_band_width)?;
290        let block_len = checked_block_output_len(job.output_stride, job.width, job.height)?;
291        let end_idx = base_idx
292            .checked_add(block_len)
293            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
294        if end_idx > output.len()
295            || job
296                .output_x
297                .checked_add(job.width)
298                .is_none_or(|x| x > plan.width)
299            || job
300                .output_y
301                .checked_add(job.height)
302                .is_none_or(|y| y > plan.height)
303        {
304            bail!(DecodingError::CodeBlockDecodeFailure);
305        }
306
307        let code_block = J2kCodeBlockDecodeJob {
308            data: &job.data,
309            segments: &job.segments,
310            width: job.width,
311            height: job.height,
312            output_stride: job.output_stride,
313            missing_bit_planes: job.missing_bit_planes,
314            number_of_coding_passes: job.number_of_coding_passes,
315            total_bitplanes: job.total_bitplanes,
316            roi_shift: job.roi_shift,
317            sub_band_type: job.sub_band_type,
318            style: job.style,
319            strict: job.strict,
320            dequantization_step: job.dequantization_step,
321        };
322        decode_j2k_code_block_scalar(code_block, &mut output[base_idx..end_idx])?;
323    }
324    Ok(())
325}
326
327fn execute_ht_sub_band(
328    plan: &HtOwnedSubBandPlan,
329    bands: &mut DirectComponentBandScratch,
330) -> Result<()> {
331    let required_len = checked_area(plan.width, plan.height)?;
332    let band_index = bands.prepare_band(plan.band_id, plan.rect, required_len);
333    let output = &mut bands.bands[band_index].coefficients;
334    let sub_band_width =
335        usize::try_from(plan.width).map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
336
337    for job in &plan.jobs {
338        let base_idx = checked_block_base(job.output_x, job.output_y, sub_band_width)?;
339        let block_len = checked_block_output_len(job.output_stride, job.width, job.height)?;
340        let end_idx = base_idx
341            .checked_add(block_len)
342            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
343        if end_idx > output.len()
344            || job
345                .output_x
346                .checked_add(job.width)
347                .is_none_or(|x| x > plan.width)
348            || job
349                .output_y
350                .checked_add(job.height)
351                .is_none_or(|y| y > plan.height)
352        {
353            bail!(DecodingError::CodeBlockDecodeFailure);
354        }
355
356        let code_block = HtCodeBlockDecodeJob {
357            data: &job.data,
358            cleanup_length: job.cleanup_length,
359            refinement_length: job.refinement_length,
360            width: job.width,
361            height: job.height,
362            output_stride: job.output_stride,
363            missing_bit_planes: job.missing_bit_planes,
364            number_of_coding_passes: job.number_of_coding_passes,
365            num_bitplanes: job.num_bitplanes,
366            roi_shift: job.roi_shift,
367            stripe_causal: job.stripe_causal,
368            strict: job.strict,
369            dequantization_step: job.dequantization_step,
370        };
371        decode_ht_code_block_scalar(code_block, &mut output[base_idx..end_idx])?;
372    }
373    Ok(())
374}
375
376fn execute_idwt_step(
377    step: &J2kDirectIdwtStep,
378    bands: &mut DirectComponentBandScratch,
379) -> Result<()> {
380    let output_index = bands.prepare_band(step.output_band_id, step.rect, 0);
381    let (input_bands, output_bands) = bands.bands.split_at_mut(output_index);
382    let output = &mut output_bands[0].coefficients;
383    let ll = find_idwt_band(input_bands, step.ll_band_id)?;
384    let hl = find_idwt_band(input_bands, step.hl_band_id)?;
385    let lh = find_idwt_band(input_bands, step.lh_band_id)?;
386    let hh = find_idwt_band(input_bands, step.hh_band_id)?;
387    let job = J2kSingleDecompositionIdwtJob {
388        rect: step.rect,
389        transform: step.transform,
390        ll,
391        hl,
392        lh,
393        hh,
394    };
395    idwt::apply_single_decomposition_idwt_job(job, output)
396}
397
398fn find_idwt_band(bands: &[DirectCpuBand], band_id: J2kDirectBandId) -> Result<J2kIdwtBand<'_>> {
399    let band = find_band(bands, band_id)?;
400    Ok(J2kIdwtBand {
401        rect: band.rect,
402        coefficients: &band.coefficients,
403    })
404}
405
406fn store_component(
407    store: &J2kDirectStoreStep,
408    bands: &[DirectCpuBand],
409    plane: &mut DirectComponentPlane,
410    output_written: &mut bool,
411) -> Result<()> {
412    let input = find_band(bands, store.input_band_id)?;
413    if !*output_written {
414        plane.width = store.output_width;
415        plane.height = store.output_height;
416        let required_len = checked_area(store.output_width, store.output_height)?;
417        resize_and_zero(&mut plane.samples, required_len);
418        *output_written = true;
419    }
420    if plane.width != store.output_width
421        || plane.height != store.output_height
422        || plane.samples.len() != checked_area(store.output_width, store.output_height)?
423    {
424        bail!(DecodingError::CodeBlockDecodeFailure);
425    }
426
427    validate_store_bounds(store, input, plane)?;
428    let input_width = input.rect.width() as usize;
429    let output_width = plane.width as usize;
430    let copy_width = store.copy_width as usize;
431    for row in 0..store.copy_height as usize {
432        let src_start = (store.source_y as usize + row)
433            .checked_mul(input_width)
434            .and_then(|base| base.checked_add(store.source_x as usize))
435            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
436        let dst_start = (store.output_y as usize + row)
437            .checked_mul(output_width)
438            .and_then(|base| base.checked_add(store.output_x as usize))
439            .ok_or(DecodingError::CodeBlockDecodeFailure)?;
440        let src = &input.coefficients[src_start..src_start + copy_width];
441        let dst = &mut plane.samples[dst_start..dst_start + copy_width];
442        for (src, dst) in src.iter().zip(dst.iter_mut()) {
443            *dst = *src + store.addend;
444        }
445    }
446    Ok(())
447}
448
449fn find_band(bands: &[DirectCpuBand], band_id: J2kDirectBandId) -> Result<&DirectCpuBand> {
450    bands
451        .iter()
452        .find(|band| band.band_id == band_id)
453        .ok_or_else(|| DecodingError::CodeBlockDecodeFailure.into())
454}
455
456fn validate_store_bounds(
457    store: &J2kDirectStoreStep,
458    input: &DirectCpuBand,
459    output: &DirectComponentPlane,
460) -> Result<()> {
461    if store
462        .source_x
463        .checked_add(store.copy_width)
464        .is_none_or(|x| x > input.rect.width())
465        || store
466            .source_y
467            .checked_add(store.copy_height)
468            .is_none_or(|y| y > input.rect.height())
469        || store
470            .output_x
471            .checked_add(store.copy_width)
472            .is_none_or(|x| x > output.width)
473        || store
474            .output_y
475            .checked_add(store.copy_height)
476            .is_none_or(|y| y > output.height)
477    {
478        bail!(DecodingError::CodeBlockDecodeFailure);
479    }
480    Ok(())
481}
482
483fn apply_inverse_mct(
484    transform: J2kWaveletTransform,
485    bit_depths: [u8; 3],
486    plane0: &mut DirectComponentPlane,
487    plane1: &mut DirectComponentPlane,
488    plane2: &mut DirectComponentPlane,
489) -> Result<()> {
490    if plane0.width != plane1.width
491        || plane1.width != plane2.width
492        || plane0.height != plane1.height
493        || plane1.height != plane2.height
494        || plane0.samples.len() != plane1.samples.len()
495        || plane1.samples.len() != plane2.samples.len()
496    {
497        bail!(DecodingError::CodeBlockDecodeFailure);
498    }
499
500    let addend0 = sign_addend(bit_depths[0]);
501    let addend1 = sign_addend(bit_depths[1]);
502    let addend2 = sign_addend(bit_depths[2]);
503    for ((y0, y1), y2) in plane0
504        .samples
505        .iter_mut()
506        .zip(plane1.samples.iter_mut())
507        .zip(plane2.samples.iter_mut())
508    {
509        let src0 = *y0;
510        let src1 = *y1;
511        let src2 = *y2;
512        let (out0, out1, out2) = match transform {
513            J2kWaveletTransform::Irreversible97 => (
514                src0 + 1.402 * src2,
515                src0 - 0.34413 * src1 - 0.71414 * src2,
516                src0 + 1.772 * src1,
517            ),
518            J2kWaveletTransform::Reversible53 => {
519                let i1 = src0 - floor_f32((src2 + src1) * 0.25);
520                (src2 + i1, i1, src1 + i1)
521            }
522        };
523        *y0 = out0 + addend0;
524        *y1 = out1 + addend1;
525        *y2 = out2 + addend2;
526    }
527    Ok(())
528}
529
530fn write_rgb8_region(
531    planes: [&DirectComponentPlane; 3],
532    bit_depths: [u8; 3],
533    output_region: J2kRect,
534    out: &mut [u8],
535    stride: usize,
536    output: DirectColorU8Output,
537) -> Result<()> {
538    let width = output_region.width() as usize;
539    let height = output_region.height() as usize;
540    let bytes_per_pixel = output.bytes_per_pixel();
541    let row_bytes = width
542        .checked_mul(bytes_per_pixel)
543        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
544    for plane in planes {
545        if output_region.x1 > plane.width || output_region.y1 > plane.height {
546            bail!(DecodingError::CodeBlockDecodeFailure);
547        }
548    }
549
550    for y in 0..height {
551        let src_y = output_region.y0 as usize + y;
552        let dst = &mut out[y * stride..y * stride + row_bytes];
553        for x in 0..width {
554            let src_x = output_region.x0 as usize + x;
555            let dst = &mut dst[x * bytes_per_pixel..x * bytes_per_pixel + bytes_per_pixel];
556            for channel in 0..3 {
557                let plane = planes[channel];
558                let sample = plane.samples[src_y * plane.width as usize + src_x];
559                dst[channel] = sample_as_u8(sample, bit_depths[channel]);
560            }
561            if matches!(output, DirectColorU8Output::Rgba8) {
562                dst[3] = u8::MAX;
563            }
564        }
565    }
566    Ok(())
567}
568
569fn validate_output_region(
570    plan: &J2kDirectColorPlan,
571    output_region: J2kRect,
572    out_len: usize,
573    stride: usize,
574    output: DirectColorU8Output,
575) -> Result<()> {
576    if output_region.x1 > plan.dimensions.0
577        || output_region.y1 > plan.dimensions.1
578        || output_region.x0 > output_region.x1
579        || output_region.y0 > output_region.y1
580    {
581        bail!(DecodingError::CodeBlockDecodeFailure);
582    }
583    let row_bytes = output_region
584        .width()
585        .checked_mul(output.bytes_per_pixel() as u32)
586        .and_then(|len| usize::try_from(len).ok())
587        .ok_or(DecodingError::CodeBlockDecodeFailure)?;
588    if stride < row_bytes {
589        bail!(DecodingError::CodeBlockDecodeFailure);
590    }
591    let height = usize::try_from(output_region.height())
592        .map_err(|_| DecodingError::CodeBlockDecodeFailure)?;
593    let required = if height == 0 {
594        0
595    } else {
596        stride
597            .checked_mul(height - 1)
598            .and_then(|prefix| prefix.checked_add(row_bytes))
599            .ok_or(DecodingError::CodeBlockDecodeFailure)?
600    };
601    if out_len < required {
602        bail!(DecodingError::CodeBlockDecodeFailure);
603    }
604    Ok(())
605}
606
607fn checked_area(width: u32, height: u32) -> Result<usize> {
608    usize::try_from(width)
609        .ok()
610        .and_then(|width| width.checked_mul(height as usize))
611        .ok_or_else(|| DecodingError::CodeBlockDecodeFailure.into())
612}
613
614fn checked_block_base(output_x: u32, output_y: u32, stride: usize) -> Result<usize> {
615    usize::try_from(output_y)
616        .ok()
617        .and_then(|y| y.checked_mul(stride))
618        .and_then(|base| base.checked_add(output_x as usize))
619        .ok_or_else(|| DecodingError::CodeBlockDecodeFailure.into())
620}
621
622fn checked_block_output_len(stride: usize, width: u32, height: u32) -> Result<usize> {
623    if height == 0 {
624        return Ok(0);
625    }
626    stride
627        .checked_mul(height as usize - 1)
628        .and_then(|prefix| prefix.checked_add(width as usize))
629        .ok_or_else(|| DecodingError::CodeBlockDecodeFailure.into())
630}
631
632fn resize_and_zero(buffer: &mut Vec<f32>, len: usize) {
633    buffer.resize(len, 0.0);
634    buffer.fill(0.0);
635}
636
637fn sign_addend(bit_depth: u8) -> f32 {
638    (1_u32 << (bit_depth - 1)) as f32
639}
640
641fn sample_as_u8(sample: f32, bit_depth: u8) -> u8 {
642    let rounded = round_f32(sample);
643    if bit_depth == 8 {
644        return rounded.clamp(0.0, f32::from(u8::MAX)) as u8;
645    }
646    let max_value = if bit_depth >= 16 {
647        f32::from(u16::MAX)
648    } else {
649        f32::from(((1_u16 << bit_depth) - 1).max(1))
650    };
651    round_f32((rounded.clamp(0.0, max_value) / max_value) * f32::from(u8::MAX)) as u8
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use crate::{encode_htj2k, DecodeSettings, DecoderContext, EncodeOptions, Image};
658    use alloc::vec;
659
660    fn direct_htj2k_rgb_plan() -> (J2kDirectColorPlan, J2kRect) {
661        let pixels = (0..16 * 16 * 3)
662            .map(|idx| ((idx * 13 + idx / 3) & 0xff) as u8)
663            .collect::<Vec<_>>();
664        let options = EncodeOptions {
665            reversible: true,
666            num_decomposition_levels: 2,
667            ..EncodeOptions::default()
668        };
669        let bytes = encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode HTJ2K RGB");
670        let image = Image::new(
671            &bytes,
672            &DecodeSettings {
673                target_resolution: Some((4, 4)),
674                ..DecodeSettings::default()
675            },
676        )
677        .expect("scaled image");
678        let output_region = J2kRect {
679            x0: 1,
680            y0: 1,
681            x1: 3,
682            y1: 3,
683        };
684        let mut context = DecoderContext::default();
685        let plan = image
686            .build_direct_color_plan_region_with_context(&mut context, (1, 1, 2, 2))
687            .expect("direct color plan");
688        (plan, output_region)
689    }
690
691    #[test]
692    fn direct_cpu_scratch_retains_component_buffers_between_executions() {
693        let (plan, output_region) = direct_htj2k_rgb_plan();
694        let stride = output_region.width() as usize * 3;
695        let mut out = vec![0_u8; stride * output_region.height() as usize];
696        let mut scratch = J2kDirectCpuScratch::new();
697
698        execute_direct_color_plan_rgb8_into(&plan, output_region, &mut scratch, &mut out, stride)
699            .expect("first direct execute");
700        let first = scratch.allocation_profile_for_tests();
701
702        execute_direct_color_plan_rgb8_into(&plan, output_region, &mut scratch, &mut out, stride)
703            .expect("second direct execute");
704        let second = scratch.allocation_profile_for_tests();
705
706        assert_eq!(first.component_band_sets, 3);
707        assert_eq!(first.component_planes, 3);
708        assert_eq!(second.component_band_sets, first.component_band_sets);
709        assert_eq!(second.component_planes, first.component_planes);
710        assert_eq!(second.band_buffers, first.band_buffers);
711        assert_eq!(
712            second.component_sample_capacity,
713            first.component_sample_capacity
714        );
715        assert_eq!(second.band_sample_capacity, first.band_sample_capacity);
716        assert!(second.band_sample_capacity >= second.band_sample_len);
717        assert!(second.component_sample_capacity >= second.component_sample_len);
718    }
719}