use super::intra_predictor::hadamard_ac_sum_16x16;
use super::motion_estimation::{MotionEstimator, MotionVector};
use super::partition_state::{predict_mv_for_partition, EncoderMvGrid};
use super::reference_buffer::ReconFrame;
fn multi_pred_enabled() -> bool {
std::env::var("PHASM_ME_MULTI_PRED")
.ok()
.is_none_or(|v| v != "0")
}
fn build_me_candidates(
grid: &EncoderMvGrid,
tl_bx: usize,
tl_by: usize,
part_w_4x4: usize,
predictor: MotionVector,
) -> Vec<MotionVector> {
if !multi_pred_enabled() {
return vec![predictor];
}
let x = tl_bx as isize;
let y = tl_by as isize;
let mut cands = Vec::with_capacity(6);
cands.push(predictor);
cands.push(MotionVector::ZERO);
if let Some((mv, _)) = grid.get(x - 1, y) {
cands.push(mv);
}
if let Some((mv, _)) = grid.get(x, y - 1) {
cands.push(mv);
}
if let Some((mv, _)) = grid.get(x + part_w_4x4 as isize, y - 1) {
cands.push(mv);
}
cands
}
fn inter_psy_strength() -> u32 {
std::env::var("PHASM_INTER_PSY_STRENGTH")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub const PENALTY_16X8: u32 = 64;
pub const PENALTY_8X16: u32 = 64;
pub const PENALTY_8X8: u32 = 256;
#[derive(Debug, Clone, Copy)]
pub enum PMbChoice {
P16x16 { mv: MotionVector },
P16x8 { mvs: [MotionVector; 2] },
P8x16 { mvs: [MotionVector; 2] },
P8x8 { sub: [SubMbChoice; 4] },
}
impl PMbChoice {
pub fn mb_type_codenum(self) -> u32 {
match self {
PMbChoice::P16x16 { .. } => 0,
PMbChoice::P16x8 { .. } => 1,
PMbChoice::P8x16 { .. } => 2,
PMbChoice::P8x8 { .. } => 3,
}
}
pub fn no_sub_mb_part_size_lt_8x8(&self) -> bool {
match self {
PMbChoice::P16x16 { .. } | PMbChoice::P16x8 { .. } | PMbChoice::P8x16 { .. } => true,
PMbChoice::P8x8 { sub } => sub.iter().all(|s| matches!(s, SubMbChoice::P8x8 { .. })),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum SubMbChoice {
P8x8 { mv: MotionVector },
P8x4 { mvs: [MotionVector; 2] },
P4x8 { mvs: [MotionVector; 2] },
P4x4 { mvs: [MotionVector; 4] },
}
impl SubMbChoice {
pub fn sub_mb_type_codenum(self) -> u32 {
match self {
SubMbChoice::P8x8 { .. } => 0,
SubMbChoice::P8x4 { .. } => 1,
SubMbChoice::P4x8 { .. } => 2,
SubMbChoice::P4x4 { .. } => 3,
}
}
}
pub const SUB_PENALTY_P8X4: u32 = 32;
pub const SUB_PENALTY_P4X8: u32 = 32;
pub const SUB_PENALTY_P4X4: u32 = 96;
pub const SUB_MB_ORIGINS_4X4: [(usize, usize); 4] = [(0, 0), (2, 0), (0, 2), (2, 2)];
pub const SUB_MB_ORIGINS_PX: [(u32, u32); 4] = [(0, 0), (8, 0), (0, 8), (8, 8)];
pub fn decide_p_mb(
src_y: &[[u8; 16]; 16],
reference: &ReconFrame,
me: &mut MotionEstimator,
grid: &mut EncoderMvGrid,
mb_x: usize,
mb_y: usize,
) -> PMbChoice {
decide_p_mb_with_cost(src_y, reference, me, grid, mb_x, mb_y).best
}
#[derive(Debug, Clone, Copy)]
pub struct PMbDecision {
pub best: PMbChoice,
pub best_cost: u32,
pub candidates: [PMbChoice; 4],
pub satd_costs: [u32; 4],
}
pub const SATD_CANDIDATE_ORDER: [&str; 4] = ["P16x16", "P16x8", "P8x16", "P8x8"];
pub fn decide_p_mb_with_cost(
src_y: &[[u8; 16]; 16],
reference: &ReconFrame,
me: &mut MotionEstimator,
grid: &mut EncoderMvGrid,
mb_x: usize,
mb_y: usize,
) -> PMbDecision {
let mb_px_x = (mb_x * 16) as u32;
let mb_px_y = (mb_y * 16) as u32;
let src_flat = src_y.as_flattened();
let pred_16x16 = predict_mv_for_partition(grid, mb_x * 4, mb_y * 4, 4, 0);
let cand_16x16 = build_me_candidates(grid, mb_x * 4, mb_y * 4, 4, pred_16x16);
let r16 = me.search_block_with_candidates(
src_flat, 16, reference, mb_px_x, mb_px_y, 16, 16, pred_16x16, &cand_16x16,
);
let cost_16x16 = r16.cost;
let src_top = extract_half(src_y, 0, 0, 16, 8);
let src_bot = extract_half(src_y, 0, 8, 16, 8);
let pred_top = predict_mv_for_partition(grid, mb_x * 4, mb_y * 4, 4, 0);
let cand_top = build_me_candidates(grid, mb_x * 4, mb_y * 4, 4, pred_top);
let r_top = me.search_block_with_candidates(
&src_top, 16, reference, mb_px_x, mb_px_y, 16, 8, pred_top, &cand_top,
);
let pred_bot = r_top.mv;
let cand_bot = build_me_candidates(grid, mb_x * 4, mb_y * 4 + 2, 4, pred_bot);
let r_bot = me.search_block_with_candidates(
&src_bot, 16, reference, mb_px_x, mb_px_y + 8, 16, 8, pred_bot, &cand_bot,
);
let cost_16x8 = r_top.cost.saturating_add(r_bot.cost).saturating_add(PENALTY_16X8);
let src_left = extract_half(src_y, 0, 0, 8, 16);
let src_right = extract_half(src_y, 8, 0, 8, 16);
let pred_left = predict_mv_for_partition(grid, mb_x * 4, mb_y * 4, 2, 0);
let cand_left = build_me_candidates(grid, mb_x * 4, mb_y * 4, 2, pred_left);
let r_left = me.search_block_with_candidates(
&src_left, 8, reference, mb_px_x, mb_px_y, 8, 16, pred_left, &cand_left,
);
let pred_right = r_left.mv;
let cand_right = build_me_candidates(grid, mb_x * 4 + 2, mb_y * 4, 2, pred_right);
let r_right = me.search_block_with_candidates(
&src_right, 8, reference, mb_px_x + 8, mb_px_y, 8, 16, pred_right, &cand_right,
);
let cost_8x16 = r_left.cost.saturating_add(r_right.cost).saturating_add(PENALTY_8X16);
let mb_mv_snap = grid.snapshot_mb(mb_x, mb_y);
let mut sub = [SubMbChoice::P8x8 { mv: MotionVector::ZERO }; 4];
let mut cost_8x8 = 0u32;
for (i, &(off_x_px, off_y_px)) in SUB_MB_ORIGINS_PX.iter().enumerate() {
let (dx_4x4, dy_4x4) = SUB_MB_ORIGINS_4X4[i];
let sub_bx = mb_x * 4 + dx_4x4;
let sub_by = mb_y * 4 + dy_4x4;
let (sub_choice, sub_cost) = decide_sub_mb(
src_y, reference, me, grid, mb_px_x, mb_px_y, off_x_px, off_y_px, sub_bx, sub_by,
);
commit_sub_mb_to_grid(grid, sub_bx, sub_by, &sub_choice);
sub[i] = sub_choice;
cost_8x8 = cost_8x8.saturating_add(sub_cost);
}
cost_8x8 = cost_8x8.saturating_add(PENALTY_8X8);
grid.restore_mb(&mb_mv_snap);
let candidates = [
PMbChoice::P16x16 { mv: r16.mv },
PMbChoice::P16x8 { mvs: [r_top.mv, r_bot.mv] },
PMbChoice::P8x16 { mvs: [r_left.mv, r_right.mv] },
PMbChoice::P8x8 { sub },
];
let mut satd_costs = [cost_16x16, cost_16x8, cost_8x16, cost_8x8];
let psy = inter_psy_strength();
if psy != 0 {
let src_ac = hadamard_ac_sum_16x16(src_y);
for i in 0..4 {
let pred_y = super::encoder::build_luma_prediction(
reference, mb_x, mb_y, &candidates[i],
);
let pred_ac = hadamard_ac_sum_16x16(&pred_y);
let ac_diff = (src_ac as i64 - pred_ac as i64).unsigned_abs() as u32;
let bias = ((ac_diff as u64 * psy as u64) / 256) as u32;
satd_costs[i] = satd_costs[i].saturating_add(bias);
}
}
let mut best_idx = 0usize;
for i in 1..4 {
if satd_costs[i] < satd_costs[best_idx] {
best_idx = i;
}
}
PMbDecision {
best: candidates[best_idx],
best_cost: satd_costs[best_idx],
candidates,
satd_costs,
}
}
fn commit_sub_mb_to_grid(
grid: &mut EncoderMvGrid,
sub_bx: usize,
sub_by: usize,
choice: &SubMbChoice,
) {
match choice {
SubMbChoice::P8x8 { mv } => grid.fill(sub_bx, sub_by, 2, 2, *mv, 0),
SubMbChoice::P8x4 { mvs } => {
grid.fill(sub_bx, sub_by, 2, 1, mvs[0], 0);
grid.fill(sub_bx, sub_by + 1, 2, 1, mvs[1], 0);
}
SubMbChoice::P4x8 { mvs } => {
grid.fill(sub_bx, sub_by, 1, 2, mvs[0], 0);
grid.fill(sub_bx + 1, sub_by, 1, 2, mvs[1], 0);
}
SubMbChoice::P4x4 { mvs } => {
grid.fill(sub_bx, sub_by, 1, 1, mvs[0], 0);
grid.fill(sub_bx + 1, sub_by, 1, 1, mvs[1], 0);
grid.fill(sub_bx, sub_by + 1, 1, 1, mvs[2], 0);
grid.fill(sub_bx + 1, sub_by + 1, 1, 1, mvs[3], 0);
}
}
}
fn extract_half(
src: &[[u8; 16]; 16],
off_x: usize,
off_y: usize,
w: usize,
h: usize,
) -> Vec<u8> {
let mut out = vec![0u8; w * h];
for dy in 0..h {
for dx in 0..w {
out[dy * w + dx] = src[off_y + dy][off_x + dx];
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn decide_sub_mb(
src_y: &[[u8; 16]; 16],
reference: &ReconFrame,
me: &mut MotionEstimator,
grid: &EncoderMvGrid,
mb_px_x: u32,
mb_px_y: u32,
off_x_px: u32,
off_y_px: u32,
sub_bx: usize,
sub_by: usize,
) -> (SubMbChoice, u32) {
let sub_px_x = mb_px_x + off_x_px;
let sub_px_y = mb_px_y + off_y_px;
let off_x = off_x_px as usize;
let off_y = off_y_px as usize;
let pred_sub_8x8 = if std::env::var("PHASM_SUBMB_MEDIAN_PRED").ok().as_deref() == Some("1") {
predict_mv_for_partition(grid, sub_bx, sub_by, 2, 0)
} else {
MotionVector::ZERO
};
let src_8x8 = extract_half(src_y, off_x, off_y, 8, 8);
let cand_8x8 = build_me_candidates(grid, sub_bx, sub_by, 2, pred_sub_8x8);
let r_8x8 = me.search_block_with_candidates(
&src_8x8,
8,
reference,
sub_px_x,
sub_px_y,
8,
8,
pred_sub_8x8,
&cand_8x8,
);
let cost_p8x8 = r_8x8.cost;
let src_top = extract_half(src_y, off_x, off_y, 8, 4);
let src_bot = extract_half(src_y, off_x, off_y + 4, 8, 4);
let r_top = me.search_block(
&src_top, 8, reference, sub_px_x, sub_px_y, 8, 4, r_8x8.mv,
);
let r_bot = me.search_block(
&src_bot, 8, reference, sub_px_x, sub_px_y + 4, 8, 4, r_top.mv,
);
let cost_p8x4 = r_top
.cost
.saturating_add(r_bot.cost)
.saturating_add(SUB_PENALTY_P8X4);
let src_left = extract_half(src_y, off_x, off_y, 4, 8);
let src_right = extract_half(src_y, off_x + 4, off_y, 4, 8);
let r_left = me.search_block(
&src_left, 4, reference, sub_px_x, sub_px_y, 4, 8, r_8x8.mv,
);
let r_right = me.search_block(
&src_right, 4, reference, sub_px_x + 4, sub_px_y, 4, 8, r_left.mv,
);
let cost_p4x8 = r_left
.cost
.saturating_add(r_right.cost)
.saturating_add(SUB_PENALTY_P4X8);
let mut r_4x4 = [MotionVector::ZERO; 4];
let mut cost_p4x4 = 0u32;
let quarter_origins = [(0u32, 0u32), (4, 0), (0, 4), (4, 4)];
for (qi, &(qx, qy)) in quarter_origins.iter().enumerate() {
let src_q = extract_half(src_y, off_x + qx as usize, off_y + qy as usize, 4, 4);
let start = if qi == 0 { r_8x8.mv } else { r_4x4[qi - 1] };
let r = me.search_block(
&src_q,
4,
reference,
sub_px_x + qx,
sub_px_y + qy,
4,
4,
start,
);
r_4x4[qi] = r.mv;
cost_p4x4 = cost_p4x4.saturating_add(r.cost);
}
cost_p4x4 = cost_p4x4.saturating_add(SUB_PENALTY_P4X4);
let mut best_cost = cost_p8x8;
let mut best = SubMbChoice::P8x8 { mv: r_8x8.mv };
if cost_p8x4 < best_cost {
best_cost = cost_p8x4;
best = SubMbChoice::P8x4 { mvs: [r_top.mv, r_bot.mv] };
}
if cost_p4x8 < best_cost {
best_cost = cost_p4x8;
best = SubMbChoice::P4x8 { mvs: [r_left.mv, r_right.mv] };
}
if cost_p4x4 < best_cost {
best_cost = cost_p4x4;
best = SubMbChoice::P4x4 { mvs: r_4x4 };
}
(best, best_cost)
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::reconstruction::ReconBuffer;
fn build_ref(w: u32, h: u32, fill: impl Fn(u32, u32) -> u8) -> ReconFrame {
let mut rb = ReconBuffer::new(w, h).unwrap();
for y in 0..h {
for x in 0..w {
rb.y[(y * w + x) as usize] = fill(x, y);
}
}
for v in rb.cb.iter_mut() {
*v = 128;
}
for v in rb.cr.iter_mut() {
*v = 128;
}
ReconFrame::snapshot(&rb)
}
fn unique_content(x: u32, y: u32) -> u8 {
((x * 11 + y * 7) & 0xFF) as u8
}
struct UmhOffGuard;
impl UmhOffGuard {
fn new() -> Self {
unsafe { std::env::set_var("PHASM_ME_UMH", "0"); }
Self
}
}
impl Drop for UmhOffGuard {
fn drop(&mut self) {
unsafe { std::env::remove_var("PHASM_ME_UMH"); }
}
}
#[test]
fn decide_prefers_16x16_on_uniform_motion() {
let _g = UmhOffGuard::new();
let reference = build_ref(64, 48, unique_content);
let mut src = [[0u8; 16]; 16];
for dy in 0..16 {
for dx in 0..16 {
src[dy][dx] = reference.y_at(20 + dx as u32, 16 + dy as u32);
}
}
let mut grid = EncoderMvGrid::new(4, 3);
let mut me = MotionEstimator::new();
let choice = decide_p_mb(&src, &reference, &mut me, &mut grid, 1, 1);
assert!(
matches!(choice, PMbChoice::P16x16 { .. }),
"expected P16x16, got {choice:?}"
);
}
#[test]
fn decide_prefers_16x8_on_horizontal_stripe_motion() {
let _g = UmhOffGuard::new();
let reference = build_ref(64, 48, unique_content);
let mut src = [[0u8; 16]; 16];
for dy in 0..8 {
for dx in 0..16 {
src[dy][dx] = reference.y_at(16 + dx as u32 + 4, 16 + dy as u32);
}
}
for dy in 8..16 {
for dx in 0..16 {
src[dy][dx] = reference.y_at(16 + dx as u32, 16 + dy as u32);
}
}
let mut grid = EncoderMvGrid::new(4, 3);
let mut me = MotionEstimator::new();
let choice = decide_p_mb(&src, &reference, &mut me, &mut grid, 1, 1);
assert!(
matches!(choice, PMbChoice::P16x8 { .. }),
"expected P16x8, got {choice:?}"
);
}
#[test]
fn sub_mb_type_codenums_match_spec_table_7_17() {
assert_eq!(
SubMbChoice::P8x8 { mv: MotionVector::ZERO }.sub_mb_type_codenum(),
0
);
assert_eq!(
SubMbChoice::P8x4 { mvs: [MotionVector::ZERO; 2] }.sub_mb_type_codenum(),
1
);
assert_eq!(
SubMbChoice::P4x8 { mvs: [MotionVector::ZERO; 2] }.sub_mb_type_codenum(),
2
);
assert_eq!(
SubMbChoice::P4x4 { mvs: [MotionVector::ZERO; 4] }.sub_mb_type_codenum(),
3
);
}
#[test]
fn decide_prefers_p8x8_on_quadrant_motion() {
let _g = UmhOffGuard::new();
let reference = build_ref(64, 48, unique_content);
let mut src = [[0u8; 16]; 16];
for dy in 0..16i32 {
for dx in 0..16i32 {
let (sx, sy) = match (dx < 8, dy < 8) {
(true, true) => (dx + 4, dy), (false, true) => (dx, dy + 4), (true, false) => (dx, dy - 4), (false, false) => (dx - 4, dy), };
src[dy as usize][dx as usize] =
reference.y_at((16 + sx) as u32, (16 + sy) as u32);
}
}
let mut grid = EncoderMvGrid::new(4, 3);
let mut me = MotionEstimator::new();
let choice = decide_p_mb(&src, &reference, &mut me, &mut grid, 1, 1);
assert!(
matches!(choice, PMbChoice::P8x8 { .. }),
"expected P8x8, got {choice:?}"
);
}
#[test]
fn decide_prefers_8x16_on_vertical_stripe_motion() {
let _g = UmhOffGuard::new();
let reference = build_ref(64, 48, unique_content);
let mut src = [[0u8; 16]; 16];
for dy in 0..16 {
for dx in 0..8 {
src[dy][dx] = reference.y_at(16 + dx as u32, 16 + dy as u32 + 4);
}
for dx in 8..16 {
src[dy][dx] = reference.y_at(16 + dx as u32, 16 + dy as u32);
}
}
let mut grid = EncoderMvGrid::new(4, 3);
let mut me = MotionEstimator::new();
let choice = decide_p_mb(&src, &reference, &mut me, &mut grid, 1, 1);
assert!(
matches!(choice, PMbChoice::P8x16 { .. }),
"expected P8x16, got {choice:?}"
);
}
}