1use core::fmt;
7
8use crate::{BatchTranscodeReport, TranscodeReport, TranscodeTimingReport};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TranscodePipelineStageKind {
13 EntropyDecode,
15 CoefficientPrep,
17 Transform,
19 QuantizationCodeBlockPrep,
21 Packetization,
23 CodestreamAssembly,
25}
26
27impl TranscodePipelineStageKind {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::EntropyDecode => "entropy_decode",
33 Self::CoefficientPrep => "coefficient_prep",
34 Self::Transform => "transform",
35 Self::QuantizationCodeBlockPrep => "quantization_code_block_prep",
36 Self::Packetization => "packetization",
37 Self::CodestreamAssembly => "codestream_assembly",
38 }
39 }
40}
41
42impl fmt::Display for TranscodePipelineStageKind {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(self.as_str())
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum TranscodeStageProcessor {
51 Cpu,
53 Metal,
55 Hybrid,
57}
58
59impl TranscodeStageProcessor {
60 #[must_use]
62 pub const fn as_str(self) -> &'static str {
63 match self {
64 Self::Cpu => "Cpu",
65 Self::Metal => "Metal",
66 Self::Hybrid => "Hybrid",
67 }
68 }
69}
70
71impl fmt::Display for TranscodeStageProcessor {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(self.as_str())
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct TranscodePipelineStageReport {
80 pub stage: TranscodePipelineStageKind,
82 pub processor: TranscodeStageProcessor,
84 pub cpu_us: u128,
86 pub metal_us: u128,
88 pub transfer_us: u128,
90 pub transfer_count: usize,
92 pub transfer_bytes: u64,
94 pub resident_handoff_count: usize,
96 pub dispatches: usize,
98 pub fallback_jobs: usize,
100 pub note: &'static str,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct TranscodeResidentStageRecommendation {
107 pub stage: TranscodePipelineStageKind,
109 pub evidence_us: u128,
111 pub evidence_dispatches: usize,
113 pub reason: &'static str,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TranscodePipelineMap {
120 pub stages: Vec<TranscodePipelineStageReport>,
122 pub recommendation: TranscodeResidentStageRecommendation,
124}
125
126impl TranscodePipelineMap {
127 #[must_use]
129 pub fn from_timings(timings: &TranscodeTimingReport) -> Self {
130 Self {
131 stages: vec![
132 entropy_decode_stage(timings),
133 coefficient_prep_stage(timings),
134 transform_stage(timings),
135 quantization_code_block_stage(timings),
136 packetization_stage(timings),
137 codestream_assembly_stage(timings),
138 ],
139 recommendation: recommend_next_resident_stage(timings),
140 }
141 }
142}
143
144impl TranscodeTimingReport {
145 #[must_use]
147 pub fn pipeline_map(&self) -> TranscodePipelineMap {
148 TranscodePipelineMap::from_timings(self)
149 }
150}
151
152impl TranscodeReport {
153 #[must_use]
155 pub fn pipeline_map(&self) -> TranscodePipelineMap {
156 self.timings.pipeline_map()
157 }
158}
159
160impl BatchTranscodeReport {
161 #[must_use]
163 pub fn pipeline_map(&self) -> TranscodePipelineMap {
164 self.timings.pipeline_map()
165 }
166}
167
168fn entropy_decode_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
169 TranscodePipelineStageReport {
170 stage: TranscodePipelineStageKind::EntropyDecode,
171 processor: TranscodeStageProcessor::Cpu,
172 cpu_us: timings.jpeg_dct_extract_us,
173 metal_us: 0,
174 transfer_us: 0,
175 transfer_count: 0,
176 transfer_bytes: 0,
177 resident_handoff_count: 0,
178 dispatches: 0,
179 fallback_jobs: 0,
180 note: "JPEG marker parsing, entropy decode, dequantization, and DCT coefficient extraction stay on CPU",
181 }
182}
183
184fn coefficient_prep_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
185 let transfer_us = timings.dwt97_batch_pack_upload_us;
186 let transfer_count = transfer_count_or_timing(
187 timings.dwt97_batch_pack_upload_transfers,
188 transfer_us,
189 timings.dwt97_batch_pack_upload_bytes,
190 );
191 let transfer_dispatch = usize::from(
192 transfer_us > 0 || transfer_count > 0 || timings.dwt97_batch_pack_upload_bytes > 0,
193 );
194 TranscodePipelineStageReport {
195 stage: TranscodePipelineStageKind::CoefficientPrep,
196 processor: processor_for(
197 timings.jpeg_dct_repack_us,
198 0,
199 transfer_us,
200 transfer_dispatch,
201 0,
202 ),
203 cpu_us: timings.jpeg_dct_repack_us,
204 metal_us: 0,
205 transfer_us,
206 transfer_count,
207 transfer_bytes: timings.dwt97_batch_pack_upload_bytes,
208 resident_handoff_count: timings.dwt97_batch_resident_dct_handoff_count,
209 dispatches: transfer_dispatch,
210 fallback_jobs: 0,
211 note: "DCT coefficient repack and Metal buffer pack/upload are visible before transform dispatch",
212 }
213}
214
215fn transform_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
216 let dwt97_kernel_us = timings
217 .dwt97_batch_idct_row_lift_us
218 .saturating_add(timings.dwt97_batch_column_lift_us);
219 let metal_us = if dwt97_kernel_us > 0 {
220 dwt97_kernel_us
221 } else if timings.accelerator_dispatches > 0 {
222 timings.dct_to_wavelet_accelerator_us
223 } else {
224 0
225 };
226 let cpu_us = timings
227 .dct_to_wavelet_cpu_fallback_us
228 .saturating_add(timings.dwt_decompose_us);
229 TranscodePipelineStageReport {
230 stage: TranscodePipelineStageKind::Transform,
231 processor: processor_for(
232 cpu_us,
233 metal_us,
234 timings.dwt97_batch_readback_us,
235 timings.accelerator_dispatches,
236 timings.cpu_fallback_jobs,
237 ),
238 cpu_us,
239 metal_us,
240 transfer_us: timings.dwt97_batch_readback_us,
241 transfer_count: transfer_count_or_timing(
242 timings.dwt97_batch_readback_transfers,
243 timings.dwt97_batch_readback_us,
244 timings.dwt97_batch_readback_bytes,
245 ),
246 transfer_bytes: timings.dwt97_batch_readback_bytes,
247 resident_handoff_count: timings.dwt97_batch_resident_dwt_handoff_count,
248 dispatches: timings.accelerator_dispatches,
249 fallback_jobs: timings.cpu_fallback_jobs,
250 note: "DCT-grid to DWT projection uses accelerator dispatches when available; Ok(None) jobs remain caller CPU fallback",
251 }
252}
253
254fn quantization_code_block_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
255 let metal_us = timings
256 .dwt97_batch_quantize_codeblock_us
257 .saturating_add(timings.dwt97_batch_ht_encode_us)
258 .saturating_add(timings.dwt97_batch_ht_kernel_us)
259 .saturating_add(timings.dwt97_batch_ht_compact_us);
260 let transfer_us = timings
261 .dwt97_batch_ht_status_readback_us
262 .saturating_add(timings.dwt97_batch_ht_output_readback_us);
263 let transfer_count = timings
264 .dwt97_batch_ht_status_readback_transfers
265 .saturating_add(timings.dwt97_batch_ht_output_readback_transfers);
266 let transfer_bytes = timings
267 .dwt97_batch_ht_status_readback_bytes
268 .saturating_add(timings.dwt97_batch_ht_output_readback_bytes);
269 let transfer_count = transfer_count_or_timing(transfer_count, transfer_us, transfer_bytes);
270 let dispatches = timings
271 .dwt97_batch_ht_codeblock_dispatches
272 .saturating_add(timings.htj2k_encode_ht_code_block_dispatches);
273 TranscodePipelineStageReport {
274 stage: TranscodePipelineStageKind::QuantizationCodeBlockPrep,
275 processor: processor_for(0, metal_us, transfer_us, dispatches, 0),
276 cpu_us: 0,
277 metal_us,
278 transfer_us,
279 transfer_count,
280 transfer_bytes,
281 resident_handoff_count: 0,
282 dispatches,
283 fallback_jobs: 0,
284 note: "9/7 quantization/code-block layout is only isolated when a backend reports resident stage timings; otherwise it is inside native encode time",
285 }
286}
287
288fn packetization_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
289 let dispatches = timings.htj2k_encode_packetization_dispatches;
290 TranscodePipelineStageReport {
291 stage: TranscodePipelineStageKind::Packetization,
292 processor: processor_for(0, 0, 0, dispatches, 0),
293 cpu_us: 0,
294 metal_us: 0,
295 transfer_us: 0,
296 transfer_count: 0,
297 transfer_bytes: 0,
298 resident_handoff_count: 0,
299 dispatches,
300 fallback_jobs: 0,
301 note: "Packetization dispatches are counted separately when an encode-stage accelerator handles them; CPU time is otherwise inside native encode time",
302 }
303}
304
305fn codestream_assembly_stage(timings: &TranscodeTimingReport) -> TranscodePipelineStageReport {
306 TranscodePipelineStageReport {
307 stage: TranscodePipelineStageKind::CodestreamAssembly,
308 processor: TranscodeStageProcessor::Cpu,
309 cpu_us: timings.htj2k_encode_us,
310 metal_us: 0,
311 transfer_us: 0,
312 transfer_count: 0,
313 transfer_bytes: 0,
314 resident_handoff_count: 0,
315 dispatches: 0,
316 fallback_jobs: 0,
317 note: "Final marker, tile-part, packet byte ordering, and codestream assembly remain at the CPU/native encoder boundary",
318 }
319}
320
321fn recommend_next_resident_stage(
322 timings: &TranscodeTimingReport,
323) -> TranscodeResidentStageRecommendation {
324 let transform_readback_us = timings.dwt97_batch_readback_us;
325 let code_block_readback_us = timings
326 .dwt97_batch_ht_status_readback_us
327 .saturating_add(timings.dwt97_batch_ht_output_readback_us);
328 let has_resident_transform = timings.accelerator_dispatches > 0
329 && (timings.dwt97_batch_idct_row_lift_us > 0
330 || timings.dwt97_batch_column_lift_us > 0
331 || timings.dct_to_wavelet_accelerator_us > 0);
332
333 if timings.cpu_fallback_jobs > 0 && timings.accelerator_dispatches == 0 {
334 return TranscodeResidentStageRecommendation {
335 stage: TranscodePipelineStageKind::Transform,
336 evidence_us: timings.dct_to_wavelet_cpu_fallback_us,
337 evidence_dispatches: timings.accelerator_attempts,
338 reason: "transform jobs are still completing through caller CPU fallback",
339 };
340 }
341
342 if has_resident_transform && timings.dwt97_batch_quantize_codeblock_us == 0 {
343 return TranscodeResidentStageRecommendation {
344 stage: TranscodePipelineStageKind::QuantizationCodeBlockPrep,
345 evidence_us: transform_readback_us.saturating_add(timings.htj2k_encode_us),
346 evidence_dispatches: timings.accelerator_dispatches,
347 reason: "resident transform output is read back before quantization/code-block prep and native encode",
348 };
349 }
350
351 if timings.dwt97_batch_quantize_codeblock_us > 0
352 && timings.dwt97_batch_ht_codeblock_dispatches == 0
353 {
354 return TranscodeResidentStageRecommendation {
355 stage: TranscodePipelineStageKind::QuantizationCodeBlockPrep,
356 evidence_us: transform_readback_us
357 .saturating_add(code_block_readback_us)
358 .saturating_add(timings.htj2k_encode_us),
359 evidence_dispatches: timings.accelerator_dispatches,
360 reason: "Metal already reaches 9/7 code-block prep; extend residency through HT code-block encode before packetization",
361 };
362 }
363
364 if timings.cpu_fallback_jobs > 0 {
365 return TranscodeResidentStageRecommendation {
366 stage: TranscodePipelineStageKind::Transform,
367 evidence_us: timings.dct_to_wavelet_cpu_fallback_us,
368 evidence_dispatches: timings.accelerator_attempts,
369 reason: "some transform jobs still use CPU fallback after accelerator attempts",
370 };
371 }
372
373 let coefficient_prep_us = timings
374 .jpeg_dct_repack_us
375 .saturating_add(timings.dwt97_batch_pack_upload_us);
376 if coefficient_prep_us > 0 {
377 return TranscodeResidentStageRecommendation {
378 stage: TranscodePipelineStageKind::CoefficientPrep,
379 evidence_us: coefficient_prep_us,
380 evidence_dispatches: timings.accelerator_dispatches,
381 reason:
382 "coefficient repack and host-to-device upload are visible before Metal work starts",
383 };
384 }
385
386 if timings.htj2k_encode_packetization_dispatches == 0 && timings.htj2k_encode_us > 0 {
387 return TranscodeResidentStageRecommendation {
388 stage: TranscodePipelineStageKind::Packetization,
389 evidence_us: timings.htj2k_encode_us,
390 evidence_dispatches: timings.htj2k_encode_accelerator_dispatches,
391 reason:
392 "packetization and codestream assembly remain inside the CPU/native encode boundary",
393 };
394 }
395
396 TranscodeResidentStageRecommendation {
397 stage: TranscodePipelineStageKind::CodestreamAssembly,
398 evidence_us: timings.htj2k_encode_us,
399 evidence_dispatches: timings.htj2k_encode_accelerator_dispatches,
400 reason: "no stronger resident-stage candidate is visible from the current counters",
401 }
402}
403
404fn processor_for(
405 cpu_us: u128,
406 metal_us: u128,
407 transfer_us: u128,
408 dispatches: usize,
409 fallback_jobs: usize,
410) -> TranscodeStageProcessor {
411 let has_cpu = cpu_us > 0 || fallback_jobs > 0;
412 let has_metal = metal_us > 0 || transfer_us > 0 || dispatches > 0;
413 match (has_cpu, has_metal) {
414 (true, true) => TranscodeStageProcessor::Hybrid,
415 (false, true) => TranscodeStageProcessor::Metal,
416 (_, false) => TranscodeStageProcessor::Cpu,
417 }
418}
419
420fn transfer_count_or_timing(
421 explicit_count: usize,
422 transfer_us: u128,
423 transfer_bytes: u64,
424) -> usize {
425 if explicit_count > 0 {
426 explicit_count
427 } else {
428 usize::from(transfer_us > 0 || transfer_bytes > 0)
429 }
430}