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}