use crate::config::{EncoderConfig, Preset};
const LOSSY_M0_BYTES_PER_PIXEL: f64 = 13.4;
const LOSSY_M0_FIXED_OVERHEAD: u64 = 115_000;
const LOSSY_M3_BYTES_PER_PIXEL: f64 = 13.7;
const LOSSY_M3_FIXED_OVERHEAD: u64 = 220_000;
const LOSSLESS_M0_BYTES_PER_PIXEL: f64 = 24.0;
const LOSSLESS_M0_FIXED_OVERHEAD: u64 = 600_000;
const LOSSLESS_M1_BYTES_PER_PIXEL: f64 = 34.0;
const LOSSLESS_M1_FIXED_OVERHEAD: u64 = 1_500_000;
const DECODE_BYTES_PER_PIXEL: f64 = 15.0;
const DECODE_FIXED_OVERHEAD: u64 = 133_000;
const DECODE_THROUGHPUT_MAX_MPIXELS: f64 = 200.0;
const DECODE_THROUGHPUT_TYP_MPIXELS: f64 = 100.0;
const DECODE_THROUGHPUT_MIN_MPIXELS: f64 = 30.0;
const LOSSY_ENCODE_THROUGHPUT_MAX_MPIXELS: f64 = 40.0;
const LOSSY_ENCODE_THROUGHPUT_TYP_MPIXELS: f64 = 15.0;
const LOSSY_ENCODE_THROUGHPUT_MIN_MPIXELS: f64 = 9.0;
const LOSSLESS_ENCODE_THROUGHPUT_MAX_MPIXELS: f64 = 150.0;
const LOSSLESS_ENCODE_THROUGHPUT_TYP_MPIXELS: f64 = 3.5;
const LOSSLESS_ENCODE_THROUGHPUT_MIN_MPIXELS: f64 = 3.0;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct EncodeEstimate {
pub peak_memory_bytes_min: u64,
pub peak_memory_bytes: u64,
pub peak_memory_bytes_max: u64,
pub allocations: u32,
pub time_ms_min: f32,
pub time_ms: f32,
pub time_ms_max: f32,
pub output_bytes: u64,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct DecodeEstimate {
pub peak_memory_bytes_min: u64,
pub peak_memory_bytes: u64,
pub peak_memory_bytes_max: u64,
pub allocations: u32,
pub time_ms_min: f32,
pub time_ms: f32,
pub time_ms_max: f32,
pub output_bytes: u64,
}
#[must_use]
pub fn estimate_encode(width: u32, height: u32, bpp: u8, config: &EncoderConfig) -> EncodeEstimate {
let pixels = (width as u64) * (height as u64);
let input_bytes = pixels * (bpp as u64);
let peak_memory_bytes = if config.lossless {
if config.method == 0 {
LOSSLESS_M0_FIXED_OVERHEAD + (pixels as f64 * LOSSLESS_M0_BYTES_PER_PIXEL) as u64
} else {
LOSSLESS_M1_FIXED_OVERHEAD + (pixels as f64 * LOSSLESS_M1_BYTES_PER_PIXEL) as u64
}
} else {
if config.method <= 2 {
LOSSY_M0_FIXED_OVERHEAD + (pixels as f64 * LOSSY_M0_BYTES_PER_PIXEL) as u64
} else {
LOSSY_M3_FIXED_OVERHEAD + (pixels as f64 * LOSSY_M3_BYTES_PER_PIXEL) as u64
}
};
let output_ratio: f64 = if config.lossless {
0.5
} else {
let q = (config.quality.clamp(0.0, 100.0)) as f64;
0.02 + (q / 100.0) * 0.18
};
let estimated_output = (input_bytes as f64 * output_ratio) as u64;
let method_speed_factor = match config.method {
0 => 4.0,
1 => 2.5,
2 => 1.8,
3 => 1.3,
4 => 1.0,
5 => 0.95,
6 => 0.9,
_ => 1.0,
};
let quality_speed_factor = if config.near_lossless < 100 {
0.77
} else {
1.0
};
let preset_speed_factor = match config.preset {
Preset::Default => 1.0,
Preset::Photo => 0.95,
Preset::Picture => 1.0,
Preset::Drawing => 1.0,
Preset::Icon => 1.05,
Preset::Text => 1.05,
};
let pixels_f = pixels as f64;
let speed_adjust = method_speed_factor * quality_speed_factor * preset_speed_factor;
let (throughput_max, throughput_typ, throughput_min) = if config.lossless {
(
LOSSLESS_ENCODE_THROUGHPUT_MAX_MPIXELS * speed_adjust,
LOSSLESS_ENCODE_THROUGHPUT_TYP_MPIXELS * speed_adjust,
LOSSLESS_ENCODE_THROUGHPUT_MIN_MPIXELS * speed_adjust,
)
} else {
(
LOSSY_ENCODE_THROUGHPUT_MAX_MPIXELS * speed_adjust,
LOSSY_ENCODE_THROUGHPUT_TYP_MPIXELS * speed_adjust,
LOSSY_ENCODE_THROUGHPUT_MIN_MPIXELS * speed_adjust,
)
};
let time_ms_min = (pixels_f / (throughput_max * 1000.0)) as f32;
let time_ms = (pixels_f / (throughput_typ * 1000.0)) as f32;
let time_ms_max = (pixels_f / (throughput_min * 1000.0)) as f32;
let allocations = 25;
let (min_mult, typ_mult, max_mult) = if config.lossless {
(0.6, 1.2, 1.5) } else {
(1.0, 1.2, 2.25) };
let peak_memory_bytes_min = (peak_memory_bytes as f64 * min_mult) as u64;
let peak_memory_bytes_typ = (peak_memory_bytes as f64 * typ_mult) as u64;
let peak_memory_bytes_max = (peak_memory_bytes as f64 * max_mult) as u64;
EncodeEstimate {
peak_memory_bytes_min,
peak_memory_bytes: peak_memory_bytes_typ,
peak_memory_bytes_max,
allocations,
time_ms_min,
time_ms,
time_ms_max,
output_bytes: estimated_output,
}
}
#[must_use]
pub fn estimate_decode(width: u32, height: u32, output_bpp: u8) -> DecodeEstimate {
let pixels = (width as u64) * (height as u64);
let output_bytes = pixels * (output_bpp as u64);
let peak_memory_bytes = DECODE_FIXED_OVERHEAD + (pixels as f64 * DECODE_BYTES_PER_PIXEL) as u64;
let peak_memory_bytes_min = peak_memory_bytes;
let peak_memory_bytes_max = (peak_memory_bytes as f64 * 1.05) as u64;
let pixels_f = pixels as f64;
let time_ms_min = (pixels_f / (DECODE_THROUGHPUT_MAX_MPIXELS * 1000.0)) as f32; let time_ms = (pixels_f / (DECODE_THROUGHPUT_TYP_MPIXELS * 1000.0)) as f32; let time_ms_max = (pixels_f / (DECODE_THROUGHPUT_MIN_MPIXELS * 1000.0)) as f32;
let allocations = 12;
DecodeEstimate {
peak_memory_bytes_min,
peak_memory_bytes,
peak_memory_bytes_max,
allocations,
time_ms_min,
time_ms,
time_ms_max,
output_bytes,
}
}
#[must_use]
pub fn estimate_decode_zerocopy(width: u32, height: u32) -> DecodeEstimate {
let mut est = estimate_decode(width, height, 4);
est.allocations = 8;
est
}
#[must_use]
pub fn estimate_animation_encode(
width: u32,
height: u32,
frame_count: u32,
config: &EncoderConfig,
) -> EncodeEstimate {
let single_frame = estimate_encode(width, height, 4, config);
let frame_bytes = (width as u64) * (height as u64) * 4;
let peak_memory = single_frame.peak_memory_bytes + frame_bytes / 2 + 200_000;
let frame_count_f = frame_count as f32;
let time_ms_min = single_frame.time_ms_min * frame_count_f;
let time_ms = single_frame.time_ms * frame_count_f;
let time_ms_max = single_frame.time_ms_max * frame_count_f;
let allocations = single_frame.allocations + (frame_count - 1) * 5;
let estimated_output = single_frame.output_bytes * (frame_count as u64);
let (min_mult, typ_mult, max_mult) = if config.lossless {
(0.6, 1.2, 1.5)
} else {
(1.0, 1.2, 2.25)
};
EncodeEstimate {
peak_memory_bytes_min: (peak_memory as f64 * min_mult) as u64,
peak_memory_bytes: (peak_memory as f64 * typ_mult) as u64,
peak_memory_bytes_max: (peak_memory as f64 * max_mult) as u64,
allocations,
time_ms_min,
time_ms,
time_ms_max,
output_bytes: estimated_output,
}
}
#[must_use]
pub fn estimate_animation_decode(width: u32, height: u32, frame_count: u32) -> DecodeEstimate {
let single_frame = estimate_decode(width, height, 4);
let frame_bytes = (width as u64) * (height as u64) * 4;
let peak_memory_min = single_frame.peak_memory_bytes_min + frame_bytes;
let peak_memory = single_frame.peak_memory_bytes + frame_bytes;
let peak_memory_max = single_frame.peak_memory_bytes_max + frame_bytes;
let frame_count_f = frame_count as f32;
let time_ms_min = single_frame.time_ms_min * frame_count_f;
let time_ms = single_frame.time_ms * frame_count_f;
let time_ms_max = single_frame.time_ms_max * frame_count_f;
let allocations = frame_count * 2 + 5;
let output_bytes = single_frame.output_bytes * (frame_count as u64);
DecodeEstimate {
peak_memory_bytes_min: peak_memory_min,
peak_memory_bytes: peak_memory,
peak_memory_bytes_max: peak_memory_max,
allocations,
time_ms_min,
time_ms,
time_ms_max,
output_bytes,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lossy_min_estimate_m4() {
let est = estimate_encode(1024, 1024, 4, &EncoderConfig::default());
let measured = 14_010_000u64;
let error = (est.peak_memory_bytes_min as f64 - measured as f64).abs() / measured as f64;
assert!(
error < 0.10,
"Lossy 1024x1024 m4 min: estimated {} vs measured {}, error {:.1}%",
est.peak_memory_bytes_min,
measured,
error * 100.0
);
}
#[test]
fn test_lossy_min_estimate_m0() {
let est = estimate_encode(1024, 1024, 4, &EncoderConfig::default().method(0));
let measured = 13_520_000u64;
let error = (est.peak_memory_bytes_min as f64 - measured as f64).abs() / measured as f64;
assert!(
error < 0.10,
"Lossy 1024x1024 m0 min: estimated {} vs measured {}, error {:.1}%",
est.peak_memory_bytes_min,
measured,
error * 100.0
);
}
#[test]
fn test_lossy_typical_is_higher_than_min() {
let est = estimate_encode(1024, 1024, 4, &EncoderConfig::default());
let ratio = est.peak_memory_bytes as f64 / est.peak_memory_bytes_min as f64;
assert!(
(ratio - 1.2).abs() < 0.05,
"Expected typ/min ratio ~1.2, got {}",
ratio
);
}
#[test]
fn test_lossless_min_estimate_m4() {
let est = estimate_encode(
1024,
1024,
4,
&EncoderConfig::default().lossless(true).method(4),
);
let measured = 35_540_000u64;
assert!(
est.peak_memory_bytes_min < measured && measured < est.peak_memory_bytes,
"Lossless gradient should fall between min ({}) and typ ({}), was {}",
est.peak_memory_bytes_min,
est.peak_memory_bytes,
measured
);
}
#[test]
fn test_lossless_min_estimate_m0() {
let est = estimate_encode(
1024,
1024,
4,
&EncoderConfig::default().lossless(true).method(0),
);
let measured = 24_510_000u64;
assert!(
est.peak_memory_bytes_min < measured && measured < est.peak_memory_bytes,
"Lossless gradient should fall between min ({}) and typ ({}), was {}",
est.peak_memory_bytes_min,
est.peak_memory_bytes,
measured
);
}
#[test]
fn test_lossless_m0_uses_less_memory() {
let m0 = estimate_encode(
1024,
1024,
4,
&EncoderConfig::default().lossless(true).method(0),
);
let m4 = estimate_encode(
1024,
1024,
4,
&EncoderConfig::default().lossless(true).method(4),
);
let ratio = m0.peak_memory_bytes as f64 / m4.peak_memory_bytes as f64;
assert!(ratio < 0.75, "Expected m0 < 75% of m4, got ratio {}", ratio);
}
#[test]
fn test_lossless_more_memory() {
let lossy = estimate_encode(512, 512, 4, &EncoderConfig::default());
let lossless = estimate_encode(512, 512, 4, &EncoderConfig::default().lossless(true));
assert!(lossless.peak_memory_bytes > lossy.peak_memory_bytes * 2);
}
#[test]
fn test_scaling() {
let small = estimate_encode(512, 512, 4, &EncoderConfig::default());
let large = estimate_encode(1024, 1024, 4, &EncoderConfig::default());
let ratio = large.peak_memory_bytes as f64 / small.peak_memory_bytes as f64;
assert!(ratio > 3.0 && ratio < 5.0, "Ratio was {}", ratio);
}
#[test]
fn test_decode_less_than_encode() {
let encode = estimate_encode(1024, 1024, 4, &EncoderConfig::default());
let decode = estimate_decode(1024, 1024, 4);
assert!(decode.peak_memory_bytes < encode.peak_memory_bytes);
}
#[test]
fn test_zerocopy_api() {
let normal = estimate_decode(1024, 1024, 4);
let zerocopy = estimate_decode_zerocopy(1024, 1024);
assert!(zerocopy.peak_memory_bytes <= normal.peak_memory_bytes);
assert!(zerocopy.allocations < normal.allocations);
}
#[test]
fn test_decode_estimate_accuracy() {
let est = estimate_decode(1024, 1024, 4);
let measured = 15_910_000u64; let error = (est.peak_memory_bytes as f64 - measured as f64).abs() / measured as f64;
assert!(
error < 0.10,
"Decode 1024x1024: estimated {} vs measured {}, error {:.1}%",
est.peak_memory_bytes,
measured,
error * 100.0
);
}
#[test]
fn test_decode_time_estimates() {
let est = estimate_decode(1024, 1024, 4);
let pixels = 1024.0 * 1024.0;
let expected_min = pixels / (200.0 * 1000.0);
assert!(
(est.time_ms_min - expected_min as f32).abs() < 1.0,
"time_ms_min: {} vs expected {}",
est.time_ms_min,
expected_min
);
let expected_typ = pixels / (100.0 * 1000.0);
assert!(
(est.time_ms - expected_typ as f32).abs() < 2.0,
"time_ms: {} vs expected {}",
est.time_ms,
expected_typ
);
let expected_max = pixels / (30.0 * 1000.0);
assert!(
(est.time_ms_max - expected_max as f32).abs() < 5.0,
"time_ms_max: {} vs expected {}",
est.time_ms_max,
expected_max
);
}
#[test]
fn test_decode_min_max_range() {
let est = estimate_decode(1024, 1024, 4);
let ratio = est.peak_memory_bytes_max as f64 / est.peak_memory_bytes_min as f64;
assert!(
(ratio - 1.05).abs() < 0.01,
"Expected max/min ratio ~1.05, got {}",
ratio
);
}
}