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}