From da8ef3bb80429da77db2039da1bcac6fcfef3557 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Thu, 30 Apr 2026 21:10:40 +0200 Subject: [PATCH] RPZA (very simple) encoder --- nihav-qt/Cargo.toml | 3 +- nihav-qt/src/codecs/mod.rs | 4 + nihav-qt/src/codecs/rpzaenc.rs | 638 +++++++++++++++++++++++++++++++++ 3 files changed, 644 insertions(+), 1 deletion(-) create mode 100644 nihav-qt/src/codecs/rpzaenc.rs diff --git a/nihav-qt/Cargo.toml b/nihav-qt/Cargo.toml index 25ee0da..af9a82b 100644 --- a/nihav-qt/Cargo.toml +++ b/nihav-qt/Cargo.toml @@ -45,8 +45,9 @@ demuxer_warhol = ["demuxers"] all_encoders = ["all_video_encoders", "all_audio_encoders"] encoders = [] -all_video_encoders = ["encoder_rawvid", "encoder_rle"] +all_video_encoders = ["encoder_rawvid", "encoder_rle", "encoder_rpza"] encoder_rawvid = ["encoders"] encoder_rle = ["encoders"] +encoder_rpza = ["encoders"] all_audio_encoders = [] diff --git a/nihav-qt/src/codecs/mod.rs b/nihav-qt/src/codecs/mod.rs index bbec675..739ffe8 100644 --- a/nihav-qt/src/codecs/mod.rs +++ b/nihav-qt/src/codecs/mod.rs @@ -115,6 +115,8 @@ pub fn qt_register_all_decoders(rd: &mut RegisteredDecoders) { mod rawvidenc; #[cfg(feature="encoder_rle")] mod rleenc; +#[cfg(feature="encoder_rpza")] +mod rpzaenc; #[cfg(feature="encoders")] const QT_ENCODERS: &[EncoderInfo] = &[ @@ -124,6 +126,8 @@ const QT_ENCODERS: &[EncoderInfo] = &[ EncoderInfo { name: "qt-yuv4", get_encoder: rawvidenc::get_encoder_yuv4 }, #[cfg(feature="encoder_rle")] EncoderInfo { name: "qt-rle", get_encoder: rleenc::get_encoder }, +#[cfg(feature="encoder_rpza")] + EncoderInfo { name: "apple-video", get_encoder: rpzaenc::get_encoder }, ]; /// Registers all available encoders provided by this crate. diff --git a/nihav-qt/src/codecs/rpzaenc.rs b/nihav-qt/src/codecs/rpzaenc.rs new file mode 100644 index 0000000..0f42eda --- /dev/null +++ b/nihav-qt/src/codecs/rpzaenc.rs @@ -0,0 +1,638 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use std::ops::{Add, AddAssign, Sub, Div, DivAssign}; + +const RGB555BE_FORMAT: NAPixelFormaton = NAPixelFormaton::make_rgb16_fmt(5, 5, 5, true, true); + +#[derive(Clone,Copy,Debug,Default,PartialEq)] +struct Pixel([u16; 3]); + +impl Pixel { + fn interpolate(&self, p1: Pixel) -> Pixel { + let mut ret = *self; + for (dst, &comp1) in ret.0.iter_mut().zip(p1.0.iter()) { + *dst = (*dst * 21 + comp1 * 11) >> 5; + } + ret + } + fn min(&self, p1: Pixel) -> Pixel { + let mut ret = *self; + for (c0, &c1) in ret.0.iter_mut().zip(p1.0.iter()) { + *c0 = (*c0).min(c1); + } + ret + } + fn max(&self, p1: Pixel) -> Pixel { + let mut ret = *self; + for (c0, &c1) in ret.0.iter_mut().zip(p1.0.iter()) { + *c0 = (*c0).max(c1); + } + ret + } + fn dist(&self, other: &Pixel) -> u32 { + self.0.iter().zip(other.0.iter()).fold(0u32, + |acc, (&a, &b)| acc + u32::from(a.abs_diff(b)) * u32::from(a.abs_diff(b))) + } +} + +impl From for Pixel { + fn from(pix: u16) -> Pixel { + Pixel([(pix >> 10) & 0x1F, (pix >> 5) & 0x1F, pix & 0x1F]) + } +} + +impl From for u16 { + fn from(pix: Pixel) -> u16 { + (pix.0[0] << 10) | (pix.0[1] << 5) | pix.0[2] + } +} + +impl Sub for Pixel { + type Output = Self; + fn sub(self, rhs: Pixel) -> Self::Output { + let mut ret = self; + for (c0, &c1) in ret.0.iter_mut().zip(rhs.0.iter()) { + *c0 = c0.abs_diff(c1); + } + ret + } +} + +impl Add for Pixel { + type Output = Self; + fn add(self, rhs: Pixel) -> Self::Output { + let mut ret = self; + for (c0, &c1) in ret.0.iter_mut().zip(rhs.0.iter()) { + *c0 += c1; + } + ret + } +} + +impl AddAssign for Pixel { + fn add_assign(&mut self, other: Pixel) { + for (dst, &src) in self.0.iter_mut().zip(other.0.iter()) { + *dst += src; + } + } +} + +impl Div for Pixel { + type Output = Self; + fn div(self, val: u8) -> Pixel { + let mut ret = self; + for comp in ret.0.iter_mut() { + *comp /= u16::from(val); + } + ret + } +} + +impl DivAssign for Pixel { + fn div_assign(&mut self, val: u8) { + for comp in self.0.iter_mut() { + *comp /= u16::from(val); + } + } +} + +impl std::fmt::Display for Pixel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{:02X},{:02X},{:02X}", self.0[0], self.0[1], self.0[2]) + } +} + +fn interpolate_u16(c0: u16, c1: u16) -> u16 { + Pixel::from(c0).interpolate(c1.into()).into() +} + +#[derive(Clone,Copy,Debug,PartialEq)] +enum EncState { + None, + Skip(usize), + Fill(u16, usize), + Four(u16, u16, [u8; 4]), + Raw, +} + +impl EncState { + fn write(&self, dst: &mut GrowableMemoryWriter<'_>, patterns: &mut Vec<[u8; 4]>) { + match *self { + EncState::None => {}, + EncState::Skip(skip) => { + for _ in 0..(skip / 32) { + let _ = dst.write_byte(0x80 | 0x1F); + } + if (skip & 0x1F) > 0 { + let _ = dst.write_byte(0x80 | ((skip & 0x1F) as u8 - 1)); + } + }, + EncState::Fill(clr, nblocks) => { + for _ in 0..(nblocks / 32) { + let _ = dst.write_byte(0xA0 | 0x1F); + let _ = dst.write_u16be(clr); + } + if (nblocks & 0x1F) > 0 { + let _ = dst.write_byte(0xA0 | ((nblocks & 0x1F) as u8 - 1)); + let _ = dst.write_u16be(clr); + } + }, + EncState::Four(clr0, clr1, _) => { + if patterns.len() == 1 { + let _ = dst.write_u16be(clr0); + let _ = dst.write_u16be(clr1 | 0x8000); + let _ = dst.write_buf(&patterns[0]); + } else { + for pats in patterns.chunks(32) { + let _ = dst.write_byte(0xC0 | (pats.len() as u8 - 1)); + let _ = dst.write_u16be(clr0); + let _ = dst.write_u16be(clr1); + for pat in pats.iter() { + let _ = dst.write_buf(pat); + } + } + } + patterns.clear() + }, + EncState::Raw => {}, + } + } +} + +#[derive(Clone,Copy,Debug,Default,PartialEq)] +enum Strategy { + Lossless, + #[default] + Normal, + Refined, +} + +fn find_nearest(cc: &[Pixel; 4], pix: &Pixel) -> (usize, u32) { + let mut best_idx = 0; + let mut best_dist = u32::MAX; + for (i, cand) in cc.iter().enumerate() { + let dist = cand.dist(pix); + if dist == 0 { + return (i, 0); + } + if dist < best_dist { + best_dist = dist; + best_idx = i; + } + } + (best_idx, best_dist) +} + +struct RoadPizzaEncoder { + stream: Option, + pkt: Option, + key_int: u8, + frm_no: u8, + last_frm: Vec, + vinfo: NAVideoInfo, + mode: Strategy, +} + +impl RoadPizzaEncoder { + fn new() -> Self { + Self { + stream: None, + pkt: None, + key_int: 25, + frm_no: 254, + last_frm: Vec::new(), + vinfo: NAVideoInfo::new(0, 0, false, RGB555BE_FORMAT), + mode: Strategy::default(), + } + } + fn paint_4clr(dst: &mut [u16], stride: usize, x: usize, c3: u16, c0: u16, pat: [u8; 4]) { + let clrs = [c0, interpolate_u16(c0, c3), interpolate_u16(c3, c0), c3]; + for (line, &pp) in dst.chunks_exact_mut(stride).zip(pat.iter()) { + let mut idx = usize::from(pp); + for pix in line[x..][..4].iter_mut() { + *pix = clrs[(idx >> 6) & 3]; + idx <<= 2; + } + } + } + fn decide_mode(mode: Strategy, _prev_state: EncState, skip_dist: u32, pixels: &[Pixel; 16]) -> EncState { + let skip_thr = if mode == Strategy::Lossless { 0 } else { 32 }; + if skip_dist.saturating_sub(skip_thr) == 0 { + return EncState::Skip(1); + } + match mode { + Strategy::Lossless => { + if pixels == &[pixels[0]; 16] { + EncState::Fill(pixels[0].into(), 1) + } else { + EncState::Raw + } + }, + Strategy::Normal | Strategy::Refined => { + let mut min_pix = Pixel([0x1F; 3]); + let mut max_pix = Pixel([0x00; 3]); + for pix in pixels.iter() { + min_pix = min_pix.min(*pix); + max_pix = max_pix.max(*pix); + } + let range = max_pix - min_pix; + let ridx = if range.0[1] >= range.0[0] && range.0[1] >= range.0[2] { + 1 + } else if range.0[0] >= range.0[1] && range.0[0] >= range.0[2] { + 0 + } else if range.0[2] >= range.0[0] && range.0[2] >= range.0[1] { + 2 + } else { + 1 + }; + if range.0[ridx] < 2 { + let fill: Pixel = pixels.iter().fold(Pixel::default(), |acc, &p| acc + p) / 16; + return EncState::Fill(u16::from(fill), 1); + } + let mut cnt = [0u8; 2]; + let mut sum = [Pixel::default(); 2]; + for pix in pixels.iter() { + if pix.0[ridx].abs_diff(min_pix.0[ridx]) < 2 { + cnt[0] += 1; + sum[0] += *pix; + } + if pix.0[ridx].abs_diff(max_pix.0[ridx]) < 2 { + cnt[1] += 1; + sum[1] += *pix; + } + } + sum[0] /= cnt[0]; + sum[1] /= cnt[1]; + + let mut centroids = [sum[0], sum[0].interpolate(sum[1]), sum[1].interpolate(sum[0]), sum[1]]; + if mode == Strategy::Refined { + let mut cur_dist = pixels.iter().fold(0u32, |acc, pix| acc + find_nearest(¢roids, pix).1); + loop { + let mut sum = [Pixel::default(); 4]; + let mut cnt = [0; 4]; + for pix in pixels.iter() { + let (idx, _dist) = find_nearest(¢roids, pix); + sum[idx] += *pix; + cnt[idx] += 1; + } + for (s, &cc) in sum.iter_mut().zip(cnt.iter()) { + *s /= cc.max(1); + } + let new_dist = pixels.iter().fold(0u32, |acc, pix| acc + find_nearest(&sum, pix).1); + if new_dist < cur_dist { + centroids[0] = sum[0]; + centroids[3] = sum[3]; + centroids[1] = centroids[0].interpolate(centroids[3]); + centroids[2] = centroids[3].interpolate(centroids[0]); + if new_dist >= cur_dist - cur_dist / 8 { + break; + } + cur_dist = new_dist; + } else { + break; + } + } + } + if u16::from(centroids[0]) > u16::from(centroids[3]) { + centroids.swap(0, 3); + centroids.swap(1, 2); + } + let mut pat = [0; 4]; + for (pp, row) in pat.iter_mut().zip(pixels.chunks_exact(4)) { + for pix in row.iter() { + let (idx, _) = find_nearest(¢roids, pix); + *pp <<= 2; + *pp |= idx as u8; + } + } + EncState::Four(centroids[3].into(), centroids[0].into(), pat) + }, + } + } + fn encode(&mut self, dbuf: &mut Vec, src: &[u16], sstride: usize, force_intra: bool) -> bool { + let mut is_intra = true; + let mut pixels = [Pixel::default(); 16]; + + let mut state = EncState::None; + let mut patterns: Vec<[u8; 4]> = Vec::new(); + let pstride = self.vinfo.width; + let mut gw = GrowableMemoryWriter::new_write(dbuf); + let _ = gw.seek(SeekFrom::End(0)); + for (sstrip, pstrip) in src.chunks_exact(sstride * 4) + .zip(self.last_frm.chunks_exact_mut(pstride * 4)) { + for x in (0..self.vinfo.width).step_by(4) { + for (row, line) in pixels.chunks_exact_mut(4).zip(sstrip.chunks_exact(sstride)) { + for (dst, &src) in row.iter_mut().zip(line[x..][..4].iter()) { + *dst = Pixel::from(src); + } + } + let mut skip_dist = if force_intra { u32::MAX } else { 0 }; + if !force_intra { + if self.mode == Strategy::Lossless { + for (line0, line1) in sstrip.chunks_exact(sstride) + .zip(pstrip.chunks_exact(pstride)) { + if line0[x..][..4] != line1[x..][..4] { + skip_dist = u32::MAX; + break; + } + } + } else { + for (line, prow) in pstrip.chunks_exact(pstride).zip(pixels.chunks_exact(4)) { + for (&ppix, npix) in line[x..][..4].iter().zip(prow.iter()) { + skip_dist += npix.dist(&ppix.into()); + } + } + } + } + + let new_state = Self::decide_mode(self.mode, state, skip_dist, &pixels); + state = match (state, new_state) { + (_, EncState::None) => unreachable!(), + (EncState::Skip(skip), EncState::Skip(_)) => { + EncState::Skip(skip + 1) + }, + (_, EncState::Skip(_)) => { + state.write(&mut gw, &mut patterns); + is_intra = false; + new_state + }, + (EncState::Fill(old_clr, old_cnt), EncState::Fill(new_clr, _)) => { + for pline in pstrip.chunks_exact_mut(pstride) { + for pix in pline[x..][..4].iter_mut() { + *pix = new_clr; + } + } + if old_clr == new_clr { + EncState::Fill(old_clr, old_cnt + 1) + } else { + state.write(&mut gw, &mut patterns); + new_state + } + }, + (_, EncState::Fill(fill_clr, _)) => { + for pline in pstrip.chunks_exact_mut(pstride) { + for pix in pline[x..][..4].iter_mut() { + *pix = fill_clr; + } + } + state.write(&mut gw, &mut patterns); + new_state + }, + (EncState::Four(oc0, oc3, _), EncState::Four(c0, c3, cpat)) => { + Self::paint_4clr(pstrip, pstride, x, c0, c3, cpat); + if oc0 != c0 || oc3 != c3 { + state.write(&mut gw, &mut patterns); + } + patterns.push(cpat); + new_state + }, + (_, EncState::Four(c0, c3, cpat)) => { + Self::paint_4clr(pstrip, pstride, x, c0, c3, cpat); + state.write(&mut gw, &mut patterns); + patterns.push(cpat); + new_state + }, + (_, EncState::Raw) => { + for (line, pline) in sstrip.chunks_exact(sstride) + .zip(pstrip.chunks_exact_mut(pstride)) { + pline[x..][..4].copy_from_slice(&line[x..][..4]); + let _ = gw.write_u16be_arr(&line[x..][..4]); + } + state.write(&mut gw, &mut patterns); + EncState::None + }, + }; + } + } + state.write(&mut gw, &mut patterns); + let _ = gw.flush(); + is_intra + } +} + +impl NAEncoder for RoadPizzaEncoder { + fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => { + Ok(EncodeParameters { + format: NACodecTypeInfo::Video(NAVideoInfo::new(0, 0, false, RGB555BE_FORMAT)), + ..Default::default() + }) + }, + NACodecTypeInfo::Video(_vinfo) => { + let mut info = *encinfo; + if let NACodecTypeInfo::Video(ref mut vinfo) = info.format { + vinfo.format = RGB555BE_FORMAT; + vinfo.width = (vinfo.width + 3) & !3; + vinfo.height = (vinfo.height + 3) & !3; + vinfo.bits = 16; + vinfo.flipped = false; + } else { + return Err(EncoderError::FormatError); + } + Ok(info) + }, + NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError), + } + } + fn get_capabilities(&self) -> u64 { ENC_CAPS_SKIPFRAME } + fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Video(vinfo) => { + self.vinfo = vinfo; + if vinfo.format != RGB555BE_FORMAT || ((vinfo.width | vinfo.height) & 3) != 0 { + return Err(EncoderError::FormatError); + } + let info = NACodecInfo::new("apple-video", encinfo.format, None); + let mut stream = NAStream::new(StreamType::Video, stream_id, info, encinfo.tb_num, encinfo.tb_den, 0); + stream.set_num(stream_id as usize); + let stream = stream.into_ref(); + self.stream = Some(stream.clone()); + Ok(stream) + } + } + } + fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> { + let buf = frm.get_buffer(); + if let Some(vinfo) = buf.get_video_info() { + if vinfo != self.vinfo { + println!("Input format differs from the initial one"); + return Err(EncoderError::FormatError); + } + } + self.frm_no += 1; + let force_intra = self.frm_no >= self.key_int; + + let mut dbuf = vec![0xE1, 0, 0, 0]; // ID + 24-bit frame size + let is_intra = if let Some(vbuf16) = buf.get_vbuf16() { + let src = vbuf16.get_data(); + let stride = vbuf16.get_stride(0); + self.last_frm.resize(self.vinfo.width * self.vinfo.height, 0); + + self.encode(&mut dbuf, src, stride, force_intra) + } else if matches!(buf, NABufferType::None) { + if force_intra { + if self.last_frm.is_empty() { + return Err(EncoderError::InvalidParameters); + } + let last_copy = self.last_frm.clone(); + self.encode(&mut dbuf, &last_copy, self.vinfo.width, force_intra); + true + } else { + let state = EncState::Skip((self.vinfo.width / 4) * (self.vinfo.height / 4)); + let mut gw = GrowableMemoryWriter::new_write(&mut dbuf); + let _ = gw.seek(SeekFrom::End(0)); + let mut pats = Vec::new(); + state.write(&mut gw, &mut pats); + let _ = gw.flush(); + false + } + } else { + return Err(EncoderError::FormatError); + }; + + if is_intra { + self.frm_no = 0; + } + + let size = dbuf.len() as u32; + let _ = write_u24be(&mut dbuf[1..], size); + + self.pkt = Some(NAPacket::new(self.stream.clone().unwrap(), frm.ts, is_intra, dbuf)); + Ok(()) + } + fn get_packet(&mut self) -> EncoderResult> { + let mut npkt = None; + std::mem::swap(&mut self.pkt, &mut npkt); + Ok(npkt) + } + fn flush(&mut self) -> EncoderResult<()> { + Ok(()) + } +} + +const MODE_OPTION: &str = "mode"; + +const ENCODER_OPTS: &[NAOptionDefinition] = &[ + NAOptionDefinition { + name: KEYFRAME_OPTION, description: KEYFRAME_OPTION_DESC, + opt_type: NAOptionDefinitionType::Int(Some(0), Some(128)) }, + NAOptionDefinition { + name: MODE_OPTION, description: "encoding strategy", + opt_type: NAOptionDefinitionType::String(Some(&["lossless", "normal", "refined"])) }, +]; + +impl NAOptionHandler for RoadPizzaEncoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { ENCODER_OPTS } + fn set_options(&mut self, options: &[NAOption]) { + for option in options.iter() { + for opt_def in ENCODER_OPTS.iter() { + if opt_def.check(option).is_ok() { + match option.name { + KEYFRAME_OPTION => { + if let NAValue::Int(intval) = option.value { + self.key_int = intval as u8; + } + }, + MODE_OPTION => { + if let NAValue::String(ref strval) = option.value { + match strval.as_str() { + "lossless" => { self.mode = Strategy::Lossless; }, + "normal" => { self.mode = Strategy::Normal; }, + "refined" => { self.mode = Strategy::Refined; }, + _ => {}, + } + } + }, + _ => {}, + } + } + } + } + } + fn query_option_value(&self, name: &str) -> Option { + match name { + KEYFRAME_OPTION => Some(NAValue::Int(i64::from(self.key_int))), + MODE_OPTION => { + match self.mode { + Strategy::Lossless => Some(NAValue::String("lossless".to_string())), + Strategy::Normal => Some(NAValue::String("normal".to_string())), + Strategy::Refined => Some(NAValue::String("refined".to_string())), + } + }, + _ => None, + } + } +} + +pub fn get_encoder() -> Box { + Box::new(RoadPizzaEncoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::demuxers::*; + use nihav_core::muxers::*; + use nihav_commonfmt::*; + use nihav_codec_support::test::enc_video::*; + use crate::*; + + // samples from https://samples.mplayerhq.hu/V-codecs/QTRLE + fn test_core(in_name: &'static str, format: NAPixelFormaton, enc_options: &[NAOption], hash: &[u32; 4]) { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + qt_register_all_decoders(&mut dec_reg); + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + let mut enc_reg = RegisteredEncoders::new(); + qt_register_all_encoders(&mut enc_reg); + + let dec_config = DecoderTestParams { + demuxer: "mov", + in_name, + stream_type: StreamType::Video, + limit: Some(4), + dmx_reg, dec_reg, + }; + let enc_config = EncoderTestParams { + muxer: "mov", + enc_name: "apple-video", + out_name: "rpza.mov", + mux_reg, enc_reg, + }; + let dst_vinfo = NAVideoInfo { + width: 0, + height: 0, + format, + flipped: false, + bits: format.get_total_depth(), + }; + let enc_params = EncodeParameters { + format: NACodecTypeInfo::Video(dst_vinfo), + quality: 0, + bitrate: 0, + tb_num: 0, + tb_den: 0, + flags: 0, + }; + //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options); + test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash); + } + + #[test] + fn test_rpza_lossless() { + test_core("assets/QT/Animation-Truecolour.mov", super::RGB555BE_FORMAT, + &[NAOption{ name: "mode", value: NAValue::String("lossless".to_owned()) }], + &[0x420025d3, 0x33b792c4, 0x8f211b13, 0xa1f6f8e6]); + } + #[test] + fn test_rpza_normal() { + test_core("assets/QT/Animation-Truecolour.mov", super::RGB555BE_FORMAT, + &[NAOption{ name: "mode", value: NAValue::String("normal".to_owned()) }], + &[0x42ca0fce, 0xe49fd607, 0xac393dd9, 0xd9d503cf]); + } +} -- 2.39.5