1use crate::PixelDescriptor;
17use crate::convert::{ConvertPlan, ConvertStep, FusedKind};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum SimdTier {
36 Unknown,
42 CurrentHost,
45 Wasm,
47 Wasm128,
49 Neon,
51 X86V1,
53 X86V2,
55 X86V3,
57 X86V4,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66#[non_exhaustive]
67pub struct ComputeEnvironment {
68 available_cores: usize,
69 available_ram_bytes: Option<u64>,
70 simd_tier: Option<SimdTier>,
71}
72
73#[rustfmt::skip]
74impl ComputeEnvironment {
75 #[must_use] pub fn new() -> Self { Self { available_cores: 1, available_ram_bytes: None, simd_tier: None } }
77 #[must_use] pub fn with_cores(mut self, cores: usize) -> Self { self.available_cores = cores.max(1); self }
80 #[must_use] pub fn with_available_ram_bytes(mut self, bytes: u64) -> Self { self.available_ram_bytes = Some(bytes); self }
82 #[must_use] pub fn with_simd_tier(mut self, tier: SimdTier) -> Self { self.simd_tier = Some(tier); self }
84 #[must_use] pub fn cores(&self) -> usize { self.available_cores }
86 #[must_use] pub fn available_ram_bytes(&self) -> Option<u64> { self.available_ram_bytes }
88 #[must_use] pub fn simd_tier(&self) -> Option<SimdTier> { self.simd_tier }
90}
91
92impl Default for ComputeEnvironment {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
103#[non_exhaustive]
104pub struct ImageCharacteristics {
105 width: u32,
106 height: u32,
107 descriptor: PixelDescriptor,
108}
109
110#[rustfmt::skip]
111impl ImageCharacteristics {
112 #[must_use] pub fn new(width: u32, height: u32, descriptor: PixelDescriptor) -> Self { Self { width, height, descriptor } }
114 #[must_use] pub fn width(&self) -> u32 { self.width }
116 #[must_use] pub fn height(&self) -> u32 { self.height }
118 #[must_use] pub fn descriptor(&self) -> &PixelDescriptor { &self.descriptor }
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
131#[non_exhaustive]
132pub struct ResourceEstimate {
133 peak_memory_bytes_est: Option<u64>,
134 wall_ms: Option<u64>,
135 intermediate_buffer_count: Option<u32>,
136}
137
138#[rustfmt::skip]
139impl ResourceEstimate {
140 #[must_use] pub fn unknown() -> Self { Self { peak_memory_bytes_est: None, wall_ms: None, intermediate_buffer_count: None } }
142 #[must_use] pub fn new(peak_memory_bytes_est: u64, wall_ms: u64) -> Self {
146 Self { peak_memory_bytes_est: Some(peak_memory_bytes_est), wall_ms: Some(wall_ms), intermediate_buffer_count: None }
147 }
148 #[must_use] pub fn with_intermediate_buffer_count(mut self, n: u32) -> Self { self.intermediate_buffer_count = Some(n); self }
152 #[must_use] pub fn peak_memory_bytes_est(&self) -> Option<u64> { self.peak_memory_bytes_est }
154 #[must_use] pub fn wall_ms(&self) -> Option<u64> { self.wall_ms }
162 #[must_use] pub fn intermediate_buffer_count(&self) -> Option<u32> { self.intermediate_buffer_count }
165}
166
167const ONE_MP: f64 = 1_048_576.0;
172const GIB: f64 = 1_073_741_824.0;
173
174const fn gib_to_ns_per_mp(throughput_gib_s: f64, bytes_per_pixel: f64) -> f64 {
176 bytes_per_pixel * ONE_MP * 1.0e9 / (throughput_gib_s * GIB)
177}
178
179#[rustfmt::skip]
185fn step_cost_ns_per_mp(step: &ConvertStep, current_bpp: usize) -> f64 {
186 let bpp = current_bpp as f64;
187 let gib = |g: f64| gib_to_ns_per_mp(g, bpp);
188 let bucketed = |bs: &[(usize, f64)], fallback: f64| -> f64 {
189 gib(bs.iter().copied().find_map(|(b, g)| (b == current_bpp).then_some(g)).unwrap_or(fallback))
190 };
191 match step {
192 ConvertStep::Identity => 0.0,
193 ConvertStep::SwizzleBgraRgba => bucketed(&[(4, 116.42)], 75.0),
195 ConvertStep::RgbToBgra => gib(80.0),
196 ConvertStep::AddAlpha => bucketed(&[(3, 125.06), (6, 40.59), (12, 104.01)], 30.0),
197 ConvertStep::DropAlpha => bucketed(&[(4, 95.90), (8, 133.63), (16, 148.81)], 80.0),
198 ConvertStep::MatteComposite { .. } => gib(5.0), ConvertStep::GrayToRgb => bucketed(&[(1, 12.85)], 60.0),
200 ConvertStep::GrayToRgba => gib(8.6),
201 ConvertStep::RgbToGray { .. } => gib(12.0),
202 ConvertStep::RgbaToGray { .. } => gib(10.0),
203 ConvertStep::GrayAlphaToRgba => bucketed(&[(2, 95.30), (4, 119.80), (8, 149.72)], 60.0),
204 ConvertStep::GrayAlphaToRgb | ConvertStep::GrayAlphaToGray => gib(80.0),
205 ConvertStep::GrayToGrayAlpha => gib(100.0),
206 ConvertStep::U8ToU16 => gib(112.82),
208 ConvertStep::U16ToU8 => gib(34.39),
209 ConvertStep::NaiveU8ToF32 => gib(95.21),
210 ConvertStep::NaiveF32ToU8 => gib(52.99),
211 ConvertStep::U16ToF32 => gib(88.68),
212 ConvertStep::F32ToU16 => gib(64.33),
213 ConvertStep::F16ToF32 => gib(7.09),
214 ConvertStep::F32ToF16 => gib(3.25),
215 ConvertStep::SrgbU8ToLinearF32 => gib(24.26),
217 ConvertStep::LinearF32ToSrgbU8 => gib(4.56),
218 ConvertStep::PqU16ToLinearF32 => gib(2.68),
219 ConvertStep::LinearF32ToPqU16 => gib(1.39),
220 ConvertStep::HlgU16ToLinearF32 => gib(6.16),
221 ConvertStep::LinearF32ToHlgU16 => gib(4.44),
222 ConvertStep::PqF32ToLinearF32 => gib(3.0),
223 ConvertStep::LinearF32ToPqF32 => gib(2.72),
224 ConvertStep::HlgF32ToLinearF32 => gib(6.0),
225 ConvertStep::LinearF32ToHlgF32 => gib(4.0),
226 ConvertStep::SrgbF32ToLinearF32 | ConvertStep::SrgbF32ToLinearF32Extended => gib(24.95),
227 ConvertStep::LinearF32ToSrgbF32 | ConvertStep::LinearF32ToSrgbF32Extended => gib(8.0),
228 ConvertStep::Bt709F32ToLinearF32 | ConvertStep::Gamma22F32ToLinearF32 => gib(6.0),
229 ConvertStep::LinearF32ToBt709F32 | ConvertStep::LinearF32ToGamma22F32 => gib(4.5),
230 ConvertStep::StraightToPremul => gib(13.74),
232 ConvertStep::PremulToStraight => gib(7.51), ConvertStep::LinearRgbToOklab => gib(1.61),
235 ConvertStep::OklabToLinearRgb => gib(53.25),
236 ConvertStep::LinearRgbaToOklaba => gib(2.14),
237 ConvertStep::OklabaToLinearRgba => gib(58.91),
238 ConvertStep::GamutMatrixRgbF32(_) => gib(21.84),
240 ConvertStep::GamutMatrixRgbaF32(_) => gib(20.0),
241 ConvertStep::Fused { kind, .. } => match kind {
242 FusedKind::SrgbU8GamutRgb => gib(3.79),
243 FusedKind::SrgbU8GamutRgba => gib(3.5),
244 FusedKind::SrgbU16GamutRgb => gib(5.84),
245 FusedKind::SrgbU8ToLinearF32Rgb => gib(11.19),
246 FusedKind::LinearF32ToSrgbU8Rgb => gib(3.11),
247 },
248 #[cfg(feature = "hdr-experimental")]
251 ConvertStep::ToneMapBt2446A { .. } => 4_194_304.0,
252 #[cfg(feature = "hdr-experimental")]
254 ConvertStep::SoftCompressOklch { .. } => gib(3.0),
255 }
256}
257
258#[rustfmt::skip]
262fn step_is_parallelizable(step: &ConvertStep) -> bool {
263 #[cfg(feature = "hdr-experimental")]
264 if matches!(step, ConvertStep::ToneMapBt2446A { .. } | ConvertStep::SoftCompressOklch { .. }) {
265 return false;
266 }
267 let _ = step;
268 true
269}
270
271#[rustfmt::skip]
275fn simd_tier_multiplier(tier: SimdTier) -> f64 {
276 match tier {
277 SimdTier::X86V4 => 0.85, SimdTier::X86V2 | SimdTier::X86V1 => 1.4, SimdTier::Wasm128 => 1.3,
280 SimdTier::Wasm => 2.0,
281 SimdTier::X86V3 | SimdTier::Neon | SimdTier::Unknown | SimdTier::CurrentHost => 1.0,
282 }
283}
284
285#[rustfmt::skip]
291pub(crate) fn estimate_plan(plan: &ConvertPlan, image: &ImageCharacteristics, compute: &ComputeEnvironment) -> ResourceEstimate {
292 let (width, height) = (image.width(), image.height());
293 let tier_mul = compute.simd_tier().map(simd_tier_multiplier).unwrap_or(1.0);
294 let pixels = u64::from(width) * u64::from(height);
295 let dst_bytes = pixels * plan.to().bytes_per_pixel() as u64;
296 if plan.is_identity() {
300 let ms = (dst_bytes as f64) / (30.0 * GIB) * 1_000.0 * tier_mul;
301 return finalize(dst_bytes, ms.ceil() as u64, 1, 0, compute);
302 }
303 let pixels_mp = (pixels as f64) / ONE_MP;
304 let multi = plan.steps().len() > 1;
307 let (mut max_bpp, mut desc) = (plan.from().bytes_per_pixel(), plan.from());
308 let knee = (u64::from(height) / 64).clamp(1, 16) as u32;
310 let (mut total_time_ms, mut any_serial, mut min_knee) = (0.0_f64, false, u32::MAX);
311 for step in plan.steps() {
312 total_time_ms += step_cost_ns_per_mp(step, desc.bytes_per_pixel()) * pixels_mp / 1e6;
313 if step_is_parallelizable(step) { min_knee = min_knee.min(knee); } else { any_serial = true; }
314 desc = intermediate_after(desc, step);
315 max_bpp = max_bpp.max(desc.bytes_per_pixel());
316 }
317 let scratch_bytes = if multi { (u64::from(width) * max_bpp as u64).saturating_mul(2) } else { 0 };
318 let buffer_count: u32 = if multi { 2 } else { 0 };
319 let bottleneck = if any_serial || min_knee == u32::MAX { 1 } else { min_knee };
320 finalize(dst_bytes.saturating_add(scratch_bytes), (total_time_ms * tier_mul).ceil() as u64, bottleneck, buffer_count, compute)
321}
322
323#[rustfmt::skip]
326fn finalize(peak: u64, wall_st: u64, bottleneck: u32, buffers: u32, compute: &ComputeEnvironment) -> ResourceEstimate {
327 let eff = (compute.cores() as u64).max(1).min(bottleneck.max(1) as u64);
328 ResourceEstimate::new(peak, wall_st.div_ceil(eff)).with_intermediate_buffer_count(buffers)
329}
330
331fn intermediate_after(current: PixelDescriptor, step: &ConvertStep) -> PixelDescriptor {
334 crate::convert::intermediate_desc_for_estimate(current, step)
335}
336
337#[cfg(test)]
338#[rustfmt::skip]
339mod local_type_contract_tests {
340 use super::*;
343 const D: PixelDescriptor = PixelDescriptor::RGB8_SRGB;
344
345 #[test]
346 fn compute_environment_builder_clamps_and_defaults() {
347 assert_eq!(ComputeEnvironment::new().cores(), 1);
348 assert_eq!(ComputeEnvironment::new().with_cores(0).cores(), 1);
349 assert_eq!(ComputeEnvironment::default().with_cores(16).cores(), 16);
350 let e = ComputeEnvironment::new().with_available_ram_bytes(1 << 30);
351 assert_eq!(e.available_ram_bytes(), Some(1 << 30));
352 assert_eq!(ComputeEnvironment::new().simd_tier(), None);
353 let t = ComputeEnvironment::new().with_simd_tier(SimdTier::X86V3);
354 assert_eq!(t.simd_tier(), Some(SimdTier::X86V3));
355 }
356 #[test]
357 fn image_characteristics_fields() {
358 let im = ImageCharacteristics::new(1024, 768, D);
359 assert_eq!((im.width(), im.height(), *im.descriptor()), (1024, 768, D));
360 }
361 #[test]
362 fn resource_estimate_new_unknown_and_buffer_count() {
363 let est = ResourceEstimate::new(200, 1000);
364 assert_eq!(est.peak_memory_bytes_est(), Some(200));
365 assert_eq!(est.wall_ms(), Some(1000));
366 assert_eq!(est.intermediate_buffer_count(), None);
367 let u = ResourceEstimate::unknown();
368 assert_eq!(u.peak_memory_bytes_est(), None);
369 assert_eq!(u.wall_ms(), None);
370 assert_eq!(u.intermediate_buffer_count(), None);
371 let withbuf = ResourceEstimate::new(200, 1000).with_intermediate_buffer_count(2);
372 assert_eq!(withbuf.intermediate_buffer_count(), Some(2));
373 }
374}