#![forbid(unsafe_code)]
#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(clippy::unused_self)]
#![allow(clippy::items_after_statements)]
#![allow(unused_assignments)]
use super::types::{BlockMatch, BlockSize, MotionVector, MvCost, MvPrecision, SearchRange};
pub const EARLY_TERMINATION_SAD: u32 = 256;
pub const EARLY_TERMINATION_RATIO: f32 = 0.9;
#[derive(Clone, Debug)]
pub struct SearchConfig {
pub range: SearchRange,
pub precision: MvPrecision,
pub early_termination: bool,
pub early_threshold: u32,
pub mv_cost: MvCost,
pub max_iterations: u32,
pub subpel_refine: bool,
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
range: SearchRange::default(),
precision: MvPrecision::QuarterPel,
early_termination: true,
early_threshold: EARLY_TERMINATION_SAD,
mv_cost: MvCost::default(),
max_iterations: 16,
subpel_refine: true,
}
}
}
impl SearchConfig {
#[must_use]
pub fn with_range(range: SearchRange) -> Self {
Self {
range,
..Default::default()
}
}
#[must_use]
pub const fn range(mut self, range: SearchRange) -> Self {
self.range = range;
self
}
#[must_use]
pub const fn precision(mut self, precision: MvPrecision) -> Self {
self.precision = precision;
self
}
#[must_use]
pub const fn early_termination(mut self, enable: bool) -> Self {
self.early_termination = enable;
self
}
#[must_use]
pub fn ref_mv(mut self, mv: MotionVector) -> Self {
self.mv_cost.set_ref_mv(mv);
self
}
#[must_use]
pub fn lambda(mut self, lambda: f32) -> Self {
self.mv_cost.lambda = lambda;
self
}
}
pub struct SearchContext<'a> {
pub src: &'a [u8],
pub src_stride: usize,
pub ref_frame: &'a [u8],
pub ref_stride: usize,
pub block_size: BlockSize,
pub block_x: usize,
pub block_y: usize,
pub ref_width: usize,
pub ref_height: usize,
}
impl<'a> SearchContext<'a> {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn new(
src: &'a [u8],
src_stride: usize,
ref_frame: &'a [u8],
ref_stride: usize,
block_size: BlockSize,
block_x: usize,
block_y: usize,
ref_width: usize,
ref_height: usize,
) -> Self {
Self {
src,
src_stride,
ref_frame,
ref_stride,
block_size,
block_x,
block_y,
ref_width,
ref_height,
}
}
#[must_use]
pub const fn src_offset(&self) -> usize {
self.block_y * self.src_stride + self.block_x
}
#[must_use]
#[allow(clippy::cast_sign_loss)]
pub fn calculate_sad(&self, mv: &MotionVector) -> Option<u32> {
let ref_x = self.block_x as i32 + mv.full_pel_x();
let ref_y = self.block_y as i32 + mv.full_pel_y();
if ref_x < 0 || ref_y < 0 {
return None;
}
let ref_x = ref_x as usize;
let ref_y = ref_y as usize;
let width = self.block_size.width();
let height = self.block_size.height();
if ref_x + width > self.ref_width || ref_y + height > self.ref_height {
return None;
}
let src_offset = self.src_offset();
let ref_offset = ref_y * self.ref_stride + ref_x;
if src_offset + (height - 1) * self.src_stride + width > self.src.len() {
return None;
}
if ref_offset + (height - 1) * self.ref_stride + width > self.ref_frame.len() {
return None;
}
let mut sad = 0u32;
for row in 0..height {
let src_row_offset = src_offset + row * self.src_stride;
let ref_row_offset = ref_offset + row * self.ref_stride;
for col in 0..width {
let src_val = self.src[src_row_offset + col];
let ref_val = self.ref_frame[ref_row_offset + col];
let diff = i32::from(src_val) - i32::from(ref_val);
sad += diff.unsigned_abs();
}
}
Some(sad)
}
#[must_use]
pub fn is_valid_mv(&self, mv: &MotionVector, range: &SearchRange) -> bool {
let ref_x = self.block_x as i32 + mv.full_pel_x();
let ref_y = self.block_y as i32 + mv.full_pel_y();
let width = self.block_size.width() as i32;
let height = self.block_size.height() as i32;
ref_x >= 0
&& ref_y >= 0
&& ref_x + width <= self.ref_width as i32
&& ref_y + height <= self.ref_height as i32
&& range.contains(mv.full_pel_x(), mv.full_pel_y())
}
}
pub trait MotionSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch;
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch;
}
#[derive(Clone, Debug, Default)]
pub struct FullSearch;
impl FullSearch {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl MotionSearch for FullSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
_predictor: MotionVector,
) -> BlockMatch {
let mut best = BlockMatch::worst();
let range = &config.range;
for dy in -range.vertical..=range.vertical {
for dx in -range.horizontal..=range.horizontal {
let mv = MotionVector::from_full_pel(dx, dy);
if let Some(sad) = ctx.calculate_sad(&mv) {
let cost = config.mv_cost.rd_cost(&mv, sad);
let candidate = BlockMatch::new(mv, sad, cost);
best.update_if_better(&candidate);
if config.early_termination && sad < config.early_threshold {
return best;
}
}
}
}
best
}
}
#[derive(Clone, Debug, Default)]
pub struct DiamondSearch {
use_large_diamond: bool,
}
impl DiamondSearch {
const SDSP: [(i32, i32); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
const LDSP: [(i32, i32); 8] = [
(0, -2),
(0, 2),
(-2, 0),
(2, 0),
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
];
#[must_use]
pub const fn new() -> Self {
Self {
use_large_diamond: true,
}
}
#[must_use]
pub const fn use_large_diamond(mut self, enable: bool) -> Self {
self.use_large_diamond = enable;
self
}
fn diamond_step(
&self,
ctx: &SearchContext,
config: &SearchConfig,
center: MotionVector,
pattern: &[(i32, i32)],
) -> (MotionVector, u32) {
let mut best_mv = center;
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
for &(dx, dy) in pattern {
let mv =
MotionVector::from_full_pel(center.full_pel_x() + dx, center.full_pel_y() + dy);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
(best_mv, best_sad)
}
}
impl MotionSearch for DiamondSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch {
let mut center = predictor.to_precision(MvPrecision::FullPel);
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
if self.use_large_diamond {
for _ in 0..config.max_iterations {
let (new_center, new_sad) = self.diamond_step(ctx, config, center, &Self::LDSP);
if new_center == center {
break;
}
center = new_center;
best_sad = new_sad;
if config.early_termination && best_sad < config.early_threshold {
let cost = config.mv_cost.rd_cost(¢er, best_sad);
return BlockMatch::new(center, best_sad, cost);
}
}
}
loop {
let (new_center, new_sad) = self.diamond_step(ctx, config, center, &Self::SDSP);
if new_center == center {
break;
}
center = new_center;
best_sad = new_sad;
}
let cost = config.mv_cost.rd_cost(¢er, best_sad);
BlockMatch::new(center, best_sad, cost)
}
}
#[derive(Clone, Debug, Default)]
pub struct HexagonSearch;
impl HexagonSearch {
const HEXAGON: [(i32, i32); 6] = [(-2, 0), (-1, -2), (1, -2), (2, 0), (1, 2), (-1, 2)];
const SQUARE: [(i32, i32); 4] = [(-1, -1), (-1, 1), (1, -1), (1, 1)];
#[must_use]
pub const fn new() -> Self {
Self
}
fn hexagon_step(
&self,
ctx: &SearchContext,
config: &SearchConfig,
center: MotionVector,
) -> (MotionVector, u32) {
let mut best_mv = center;
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
for &(dx, dy) in &Self::HEXAGON {
let mv =
MotionVector::from_full_pel(center.full_pel_x() + dx, center.full_pel_y() + dy);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
(best_mv, best_sad)
}
fn square_refine(
&self,
ctx: &SearchContext,
config: &SearchConfig,
center: MotionVector,
) -> (MotionVector, u32) {
let mut best_mv = center;
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
for &(dx, dy) in &Self::SQUARE {
let mv =
MotionVector::from_full_pel(center.full_pel_x() + dx, center.full_pel_y() + dy);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
(best_mv, best_sad)
}
}
impl MotionSearch for HexagonSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch {
let mut center = predictor.to_precision(MvPrecision::FullPel);
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
for _ in 0..config.max_iterations {
let (new_center, new_sad) = self.hexagon_step(ctx, config, center);
if new_center == center {
break;
}
center = new_center;
best_sad = new_sad;
if config.early_termination && best_sad < config.early_threshold {
let cost = config.mv_cost.rd_cost(¢er, best_sad);
return BlockMatch::new(center, best_sad, cost);
}
}
let (final_center, final_sad) = self.square_refine(ctx, config, center);
let cost = config.mv_cost.rd_cost(&final_center, final_sad);
BlockMatch::new(final_center, final_sad, cost)
}
}
#[derive(Clone, Debug, Default)]
pub struct UmhSearch {
cross_range: i32,
}
impl UmhSearch {
#[must_use]
pub const fn new() -> Self {
Self { cross_range: 2 }
}
#[must_use]
pub const fn cross_range(mut self, range: i32) -> Self {
self.cross_range = range;
self
}
fn cross_search(
&self,
ctx: &SearchContext,
config: &SearchConfig,
center: MotionVector,
) -> (MotionVector, u32) {
let mut best_mv = center;
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
let range = config.range.horizontal.min(config.range.vertical) * self.cross_range;
for dx in (-range..=range).step_by(2) {
if dx == 0 {
continue;
}
let mv = MotionVector::from_full_pel(center.full_pel_x() + dx, center.full_pel_y());
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
for dy in (-range / 2..=range / 2).step_by(2) {
if dy == 0 {
continue;
}
let mv = MotionVector::from_full_pel(center.full_pel_x(), center.full_pel_y() + dy);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
(best_mv, best_sad)
}
fn multi_hexagon(
&self,
ctx: &SearchContext,
config: &SearchConfig,
center: MotionVector,
) -> (MotionVector, u32) {
let mut best_mv = center;
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
const MULTI_HEX: [(i32, i32); 16] = [
(-4, 0),
(-2, -2),
(0, -4),
(2, -2),
(4, 0),
(2, 2),
(0, 4),
(-2, 2),
(-2, -4),
(2, -4),
(4, -2),
(4, 2),
(2, 4),
(-2, 4),
(-4, 2),
(-4, -2),
];
for &(dx, dy) in &MULTI_HEX {
let mv =
MotionVector::from_full_pel(center.full_pel_x() + dx, center.full_pel_y() + dy);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
best_mv = mv;
}
}
}
(best_mv, best_sad)
}
}
impl MotionSearch for UmhSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch {
let mut center = predictor.to_precision(MvPrecision::FullPel);
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
let (cross_mv, cross_sad) = self.cross_search(ctx, config, center);
if cross_sad < best_sad {
center = cross_mv;
best_sad = cross_sad;
}
if config.early_termination && best_sad < config.early_threshold {
let cost = config.mv_cost.rd_cost(¢er, best_sad);
return BlockMatch::new(center, best_sad, cost);
}
let (hex_mv, hex_sad) = self.multi_hexagon(ctx, config, center);
if hex_sad < best_sad {
center = hex_mv;
best_sad = hex_sad;
}
let hex_search = HexagonSearch::new();
for _ in 0..config.max_iterations / 2 {
let (new_center, new_sad) = hex_search.hexagon_step(ctx, config, center);
if new_center == center {
break;
}
center = new_center;
best_sad = new_sad;
}
let diamond = DiamondSearch::new().use_large_diamond(false);
let result = diamond.search_with_predictor(ctx, config, center);
if result.sad < best_sad {
result
} else {
let cost = config.mv_cost.rd_cost(¢er, best_sad);
BlockMatch::new(center, best_sad, cost)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ThreeStepSearch;
impl ThreeStepSearch {
#[must_use]
pub const fn new() -> Self {
Self
}
const SQUARE_8: [(i32, i32); 8] = [
(-1, -1),
(0, -1),
(1, -1),
(-1, 0),
(1, 0),
(-1, 1),
(0, 1),
(1, 1),
];
}
impl MotionSearch for ThreeStepSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch {
let mut center = predictor.to_precision(MvPrecision::FullPel);
let mut best_sad = ctx.calculate_sad(¢er).unwrap_or(u32::MAX);
let initial_step = config.range.horizontal.min(config.range.vertical) / 2;
let mut step = initial_step.max(4);
while step >= 1 {
let mut moved = false;
for &(dx, dy) in &Self::SQUARE_8 {
let mv = MotionVector::from_full_pel(
center.full_pel_x() + dx * step,
center.full_pel_y() + dy * step,
);
if !ctx.is_valid_mv(&mv, &config.range) {
continue;
}
if let Some(sad) = ctx.calculate_sad(&mv) {
if sad < best_sad {
best_sad = sad;
center = mv;
moved = true;
}
}
}
if !moved {
step /= 2;
}
if config.early_termination && best_sad < config.early_threshold {
break;
}
}
let cost = config.mv_cost.rd_cost(¢er, best_sad);
BlockMatch::new(center, best_sad, cost)
}
}
#[derive(Clone, Debug)]
pub struct AdaptiveSearch {
complexity_threshold: u32,
}
impl Default for AdaptiveSearch {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveSearch {
#[must_use]
pub const fn new() -> Self {
Self {
complexity_threshold: 1000,
}
}
#[must_use]
pub const fn threshold(mut self, threshold: u32) -> Self {
self.complexity_threshold = threshold;
self
}
fn estimate_complexity(&self, ctx: &SearchContext) -> u32 {
let src_offset = ctx.src_offset();
let width = ctx.block_size.width();
let height = ctx.block_size.height();
let mut sum = 0u32;
let mut sum_sq = 0u64;
let mut count = 0u32;
for row in 0..height {
let row_offset = src_offset + row * ctx.src_stride;
for col in 0..width {
if row_offset + col < ctx.src.len() {
let val = u32::from(ctx.src[row_offset + col]);
sum += val;
sum_sq += u64::from(val * val);
count += 1;
}
}
}
if count == 0 {
return 0;
}
let mean = sum / count;
(sum_sq / u64::from(count)) as u32 - mean * mean
}
}
impl MotionSearch for AdaptiveSearch {
fn search(&self, ctx: &SearchContext, config: &SearchConfig) -> BlockMatch {
self.search_with_predictor(ctx, config, MotionVector::zero())
}
fn search_with_predictor(
&self,
ctx: &SearchContext,
config: &SearchConfig,
predictor: MotionVector,
) -> BlockMatch {
let complexity = self.estimate_complexity(ctx);
if complexity < self.complexity_threshold {
DiamondSearch::new().search_with_predictor(ctx, config, predictor)
} else {
UmhSearch::new().search_with_predictor(ctx, config, predictor)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_context<'a>(
src: &'a [u8],
ref_frame: &'a [u8],
width: usize,
height: usize,
) -> SearchContext<'a> {
SearchContext::new(
src,
width,
ref_frame,
width,
BlockSize::Block8x8,
0,
0,
width,
height,
)
}
#[test]
fn test_search_config_default() {
let config = SearchConfig::default();
assert_eq!(config.precision, MvPrecision::QuarterPel);
assert!(config.early_termination);
}
#[test]
fn test_search_config_builder() {
let config = SearchConfig::default()
.range(SearchRange::symmetric(32))
.precision(MvPrecision::HalfPel)
.early_termination(false);
assert_eq!(config.range.horizontal, 32);
assert_eq!(config.precision, MvPrecision::HalfPel);
assert!(!config.early_termination);
}
#[test]
fn test_search_context_sad_calculation() {
let src = vec![100u8; 64]; let ref_frame = vec![110u8; 64];
let ctx = create_test_context(&src, &ref_frame, 8, 8);
let mv = MotionVector::zero();
let sad = ctx.calculate_sad(&mv);
assert!(sad.is_some());
assert_eq!(sad.expect("should succeed"), 640); }
#[test]
fn test_search_context_identical_blocks() {
let data = vec![128u8; 64];
let ctx = create_test_context(&data, &data, 8, 8);
let mv = MotionVector::zero();
let sad = ctx.calculate_sad(&mv);
assert_eq!(sad, Some(0));
}
#[test]
fn test_full_search() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = FullSearch::new();
let result = searcher.search(&ctx, &config);
assert_eq!(result.mv.full_pel_x(), 4);
assert_eq!(result.mv.full_pel_y(), 4);
assert_eq!(result.sad, 0);
}
#[test]
fn test_diamond_search() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = DiamondSearch::new();
let result = searcher.search(&ctx, &config);
assert!(result.sad < 1000);
}
#[test]
fn test_hexagon_search() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = HexagonSearch::new();
let result = searcher.search(&ctx, &config);
assert!(result.sad < 1000);
}
#[test]
fn test_umh_search() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = UmhSearch::new();
let result = searcher.search(&ctx, &config);
assert!(result.sad < 1000);
}
#[test]
fn test_three_step_search() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = ThreeStepSearch::new();
let result = searcher.search(&ctx, &config);
assert!(result.sad < 1000);
}
#[test]
fn test_search_with_predictor() {
let src = vec![100u8; 64];
let mut ref_frame = vec![50u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[(row + 4) * 16 + col + 4] = 100;
}
}
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let predictor = MotionVector::from_full_pel(3, 3);
let searcher = DiamondSearch::new();
let result = searcher.search_with_predictor(&ctx, &config, predictor);
assert!(result.sad < 500);
}
#[test]
fn test_early_termination() {
let data = vec![128u8; 64];
let mut ref_frame = vec![128u8; 256];
for row in 0..8 {
for col in 0..8 {
ref_frame[row * 16 + col] = 128;
}
}
let ctx = SearchContext::new(&data, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::default().early_termination(true);
let searcher = FullSearch::new();
let result = searcher.search(&ctx, &config);
assert_eq!(result.sad, 0);
assert_eq!(result.mv.full_pel_x(), 0);
assert_eq!(result.mv.full_pel_y(), 0);
}
#[test]
fn test_adaptive_search() {
let src = vec![100u8; 64];
let ref_frame = vec![100u8; 256];
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let config = SearchConfig::with_range(SearchRange::symmetric(8));
let searcher = AdaptiveSearch::new();
let result = searcher.search(&ctx, &config);
assert!(result.cost < u32::MAX);
}
#[test]
fn test_out_of_bounds_mv() {
let src = vec![100u8; 64];
let ref_frame = vec![100u8; 64];
let ctx = SearchContext::new(&src, 8, &ref_frame, 8, BlockSize::Block8x8, 0, 0, 8, 8);
let mv = MotionVector::from_full_pel(5, 5);
let sad = ctx.calculate_sad(&mv);
assert!(sad.is_none());
}
#[test]
fn test_is_valid_mv() {
let src = vec![100u8; 64];
let ref_frame = vec![100u8; 256];
let ctx = SearchContext::new(&src, 8, &ref_frame, 16, BlockSize::Block8x8, 0, 0, 16, 16);
let range = SearchRange::symmetric(8);
assert!(ctx.is_valid_mv(&MotionVector::from_full_pel(4, 4), &range));
assert!(!ctx.is_valid_mv(&MotionVector::from_full_pel(10, 0), &range));
assert!(!ctx.is_valid_mv(&MotionVector::from_full_pel(9, 9), &range));
}
}