#![forbid(unsafe_code)]
#![allow(dead_code)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::too_many_arguments)]
use super::tile::TileInfo;
use crate::error::{CodecError, CodecResult};
use crate::frame::VideoFrame;
use rayon::prelude::*;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct TileEncoderConfig {
pub tile_cols: u32,
pub tile_rows: u32,
pub threads: usize,
pub sb_size: u32,
pub uniform_spacing: bool,
pub min_tile_width_sb: u32,
pub max_tile_width_sb: u32,
pub min_tile_height_sb: u32,
pub max_tile_height_sb: u32,
}
impl Default for TileEncoderConfig {
fn default() -> Self {
Self {
tile_cols: 1,
tile_rows: 1,
threads: 0,
sb_size: 64,
uniform_spacing: true,
min_tile_width_sb: 1,
max_tile_width_sb: 64,
min_tile_height_sb: 1,
max_tile_height_sb: 64,
}
}
}
impl TileEncoderConfig {
#[must_use]
pub fn auto(width: u32, height: u32, threads: usize) -> Self {
let mut config = Self::default();
config.threads = threads;
config.configure_for_dimensions(width, height);
config
}
pub fn with_tile_counts(tile_cols: u32, tile_rows: u32, threads: usize) -> CodecResult<Self> {
if tile_cols == 0 || tile_rows == 0 {
return Err(CodecError::InvalidParameter(
"Tile counts must be positive".to_string(),
));
}
if tile_cols > 64 || tile_rows > 64 {
return Err(CodecError::InvalidParameter(
"Maximum 64 tile columns/rows".to_string(),
));
}
if !tile_cols.is_power_of_two() || !tile_rows.is_power_of_two() {
return Err(CodecError::InvalidParameter(
"Tile counts must be power of 2".to_string(),
));
}
Ok(Self {
tile_cols,
tile_rows,
threads,
..Default::default()
})
}
pub fn configure_for_dimensions(&mut self, width: u32, height: u32) {
let sb_cols = width.div_ceil(self.sb_size);
let sb_rows = height.div_ceil(self.sb_size);
let thread_count = if self.threads == 0 {
rayon::current_num_threads()
} else {
self.threads
};
let target_tiles = thread_count.next_power_of_two() as u32;
let aspect_ratio = width as f32 / height.max(1) as f32;
if aspect_ratio > 2.0 {
self.tile_cols = (target_tiles as f32).sqrt().ceil() as u32;
self.tile_cols = self.tile_cols.next_power_of_two();
self.tile_rows = (target_tiles / self.tile_cols).max(1);
self.tile_rows = self.tile_rows.next_power_of_two();
} else if aspect_ratio < 0.5 {
self.tile_rows = (target_tiles as f32).sqrt().ceil() as u32;
self.tile_rows = self.tile_rows.next_power_of_two();
self.tile_cols = (target_tiles / self.tile_rows).max(1);
self.tile_cols = self.tile_cols.next_power_of_two();
} else {
let sqrt_tiles = (target_tiles as f32).sqrt() as u32;
self.tile_cols = sqrt_tiles.next_power_of_two();
self.tile_rows = sqrt_tiles.next_power_of_two();
}
self.tile_cols = self.tile_cols.clamp(1, 64.min(sb_cols));
self.tile_rows = self.tile_rows.clamp(1, 64.min(sb_rows));
while self.tile_cols * self.tile_rows > 4096 {
if self.tile_cols > self.tile_rows {
self.tile_cols /= 2;
} else {
self.tile_rows /= 2;
}
}
}
#[must_use]
pub const fn tile_count(&self) -> u32 {
self.tile_cols * self.tile_rows
}
#[must_use]
pub fn thread_count(&self) -> usize {
if self.threads == 0 {
rayon::current_num_threads()
} else {
self.threads
}
}
pub fn validate(&self) -> CodecResult<()> {
if self.tile_cols == 0 || self.tile_rows == 0 {
return Err(CodecError::InvalidParameter(
"Tile counts must be positive".to_string(),
));
}
if self.tile_cols > 64 || self.tile_rows > 64 {
return Err(CodecError::InvalidParameter(
"Maximum 64 tile columns/rows".to_string(),
));
}
if self.tile_count() > 4096 {
return Err(CodecError::InvalidParameter(
"Maximum 4096 total tiles".to_string(),
));
}
if self.sb_size != 64 && self.sb_size != 128 {
return Err(CodecError::InvalidParameter(
"Superblock size must be 64 or 128".to_string(),
));
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct TileRegion {
pub col: u32,
pub row: u32,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub index: u32,
}
impl TileRegion {
#[must_use]
pub const fn new(
col: u32,
row: u32,
x: u32,
y: u32,
width: u32,
height: u32,
tile_cols: u32,
) -> Self {
Self {
col,
row,
x,
y,
width,
height,
index: row * tile_cols + col,
}
}
#[must_use]
pub const fn is_valid(&self) -> bool {
self.width > 0 && self.height > 0
}
#[must_use]
pub const fn area(&self) -> u32 {
self.width * self.height
}
#[must_use]
pub const fn is_left_edge(&self) -> bool {
self.col == 0
}
#[must_use]
pub const fn is_top_edge(&self) -> bool {
self.row == 0
}
}
#[derive(Clone, Debug)]
pub struct TileFrameSplitter {
config: TileEncoderConfig,
frame_width: u32,
frame_height: u32,
regions: Vec<TileRegion>,
}
impl TileFrameSplitter {
pub fn new(
config: TileEncoderConfig,
frame_width: u32,
frame_height: u32,
) -> CodecResult<Self> {
config.validate()?;
let mut splitter = Self {
config,
frame_width,
frame_height,
regions: Vec::new(),
};
splitter.compute_regions();
Ok(splitter)
}
fn compute_regions(&mut self) {
self.regions.clear();
if self.config.uniform_spacing {
self.compute_uniform_regions();
} else {
self.compute_custom_regions();
}
}
fn compute_uniform_regions(&mut self) {
let tile_width = self.frame_width.div_ceil(self.config.tile_cols);
let tile_height = self.frame_height.div_ceil(self.config.tile_rows);
for row in 0..self.config.tile_rows {
for col in 0..self.config.tile_cols {
let x = col * tile_width;
let y = row * tile_height;
let width = if col == self.config.tile_cols - 1 {
self.frame_width - x
} else {
tile_width
};
let height = if row == self.config.tile_rows - 1 {
self.frame_height - y
} else {
tile_height
};
self.regions.push(TileRegion::new(
col,
row,
x,
y,
width,
height,
self.config.tile_cols,
));
}
}
}
fn compute_custom_regions(&mut self) {
self.compute_uniform_regions();
}
#[must_use]
pub fn regions(&self) -> &[TileRegion] {
&self.regions
}
#[must_use]
pub fn region(&self, index: usize) -> Option<&TileRegion> {
self.regions.get(index)
}
#[must_use]
pub fn tile_count(&self) -> usize {
self.regions.len()
}
}
#[derive(Clone, Debug)]
pub struct TileEncoder {
region: TileRegion,
quality: u8,
is_keyframe: bool,
}
impl TileEncoder {
#[must_use]
pub const fn new(region: TileRegion, quality: u8, is_keyframe: bool) -> Self {
Self {
region,
quality,
is_keyframe,
}
}
pub fn encode(&self, frame: &VideoFrame) -> CodecResult<TileEncodedData> {
if self.region.x + self.region.width > frame.width
|| self.region.y + self.region.height > frame.height
{
return Err(CodecError::InvalidParameter(
"Tile region exceeds frame bounds".to_string(),
));
}
let tile_data = self.extract_tile_data(frame)?;
let encoded = self.encode_tile_data(&tile_data)?;
Ok(TileEncodedData {
region: self.region.clone(),
data: encoded,
size: 0, })
}
fn extract_tile_data(&self, frame: &VideoFrame) -> CodecResult<Vec<u8>> {
let mut tile_pixels = Vec::new();
if let Some(y_plane) = frame.planes.first() {
for y in self.region.y..(self.region.y + self.region.height) {
let row_start = (y as usize * y_plane.stride) + self.region.x as usize;
let row_end = row_start + self.region.width as usize;
if row_end <= y_plane.data.len() {
tile_pixels.extend_from_slice(&y_plane.data[row_start..row_end]);
}
}
}
let chroma_x = self.region.x / 2;
let chroma_y = self.region.y / 2;
let chroma_width = self.region.width / 2;
let chroma_height = self.region.height / 2;
for plane_idx in 1..frame.planes.len() {
if let Some(plane) = frame.planes.get(plane_idx) {
for y in chroma_y..(chroma_y + chroma_height) {
let row_start = (y as usize * plane.stride) + chroma_x as usize;
let row_end = row_start + chroma_width as usize;
if row_end <= plane.data.len() {
tile_pixels.extend_from_slice(&plane.data[row_start..row_end]);
}
}
}
}
Ok(tile_pixels)
}
fn encode_tile_data(&self, _tile_data: &[u8]) -> CodecResult<Vec<u8>> {
let mut encoded = Vec::new();
encoded.push(if self.is_keyframe { 0x80 } else { 0x00 });
encoded.push(self.quality);
encoded.extend_from_slice(&(self.region.width).to_le_bytes());
encoded.extend_from_slice(&(self.region.height).to_le_bytes());
let compressed_size = (self.region.width * self.region.height / 32) as usize;
encoded.resize(encoded.len() + compressed_size, 0);
Ok(encoded)
}
#[must_use]
pub const fn region(&self) -> &TileRegion {
&self.region
}
}
#[derive(Clone, Debug)]
pub struct TileEncodedData {
pub region: TileRegion,
pub data: Vec<u8>,
pub size: usize,
}
impl TileEncodedData {
#[must_use]
pub const fn index(&self) -> u32 {
self.region.index
}
#[must_use]
pub fn encoded_size(&self) -> usize {
self.data.len()
}
}
#[derive(Debug)]
pub struct ParallelTileEncoder {
config: Arc<TileEncoderConfig>,
splitter: TileFrameSplitter,
frame_width: u32,
frame_height: u32,
}
impl ParallelTileEncoder {
pub fn new(
config: TileEncoderConfig,
frame_width: u32,
frame_height: u32,
) -> CodecResult<Self> {
let splitter = TileFrameSplitter::new(config.clone(), frame_width, frame_height)?;
Ok(Self {
config: Arc::new(config),
splitter,
frame_width,
frame_height,
})
}
pub fn encode_frame(
&self,
frame: &VideoFrame,
quality: u8,
is_keyframe: bool,
) -> CodecResult<Vec<TileEncodedData>> {
if frame.width != self.frame_width || frame.height != self.frame_height {
return Err(CodecError::InvalidParameter(format!(
"Frame dimensions {}x{} don't match encoder {}x{}",
frame.width, frame.height, self.frame_width, self.frame_height
)));
}
if self.config.threads > 0 {
rayon::ThreadPoolBuilder::new()
.num_threads(self.config.threads)
.build()
.map_err(|e| {
CodecError::Internal(format!("Failed to create thread pool: {}", e))
})?;
}
let encoded_tiles: Vec<CodecResult<TileEncodedData>> = self
.splitter
.regions()
.par_iter()
.map(|region| {
let encoder = TileEncoder::new(region.clone(), quality, is_keyframe);
encoder.encode(frame)
})
.collect();
let mut tiles = Vec::with_capacity(encoded_tiles.len());
for result in encoded_tiles {
tiles.push(result?);
}
tiles.sort_by_key(TileEncodedData::index);
Ok(tiles)
}
pub fn merge_tiles(&self, tiles: &[TileEncodedData]) -> CodecResult<Vec<u8>> {
if tiles.is_empty() {
return Ok(Vec::new());
}
let mut merged = Vec::new();
self.write_tile_group_header(&mut merged, tiles.len() as u32);
for (i, tile) in tiles.iter().enumerate() {
let is_last = i == tiles.len() - 1;
if !is_last {
let size = tile.data.len() as u32;
self.write_tile_size(&mut merged, size);
}
merged.extend_from_slice(&tile.data);
}
Ok(merged)
}
fn write_tile_group_header(&self, output: &mut Vec<u8>, _num_tiles: u32) {
if self.config.tile_count() > 1 {
output.push(0x01); }
}
fn write_tile_size(&self, output: &mut Vec<u8>, size: u32) {
output.extend_from_slice(&size.to_le_bytes());
}
#[must_use]
pub fn config(&self) -> &TileEncoderConfig {
&self.config
}
#[must_use]
pub fn tile_count(&self) -> usize {
self.splitter.tile_count()
}
#[must_use]
pub fn regions(&self) -> &[TileRegion] {
self.splitter.regions()
}
}
pub struct TileInfoBuilder;
impl TileInfoBuilder {
#[must_use]
pub fn from_config(
config: &TileEncoderConfig,
frame_width: u32,
frame_height: u32,
) -> TileInfo {
let sb_cols = frame_width.div_ceil(config.sb_size);
let sb_rows = frame_height.div_ceil(config.sb_size);
let tile_width_sb = sb_cols.div_ceil(config.tile_cols);
let tile_height_sb = sb_rows.div_ceil(config.tile_rows);
let mut tile_col_starts = Vec::new();
for i in 0..=config.tile_cols {
let start = (i * tile_width_sb).min(sb_cols);
tile_col_starts.push(start);
}
let mut tile_row_starts = Vec::new();
for i in 0..=config.tile_rows {
let start = (i * tile_height_sb).min(sb_rows);
tile_row_starts.push(start);
}
let tile_cols_log2 = (config.tile_cols as f32).log2() as u8;
let tile_rows_log2 = (config.tile_rows as f32).log2() as u8;
TileInfo {
tile_cols: config.tile_cols,
tile_rows: config.tile_rows,
tile_col_starts,
tile_row_starts,
context_update_tile_id: 0,
tile_size_bytes: 4,
uniform_tile_spacing: config.uniform_spacing,
tile_cols_log2,
tile_rows_log2,
min_tile_cols_log2: 0,
max_tile_cols_log2: 6,
min_tile_rows_log2: 0,
max_tile_rows_log2: 6,
sb_cols,
sb_rows,
sb_size: config.sb_size,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use oximedia_core::PixelFormat;
#[test]
fn test_tile_encoder_config_default() {
let config = TileEncoderConfig::default();
assert_eq!(config.tile_cols, 1);
assert_eq!(config.tile_rows, 1);
assert_eq!(config.tile_count(), 1);
}
#[test]
fn test_tile_encoder_config_auto() {
let config = TileEncoderConfig::auto(1920, 1080, 4);
assert!(config.tile_count() > 0);
assert!(config.tile_count() <= 4096);
}
#[test]
fn test_tile_encoder_config_manual() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
assert_eq!(config.tile_cols, 2);
assert_eq!(config.tile_rows, 2);
assert_eq!(config.tile_count(), 4);
}
#[test]
fn test_tile_encoder_config_validation() {
let config = TileEncoderConfig::default();
assert!(config.validate().is_ok());
let mut invalid = TileEncoderConfig::default();
invalid.tile_cols = 0;
assert!(invalid.validate().is_err());
}
#[test]
fn test_tile_region() {
let region = TileRegion::new(0, 0, 0, 0, 640, 480, 2);
assert!(region.is_valid());
assert_eq!(region.area(), 640 * 480);
assert!(region.is_left_edge());
assert!(region.is_top_edge());
}
#[test]
fn test_tile_frame_splitter() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
let splitter = TileFrameSplitter::new(config, 1920, 1080).expect("should succeed");
assert_eq!(splitter.tile_count(), 4);
assert_eq!(splitter.regions().len(), 4);
let region = splitter.region(0).expect("should succeed");
assert_eq!(region.col, 0);
assert_eq!(region.row, 0);
assert_eq!(region.x, 0);
assert_eq!(region.y, 0);
}
#[test]
fn test_tile_encoder() {
let region = TileRegion::new(0, 0, 0, 0, 320, 240, 1);
let encoder = TileEncoder::new(region, 128, true);
let mut frame = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
frame.allocate();
let result = encoder.encode(&frame);
assert!(result.is_ok());
let encoded = result.expect("should succeed");
assert!(encoded.encoded_size() > 0);
}
#[test]
fn test_parallel_tile_encoder() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
let encoder = ParallelTileEncoder::new(config, 1920, 1080).expect("should succeed");
assert_eq!(encoder.tile_count(), 4);
assert_eq!(encoder.regions().len(), 4);
}
#[test]
fn test_parallel_encode_frame() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
let encoder = ParallelTileEncoder::new(config, 1920, 1080).expect("should succeed");
let mut frame = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
frame.allocate();
let result = encoder.encode_frame(&frame, 128, true);
assert!(result.is_ok());
let tiles = result.expect("should succeed");
assert_eq!(tiles.len(), 4);
}
#[test]
fn test_merge_tiles() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
let encoder = ParallelTileEncoder::new(config, 1920, 1080).expect("should succeed");
let mut frame = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
frame.allocate();
let tiles = encoder
.encode_frame(&frame, 128, true)
.expect("should succeed");
let merged = encoder.merge_tiles(&tiles);
assert!(merged.is_ok());
assert!(!merged.expect("should succeed").is_empty());
}
#[test]
fn test_tile_info_builder() {
let config = TileEncoderConfig::with_tile_counts(2, 2, 4).expect("should succeed");
let tile_info = TileInfoBuilder::from_config(&config, 1920, 1080);
assert_eq!(tile_info.tile_cols, 2);
assert_eq!(tile_info.tile_rows, 2);
assert_eq!(tile_info.tile_count(), 4);
}
#[test]
fn test_aspect_ratio_configuration() {
let mut config = TileEncoderConfig::default();
config.configure_for_dimensions(3840, 1080);
assert!(config.tile_cols >= config.tile_rows);
let mut config = TileEncoderConfig::default();
config.configure_for_dimensions(1080, 3840);
assert!(config.tile_rows >= config.tile_cols);
}
}