Demilade Sonuga's blog
All postsThe Graphics Output Protocol I
In the previous post, we learned a few things about how graphics are displayed. Now, we're going to take our first steps to getting graphics on screen.
At this point, your code should be looking like this:
#![no_std]
#![no_main]
#![feature(abi_efiapi)]
#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
let mut string_u16 = [0u16; 14];
let string = "Hello World!\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
0
}
#[repr(C)]
struct SystemTable {
unneeded: [u8; 60],
simple_text_output: *mut SimpleTextOutput
}
#[repr(C)]
struct SimpleTextOutput {
unneeded: [u8; 8],
output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}
#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
loop {}
}
To use the Graphics Output Protocol, we first need to model it in our code. In the UEFI spec, the GOP is described in C as:
typedef struct EFI_GRAPHICS_OUTPUT_PROTCOL {
EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode;
EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt;
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;
Modeling this structure in our code begins with understanding the exact definitions of these fields and translating them to Rust.
We start with the EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE
. In the UEFI spec, this is defined as:
typedef
EFI_STATUS
(EFIAPI *EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE) (
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
IN UINT32 ModeNumber,
OUT UINTN *SizeOfInfo,
OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info
);
That is, this is just a pointer to a function. The spec says that the purpose of this function is to return
information about some available graphics mode. This function takes the pointer to the GOP instance as it's first
argument This
, a mode number as it's second argument ModeNumber
, a pointer to the size of the Info
buffer as the third SizeOfInfo
, and a pointer to a buffer that will hold the requested information on
return if the function is successful, Info
. The EFI_STATUS
written up there indicates that the
function returns a number that tells if the operation was successful. The EFI_STATUS
is defined as a
UINTN
, meaning it's an unsigned integer (an integer that cannot be negative) whose bit width is the
native width of the computer.
The IN
in front of the This
and ModeNumber
arguments signify that whatever we pass there are going
to be used solely as input. The OUT
in front of SizeOfInfo
and Info
signify that these arguments will
be used solely as output. So, on a call to the function, the pointer to the GOP will be in the first
argument in This
and the mode number associated with a mode we would like to get information about will be in
the ModeNumber
argument. For SizeOfInfo
, what will be passed is a pointer to some location we have
access to, telling the firmware that "When this function ends, I want the size of the mode information
you've given me to be placed in this location". For Info
, what will be passed is a pointer to a pointer,
telling the firmware that "When the function ends, I want the pointer to the mode information to be in this
location I have given to you".
To model this, we add the following definition to our code:
// The Graphics Output Protocol which has some useful utilities for handling
// drawing to the screen
#[repr(C)]
struct GraphicsOutput {
// This function collects information about the graphics mode
// specified in `mode_number` and puts a pointer to that information
// in the location pointed to by `info`
// This returns a usize which tells if the function was successful
query_mode: extern "efiapi" fn(
// A pointer to the GraphicsOutput instance
this: *mut GraphicsOutput,
// The number associated with the mode which you
// want to get information about
mode_number: u32,
// The size of the buffer in **info
size_of_info: *const usize,
// The pointer to a location in which the firmware will place a pointer
// to the information collected on a successful return
info: *mut *const GraphicsModeInfo
) -> usize,
}
This is the first GOP function we're modeling. The this
argument is just a pointer to the GraphicsOutput
instance, the mode_number
is a u32, since UINT32
is an unsigned integer with a bit width of 32 bits.
The size_of_info
is a pointer to a usize
because, in Rust, a usize is an unsigned integer whose bit
width is the computer's native width. And finally, we have info
, the pointer to a pointer to a
GraphicsModeInfo
instance. The function returns a usize because EFI_STATUS
is defined as UINTN
,
which is an unsigned integer whose bit width is the processor's native bit width.
Our GraphicsOutput
has the #[repr(C)]
attribute because the field order matters. We don't want our
fields to get rearranged by the compiler.
Now, we need to define our GraphicsModeInfo
struct, which corresponds to EFI_GRAPHICS_OUTPUT_MODE_INFORMATION
.
The EFI_GRAPHICS_OUTPUT_MODE_INFORMATION
is defined as:
typedef struct {
UINT32 Version;
UINT32 HorizontalResolution;
UINT32 VerticalResolution;
EFI_GRAPHICS_PIXEL_FORMAT PixelFormat;
EFI_PIXEL_BITMASK PixelInformation;
UINT32 PixelsPerScanLine;
} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION;
This just defines how we ought to interpret the bits pointed to by the pointer in info
on a successful
function return. The first 32 bits should be interpreted as an unsigned 32 bit integer named Version
.
And this Version
field tells the version number of this data structure. The second 32 bits, which is
at an offset of 4 bytes (32 bits) from the starting address of the structure should be interpreted as an
unsigned 32 bit integer named HorizontalResolution
. This field tells the number of pixels the screen
will have in one row, that is the number of pixels on the horizontal axis. The next field, which is at
an offset of 12 bytes (32 bits + 32 bits + 32 bits) from the structure's starting address is the
EFI_GRAPHICS_PIXEL_FORMAT
instance PixelFormat
, which tells us how the bits representing a single
pixel should be interpreted. The next field, offset of 12 bytes + size of EFI_GRAPHICS_PIXEL_FORMAT
,
PixelInformation
of type EFI_PIXEL_BITMASK
, offset 12 bytes + size of EFI_GRAPHICS_PIXEL_FORMAT
+
size of EFI_PIXEL_BITMASK
, whose meaning depends on the value of PixelFormat
.
Finally, we have another unsigned 32 bit integer PixelsPerScanLine
which is the number of pixels in
a video memory line. This PixelsPerScanLine
is very similar to HorizontalResolution
except for a few
minor things I think are irrelevant.
Adding to our Rust code:
// The blueprint to intepret the bits in **info upon a successful return from calling the
// GraphicsOutput's `query_mode` function
#[repr(C)]
struct GraphicsModeInfo {
// The UEFI version number of this data structure
version: u32,
// The number of pixels that can be contained in one
// horizontal row of the video screen in the mode whose info was requested
horizontal_resolution: u32,
// The number of pixels that can be contained in one vertical
// column of the video screen in this mode whose info was requested
vertical_resolution: u32,
// Indicates how the bits of representing a single pixel should
// be interpreted
pixel_format: PixelFormat,
// Some value whose meaning depends on the value of `pixel_format`
pixel_info: PixelBitmask,
// The number of pixels in one line of video memory.
// Similar to `horizontal_resolution`, but different in a few way I think
// are irrelevant
pixels_per_scan_line: u32
}
To continue modeling the EFI_GRAPHICS_OUTPUT_MODE_INFORMATION
, we need to precisely define what exactly
the EFI_GRAPHICS_PIXEL_FORMAT
and EFI_PIXEL_BITMASK
structures are.
The EFI_GRAPHICS_PIXEL_FORMAT
is defined as:
typedef enum {
PixelRedGreenBlueReserved8BitPerColor,
PixelBlueGreenRedReserved8BitPerColor,
PixelBitMask,
PixelBltOnly,
PixelFormatMax
} EFI_GRAPHICS_PIXEL_FORMAT;
This one is an enum. This tells us that a value of type EFI_GRAPHICS_PIXEL_FORMAT
can be any of
the specified fields in the enum.
In C, enum values are simply 4 byte integers whose values start from 0 and increase in order (except when
otherwise is specified). So, in the above enum, PixelRedGreenBlueReserved8BitPerColor
will have value 0,
PixelBlueGreenRedReserved8BitPerColor
will have value 1, PixelBitMask
will have value 2 and it goes this
way till the last variant in the enum which will have a value of number of variants - 1.
// Defines how to interpret the bits that represent a single pixel
#[repr(u32)]
enum PixelFormat {
RedGreenBlueReserved = 0,
BlueGreenRedReserved = 1,
BitMask = 2,
BltOnly = 3
}
As for the EFI_PIXEL_BITMASK
structure:
typedef struct {
UINT32 RedMask;
UINT32 GreenMask;
UINT32 BlueMask;
UINT32 ReservedMask;
} EFI_PIXEL_BITMASK;
When GraphicsModeInfo
's pixel_format
is set to PixelFormat::BIT_MASK
, this structure tells
which bits in a pixel should be interpreted as a red, green or blue.
A pixel, with the GOP, is represented as:
typedef struct {
UINT8 Blue;
UINT8 Green;
UINT8 Red;
UINT8 Reserved;
} EFI_GRAPHICS_OUTPUT_BLT_PIXEL;
This is just a 32 bit number (8 + 8 + 8 + 8) where the first 8 bits is interpreted as the intensity of the blue color, the second 8 bits is interpreted as the green color intensity and the third 8 bits is interpreted as the red color intensity. The reserved field is in there because we don't want a 24 bit structure. A 32 bit one is much easier to deal with.
In our Rust code, this becomes:
// A description of the color channels of a pixel in the GOP's framebuffer
#[repr(C)]
struct Pixel {
// The bits representing the blue color intensity in this pixel
blue: u8,
// The bits representing the green color intensity in this pixel
green: u8,
// The bits representing the red color intensity in this pixel
red: u8,
// Unused bits
reserved: u8
}
Apparently, according to the PixelFormat
field of GraphicsModeInfo
, this interpretation of the
pixel can be changed. This default representation given here just corresponds to
PixelFormat::BlueGreenRedReserved
, because the bits that represent the colors are given in the order
blue then green then red then reserved.
The EFI_PIXEL_BITMASK
structure, when the GraphicsModeInfo
's pixel_format
is set to
PixelFormat::BitMask
, tells how the Pixel
fields should be re-interpreted. If the first 8 bits of
The RedMask
field is set, then the first 8 bits of a Pixel
instance will represent the red color
intensity. If the third 8 bits of the BlueMask
field is set, then the third 8 bits of a Pixel
instance
will represent the blue color intensity.
// A structure telling how to re-interpret the bits in a pixel instance
// when the `GraphicsModeInfo` instance is set to `PixelFormat::BIT_MASK`
#[repr(C)]
struct PixelBitmask {
// The bits set to 1 in this field tells which bits in a pixel should be
// interpreted as the red color intensity when the `GraphicsModeInfo` instance
// is set to `PixelFormat::BitMask`
red_mask: u32,
// The bits set to 1 in this field tells which bits in a pixel should be
// interpreted as the green color intensity when the `GraphicsModeInfo` instance
// is set to `PixelFormat::BitMask`
green_mask: u32,
// The bits set to 1 in this field tells which bits in a pixel should be
// interpreted as the blue color intensity when the `GraphicsModeInfo` instance
// is set to `PixelFormat::BitMask`
blue_mask: u32,
// The bits set to 1 in this field tells which bits in a pixel should be
// interpreted as the reserved field when the `GraphicsModeInfo` instance
// is set to `PixelFormat::BitMask`
reserved: u32
}
Okay. And that's it for the GOP's query_mode
function.
Take Away
- Modeling structures in code begins with understanding precisely what those structures are and translating them to Rust.
Your code should be looking like this now:
Directory view:
blasterball/
| .cargo/
| | config.toml
| src/
| | main.rs
| .gitignore
| Cargo.lock
| Cargo.toml
main.rs
contents
#![no_std]
#![no_main]
#![feature(abi_efiapi)]
#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
let mut string_u16 = [0u16; 14];
let string = "Hello World!\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
0
}
#[repr(C)]
struct SystemTable {
unneeded: [u8; 60],
simple_text_output: *mut SimpleTextOutput
}
#[repr(C)]
struct SimpleTextOutput {
unneeded: [u8; 8],
output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}
#[repr(C)]
struct GraphicsOutput {
query_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32,
size_of_info: *const usize,
info: *mut *const GraphicsModeInfo
) -> usize
}
#[repr(C)]
struct GraphicsModeInfo {
version: u32,
horizontal_resolution: u32,
vertical_resolution: u32,
pixel_format: PixelFormat,
pixel_info: PixelBitmask,
pixels_per_scan_line: u32
}
#[repr(u32)]
enum PixelFormat {
RedGreenBlueReserved = 0,
BlueGreenRedReserved = 1,
BitMask = 2,
BltOnly = 3
}
#[repr(C)]
struct Pixel {
blue: u8,
green: u8,
red: u8,
reserved: u8
}
#[repr(C)]
struct PixelBitmask {
red_mask: u32,
green_mask: u32,
blue_mask: u32,
reserved: u32
}
#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
loop {}
}
In the Next Post
We'll continue modeling the Graphics Output Protocol
References
- UEFI spec, version 2.7, section 12.9 (https://uefi.org/specifications)
- https://doc.rust-lang.org/nomicon/other-reprs.html