Skip to main content

j2k_transcode/jpeg_to_htj2k/
report.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// j2k-coverage: shared-accelerator-host
3
4use super::{ErrorMetrics, JpegToHtj2kCoefficientPath};
5
6/// Aggregate report for multi-tile transcode.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct BatchTranscodeReport {
9    /// Number of input tiles.
10    pub tile_count: usize,
11    /// Number of successfully encoded output tiles.
12    pub successful_tiles: usize,
13    /// Number of tile-local failures.
14    pub failed_tiles: usize,
15    /// Number of transformed components across successful extracted tiles.
16    pub transformed_components: usize,
17    /// Number of same-geometry reversible 5/3 batches submitted.
18    pub reversible_dwt53_batches: usize,
19    /// Number of reversible 5/3 component jobs in submitted batches.
20    pub reversible_dwt53_batch_jobs: usize,
21    /// Batch extraction time in microseconds.
22    pub extract_us: u128,
23    /// Batch DCT-to-wavelet time in microseconds.
24    pub transform_us: u128,
25    /// Batch HTJ2K encode time in microseconds.
26    pub encode_us: u128,
27    /// Detailed stage timings for the batch. Batch-accelerated 5/3 transform
28    /// timings stay here instead of being copied into every tile report.
29    pub timings: TranscodeTimingReport,
30    /// Coefficient path used by the batch.
31    pub coefficient_path: JpegToHtj2kCoefficientPath,
32}
33
34/// Stable profile request label for transcode batch telemetry.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum TranscodeBatchProfileRequest {
37    /// CPU-only transcode request.
38    Cpu,
39    /// Auto-routing request that may use an accelerator.
40    MetalAuto,
41    /// Explicit Metal request.
42    MetalExplicit,
43}
44
45impl TranscodeBatchProfileRequest {
46    /// Stable `request` label emitted in `j2k_profile` rows.
47    #[must_use]
48    pub const fn as_str(self) -> &'static str {
49        match self {
50            Self::Cpu => "cpu",
51            Self::MetalAuto => "metal_auto",
52            Self::MetalExplicit => "metal_explicit",
53        }
54    }
55
56    /// Stable `transform_processor` label for this request and timing report.
57    #[must_use]
58    pub fn transform_processor(self, timings: &TranscodeTimingReport) -> &'static str {
59        if matches!(self, Self::MetalAuto | Self::MetalExplicit)
60            && timings.accelerator_work_observed()
61        {
62            "metal"
63        } else {
64            "cpu"
65        }
66    }
67
68    /// Stable `path` label for this request and timing report.
69    #[must_use]
70    pub fn profile_path(self, timings: &TranscodeTimingReport) -> &'static str {
71        if self.transform_processor(timings) != "metal" {
72            return "cpu";
73        }
74        match self {
75            Self::Cpu => "cpu",
76            Self::MetalAuto => "auto",
77            Self::MetalExplicit => "metal",
78        }
79    }
80}
81
82const TRANSCODE_PROFILE_FIELD_COUNT: usize = 65;
83
84macro_rules! push_labels {
85    ($builder:ident; $($key:literal => $value:expr),+ $(,)?) => {
86        $($builder.label($key, $value)?;)+
87    };
88}
89
90macro_rules! push_metrics {
91    ($builder:ident; $($key:literal => $value:expr),+ $(,)?) => {
92        $($builder.metric($key, $value)?;)+
93    };
94}
95
96/// Shared transcode batch profile row fields.
97#[derive(Debug, PartialEq, Eq)]
98pub struct TranscodeBatchProfileRow {
99    fields: Vec<(&'static str, String)>,
100    path: &'static str,
101}
102
103pub(super) type TranscodeBatchProfileFields = Vec<(&'static str, String)>;
104
105impl TranscodeBatchProfileRow {
106    /// Builds bounded profile fields for a batch transcode report.
107    pub fn new(
108        report: &BatchTranscodeReport,
109        context: impl AsRef<str>,
110        request: TranscodeBatchProfileRequest,
111    ) -> j2k_profile::ProfileResult<Self> {
112        let timings = report.timings;
113        let total_us = report
114            .extract_us
115            .checked_add(report.transform_us)
116            .and_then(|value| value.checked_add(report.encode_us))
117            .ok_or(j2k_profile::ProfileError::SizeOverflow {
118                what: "transcode batch total profile time",
119            })?;
120        let transform_processor = request.transform_processor(&timings);
121        let path = request.profile_path(&timings);
122        let mut builder = TranscodeProfileRowBuilder::new()?;
123
124        push_labels!(builder;
125            "codec" => "transcode",
126            "op" => "transcode_batch",
127            "request" => request.as_str(),
128            "path" => path,
129            "pipeline" => "jpeg_to_htj2k",
130            "context" => SanitizedContext(context.as_ref()),
131            "coefficient_path" => coefficient_path_label(report.coefficient_path),
132            "extract_processor" => "cpu",
133            "transform_processor" => transform_processor,
134            "encode_processor" => "cpu",
135        );
136        Self::push_batch_fields(&mut builder, report, total_us)?;
137        Self::push_input_timing_fields(&mut builder, &timings)?;
138        Self::push_dwt97_timing_fields(&mut builder, &timings)?;
139        Self::push_transfer_fields(&mut builder, &timings)?;
140        Self::push_encode_timing_fields(&mut builder, &timings)?;
141        Self::push_accelerator_fields(&mut builder, &timings)?;
142
143        Ok(Self {
144            fields: builder.finish()?,
145            path,
146        })
147    }
148
149    fn push_batch_fields(
150        builder: &mut TranscodeProfileRowBuilder,
151        report: &BatchTranscodeReport,
152        total_us: u128,
153    ) -> j2k_profile::ProfileResult<()> {
154        push_metrics!(builder;
155            "tile_count" => report.tile_count,
156            "successful_tiles" => report.successful_tiles,
157            "failed_tiles" => report.failed_tiles,
158            "transformed_components" => report.transformed_components,
159            "reversible_dwt53_batches" => report.reversible_dwt53_batches,
160            "reversible_dwt53_batch_jobs" => report.reversible_dwt53_batch_jobs,
161            "extract_us" => report.extract_us,
162            "transform_us" => report.transform_us,
163            "encode_us" => report.encode_us,
164            "total_us" => total_us,
165        );
166        Ok(())
167    }
168
169    fn push_input_timing_fields(
170        builder: &mut TranscodeProfileRowBuilder,
171        timings: &TranscodeTimingReport,
172    ) -> j2k_profile::ProfileResult<()> {
173        push_metrics!(builder;
174            "source_raw_probe_us" => timings.source_raw_probe_us,
175            "read_region_decode_us" => timings.read_region_decode_us,
176            "compose_pad_us" => timings.compose_pad_us,
177            "generated_jpeg_encode_us" => timings.generated_jpeg_encode_us,
178            "jpeg_dct_extract_us" => timings.jpeg_dct_extract_us,
179            "jpeg_dct_repack_us" => timings.jpeg_dct_repack_us,
180            "dct_to_wavelet_total_us" => timings.dct_to_wavelet_total_us,
181            "dct_to_wavelet_accelerator_us" => timings.dct_to_wavelet_accelerator_us,
182            "dct_to_wavelet_cpu_fallback_us" => timings.dct_to_wavelet_cpu_fallback_us,
183            "dwt_decompose_us" => timings.dwt_decompose_us,
184        );
185        Ok(())
186    }
187
188    fn push_dwt97_timing_fields(
189        builder: &mut TranscodeProfileRowBuilder,
190        timings: &TranscodeTimingReport,
191    ) -> j2k_profile::ProfileResult<()> {
192        push_metrics!(builder;
193            "dwt97_batch_pack_upload_us" => timings.dwt97_batch_pack_upload_us,
194            "dwt97_batch_pack_upload_transfers" => timings.dwt97_batch_pack_upload_transfers,
195            "dwt97_batch_pack_upload_bytes" => timings.dwt97_batch_pack_upload_bytes,
196            "dwt97_batch_resident_dct_handoff_count" => timings.dwt97_batch_resident_dct_handoff_count,
197            "dwt97_batch_idct_row_lift_us" => timings.dwt97_batch_idct_row_lift_us,
198            "dwt97_batch_column_lift_us" => timings.dwt97_batch_column_lift_us,
199            "dwt97_batch_resident_dwt_handoff_count" => timings.dwt97_batch_resident_dwt_handoff_count,
200            "dwt97_batch_quantize_codeblock_us" => timings.dwt97_batch_quantize_codeblock_us,
201            "dwt97_batch_ht_encode_us" => timings.dwt97_batch_ht_encode_us,
202            "dwt97_batch_ht_codeblock_dispatches" => timings.dwt97_batch_ht_codeblock_dispatches,
203        );
204        Ok(())
205    }
206
207    fn push_transfer_fields(
208        builder: &mut TranscodeProfileRowBuilder,
209        timings: &TranscodeTimingReport,
210    ) -> j2k_profile::ProfileResult<()> {
211        let device_to_host_transfer_count = timings
212            .dwt97_batch_readback_transfers
213            .checked_add(timings.dwt97_batch_ht_status_readback_transfers)
214            .and_then(|value| value.checked_add(timings.dwt97_batch_ht_output_readback_transfers))
215            .ok_or(j2k_profile::ProfileError::SizeOverflow {
216                what: "device-to-host profile transfer count",
217            })?;
218        let device_to_host_transfer_bytes = timings
219            .dwt97_batch_readback_bytes
220            .checked_add(timings.dwt97_batch_ht_status_readback_bytes)
221            .and_then(|value| value.checked_add(timings.dwt97_batch_ht_output_readback_bytes))
222            .ok_or(j2k_profile::ProfileError::SizeOverflow {
223                what: "device-to-host profile transfer bytes",
224            })?;
225        push_metrics!(builder;
226            "dwt97_batch_ht_status_readback_us" => timings.dwt97_batch_ht_status_readback_us,
227            "dwt97_batch_ht_status_readback_transfers" => timings.dwt97_batch_ht_status_readback_transfers,
228            "dwt97_batch_ht_status_readback_bytes" => timings.dwt97_batch_ht_status_readback_bytes,
229            "dwt97_batch_ht_output_readback_us" => timings.dwt97_batch_ht_output_readback_us,
230            "dwt97_batch_ht_output_readback_transfers" => timings.dwt97_batch_ht_output_readback_transfers,
231            "dwt97_batch_ht_output_readback_bytes" => timings.dwt97_batch_ht_output_readback_bytes,
232            "dwt97_batch_readback_us" => timings.dwt97_batch_readback_us,
233            "dwt97_batch_readback_transfers" => timings.dwt97_batch_readback_transfers,
234            "dwt97_batch_readback_bytes" => timings.dwt97_batch_readback_bytes,
235            "host_to_device_transfer_count" => timings.dwt97_batch_pack_upload_transfers,
236            "host_to_device_transfer_bytes" => timings.dwt97_batch_pack_upload_bytes,
237            "device_to_host_transfer_count" => device_to_host_transfer_count,
238            "device_to_host_transfer_bytes" => device_to_host_transfer_bytes,
239        );
240        Ok(())
241    }
242
243    fn push_encode_timing_fields(
244        builder: &mut TranscodeProfileRowBuilder,
245        timings: &TranscodeTimingReport,
246    ) -> j2k_profile::ProfileResult<()> {
247        push_metrics!(builder;
248            "htj2k_encode_us" => timings.htj2k_encode_us,
249            "htj2k_encode_accelerator_dispatches" => timings.htj2k_encode_accelerator_dispatches,
250            "htj2k_encode_ht_code_block_dispatches" => timings.htj2k_encode_ht_code_block_dispatches,
251            "htj2k_encode_packetization_dispatches" => timings.htj2k_encode_packetization_dispatches,
252            "component_count" => timings.component_count,
253            "batch_count" => timings.batch_count,
254            "batch_jobs" => timings.batch_jobs,
255        );
256        Ok(())
257    }
258
259    fn push_accelerator_fields(
260        builder: &mut TranscodeProfileRowBuilder,
261        timings: &TranscodeTimingReport,
262    ) -> j2k_profile::ProfileResult<()> {
263        push_metrics!(builder;
264            "accelerator_attempts" => timings.accelerator_attempts,
265            "accelerator_jobs" => timings.accelerator_jobs,
266            "accelerator_dispatches" => timings.accelerator_dispatches,
267            "accelerator_dispatched_jobs" => timings.accelerator_dispatched_jobs,
268            "cpu_fallback_jobs" => timings.cpu_fallback_jobs,
269        );
270        Ok(())
271    }
272
273    /// Ordered profile row fields.
274    #[must_use]
275    pub fn fields(&self) -> &[(&'static str, String)] {
276        &self.fields
277    }
278
279    /// Stable profile codec label.
280    #[must_use]
281    pub const fn codec(&self) -> &'static str {
282        "transcode"
283    }
284
285    /// Stable profile operation label.
286    #[must_use]
287    pub const fn op(&self) -> &'static str {
288        "transcode_batch"
289    }
290
291    /// Stable profile path label.
292    #[must_use]
293    pub const fn path(&self) -> &'static str {
294        self.path
295    }
296}
297
298struct TranscodeProfileRowBuilder {
299    fields: TranscodeBatchProfileFields,
300    limits: j2k_profile::ProfileLimits,
301    input_bytes: usize,
302    retained_bytes: usize,
303}
304
305impl TranscodeProfileRowBuilder {
306    fn new() -> j2k_profile::ProfileResult<Self> {
307        let limits = j2k_profile::ProfileLimits::default();
308        if TRANSCODE_PROFILE_FIELD_COUNT > limits.max_fields() {
309            return Err(j2k_profile::ProfileError::LimitExceeded {
310                what: "transcode profile field count",
311                requested: TRANSCODE_PROFILE_FIELD_COUNT,
312                limit: limits.max_fields(),
313            });
314        }
315        let mut fields = Vec::new();
316        fields
317            .try_reserve_exact(TRANSCODE_PROFILE_FIELD_COUNT)
318            .map_err(|_| j2k_profile::ProfileError::AllocationFailed {
319                what: "transcode profile field storage",
320                requested: TRANSCODE_PROFILE_FIELD_COUNT,
321            })?;
322        let retained_bytes = fields
323            .capacity()
324            .checked_mul(core::mem::size_of::<(&'static str, String)>())
325            .ok_or(j2k_profile::ProfileError::SizeOverflow {
326                what: "transcode profile field storage",
327            })?;
328        if retained_bytes > limits.max_retained_bytes() {
329            return Err(j2k_profile::ProfileError::LimitExceeded {
330                what: "transcode profile retained bytes",
331                requested: retained_bytes,
332                limit: limits.max_retained_bytes(),
333            });
334        }
335        Ok(Self {
336            fields,
337            limits,
338            input_bytes: 0,
339            retained_bytes,
340        })
341    }
342
343    fn label(
344        &mut self,
345        key: &'static str,
346        value: impl core::fmt::Display,
347    ) -> j2k_profile::ProfileResult<()> {
348        let field = j2k_profile::ProfileField::label_with_limits(key, value, self.limits)?;
349        self.push(key, field.into_value())
350    }
351
352    fn metric(
353        &mut self,
354        key: &'static str,
355        value: impl core::fmt::Display,
356    ) -> j2k_profile::ProfileResult<()> {
357        let field = j2k_profile::ProfileField::metric_with_limits(key, value, self.limits)?;
358        self.push(key, field.into_value())
359    }
360
361    fn push(&mut self, key: &'static str, value: String) -> j2k_profile::ProfileResult<()> {
362        let field_count =
363            self.fields
364                .len()
365                .checked_add(1)
366                .ok_or(j2k_profile::ProfileError::SizeOverflow {
367                    what: "transcode profile field count",
368                })?;
369        if field_count > self.limits.max_fields() {
370            return Err(j2k_profile::ProfileError::LimitExceeded {
371                what: "transcode profile field count",
372                requested: field_count,
373                limit: self.limits.max_fields(),
374            });
375        }
376        let input_bytes = self
377            .input_bytes
378            .checked_add(key.len())
379            .and_then(|bytes| bytes.checked_add(value.len()))
380            .ok_or(j2k_profile::ProfileError::SizeOverflow {
381                what: "transcode profile input bytes",
382            })?;
383        if input_bytes > self.limits.max_input_bytes() {
384            return Err(j2k_profile::ProfileError::LimitExceeded {
385                what: "transcode profile input bytes",
386                requested: input_bytes,
387                limit: self.limits.max_input_bytes(),
388            });
389        }
390        let retained_bytes = self.retained_bytes.checked_add(value.capacity()).ok_or(
391            j2k_profile::ProfileError::SizeOverflow {
392                what: "transcode profile retained bytes",
393            },
394        )?;
395        if retained_bytes > self.limits.max_retained_bytes() {
396            return Err(j2k_profile::ProfileError::LimitExceeded {
397                what: "transcode profile retained bytes",
398                requested: retained_bytes,
399                limit: self.limits.max_retained_bytes(),
400            });
401        }
402        if self.fields.len() == self.fields.capacity() {
403            return Err(j2k_profile::ProfileError::InvalidInput {
404                what: "transcode profile field reservation exhausted",
405            });
406        }
407        self.fields.push((key, value));
408        self.input_bytes = input_bytes;
409        self.retained_bytes = retained_bytes;
410        Ok(())
411    }
412
413    fn finish(self) -> j2k_profile::ProfileResult<TranscodeBatchProfileFields> {
414        if self.fields.len() != TRANSCODE_PROFILE_FIELD_COUNT {
415            return Err(j2k_profile::ProfileError::InvalidInput {
416                what: "transcode profile field schema is incomplete",
417            });
418        }
419        Ok(self.fields)
420    }
421}
422
423struct SanitizedContext<'a>(&'a str);
424
425impl core::fmt::Display for SanitizedContext<'_> {
426    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
427        let mut parts = self.0.split(' ');
428        if let Some(first) = parts.next() {
429            formatter.write_str(first)?;
430        }
431        for part in parts {
432            formatter.write_str("_")?;
433            formatter.write_str(part)?;
434        }
435        Ok(())
436    }
437}
438
439const fn coefficient_path_label(path: JpegToHtj2kCoefficientPath) -> &'static str {
440    match path {
441        JpegToHtj2kCoefficientPath::IntegerDirect53 => "IntegerDirect53",
442        JpegToHtj2kCoefficientPath::FloatDirectLinear53 => "FloatDirectLinear53",
443        JpegToHtj2kCoefficientPath::FloatDirectLinear97 => "FloatDirectLinear97",
444    }
445}
446
447impl BatchTranscodeReport {
448    /// Builds bounded shared profile fields for a batch transcode report.
449    pub fn profile_row(
450        &self,
451        context: impl AsRef<str>,
452        request: TranscodeBatchProfileRequest,
453    ) -> j2k_profile::ProfileResult<TranscodeBatchProfileRow> {
454        TranscodeBatchProfileRow::new(self, context, request)
455    }
456}
457
458/// Detailed timing and dispatch counters for JPEG-to-HTJ2K transcode.
459///
460/// Durations are wall-clock microseconds measured around the current Rust API
461/// boundaries. Accelerator time includes backend submission and wait overhead
462/// visible to this crate; backend-specific hardware counters are not exposed
463/// here.
464#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
465pub struct TranscodeTimingReport {
466    /// Raw compressed-tile probe/read time before JPEG DCT extraction.
467    pub source_raw_probe_us: u128,
468    /// Source region decode time for strip/retile workflows.
469    pub read_region_decode_us: u128,
470    /// Region compose/pad time for generated regular tiles.
471    pub compose_pad_us: u128,
472    /// JPEG encode time when the workflow generates regular JPEG tiles.
473    pub generated_jpeg_encode_us: u128,
474    /// JPEG DCT extraction time in microseconds.
475    pub jpeg_dct_extract_us: u128,
476    /// Time spent repacking integer DCT coefficients into float block grids.
477    pub jpeg_dct_repack_us: u128,
478    /// Total wall time spent producing DWT bands from JPEG DCT coefficients.
479    pub dct_to_wavelet_total_us: u128,
480    /// Wall time spent inside accelerator hook calls.
481    pub dct_to_wavelet_accelerator_us: u128,
482    /// Wall time spent in scalar CPU fallback transforms.
483    pub dct_to_wavelet_cpu_fallback_us: u128,
484    /// Time spent decomposing first-level DWT output into requested levels.
485    pub dwt_decompose_us: u128,
486    /// Backend 9/7 batch host pack/upload time in microseconds.
487    pub dwt97_batch_pack_upload_us: u128,
488    /// Logical host-to-device transfers during backend 9/7 batch pack/upload.
489    pub dwt97_batch_pack_upload_transfers: usize,
490    /// Host-to-device bytes during backend 9/7 batch pack/upload.
491    pub dwt97_batch_pack_upload_bytes: u64,
492    /// Resident JPEG DCT-grid descriptors validated during backend 9/7 batches.
493    pub dwt97_batch_resident_dct_handoff_count: usize,
494    /// Backend 9/7 batch IDCT plus horizontal row-lift time in microseconds.
495    pub dwt97_batch_idct_row_lift_us: u128,
496    /// Backend 9/7 batch vertical column-lift time in microseconds.
497    pub dwt97_batch_column_lift_us: u128,
498    /// Resident DWT subband descriptors validated during backend 9/7 batches.
499    pub dwt97_batch_resident_dwt_handoff_count: usize,
500    /// Backend 9/7 batch quantize/code-block layout time in microseconds.
501    pub dwt97_batch_quantize_codeblock_us: u128,
502    /// Backend 9/7 resident HT code-block encode time in microseconds.
503    pub dwt97_batch_ht_encode_us: u128,
504    /// Backend 9/7 resident HT cleanup-pass encode kernel time in microseconds.
505    pub dwt97_batch_ht_kernel_us: u128,
506    /// Backend 9/7 resident HT status-buffer device-to-host readback time in microseconds.
507    pub dwt97_batch_ht_status_readback_us: u128,
508    /// Logical device-to-host status readbacks after resident HT encode.
509    pub dwt97_batch_ht_status_readback_transfers: usize,
510    /// Device-to-host status bytes after resident HT encode.
511    pub dwt97_batch_ht_status_readback_bytes: u64,
512    /// Backend 9/7 resident HT encoded-byte compaction kernel time in microseconds.
513    pub dwt97_batch_ht_compact_us: u128,
514    /// Backend 9/7 resident HT compacted encoded-byte device-to-host readback time in microseconds.
515    pub dwt97_batch_ht_output_readback_us: u128,
516    /// Logical device-to-host output readbacks after resident HT compaction.
517    pub dwt97_batch_ht_output_readback_transfers: usize,
518    /// Device-to-host output bytes after resident HT compaction.
519    pub dwt97_batch_ht_output_readback_bytes: u64,
520    /// Backend 9/7 resident HT code-block encode dispatches.
521    pub dwt97_batch_ht_codeblock_dispatches: usize,
522    /// Backend 9/7 batch output readback/unpack time in microseconds.
523    pub dwt97_batch_readback_us: u128,
524    /// Logical device-to-host transfers during backend 9/7 batch output readback.
525    pub dwt97_batch_readback_transfers: usize,
526    /// Device-to-host bytes during backend 9/7 batch output readback.
527    pub dwt97_batch_readback_bytes: u64,
528    /// HTJ2K encode time in microseconds.
529    pub htj2k_encode_us: u128,
530    /// Encode-stage accelerator dispatches during HTJ2K encode.
531    pub htj2k_encode_accelerator_dispatches: usize,
532    /// HT cleanup code-block accelerator dispatches during HTJ2K encode.
533    pub htj2k_encode_ht_code_block_dispatches: usize,
534    /// Packetization accelerator dispatches during HTJ2K encode.
535    pub htj2k_encode_packetization_dispatches: usize,
536    /// Time spent writing compressed frames to a DICOM `PixelData` spool.
537    pub dicom_spool_write_us: u128,
538    /// Time spent writing final DICOM instances.
539    pub dicom_final_write_us: u128,
540    /// Number of source tiles represented by this timing report.
541    pub tile_count: usize,
542    /// Number of components transformed into wavelet bands.
543    pub component_count: usize,
544    /// Number of same-geometry transform batches offered to the accelerator.
545    pub batch_count: usize,
546    /// Number of component jobs in same-geometry transform batches.
547    pub batch_jobs: usize,
548    /// Number of accelerator hook calls.
549    pub accelerator_attempts: usize,
550    /// Number of component jobs offered through accelerator hook calls.
551    pub accelerator_jobs: usize,
552    /// Number of accelerator hook calls that returned an accelerated result.
553    pub accelerator_dispatches: usize,
554    /// Number of component jobs completed by accelerated results.
555    pub accelerator_dispatched_jobs: usize,
556    /// Number of component jobs completed by scalar CPU fallback transforms.
557    pub cpu_fallback_jobs: usize,
558}
559
560impl TranscodeTimingReport {
561    /// Returns true when the report contains evidence that accelerator-backed
562    /// work executed for the transcode transform path.
563    pub fn accelerator_work_observed(&self) -> bool {
564        self.accelerator_dispatches > 0
565            || self.dwt97_batch_pack_upload_transfers > 0
566            || self.dwt97_batch_pack_upload_bytes > 0
567            || self.dwt97_batch_resident_dct_handoff_count > 0
568            || self.dwt97_batch_idct_row_lift_us > 0
569            || self.dwt97_batch_column_lift_us > 0
570            || self.dwt97_batch_resident_dwt_handoff_count > 0
571            || self.dwt97_batch_quantize_codeblock_us > 0
572            || self.dwt97_batch_ht_encode_us > 0
573            || self.dwt97_batch_ht_kernel_us > 0
574            || self.dwt97_batch_ht_compact_us > 0
575            || self.dwt97_batch_ht_codeblock_dispatches > 0
576            || self.dwt97_batch_readback_transfers > 0
577            || self.dwt97_batch_readback_bytes > 0
578            || self.dwt97_batch_ht_status_readback_transfers > 0
579            || self.dwt97_batch_ht_status_readback_bytes > 0
580            || self.dwt97_batch_ht_output_readback_transfers > 0
581            || self.dwt97_batch_ht_output_readback_bytes > 0
582    }
583
584    pub(super) fn add_assign(&mut self, other: Self) {
585        macro_rules! saturating_add_fields {
586            ($($field:ident),+ $(,)?) => {
587                $(
588                    self.$field = self.$field.saturating_add(other.$field);
589                )+
590            };
591        }
592
593        saturating_add_fields!(
594            source_raw_probe_us,
595            read_region_decode_us,
596            compose_pad_us,
597            generated_jpeg_encode_us,
598            jpeg_dct_extract_us,
599            jpeg_dct_repack_us,
600            dct_to_wavelet_total_us,
601            dct_to_wavelet_accelerator_us,
602            dct_to_wavelet_cpu_fallback_us,
603            dwt_decompose_us,
604            dwt97_batch_pack_upload_us,
605            dwt97_batch_pack_upload_transfers,
606            dwt97_batch_pack_upload_bytes,
607            dwt97_batch_resident_dct_handoff_count,
608            dwt97_batch_idct_row_lift_us,
609            dwt97_batch_column_lift_us,
610            dwt97_batch_resident_dwt_handoff_count,
611            dwt97_batch_quantize_codeblock_us,
612            dwt97_batch_ht_encode_us,
613            dwt97_batch_ht_kernel_us,
614            dwt97_batch_ht_status_readback_us,
615            dwt97_batch_ht_status_readback_transfers,
616            dwt97_batch_ht_status_readback_bytes,
617            dwt97_batch_ht_compact_us,
618            dwt97_batch_ht_output_readback_us,
619            dwt97_batch_ht_output_readback_transfers,
620            dwt97_batch_ht_output_readback_bytes,
621            dwt97_batch_ht_codeblock_dispatches,
622            dwt97_batch_readback_us,
623            dwt97_batch_readback_transfers,
624            dwt97_batch_readback_bytes,
625            htj2k_encode_us,
626            htj2k_encode_accelerator_dispatches,
627            htj2k_encode_ht_code_block_dispatches,
628            htj2k_encode_packetization_dispatches,
629            dicom_spool_write_us,
630            dicom_final_write_us,
631            tile_count,
632            component_count,
633            batch_count,
634            batch_jobs,
635            accelerator_attempts,
636            accelerator_jobs,
637            accelerator_dispatches,
638            accelerator_dispatched_jobs,
639            cpu_fallback_jobs,
640        );
641    }
642}
643
644/// Per-component transcode geometry preserved in the generated codestream.
645#[derive(Debug, Clone, PartialEq, Eq)]
646pub struct TranscodeComponentReport {
647    /// Component index in JPEG SOF order.
648    pub component_index: usize,
649    /// Native component width in samples before HTJ2K SIZ expansion.
650    pub width: u32,
651    /// Native component height in samples before HTJ2K SIZ expansion.
652    pub height: u32,
653    /// Number of DCT blocks per component row, including padded edge blocks.
654    pub block_cols: u32,
655    /// Number of DCT block rows, including padded edge blocks.
656    pub block_rows: u32,
657    /// HTJ2K SIZ horizontal sampling factor.
658    pub x_rsiz: u8,
659    /// HTJ2K SIZ vertical sampling factor.
660    pub y_rsiz: u8,
661}
662
663/// Error metrics from an optional validation oracle.
664pub type TranscodeValidationMetrics = ErrorMetrics;
665
666/// Classification for optional coefficient-validation metrics.
667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub enum TranscodeValidationClassification {
669    /// All compared coefficients match the selected oracle exactly.
670    Exact,
671    /// Coefficients satisfy the experimental one-LSB-bounded threshold:
672    /// maximum absolute error is at most one LSB and at least 99.9% of
673    /// coefficients match exactly.
674    OneLsbBounded,
675    /// Coefficients do not satisfy the exact or one-LSB-bounded thresholds.
676    OutsideThreshold,
677}
678
679impl TranscodeValidationClassification {
680    /// Classify validation metrics using the experimental acceptance
681    /// thresholds documented for this coefficient-domain path.
682    #[must_use]
683    pub fn classify_metrics(metrics: &TranscodeValidationMetrics) -> Self {
684        if metrics.exact_matches == metrics.total && metrics.max_abs_error == 0 {
685            Self::Exact
686        } else if metrics.is_one_lsb_bounded(0.999) {
687            Self::OneLsbBounded
688        } else {
689            Self::OutsideThreshold
690        }
691    }
692}
693
694/// Transcode summary for validation and benchmarking.
695#[derive(Debug, PartialEq, Eq)]
696pub struct TranscodeReport {
697    /// Source reference-grid width.
698    pub width: u32,
699    /// Source reference-grid height.
700    pub height: u32,
701    /// Number of transformed components.
702    pub component_count: usize,
703    /// Native transformed component geometry and SIZ sampling.
704    pub components: Vec<TranscodeComponentReport>,
705    /// Rounded coefficient metrics against the optional float IDCT-then-DWT
706    /// oracle.
707    pub float_reference_metrics: Option<TranscodeValidationMetrics>,
708    /// Threshold classification for `float_reference_metrics`.
709    pub float_reference_classification: Option<TranscodeValidationClassification>,
710    /// Rounded direct coefficients compared with j2k-jpeg scalar
711    /// ISLOW-IDCT-then-reversible-5/3 coefficients.
712    pub integer_reference_metrics: Option<TranscodeValidationMetrics>,
713    /// Threshold classification for `integer_reference_metrics`.
714    pub integer_reference_classification: Option<TranscodeValidationClassification>,
715    /// Number of DWT decomposition levels encoded.
716    pub decomposition_levels: u8,
717    /// Coefficient path used to generate the HTJ2K bands.
718    pub coefficient_path: JpegToHtj2kCoefficientPath,
719    /// Name of the experimental path used.
720    pub path: &'static str,
721    /// Wall-clock extraction time in microseconds.
722    pub extract_us: u128,
723    /// Wall-clock DCT-to-wavelet time in microseconds.
724    pub transform_us: u128,
725    /// Wall-clock HTJ2K encode time in microseconds.
726    pub encode_us: u128,
727    /// Detailed stage timings and accelerator/fallback counters.
728    pub timings: TranscodeTimingReport,
729}