embedded_graphics_gop/blt.rs
1// Copyright (c) 2025 nytpu <alex [at] nytpu.com>
2// SPDX-License-Identifier: MPL-2.0
3// For more license details, see LICENSE or <https://www.mozilla.org/en-US/MPL/2.0/>.
4
5use alloc::{vec, vec::Vec};
6use embedded_graphics_core::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle};
7use uefi::{
8 boot::ScopedProtocol,
9 proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput},
10};
11
12/// A [`DrawTarget`] using only BLT routines to write to the framebuffer.
13///
14/// Works on *all* UEFI implementations providing the GOP protocol---UEFI Specification 2.11
15/// § 12.9.2; BLT is required to be provided, but drivers/hardware are allowed to not provide
16/// direct framebuffer access. However, it *requires* UEFI boot services to be active as it makes
17/// boot services function calls.
18///
19/// Offers single- and double-buffered modes, defaulting to single-buffer mode. In single-buffered
20/// mode, drawing is transferred to the framebuffer immediately. In double-buffered mode, drawing
21/// is saved to an internal back-buffer and are only transferred to the primary buffer after being
22/// explicitly committed.
23///
24/// Note that even in single-buffered mode, an internal framebuffer is still allocated for
25/// optimizing draw operations. This framebuffer is kept in sync with any drawing operations
26/// performed, so the canvas state stays consistent even when switching between single- and
27/// double-buffered mode.
28///
29/// In double-buffered mode, rudimentary damage tracking is performed to minimize the region that
30/// gets transferred to the primary buffer. In double-buffered mode, any untransferred changes are
31/// automatically transferred when the `BltDrawTarget` is dropped. Note that the routines in
32/// the `DrawTarget` implementation will *never* return an error in double-buffered mode and can be
33/// considered infallible; as only [`BltDrawTarget::commit`] calls UEFI routines.
34///
35/// # Examples
36///
37/// ```no_run
38/// use embedded_graphics_gop::BltDrawTarget;
39/// use uefi::{prelude::*, proto::console::gop::GraphicsOutput};
40///
41/// // Get the first available handle for the GOP.
42/// let handle = boot::get_handle_for_protocol::<GraphicsOutput>().unwrap();
43/// // Open the protocol in exclusive mode (exclusive mode is for applications, other modes are
44/// // intended for drivers)
45/// let mut protocol = boot::open_protocol_exclusive::<GraphicsOutput>(handle).unwrap();
46/// // Configure protocol here if desired, for example
47/// let mode = protocol.modes().find(|m| m.info().resolution() == (800, 600)).unwrap();
48/// protocol.set_mode(&mode).unwrap();
49///
50/// // Create the draw target utilizing the configured protocol.
51/// let mut target = BltDrawTarget::new(&mut protocol).unwrap();
52/// // Make it double-buffered
53/// target.double_buffer(true).unwrap();
54///
55/// // ...draw on it...
56///
57/// // Transfer changes to the framebuffer
58/// target.commit().unwrap();
59/// ```
60#[derive(Debug)]
61pub struct BltDrawTarget<'proto> {
62 /// GOP protocol handle
63 handle: &'proto mut ScopedProtocol<GraphicsOutput>,
64 /// Internal framebuffer to optimize `draw_iter` calls
65 backbuffer: Vec<BltPixel>,
66 /// If it is in double buffered mode
67 double_buffer: bool,
68 /// Damage to backbuffer since last commit, if in double-buffered mode.
69 damage: Rectangle,
70 /// Width of the current video mode
71 width: u32,
72 /// Height of the current video mode
73 height: u32,
74}
75
76impl<'proto> BltDrawTarget<'proto> {
77 /// Create a new `BltDrawTarget` given a GOP protocol handle.
78 ///
79 /// Fills the framebuffer and back-buffer with black.
80 ///
81 /// Note that one should configure the desired graphics mode for the protocol prior to creating
82 /// the `BltDrawTarget`.
83 ///
84 /// # Errors
85 ///
86 /// Returns a [`uefi::Error`] if the BLT operation to black out the framebuffer fails.
87 ///
88 /// # Panics
89 ///
90 /// - If the reported resolution of the current graphics mode is ≥ 2^31 - 1 in either
91 /// dimension.
92 /// - If the reported number of pixels addressed by the current graphics mode (width×height)
93 /// exceeds `isize::MAX / 4` (i.e. largest possible size for an array of `u32`s).
94 #[expect(
95 clippy::cast_possible_truncation,
96 reason = "Asserted width and height always fit in [0, i32::MAX), so they will always fit in u32."
97 )]
98 pub fn new(protocol: &'proto mut ScopedProtocol<GraphicsOutput>) -> uefi::Result<Self> {
99 let mode_info = protocol.current_mode_info();
100 let (width, height) = mode_info.resolution();
101 assert!(
102 i32::try_from(width).is_ok(),
103 "reported output width {width} >= 2^31 - 1"
104 );
105 assert!(
106 i32::try_from(height).is_ok(),
107 "reported output height {height} >= 2^31 - 1"
108 );
109 // Fill framebuffer with black
110 protocol.blt(BltOp::VideoFill {
111 color: BltPixel::new(0, 0, 0),
112 dest: (0, 0),
113 dims: (width, height),
114 })?;
115 Ok(Self {
116 handle: protocol,
117 // checked_mul checks for usize::MAX, but then allocating the vector will also check
118 // against allocating more than isize::MAX bytes.
119 backbuffer: vec![
120 BltPixel::new(0, 0, 0);
121 width
122 .checked_mul(height)
123 .expect("width*height exceeds usize::MAX")
124 ],
125 double_buffer: false,
126 damage: Rectangle::zero(),
127 width: width as u32,
128 height: height as u32,
129 })
130 }
131
132 /// Enable or disable the double-buffering mode.
133 ///
134 /// If current in double-buffered mode and it is being disabled, then the buffer contents will
135 /// be committed.
136 ///
137 /// # Errors
138 ///
139 /// Returns a [`uefi::Error`] if disabling double-buffering and an error occured when
140 /// committing current contents.
141 pub fn double_buffer(&mut self, double_buffer: bool) -> uefi::Result<()> {
142 if self.double_buffer && !double_buffer {
143 self.commit()?;
144 }
145 self.double_buffer = double_buffer;
146 Ok(())
147 }
148
149 /// Return whether double-buffering mode is currently enabled.
150 pub fn get_double_buffer(&self) -> bool {
151 self.double_buffer
152 }
153
154 /// Copy the given `rect` (or the current damage region if `None`) to the framebuffer, or from
155 /// the backbuffer to the framebuffer if `backwards` is true. Clears the damage rectangle after
156 /// copying.
157 #[expect(clippy::cast_sign_loss, reason = "Known to fit in usize")]
158 fn copy_rect(&mut self, rect: Option<Rectangle>, backwards: bool) -> uefi::Result<()> {
159 let area = self
160 .bounding_box()
161 .intersection(&rect.unwrap_or(self.damage));
162 if self.double_buffer && !area.is_zero_sized() {
163 let top_left = (
164 self.damage.top_left.x as usize,
165 self.damage.top_left.y as usize,
166 );
167 let dims = (
168 self.damage.size.width as usize,
169 self.damage.size.height as usize,
170 );
171 let subrectangle = BltRegion::SubRectangle {
172 coords: top_left,
173 px_stride: self.width as usize,
174 };
175
176 if backwards {
177 self.handle.blt(BltOp::VideoToBltBuffer {
178 buffer: &mut self.backbuffer,
179 src: top_left,
180 dest: subrectangle,
181 dims,
182 })?;
183 } else {
184 self.handle.blt(BltOp::BufferToVideo {
185 buffer: self.backbuffer.as_slice(),
186 src: subrectangle,
187 dest: top_left,
188 dims,
189 })?;
190 }
191 }
192 self.damage = Rectangle::zero();
193 Ok(())
194 }
195
196 /// Transfer the backbuffer contents to the primary buffer.
197 ///
198 /// If in single-buffering mode, has no effect.
199 ///
200 /// # Errors
201 ///
202 /// Returns a [`uefi::Error`] if an error occurs while transferring the backbuffer. Will never
203 /// return an error in single-buffered mode.
204 pub fn commit(&mut self) -> uefi::Result<()> {
205 self.copy_rect(None, false)
206 }
207
208 /// Discard any uncommitted changes when in double-buffered mode.
209 ///
210 /// The damaged portion of the backbuffer will have its contents overwritten with the current
211 /// state of the primary buffer, truly discarding any changes since the last commit.
212 ///
213 /// # Errors
214 ///
215 /// Returns a [`uefi::Error`] if an error occurs while reading the primary buffer data into the
216 /// backbuffer.
217 pub fn discard_changes(&mut self) -> uefi::Result<()> {
218 self.copy_rect(None, true)
219 }
220
221 /// When in double-buffered mode, discard any uncommitted changes without resetting the
222 /// backbuffer contents.
223 ///
224 /// Note that this **leaves the backbuffer untouched but marked as not needing to be
225 /// transferred**. This will make future drawing and committing very inconsistent and should
226 /// likely only be used to discard changes prior to dropping the `BltDrawTarget`.
227 pub fn discard_changes_no_reset(&mut self) {
228 self.damage = Rectangle::zero();
229 }
230}
231
232impl Drop for BltDrawTarget<'_> {
233 fn drop(&mut self) {
234 let _ = self.commit();
235 }
236}
237
238impl OriginDimensions for BltDrawTarget<'_> {
239 fn size(&self) -> Size {
240 Size::new(self.width, self.height)
241 }
242}
243
244impl DrawTarget for BltDrawTarget<'_> {
245 // Specified BLTPixel color format
246 type Color = Rgb888;
247 type Error = uefi::Error;
248
249 #[expect(
250 clippy::cast_possible_truncation,
251 clippy::cast_possible_wrap,
252 clippy::cast_sign_loss,
253 reason = "width and height were confirmed to fit within range of i32 and usize in constructors."
254 )]
255 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
256 where
257 I: IntoIterator<Item = Pixel<Self::Color>>,
258 {
259 // Just computing one rectangle containing all pixels drawn rather than trying to do more
260 // granular damage tracking
261 let mut top_left = (self.width as usize, self.height as usize);
262 let mut bottom_right = (0, 0);
263
264 let (width, height) = (self.width as i32, self.height as i32);
265 for Pixel(Point { x, y }, color) in pixels {
266 if (0..width).contains(&x) && (0..height).contains(&y) {
267 let (x, y) = (x as usize, y as usize);
268 // Always sound arithmetic? I think so since width*height (i.e. one after last
269 // pixel) is always <=(isize::MAX/4)
270 //
271 // The color conversion will likely be more efficient using these constructors,
272 // rather than using `.into_storage().into()` which would shift the Rgb888 into a
273 // u32, only to immediately shift and mask it back into a BltPixel.
274 self.backbuffer[(y * self.width as usize) + x] =
275 BltPixel::new(color.r(), color.g(), color.b());
276
277 top_left = (usize::min(top_left.0, x), usize::min(top_left.1, y));
278 bottom_right = (usize::max(bottom_right.0, x), usize::max(bottom_right.1, y));
279 }
280 }
281
282 let damage_rectangle = if top_left.0 > bottom_right.0 || top_left.1 > bottom_right.1 {
283 Rectangle::zero()
284 } else {
285 Rectangle::with_corners(
286 Point::new(top_left.0 as i32, top_left.1 as i32),
287 Point::new(bottom_right.0 as i32, bottom_right.1 as i32),
288 )
289 .intersection(&self.bounding_box())
290 };
291 if !damage_rectangle.is_zero_sized() {
292 if self.double_buffer {
293 self.damage = crate::update_damage(self.damage, damage_rectangle);
294 } else {
295 self.copy_rect(Some(damage_rectangle), true)?;
296 }
297 }
298 Ok(())
299 }
300
301 // Could in theory provide a `DrawTarget::fill_contiguous` implementation that doesn't need to
302 // track the bounding rectangle nor check if points lie inside the framebuffer, but would need
303 // to keep track of whether the point from the color iterator lies inside of it anyways and
304 // it'd be a lot of code duplication and effort for little gain
305
306 #[expect(
307 clippy::cast_sign_loss,
308 reason = "top-left and bottom-right corners confirmed to be in bounds of screen which are confirmed to fit in usize"
309 )]
310 fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
311 let area = area.intersection(&self.bounding_box());
312 if !area.is_zero_sized() {
313 let color = BltPixel::new(color.r(), color.g(), color.b());
314
315 // XXX: needed to keep the backbuffer in sync even in single-buffered mode, but seems
316 // janky and inefficent.
317 for y in
318 (area.top_left.y as usize)..(area.top_left.y as usize + area.size.height as usize)
319 {
320 for x in (area.top_left.x as usize)
321 ..(area.top_left.x as usize + area.size.width as usize)
322 {
323 // See draw_iter for reasoning around math staying in-bounds
324 self.backbuffer[y * self.width as usize + x] = color;
325 }
326 }
327
328 if self.double_buffer {
329 self.damage = crate::update_damage(self.damage, area);
330 } else {
331 self.handle.blt(BltOp::VideoFill {
332 color,
333 dest: (area.top_left.x as usize, area.top_left.y as usize),
334 dims: (area.size.width as usize, area.size.height as usize),
335 })?;
336 }
337 }
338 Ok(())
339 }
340}