1#![forbid(unsafe_code)]
69#![allow(clippy::doc_markdown)]
70#![allow(clippy::cast_possible_truncation)]
71#![allow(clippy::cast_sign_loss)]
72#![allow(clippy::cast_precision_loss)]
73
74use crate::error::{CodecError, CodecResult};
75use crate::frame::VideoFrame;
76use rayon::prelude::*;
77use std::sync::Arc;
78
79#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct TileConfig {
89 pub tile_cols: u32,
91 pub tile_rows: u32,
93 pub threads: usize,
95}
96
97impl TileConfig {
98 pub fn new(tile_cols: u32, tile_rows: u32, threads: usize) -> CodecResult<Self> {
106 if tile_cols == 0 || tile_cols > 64 {
107 return Err(CodecError::InvalidParameter(format!(
108 "tile_cols must be 1–64, got {tile_cols}"
109 )));
110 }
111 if tile_rows == 0 || tile_rows > 64 {
112 return Err(CodecError::InvalidParameter(format!(
113 "tile_rows must be 1–64, got {tile_rows}"
114 )));
115 }
116 if tile_cols * tile_rows > 4096 {
117 return Err(CodecError::InvalidParameter(format!(
118 "total tile count {} exceeds 4096",
119 tile_cols * tile_rows
120 )));
121 }
122 Ok(Self {
123 tile_cols,
124 tile_rows,
125 threads,
126 })
127 }
128
129 #[must_use]
131 pub const fn tile_count(&self) -> u32 {
132 self.tile_cols * self.tile_rows
133 }
134
135 #[must_use]
137 pub fn thread_count(&self) -> usize {
138 if self.threads == 0 {
139 rayon::current_num_threads()
140 } else {
141 self.threads
142 }
143 }
144
145 #[must_use]
151 pub fn auto(width: u32, height: u32, threads: usize) -> Self {
152 let t = if threads == 0 {
153 rayon::current_num_threads()
154 } else {
155 threads
156 };
157
158 let aspect = width as f32 / height.max(1) as f32;
160 let target = t.next_power_of_two() as u32;
161
162 let mut cols = ((target as f32 * aspect).sqrt().ceil() as u32)
163 .next_power_of_two()
164 .clamp(1, 64);
165 let mut rows = ((target as f32 / aspect).sqrt().ceil() as u32)
166 .next_power_of_two()
167 .clamp(1, 64);
168
169 while cols > 1 && width / cols < 64 {
171 cols /= 2;
172 }
173 while rows > 1 && height / rows < 64 {
174 rows /= 2;
175 }
176 while cols * rows > 4096 {
178 if cols > rows {
179 cols /= 2;
180 } else {
181 rows /= 2;
182 }
183 }
184
185 Self {
186 tile_cols: cols,
187 tile_rows: rows,
188 threads,
189 }
190 }
191}
192
193impl Default for TileConfig {
194 fn default() -> Self {
196 Self {
197 tile_cols: 1,
198 tile_rows: 1,
199 threads: 0,
200 }
201 }
202}
203
204#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct TileCoord {
211 pub col: u32,
213 pub row: u32,
215 pub x: u32,
217 pub y: u32,
219 pub width: u32,
221 pub height: u32,
223 pub index: u32,
225}
226
227impl TileCoord {
228 #[must_use]
230 pub const fn new(
231 col: u32,
232 row: u32,
233 x: u32,
234 y: u32,
235 width: u32,
236 height: u32,
237 tile_cols: u32,
238 ) -> Self {
239 Self {
240 col,
241 row,
242 x,
243 y,
244 width,
245 height,
246 index: row * tile_cols + col,
247 }
248 }
249
250 #[must_use]
252 pub const fn area(&self) -> u32 {
253 self.width * self.height
254 }
255
256 #[must_use]
258 pub const fn is_left_edge(&self) -> bool {
259 self.col == 0
260 }
261
262 #[must_use]
264 pub const fn is_top_edge(&self) -> bool {
265 self.row == 0
266 }
267}
268
269#[derive(Clone, Debug)]
279pub struct TileResult {
280 pub coord: TileCoord,
282 pub data: Vec<u8>,
284}
285
286impl TileResult {
287 #[must_use]
289 pub fn new(coord: TileCoord, data: Vec<u8>) -> Self {
290 Self { coord, data }
291 }
292
293 #[must_use]
295 pub const fn index(&self) -> u32 {
296 self.coord.index
297 }
298
299 #[must_use]
301 pub fn encoded_size(&self) -> usize {
302 self.data.len()
303 }
304
305 #[must_use]
307 pub fn is_empty(&self) -> bool {
308 self.data.is_empty()
309 }
310}
311
312pub trait TileEncodeOp: Send + Sync {
327 fn encode_tile(
333 &self,
334 frame: &VideoFrame,
335 x: u32,
336 y: u32,
337 width: u32,
338 height: u32,
339 ) -> CodecResult<Vec<u8>>;
340}
341
342pub struct TileEncoder {
375 config: Arc<TileConfig>,
376 frame_width: u32,
377 frame_height: u32,
378 coords: Vec<TileCoord>,
380}
381
382impl TileEncoder {
383 #[must_use]
385 pub fn new(config: TileConfig, frame_width: u32, frame_height: u32) -> Self {
386 let coords = Self::compute_coords(&config, frame_width, frame_height);
387 Self {
388 config: Arc::new(config),
389 frame_width,
390 frame_height,
391 coords,
392 }
393 }
394
395 pub fn encode<O: TileEncodeOp>(
406 &self,
407 frame: &VideoFrame,
408 op: &O,
409 ) -> CodecResult<Vec<TileResult>> {
410 if frame.width != self.frame_width || frame.height != self.frame_height {
411 return Err(CodecError::InvalidParameter(format!(
412 "frame {}×{} does not match encoder {}×{}",
413 frame.width, frame.height, self.frame_width, self.frame_height
414 )));
415 }
416
417 let results: Vec<CodecResult<TileResult>> = self
419 .coords
420 .par_iter()
421 .map(|coord| {
422 let data = op.encode_tile(frame, coord.x, coord.y, coord.width, coord.height)?;
423 Ok(TileResult::new(coord.clone(), data))
424 })
425 .collect();
426
427 let mut tiles = Vec::with_capacity(results.len());
429 for r in results {
430 tiles.push(r?);
431 }
432 tiles.sort_by_key(TileResult::index);
433 Ok(tiles)
434 }
435
436 #[must_use]
438 pub fn config(&self) -> &TileConfig {
439 &self.config
440 }
441
442 #[must_use]
444 pub const fn frame_width(&self) -> u32 {
445 self.frame_width
446 }
447
448 #[must_use]
450 pub const fn frame_height(&self) -> u32 {
451 self.frame_height
452 }
453
454 #[must_use]
456 pub fn coords(&self) -> &[TileCoord] {
457 &self.coords
458 }
459
460 #[must_use]
462 pub fn tile_count(&self) -> usize {
463 self.coords.len()
464 }
465
466 fn compute_coords(config: &TileConfig, fw: u32, fh: u32) -> Vec<TileCoord> {
468 let cols = config.tile_cols;
469 let rows = config.tile_rows;
470 let tw = fw.div_ceil(cols); let th = fh.div_ceil(rows); let mut coords = Vec::with_capacity((cols * rows) as usize);
474 for row in 0..rows {
475 for col in 0..cols {
476 let x = col * tw;
477 let y = row * th;
478 let width = if col == cols - 1 { fw - x } else { tw };
479 let height = if row == rows - 1 { fh - y } else { th };
480 coords.push(TileCoord::new(col, row, x, y, width, height, cols));
481 }
482 }
483 coords
484 }
485}
486
487impl std::fmt::Debug for TileEncoder {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct("TileEncoder")
490 .field("config", &self.config)
491 .field("frame_width", &self.frame_width)
492 .field("frame_height", &self.frame_height)
493 .field("tile_count", &self.tile_count())
494 .finish()
495 }
496}
497
498#[must_use]
521pub fn assemble_tiles(tiles: &[TileResult]) -> Vec<u8> {
522 if tiles.is_empty() {
523 return Vec::new();
524 }
525
526 let total_data: usize = tiles.iter().map(|t| t.encoded_size()).sum();
528 let mut out = Vec::with_capacity(4 + total_data + (tiles.len() - 1) * 4);
529
530 out.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
532
533 for (i, tile) in tiles.iter().enumerate() {
535 let is_last = i == tiles.len() - 1;
536 if !is_last {
537 out.extend_from_slice(&(tile.data.len() as u32).to_le_bytes());
539 }
540 out.extend_from_slice(&tile.data);
541 }
542
543 out
544}
545
546pub fn decode_tile_stream(stream: &[u8]) -> CodecResult<Vec<Vec<u8>>> {
556 if stream.len() < 4 {
557 return Err(CodecError::InvalidBitstream(
558 "tile stream too short for header".to_string(),
559 ));
560 }
561
562 let num_tiles = u32::from_le_bytes([stream[0], stream[1], stream[2], stream[3]]) as usize;
563 if num_tiles == 0 {
564 return Ok(Vec::new());
565 }
566
567 let mut tiles: Vec<Vec<u8>> = Vec::with_capacity(num_tiles);
568 let mut pos = 4usize;
569
570 for i in 0..num_tiles {
571 let is_last = i == num_tiles - 1;
572
573 if is_last {
574 tiles.push(stream[pos..].to_vec());
576 pos = stream.len();
577 } else {
578 if pos + 4 > stream.len() {
579 return Err(CodecError::InvalidBitstream(format!(
580 "tile {i}: stream truncated before size field"
581 )));
582 }
583 let tile_size = u32::from_le_bytes([
584 stream[pos],
585 stream[pos + 1],
586 stream[pos + 2],
587 stream[pos + 3],
588 ]) as usize;
589 pos += 4;
590
591 if pos + tile_size > stream.len() {
592 return Err(CodecError::InvalidBitstream(format!(
593 "tile {i}: declared size {tile_size} exceeds remaining stream bytes"
594 )));
595 }
596 tiles.push(stream[pos..pos + tile_size].to_vec());
597 pos += tile_size;
598 }
599 }
600
601 Ok(tiles)
602}
603
604pub struct RawLumaEncodeOp;
612
613impl TileEncodeOp for RawLumaEncodeOp {
614 fn encode_tile(
615 &self,
616 frame: &VideoFrame,
617 x: u32,
618 y: u32,
619 width: u32,
620 height: u32,
621 ) -> CodecResult<Vec<u8>> {
622 let mut out = Vec::with_capacity((width * height) as usize);
623 if let Some(plane) = frame.planes.first() {
624 for row in y..(y + height) {
625 let start = row as usize * plane.stride + x as usize;
626 let end = start + width as usize;
627 if end <= plane.data.len() {
628 out.extend_from_slice(&plane.data[start..end]);
629 } else {
630 let available = plane.data.len().saturating_sub(start);
632 out.extend_from_slice(&plane.data[start..start + available]);
633 out.resize(out.len() + (width as usize - available), 0);
634 }
635 }
636 }
637 Ok(out)
638 }
639}
640
641pub struct HeaderedTileEncodeOp;
655
656impl TileEncodeOp for HeaderedTileEncodeOp {
657 fn encode_tile(
658 &self,
659 frame: &VideoFrame,
660 x: u32,
661 y: u32,
662 width: u32,
663 height: u32,
664 ) -> CodecResult<Vec<u8>> {
665 let mut out = Vec::with_capacity(14 + (width * height) as usize);
666 out.extend_from_slice(&x.to_le_bytes());
667 out.extend_from_slice(&y.to_le_bytes());
668 out.extend_from_slice(&width.to_le_bytes());
669 out.extend_from_slice(&height.to_le_bytes());
670
671 let raw = RawLumaEncodeOp.encode_tile(frame, x, y, width, height)?;
673 out.extend_from_slice(&raw);
674 Ok(out)
675 }
676}
677
678#[derive(Clone, Debug, Default)]
684pub struct TileEncodeStats {
685 pub total_bytes: usize,
687 pub min_tile_bytes: usize,
689 pub max_tile_bytes: usize,
691 pub mean_tile_bytes: f64,
693 pub tile_count: usize,
695}
696
697impl TileEncodeStats {
698 #[must_use]
702 pub fn from_results(results: &[TileResult]) -> Option<Self> {
703 if results.is_empty() {
704 return None;
705 }
706 let sizes: Vec<usize> = results.iter().map(TileResult::encoded_size).collect();
707 let total: usize = sizes.iter().sum();
708 let min = *sizes.iter().min().unwrap_or(&0);
709 let max = *sizes.iter().max().unwrap_or(&0);
710 Some(Self {
711 total_bytes: total,
712 min_tile_bytes: min,
713 max_tile_bytes: max,
714 mean_tile_bytes: total as f64 / sizes.len() as f64,
715 tile_count: sizes.len(),
716 })
717 }
718
719 #[must_use]
723 pub fn compression_ratio(&self, raw_luma_bytes: usize) -> Option<f64> {
724 if raw_luma_bytes == 0 {
725 return None;
726 }
727 Some(self.total_bytes as f64 / raw_luma_bytes as f64)
728 }
729}
730
731pub trait TileDecodeOp: Send + Sync {
745 fn decode_tile(&self, coord: &TileCoord, data: &[u8]) -> CodecResult<Vec<u8>>;
753}
754
755#[derive(Clone, Debug)]
757pub struct TileDecodeResult {
758 pub coord: TileCoord,
760 pub data: Vec<u8>,
762}
763
764impl TileDecodeResult {
765 #[must_use]
767 pub fn new(coord: TileCoord, data: Vec<u8>) -> Self {
768 Self { coord, data }
769 }
770
771 #[must_use]
773 pub const fn index(&self) -> u32 {
774 self.coord.index
775 }
776
777 #[must_use]
779 pub fn decoded_size(&self) -> usize {
780 self.data.len()
781 }
782
783 #[must_use]
785 pub fn is_empty(&self) -> bool {
786 self.data.is_empty()
787 }
788}
789
790pub fn decode_tiles_parallel(
839 stream: &[u8],
840 op: &(impl TileDecodeOp + ?Sized),
841 encoder: &TileEncoder,
842) -> CodecResult<Vec<TileDecodeResult>> {
843 let tile_bytes = decode_tile_stream(stream)?;
845
846 let coords = encoder.coords();
847 if tile_bytes.len() != coords.len() {
848 return Err(CodecError::InvalidBitstream(format!(
849 "decode_tiles_parallel: stream has {} tiles, expected {}",
850 tile_bytes.len(),
851 coords.len()
852 )));
853 }
854
855 let pairs: Vec<(&TileCoord, &[u8])> = coords
857 .iter()
858 .zip(tile_bytes.iter().map(|v| v.as_slice()))
859 .collect();
860
861 let results: Vec<CodecResult<TileDecodeResult>> = pairs
862 .par_iter()
863 .map(|(coord, data)| {
864 let decoded = op.decode_tile(coord, data)?;
865 Ok(TileDecodeResult::new((*coord).clone(), decoded))
866 })
867 .collect();
868
869 let mut decoded: Vec<TileDecodeResult> = results.into_iter().collect::<CodecResult<_>>()?;
871 decoded.sort_by_key(TileDecodeResult::index);
872 Ok(decoded)
873}
874
875#[cfg(test)]
880mod tests {
881 use super::*;
882 use oximedia_core::PixelFormat;
883
884 fn make_frame(w: u32, h: u32) -> VideoFrame {
887 let mut f = VideoFrame::new(PixelFormat::Yuv420p, w, h);
888 f.allocate();
889 f
890 }
891
892 struct FixedSizeOp(usize);
894
895 impl TileEncodeOp for FixedSizeOp {
896 fn encode_tile(
897 &self,
898 _frame: &VideoFrame,
899 _x: u32,
900 _y: u32,
901 _w: u32,
902 _h: u32,
903 ) -> CodecResult<Vec<u8>> {
904 Ok(vec![0xABu8; self.0])
905 }
906 }
907
908 struct ErrorOp;
910
911 impl TileEncodeOp for ErrorOp {
912 fn encode_tile(
913 &self,
914 _frame: &VideoFrame,
915 _x: u32,
916 _y: u32,
917 _w: u32,
918 _h: u32,
919 ) -> CodecResult<Vec<u8>> {
920 Err(CodecError::InvalidParameter("deliberate error".to_string()))
921 }
922 }
923
924 #[test]
927 fn test_tile_config_default() {
928 let cfg = TileConfig::default();
929 assert_eq!(cfg.tile_cols, 1);
930 assert_eq!(cfg.tile_rows, 1);
931 assert_eq!(cfg.tile_count(), 1);
932 }
933
934 #[test]
935 fn test_tile_config_new_valid() {
936 let cfg = TileConfig::new(4, 2, 8).expect("should succeed");
937 assert_eq!(cfg.tile_cols, 4);
938 assert_eq!(cfg.tile_rows, 2);
939 assert_eq!(cfg.tile_count(), 8);
940 }
941
942 #[test]
943 fn test_tile_config_new_zero_cols() {
944 assert!(TileConfig::new(0, 1, 0).is_err());
945 }
946
947 #[test]
948 fn test_tile_config_new_zero_rows() {
949 assert!(TileConfig::new(1, 0, 0).is_err());
950 }
951
952 #[test]
953 fn test_tile_config_new_too_many_cols() {
954 assert!(TileConfig::new(65, 1, 0).is_err());
955 }
956
957 #[test]
958 fn test_tile_config_new_too_many_rows() {
959 assert!(TileConfig::new(1, 65, 0).is_err());
960 }
961
962 #[test]
963 fn test_tile_config_overflow() {
964 assert!(TileConfig::new(64, 64, 0).is_ok());
966 }
968
969 #[test]
970 fn test_tile_config_auto_wide() {
971 let cfg = TileConfig::auto(3840, 1080, 8);
972 assert!(
973 cfg.tile_cols >= cfg.tile_rows,
974 "wide frame should have more columns"
975 );
976 assert!(cfg.tile_count() >= 1);
977 }
978
979 #[test]
980 fn test_tile_config_auto_tall() {
981 let cfg = TileConfig::auto(1080, 3840, 8);
982 assert!(
983 cfg.tile_rows >= cfg.tile_cols,
984 "tall frame should have more rows"
985 );
986 }
987
988 #[test]
989 fn test_tile_config_auto_single_thread() {
990 let cfg = TileConfig::auto(1920, 1080, 1);
991 assert!(cfg.tile_count() >= 1);
993 }
994
995 #[test]
996 fn test_tile_config_thread_count_auto() {
997 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
998 assert!(cfg.thread_count() >= 1);
999 }
1000
1001 #[test]
1002 fn test_tile_config_thread_count_explicit() {
1003 let cfg = TileConfig::new(1, 1, 4).expect("should succeed");
1004 assert_eq!(cfg.thread_count(), 4);
1005 }
1006
1007 #[test]
1010 fn test_tile_coord_index() {
1011 let c = TileCoord::new(1, 0, 960, 0, 960, 540, 2);
1013 assert_eq!(c.index, 1);
1014 assert_eq!(c.area(), 960 * 540);
1015 assert!(!c.is_left_edge());
1016 assert!(c.is_top_edge());
1017 }
1018
1019 #[test]
1020 fn test_tile_coord_top_left() {
1021 let c = TileCoord::new(0, 0, 0, 0, 480, 270, 4);
1022 assert_eq!(c.index, 0);
1023 assert!(c.is_left_edge());
1024 assert!(c.is_top_edge());
1025 }
1026
1027 #[test]
1030 fn test_encoder_single_tile() {
1031 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
1032 let encoder = TileEncoder::new(cfg, 1920, 1080);
1033 assert_eq!(encoder.tile_count(), 1);
1034
1035 let c = &encoder.coords()[0];
1036 assert_eq!(c.x, 0);
1037 assert_eq!(c.y, 0);
1038 assert_eq!(c.width, 1920);
1039 assert_eq!(c.height, 1080);
1040 }
1041
1042 #[test]
1043 fn test_encoder_2x2_coverage() {
1044 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1045 let encoder = TileEncoder::new(cfg, 1920, 1080);
1046 assert_eq!(encoder.tile_count(), 4);
1047
1048 let mut covered = vec![0u32; 1920 * 1080];
1050 for coord in encoder.coords() {
1051 for row in coord.y..(coord.y + coord.height) {
1052 for col in coord.x..(coord.x + coord.width) {
1053 covered[(row * 1920 + col) as usize] += 1;
1054 }
1055 }
1056 }
1057 assert!(
1058 covered.iter().all(|&c| c == 1),
1059 "some pixels covered ≠ 1 time"
1060 );
1061 }
1062
1063 #[test]
1064 fn test_encoder_4x3_coverage() {
1065 let cfg = TileConfig::new(4, 3, 0).expect("should succeed");
1066 let encoder = TileEncoder::new(cfg, 1280, 720);
1067 assert_eq!(encoder.tile_count(), 12);
1068
1069 let mut total_area: u64 = 0;
1070 for coord in encoder.coords() {
1071 assert!(coord.width > 0 && coord.height > 0, "empty tile");
1072 total_area += u64::from(coord.area());
1073 }
1074 assert_eq!(total_area, 1280 * 720, "total tile area != frame area");
1075 }
1076
1077 #[test]
1078 fn test_encoder_raster_order() {
1079 let cfg = TileConfig::new(3, 2, 0).expect("should succeed");
1080 let encoder = TileEncoder::new(cfg, 1920, 1080);
1081 for (i, coord) in encoder.coords().iter().enumerate() {
1082 assert_eq!(coord.index as usize, i, "coords not in raster order");
1083 }
1084 }
1085
1086 #[test]
1087 fn test_encoder_encode_parallel() {
1088 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1089 let encoder = TileEncoder::new(cfg, 1920, 1080);
1090 let frame = make_frame(1920, 1080);
1091
1092 let results = encoder
1093 .encode(&frame, &FixedSizeOp(64))
1094 .expect("encode should succeed");
1095 assert_eq!(results.len(), 4);
1096 for (i, r) in results.iter().enumerate() {
1098 assert_eq!(r.index() as usize, i);
1099 assert_eq!(r.encoded_size(), 64);
1100 }
1101 }
1102
1103 #[test]
1104 fn test_encoder_encode_error_propagates() {
1105 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1106 let encoder = TileEncoder::new(cfg, 1920, 1080);
1107 let frame = make_frame(1920, 1080);
1108 assert!(encoder.encode(&frame, &ErrorOp).is_err());
1109 }
1110
1111 #[test]
1112 fn test_encoder_wrong_frame_dimensions() {
1113 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1114 let encoder = TileEncoder::new(cfg, 1920, 1080);
1115 let frame = make_frame(1280, 720);
1116 assert!(encoder.encode(&frame, &FixedSizeOp(1)).is_err());
1117 }
1118
1119 #[test]
1122 fn test_assemble_empty() {
1123 assert!(assemble_tiles(&[]).is_empty());
1124 }
1125
1126 #[test]
1127 fn test_assemble_single_tile() {
1128 let coord = TileCoord::new(0, 0, 0, 0, 1920, 1080, 1);
1129 let result = TileResult::new(coord, vec![1u8, 2, 3, 4]);
1130 let stream = assemble_tiles(&[result]);
1131
1132 assert_eq!(stream.len(), 4 + 4);
1134 assert_eq!(
1135 u32::from_le_bytes([stream[0], stream[1], stream[2], stream[3]]),
1136 1
1137 );
1138 }
1139
1140 #[test]
1141 fn test_assemble_decode_roundtrip_two_tiles() {
1142 let payload_a = vec![0xAA; 128];
1143 let payload_b = vec![0xBB; 256];
1144
1145 let ta = TileResult::new(TileCoord::new(0, 0, 0, 0, 960, 540, 2), payload_a.clone());
1146 let tb = TileResult::new(TileCoord::new(1, 0, 960, 0, 960, 540, 2), payload_b.clone());
1147
1148 let stream = assemble_tiles(&[ta, tb]);
1149 let decoded = decode_tile_stream(&stream).expect("should succeed");
1150
1151 assert_eq!(decoded.len(), 2);
1152 assert_eq!(decoded[0], payload_a);
1153 assert_eq!(decoded[1], payload_b);
1154 }
1155
1156 #[test]
1157 fn test_assemble_decode_roundtrip_four_tiles() {
1158 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1159 let encoder = TileEncoder::new(cfg, 640, 480);
1160 let frame = make_frame(640, 480);
1161
1162 let results = encoder
1163 .encode(&frame, &RawLumaEncodeOp)
1164 .expect("encode should succeed");
1165 let stream = assemble_tiles(&results);
1166 let decoded = decode_tile_stream(&stream).expect("should succeed");
1167
1168 assert_eq!(decoded.len(), 4);
1169 for (orig, dec) in results.iter().zip(decoded.iter()) {
1171 assert_eq!(&orig.data, dec, "tile data mismatch after roundtrip");
1172 }
1173 }
1174
1175 #[test]
1176 fn test_decode_tile_stream_truncated_header() {
1177 assert!(decode_tile_stream(&[0, 1]).is_err());
1178 }
1179
1180 #[test]
1181 fn test_decode_tile_stream_truncated_size() {
1182 let stream = [2u8, 0, 0, 0]; assert!(decode_tile_stream(&stream).is_err());
1185 }
1186
1187 #[test]
1188 fn test_decode_tile_stream_truncated_data() {
1189 let mut stream = vec![2u8, 0, 0, 0]; stream.extend_from_slice(&1000u32.to_le_bytes()); stream.extend(vec![0u8; 10]); assert!(decode_tile_stream(&stream).is_err());
1194 }
1195
1196 #[test]
1197 fn test_decode_empty_stream() {
1198 let stream = [0u8, 0, 0, 0];
1200 let decoded = decode_tile_stream(&stream).expect("should succeed");
1201 assert!(decoded.is_empty());
1202 }
1203
1204 #[test]
1207 fn test_raw_luma_op_size() {
1208 let frame = make_frame(320, 240);
1209 let op = RawLumaEncodeOp;
1210 let data = op
1211 .encode_tile(&frame, 0, 0, 320, 240)
1212 .expect("should succeed");
1213 assert_eq!(data.len(), 320 * 240);
1215 }
1216
1217 #[test]
1218 fn test_raw_luma_op_partial_tile() {
1219 let frame = make_frame(100, 50);
1220 let op = RawLumaEncodeOp;
1221 let data = op
1222 .encode_tile(&frame, 0, 0, 50, 25)
1223 .expect("should succeed");
1224 assert_eq!(data.len(), 50 * 25);
1225 }
1226
1227 #[test]
1228 fn test_headered_tile_op_header_content() {
1229 let frame = make_frame(128, 64);
1230 let op = HeaderedTileEncodeOp;
1231 let data = op
1232 .encode_tile(&frame, 32, 16, 64, 32)
1233 .expect("should succeed");
1234
1235 assert!(data.len() >= 16);
1237 let x = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1238 let y = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
1239 let w = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
1240 let h = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
1241
1242 assert_eq!(x, 32);
1243 assert_eq!(y, 16);
1244 assert_eq!(w, 64);
1245 assert_eq!(h, 32);
1246 assert_eq!(data.len(), 16 + 64 * 32);
1248 }
1249
1250 #[test]
1253 fn test_stats_from_empty() {
1254 assert!(TileEncodeStats::from_results(&[]).is_none());
1255 }
1256
1257 #[test]
1258 fn test_stats_from_uniform() {
1259 let cfg = TileConfig::new(4, 2, 0).expect("should succeed");
1260 let encoder = TileEncoder::new(cfg, 1920, 1080);
1261 let frame = make_frame(1920, 1080);
1262
1263 let results = encoder
1264 .encode(&frame, &FixedSizeOp(200))
1265 .expect("encode should succeed");
1266 let stats = TileEncodeStats::from_results(&results).expect("should succeed");
1267
1268 assert_eq!(stats.tile_count, 8);
1269 assert_eq!(stats.total_bytes, 8 * 200);
1270 assert_eq!(stats.min_tile_bytes, 200);
1271 assert_eq!(stats.max_tile_bytes, 200);
1272 assert!((stats.mean_tile_bytes - 200.0).abs() < 1e-9);
1273 }
1274
1275 #[test]
1276 fn test_stats_compression_ratio() {
1277 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
1278 let encoder = TileEncoder::new(cfg, 100, 100);
1279 let frame = make_frame(100, 100);
1280
1281 let results = encoder
1282 .encode(&frame, &FixedSizeOp(500))
1283 .expect("encode should succeed");
1284 let stats = TileEncodeStats::from_results(&results).expect("should succeed");
1285
1286 let ratio = stats.compression_ratio(10000).expect("should succeed");
1288 assert!((ratio - 0.05).abs() < 1e-9);
1289
1290 assert!(stats.compression_ratio(0).is_none());
1291 }
1292
1293 #[test]
1296 fn test_tile_result_metadata() {
1297 let coord = TileCoord::new(2, 1, 640, 360, 320, 180, 4);
1298 let result = TileResult::new(coord.clone(), vec![1, 2, 3]);
1299
1300 assert_eq!(result.index(), 1 * 4 + 2);
1301 assert_eq!(result.encoded_size(), 3);
1302 assert!(!result.is_empty());
1303 }
1304
1305 #[test]
1306 fn test_tile_result_empty() {
1307 let coord = TileCoord::new(0, 0, 0, 0, 10, 10, 1);
1308 let result = TileResult::new(coord, vec![]);
1309 assert!(result.is_empty());
1310 assert_eq!(result.encoded_size(), 0);
1311 }
1312
1313 #[test]
1316 fn test_tile_encoder_debug() {
1317 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1318 let encoder = TileEncoder::new(cfg, 1920, 1080);
1319 let s = format!("{encoder:?}");
1320 assert!(s.contains("TileEncoder"));
1321 assert!(s.contains("1920"));
1322 }
1323}