mod analyse;
mod chain;
mod compensate;
mod confidence;
mod pyramid;
#[cfg(all(test, any(feature = "vulkan", feature = "metal")))]
pub(crate) use analyse::mv_field_byte_offset;
pub(crate) use analyse::{confidence_byte_offset, run_analyse, run_seeded_refine};
#[cfg(all(test, any(feature = "vulkan", feature = "metal")))]
pub(crate) use chain::{neighbour_idx_for_k, pair_byte_offset};
pub(crate) use chain::{run_pair_analyse, zero_pair_slot};
pub(crate) use compensate::run_compensate;
pub(crate) use confidence::{run_confidence_for_neighbour, sad_noise_floor, thsad};
use cubecl::prelude::*;
use cubecl::server::Handle;
pub(crate) use pyramid::{pyramid_pixels_per_frame, run_pyramid_build};
use crate::nlmeans::align::StorageAlign;
#[non_exhaustive]
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum MotionCompensationMode {
#[default]
None,
Mvtools {
blksize: u32,
overlap: u32,
search_radius: u32,
pyramid_levels: u32,
estimation: MotionEstimation,
},
}
#[non_exhaustive]
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum MotionEstimation {
#[default]
Auto,
Direct,
Chained {
refine_radius: u32,
},
}
pub const DEFAULT_REFINE_RADIUS: u32 = 2;
pub const CHAINED_RADIUS_THRESHOLD: u32 = 3;
impl MotionEstimation {
pub fn chained_default() -> Self {
Self::Chained {
refine_radius: DEFAULT_REFINE_RADIUS,
}
}
pub fn resolve(self, temporal_radius: u32) -> Self {
match self {
Self::Auto if temporal_radius >= CHAINED_RADIUS_THRESHOLD => Self::chained_default(),
Self::Auto => Self::Direct,
other => other,
}
}
pub(crate) fn validate(&self) -> Result<(), anyhow::Error> {
let Self::Chained { refine_radius } = *self else {
return Ok(());
};
if refine_radius == 0 || refine_radius > MAX_SEARCH_RADIUS {
anyhow::bail!(
"motion-estimation refine_radius={refine_radius} must be in 1..={MAX_SEARCH_RADIUS}"
);
}
Ok(())
}
}
pub const DEFAULT_BLKSIZE: u32 = 16;
pub const DEFAULT_OVERLAP: u32 = 8;
pub const DEFAULT_SEARCH_RADIUS: u32 = 4;
pub const DEFAULT_PYRAMID_LEVELS: u32 = 2;
pub const MAX_PYRAMID_LEVELS: u32 = 3;
pub const MAX_SEARCH_RADIUS: u32 = 8;
pub const MAX_BLKSIZE: u32 = 32;
impl MotionCompensationMode {
pub fn mvtools_default() -> Self {
Self::Mvtools {
blksize: DEFAULT_BLKSIZE,
overlap: DEFAULT_OVERLAP,
search_radius: DEFAULT_SEARCH_RADIUS,
pyramid_levels: DEFAULT_PYRAMID_LEVELS,
estimation: MotionEstimation::Direct,
}
}
pub(crate) fn is_active(self) -> bool {
!matches!(self, Self::None)
}
pub(crate) fn resolved_estimation(&self, temporal_radius: u32) -> Option<MotionEstimation> {
match *self {
Self::Mvtools { estimation, .. } => Some(estimation.resolve(temporal_radius)),
Self::None => None,
}
}
pub fn validate(&self) -> Result<(), anyhow::Error> {
let Self::Mvtools {
blksize,
overlap,
search_radius,
pyramid_levels,
estimation,
} = *self
else {
return Ok(());
};
if blksize < 4 {
anyhow::bail!("motion-compensation blksize={blksize} is too small; minimum is 4 pixels per side");
}
if blksize > MAX_BLKSIZE {
anyhow::bail!(
"motion-compensation blksize={blksize} exceeds the supported maximum ({MAX_BLKSIZE})"
);
}
if blksize % 2 != 0 {
anyhow::bail!(
"motion-compensation blksize={blksize} must be even so the /2 coarse level is well-defined"
);
}
if overlap >= blksize {
anyhow::bail!(
"motion-compensation overlap={overlap} must be strictly less than blksize ({blksize}) so step > 0"
);
}
if search_radius == 0 || search_radius > MAX_SEARCH_RADIUS {
anyhow::bail!(
"motion-compensation search_radius={search_radius} must be in 1..={MAX_SEARCH_RADIUS}"
);
}
if pyramid_levels == 0 || pyramid_levels > MAX_PYRAMID_LEVELS {
anyhow::bail!(
"motion-compensation pyramid_levels={pyramid_levels} must be in 1..={MAX_PYRAMID_LEVELS}"
);
}
estimation.validate()?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub(crate) struct MotionCtx {
pub blksize: u32,
pub step: u32,
pub search_radius: u32,
pub pyramid_levels: u32,
pub blocks_x: u32,
pub blocks_y: u32,
pub align: StorageAlign,
}
impl MotionCtx {
pub fn new(mode: MotionCompensationMode, width: u32, height: u32, align: StorageAlign) -> Option<Self> {
let MotionCompensationMode::Mvtools {
blksize,
overlap,
search_radius,
pyramid_levels,
estimation: _,
} = mode
else {
return None;
};
let step = blksize - overlap;
let blocks_x = width.div_ceil(step).max(1);
let blocks_y = height.div_ceil(step).max(1);
Some(Self {
blksize,
step,
search_radius,
pyramid_levels,
blocks_x,
blocks_y,
align,
})
}
pub fn mv_slots_per_neighbour(&self) -> usize {
(self.blocks_x * self.blocks_y) as usize
}
pub(crate) fn mv_field_bytes_per_neighbour(&self) -> u64 {
let blocks = (self.blocks_x as u64) * (self.blocks_y as u64);
self.align.pad_bytes(blocks * 2 * size_of::<i32>() as u64)
}
pub(crate) fn confidence_bytes_per_neighbour(&self) -> u64 {
let blocks = (self.blocks_x as u64) * (self.blocks_y as u64);
self.align.pad_bytes(blocks * size_of::<f32>() as u64)
}
pub(crate) fn pair_direction_len(&self) -> u32 {
self.blocks_x * self.blocks_y * 2
}
pub(crate) fn pair_direction_bytes(&self) -> u64 {
self.align
.pad_bytes(self.pair_direction_len() as u64 * size_of::<i32>() as u64)
}
pub(crate) fn pair_slot_bytes(&self) -> u64 {
2 * self.pair_direction_bytes()
}
pub(crate) fn pair_direction_stride(&self) -> u32 {
(self.pair_direction_bytes() / size_of::<i32>() as u64) as u32
}
pub(crate) fn pair_slot_stride(&self) -> u32 {
2 * self.pair_direction_stride()
}
pub(crate) fn confidence_only(width: u32, height: u32, align: StorageAlign) -> Self {
Self::new(
MotionCompensationMode::Mvtools {
blksize: DEFAULT_BLKSIZE,
overlap: DEFAULT_OVERLAP,
search_radius: 0,
pyramid_levels: 1,
estimation: MotionEstimation::Direct,
},
width,
height,
align,
)
.expect("Mvtools variant always yields Some")
}
}
pub(crate) fn pair_ring_slot_count(temporal_radius: u32) -> u32 {
2 * temporal_radius
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_pyramid_for_slot<R: Runtime>(
client: &ComputeClient<R>,
mc: &MotionCtx,
width: u32,
height: u32,
frame_count: u32,
slot: u32,
full_res: &Handle,
pyramid: &Handle,
stored_ch: u32,
) -> Result<(), anyhow::Error> {
run_pyramid_build::<R>(
client,
mc,
width,
height,
frame_count,
slot,
full_res,
pyramid,
stored_ch,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_is_inactive() {
let m = MotionCompensationMode::None;
assert!(!m.is_active());
m.validate().unwrap();
}
#[test]
fn mvtools_default_is_active() {
let m = MotionCompensationMode::mvtools_default();
assert!(m.is_active());
m.validate().unwrap();
}
#[test]
fn validate_rejects_tiny_blksize() {
let m = MotionCompensationMode::Mvtools {
blksize: 2,
overlap: 0,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
assert!(m.validate().is_err());
}
#[test]
fn validate_rejects_odd_blksize() {
let m = MotionCompensationMode::Mvtools {
blksize: 9,
overlap: 0,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
assert!(m.validate().is_err());
}
#[test]
fn validate_rejects_overlap_equal_to_blksize() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 16,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
assert!(m.validate().is_err());
}
#[test]
fn validate_accepts_half_overlap() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
m.validate().unwrap();
}
#[test]
fn validate_rejects_zero_search_radius() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 4,
search_radius: 0,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
assert!(m.validate().is_err());
}
#[test]
fn validate_rejects_zero_pyramid_levels() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 4,
search_radius: 4,
pyramid_levels: 0,
estimation: MotionEstimation::Direct,
};
assert!(m.validate().is_err());
}
#[test]
fn chained_default_is_valid() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::chained_default(),
};
m.validate().unwrap();
assert_eq!(
m,
MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Chained {
refine_radius: DEFAULT_REFINE_RADIUS
},
}
);
}
#[test]
fn validate_rejects_zero_refine_radius() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Chained { refine_radius: 0 },
};
assert!(m.validate().is_err());
}
#[test]
fn validate_rejects_refine_radius_above_max() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Chained {
refine_radius: MAX_SEARCH_RADIUS + 1,
},
};
assert!(m.validate().is_err());
}
#[test]
fn validate_accepts_refine_radius_at_max() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Chained {
refine_radius: MAX_SEARCH_RADIUS,
},
};
m.validate().unwrap();
}
#[test]
fn motion_estimation_default_is_auto() {
assert_eq!(MotionEstimation::default(), MotionEstimation::Auto);
}
#[test]
fn resolve_auto_below_threshold_gives_direct() {
assert_eq!(MotionEstimation::Auto.resolve(1), MotionEstimation::Direct);
assert_eq!(MotionEstimation::Auto.resolve(2), MotionEstimation::Direct);
}
#[test]
fn resolve_auto_at_and_above_threshold_gives_chained_default() {
assert_eq!(
MotionEstimation::Auto.resolve(CHAINED_RADIUS_THRESHOLD),
MotionEstimation::chained_default()
);
assert_eq!(
MotionEstimation::Auto.resolve(8),
MotionEstimation::chained_default()
);
}
#[test]
fn resolve_leaves_explicit_direct_unchanged_at_every_radius() {
for radius in 1..=8u32 {
assert_eq!(MotionEstimation::Direct.resolve(radius), MotionEstimation::Direct);
}
}
#[test]
fn resolve_leaves_explicit_chained_unchanged_at_every_radius() {
let chained = MotionEstimation::Chained { refine_radius: 5 };
for radius in 1..=8u32 {
assert_eq!(chained.resolve(radius), chained);
}
}
#[test]
fn validate_accepts_auto() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
};
m.validate().unwrap();
}
#[test]
fn resolved_estimation_is_none_when_mode_is_none() {
assert_eq!(MotionCompensationMode::None.resolved_estimation(4), None);
}
#[test]
fn resolved_estimation_resolves_auto_from_the_mode() {
let m = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
};
assert_eq!(m.resolved_estimation(1), Some(MotionEstimation::Direct));
assert_eq!(
m.resolved_estimation(4),
Some(MotionEstimation::chained_default())
);
}
#[test]
fn pair_ring_slot_count_is_double_radius() {
assert_eq!(pair_ring_slot_count(3), 6);
assert_eq!(pair_ring_slot_count(1), 2);
}
#[test]
fn motion_ctx_blocks_match_step() {
let mode = MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Direct,
};
let ctx = MotionCtx::new(mode, 1920, 1080, StorageAlign::new(32)).unwrap();
assert_eq!(ctx.step, 8);
assert_eq!(ctx.blocks_x, 1920u32.div_ceil(8));
assert_eq!(ctx.blocks_y, 1080u32.div_ceil(8));
}
}