Skip to main content

gravita_renderer/
lib.rs

1//! Minimal 2D rendering helpers shared across examples.
2//!
3//! This crate intentionally stays lightweight: it provides
4//! simple drawing primitives on top of a raw RGBA frame buffer.
5//! Higher-level scene logic lives in the individual examples.
6//!
7//! # Design Philosophy
8//!
9//! - **CPU-based**: All rendering happens on the CPU, no GPU required
10//! - **Frame buffer**: Works with any `&mut [u8]` RGBA buffer
11//! - **Coordinate system**: Y increases upward (world space), caller handles conversion
12//!
13//! # Available Primitives
14//!
15//! - [`clear`] — Fill the entire frame with a solid color
16//! - [`draw_circle`] — Draw a filled circle
17//! - [`draw_line`] — Draw a 1-pixel wide line (Bresenham's algorithm)
18//! - [`draw_axes`] — Draw X/Y debug axes
19//!
20//! # Example
21//!
22//! ```ignore
23//! use gravita_renderer::{clear, draw_circle, draw_line};
24//! use gravita_math::Vec2;
25//!
26//! let mut frame = vec![0u8; 800 * 600 * 4];
27//!
28//! // Clear to dark blue
29//! clear(&mut frame, [0x20, 0x20, 0x40, 0xff]);
30//!
31//! // Draw a white circle
32//! draw_circle(&mut frame, Vec2::new(400.0, 300.0), 50.0, [0xff; 4], 800, 600);
33//! ```
34
35#![warn(missing_docs)]
36
37use gravita_math::Vec2;
38
39/// Clear the entire frame to a solid color (RGBA).
40pub fn clear(frame: &mut [u8], color: [u8; 4]) {
41    for px in frame.chunks_exact_mut(4) {
42        px.copy_from_slice(&color);
43    }
44}
45
46/// Draw a filled circle into the frame.
47pub fn draw_circle(
48    frame: &mut [u8],
49    center: Vec2,
50    radius: f32,
51    color: [u8; 4],
52    width: u32,
53    height: u32,
54) {
55    let cx = center.x.round() as i32;
56    let cy = center.y.round() as i32;
57    let r = radius.round() as i32;
58
59    for y in (cy - r).max(0)..(cy + r).min(height as i32) {
60        for x in (cx - r).max(0)..(cx + r).min(width as i32) {
61            let dx = x - cx;
62            let dy = y - cy;
63            if dx * dx + dy * dy <= r * r {
64                let idx = ((y as u32 * width + x as u32) * 4) as usize;
65                if idx + 3 < frame.len() {
66                    frame[idx..idx + 4].copy_from_slice(&color);
67                }
68            }
69        }
70    }
71}
72
73/// Draw a 1‑pixel wide line between two points using Bresenham's algorithm.
74pub fn draw_line(
75    frame: &mut [u8],
76    start: Vec2,
77    end: Vec2,
78    color: [u8; 4],
79    width: u32,
80    height: u32,
81) {
82    let x0 = start.x.round() as i32;
83    let y0 = start.y.round() as i32;
84    let x1 = end.x.round() as i32;
85    let y1 = end.y.round() as i32;
86
87    let dx = (x1 - x0).abs();
88    let dy = (y1 - y0).abs();
89    let sx = if x0 < x1 { 1 } else { -1 };
90    let sy = if y0 < y1 { 1 } else { -1 };
91    let mut err = dx - dy;
92
93    let mut x = x0;
94    let mut y = y0;
95
96    loop {
97        if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
98            let idx = ((y as u32 * width + x as u32) * 4) as usize;
99            if idx + 3 < frame.len() {
100                frame[idx..idx + 4].copy_from_slice(&color);
101            }
102        }
103
104        if x == x1 && y == y1 {
105            break;
106        }
107
108        let e2 = 2 * err;
109        if e2 > -dy {
110            err -= dy;
111            x += sx;
112        }
113        if e2 < dx {
114            err += dx;
115            y += sy;
116        }
117    }
118}
119
120/// Draw simple X/Y axes crossing at `origin`, useful for debugging.
121pub fn draw_axes(frame: &mut [u8], origin: Vec2, color: [u8; 4], width: u32, height: u32) {
122    let origin = Vec2::new(origin.x, origin.y);
123    // Horizontal axis
124    draw_line(
125        frame,
126        Vec2::new(0.0, origin.y),
127        Vec2::new(width as f32, origin.y),
128        color,
129        width,
130        height,
131    );
132    // Vertical axis
133    draw_line(
134        frame,
135        Vec2::new(origin.x, 0.0),
136        Vec2::new(origin.x, height as f32),
137        color,
138        width,
139        height,
140    );
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    const WIDTH: u32 = 100;
148    const HEIGHT: u32 = 100;
149
150    fn create_frame() -> Vec<u8> {
151        vec![0u8; (WIDTH * HEIGHT * 4) as usize]
152    }
153
154    fn pixel_at(frame: &[u8], x: u32, y: u32) -> [u8; 4] {
155        let idx = ((y * WIDTH + x) * 4) as usize;
156        [frame[idx], frame[idx + 1], frame[idx + 2], frame[idx + 3]]
157    }
158
159    // =========================================================================
160    // Clear
161    // =========================================================================
162
163    #[test]
164    fn clear_fills_entire_frame() {
165        let mut frame = create_frame();
166        let color = [0x12, 0x34, 0x56, 0x78];
167        clear(&mut frame, color);
168
169        // Check first pixel
170        assert_eq!(pixel_at(&frame, 0, 0), color);
171        // Check last pixel
172        assert_eq!(pixel_at(&frame, WIDTH - 1, HEIGHT - 1), color);
173        // Check middle pixel
174        assert_eq!(pixel_at(&frame, 50, 50), color);
175    }
176
177    #[test]
178    fn clear_with_black() {
179        let mut frame = create_frame();
180        // Fill with white first
181        clear(&mut frame, [0xFF, 0xFF, 0xFF, 0xFF]);
182        // Then clear to black
183        clear(&mut frame, [0x00, 0x00, 0x00, 0xFF]);
184        assert_eq!(pixel_at(&frame, 50, 50), [0x00, 0x00, 0x00, 0xFF]);
185    }
186
187    // =========================================================================
188    // Draw Circle
189    // =========================================================================
190
191    #[test]
192    fn draw_circle_at_center() {
193        let mut frame = create_frame();
194        let color = [0xFF, 0x00, 0x00, 0xFF];
195        draw_circle(
196            &mut frame,
197            Vec2::new(50.0, 50.0),
198            10.0,
199            color,
200            WIDTH,
201            HEIGHT,
202        );
203
204        // Center should be colored
205        assert_eq!(pixel_at(&frame, 50, 50), color);
206    }
207
208    #[test]
209    fn draw_circle_does_not_affect_outside() {
210        let mut frame = create_frame();
211        let color = [0xFF, 0x00, 0x00, 0xFF];
212        draw_circle(&mut frame, Vec2::new(50.0, 50.0), 5.0, color, WIDTH, HEIGHT);
213
214        // Far corner should still be black (unaffected)
215        assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
216    }
217
218    #[test]
219    fn draw_circle_clipped_at_edge() {
220        let mut frame = create_frame();
221        let color = [0xFF, 0x00, 0x00, 0xFF];
222        // Circle partially outside left edge
223        draw_circle(
224            &mut frame,
225            Vec2::new(-5.0, 50.0),
226            10.0,
227            color,
228            WIDTH,
229            HEIGHT,
230        );
231
232        // Should not panic and should affect edge pixels
233        assert_eq!(pixel_at(&frame, 0, 50), color);
234    }
235
236    #[test]
237    fn draw_circle_completely_outside_does_not_panic() {
238        let mut frame = create_frame();
239        let color = [0xFF, 0x00, 0x00, 0xFF];
240        // Circle completely outside
241        draw_circle(
242            &mut frame,
243            Vec2::new(-100.0, -100.0),
244            10.0,
245            color,
246            WIDTH,
247            HEIGHT,
248        );
249        // Should not panic - frame should be unchanged
250        assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
251    }
252
253    #[test]
254    fn draw_circle_zero_radius() {
255        let mut frame = create_frame();
256        let color = [0xFF, 0x00, 0x00, 0xFF];
257        // Zero radius circle (essentially a point)
258        draw_circle(&mut frame, Vec2::new(50.0, 50.0), 0.0, color, WIDTH, HEIGHT);
259        // Should not panic
260    }
261
262    // =========================================================================
263    // Draw Line
264    // =========================================================================
265
266    #[test]
267    fn draw_line_horizontal() {
268        let mut frame = create_frame();
269        let color = [0x00, 0xFF, 0x00, 0xFF];
270        draw_line(
271            &mut frame,
272            Vec2::new(10.0, 50.0),
273            Vec2::new(90.0, 50.0),
274            color,
275            WIDTH,
276            HEIGHT,
277        );
278
279        // Start point
280        assert_eq!(pixel_at(&frame, 10, 50), color);
281        // End point
282        assert_eq!(pixel_at(&frame, 90, 50), color);
283        // Middle point
284        assert_eq!(pixel_at(&frame, 50, 50), color);
285    }
286
287    #[test]
288    fn draw_line_vertical() {
289        let mut frame = create_frame();
290        let color = [0x00, 0xFF, 0x00, 0xFF];
291        draw_line(
292            &mut frame,
293            Vec2::new(50.0, 10.0),
294            Vec2::new(50.0, 90.0),
295            color,
296            WIDTH,
297            HEIGHT,
298        );
299
300        assert_eq!(pixel_at(&frame, 50, 10), color);
301        assert_eq!(pixel_at(&frame, 50, 90), color);
302        assert_eq!(pixel_at(&frame, 50, 50), color);
303    }
304
305    #[test]
306    fn draw_line_diagonal() {
307        let mut frame = create_frame();
308        let color = [0x00, 0xFF, 0x00, 0xFF];
309        draw_line(
310            &mut frame,
311            Vec2::new(10.0, 10.0),
312            Vec2::new(90.0, 90.0),
313            color,
314            WIDTH,
315            HEIGHT,
316        );
317
318        // Start and end should be colored
319        assert_eq!(pixel_at(&frame, 10, 10), color);
320        assert_eq!(pixel_at(&frame, 90, 90), color);
321    }
322
323    #[test]
324    fn draw_line_clipped_does_not_panic() {
325        let mut frame = create_frame();
326        let color = [0x00, 0xFF, 0x00, 0xFF];
327        // Line extending outside frame
328        draw_line(
329            &mut frame,
330            Vec2::new(-50.0, 50.0),
331            Vec2::new(150.0, 50.0),
332            color,
333            WIDTH,
334            HEIGHT,
335        );
336        // Should not panic
337        assert_eq!(pixel_at(&frame, 0, 50), color);
338        assert_eq!(pixel_at(&frame, 99, 50), color);
339    }
340
341    #[test]
342    fn draw_line_completely_outside_does_not_panic() {
343        let mut frame = create_frame();
344        let color = [0x00, 0xFF, 0x00, 0xFF];
345        draw_line(
346            &mut frame,
347            Vec2::new(-50.0, -50.0),
348            Vec2::new(-10.0, -10.0),
349            color,
350            WIDTH,
351            HEIGHT,
352        );
353        // Should not panic - frame unchanged
354        assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
355    }
356
357    #[test]
358    fn draw_line_single_point() {
359        let mut frame = create_frame();
360        let color = [0x00, 0xFF, 0x00, 0xFF];
361        // Start == end
362        draw_line(
363            &mut frame,
364            Vec2::new(50.0, 50.0),
365            Vec2::new(50.0, 50.0),
366            color,
367            WIDTH,
368            HEIGHT,
369        );
370        assert_eq!(pixel_at(&frame, 50, 50), color);
371    }
372
373    // =========================================================================
374    // Draw Axes
375    // =========================================================================
376
377    #[test]
378    fn draw_axes_draws_cross() {
379        let mut frame = create_frame();
380        let color = [0x00, 0x00, 0xFF, 0xFF];
381        draw_axes(&mut frame, Vec2::new(50.0, 50.0), color, WIDTH, HEIGHT);
382
383        // Check horizontal axis
384        assert_eq!(pixel_at(&frame, 0, 50), color);
385        assert_eq!(pixel_at(&frame, 99, 50), color);
386        // Check vertical axis
387        assert_eq!(pixel_at(&frame, 50, 0), color);
388        assert_eq!(pixel_at(&frame, 50, 99), color);
389    }
390
391    #[test]
392    fn draw_axes_at_origin() {
393        let mut frame = create_frame();
394        let color = [0x00, 0x00, 0xFF, 0xFF];
395        draw_axes(&mut frame, Vec2::new(0.0, 0.0), color, WIDTH, HEIGHT);
396        // Should not panic
397    }
398
399    // =========================================================================
400    // Visual Output Correctness Tests
401    // =========================================================================
402
403    /// Count how many pixels have a specific color
404    fn count_pixels_with_color(frame: &[u8], color: [u8; 4]) -> usize {
405        frame.chunks_exact(4).filter(|px| px == &color).count()
406    }
407
408    /// Check if a pixel is colored (non-zero)
409    #[allow(dead_code)]
410    fn is_pixel_colored(frame: &[u8], x: u32, y: u32, width: u32) -> bool {
411        let idx = ((y * width + x) * 4) as usize;
412        frame[idx] != 0 || frame[idx + 1] != 0 || frame[idx + 2] != 0 || frame[idx + 3] != 0
413    }
414
415    #[test]
416    fn visual_circle_fills_approximate_area() {
417        let mut frame = create_frame();
418        let color = [0xFF, 0x00, 0x00, 0xFF];
419        let radius = 10.0;
420        draw_circle(
421            &mut frame,
422            Vec2::new(50.0, 50.0),
423            radius,
424            color,
425            WIDTH,
426            HEIGHT,
427        );
428
429        let filled_pixels = count_pixels_with_color(&frame, color);
430        // Theoretical area: π * r² ≈ 314.159
431        // Due to rasterization, actual count will differ but should be close
432        let expected_area = std::f32::consts::PI * radius * radius;
433        let tolerance = expected_area * 0.15; // 15% tolerance for rasterization
434
435        assert!(
436            (filled_pixels as f32 - expected_area).abs() < tolerance,
437            "Circle area mismatch: got {filled_pixels} pixels, expected ~{expected_area:.0} \
438             (±{tolerance:.0})"
439        );
440    }
441
442    #[test]
443    fn visual_circle_is_symmetric() {
444        let mut frame = create_frame();
445        let color = [0xFF, 0x00, 0x00, 0xFF];
446        draw_circle(
447            &mut frame,
448            Vec2::new(50.0, 50.0),
449            15.0,
450            color,
451            WIDTH,
452            HEIGHT,
453        );
454
455        // Check horizontal symmetry (exclude boundary at radius=15 due to rasterization)
456        for offset in 1..=14 {
457            let left = pixel_at(&frame, 50 - offset, 50);
458            let right = pixel_at(&frame, 50 + offset, 50);
459            assert_eq!(left, right, "Horizontal asymmetry at offset {offset}");
460        }
461
462        // Check vertical symmetry (exclude boundary)
463        for offset in 1..=14 {
464            let up = pixel_at(&frame, 50, 50 - offset);
465            let down = pixel_at(&frame, 50, 50 + offset);
466            assert_eq!(up, down, "Vertical asymmetry at offset {offset}");
467        }
468    }
469
470    #[test]
471    fn visual_circle_boundary_is_correct() {
472        let mut frame = create_frame();
473        let color = [0xFF, 0x00, 0x00, 0xFF];
474        let radius = 10.0;
475        draw_circle(
476            &mut frame,
477            Vec2::new(50.0, 50.0),
478            radius,
479            color,
480            WIDTH,
481            HEIGHT,
482        );
483
484        // Pixels just inside radius should be colored
485        assert_eq!(pixel_at(&frame, 50, 41), color); // Just inside top
486        assert_eq!(pixel_at(&frame, 50, 59), color); // Just inside bottom
487        assert_eq!(pixel_at(&frame, 41, 50), color); // Just inside left
488        assert_eq!(pixel_at(&frame, 59, 50), color); // Just inside right
489
490        // Pixels outside radius should NOT be colored
491        assert_eq!(pixel_at(&frame, 50, 38), [0, 0, 0, 0]); // Outside top
492        assert_eq!(pixel_at(&frame, 50, 62), [0, 0, 0, 0]); // Outside bottom
493    }
494
495    #[test]
496    fn visual_line_is_continuous() {
497        let mut frame = create_frame();
498        let color = [0x00, 0xFF, 0x00, 0xFF];
499        draw_line(
500            &mut frame,
501            Vec2::new(10.0, 10.0),
502            Vec2::new(90.0, 90.0),
503            color,
504            WIDTH,
505            HEIGHT,
506        );
507
508        // For a 45° diagonal, Bresenham should produce a continuous line
509        // Check that no pixel along the expected path is uncolored
510        let mut colored_count = 0;
511        for i in 10..=90 {
512            // The diagonal should color pixels at approximately (i, i)
513            // Due to Bresenham, check if either (i, i) or adjacent pixel is colored
514            if pixel_at(&frame, i, i) == color {
515                colored_count += 1;
516            }
517        }
518
519        // At least 80% of expected points should be directly on the line
520        assert!(
521            colored_count >= 65,
522            "Line should be continuous: only {colored_count} of 81 pixels colored"
523        );
524    }
525
526    #[test]
527    fn visual_line_no_gaps_horizontal() {
528        let mut frame = create_frame();
529        let color = [0x00, 0xFF, 0x00, 0xFF];
530        draw_line(
531            &mut frame,
532            Vec2::new(10.0, 50.0),
533            Vec2::new(90.0, 50.0),
534            color,
535            WIDTH,
536            HEIGHT,
537        );
538
539        // Horizontal line should have no gaps
540        for x in 10..=90 {
541            assert_eq!(
542                pixel_at(&frame, x, 50),
543                color,
544                "Gap in horizontal line at x={x}"
545            );
546        }
547    }
548
549    #[test]
550    fn visual_line_no_gaps_vertical() {
551        let mut frame = create_frame();
552        let color = [0x00, 0xFF, 0x00, 0xFF];
553        draw_line(
554            &mut frame,
555            Vec2::new(50.0, 10.0),
556            Vec2::new(50.0, 90.0),
557            color,
558            WIDTH,
559            HEIGHT,
560        );
561
562        // Vertical line should have no gaps
563        for y in 10..=90 {
564            assert_eq!(
565                pixel_at(&frame, 50, y),
566                color,
567                "Gap in vertical line at y={y}"
568            );
569        }
570    }
571
572    #[test]
573    fn visual_line_steep_slope_continuous() {
574        let mut frame = create_frame();
575        let color = [0x00, 0xFF, 0x00, 0xFF];
576        // Steep slope (more vertical than horizontal)
577        draw_line(
578            &mut frame,
579            Vec2::new(45.0, 10.0),
580            Vec2::new(55.0, 90.0),
581            color,
582            WIDTH,
583            HEIGHT,
584        );
585
586        // Count colored pixels to ensure line is drawn
587        let filled = count_pixels_with_color(&frame, color);
588        // Line length ≈ sqrt((55-45)² + (90-10)²) ≈ 80.6
589        assert!(
590            filled >= 75,
591            "Steep line should have ~80 pixels, got {filled}"
592        );
593    }
594
595    #[test]
596    fn visual_color_channels_independent() {
597        let mut frame = create_frame();
598
599        // Draw with pure red
600        draw_circle(
601            &mut frame,
602            Vec2::new(25.0, 50.0),
603            5.0,
604            [0xFF, 0x00, 0x00, 0xFF],
605            WIDTH,
606            HEIGHT,
607        );
608        // Draw with pure green
609        draw_circle(
610            &mut frame,
611            Vec2::new(50.0, 50.0),
612            5.0,
613            [0x00, 0xFF, 0x00, 0xFF],
614            WIDTH,
615            HEIGHT,
616        );
617        // Draw with pure blue
618        draw_circle(
619            &mut frame,
620            Vec2::new(75.0, 50.0),
621            5.0,
622            [0x00, 0x00, 0xFF, 0xFF],
623            WIDTH,
624            HEIGHT,
625        );
626
627        // Verify each circle has correct color
628        let red_px = pixel_at(&frame, 25, 50);
629        let green_px = pixel_at(&frame, 50, 50);
630        let blue_px = pixel_at(&frame, 75, 50);
631
632        assert_eq!(red_px, [0xFF, 0x00, 0x00, 0xFF], "Red channel incorrect");
633        assert_eq!(
634            green_px,
635            [0x00, 0xFF, 0x00, 0xFF],
636            "Green channel incorrect"
637        );
638        assert_eq!(blue_px, [0x00, 0x00, 0xFF, 0xFF], "Blue channel incorrect");
639    }
640
641    #[test]
642    fn visual_alpha_channel_preserved() {
643        let mut frame = create_frame();
644        let semi_transparent = [0xFF, 0x00, 0x00, 0x80]; // 50% alpha
645        draw_circle(
646            &mut frame,
647            Vec2::new(50.0, 50.0),
648            10.0,
649            semi_transparent,
650            WIDTH,
651            HEIGHT,
652        );
653
654        let px = pixel_at(&frame, 50, 50);
655        assert_eq!(px[3], 0x80, "Alpha channel not preserved");
656    }
657
658    #[test]
659    fn visual_overdraw_replaces_pixels() {
660        let mut frame = create_frame();
661
662        // Draw red circle first
663        draw_circle(
664            &mut frame,
665            Vec2::new(50.0, 50.0),
666            20.0,
667            [0xFF, 0x00, 0x00, 0xFF],
668            WIDTH,
669            HEIGHT,
670        );
671        // Draw smaller blue circle on top
672        draw_circle(
673            &mut frame,
674            Vec2::new(50.0, 50.0),
675            10.0,
676            [0x00, 0x00, 0xFF, 0xFF],
677            WIDTH,
678            HEIGHT,
679        );
680
681        // Center should be blue (overwritten)
682        assert_eq!(pixel_at(&frame, 50, 50), [0x00, 0x00, 0xFF, 0xFF]);
683        // Outer ring should still be red
684        assert_eq!(pixel_at(&frame, 50, 35), [0xFF, 0x00, 0x00, 0xFF]);
685    }
686
687    #[test]
688    fn visual_multiple_shapes_composite_correctly() {
689        let mut frame = create_frame();
690        let bg = [0x20, 0x20, 0x20, 0xFF];
691        let circle_color = [0xFF, 0x00, 0x00, 0xFF];
692        let line_color = [0x00, 0xFF, 0x00, 0xFF];
693
694        clear(&mut frame, bg);
695        draw_circle(
696            &mut frame,
697            Vec2::new(50.0, 50.0),
698            30.0,
699            circle_color,
700            WIDTH,
701            HEIGHT,
702        );
703        draw_line(
704            &mut frame,
705            Vec2::new(0.0, 50.0),
706            Vec2::new(99.0, 50.0),
707            line_color,
708            WIDTH,
709            HEIGHT,
710        );
711
712        // Line should overwrite circle where they intersect
713        assert_eq!(pixel_at(&frame, 50, 50), line_color);
714        // Circle should be visible where line doesn't cross
715        assert_eq!(pixel_at(&frame, 50, 30), circle_color);
716        // Background should be visible outside both
717        assert_eq!(pixel_at(&frame, 5, 5), bg);
718    }
719
720    #[test]
721    fn visual_frame_buffer_integrity() {
722        let mut frame = create_frame();
723        let original_len = frame.len();
724
725        // Draw various shapes
726        clear(&mut frame, [0x10, 0x20, 0x30, 0xFF]);
727        draw_circle(
728            &mut frame,
729            Vec2::new(50.0, 50.0),
730            25.0,
731            [0xFF, 0x00, 0x00, 0xFF],
732            WIDTH,
733            HEIGHT,
734        );
735        draw_line(
736            &mut frame,
737            Vec2::new(0.0, 0.0),
738            Vec2::new(99.0, 99.0),
739            [0x00, 0xFF, 0x00, 0xFF],
740            WIDTH,
741            HEIGHT,
742        );
743        draw_axes(
744            &mut frame,
745            Vec2::new(50.0, 50.0),
746            [0x00, 0x00, 0xFF, 0xFF],
747            WIDTH,
748            HEIGHT,
749        );
750
751        // Frame buffer should maintain its size
752        assert_eq!(
753            frame.len(),
754            original_len,
755            "Frame buffer size changed during rendering"
756        );
757
758        // All pixels should have exactly 4 bytes (RGBA)
759        for (i, chunk) in frame.chunks_exact(4).enumerate() {
760            assert_eq!(chunk.len(), 4, "Pixel {i} should have exactly 4 bytes");
761        }
762    }
763}