Demilade Sonuga's blog

All posts

Drawing Bitmaps I

2022-12-12

In the previous post, we learned some things about bitmaps. Now, we're going to draw them. First, download the image we're going to draw from

here

The image we're drawing is a block:

A block

Before we proceed, we must first ask ourselves: What are the steps that must be taken to get the block on the screen?

When we say you "downloaded that BMP image", what we really mean is that the bits representing the image have been transferred over the network from the GitHub servers to your computer. Those bits are in the BMP file format. This means that bits in specific locations have meaning according to the specification of the BMP file format which was described in the previous post.

So, drawing the bitmap on screen is just a matter of getting those bits into our code and putting the colors described by the pixel array into the screen's memory. Our steps to draw bitmaps on screen now looks like this:

  1. Get the block's bits into our code.
  2. Get the colors described by the pixel array on the screen.

To get the block's bits into our code, we first put the image in our directory:

blasterball/
| .cargo/
| | config.toml
| src/
| | font.rs
| | main.rs
| | uefi.rs
| | block.bmp // NEW
| .gitignore
| Cargo.lock
| Cargo.toml

Now, we modify our efi_main:

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ... Others

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    
    let screen = unsafe { &mut *screen };

    // DELETED: print_str(screen, "Hello World!");

    // Throw block.bmp's bits into the output binary
    // Retrieve a slice to those bits
    let block_bytes = include_bytes!("./block.bmp");

    0
}

The include_bytes!("./block.bmp") above tells the compiler that it should look for the file block.bmp in the current directory, grab the file's bits, and put it somewhere in the output binary at build time. If you build now and compare the output binary before adding this line to after, you'll see that there is a significant increase in the binary's size. This is because the BMP image is fully contained in the binary.

block_bytes is a slice of bytes (&[u8]) which refers to the block.bmp bits. So, block_bytes[0] is the first byte in the block.bmp file, and block_bytes[block_bytes.len() - 1] is the last byte in the block.bmp file.

Now, we're done with step 1. We have the block's bits in our code.

Onto step 2:

  1. Get the colors described by the pixel array on the screen.

From the specification of the BMP file format, we can determine where exactly the pixel array and the color table will be.

The color table, which defines the index to color mapping is at an offset of 124 bytes from the start of the file. So, the first byte in block.bmp is block_bytes[124]. The pixel array which contains the pixel color descriptions of the picture in terms of color table indexes is immediately after the color table. So, the first byte in the pixel array is block_bytes[124 + color_table_length]. The pixel array describes pixel colors in terms of rows and columns, so the length of one row will be the image width and the length of one column will be the image height. We can get the image width and height at offsets 4 and 8 from the starting point of the file, respectively.

At this point, we have all we need to start drawing on the screen but doing it now will be messy and it will be easy to make a mistake.

Before we proceed to draw the actual bitmap on the screen, we first need to model the structure of a bitmap.

Take Away

  • include_bytes(filename) puts the file with the name filename into the output binary.

Code till now:

Directory view:

blasterball/
| .cargo/
| | config.toml
| src/
| | block.bmp
| | font.rs
| | main.rs
| | uefi.rs
| .gitignore
| Cargo.lock
| Cargo.toml

main.rs contents

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ... Others

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    
    let screen = unsafe { &mut *screen };

    // DELETED: print_str(screen, "Hello World!");

    // Throw block.bmp's bits into the output binary
    // Retrieve a slice to those bits
    let block_bytes = include_bytes!("./block.bmp");

    0
}

In the Next Post

We'll get on to modeling the bitmap