Demilade Sonuga's blog

All posts

The Graphics Output Protocol V

2022-11-16

We're finally on our last step to drawing graphics on screen:

  1. Get boot services table from system table.
  2. Call the LocateProtocol function in the boot services table.
  3. Figure out how to change the mode to a graphics mode with the GOP.
  4. Figure out how to draw on screen.

In the last post, we successfully figured out how to change the mode to a graphics mode. We tried the using the GraphicsOutput's output_string function after that, but it didn't work. That's because it only works in text modes, not graphic modes. To print on the screen again, we have to develop our own way of doing that.

Now, we have to ask ourselves some questions. How exactly do we go about drawing on the screen? What exactly are the steps that we need to take to go from pixel descriptions to colors on screen?

Taking a look at the GraphicsOutput:

#[repr(C)]
struct GraphicsOutput {
    query_mode: extern "efiapi" fn(
        this: *mut GraphicsOutput,
        mode_number: u32,
        size_of_info: *const usize,
        info: *mut *const GraphicsModeInfo
    ) -> Status,
    set_mode: extern "efiapi" fn(
        this: *mut GraphicsOutput,
        mode_number: u32
    ) -> Status,
    unneeded: [u8; 8],
    mode: *const GraphicsMode
}

The only functions provided here are the query_mode and set_mode, which are used for getting information, not drawing. The second to the last field there, unneeded, is actually supposed to be the Blt function, a function used to draw rectangles on the screen. But we're not going to be using that in this project.

Taking a look at GraphicsMode:

#[repr(C)]
struct GraphicsMode {
    max_mode: u32,
    mode: u32,
    info: *const GraphicsModeInfo,
    size_of_info: usize,
    framebuffer_base: PhysAddr,
    framebuffer_size: usize
}

Now we're getting somewhere. This struct has a framebuffer_base field. This field holds the address of the area of memory that contains pixels which will be drawn to the screen.

Taking a look at GraphicsModeInfo:

#[repr(C)]
struct GraphicsModeInfo {
    version: u32,
    horizontal_resolution: u32,
    vertical_resolution: u32,
    pixel_format: PixelFormat,
    pixel_info: PixelBitmask,
    pixels_per_scan_line: u32
}

This struct has horizontal_resolution and vertical_resolution fields. horizontal_resolution tells us the number of pixels per row and the vertical_resolution tells us the number of pixels per column on the screen.

To fully understand this, we have to picture the screen as an array of pixels:

Screen:
  0     1     2     3
0 black black black black
1 black black black black
2 black black black black
3 black black black black
4 black black black black

The diagram above pictures a screen as a two-dimensional array of pixels where all the pixels have been set to the color black. This screen has a horizontal resolution of 4 because there are 4 pixels in one row. It has a vertical resolution of 5 because it has 5 pixels in one column.

The situation of this screen is the same situation ours is in. Each pixel value in the pixel memory has been set to the color black. Drawing on the screen is simply putting different pixels of different colors in this 2D array.

From the above, a picture of how to model this in code is emerging:

// The horizontal resolution of our desired mode
const NO_OF_PIXELS_IN_A_ROW: usize = 640;
// The vertical resolution of our desired mode
const NO_OF_PIXELS_IN_A_COLUMN: usize = 480;

// The pixels on the screen
struct Screen {
    pixels: [[Pixel; NO_OF_PIXELS_IN_A_ROW]; NO_OF_PIXELS_IN_A_COLUMN]
}

This struct Screen has a single field which is simply just a two dimensional array of pixels. The NO_OF_PIXELS_IN_A_ROW constant is the horizontal resolution of our desired mode and the NO_OF_PIXELS_IN_A_COLUMN constant is the vertical resolution of our desired mode.

Accessing a single pixel in the screen will be as easy as screen.pixels[row number][column number] where the row number can be any value in 0..=NO_OF_PIXELS_IN_A_COLUMN-1 and the column number can be any value in 0..=NO_OF_PIXELS_IN_A_ROW-1. The row numbers depend on the number of pixels in a column because the number of pixels in a column is the number of rows. Similarly, the column numbers depend on the number of pixels in a row because the number of pixels in a row is the number of columns.

Screen:
        Column 0    Column 1    Column 2    Column 3
        0           1           2           3
Row 0   0 black     black       black       black
Row 1   1 black     black       black       black
Row 2   2 black     black       black       black
Row 3   3 black     black       black       black
Row 4   4 black     black       black       black

In the above screen pixel array, there are 5 rows because there are 5 pixels in a column. And there are 4 columns because there are 4 pixels in a row.

The first pixel in the first row of a screen(the upper left corner pixel) will be screen.pixels[0][0] while the last pixel in the last row of the screen (the bottom right corner pixel) will be screen.pixels[NO_OF_PIXELS_IN_A_ROW - 1][NO_OF_PIXELS_IN_A_COLUMN - 1].

One more thing is missing here:

const NO_OF_PIXELS_IN_A_ROW: usize = 640;
const NO_OF_PIXELS_IN_A_COLUMN: usize = 480;

#[repr(transparent)] // NEW
struct Screen {
    pixels: [[Pixel; NO_OF_PIXELS_IN_A_ROW]; NO_OF_PIXELS_IN_A_COLUMN]
}

The #[repr(transparent)] added above tells the compiler that the way Screen is represented in memory is exactly how its sole field (which is the pixels array in our case) is represented. Why do we need this exactly? Well, the struct is going to be used to interpret some area of memory whose layout is precisely the layout of the struct's sole field. The #[repr(transparent)] gives a guarantee that the layout of this struct will definitely be the layout of the pixels array.

Now, what do we do next?

We take the address of the pixel memory and interpret it as a pointer to an instance of Screen. Then from there, the color of any pixel that we place in the instance of the Screen struct will reflect on the screen.

So what do we want to draw on the screen now?

For now, we're just going to draw a bunch of lines, or more specifically, we're going to draw a bunch of equally spaced red horizontal lines from the top of the screen to the bottom:

Screen:

---------------------
|                   |
|-------------------| red line
|                   |
|-------------------| red line
|                   |
|--------------------

Before we continue, think about how you'll do this yourself.

With our screen model as a 2D array of pixels, this shouldn't be too hard. We just pick some number, let's say 20, as the space that will be in between the horizontal lines and we pick some color, let's say red, that will be the color of the horizontal lines we'll be drawing. And do this:

Draw-Lines

  1. Set row_number to be the current row number, initialized to 0
  2. Increase the row_number SPACE times (in our case, 20)
  3. If row_number is not valid anymore (if row_number >= NO_OF_PIXELS_PER_COLUMN), end
  4. Else if row_number is still valid, Draw-Line
  5. Goto step 2

Draw-Line

  1. Set column_number to be the current column number, initialized to 0
  2. Place a red pixel in the screen at position screen.pixels[row_number][column_number]
  3. Increase column_number by 1
  4. If column_number is not valid anymore (if column_number >= NO_OF_PIXELS_PER_ROW), end
  5. Else if column_number is still valid, goto step 2

The above procedure for drawing these lines just increases the current row number by the space value. If the row number is valid after an increase, it draws a red line (sets all the pixels in that row to the color red).

More concisely:

row_number <- 0
space <- 20
repeat
    row_number <- row_number + space
    if row_number >= NO_OF_PIXELS_IN_A_COLUMN
        break
    else
        column_number <- 0
        while column_number < NO_OF_PIXELS_IN_A_ROW
            screen.pixels[row_number][column_number] <- red pixel
            column_number <- column_number + 1

We modify our efi_main as follows:

#![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 boot_services = unsafe { (*sys_table).boot_services };
    let gop_guid = Guid {
        first_chunk: 0x9042a9de,
        second_chunk: 0x23dc,
        third_chunk: 0x4a38,
        other_chunks: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]
    };
    let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
    let guid_ptr = &gop_guid as *const Guid;
    let registration = core::ptr::null_mut();
    let gop_ptr = &mut gop as *mut _;
    let locate_gop_status = unsafe { ((*boot_services).locate_protocol)(
        guid_ptr,
        registration,
        gop_ptr
    ) };

    if locate_gop_status != 0 {
        let mut string_u16 = [0u16; 22];
        let string = "Failed to locate GOP\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()); }
        loop {}
    }

    let gop = gop as *mut GraphicsOutput;
    let mode = unsafe { (*gop).mode };
    let max_mode = unsafe { (*mode).max_mode };
    let mut desired_mode = 0;

    for mode_number in 0..max_mode {
        let size_of_info = core::mem::size_of::<GraphicsModeInfo>();
        let mut mode: *const GraphicsModeInfo = core::ptr::null_mut();
        let query_mode = unsafe { (*gop).query_mode };
        let query_status = (query_mode)(
            gop,
            mode_number,
            &size_of_info as *const _,
            &mut mode as *mut _
        );
        if query_status != 0 {
            let mut string_u16 = [0u16; 19];
            let string = "query_mode failed\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()); }
            loop {}
        }
        
        let horizontal_resolution = unsafe { (*mode).horizontal_resolution };
        let vertical_resolution = unsafe { (*mode).vertical_resolution };
        let pixel_format = unsafe { (*mode).pixel_format };
        if horizontal_resolution == 640 && vertical_resolution == 480
            && pixel_format == PixelFormat::BlueGreenRedReserved {
            desired_mode = mode_number;
            break;
        }
        if mode_number == max_mode - 1 {
            let mut string_u16 = [0u16; 32];
            let string = "Couldn't find the desired mode\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()); }
            loop {}
        }
    }

    let set_mode_status = unsafe { ((*gop).set_mode)(
        gop,
        desired_mode
    ) };

    if set_mode_status != 0 {
        let mut string_u16 = [0u16; 32];
        let string = "Failed to set the desired mode\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()); }
        loop {}
    } /* DELETED: else {
        let mut string_u16 = [0u16; 35];
        let string = "Successfully set the desired mode\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()); }
    }*/

    // NEW:
    // Setting the address of the pixel memory to framebuffer_base
    let framebuffer_base = unsafe { (*mode).framebuffer_base };

    // Interpreting the address of the pixel memory as an instance of the Screen struct
    let screen = framebuffer_base as *mut Screen;
    
    // The number of uncolored rows that will be inbetween our red horizontal lines
    let space = 20;
    // Initializing row_number to the first row of the screen
    let mut row_number = 0;
    loop {
        // Jumping over lines that we'll not be coloring
        row_number += space;
        // Now that we've increased row_number, is the row_number still valid?
        if row_number >= NO_OF_PIXELS_IN_A_COLUMN {
            // The row_number isn't valid anymore, stop this loop
            break;
        } else {
            // Initializing the column_number to the first column on the row
            let mut column_number = 0;
            // Keep putting red pixels in the row until all pixels in the row are red
            while column_number < NO_OF_PIXELS_IN_A_ROW {
                // Putting a red pixel in the screen at row row_number and column column_number
                unsafe {
                    (*screen).pixels[row_number][column_number] = Pixel {
                        red: u8::MAX,
                        blue: 0,
                        green: 0,
                        reserved: 0
                    };
                }
                // Move to the next column
                column_number += 1;
            }
        }
    }

    0
}

Upon building and running, you should have this output:

Red Lines

We've successfully drawn to the screen.

We're done with our list:

  1. Get boot services table from system table.
  2. Call the LocateProtocol function in the boot services table.
  3. Figure out how to change the mode to a graphics mode with the GOP.
  4. Figure out how to draw on screen.

Take Away

  • The #[repr(transparent)] attribute tells the compiler to represent a struct in memory the exact same way its single field is represented.

Code till 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 boot_services = unsafe { (*sys_table).boot_services };
    let gop_guid = Guid {
        first_chunk: 0x9042a9de,
        second_chunk: 0x23dc,
        third_chunk: 0x4a38,
        other_chunks: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]
    };
    let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
    let guid_ptr = &gop_guid as *const Guid;
    let registration = core::ptr::null_mut();
    let gop_ptr = &mut gop as *mut _;
    let locate_gop_status = unsafe { ((*boot_services).locate_protocol)(
        guid_ptr,
        registration,
        gop_ptr
    ) };

    if locate_gop_status != 0 {
        let mut string_u16 = [0u16; 22];
        let string = "Failed to locate GOP\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()); }
        loop {}
    }

    let gop = gop as *mut GraphicsOutput;
    let mode = unsafe { (*gop).mode };
    let max_mode = unsafe { (*mode).max_mode };
    let mut desired_mode = 0;

    for mode_number in 0..max_mode {
        let size_of_info = core::mem::size_of::<GraphicsModeInfo>();
        let mut mode: *const GraphicsModeInfo = core::ptr::null_mut();
        let query_mode = unsafe { (*gop).query_mode };
        let query_status = (query_mode)(
            gop,
            mode_number,
            &size_of_info as *const _,
            &mut mode as *mut _
        );
        if query_status != 0 {
            let mut string_u16 = [0u16; 19];
            let string = "query_mode failed\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()); }
            loop {}
        }
        
        let horizontal_resolution = unsafe { (*mode).horizontal_resolution };
        let vertical_resolution = unsafe { (*mode).vertical_resolution };
        let pixel_format = unsafe { (*mode).pixel_format };
        if horizontal_resolution == 640 && vertical_resolution == 480
            && pixel_format == PixelFormat::BlueGreenRedReserved {
            desired_mode = mode_number;
            break;
        }
        if mode_number == max_mode - 1 {
            let mut string_u16 = [0u16; 32];
            let string = "Couldn't find the desired mode\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()); }
            loop {}
        }
    }

    let set_mode_status = unsafe { ((*gop).set_mode)(
        gop,
        desired_mode
    ) };

    if set_mode_status != 0 {
        let mut string_u16 = [0u16; 32];
        let string = "Failed to set the desired mode\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()); }
        loop {}
    }

    let framebuffer_base = unsafe { (*mode).framebuffer_base };
    let screen = framebuffer_base as *mut Screen;
    let space = 20;
    let mut row_number = 0;
    loop {
        row_number += space;
        if row_number >= NO_OF_PIXELS_IN_A_COLUMN {
            break;
        } else {
            let mut column_number = 0;
            while column_number < NO_OF_PIXELS_IN_A_ROW {
                unsafe {
                    (*screen).pixels[row_number][column_number] = Pixel {
                        red: u8::MAX,
                        blue: 0,
                        green: 0,
                        reserved: 0
                    };
                }
                column_number += 1;
            }
        }
    }

    0
}

const NO_OF_PIXELS_IN_A_ROW: usize = 640;
const NO_OF_PIXELS_IN_A_COLUMN: usize = 480;

#[repr(transparent)]
struct Screen {
    pixels: [[Pixel; NO_OF_PIXELS_IN_A_ROW]; NO_OF_PIXELS_IN_A_COLUMN]
}

fn printdigit(n: u32, simple_text_output: *mut SimpleTextOutput) {
    let mut digit_u16 = [48 + n as u16, 0];
    unsafe { ((*simple_text_output).output_string)(simple_text_output, digit_u16.as_mut_ptr()); }
}

fn printint(n: u32, simple_text_output: *mut SimpleTextOutput) {
    if n >= 10 {
        let quotient = n / 10;
        let remainder = n % 10;
        printint(quotient, simple_text_output);
        printdigit(remainder, simple_text_output);
    } else {
        printdigit(n, simple_text_output);
    }
}

#[repr(C)]
struct SystemTable {
    unneeded: [u8; 60],
    simple_text_output: *mut SimpleTextOutput,
    unneeded2: [u8; 24],
    boot_services: *const BootServices
}

#[repr(C)]
struct Guid {
    first_chunk: u32,
    second_chunk: u16,
    third_chunk: u16,
    other_chunks: [u8; 8]
}

type Status = usize;

#[repr(C)]
struct BootServices {
    unneeded: [u8; 320],
    locate_protocol: extern "efiapi" fn(
        protocol: *const Guid,
        registration: *const core::ffi::c_void,
        interface: *mut *mut core::ffi::c_void
    ) -> Status
}


#[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
    ) -> Status,
    set_mode: extern "efiapi" fn(
        this: *mut GraphicsOutput,
        mode_number: u32
    ) -> Status,
    unneeded: [u8; 8],
    mode: *const GraphicsMode
}

#[repr(C)]
struct GraphicsModeInfo {
    version: u32,
    horizontal_resolution: u32,
    vertical_resolution: u32,
    pixel_format: PixelFormat,
    pixel_info: PixelBitmask,
    pixels_per_scan_line: u32
}

#[derive(PartialEq, Clone, Copy)]
#[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
}

type PhysAddr = u64;

#[repr(C)]
struct GraphicsMode {
    max_mode: u32,
    mode: u32,
    info: *const GraphicsModeInfo,
    size_of_info: usize,
    framebuffer_base: PhysAddr,
    framebuffer_size: usize
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

In the Next Post

We'll get started on our quest to print hello world again

References

  • https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent