| //! Graphics output protocol. |
| //! |
| //! The UEFI GOP is meant to replace existing [VGA][vga] hardware interfaces. |
| //! |
| //! The GOP provides access to a hardware frame buffer and allows UEFI apps |
| //! to draw directly to the graphics output device. |
| //! |
| //! The advantage of the GOP over legacy VGA is that it allows multiple GPUs |
| //! to exist and be used on the system. There is a GOP implementation for every |
| //! unique GPU in the system which supports UEFI. |
| //! |
| //! [vga]: https://en.wikipedia.org/wiki/Video_Graphics_Array |
| //! |
| //! # Definitions |
| //! |
| //! All graphics operations use a coordinate system where the top-left of the screen |
| //! is mapped to the point (0, 0), and `y` increases going down. |
| //! |
| //! Rectangles are defined by their top-left corner, and their width and height. |
| //! |
| //! The stride is understood as the length in bytes of a scan line / row of a buffer. |
| //! In theory, a buffer with a width of 640 should have (640 * 4) bytes per row, |
| //! but in practice there might be some extra padding used for efficiency. |
| //! |
| //! Frame buffers represent the graphics card's image buffers, backing the displays. |
| //! |
| //! Blits (**bl**ock **t**ransfer) can do high-speed memory copy between |
| //! the frame buffer and itself, or to and from some other buffers. |
| //! |
| //! # Blitting |
| //! |
| //! On certain hardware, the frame buffer is in a opaque format, |
| //! or cannot be accessed by the CPU. In those cases, it is not possible |
| //! to draw directly to the frame buffer. You must draw to another buffer |
| //! with a known pixel format, and then submit a blit command to copy that buffer |
| //! into the back buffer. |
| //! |
| //! Blitting can also copy a rectangle from the frame buffer to |
| //! another rectangle in the frame buffer, or move data out of the frame buffer |
| //! into a CPU-visible buffer. It can also do very fast color fills. |
| //! |
| //! The source and destination rectangles must always be of the same size: |
| //! no stretching / squashing will be done. |
| //! |
| //! # Animations |
| //! |
| //! UEFI does not mention if double buffering is used, nor how often |
| //! the frame buffer gets sent to the screen, but it's safe to assume that |
| //! the graphics card will re-draw the buffer at around the monitor's refresh rate. |
| //! You will have to implement your own double buffering if you want to |
| //! avoid tearing with animations. |
| |
| use crate::proto::unsafe_protocol; |
| use crate::util::usize_from_u32; |
| use crate::{boot, Result, StatusExt}; |
| use core::fmt::{Debug, Formatter}; |
| use core::marker::PhantomData; |
| use core::mem; |
| use core::ptr::{self, NonNull}; |
| use uefi_raw::protocol::console::{ |
| GraphicsOutputBltOperation, GraphicsOutputModeInformation, GraphicsOutputProtocol, |
| GraphicsOutputProtocolMode, |
| }; |
| |
| pub use uefi_raw::protocol::console::PixelBitmask; |
| |
| /// Provides access to the video hardware's frame buffer. |
| /// |
| /// The GOP can be used to set the properties of the frame buffer, |
| /// and also allows the app to access the in-memory buffer. |
| #[derive(Debug)] |
| #[repr(transparent)] |
| #[unsafe_protocol(GraphicsOutputProtocol::GUID)] |
| pub struct GraphicsOutput(GraphicsOutputProtocol); |
| |
| impl GraphicsOutput { |
| /// Returns information for an available graphics mode that the graphics |
| /// device and the set of active video output devices supports. |
| fn query_mode(&self, index: u32) -> Result<Mode> { |
| let mut info_sz = 0; |
| let mut info_heap_ptr = ptr::null(); |
| // query_mode allocates a buffer and stores the heap ptr in the provided |
| // variable. In this buffer, the queried data can be found. |
| unsafe { (self.0.query_mode)(&self.0, index, &mut info_sz, &mut info_heap_ptr) } |
| .to_result_with_val(|| { |
| // Transform to owned info on the stack. |
| let info = unsafe { *info_heap_ptr }; |
| |
| let info_heap_ptr = info_heap_ptr.cast::<u8>().cast_mut(); |
| |
| // User has no benefit from propagating this error. If this |
| // fails, it is an error of the UEFI implementation. |
| unsafe { boot::free_pool(NonNull::new(info_heap_ptr).unwrap()) } |
| .expect("buffer should be deallocatable"); |
| |
| Mode { |
| index, |
| info_sz, |
| info: ModeInfo(info), |
| } |
| }) |
| } |
| |
| /// Returns a [`ModeIter`]. |
| #[must_use] |
| pub const fn modes(&self) -> ModeIter { |
| ModeIter { |
| gop: self, |
| current: 0, |
| max: self.mode().max_mode, |
| } |
| } |
| |
| /// Sets the video device into the specified mode, clearing visible portions |
| /// of the output display to black. |
| /// |
| /// This function will invalidate the current framebuffer. |
| pub fn set_mode(&mut self, mode: &Mode) -> Result { |
| unsafe { (self.0.set_mode)(&mut self.0, mode.index) }.to_result() |
| } |
| |
| /// Performs a blt (block transfer) operation on the frame buffer. |
| /// |
| /// Every operation requires different parameters. |
| pub fn blt(&mut self, op: BltOp) -> Result { |
| // Demultiplex the operation type. |
| unsafe { |
| match op { |
| BltOp::VideoFill { |
| color, |
| dest: (dest_x, dest_y), |
| dims: (width, height), |
| } => { |
| self.check_framebuffer_region((dest_x, dest_y), (width, height)); |
| (self.0.blt)( |
| &mut self.0, |
| &color as *const _ as *mut _, |
| GraphicsOutputBltOperation::BLT_VIDEO_FILL, |
| 0, |
| 0, |
| dest_x, |
| dest_y, |
| width, |
| height, |
| 0, |
| ) |
| .to_result() |
| } |
| BltOp::VideoToBltBuffer { |
| buffer, |
| src: (src_x, src_y), |
| dest: dest_region, |
| dims: (width, height), |
| } => { |
| self.check_framebuffer_region((src_x, src_y), (width, height)); |
| self.check_blt_buffer_region(dest_region, (width, height), buffer.len()); |
| match dest_region { |
| BltRegion::Full => (self.0.blt)( |
| &mut self.0, |
| buffer.as_mut_ptr().cast(), |
| GraphicsOutputBltOperation::BLT_VIDEO_TO_BLT_BUFFER, |
| src_x, |
| src_y, |
| 0, |
| 0, |
| width, |
| height, |
| 0, |
| ) |
| .to_result(), |
| BltRegion::SubRectangle { |
| coords: (dest_x, dest_y), |
| px_stride, |
| } => (self.0.blt)( |
| &mut self.0, |
| buffer.as_mut_ptr().cast(), |
| GraphicsOutputBltOperation::BLT_VIDEO_TO_BLT_BUFFER, |
| src_x, |
| src_y, |
| dest_x, |
| dest_y, |
| width, |
| height, |
| px_stride * core::mem::size_of::<BltPixel>(), |
| ) |
| .to_result(), |
| } |
| } |
| BltOp::BufferToVideo { |
| buffer, |
| src: src_region, |
| dest: (dest_x, dest_y), |
| dims: (width, height), |
| } => { |
| self.check_blt_buffer_region(src_region, (width, height), buffer.len()); |
| self.check_framebuffer_region((dest_x, dest_y), (width, height)); |
| match src_region { |
| BltRegion::Full => (self.0.blt)( |
| &mut self.0, |
| buffer.as_ptr() as *mut _, |
| GraphicsOutputBltOperation::BLT_BUFFER_TO_VIDEO, |
| 0, |
| 0, |
| dest_x, |
| dest_y, |
| width, |
| height, |
| 0, |
| ) |
| .to_result(), |
| BltRegion::SubRectangle { |
| coords: (src_x, src_y), |
| px_stride, |
| } => (self.0.blt)( |
| &mut self.0, |
| buffer.as_ptr() as *mut _, |
| GraphicsOutputBltOperation::BLT_BUFFER_TO_VIDEO, |
| src_x, |
| src_y, |
| dest_x, |
| dest_y, |
| width, |
| height, |
| px_stride * core::mem::size_of::<BltPixel>(), |
| ) |
| .to_result(), |
| } |
| } |
| BltOp::VideoToVideo { |
| src: (src_x, src_y), |
| dest: (dest_x, dest_y), |
| dims: (width, height), |
| } => { |
| self.check_framebuffer_region((src_x, src_y), (width, height)); |
| self.check_framebuffer_region((dest_x, dest_y), (width, height)); |
| (self.0.blt)( |
| &mut self.0, |
| ptr::null_mut(), |
| GraphicsOutputBltOperation::BLT_VIDEO_TO_VIDEO, |
| src_x, |
| src_y, |
| dest_x, |
| dest_y, |
| width, |
| height, |
| 0, |
| ) |
| .to_result() |
| } |
| } |
| } |
| } |
| |
| /// Memory-safety check for accessing a region of the framebuffer |
| fn check_framebuffer_region(&self, coords: (usize, usize), dims: (usize, usize)) { |
| let (width, height) = self.current_mode_info().resolution(); |
| assert!( |
| coords.0.saturating_add(dims.0) <= width, |
| "Horizontal framebuffer coordinate out of bounds" |
| ); |
| assert!( |
| coords.1.saturating_add(dims.1) <= height, |
| "Vertical framebuffer coordinate out of bounds" |
| ); |
| } |
| |
| /// Memory-safety check for accessing a region of a user-provided buffer |
| fn check_blt_buffer_region(&self, region: BltRegion, dims: (usize, usize), buf_length: usize) { |
| match region { |
| BltRegion::Full => assert!( |
| dims.1.saturating_mul(dims.0) <= buf_length, |
| "BltBuffer access out of bounds" |
| ), |
| BltRegion::SubRectangle { |
| coords: (x, y), |
| px_stride, |
| } => { |
| assert!( |
| x.saturating_add(dims.0) <= px_stride, |
| "Horizontal BltBuffer coordinate out of bounds" |
| ); |
| assert!( |
| y.saturating_add(dims.1).saturating_mul(px_stride) <= buf_length, |
| "Vertical BltBuffer coordinate out of bounds" |
| ); |
| } |
| } |
| } |
| |
| /// Returns the frame buffer information for the current mode. |
| #[must_use] |
| pub const fn current_mode_info(&self) -> ModeInfo { |
| unsafe { *self.mode().info.cast_const().cast::<ModeInfo>() } |
| } |
| |
| /// Access the frame buffer directly |
| pub fn frame_buffer(&mut self) -> FrameBuffer { |
| assert!( |
| self.current_mode_info().pixel_format() != PixelFormat::BltOnly, |
| "Cannot access the framebuffer in a Blt-only mode" |
| ); |
| let base = self.mode().frame_buffer_base as *mut u8; |
| let size = self.mode().frame_buffer_size; |
| |
| FrameBuffer { |
| base, |
| size, |
| _lifetime: PhantomData, |
| } |
| } |
| |
| const fn mode(&self) -> &GraphicsOutputProtocolMode { |
| unsafe { &*self.0.mode.cast_const() } |
| } |
| } |
| |
| /// Represents the format of the pixels in a frame buffer. |
| #[derive(Debug, Copy, Clone, Eq, PartialEq)] |
| #[repr(u32)] |
| pub enum PixelFormat { |
| /// Each pixel is 32-bit long, with 24-bit RGB, and the last byte is reserved. |
| Rgb = 0, |
| /// Each pixel is 32-bit long, with 24-bit BGR, and the last byte is reserved. |
| Bgr, |
| /// Custom pixel format, check the associated bitmask. |
| Bitmask, |
| /// The graphics mode does not support drawing directly to the frame buffer. |
| /// |
| /// This means you will have to use the `blt` function which will |
| /// convert the graphics data to the device's internal pixel format. |
| BltOnly, |
| // SAFETY: UEFI also defines a PixelFormatMax variant, and states that all |
| // valid enum values are guaranteed to be smaller. Since that is the |
| // case, adding a new enum variant would be a breaking change, so it |
| // is safe to model this C enum as a Rust enum. |
| } |
| |
| /// Represents a graphics mode compatible with a given graphics device. |
| #[derive(Copy, Clone, Debug)] |
| pub struct Mode { |
| index: u32, |
| info_sz: usize, |
| info: ModeInfo, |
| } |
| |
| impl Mode { |
| /// The size of the info structure in bytes. |
| /// |
| /// Newer versions of the spec might add extra information, in a backwards compatible way. |
| #[must_use] |
| pub const fn info_size(&self) -> usize { |
| self.info_sz |
| } |
| |
| /// Returns a reference to the mode info structure. |
| #[must_use] |
| pub const fn info(&self) -> &ModeInfo { |
| &self.info |
| } |
| } |
| |
| /// Information about a graphics output mode. |
| #[derive(Debug, Copy, Clone, Eq, PartialEq)] |
| #[repr(transparent)] |
| pub struct ModeInfo(GraphicsOutputModeInformation); |
| |
| impl ModeInfo { |
| /// Returns the (horizontal, vertical) resolution. |
| /// |
| /// On desktop monitors, this usually means (width, height). |
| #[must_use] |
| pub const fn resolution(&self) -> (usize, usize) { |
| ( |
| usize_from_u32(self.0.horizontal_resolution), |
| usize_from_u32(self.0.vertical_resolution), |
| ) |
| } |
| |
| /// Returns the format of the frame buffer. |
| #[must_use] |
| pub const fn pixel_format(&self) -> PixelFormat { |
| match self.0.pixel_format.0 { |
| 0 => PixelFormat::Rgb, |
| 1 => PixelFormat::Bgr, |
| 2 => PixelFormat::Bitmask, |
| 3 => PixelFormat::BltOnly, |
| _ => panic!("invalid pixel format"), |
| } |
| } |
| |
| /// Returns the bitmask of the custom pixel format, if available. |
| #[must_use] |
| pub const fn pixel_bitmask(&self) -> Option<PixelBitmask> { |
| match self.pixel_format() { |
| PixelFormat::Bitmask => Some(self.0.pixel_information), |
| _ => None, |
| } |
| } |
| |
| /// Returns the number of pixels per scanline. |
| /// |
| /// Due to performance reasons, the stride might not be equal to the width, |
| /// instead the stride might be bigger for better alignment. |
| #[must_use] |
| pub const fn stride(&self) -> usize { |
| usize_from_u32(self.0.pixels_per_scan_line) |
| } |
| } |
| |
| /// Iterator for [`Mode`]s of the [`GraphicsOutput`] protocol. |
| pub struct ModeIter<'gop> { |
| gop: &'gop GraphicsOutput, |
| current: u32, |
| max: u32, |
| } |
| |
| impl<'gop> Iterator for ModeIter<'gop> { |
| type Item = Mode; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| let index = self.current; |
| if index < self.max { |
| let m = self.gop.query_mode(index); |
| self.current += 1; |
| |
| m.ok().or_else(|| self.next()) |
| } else { |
| None |
| } |
| } |
| |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| let size = (self.max - self.current) as usize; |
| (size, Some(size)) |
| } |
| } |
| |
| impl<'gop> Debug for ModeIter<'gop> { |
| fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { |
| f.debug_struct("ModeIter") |
| .field("current", &self.current) |
| .field("max", &self.max) |
| .finish() |
| } |
| } |
| |
| impl ExactSizeIterator for ModeIter<'_> {} |
| |
| /// Format of pixel data used for blitting. |
| /// |
| /// This is a BGR 24-bit format with an 8-bit padding, to keep each pixel 32-bit in size. |
| #[allow(missing_docs)] |
| #[derive(Debug, Copy, Clone)] |
| #[repr(C)] |
| pub struct BltPixel { |
| pub blue: u8, |
| pub green: u8, |
| pub red: u8, |
| _reserved: u8, |
| } |
| |
| impl BltPixel { |
| /// Create a new pixel from RGB values. |
| #[must_use] |
| pub const fn new(red: u8, green: u8, blue: u8) -> Self { |
| Self { |
| red, |
| green, |
| blue, |
| _reserved: 0, |
| } |
| } |
| } |
| |
| impl From<u32> for BltPixel { |
| fn from(color: u32) -> Self { |
| Self { |
| blue: (color & 0x00_00_FF) as u8, |
| green: ((color & 0x00_FF_00) >> 8) as u8, |
| red: ((color & 0xFF_00_00) >> 16) as u8, |
| _reserved: 0, |
| } |
| } |
| } |
| |
| /// Region of the `BltBuffer` which we are operating on |
| /// |
| /// Some `Blt` operations can operate on either the full `BltBuffer` or a |
| /// sub-rectangle of it, but require the stride to be known in the latter case. |
| #[derive(Clone, Copy, Debug)] |
| pub enum BltRegion { |
| /// Operate on the full `BltBuffer` |
| Full, |
| |
| /// Operate on a sub-rectangle of the `BltBuffer` |
| SubRectangle { |
| /// Coordinate of the rectangle in the `BltBuffer` |
| coords: (usize, usize), |
| |
| /// Stride (length of each row of the `BltBuffer`) in **pixels** |
| px_stride: usize, |
| }, |
| } |
| |
| /// Blit operation to perform. |
| #[derive(Debug)] |
| pub enum BltOp<'buf> { |
| /// Fills a rectangle of video display with a pixel color. |
| VideoFill { |
| /// The color to fill with. |
| color: BltPixel, |
| /// The X / Y coordinates of the destination rectangle. |
| dest: (usize, usize), |
| /// The width / height of the rectangle. |
| dims: (usize, usize), |
| }, |
| /// Reads data from the video display to the buffer. |
| VideoToBltBuffer { |
| /// Buffer into which to copy data. |
| buffer: &'buf mut [BltPixel], |
| /// Coordinates of the source rectangle, in the frame buffer. |
| src: (usize, usize), |
| /// Location of the destination rectangle in the user-provided buffer |
| dest: BltRegion, |
| /// Width / height of the rectangles. |
| dims: (usize, usize), |
| }, |
| /// Write data from the buffer to the video rectangle. |
| /// Delta must be the stride (count of bytes in a row) of the buffer. |
| BufferToVideo { |
| /// Buffer from which to copy data. |
| buffer: &'buf [BltPixel], |
| /// Location of the source rectangle in the user-provided buffer. |
| src: BltRegion, |
| /// Coordinates of the destination rectangle, in the frame buffer. |
| dest: (usize, usize), |
| /// Width / height of the rectangles. |
| dims: (usize, usize), |
| }, |
| /// Copy from the source rectangle in video memory to |
| /// the destination rectangle, also in video memory. |
| VideoToVideo { |
| /// Coordinates of the source rectangle, in the frame buffer. |
| src: (usize, usize), |
| /// Coordinates of the destination rectangle, also in the frame buffer. |
| dest: (usize, usize), |
| /// Width / height of the rectangles. |
| dims: (usize, usize), |
| }, |
| } |
| |
| /// Direct access to a memory-mapped frame buffer |
| #[derive(Debug)] |
| pub struct FrameBuffer<'gop> { |
| base: *mut u8, |
| size: usize, |
| _lifetime: PhantomData<&'gop mut u8>, |
| } |
| |
| impl<'gop> FrameBuffer<'gop> { |
| /// Access the raw framebuffer pointer |
| /// |
| /// To use this pointer safely and correctly, you must... |
| /// - Honor the pixel format and stride specified by the mode info |
| /// - Keep memory accesses in bound |
| /// - Use volatile reads and writes |
| /// - Make sure that the pointer does not outlive the FrameBuffer |
| /// |
| /// On some implementations this framebuffer pointer can be used after |
| /// exiting boot services, but that is not guaranteed by the UEFI Specification. |
| pub fn as_mut_ptr(&mut self) -> *mut u8 { |
| self.base |
| } |
| |
| /// Query the framebuffer size in bytes |
| #[must_use] |
| pub const fn size(&self) -> usize { |
| self.size |
| } |
| |
| /// Modify the i-th byte of the frame buffer |
| /// |
| /// # Safety |
| /// |
| /// This operation is unsafe because... |
| /// - You must honor the pixel format and stride specified by the mode info |
| /// - There is no bound checking on memory accesses in release mode |
| #[inline] |
| pub unsafe fn write_byte(&mut self, index: usize, value: u8) { |
| debug_assert!(index < self.size, "Frame buffer accessed out of bounds"); |
| self.base.add(index).write_volatile(value) |
| } |
| |
| /// Read the i-th byte of the frame buffer |
| /// |
| /// # Safety |
| /// |
| /// This operation is unsafe because... |
| /// - You must honor the pixel format and stride specified by the mode info |
| /// - There is no bound checking on memory accesses in release mode |
| #[inline] |
| #[must_use] |
| pub unsafe fn read_byte(&self, index: usize) -> u8 { |
| debug_assert!(index < self.size, "Frame buffer accessed out of bounds"); |
| self.base.add(index).read_volatile() |
| } |
| |
| /// Write a value in the frame buffer, starting at the i-th byte |
| /// |
| /// We only recommend using this method with [u8; N] arrays. Once Rust has |
| /// const generics, it will be deprecated and replaced with a write_bytes() |
| /// method that only accepts [u8; N] input. |
| /// |
| /// # Safety |
| /// |
| /// This operation is unsafe because... |
| /// - It is your responsibility to make sure that the value type makes sense |
| /// - You must honor the pixel format and stride specified by the mode info |
| /// - There is no bound checking on memory accesses in release mode |
| #[inline] |
| pub unsafe fn write_value<T>(&mut self, index: usize, value: T) { |
| debug_assert!( |
| index.saturating_add(mem::size_of::<T>()) <= self.size, |
| "Frame buffer accessed out of bounds" |
| ); |
| let ptr = self.base.add(index).cast::<T>(); |
| ptr.write_volatile(value) |
| } |
| |
| /// Read a value from the frame buffer, starting at the i-th byte |
| /// |
| /// We only recommend using this method with [u8; N] arrays. Once Rust has |
| /// const generics, it will be deprecated and replaced with a read_bytes() |
| /// method that only accepts [u8; N] input. |
| /// |
| /// # Safety |
| /// |
| /// This operation is unsafe because... |
| /// - It is your responsibility to make sure that the value type makes sense |
| /// - You must honor the pixel format and stride specified by the mode info |
| /// - There is no bound checking on memory accesses in release mode |
| #[inline] |
| #[must_use] |
| pub unsafe fn read_value<T>(&self, index: usize) -> T { |
| debug_assert!( |
| index.saturating_add(mem::size_of::<T>()) <= self.size, |
| "Frame buffer accessed out of bounds" |
| ); |
| (self.base.add(index) as *const T).read_volatile() |
| } |
| } |