1#![forbid(unsafe_code)]
44#![allow(clippy::cast_possible_truncation)]
45#![allow(clippy::missing_errors_doc)]
46
47use rayon::prelude::*;
48
49use crate::error::{CodecError, CodecResult};
50
51const TILE_MAGIC: [u8; 4] = [0x41, 0x56, 0x31, 0x54]; const TILE_HEADER_SIZE: usize = 16;
60
61#[derive(Clone, Debug)]
67pub struct TileEncoderConfig {
68 pub tile_cols: u32,
70 pub tile_rows: u32,
72 pub threads: usize,
74 pub base_qp: u32,
77}
78
79impl Default for TileEncoderConfig {
80 fn default() -> Self {
81 Self {
82 tile_cols: 1,
83 tile_rows: 1,
84 threads: 0,
85 base_qp: 32,
86 }
87 }
88}
89
90impl TileEncoderConfig {
91 pub fn validate(&self) -> CodecResult<()> {
97 if self.tile_cols == 0 {
98 return Err(CodecError::InvalidParameter(
99 "tile_cols must be at least 1".to_string(),
100 ));
101 }
102 if self.tile_rows == 0 {
103 return Err(CodecError::InvalidParameter(
104 "tile_rows must be at least 1".to_string(),
105 ));
106 }
107 if self.base_qp > 255 {
108 return Err(CodecError::InvalidParameter(
109 "base_qp must be in range 0–255".to_string(),
110 ));
111 }
112 Ok(())
113 }
114
115 #[must_use]
117 pub const fn tile_count(&self) -> u32 {
118 self.tile_cols * self.tile_rows
119 }
120}
121
122#[derive(Clone, Debug)]
128pub struct TileRegionInfo {
129 pub col: u32,
131 pub row: u32,
133 pub x: u32,
135 pub y: u32,
137 pub width: u32,
139 pub height: u32,
141}
142
143impl TileRegionInfo {
144 #[must_use]
146 pub fn raster_index(&self, tile_cols: u32) -> u32 {
147 self.row * tile_cols + self.col
148 }
149
150 #[must_use]
152 pub const fn area(&self) -> u32 {
153 self.width * self.height
154 }
155}
156
157#[derive(Clone, Debug)]
163pub struct EncodedTile {
164 pub tile_col: u32,
166 pub tile_row: u32,
168 pub tile_offset: (u32, u32),
170 pub tile_size: (u32, u32),
172 pub data: Vec<u8>,
174 pub qp: u32,
176}
177
178impl EncodedTile {
179 #[must_use]
181 pub fn raster_index(&self, tile_cols: u32) -> u32 {
182 self.tile_row * tile_cols + self.tile_col
183 }
184}
185
186pub fn encode_tiles_parallel(
207 frame: &[u8],
208 width: u32,
209 height: u32,
210 config: &TileEncoderConfig,
211) -> CodecResult<Vec<Vec<u8>>> {
212 config.validate()?;
213
214 if width == 0 || height == 0 {
215 return Err(CodecError::InvalidParameter(
216 "frame width and height must be non-zero".to_string(),
217 ));
218 }
219
220 let encoder = ParallelTileEncoder::new(width, height, config.clone())?;
221 let split_tiles = encoder.split_frame(frame);
222
223 let encoded: CodecResult<Vec<(u32, Vec<u8>)>> = if config.threads > 0 {
225 let pool = rayon::ThreadPoolBuilder::new()
227 .num_threads(config.threads)
228 .build()
229 .map_err(|e| CodecError::Internal(format!("thread pool error: {}", e)))?;
230
231 pool.install(|| {
232 split_tiles
233 .par_iter()
234 .map(|(region, tile_data)| {
235 let idx = region.raster_index(config.tile_cols);
236 let encoded = encode_single_tile(tile_data, region, config.base_qp)?;
237 Ok((idx, encoded))
238 })
239 .collect()
240 })
241 } else {
242 split_tiles
243 .par_iter()
244 .map(|(region, tile_data)| {
245 let idx = region.raster_index(config.tile_cols);
246 let encoded = encode_single_tile(tile_data, region, config.base_qp)?;
247 Ok((idx, encoded))
248 })
249 .collect()
250 };
251
252 let mut indexed = encoded?;
253 indexed.sort_by_key(|(idx, _)| *idx);
255 Ok(indexed.into_iter().map(|(_, data)| data).collect())
256}
257
258#[derive(Clone, Debug)]
267pub struct ParallelTileEncoder {
268 pub frame_width: u32,
270 pub frame_height: u32,
272 pub config: TileEncoderConfig,
274}
275
276impl ParallelTileEncoder {
277 pub fn new(
284 frame_width: u32,
285 frame_height: u32,
286 config: TileEncoderConfig,
287 ) -> CodecResult<Self> {
288 config.validate()?;
289 if frame_width == 0 || frame_height == 0 {
290 return Err(CodecError::InvalidParameter(
291 "frame width and height must be non-zero".to_string(),
292 ));
293 }
294 Ok(Self {
295 frame_width,
296 frame_height,
297 config,
298 })
299 }
300
301 #[must_use]
306 pub fn split_frame(&self, frame: &[u8]) -> Vec<(TileRegionInfo, Vec<u8>)> {
307 let tile_cols = self.config.tile_cols;
308 let tile_rows = self.config.tile_rows;
309
310 if tile_cols == 0 || tile_rows == 0 {
311 return Vec::new();
312 }
313
314 let base_w = self.frame_width / tile_cols;
315 let rem_w = self.frame_width % tile_cols;
316 let base_h = self.frame_height / tile_rows;
317 let rem_h = self.frame_height % tile_rows;
318
319 let mut result = Vec::with_capacity((tile_rows * tile_cols) as usize);
320
321 for row in 0..tile_rows {
322 let tile_h = if row == tile_rows - 1 {
323 base_h + rem_h
324 } else {
325 base_h
326 };
327 let y_off = row * base_h;
328
329 for col in 0..tile_cols {
330 let tile_w = if col == tile_cols - 1 {
331 base_w + rem_w
332 } else {
333 base_w
334 };
335 let x_off = col * base_w;
336
337 let tile_bytes =
338 extract_luma_region(frame, self.frame_width, x_off, y_off, tile_w, tile_h);
339
340 let region = TileRegionInfo {
341 col,
342 row,
343 x: x_off,
344 y: y_off,
345 width: tile_w,
346 height: tile_h,
347 };
348
349 result.push((region, tile_bytes));
350 }
351 }
352
353 result
354 }
355
356 pub fn encode_frame_parallel(&self, frame: &[u8]) -> CodecResult<Vec<EncodedTile>> {
363 let split = self.split_frame(frame);
364
365 let results: CodecResult<Vec<EncodedTile>> = split
366 .into_par_iter()
367 .map(|(region, tile_data)| {
368 let data = encode_single_tile(&tile_data, ®ion, self.config.base_qp)?;
369 Ok(EncodedTile {
370 tile_col: region.col,
371 tile_row: region.row,
372 tile_offset: (region.x, region.y),
373 tile_size: (region.width, region.height),
374 data,
375 qp: self.config.base_qp,
376 })
377 })
378 .collect();
379
380 let mut tiles = results?;
381 tiles.sort_by_key(|t| t.raster_index(self.config.tile_cols));
382 Ok(tiles)
383 }
384
385 #[must_use]
392 pub fn assemble_encoded(&self, tiles: &[EncodedTile]) -> Vec<u8> {
393 assemble_encoded_tiles(tiles)
394 }
395}
396
397fn extract_luma_region(
403 frame: &[u8],
404 frame_width: u32,
405 x_off: u32,
406 y_off: u32,
407 tile_w: u32,
408 tile_h: u32,
409) -> Vec<u8> {
410 let mut out = Vec::with_capacity((tile_w * tile_h) as usize);
411
412 for row in 0..tile_h {
413 let src_start = ((y_off + row) * frame_width + x_off) as usize;
414 let src_end = src_start + tile_w as usize;
415
416 if src_start >= frame.len() {
417 out.extend(std::iter::repeat(128u8).take(tile_w as usize));
419 } else {
420 let avail_end = src_end.min(frame.len());
421 out.extend_from_slice(&frame[src_start..avail_end]);
422 if avail_end < src_end {
423 out.extend(std::iter::repeat(128u8).take(src_end - avail_end));
424 }
425 }
426 }
427
428 out
429}
430
431fn encode_single_tile(tile_data: &[u8], region: &TileRegionInfo, qp: u32) -> CodecResult<Vec<u8>> {
441 if region.width == 0 || region.height == 0 {
442 return Err(CodecError::InvalidBitstream(format!(
443 "tile ({},{}) has zero dimension: {}×{}",
444 region.col, region.row, region.width, region.height
445 )));
446 }
447
448 let payload_len = (region.width * region.height) as usize;
449 let mut out = Vec::with_capacity(TILE_HEADER_SIZE + payload_len);
450
451 out.extend_from_slice(&TILE_MAGIC);
453 out.extend_from_slice(®ion.width.to_le_bytes());
454 out.extend_from_slice(®ion.height.to_le_bytes());
455 out.extend_from_slice(&qp.to_le_bytes());
456
457 let xor_mask = (qp & 0xFF) as u8;
459 let copy_len = payload_len.min(tile_data.len());
460 for &b in &tile_data[..copy_len] {
461 out.push(b ^ xor_mask);
462 }
463 for _ in copy_len..payload_len {
465 out.push(128u8 ^ xor_mask);
466 }
467
468 Ok(out)
469}
470
471fn assemble_encoded_tiles(tiles: &[EncodedTile]) -> Vec<u8> {
475 if tiles.is_empty() {
476 return Vec::new();
477 }
478
479 let total: usize = tiles
480 .iter()
481 .enumerate()
482 .map(|(i, t)| {
483 if i < tiles.len() - 1 {
484 4 + t.data.len()
485 } else {
486 t.data.len()
487 }
488 })
489 .sum();
490
491 let mut out = Vec::with_capacity(total);
492
493 for (i, tile) in tiles.iter().enumerate() {
494 let is_last = i == tiles.len() - 1;
495 if !is_last {
496 let size = tile.data.len() as u32;
497 out.extend_from_slice(&size.to_le_bytes());
498 }
499 out.extend_from_slice(&tile.data);
500 }
501
502 out
503}
504
505#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn make_frame(width: u32, height: u32, fill: u8) -> Vec<u8> {
516 vec![fill; (width * height) as usize]
517 }
518
519 fn default_config_2x2() -> TileEncoderConfig {
520 TileEncoderConfig {
521 tile_cols: 2,
522 tile_rows: 2,
523 threads: 0,
524 base_qp: 32,
525 }
526 }
527
528 #[test]
531 fn test_config_default_valid() {
532 let cfg = TileEncoderConfig::default();
533 assert!(cfg.validate().is_ok());
534 }
535
536 #[test]
537 fn test_config_tile_count() {
538 let cfg = TileEncoderConfig {
539 tile_cols: 4,
540 tile_rows: 2,
541 ..Default::default()
542 };
543 assert_eq!(cfg.tile_count(), 8);
544 }
545
546 #[test]
549 fn test_new_valid_config() {
550 let cfg = default_config_2x2();
551 let enc = ParallelTileEncoder::new(1920, 1080, cfg);
552 assert!(enc.is_ok());
553 }
554
555 #[test]
556 fn test_new_zero_cols_errors() {
557 let cfg = TileEncoderConfig {
558 tile_cols: 0,
559 tile_rows: 2,
560 threads: 0,
561 base_qp: 32,
562 };
563 let result = ParallelTileEncoder::new(1920, 1080, cfg);
564 assert!(result.is_err(), "zero tile_cols should fail");
565 }
566
567 #[test]
568 fn test_new_zero_rows_errors() {
569 let cfg = TileEncoderConfig {
570 tile_cols: 2,
571 tile_rows: 0,
572 threads: 0,
573 base_qp: 32,
574 };
575 let result = ParallelTileEncoder::new(1920, 1080, cfg);
576 assert!(result.is_err(), "zero tile_rows should fail");
577 }
578
579 #[test]
580 fn test_new_zero_width_errors() {
581 let cfg = default_config_2x2();
582 let result = ParallelTileEncoder::new(0, 1080, cfg);
583 assert!(result.is_err(), "zero width should fail");
584 }
585
586 #[test]
589 fn test_split_frame_tile_count_2x2() {
590 let cfg = default_config_2x2();
591 let enc = ParallelTileEncoder::new(640, 480, cfg).expect("ok");
592 let frame = make_frame(640, 480, 0);
593 let tiles = enc.split_frame(&frame);
594 assert_eq!(tiles.len(), 4, "2×2 grid must yield 4 tiles");
595 }
596
597 #[test]
598 fn test_split_frame_tile_sizes_sum_to_frame() {
599 let cfg = default_config_2x2();
600 let enc = ParallelTileEncoder::new(800, 600, cfg).expect("ok");
601 let frame = make_frame(800, 600, 0);
602 let tiles = enc.split_frame(&frame);
603
604 let row0_width: u32 = tiles
605 .iter()
606 .filter(|(r, _)| r.row == 0)
607 .map(|(r, _)| r.width)
608 .sum();
609 let col0_height: u32 = tiles
610 .iter()
611 .filter(|(r, _)| r.col == 0)
612 .map(|(r, _)| r.height)
613 .sum();
614 assert_eq!(
615 row0_width, 800,
616 "tile widths in row 0 must sum to frame width"
617 );
618 assert_eq!(
619 col0_height, 600,
620 "tile heights in col 0 must sum to frame height"
621 );
622 }
623
624 #[test]
625 fn test_split_frame_non_divisible() {
626 let cfg = TileEncoderConfig {
628 tile_cols: 3,
629 tile_rows: 2,
630 threads: 0,
631 base_qp: 16,
632 };
633 let enc = ParallelTileEncoder::new(1000, 700, cfg).expect("ok");
634 let frame = make_frame(1000, 700, 0);
635 let tiles = enc.split_frame(&frame);
636 assert_eq!(tiles.len(), 6);
637
638 let row0_width: u32 = tiles
639 .iter()
640 .filter(|(r, _)| r.row == 0)
641 .map(|(r, _)| r.width)
642 .sum();
643 let col0_height: u32 = tiles
644 .iter()
645 .filter(|(r, _)| r.col == 0)
646 .map(|(r, _)| r.height)
647 .sum();
648 assert_eq!(row0_width, 1000);
649 assert_eq!(col0_height, 700);
650 }
651
652 #[test]
653 fn test_split_frame_data_length_equals_area() {
654 let cfg = default_config_2x2();
655 let enc = ParallelTileEncoder::new(200, 100, cfg).expect("ok");
656 let frame = make_frame(200, 100, 42);
657 for (region, tile_data) in enc.split_frame(&frame) {
658 let expected = (region.width * region.height) as usize;
659 assert_eq!(
660 tile_data.len(),
661 expected,
662 "tile ({},{}) data length mismatch",
663 region.col,
664 region.row
665 );
666 }
667 }
668
669 #[test]
672 fn test_encode_tiles_parallel_output_count() {
673 let cfg = default_config_2x2();
674 let frame = make_frame(640, 480, 128);
675 let result = encode_tiles_parallel(&frame, 640, 480, &cfg).expect("ok");
676 assert_eq!(result.len(), 4, "must return one Vec<u8> per tile");
677 }
678
679 #[test]
680 fn test_encode_tiles_parallel_output_sizes() {
681 let cfg = default_config_2x2();
682 let frame = make_frame(640, 480, 0);
683 let tiles = encode_tiles_parallel(&frame, 640, 480, &cfg).expect("ok");
684 for tile in &tiles {
685 assert!(
686 tile.len() >= TILE_HEADER_SIZE,
687 "each tile must be at least {} bytes",
688 TILE_HEADER_SIZE
689 );
690 }
691 }
692
693 #[test]
694 fn test_encode_tiles_parallel_single_tile() {
695 let cfg = TileEncoderConfig {
696 tile_cols: 1,
697 tile_rows: 1,
698 threads: 0,
699 base_qp: 0,
700 };
701 let frame = make_frame(320, 240, 77);
702 let tiles = encode_tiles_parallel(&frame, 320, 240, &cfg).expect("ok");
703 assert_eq!(tiles.len(), 1);
704 let payload = &tiles[0][TILE_HEADER_SIZE..];
706 assert!(
707 payload.iter().all(|&b| b == 77),
708 "with qp=0 payload must equal original pixels"
709 );
710 }
711
712 #[test]
713 fn test_encode_tiles_parallel_content_header_magic() {
714 let cfg = default_config_2x2();
715 let frame = make_frame(64, 32, 0);
716 let tiles = encode_tiles_parallel(&frame, 64, 32, &cfg).expect("ok");
717 for tile in &tiles {
718 assert_eq!(
719 &tile[0..4],
720 &TILE_MAGIC,
721 "tile header must start with TILE_MAGIC"
722 );
723 }
724 }
725
726 #[test]
727 fn test_encode_tiles_parallel_header_width_height_encoded() {
728 let cfg = TileEncoderConfig {
729 tile_cols: 1,
730 tile_rows: 1,
731 threads: 0,
732 base_qp: 8,
733 };
734 let frame = make_frame(128, 96, 0);
735 let tiles = encode_tiles_parallel(&frame, 128, 96, &cfg).expect("ok");
736 assert_eq!(tiles.len(), 1);
737 let w = u32::from_le_bytes(tiles[0][4..8].try_into().expect("slice"));
739 let h = u32::from_le_bytes(tiles[0][8..12].try_into().expect("slice"));
740 assert_eq!(w, 128);
741 assert_eq!(h, 96);
742 }
743
744 #[test]
745 fn test_encode_tiles_parallel_zero_cols_errors() {
746 let cfg = TileEncoderConfig {
747 tile_cols: 0,
748 tile_rows: 2,
749 threads: 0,
750 base_qp: 32,
751 };
752 let frame = make_frame(640, 480, 0);
753 let result = encode_tiles_parallel(&frame, 640, 480, &cfg);
754 assert!(result.is_err());
755 }
756
757 #[test]
760 fn test_assemble_encoded_non_empty() {
761 let cfg = default_config_2x2();
762 let enc = ParallelTileEncoder::new(640, 480, cfg).expect("ok");
763 let frame = make_frame(640, 480, 55);
764 let encoded_tiles = enc.encode_frame_parallel(&frame).expect("ok");
765 let assembled = enc.assemble_encoded(&encoded_tiles);
766 assert!(!assembled.is_empty(), "assembled output must not be empty");
767 }
768
769 #[test]
770 fn test_assemble_encoded_single_tile_no_size_prefix() {
771 let cfg = TileEncoderConfig {
773 tile_cols: 1,
774 tile_rows: 1,
775 threads: 0,
776 base_qp: 0,
777 };
778 let enc = ParallelTileEncoder::new(64, 32, cfg).expect("ok");
779 let frame = make_frame(64, 32, 10);
780 let encoded_tiles = enc.encode_frame_parallel(&frame).expect("ok");
781 assert_eq!(encoded_tiles.len(), 1);
782
783 let assembled = enc.assemble_encoded(&encoded_tiles);
784 assert_eq!(assembled.len(), encoded_tiles[0].data.len());
786 }
787
788 #[test]
791 fn test_tile_region_info_fields() {
792 let region = TileRegionInfo {
793 col: 1,
794 row: 2,
795 x: 320,
796 y: 240,
797 width: 320,
798 height: 240,
799 };
800 assert_eq!(region.raster_index(4), 2 * 4 + 1);
801 assert_eq!(region.area(), 320 * 240);
802 }
803}