Skip to main content

drmkit_csd/
presenter_fb.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Decorations without a plane, into a framebuffer.
5//!
6//! The tier of last resort: no KMS plane is available, so the decorations are
7//! composited into whatever buffer the caller already presents — an fbdev
8//! mapping, typically. Every frame costs a blend over the damaged region plus
9//! a channel conversion, which is why it is last.
10
11use drmkit_scene::{CompositeRect, CompositeSrc, blend_into, clear_into, convert_row};
12
13use crate::{DamageRect, intersect_rect};
14
15/// One decoration to blend in.
16#[derive(Debug, Clone, Copy)]
17#[allow(
18    clippy::exhaustive_structs,
19    reason = "built by callers, with no default to fill a new field from"
20)]
21pub struct BlitItem<'a> {
22    /// Its pixels.
23    pub pixels: &'a [u8],
24    /// Bytes between its rows.
25    pub stride: u32,
26    /// Width in pixels.
27    pub width: u32,
28    /// Height in pixels.
29    pub height: u32,
30    /// What its pixels are.
31    pub fourcc: u32,
32    /// Destination x.
33    pub x: i32,
34    /// Destination y.
35    pub y: i32,
36}
37
38/// Pick a `FourCC` from what fbdev reports about the framebuffer.
39///
40/// `None` for a depth this cannot describe. Refusing is the point: a caller
41/// handed a guess would write 32-bit pixels into a 24-bit buffer, and the
42/// result is not a wrong color but a sheared image, because every row lands
43/// at the wrong offset.
44///
45/// The red-versus-blue offset decides channel order, which is the one thing
46/// fbdev reports that cannot be inferred: a buffer described as 32bpp is
47/// equally likely to be `XRGB8888` or `XBGR8888`, and getting it backwards
48/// swaps red and blue on every pixel.
49#[must_use]
50pub const fn fb_fourcc_for(
51    bpp: u32,
52    red_offset: u32,
53    blue_offset: u32,
54    transparency_length: u32,
55) -> Option<u32> {
56    match bpp {
57        32 => {
58            // A zero-length transparency field means the buffer has no alpha
59            // channel, whatever the byte width suggests -- the fourth byte is
60            // padding, and describing it as alpha would have the display
61            // engine blend against uninitialized data.
62            let has_alpha = transparency_length != 0;
63            Some(if red_offset > blue_offset {
64                if has_alpha {
65                    drmkit_fmt::fourcc::ARGB8888
66                } else {
67                    drmkit_fmt::fourcc::XRGB8888
68                }
69            } else if has_alpha {
70                drmkit_fmt::fourcc::ABGR8888
71            } else {
72                drmkit_fmt::fourcc::XBGR8888
73            })
74        }
75        16 => Some(if red_offset > blue_offset {
76            drmkit_fmt::fourcc::RGB565
77        } else {
78            drmkit_fmt::fourcc::BGR565
79        }),
80        _ => None,
81    }
82}
83
84/// The framebuffer being composited into.
85#[derive(Debug)]
86#[allow(
87    clippy::exhaustive_structs,
88    reason = "built by callers, with no default to fill a new field from"
89)]
90pub struct FbTarget<'a> {
91    /// The mapping.
92    pub pixels: &'a mut [u8],
93    /// Bytes between rows.
94    pub stride: u32,
95    /// Width in pixels.
96    pub width: u32,
97    /// Height in pixels.
98    pub height: u32,
99    /// What its pixels are, from [`fb_fourcc_for`].
100    pub fourcc: u32,
101}
102
103/// Blend `items` into `target` over `damage`.
104///
105/// `shadow` is an `ARGB8888` scratch buffer the size of the framebuffer.
106/// Compositing happens there and is converted into the framebuffer's own
107/// format afterwards, rather than blending directly: a blend needs a
108/// consistent channel order and an alpha channel, and the destination has
109/// neither in general.
110///
111/// **Only the damaged rows are converted.** The blend is already confined to
112/// the damage; converting the whole buffer afterwards would make the damage
113/// tracking pointless, since the conversion is the same cost per row as the
114/// blend.
115///
116/// Does nothing when the damage is empty, or when the shadow is too small for
117/// the framebuffer it is meant to shadow.
118pub fn compose_into_framebuffer(
119    target: &mut FbTarget<'_>,
120    shadow: &mut [u8],
121    items: &[BlitItem<'_>],
122    damage: DamageRect,
123) {
124    let (fb_width, fb_height, fb_stride, fb_fourcc) =
125        (target.width, target.height, target.stride, target.fourcc);
126    let shadow_stride = fb_width * 4;
127    if damage.is_empty()
128        || shadow.len() < shadow_stride as usize * fb_height as usize
129        || fb_width == 0
130        || fb_height == 0
131    {
132        return;
133    }
134
135    let rect = |region: DamageRect| CompositeRect {
136        x: region.x,
137        y: region.y,
138        w: region.w,
139        h: region.h,
140    };
141
142    clear_into(shadow, shadow_stride, fb_width, fb_height, rect(damage));
143
144    for item in items {
145        // Each decoration contributes only where it overlaps the damage.
146        // Blending its whole extent would repaint pixels the damage says are
147        // already correct, at the cost the damage exists to avoid.
148        let overlap = intersect_rect(item.x, item.y, item.width, item.height, damage);
149        if overlap.is_empty() {
150            continue;
151        }
152        let src = CompositeSrc {
153            pixels: item.pixels,
154            src_stride_bytes: item.stride,
155            src_width: item.width,
156            src_height: item.height,
157            drm_fourcc: item.fourcc,
158            // Fully opaque: a decoration's transparency is in its own pixels,
159            // and a plane alpha on top would fade the whole thing including
160            // the parts meant to be solid.
161            plane_alpha: u16::MAX,
162        };
163        blend_into(
164            shadow,
165            shadow_stride,
166            fb_width,
167            fb_height,
168            &src,
169            CompositeRect {
170                x: overlap.x - item.x,
171                y: overlap.y - item.y,
172                w: overlap.w,
173                h: overlap.h,
174            },
175            rect(overlap),
176        );
177    }
178
179    let first = damage.y.max(0).unsigned_abs();
180    let last = (first + damage.h).min(fb_height);
181    for row in first..last {
182        let to = row as usize * fb_stride as usize;
183        let from = row as usize * shadow_stride as usize;
184        let (Some(src), Some(dst)) = (
185            shadow.get(from..from + shadow_stride as usize),
186            target.pixels.get_mut(to..to + fb_stride as usize),
187        ) else {
188            // A framebuffer shorter than its own stride and height describe is
189            // the caller's error; stopping short beats writing past its end.
190            break;
191        };
192        convert_row(dst, src, fb_width as usize, fb_fourcc);
193    }
194}