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}