Demilade Sonuga's blog

All posts

The Graphics Output Protocol I

2022-11-04

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