1#![forbid(unsafe_code)]
68#![allow(clippy::doc_markdown)]
69#![allow(clippy::cast_possible_truncation)]
70#![allow(clippy::cast_sign_loss)]
71#![allow(clippy::cast_precision_loss)]
72
73use crate::error::{CodecError, CodecResult};
74use crate::frame::VideoFrame;
75use rayon::prelude::*;
76use std::sync::Arc;
77
78#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct TileConfig {
88 pub tile_cols: u32,
90 pub tile_rows: u32,
92 pub threads: usize,
94}
95
96impl TileConfig {
97 pub fn new(tile_cols: u32, tile_rows: u32, threads: usize) -> CodecResult<Self> {
105 if tile_cols == 0 || tile_cols > 64 {
106 return Err(CodecError::InvalidParameter(format!(
107 "tile_cols must be 1–64, got {tile_cols}"
108 )));
109 }
110 if tile_rows == 0 || tile_rows > 64 {
111 return Err(CodecError::InvalidParameter(format!(
112 "tile_rows must be 1–64, got {tile_rows}"
113 )));
114 }
115 if tile_cols * tile_rows > 4096 {
116 return Err(CodecError::InvalidParameter(format!(
117 "total tile count {} exceeds 4096",
118 tile_cols * tile_rows
119 )));
120 }
121 Ok(Self {
122 tile_cols,
123 tile_rows,
124 threads,
125 })
126 }
127
128 #[must_use]
130 pub const fn tile_count(&self) -> u32 {
131 self.tile_cols * self.tile_rows
132 }
133
134 #[must_use]
136 pub fn thread_count(&self) -> usize {
137 if self.threads == 0 {
138 rayon::current_num_threads()
139 } else {
140 self.threads
141 }
142 }
143
144 #[must_use]
150 pub fn auto(width: u32, height: u32, threads: usize) -> Self {
151 let t = if threads == 0 {
152 rayon::current_num_threads()
153 } else {
154 threads
155 };
156
157 let aspect = width as f32 / height.max(1) as f32;
159 let target = t.next_power_of_two() as u32;
160
161 let mut cols = ((target as f32 * aspect).sqrt().ceil() as u32)
162 .next_power_of_two()
163 .clamp(1, 64);
164 let mut rows = ((target as f32 / aspect).sqrt().ceil() as u32)
165 .next_power_of_two()
166 .clamp(1, 64);
167
168 while cols > 1 && width / cols < 64 {
170 cols /= 2;
171 }
172 while rows > 1 && height / rows < 64 {
173 rows /= 2;
174 }
175 while cols * rows > 4096 {
177 if cols > rows {
178 cols /= 2;
179 } else {
180 rows /= 2;
181 }
182 }
183
184 Self {
185 tile_cols: cols,
186 tile_rows: rows,
187 threads,
188 }
189 }
190}
191
192impl Default for TileConfig {
193 fn default() -> Self {
195 Self {
196 tile_cols: 1,
197 tile_rows: 1,
198 threads: 0,
199 }
200 }
201}
202
203#[derive(Clone, Debug, PartialEq, Eq)]
209pub struct TileCoord {
210 pub col: u32,
212 pub row: u32,
214 pub x: u32,
216 pub y: u32,
218 pub width: u32,
220 pub height: u32,
222 pub index: u32,
224}
225
226impl TileCoord {
227 #[must_use]
229 pub const fn new(
230 col: u32,
231 row: u32,
232 x: u32,
233 y: u32,
234 width: u32,
235 height: u32,
236 tile_cols: u32,
237 ) -> Self {
238 Self {
239 col,
240 row,
241 x,
242 y,
243 width,
244 height,
245 index: row * tile_cols + col,
246 }
247 }
248
249 #[must_use]
251 pub const fn area(&self) -> u32 {
252 self.width * self.height
253 }
254
255 #[must_use]
257 pub const fn is_left_edge(&self) -> bool {
258 self.col == 0
259 }
260
261 #[must_use]
263 pub const fn is_top_edge(&self) -> bool {
264 self.row == 0
265 }
266}
267
268#[derive(Clone, Debug)]
278pub struct TileResult {
279 pub coord: TileCoord,
281 pub data: Vec<u8>,
283}
284
285impl TileResult {
286 #[must_use]
288 pub fn new(coord: TileCoord, data: Vec<u8>) -> Self {
289 Self { coord, data }
290 }
291
292 #[must_use]
294 pub const fn index(&self) -> u32 {
295 self.coord.index
296 }
297
298 #[must_use]
300 pub fn encoded_size(&self) -> usize {
301 self.data.len()
302 }
303
304 #[must_use]
306 pub fn is_empty(&self) -> bool {
307 self.data.is_empty()
308 }
309}
310
311pub trait TileEncodeOp: Send + Sync {
326 fn encode_tile(
332 &self,
333 frame: &VideoFrame,
334 x: u32,
335 y: u32,
336 width: u32,
337 height: u32,
338 ) -> CodecResult<Vec<u8>>;
339}
340
341pub struct TileEncoder {
373 config: Arc<TileConfig>,
374 frame_width: u32,
375 frame_height: u32,
376 coords: Vec<TileCoord>,
378}
379
380impl TileEncoder {
381 #[must_use]
383 pub fn new(config: TileConfig, frame_width: u32, frame_height: u32) -> Self {
384 let coords = Self::compute_coords(&config, frame_width, frame_height);
385 Self {
386 config: Arc::new(config),
387 frame_width,
388 frame_height,
389 coords,
390 }
391 }
392
393 pub fn encode<O: TileEncodeOp>(
404 &self,
405 frame: &VideoFrame,
406 op: &O,
407 ) -> CodecResult<Vec<TileResult>> {
408 if frame.width != self.frame_width || frame.height != self.frame_height {
409 return Err(CodecError::InvalidParameter(format!(
410 "frame {}×{} does not match encoder {}×{}",
411 frame.width, frame.height, self.frame_width, self.frame_height
412 )));
413 }
414
415 let results: Vec<CodecResult<TileResult>> = self
417 .coords
418 .par_iter()
419 .map(|coord| {
420 let data = op.encode_tile(frame, coord.x, coord.y, coord.width, coord.height)?;
421 Ok(TileResult::new(coord.clone(), data))
422 })
423 .collect();
424
425 let mut tiles = Vec::with_capacity(results.len());
427 for r in results {
428 tiles.push(r?);
429 }
430 tiles.sort_by_key(TileResult::index);
431 Ok(tiles)
432 }
433
434 #[must_use]
436 pub fn config(&self) -> &TileConfig {
437 &self.config
438 }
439
440 #[must_use]
442 pub const fn frame_width(&self) -> u32 {
443 self.frame_width
444 }
445
446 #[must_use]
448 pub const fn frame_height(&self) -> u32 {
449 self.frame_height
450 }
451
452 #[must_use]
454 pub fn coords(&self) -> &[TileCoord] {
455 &self.coords
456 }
457
458 #[must_use]
460 pub fn tile_count(&self) -> usize {
461 self.coords.len()
462 }
463
464 fn compute_coords(config: &TileConfig, fw: u32, fh: u32) -> Vec<TileCoord> {
466 let cols = config.tile_cols;
467 let rows = config.tile_rows;
468 let tw = fw.div_ceil(cols); let th = fh.div_ceil(rows); let mut coords = Vec::with_capacity((cols * rows) as usize);
472 for row in 0..rows {
473 for col in 0..cols {
474 let x = col * tw;
475 let y = row * th;
476 let width = if col == cols - 1 { fw - x } else { tw };
477 let height = if row == rows - 1 { fh - y } else { th };
478 coords.push(TileCoord::new(col, row, x, y, width, height, cols));
479 }
480 }
481 coords
482 }
483}
484
485impl std::fmt::Debug for TileEncoder {
486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487 f.debug_struct("TileEncoder")
488 .field("config", &self.config)
489 .field("frame_width", &self.frame_width)
490 .field("frame_height", &self.frame_height)
491 .field("tile_count", &self.tile_count())
492 .finish()
493 }
494}
495
496#[must_use]
519pub fn assemble_tiles(tiles: &[TileResult]) -> Vec<u8> {
520 if tiles.is_empty() {
521 return Vec::new();
522 }
523
524 let total_data: usize = tiles.iter().map(|t| t.encoded_size()).sum();
526 let mut out = Vec::with_capacity(4 + total_data + (tiles.len() - 1) * 4);
527
528 out.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
530
531 for (i, tile) in tiles.iter().enumerate() {
533 let is_last = i == tiles.len() - 1;
534 if !is_last {
535 out.extend_from_slice(&(tile.data.len() as u32).to_le_bytes());
537 }
538 out.extend_from_slice(&tile.data);
539 }
540
541 out
542}
543
544pub fn decode_tile_stream(stream: &[u8]) -> CodecResult<Vec<Vec<u8>>> {
554 if stream.len() < 4 {
555 return Err(CodecError::InvalidBitstream(
556 "tile stream too short for header".to_string(),
557 ));
558 }
559
560 let num_tiles = u32::from_le_bytes([stream[0], stream[1], stream[2], stream[3]]) as usize;
561 if num_tiles == 0 {
562 return Ok(Vec::new());
563 }
564
565 let mut tiles: Vec<Vec<u8>> = Vec::with_capacity(num_tiles);
566 let mut pos = 4usize;
567
568 for i in 0..num_tiles {
569 let is_last = i == num_tiles - 1;
570
571 if is_last {
572 tiles.push(stream[pos..].to_vec());
574 pos = stream.len();
575 } else {
576 if pos + 4 > stream.len() {
577 return Err(CodecError::InvalidBitstream(format!(
578 "tile {i}: stream truncated before size field"
579 )));
580 }
581 let tile_size = u32::from_le_bytes([
582 stream[pos],
583 stream[pos + 1],
584 stream[pos + 2],
585 stream[pos + 3],
586 ]) as usize;
587 pos += 4;
588
589 if pos + tile_size > stream.len() {
590 return Err(CodecError::InvalidBitstream(format!(
591 "tile {i}: declared size {tile_size} exceeds remaining stream bytes"
592 )));
593 }
594 tiles.push(stream[pos..pos + tile_size].to_vec());
595 pos += tile_size;
596 }
597 }
598
599 Ok(tiles)
600}
601
602pub struct RawLumaEncodeOp;
610
611impl TileEncodeOp for RawLumaEncodeOp {
612 fn encode_tile(
613 &self,
614 frame: &VideoFrame,
615 x: u32,
616 y: u32,
617 width: u32,
618 height: u32,
619 ) -> CodecResult<Vec<u8>> {
620 let mut out = Vec::with_capacity((width * height) as usize);
621 if let Some(plane) = frame.planes.first() {
622 for row in y..(y + height) {
623 let start = row as usize * plane.stride + x as usize;
624 let end = start + width as usize;
625 if end <= plane.data.len() {
626 out.extend_from_slice(&plane.data[start..end]);
627 } else {
628 let available = plane.data.len().saturating_sub(start);
630 out.extend_from_slice(&plane.data[start..start + available]);
631 out.resize(out.len() + (width as usize - available), 0);
632 }
633 }
634 }
635 Ok(out)
636 }
637}
638
639pub struct HeaderedTileEncodeOp;
653
654impl TileEncodeOp for HeaderedTileEncodeOp {
655 fn encode_tile(
656 &self,
657 frame: &VideoFrame,
658 x: u32,
659 y: u32,
660 width: u32,
661 height: u32,
662 ) -> CodecResult<Vec<u8>> {
663 let mut out = Vec::with_capacity(14 + (width * height) as usize);
664 out.extend_from_slice(&x.to_le_bytes());
665 out.extend_from_slice(&y.to_le_bytes());
666 out.extend_from_slice(&width.to_le_bytes());
667 out.extend_from_slice(&height.to_le_bytes());
668
669 let raw = RawLumaEncodeOp.encode_tile(frame, x, y, width, height)?;
671 out.extend_from_slice(&raw);
672 Ok(out)
673 }
674}
675
676#[derive(Clone, Debug, Default)]
682pub struct TileEncodeStats {
683 pub total_bytes: usize,
685 pub min_tile_bytes: usize,
687 pub max_tile_bytes: usize,
689 pub mean_tile_bytes: f64,
691 pub tile_count: usize,
693}
694
695impl TileEncodeStats {
696 #[must_use]
700 pub fn from_results(results: &[TileResult]) -> Option<Self> {
701 if results.is_empty() {
702 return None;
703 }
704 let sizes: Vec<usize> = results.iter().map(TileResult::encoded_size).collect();
705 let total: usize = sizes.iter().sum();
706 let min = *sizes.iter().min().unwrap_or(&0);
707 let max = *sizes.iter().max().unwrap_or(&0);
708 Some(Self {
709 total_bytes: total,
710 min_tile_bytes: min,
711 max_tile_bytes: max,
712 mean_tile_bytes: total as f64 / sizes.len() as f64,
713 tile_count: sizes.len(),
714 })
715 }
716
717 #[must_use]
721 pub fn compression_ratio(&self, raw_luma_bytes: usize) -> Option<f64> {
722 if raw_luma_bytes == 0 {
723 return None;
724 }
725 Some(self.total_bytes as f64 / raw_luma_bytes as f64)
726 }
727}
728
729#[cfg(test)]
734mod tests {
735 use super::*;
736 use oximedia_core::PixelFormat;
737
738 fn make_frame(w: u32, h: u32) -> VideoFrame {
741 let mut f = VideoFrame::new(PixelFormat::Yuv420p, w, h);
742 f.allocate();
743 f
744 }
745
746 struct FixedSizeOp(usize);
748
749 impl TileEncodeOp for FixedSizeOp {
750 fn encode_tile(
751 &self,
752 _frame: &VideoFrame,
753 _x: u32,
754 _y: u32,
755 _w: u32,
756 _h: u32,
757 ) -> CodecResult<Vec<u8>> {
758 Ok(vec![0xABu8; self.0])
759 }
760 }
761
762 struct ErrorOp;
764
765 impl TileEncodeOp for ErrorOp {
766 fn encode_tile(
767 &self,
768 _frame: &VideoFrame,
769 _x: u32,
770 _y: u32,
771 _w: u32,
772 _h: u32,
773 ) -> CodecResult<Vec<u8>> {
774 Err(CodecError::InvalidParameter("deliberate error".to_string()))
775 }
776 }
777
778 #[test]
781 fn test_tile_config_default() {
782 let cfg = TileConfig::default();
783 assert_eq!(cfg.tile_cols, 1);
784 assert_eq!(cfg.tile_rows, 1);
785 assert_eq!(cfg.tile_count(), 1);
786 }
787
788 #[test]
789 fn test_tile_config_new_valid() {
790 let cfg = TileConfig::new(4, 2, 8).expect("should succeed");
791 assert_eq!(cfg.tile_cols, 4);
792 assert_eq!(cfg.tile_rows, 2);
793 assert_eq!(cfg.tile_count(), 8);
794 }
795
796 #[test]
797 fn test_tile_config_new_zero_cols() {
798 assert!(TileConfig::new(0, 1, 0).is_err());
799 }
800
801 #[test]
802 fn test_tile_config_new_zero_rows() {
803 assert!(TileConfig::new(1, 0, 0).is_err());
804 }
805
806 #[test]
807 fn test_tile_config_new_too_many_cols() {
808 assert!(TileConfig::new(65, 1, 0).is_err());
809 }
810
811 #[test]
812 fn test_tile_config_new_too_many_rows() {
813 assert!(TileConfig::new(1, 65, 0).is_err());
814 }
815
816 #[test]
817 fn test_tile_config_overflow() {
818 assert!(TileConfig::new(64, 64, 0).is_ok());
820 }
822
823 #[test]
824 fn test_tile_config_auto_wide() {
825 let cfg = TileConfig::auto(3840, 1080, 8);
826 assert!(
827 cfg.tile_cols >= cfg.tile_rows,
828 "wide frame should have more columns"
829 );
830 assert!(cfg.tile_count() >= 1);
831 }
832
833 #[test]
834 fn test_tile_config_auto_tall() {
835 let cfg = TileConfig::auto(1080, 3840, 8);
836 assert!(
837 cfg.tile_rows >= cfg.tile_cols,
838 "tall frame should have more rows"
839 );
840 }
841
842 #[test]
843 fn test_tile_config_auto_single_thread() {
844 let cfg = TileConfig::auto(1920, 1080, 1);
845 assert!(cfg.tile_count() >= 1);
847 }
848
849 #[test]
850 fn test_tile_config_thread_count_auto() {
851 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
852 assert!(cfg.thread_count() >= 1);
853 }
854
855 #[test]
856 fn test_tile_config_thread_count_explicit() {
857 let cfg = TileConfig::new(1, 1, 4).expect("should succeed");
858 assert_eq!(cfg.thread_count(), 4);
859 }
860
861 #[test]
864 fn test_tile_coord_index() {
865 let c = TileCoord::new(1, 0, 960, 0, 960, 540, 2);
867 assert_eq!(c.index, 1);
868 assert_eq!(c.area(), 960 * 540);
869 assert!(!c.is_left_edge());
870 assert!(c.is_top_edge());
871 }
872
873 #[test]
874 fn test_tile_coord_top_left() {
875 let c = TileCoord::new(0, 0, 0, 0, 480, 270, 4);
876 assert_eq!(c.index, 0);
877 assert!(c.is_left_edge());
878 assert!(c.is_top_edge());
879 }
880
881 #[test]
884 fn test_encoder_single_tile() {
885 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
886 let encoder = TileEncoder::new(cfg, 1920, 1080);
887 assert_eq!(encoder.tile_count(), 1);
888
889 let c = &encoder.coords()[0];
890 assert_eq!(c.x, 0);
891 assert_eq!(c.y, 0);
892 assert_eq!(c.width, 1920);
893 assert_eq!(c.height, 1080);
894 }
895
896 #[test]
897 fn test_encoder_2x2_coverage() {
898 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
899 let encoder = TileEncoder::new(cfg, 1920, 1080);
900 assert_eq!(encoder.tile_count(), 4);
901
902 let mut covered = vec![0u32; 1920 * 1080];
904 for coord in encoder.coords() {
905 for row in coord.y..(coord.y + coord.height) {
906 for col in coord.x..(coord.x + coord.width) {
907 covered[(row * 1920 + col) as usize] += 1;
908 }
909 }
910 }
911 assert!(
912 covered.iter().all(|&c| c == 1),
913 "some pixels covered ≠ 1 time"
914 );
915 }
916
917 #[test]
918 fn test_encoder_4x3_coverage() {
919 let cfg = TileConfig::new(4, 3, 0).expect("should succeed");
920 let encoder = TileEncoder::new(cfg, 1280, 720);
921 assert_eq!(encoder.tile_count(), 12);
922
923 let mut total_area: u64 = 0;
924 for coord in encoder.coords() {
925 assert!(coord.width > 0 && coord.height > 0, "empty tile");
926 total_area += u64::from(coord.area());
927 }
928 assert_eq!(total_area, 1280 * 720, "total tile area != frame area");
929 }
930
931 #[test]
932 fn test_encoder_raster_order() {
933 let cfg = TileConfig::new(3, 2, 0).expect("should succeed");
934 let encoder = TileEncoder::new(cfg, 1920, 1080);
935 for (i, coord) in encoder.coords().iter().enumerate() {
936 assert_eq!(coord.index as usize, i, "coords not in raster order");
937 }
938 }
939
940 #[test]
941 fn test_encoder_encode_parallel() {
942 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
943 let encoder = TileEncoder::new(cfg, 1920, 1080);
944 let frame = make_frame(1920, 1080);
945
946 let results = encoder
947 .encode(&frame, &FixedSizeOp(64))
948 .expect("encode should succeed");
949 assert_eq!(results.len(), 4);
950 for (i, r) in results.iter().enumerate() {
952 assert_eq!(r.index() as usize, i);
953 assert_eq!(r.encoded_size(), 64);
954 }
955 }
956
957 #[test]
958 fn test_encoder_encode_error_propagates() {
959 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
960 let encoder = TileEncoder::new(cfg, 1920, 1080);
961 let frame = make_frame(1920, 1080);
962 assert!(encoder.encode(&frame, &ErrorOp).is_err());
963 }
964
965 #[test]
966 fn test_encoder_wrong_frame_dimensions() {
967 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
968 let encoder = TileEncoder::new(cfg, 1920, 1080);
969 let frame = make_frame(1280, 720);
970 assert!(encoder.encode(&frame, &FixedSizeOp(1)).is_err());
971 }
972
973 #[test]
976 fn test_assemble_empty() {
977 assert!(assemble_tiles(&[]).is_empty());
978 }
979
980 #[test]
981 fn test_assemble_single_tile() {
982 let coord = TileCoord::new(0, 0, 0, 0, 1920, 1080, 1);
983 let result = TileResult::new(coord, vec![1u8, 2, 3, 4]);
984 let stream = assemble_tiles(&[result]);
985
986 assert_eq!(stream.len(), 4 + 4);
988 assert_eq!(
989 u32::from_le_bytes([stream[0], stream[1], stream[2], stream[3]]),
990 1
991 );
992 }
993
994 #[test]
995 fn test_assemble_decode_roundtrip_two_tiles() {
996 let payload_a = vec![0xAA; 128];
997 let payload_b = vec![0xBB; 256];
998
999 let ta = TileResult::new(TileCoord::new(0, 0, 0, 0, 960, 540, 2), payload_a.clone());
1000 let tb = TileResult::new(TileCoord::new(1, 0, 960, 0, 960, 540, 2), payload_b.clone());
1001
1002 let stream = assemble_tiles(&[ta, tb]);
1003 let decoded = decode_tile_stream(&stream).expect("should succeed");
1004
1005 assert_eq!(decoded.len(), 2);
1006 assert_eq!(decoded[0], payload_a);
1007 assert_eq!(decoded[1], payload_b);
1008 }
1009
1010 #[test]
1011 fn test_assemble_decode_roundtrip_four_tiles() {
1012 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1013 let encoder = TileEncoder::new(cfg, 640, 480);
1014 let frame = make_frame(640, 480);
1015
1016 let results = encoder
1017 .encode(&frame, &RawLumaEncodeOp)
1018 .expect("encode should succeed");
1019 let stream = assemble_tiles(&results);
1020 let decoded = decode_tile_stream(&stream).expect("should succeed");
1021
1022 assert_eq!(decoded.len(), 4);
1023 for (orig, dec) in results.iter().zip(decoded.iter()) {
1025 assert_eq!(&orig.data, dec, "tile data mismatch after roundtrip");
1026 }
1027 }
1028
1029 #[test]
1030 fn test_decode_tile_stream_truncated_header() {
1031 assert!(decode_tile_stream(&[0, 1]).is_err());
1032 }
1033
1034 #[test]
1035 fn test_decode_tile_stream_truncated_size() {
1036 let stream = [2u8, 0, 0, 0]; assert!(decode_tile_stream(&stream).is_err());
1039 }
1040
1041 #[test]
1042 fn test_decode_tile_stream_truncated_data() {
1043 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());
1048 }
1049
1050 #[test]
1051 fn test_decode_empty_stream() {
1052 let stream = [0u8, 0, 0, 0];
1054 let decoded = decode_tile_stream(&stream).expect("should succeed");
1055 assert!(decoded.is_empty());
1056 }
1057
1058 #[test]
1061 fn test_raw_luma_op_size() {
1062 let frame = make_frame(320, 240);
1063 let op = RawLumaEncodeOp;
1064 let data = op
1065 .encode_tile(&frame, 0, 0, 320, 240)
1066 .expect("should succeed");
1067 assert_eq!(data.len(), 320 * 240);
1069 }
1070
1071 #[test]
1072 fn test_raw_luma_op_partial_tile() {
1073 let frame = make_frame(100, 50);
1074 let op = RawLumaEncodeOp;
1075 let data = op
1076 .encode_tile(&frame, 0, 0, 50, 25)
1077 .expect("should succeed");
1078 assert_eq!(data.len(), 50 * 25);
1079 }
1080
1081 #[test]
1082 fn test_headered_tile_op_header_content() {
1083 let frame = make_frame(128, 64);
1084 let op = HeaderedTileEncodeOp;
1085 let data = op
1086 .encode_tile(&frame, 32, 16, 64, 32)
1087 .expect("should succeed");
1088
1089 assert!(data.len() >= 16);
1091 let x = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1092 let y = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
1093 let w = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
1094 let h = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
1095
1096 assert_eq!(x, 32);
1097 assert_eq!(y, 16);
1098 assert_eq!(w, 64);
1099 assert_eq!(h, 32);
1100 assert_eq!(data.len(), 16 + 64 * 32);
1102 }
1103
1104 #[test]
1107 fn test_stats_from_empty() {
1108 assert!(TileEncodeStats::from_results(&[]).is_none());
1109 }
1110
1111 #[test]
1112 fn test_stats_from_uniform() {
1113 let cfg = TileConfig::new(4, 2, 0).expect("should succeed");
1114 let encoder = TileEncoder::new(cfg, 1920, 1080);
1115 let frame = make_frame(1920, 1080);
1116
1117 let results = encoder
1118 .encode(&frame, &FixedSizeOp(200))
1119 .expect("encode should succeed");
1120 let stats = TileEncodeStats::from_results(&results).expect("should succeed");
1121
1122 assert_eq!(stats.tile_count, 8);
1123 assert_eq!(stats.total_bytes, 8 * 200);
1124 assert_eq!(stats.min_tile_bytes, 200);
1125 assert_eq!(stats.max_tile_bytes, 200);
1126 assert!((stats.mean_tile_bytes - 200.0).abs() < 1e-9);
1127 }
1128
1129 #[test]
1130 fn test_stats_compression_ratio() {
1131 let cfg = TileConfig::new(1, 1, 0).expect("should succeed");
1132 let encoder = TileEncoder::new(cfg, 100, 100);
1133 let frame = make_frame(100, 100);
1134
1135 let results = encoder
1136 .encode(&frame, &FixedSizeOp(500))
1137 .expect("encode should succeed");
1138 let stats = TileEncodeStats::from_results(&results).expect("should succeed");
1139
1140 let ratio = stats.compression_ratio(10000).expect("should succeed");
1142 assert!((ratio - 0.05).abs() < 1e-9);
1143
1144 assert!(stats.compression_ratio(0).is_none());
1145 }
1146
1147 #[test]
1150 fn test_tile_result_metadata() {
1151 let coord = TileCoord::new(2, 1, 640, 360, 320, 180, 4);
1152 let result = TileResult::new(coord.clone(), vec![1, 2, 3]);
1153
1154 assert_eq!(result.index(), 1 * 4 + 2);
1155 assert_eq!(result.encoded_size(), 3);
1156 assert!(!result.is_empty());
1157 }
1158
1159 #[test]
1160 fn test_tile_result_empty() {
1161 let coord = TileCoord::new(0, 0, 0, 0, 10, 10, 1);
1162 let result = TileResult::new(coord, vec![]);
1163 assert!(result.is_empty());
1164 assert_eq!(result.encoded_size(), 0);
1165 }
1166
1167 #[test]
1170 fn test_tile_encoder_debug() {
1171 let cfg = TileConfig::new(2, 2, 0).expect("should succeed");
1172 let encoder = TileEncoder::new(cfg, 1920, 1080);
1173 let s = format!("{encoder:?}");
1174 assert!(s.contains("TileEncoder"));
1175 assert!(s.contains("1920"));
1176 }
1177}