use crate::PixelDescriptor;
use crate::convert::{ConvertPlan, ConvertStep, FusedKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SimdTier {
Unknown,
CurrentHost,
Wasm,
Wasm128,
Neon,
X86V1,
X86V2,
X86V3,
X86V4,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ComputeEnvironment {
available_cores: usize,
available_ram_bytes: Option<u64>,
simd_tier: Option<SimdTier>,
}
#[rustfmt::skip]
impl ComputeEnvironment {
#[must_use] pub fn new() -> Self { Self { available_cores: 1, available_ram_bytes: None, simd_tier: None } }
#[must_use] pub fn with_cores(mut self, cores: usize) -> Self { self.available_cores = cores.max(1); self }
#[must_use] pub fn with_available_ram_bytes(mut self, bytes: u64) -> Self { self.available_ram_bytes = Some(bytes); self }
#[must_use] pub fn with_simd_tier(mut self, tier: SimdTier) -> Self { self.simd_tier = Some(tier); self }
#[must_use] pub fn cores(&self) -> usize { self.available_cores }
#[must_use] pub fn available_ram_bytes(&self) -> Option<u64> { self.available_ram_bytes }
#[must_use] pub fn simd_tier(&self) -> Option<SimdTier> { self.simd_tier }
}
impl Default for ComputeEnvironment {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ImageCharacteristics {
width: u32,
height: u32,
descriptor: PixelDescriptor,
}
#[rustfmt::skip]
impl ImageCharacteristics {
#[must_use] pub fn new(width: u32, height: u32, descriptor: PixelDescriptor) -> Self { Self { width, height, descriptor } }
#[must_use] pub fn width(&self) -> u32 { self.width }
#[must_use] pub fn height(&self) -> u32 { self.height }
#[must_use] pub fn descriptor(&self) -> &PixelDescriptor { &self.descriptor }
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ResourceEstimate {
peak_memory_bytes_est: Option<u64>,
wall_ms: Option<u64>,
intermediate_buffer_count: Option<u32>,
}
#[rustfmt::skip]
impl ResourceEstimate {
#[must_use] pub fn unknown() -> Self { Self { peak_memory_bytes_est: None, wall_ms: None, intermediate_buffer_count: None } }
#[must_use] pub fn new(peak_memory_bytes_est: u64, wall_ms: u64) -> Self {
Self { peak_memory_bytes_est: Some(peak_memory_bytes_est), wall_ms: Some(wall_ms), intermediate_buffer_count: None }
}
#[must_use] pub fn with_intermediate_buffer_count(mut self, n: u32) -> Self { self.intermediate_buffer_count = Some(n); self }
#[must_use] pub fn peak_memory_bytes_est(&self) -> Option<u64> { self.peak_memory_bytes_est }
#[must_use] pub fn wall_ms(&self) -> Option<u64> { self.wall_ms }
#[must_use] pub fn intermediate_buffer_count(&self) -> Option<u32> { self.intermediate_buffer_count }
}
const ONE_MP: f64 = 1_048_576.0;
const GIB: f64 = 1_073_741_824.0;
const fn gib_to_ns_per_mp(throughput_gib_s: f64, bytes_per_pixel: f64) -> f64 {
bytes_per_pixel * ONE_MP * 1.0e9 / (throughput_gib_s * GIB)
}
#[rustfmt::skip]
fn step_cost_ns_per_mp(step: &ConvertStep, current_bpp: usize) -> f64 {
let bpp = current_bpp as f64;
let gib = |g: f64| gib_to_ns_per_mp(g, bpp);
let bucketed = |bs: &[(usize, f64)], fallback: f64| -> f64 {
gib(bs.iter().copied().find_map(|(b, g)| (b == current_bpp).then_some(g)).unwrap_or(fallback))
};
match step {
ConvertStep::Identity => 0.0,
ConvertStep::SwizzleBgraRgba => bucketed(&[(4, 116.42)], 75.0),
ConvertStep::RgbToBgra => gib(80.0),
ConvertStep::AddAlpha => bucketed(&[(3, 125.06), (6, 40.59), (12, 104.01)], 30.0),
ConvertStep::DropAlpha => bucketed(&[(4, 95.90), (8, 133.63), (16, 148.81)], 80.0),
ConvertStep::MatteComposite { .. } => gib(5.0), ConvertStep::GrayToRgb => bucketed(&[(1, 12.85)], 60.0),
ConvertStep::GrayToRgba => gib(8.6),
ConvertStep::RgbToGray { .. } => gib(12.0),
ConvertStep::RgbaToGray { .. } => gib(10.0),
ConvertStep::GrayAlphaToRgba => bucketed(&[(2, 95.30), (4, 119.80), (8, 149.72)], 60.0),
ConvertStep::GrayAlphaToRgb | ConvertStep::GrayAlphaToGray => gib(80.0),
ConvertStep::GrayToGrayAlpha => gib(100.0),
ConvertStep::U8ToU16 => gib(112.82),
ConvertStep::U16ToU8 => gib(34.39),
ConvertStep::NaiveU8ToF32 => gib(95.21),
ConvertStep::NaiveF32ToU8 => gib(52.99),
ConvertStep::U16ToF32 => gib(88.68),
ConvertStep::F32ToU16 => gib(64.33),
ConvertStep::F16ToF32 => gib(7.09),
ConvertStep::F32ToF16 => gib(3.25),
ConvertStep::SrgbU8ToLinearF32 => gib(24.26),
ConvertStep::LinearF32ToSrgbU8 => gib(4.56),
ConvertStep::PqU16ToLinearF32 => gib(2.68),
ConvertStep::LinearF32ToPqU16 => gib(1.39),
ConvertStep::HlgU16ToLinearF32 => gib(6.16),
ConvertStep::LinearF32ToHlgU16 => gib(4.44),
ConvertStep::PqF32ToLinearF32 => gib(3.0),
ConvertStep::LinearF32ToPqF32 => gib(2.72),
ConvertStep::HlgF32ToLinearF32 => gib(6.0),
ConvertStep::LinearF32ToHlgF32 => gib(4.0),
ConvertStep::SrgbF32ToLinearF32 | ConvertStep::SrgbF32ToLinearF32Extended => gib(24.95),
ConvertStep::LinearF32ToSrgbF32 | ConvertStep::LinearF32ToSrgbF32Extended => gib(8.0),
ConvertStep::Bt709F32ToLinearF32 | ConvertStep::Gamma22F32ToLinearF32 => gib(6.0),
ConvertStep::LinearF32ToBt709F32 | ConvertStep::LinearF32ToGamma22F32 => gib(4.5),
ConvertStep::StraightToPremul => gib(13.74),
ConvertStep::PremulToStraight => gib(7.51), ConvertStep::LinearRgbToOklab => gib(1.61),
ConvertStep::OklabToLinearRgb => gib(53.25),
ConvertStep::LinearRgbaToOklaba => gib(2.14),
ConvertStep::OklabaToLinearRgba => gib(58.91),
ConvertStep::GamutMatrixRgbF32(_) => gib(21.84),
ConvertStep::GamutMatrixRgbaF32(_) => gib(20.0),
ConvertStep::Fused { kind, .. } => match kind {
FusedKind::SrgbU8GamutRgb => gib(3.79),
FusedKind::SrgbU8GamutRgba => gib(3.5),
FusedKind::SrgbU16GamutRgb => gib(5.84),
FusedKind::SrgbU8ToLinearF32Rgb => gib(11.19),
FusedKind::LinearF32ToSrgbU8Rgb => gib(3.11),
},
#[cfg(feature = "hdr-experimental")]
ConvertStep::ToneMapBt2446A { .. } => 4_194_304.0,
#[cfg(feature = "hdr-experimental")]
ConvertStep::SoftCompressOklch { .. } => gib(3.0),
}
}
#[rustfmt::skip]
fn step_is_parallelizable(step: &ConvertStep) -> bool {
#[cfg(feature = "hdr-experimental")]
if matches!(step, ConvertStep::ToneMapBt2446A { .. } | ConvertStep::SoftCompressOklch { .. }) {
return false;
}
let _ = step;
true
}
#[rustfmt::skip]
fn simd_tier_multiplier(tier: SimdTier) -> f64 {
match tier {
SimdTier::X86V4 => 0.85, SimdTier::X86V2 | SimdTier::X86V1 => 1.4, SimdTier::Wasm128 => 1.3,
SimdTier::Wasm => 2.0,
SimdTier::X86V3 | SimdTier::Neon | SimdTier::Unknown | SimdTier::CurrentHost => 1.0,
}
}
#[rustfmt::skip]
pub(crate) fn estimate_plan(plan: &ConvertPlan, image: &ImageCharacteristics, compute: &ComputeEnvironment) -> ResourceEstimate {
let (width, height) = (image.width(), image.height());
let tier_mul = compute.simd_tier().map(simd_tier_multiplier).unwrap_or(1.0);
let pixels = u64::from(width) * u64::from(height);
let dst_bytes = pixels * plan.to().bytes_per_pixel() as u64;
if plan.is_identity() {
let ms = (dst_bytes as f64) / (30.0 * GIB) * 1_000.0 * tier_mul;
return finalize(dst_bytes, ms.ceil() as u64, 1, 0, compute);
}
let pixels_mp = (pixels as f64) / ONE_MP;
let multi = plan.steps().len() > 1;
let (mut max_bpp, mut desc) = (plan.from().bytes_per_pixel(), plan.from());
let knee = (u64::from(height) / 64).clamp(1, 16) as u32;
let (mut total_time_ms, mut any_serial, mut min_knee) = (0.0_f64, false, u32::MAX);
for step in plan.steps() {
total_time_ms += step_cost_ns_per_mp(step, desc.bytes_per_pixel()) * pixels_mp / 1e6;
if step_is_parallelizable(step) { min_knee = min_knee.min(knee); } else { any_serial = true; }
desc = intermediate_after(desc, step);
max_bpp = max_bpp.max(desc.bytes_per_pixel());
}
let scratch_bytes = if multi { (u64::from(width) * max_bpp as u64).saturating_mul(2) } else { 0 };
let buffer_count: u32 = if multi { 2 } else { 0 };
let bottleneck = if any_serial || min_knee == u32::MAX { 1 } else { min_knee };
finalize(dst_bytes.saturating_add(scratch_bytes), (total_time_ms * tier_mul).ceil() as u64, bottleneck, buffer_count, compute)
}
#[rustfmt::skip]
fn finalize(peak: u64, wall_st: u64, bottleneck: u32, buffers: u32, compute: &ComputeEnvironment) -> ResourceEstimate {
let eff = (compute.cores() as u64).max(1).min(bottleneck.max(1) as u64);
ResourceEstimate::new(peak, wall_st.div_ceil(eff)).with_intermediate_buffer_count(buffers)
}
fn intermediate_after(current: PixelDescriptor, step: &ConvertStep) -> PixelDescriptor {
crate::convert::intermediate_desc_for_estimate(current, step)
}
#[cfg(test)]
#[rustfmt::skip]
mod local_type_contract_tests {
use super::*;
const D: PixelDescriptor = PixelDescriptor::RGB8_SRGB;
#[test]
fn compute_environment_builder_clamps_and_defaults() {
assert_eq!(ComputeEnvironment::new().cores(), 1);
assert_eq!(ComputeEnvironment::new().with_cores(0).cores(), 1);
assert_eq!(ComputeEnvironment::default().with_cores(16).cores(), 16);
let e = ComputeEnvironment::new().with_available_ram_bytes(1 << 30);
assert_eq!(e.available_ram_bytes(), Some(1 << 30));
assert_eq!(ComputeEnvironment::new().simd_tier(), None);
let t = ComputeEnvironment::new().with_simd_tier(SimdTier::X86V3);
assert_eq!(t.simd_tier(), Some(SimdTier::X86V3));
}
#[test]
fn image_characteristics_fields() {
let im = ImageCharacteristics::new(1024, 768, D);
assert_eq!((im.width(), im.height(), *im.descriptor()), (1024, 768, D));
}
#[test]
fn resource_estimate_new_unknown_and_buffer_count() {
let est = ResourceEstimate::new(200, 1000);
assert_eq!(est.peak_memory_bytes_est(), Some(200));
assert_eq!(est.wall_ms(), Some(1000));
assert_eq!(est.intermediate_buffer_count(), None);
let u = ResourceEstimate::unknown();
assert_eq!(u.peak_memory_bytes_est(), None);
assert_eq!(u.wall_ms(), None);
assert_eq!(u.intermediate_buffer_count(), None);
let withbuf = ResourceEstimate::new(200, 1000).with_intermediate_buffer_count(2);
assert_eq!(withbuf.intermediate_buffer_count(), Some(2));
}
}