⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/video/
framebuffer.rs

1//! Video frame buffer operations
2//!
3//! Provides scaling (nearest-neighbor, bilinear), pixel format conversion,
4//! color space conversion (YUV <-> RGB via BT.601), alpha blending, and
5//! framebuffer blitting. All math is integer-only (no FPU).
6
7#![allow(clippy::upper_case_acronyms)]
8#![allow(dead_code)]
9
10use super::VideoFrame;
11use crate::graphics::PixelFormat;
12
13// ---------------------------------------------------------------------------
14// Scale mode
15// ---------------------------------------------------------------------------
16
17/// Scaling algorithm.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(crate) enum ScaleMode {
20    /// Point sampling -- fastest, blocky.
21    NearestNeighbor,
22    /// Fixed-point 8.8 bilinear interpolation -- smoother.
23    Bilinear,
24}
25
26// ---------------------------------------------------------------------------
27// Color space
28// ---------------------------------------------------------------------------
29
30/// Color space descriptor (for future pipeline use).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub(crate) enum ColorSpace {
33    SRGB,
34    LinearRGB,
35    YUV420,
36    YUV422,
37}
38
39// ---------------------------------------------------------------------------
40// Scaling
41// ---------------------------------------------------------------------------
42
43/// Scale a `VideoFrame` to the requested dimensions.
44///
45/// Source pixels are read through `get_pixel()` so any pixel format is
46/// supported.  The output frame uses the same `PixelFormat` as the source.
47pub(crate) fn scale_frame(
48    src: &VideoFrame,
49    dst_width: u32,
50    dst_height: u32,
51    mode: ScaleMode,
52) -> VideoFrame {
53    if dst_width == 0 || dst_height == 0 || src.width == 0 || src.height == 0 {
54        return VideoFrame::new(dst_width, dst_height, src.format);
55    }
56
57    let mut dst = VideoFrame::new(dst_width, dst_height, src.format);
58
59    match mode {
60        ScaleMode::NearestNeighbor => {
61            for dy in 0..dst_height {
62                let sy = (dy as u64 * src.height as u64 / dst_height as u64) as u32;
63                let sy = sy.min(src.height - 1);
64                for dx in 0..dst_width {
65                    let sx = (dx as u64 * src.width as u64 / dst_width as u64) as u32;
66                    let sx = sx.min(src.width - 1);
67                    let (r, g, b, a) = src.get_pixel(sx, sy);
68                    dst.set_pixel(dx, dy, r, g, b, a);
69                }
70            }
71        }
72        ScaleMode::Bilinear => {
73            // Fixed-point 8.8 bilinear interpolation.
74            // scale_x_fp = src_width * 256 / dst_width  (8.8 step)
75            let scale_x_fp = (src.width as u64 * 256) / dst_width as u64;
76            let scale_y_fp = (src.height as u64 * 256) / dst_height as u64;
77
78            for dy in 0..dst_height {
79                let src_y_fp = (dy as u64 * scale_y_fp) as u32;
80                let sy0 = (src_y_fp >> 8).min(src.height - 1);
81                let sy1 = (sy0 + 1).min(src.height - 1);
82                let fy = src_y_fp & 0xFF; // fractional part 0..255
83
84                for dx in 0..dst_width {
85                    let src_x_fp = (dx as u64 * scale_x_fp) as u32;
86                    let sx0 = (src_x_fp >> 8).min(src.width - 1);
87                    let sx1 = (sx0 + 1).min(src.width - 1);
88                    let fx = src_x_fp & 0xFF;
89
90                    // Fetch four neighbouring pixels
91                    let (r00, g00, b00, a00) = src.get_pixel(sx0, sy0);
92                    let (r10, g10, b10, a10) = src.get_pixel(sx1, sy0);
93                    let (r01, g01, b01, a01) = src.get_pixel(sx0, sy1);
94                    let (r11, g11, b11, a11) = src.get_pixel(sx1, sy1);
95
96                    // Bilinear weights (all in 0..255)
97                    let inv_fx = 256 - fx;
98                    let inv_fy = 256 - fy;
99
100                    let w00 = inv_fx * inv_fy; // max 256*256 = 65536
101                    let w10 = fx * inv_fy;
102                    let w01 = inv_fx * fy;
103                    let w11 = fx * fy;
104
105                    let r = ((r00 as u32 * w00
106                        + r10 as u32 * w10
107                        + r01 as u32 * w01
108                        + r11 as u32 * w11)
109                        >> 16) as u8;
110                    let g = ((g00 as u32 * w00
111                        + g10 as u32 * w10
112                        + g01 as u32 * w01
113                        + g11 as u32 * w11)
114                        >> 16) as u8;
115                    let b = ((b00 as u32 * w00
116                        + b10 as u32 * w10
117                        + b01 as u32 * w01
118                        + b11 as u32 * w11)
119                        >> 16) as u8;
120                    let a = ((a00 as u32 * w00
121                        + a10 as u32 * w10
122                        + a01 as u32 * w01
123                        + a11 as u32 * w11)
124                        >> 16) as u8;
125
126                    dst.set_pixel(dx, dy, r, g, b, a);
127                }
128            }
129        }
130    }
131
132    dst
133}
134
135// ---------------------------------------------------------------------------
136// Pixel format conversion
137// ---------------------------------------------------------------------------
138
139/// Convert a frame from one pixel format to another.
140///
141/// Pixels are read with `get_pixel()` (format-aware) and written with
142/// `set_pixel()`, so any combination of source/destination formats works.
143pub(crate) fn convert_pixel_format(src: &VideoFrame, dst_format: PixelFormat) -> VideoFrame {
144    let mut dst = VideoFrame::new(src.width, src.height, dst_format);
145    for y in 0..src.height {
146        for x in 0..src.width {
147            let (r, g, b, a) = src.get_pixel(x, y);
148            dst.set_pixel(x, y, r, g, b, a);
149        }
150    }
151    dst
152}
153
154// ---------------------------------------------------------------------------
155// Framebuffer blitting
156// ---------------------------------------------------------------------------
157
158/// Blit a `VideoFrame` onto a raw framebuffer (BGRA u32 layout).
159///
160/// The frame is placed at `(x, y)` and clipped to `(fb_width, fb_height)`.
161/// `fb_addr` must point to valid, writeable memory of at least
162/// `fb_stride * fb_height` bytes.
163///
164/// # Safety
165///
166/// The caller must ensure `fb_addr` points to a valid writable region of
167/// at least `fb_stride * fb_height` bytes.
168pub(crate) unsafe fn blit_to_framebuffer(
169    frame: &VideoFrame,
170    fb_addr: usize,
171    fb_width: u32,
172    fb_height: u32,
173    fb_stride: u32,
174    x: u32,
175    y: u32,
176) {
177    if frame.width == 0 || frame.height == 0 {
178        return;
179    }
180
181    let fb_ptr = fb_addr as *mut u8;
182
183    // Clip region
184    let clip_x_start = x;
185    let clip_y_start = y;
186    let clip_x_end = (x + frame.width).min(fb_width);
187    let clip_y_end = (y + frame.height).min(fb_height);
188
189    if clip_x_start >= clip_x_end || clip_y_start >= clip_y_end {
190        return;
191    }
192
193    for fy in 0..(clip_y_end - clip_y_start) {
194        let dst_y = clip_y_start + fy;
195        let dst_row_offset = (dst_y as usize) * (fb_stride as usize);
196
197        for fx in 0..(clip_x_end - clip_x_start) {
198            let (r, g, b, a) = frame.get_pixel(fx, fy);
199            let dst_x = clip_x_start + fx;
200            // Framebuffer is BGRA u32 (native VeridianOS format)
201            let dst_off = dst_row_offset + (dst_x as usize) * 4;
202
203            if a == 0xFF {
204                // Opaque -- direct write
205                // SAFETY: dst_off is within the framebuffer region (clipped above).
206                let dst = fb_ptr.add(dst_off);
207                *dst = b;
208                *dst.add(1) = g;
209                *dst.add(2) = r;
210                *dst.add(3) = 0xFF;
211            } else if a > 0 {
212                // Alpha blend with existing framebuffer content
213                // SAFETY: dst_off is within the framebuffer region (clipped above).
214                let dst = fb_ptr.add(dst_off);
215                let bg_b = *dst;
216                let bg_g = *dst.add(1);
217                let bg_r = *dst.add(2);
218                let (br, bg, bb) = alpha_blend(r, g, b, a, bg_r, bg_g, bg_b);
219                *dst = bb;
220                *dst.add(1) = bg;
221                *dst.add(2) = br;
222                *dst.add(3) = 0xFF;
223            }
224            // a == 0: fully transparent, skip
225        }
226    }
227}
228
229/// Blit a `VideoFrame` onto a u32 slice buffer (BGRA layout).
230///
231/// Safe variant that works with Rust slices instead of raw pointers.
232pub(crate) fn blit_to_buffer(
233    frame: &VideoFrame,
234    buffer: &mut [u32],
235    buf_width: u32,
236    buf_height: u32,
237    x: u32,
238    y: u32,
239) {
240    if frame.width == 0 || frame.height == 0 {
241        return;
242    }
243
244    let clip_x_end = (x + frame.width).min(buf_width);
245    let clip_y_end = (y + frame.height).min(buf_height);
246
247    if x >= clip_x_end || y >= clip_y_end {
248        return;
249    }
250
251    for fy in 0..(clip_y_end - y) {
252        let dst_y = y + fy;
253        for fx in 0..(clip_x_end - x) {
254            let dst_x = x + fx;
255            let idx = (dst_y as usize) * (buf_width as usize) + (dst_x as usize);
256            if idx >= buffer.len() {
257                continue;
258            }
259
260            let (r, g, b, a) = frame.get_pixel(fx, fy);
261            if a == 0xFF {
262                buffer[idx] = 0xFF00_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
263            } else if a > 0 {
264                let existing = buffer[idx];
265                let bg_r = ((existing >> 16) & 0xFF) as u8;
266                let bg_g = ((existing >> 8) & 0xFF) as u8;
267                let bg_b = (existing & 0xFF) as u8;
268                let (br, bg, bb) = alpha_blend(r, g, b, a, bg_r, bg_g, bg_b);
269                buffer[idx] = 0xFF00_0000 | ((br as u32) << 16) | ((bg as u32) << 8) | (bb as u32);
270            }
271        }
272    }
273}
274
275// ---------------------------------------------------------------------------
276// YUV <-> RGB  (BT.601)
277// ---------------------------------------------------------------------------
278
279/// Convert YUV to RGB using BT.601 integer coefficients.
280///
281/// ```text
282/// R = Y + 1.402 * (V - 128)     ~  Y + (359 * (V-128)) >> 8
283/// G = Y - 0.344 * (U - 128)
284///       - 0.714 * (V - 128)     ~  Y - (88*(U-128) + 183*(V-128)) >> 8
285/// B = Y + 1.772 * (U - 128)     ~  Y + (454 * (U-128)) >> 8
286/// ```
287pub(crate) fn yuv_to_rgb(y: u8, u: u8, v: u8) -> (u8, u8, u8) {
288    let y = y as i32;
289    let cb = u as i32 - 128;
290    let cr = v as i32 - 128;
291
292    let r = y + ((359 * cr) >> 8);
293    let g = y - ((88 * cb + 183 * cr) >> 8);
294    let b = y + ((454 * cb) >> 8);
295
296    (clamp_u8(r), clamp_u8(g), clamp_u8(b))
297}
298
299/// Convert RGB to YUV using BT.601 integer coefficients.
300///
301/// ```text
302/// Y  =  0.299*R + 0.587*G + 0.114*B   ~ (77*R + 150*G + 29*B) >> 8
303/// U  = -0.169*R - 0.331*G + 0.500*B + 128  ~ ((-43*R - 85*G + 128*B) >> 8) + 128
304/// V  =  0.500*R - 0.419*G - 0.081*B + 128  ~ ((128*R - 107*G - 21*B) >> 8) + 128
305/// ```
306pub(crate) fn rgb_to_yuv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
307    let ri = r as i32;
308    let gi = g as i32;
309    let bi = b as i32;
310
311    let y = (77 * ri + 150 * gi + 29 * bi) >> 8;
312    let u = ((-43 * ri - 85 * gi + 128 * bi) >> 8) + 128;
313    let v = ((128 * ri - 107 * gi - 21 * bi) >> 8) + 128;
314
315    (clamp_u8(y), clamp_u8(u), clamp_u8(v))
316}
317
318// ---------------------------------------------------------------------------
319// Alpha blending
320// ---------------------------------------------------------------------------
321
322/// Alpha-blend a source pixel over a destination pixel.
323///
324/// Uses integer math: `result = (src * alpha + dst * (255 - alpha)) / 255`.
325/// This is the standard "over" compositing operation.
326pub(crate) fn alpha_blend(
327    src_r: u8,
328    src_g: u8,
329    src_b: u8,
330    src_a: u8,
331    dst_r: u8,
332    dst_g: u8,
333    dst_b: u8,
334) -> (u8, u8, u8) {
335    let a = src_a as u16;
336    let inv_a = 255 - a;
337
338    let r = ((src_r as u16 * a + dst_r as u16 * inv_a) / 255) as u8;
339    let g = ((src_g as u16 * a + dst_g as u16 * inv_a) / 255) as u8;
340    let b = ((src_b as u16 * a + dst_b as u16 * inv_a) / 255) as u8;
341
342    (r, g, b)
343}
344
345// ---------------------------------------------------------------------------
346// Helpers
347// ---------------------------------------------------------------------------
348
349/// Clamp an i32 to the 0..=255 range and return as u8.
350#[inline]
351fn clamp_u8(val: i32) -> u8 {
352    if val < 0 {
353        0
354    } else if val > 255 {
355        255
356    } else {
357        val as u8
358    }
359}
360
361// ---------------------------------------------------------------------------
362// Tests
363// ---------------------------------------------------------------------------
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_nearest_neighbor_identity() {
371        let mut src = VideoFrame::new(4, 4, PixelFormat::Argb8888);
372        src.set_pixel(0, 0, 255, 0, 0, 255);
373        src.set_pixel(3, 3, 0, 255, 0, 255);
374
375        let dst = scale_frame(&src, 4, 4, ScaleMode::NearestNeighbor);
376        assert_eq!(dst.get_pixel(0, 0), (255, 0, 0, 255));
377        assert_eq!(dst.get_pixel(3, 3), (0, 255, 0, 255));
378    }
379
380    #[test]
381    fn test_nearest_neighbor_upscale() {
382        let mut src = VideoFrame::new(2, 2, PixelFormat::Argb8888);
383        src.set_pixel(0, 0, 100, 200, 50, 255);
384        src.set_pixel(1, 1, 10, 20, 30, 255);
385
386        let dst = scale_frame(&src, 4, 4, ScaleMode::NearestNeighbor);
387        // (0,0) should map to src(0,0)
388        assert_eq!(dst.get_pixel(0, 0), (100, 200, 50, 255));
389        // (3,3) should map to src(1,1)
390        assert_eq!(dst.get_pixel(3, 3), (10, 20, 30, 255));
391    }
392
393    #[test]
394    fn test_bilinear_identity() {
395        let mut src = VideoFrame::new(4, 4, PixelFormat::Argb8888);
396        src.set_pixel(0, 0, 255, 0, 0, 255);
397
398        let dst = scale_frame(&src, 4, 4, ScaleMode::Bilinear);
399        let (r, _g, _b, _a) = dst.get_pixel(0, 0);
400        // Should be close to the original (may differ slightly due to fixed-point)
401        assert!(r > 240);
402    }
403
404    #[test]
405    fn test_convert_xrgb_to_rgb888() {
406        let mut src = VideoFrame::new(2, 2, PixelFormat::Xrgb8888);
407        src.set_pixel(0, 0, 0xAA, 0xBB, 0xCC, 0xFF);
408
409        let dst = convert_pixel_format(&src, PixelFormat::Rgb888);
410        assert_eq!(dst.format, PixelFormat::Rgb888);
411        let (r, g, b, a) = dst.get_pixel(0, 0);
412        assert_eq!((r, g, b), (0xAA, 0xBB, 0xCC));
413        assert_eq!(a, 0xFF);
414    }
415
416    #[test]
417    fn test_yuv_rgb_roundtrip() {
418        // Test that RGB -> YUV -> RGB is approximately identity
419        let r0: u8 = 180;
420        let g0: u8 = 100;
421        let b0: u8 = 60;
422
423        let (y, u, v) = rgb_to_yuv(r0, g0, b0);
424        let (r1, g1, b1) = yuv_to_rgb(y, u, v);
425
426        // Allow +/- 2 rounding error
427        assert!((r1 as i16 - r0 as i16).unsigned_abs() <= 2);
428        assert!((g1 as i16 - g0 as i16).unsigned_abs() <= 2);
429        assert!((b1 as i16 - b0 as i16).unsigned_abs() <= 2);
430    }
431
432    #[test]
433    fn test_yuv_black_white() {
434        // Black: Y=0, U=128, V=128 -> R=0, G=0, B=0
435        let (r, g, b) = yuv_to_rgb(0, 128, 128);
436        assert_eq!((r, g, b), (0, 0, 0));
437
438        // White: Y=255, U=128, V=128 -> R=255, G=255, B=255
439        let (r, g, b) = yuv_to_rgb(255, 128, 128);
440        assert_eq!((r, g, b), (255, 255, 255));
441    }
442
443    #[test]
444    fn test_alpha_blend_opaque() {
445        let (r, g, b) = alpha_blend(100, 200, 50, 255, 0, 0, 0);
446        assert_eq!((r, g, b), (100, 200, 50));
447    }
448
449    #[test]
450    fn test_alpha_blend_transparent() {
451        let (r, g, b) = alpha_blend(100, 200, 50, 0, 10, 20, 30);
452        assert_eq!((r, g, b), (10, 20, 30));
453    }
454
455    #[test]
456    fn test_alpha_blend_half() {
457        let (r, g, b) = alpha_blend(200, 100, 0, 128, 0, 0, 200);
458        // r ~ (200*128 + 0*127) / 255 ~ 100
459        // b ~ (0*128 + 200*127) / 255 ~ 99
460        assert!(r > 90 && r < 110);
461        assert!(b > 90 && b < 110);
462    }
463}