embedded_graphics_gop/fb.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
5//! [`DrawTarget`s][DrawTarget] directly writing to the hardware framebuffer.
6//!
7//! These rely on the GPU's device driver to expose direct framebuffer access, which is very common
8//! (near-universal?) but not strictly required by the UEFI specification. However, they allow a
9//! potentially much more efficient implementation, especially in single-buffered mode.
10//!
11//! Most UEFI graphics drivers also keep the framebuffer valid after exiting the UEFI boot
12//! services, which is the only way to perform graphics after exiting boot services without
13//! including full GPU-specific drivers (or using VGA, or using VESA BIOS Extensions via a CSM; but
14//! those are even more limited than the GOP). Note there are numerous safety and compatibility
15//! caveats when doing this, see [`FbDrawTarget::new_free_lifetime`].
16//!
17//! Currently only support for [`PixelFormat::Rgb`] and [`PixelFormat::Bgr`] framebuffers is
18//! implemented; [framebuffers with custom pixel formats][uefi::proto::console::gop::PixelBitmask]
19//! cannot be used. Obviously [`PixelFormat::BltOnly`] framebuffers cannot be used.
20//!
21//! For [`PixelFormat::Bgr`], these directly writes to the framebuffer (that's correct, even though
22//! the input format is [`Rgb888`]; embedded-graphics uses names that are backwards from how
23//! everyone else refers to pixel formats), but it has to perform color conversion for every pixel
24//! if the implementation framebuffer uses [`PixelFormat::Rgb`]. Although the color conversion
25//! performed here should be more efficient than using the generic
26//! [`embedded_graphics::draw_target::ColorConverted`][ColorConverted]
27//! because it only needs to do a byte swap between two specific formats rather than needing to
28//! support all the disparate color formats supported by embedded-graphics.
29//!
30//! [ColorConverted]: https://docs.rs/embedded-graphics/0.8.1/embedded_graphics/draw_target/struct.ColorConverted.html
31
32use core::convert::Infallible;
33use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb888, prelude::*};
34use uefi::{
35 boot::ScopedProtocol,
36 proto::console::gop::{FrameBuffer, GraphicsOutput, PixelFormat},
37};
38
39#[cfg(feature = "alloc")]
40use alloc::{vec, vec::Vec};
41#[cfg(feature = "alloc")]
42use core::{mem::ManuallyDrop, ptr};
43#[cfg(feature = "alloc")]
44use embedded_graphics_core::primitives::Rectangle;
45
46#[inline]
47fn color_to_storage(pixel_format: PixelFormat, color: Rgb888) -> u32 {
48 // Seems backwards because embedded-graphics uses completely backwards names for its pixel
49 // formats. What it calls "RGB" puts red in the *most* significant byte (of a conceptual u24),
50 // while everything else would call that BGR since blue is the least-significant (and also
51 // comes first in a linear data stream).
52 if pixel_format == PixelFormat::Bgr {
53 color.into_storage()
54 } else {
55 Rgb888::new(color.r(), color.g(), color.b()).into_storage()
56 }
57}
58
59/// A single-buffered framebuffer [`DrawTarget`].
60///
61/// See [the module-level documentation][crate::fb] for more information on the framebuffer
62/// `DrawTarget`s.
63///
64/// # Examples
65///
66/// ```no_run
67/// use embedded_graphics_gop::fb::FbDrawTarget;
68/// use uefi::{prelude::*, proto::console::gop::GraphicsOutput};
69///
70/// // Get the first available handle for the GOP.
71/// let handle = boot::get_handle_for_protocol::<GraphicsOutput>().unwrap();
72/// // Open the protocol in exclusive mode (exclusive mode is for applications, other modes are
73/// // intended for drivers)
74/// let mut protocol = boot::open_protocol_exclusive::<GraphicsOutput>(handle).unwrap();
75/// // Configure protocol here if desired, for example
76/// let mode = protocol.modes().find(|m| m.info().resolution() == (800, 600)).unwrap();
77/// protocol.set_mode(&mode).unwrap();
78///
79/// // Create the draw target utilizing the configured protocol.
80/// let mut target = FbDrawTarget::new(&mut protocol);
81///
82/// // ...draw on it...
83/// ```
84#[derive(Debug)]
85pub struct FbDrawTarget<'proto> {
86 /// Pointer to top-left pixel of the framebuffer
87 framebuffer: *mut u32,
88 /// Length of the framebuffer in elements
89 size: isize,
90 /// width in pixels.
91 width: u32,
92 /// height in pixels.
93 height: u32,
94 /// The stride, or number of pixels per scanline (must use this rather than resolution.0 when
95 /// computing index into framebuffer)
96 stride: isize,
97 /// The pixel format for the underlying framebuffer
98 pixel_format: PixelFormat,
99 /// Saved framebuffer object to follow (stated) safety requirements for `framebuffer` pointer.
100 /// Could really just use `PhantomData` for it but `FrameBuffer` does explicitly state that to
101 /// be safe the pointer cannot outlive the *`FrameBuffer` object* (not the protocol handle) so
102 /// just hold it here. Not like the few bytes are a big deal since there will be at most one
103 /// `FbDrawTarget` per display (and more likely one `FbDrawTarget` per GPU)
104 _fb_obj: Option<FrameBuffer<'proto>>,
105}
106
107impl<'proto> FbDrawTarget<'proto> {
108 /// Create a new `FbDrawTarget` given a GOP protocol handle.
109 ///
110 /// Fills the framebuffer with black.
111 ///
112 /// Note that one should configure the desired graphics mode for the protocol prior to creating
113 /// the `FbDrawTarget`.
114 ///
115 /// # Panics
116 ///
117 /// - If the pixel format for the current graphics mode is not [`PixelFormat::Rgb`] or
118 /// [`PixelFormat::Bgr`].
119 /// - If the reported resolution of the current graphics mode is ≥ 2^31 - 1 in either dimension.
120 /// - If the reported number of pixels addressed by the current graphics mode (width×height)
121 /// exceeds `isize::MAX / 4` (i.e. largest possible size for an array of `u32`s).
122 /// - If the framebuffer pointer returned from UEFI is not aligned to `align_of::<u32>()`
123 /// (typically four bytes).
124 #[expect(
125 clippy::cast_possible_wrap,
126 reason = "framebuffer size implicilty fits in isize given height*stride check"
127 )]
128 #[expect(
129 clippy::cast_possible_truncation,
130 reason = "width and height confirmed to fit in i32, stride implicitly fits in isize given height*stride fits in isize"
131 )]
132 pub fn new(protocol: &'proto mut ScopedProtocol<GraphicsOutput>) -> Self {
133 let mode_info = protocol.current_mode_info();
134
135 let pixel_format = mode_info.pixel_format();
136 assert!(
137 matches!(pixel_format, PixelFormat::Rgb | PixelFormat::Bgr),
138 "Only RGB and BGR framebuffer pixel formats are supported."
139 );
140
141 let (width, height) = mode_info.resolution();
142 assert!(
143 i32::try_from(width).is_ok(),
144 "reported output width {width} >= 2^32 - 1"
145 );
146 assert!(
147 i32::try_from(height).is_ok(),
148 "reported output height {height} >= 2^32 - 1"
149 );
150
151 let stride = mode_info.stride();
152 assert!(
153 stride
154 .checked_mul(height)
155 .and_then(|v| isize::try_from(v).ok())
156 .is_some(),
157 "height*stride exceeds isize::MAX"
158 );
159
160 let mut fb_obj = protocol.frame_buffer();
161 let framebuffer = fb_obj.as_mut_ptr();
162 // Should be fine even though it's allowed to return `isize::MAX` if the pointer "can't be
163 // aligned": https://stackoverflow.com/a/71982312
164 assert_eq!(
165 framebuffer.align_offset(align_of::<u32>()),
166 0,
167 "framebuffer pointer not aligned to u32 boundary"
168 );
169
170 let fb_size = (fb_obj.size() / 4) as isize; // u32 length
171
172 // Black out the framebuffer
173 for i in 0..fb_size {
174 // SAFETY: offset is always in bounds of `framebuffer` and should always fit in isize
175 // given checks above (and in practice it certainly always will). Destination is valid
176 // and aligned per note and alignment check above.
177 unsafe {
178 framebuffer.offset(i).write_volatile(0);
179 }
180 }
181
182 Self {
183 framebuffer: framebuffer.cast(),
184 size: fb_size,
185 width: width as u32,
186 height: height as u32,
187 stride: stride as isize, // checked above implicitly
188 pixel_format,
189 _fb_obj: Some(fb_obj),
190 }
191 }
192}
193
194impl FbDrawTarget<'static> {
195 /// Equivalent to [`FbDrawTarget::new`], but allows the `FbDrawTarget` to outlive the input
196 /// `ScopedProtocol`.
197 ///
198 /// Allows preserving the framebuffer after closing the GOP handle or even after exiting UEFI
199 /// boot services (with caveats). See the [module-level documentation][crate::fb].
200 ///
201 /// Other than breaking the input protocol lifetime, precisely equivalent to the `new` method in
202 /// behavior.
203 ///
204 /// # Safety
205 ///
206 /// This technically violates the requirements specified in [`FrameBuffer`], as the pointer
207 /// outlives the `FrameBuffer` that created it (in fact, this immediately discards the
208 /// `Framebuffer` so it violates the specified safety requirements even before the protocol gets
209 /// dropped). However, per the implementation in uefi-rs 0.36.1, it directly passes through the
210 /// pointer given by the UEFI implementation, so the rules for handling it correspond to how it
211 /// could be handled in C. The implementation follows all the other safety requirements
212 /// specified by `FrameBuffer`.
213 ///
214 /// Re-opening a specific GOP handle *may* invalidate all previous framebuffer pointers, and
215 /// changing the graphics mode will almost certainly invalidate them.
216 ///
217 /// The ability to preserve the framebuffer pointer after exiting boot services is not specified
218 /// at all in the UEFI Specification as of version 2.11. As such, it is
219 /// implementation-specific. One must also ensure the system memory map remains set up properly
220 /// to keep the pointer pointing to the same physical address.
221 pub unsafe fn new_free_lifetime(protocol: &mut ScopedProtocol<GraphicsOutput>) -> Self {
222 // Just clear owned Framebuffer referencing protocol
223 Self {
224 _fb_obj: None,
225 ..FbDrawTarget::new(protocol)
226 }
227 }
228}
229
230impl OriginDimensions for FbDrawTarget<'_> {
231 fn size(&self) -> Size {
232 Size::new(self.width, self.height)
233 }
234}
235
236impl DrawTarget for FbDrawTarget<'_> {
237 type Color = Rgb888;
238 type Error = Infallible;
239
240 #[expect(
241 clippy::cast_possible_wrap,
242 reason = "width and height confirmed to fit in i32 in constructor"
243 )]
244 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
245 where
246 I: IntoIterator<Item = Pixel<Self::Color>>,
247 {
248 let (width, height) = (self.width as i32, self.height as i32);
249 for Pixel(Point { x, y }, color) in pixels {
250 if (0..width).contains(&x) && (0..height).contains(&y) {
251 let (x, y) = (x as isize, y as isize);
252 // Always sound arithmetic when offsetting pointer? I think so since stride*height
253 // (i.e. one after last pixel) is always <=isize::MAX
254 let offset = y * self.stride + x;
255 if offset < self.size {
256 let color = color_to_storage(self.pixel_format, color);
257
258 // SAFETY:
259 // `ptr::offset`: I believe the offset will always fit in an isize (and in
260 // practice it certainly always will). The offset is checked above to be
261 // in-bounds of the framebuffer (and really checked twice, as the size is
262 // implicitly encoded in the framebuffer's width and height in pixels).
263 //
264 // `ptr::write_volatile`: Destination is valid as per the offset above. It
265 // should always be aligned as the base alignment is verified in the
266 // constructors and `ptr::offset` always computes in multiples of the pointer
267 // type.
268 unsafe {
269 self.framebuffer.offset(offset).write_volatile(color);
270 }
271 }
272 }
273 }
274 Ok(())
275 }
276}
277
278// FIXME: really unhappy with the lack of range checks on integer conversions added to this
279// implementation differing from `FbDrawTarget`
280// FIXME: also really really don't like all the duplication with both `FbDrawTarget` and from
281// `BltDrawTarget`
282
283/// A double-buffered framebuffer [`DrawTarget`].
284///
285/// Rudimentary damage tracking is performed to minimize the region that gets copied to the primary
286/// buffer.
287///
288/// Any untransferred changes are automatically transferred when the `FbDbDrawTarget` is dropped.
289///
290/// See [the module-level documentation][crate::fb] for more information on the framebuffer
291/// `DrawTarget`s.
292///
293/// # Examples
294///
295/// ```no_run
296/// use embedded_graphics_gop::fb::FbDbDrawTarget;
297/// use uefi::{prelude::*, proto::console::gop::GraphicsOutput};
298///
299/// // Get the first available handle for the GOP.
300/// let handle = boot::get_handle_for_protocol::<GraphicsOutput>().unwrap();
301/// // Open the protocol in exclusive mode (exclusive mode is for applications, other modes are
302/// // intended for drivers)
303/// let mut protocol = boot::open_protocol_exclusive::<GraphicsOutput>(handle).unwrap();
304/// // Configure protocol here if desired, for example
305/// let mode = protocol.modes().find(|m| m.info().resolution() == (800, 600)).unwrap();
306/// protocol.set_mode(&mode).unwrap();
307///
308/// // Create the draw target utilizing the configured protocol.
309/// let mut target = FbDbDrawTarget::new(&mut protocol);
310///
311/// // ...draw on it...
312///
313/// // Transfer changes to the framebuffer
314/// target.commit();
315/// ```
316#[cfg(feature = "alloc")]
317#[derive(Debug)]
318pub struct FbDbDrawTarget<'proto> {
319 framebuffer: *mut u32,
320 /// Backbuffer. Of same size as framebuffer (including stride)
321 backbuffer: Vec<u32>,
322 width: u32,
323 height: u32,
324 stride: isize,
325 pixel_format: PixelFormat,
326 damage: Rectangle,
327 fb_obj: Option<FrameBuffer<'proto>>,
328}
329
330#[cfg(feature = "alloc")]
331impl<'proto> FbDbDrawTarget<'proto> {
332 /// Create a new `FbDbDrawTarget` given a GOP protocol handle.
333 ///
334 /// Fills the framebuffer and backbuffer with black.
335 ///
336 /// Note that one should configure the desired graphics mode for the protocol prior to creating
337 /// the `FbDbDrawTarget`.
338 ///
339 /// # Panics
340 ///
341 /// Exactly the same as [`FbDrawTarget::new`].
342 #[expect(
343 clippy::cast_possible_wrap,
344 clippy::cast_sign_loss,
345 reason = "framebuffer size implicilty fits in isize and usize given height*stride check"
346 )]
347 #[expect(
348 clippy::cast_possible_truncation,
349 reason = "width and height confirmed to fit in i32, stride implicitly fits in isize given height*stride fits in isize"
350 )]
351 pub fn new(protocol: &'proto mut ScopedProtocol<GraphicsOutput>) -> Self {
352 let mode_info = protocol.current_mode_info();
353
354 let pixel_format = mode_info.pixel_format();
355 assert!(
356 matches!(pixel_format, PixelFormat::Rgb | PixelFormat::Bgr),
357 "Only RGB and BGR framebuffer pixel formats are supported."
358 );
359
360 let (width, height) = mode_info.resolution();
361 assert!(
362 i32::try_from(width).is_ok(),
363 "reported output width {width} >= 2^32 - 1"
364 );
365 assert!(
366 i32::try_from(height).is_ok(),
367 "reported output height {height} >= 2^32 - 1"
368 );
369
370 let stride = mode_info.stride();
371 assert!(
372 stride
373 .checked_mul(height)
374 .and_then(|v| isize::try_from(v).ok())
375 .is_some(),
376 "height*stride exceeds isize::MAX"
377 );
378
379 let mut fb_obj = protocol.frame_buffer();
380 let framebuffer = fb_obj.as_mut_ptr();
381 assert_eq!(
382 framebuffer.align_offset(align_of::<u32>()),
383 0,
384 "framebuffer pointer not aligned to u32 boundary"
385 );
386
387 let fb_size = (fb_obj.size() / 4) as isize;
388
389 // Black out the framebuffer
390 for i in 0..fb_size {
391 // SAFETY: offset is always in bounds of `framebuffer` and should always fit in isize
392 // given checks above (and in practice it certainly always will). Destination is valid
393 // and aligned per note and alignment check above.
394 unsafe {
395 framebuffer.offset(i).write_volatile(0);
396 }
397 }
398
399 Self {
400 framebuffer: framebuffer.cast(),
401 backbuffer: vec![0; fb_size as usize],
402 width: width as u32,
403 height: height as u32,
404 stride: stride as isize,
405 pixel_format,
406 damage: Rectangle::zero(),
407 fb_obj: Some(fb_obj),
408 }
409 }
410
411 /// Copy the current damage region to the framebuffer, or from the backbuffer to the framebuffer
412 /// if `backwards` is true. Clears the damage rectangle after copying.
413 fn copy_rect(&mut self, backwards: bool) {
414 let area = self.damage.intersection(&self.bounding_box());
415 if !area.is_zero_sized() {
416 let src = if backwards {
417 self.framebuffer
418 } else {
419 self.backbuffer.as_ptr()
420 };
421 let dest = if backwards {
422 self.backbuffer.as_mut_ptr()
423 } else {
424 self.framebuffer
425 };
426
427 // XXX: would it be more efficient to just memcpy() from top-left to bottom-right even
428 // though that'd copy a bunch of superfluous unmodified data too?
429 for y in area.rows() {
430 let y = y as isize;
431 let scanline = y * self.stride + area.top_left.x as isize;
432 // SAFETY: the framebuffer and backbuffer cannot possibly overlap as one is from
433 // the Rust allocator and one is static from the GPU. They are the same size. The
434 // area was clipped to be in-bounds of the framebuffer.
435 unsafe {
436 ptr::copy_nonoverlapping(
437 src.offset(scanline),
438 dest.offset(scanline),
439 area.size.width as usize,
440 );
441 }
442 }
443 }
444 self.damage = Rectangle::zero();
445 }
446
447 /// Transfer the backbuffer contents to the primary buffer.
448 pub fn commit(&mut self) {
449 self.copy_rect(false);
450 }
451
452 /// Discard any uncommitted changes when in double-buffered mode.
453 ///
454 /// The damaged portion of the backbuffer will have its contents overwritten with the current
455 /// state of the primary buffer, truly discarding any changes since the last commit.
456 pub fn discard_changes(&mut self) {
457 self.copy_rect(true);
458 }
459
460 /// When in double-buffered mode, discard any uncommitted changes without resetting the
461 /// backbuffer contents.
462 ///
463 /// Note that this **leaves the backbuffer untouched but marked as not needing to be
464 /// transferred**. This will make future drawing and committing very inconsistent and should
465 /// likely only be used to discard changes prior to dropping the `FbDbDrawTarget`.
466 pub fn discard_changes_no_reset(&mut self) {
467 self.damage = Rectangle::zero();
468 }
469}
470
471#[cfg(feature = "alloc")]
472impl FbDbDrawTarget<'static> {
473 /// Equivalent to [`FbDbDrawTarget::new`], but allows the `FbDrawTarget` to outlive the input
474 /// `ScopedProtocol`.
475 ///
476 /// See [`FbDrawTarget::new_free_lifetime`] for complete usage and safety information.
477 #[expect(
478 clippy::missing_safety_doc,
479 reason = "Links to FbDrawTarget::new_free_lifetime"
480 )]
481 pub unsafe fn new_free_lifetime(protocol: &mut ScopedProtocol<GraphicsOutput>) -> Self {
482 let p = ManuallyDrop::new(FbDbDrawTarget::new(protocol));
483 // Just clear owned Framebuffer referencing protocol, but since `Drop` is implemented we
484 // have to do this...
485 // <https://github.com/rust-lang/rfcs/pull/3466>
486 // SAFETY: read is from a known-valid pointer. Struct read from is immediately forgotten
487 // after reading so no issues with use-after-free/double-free/etc. All things that need to
488 // be dropped (i.e. have explicit `Drop` and aren't preserved into here) are dropped.
489 unsafe {
490 // just extract `_fb_obj` so it gets dropped immediately
491 let _ = ptr::read(&raw const p.fb_obj);
492 Self {
493 framebuffer: p.framebuffer,
494 backbuffer: ptr::read(&raw const p.backbuffer),
495 width: p.width,
496 height: p.height,
497 stride: p.stride,
498 pixel_format: p.pixel_format,
499 damage: p.damage,
500 fb_obj: None,
501 }
502 }
503 }
504}
505
506#[cfg(feature = "alloc")]
507impl Drop for FbDbDrawTarget<'_> {
508 fn drop(&mut self) {
509 self.commit();
510 }
511}
512
513#[cfg(feature = "alloc")]
514impl OriginDimensions for FbDbDrawTarget<'_> {
515 fn size(&self) -> Size {
516 Size::new(self.width, self.height)
517 }
518}
519
520#[cfg(feature = "alloc")]
521impl DrawTarget for FbDbDrawTarget<'_> {
522 type Color = Rgb888;
523 type Error = Infallible;
524
525 #[expect(
526 clippy::cast_possible_wrap,
527 clippy::cast_possible_truncation,
528 clippy::cast_sign_loss,
529 reason = "width and height confirmed to fit in i32 in constructor"
530 )]
531 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
532 where
533 I: IntoIterator<Item = Pixel<Self::Color>>,
534 {
535 let mut top_left = (self.width as usize, self.height as usize);
536 let mut bottom_right = (0, 0);
537
538 let (width, height) = (self.width as i32, self.height as i32);
539 for Pixel(Point { x, y }, color) in pixels {
540 if (0..width).contains(&x) && (0..height).contains(&y) {
541 let (x, y) = (x as isize, y as isize);
542 let offset = y as usize * self.stride as usize + x as usize;
543 // superfluous as offset is already implicitly in bounds
544 if offset < self.backbuffer.len() {
545 self.backbuffer[offset] = color_to_storage(self.pixel_format, color);
546
547 let (x, y) = (x as usize, y as usize);
548 top_left = (usize::min(top_left.0, x), usize::min(top_left.1, y));
549 bottom_right = (usize::max(bottom_right.0, x), usize::max(bottom_right.1, y));
550 }
551 }
552 }
553 let damage_rectangle = if top_left.0 > bottom_right.0 || top_left.1 > bottom_right.1 {
554 Rectangle::zero()
555 } else {
556 Rectangle::with_corners(
557 Point::new(top_left.0 as i32, top_left.1 as i32),
558 Point::new(bottom_right.0 as i32, bottom_right.1 as i32),
559 )
560 .intersection(&self.bounding_box())
561 };
562 self.damage = crate::update_damage(self.damage, damage_rectangle);
563 Ok(())
564 }
565
566 #[expect(
567 clippy::cast_sign_loss,
568 reason = "top-left and bottom-right corners confirmed to be in bounds of screen which are confirmed to fit in usize"
569 )]
570 fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
571 let area = area.intersection(&self.bounding_box());
572 if !area.is_zero_sized() {
573 let color = color_to_storage(self.pixel_format, color);
574
575 for y in
576 (area.top_left.y as usize)..(area.top_left.y as usize + area.size.height as usize)
577 {
578 let scanline = y * self.stride as usize;
579 for x in (area.top_left.x as usize)
580 ..(area.top_left.x as usize + area.size.width as usize)
581 {
582 self.backbuffer[scanline + x] = color;
583 }
584 }
585
586 self.damage = crate::update_damage(self.damage, area);
587 }
588 Ok(())
589 }
590}