One of the first thing we want to do is make our Operating System capable of producing some kind of screen output, even if not strictly necessary (there can be different ways to debug our OS behaviour while developing), it can be useful sometime to visualize something in real time, and probably especially if at the beginning of our project, is probably very motivating having our os print a nice logo, or write some fancy text.
As per many other parts there’s a few ways we can get output on the screen, in this book we’re going to use a linear framebuffer (linear meaning all its pixels are arranged directly after each other in memory), but historically there have been other ways to display output, some of them listed below:
0xB800
address. This buffer is comprised of pairs of bytes, the first being the ascii character to display, and the second encodes the foreground and background colour.In these chapters we’re going to use a linear framebuffer, since it’s the only framebuffer type reliably available on x86_64
and other platforms.
One way to enable framebuffer is asking grub to do it (this can be done also using uefi but it is not covered in this chapter).
To enable it, we need to add the relevant tag in the multiboot2 header. Simply we just need to add in the tag section a new item, like the one below, to request to grub to enable the framebuffer if available, with the requested configuration:
framebuffer_tag_start:
dw 0x05 ;Type: framebuffer
dw 0x01 ;Optional tag
dd framebuffer_tag_end - framebuffer_tag_start ;size
dd 0 ;Width - if 0 we let the bootloader decide
dd 0 ;Height - same as above
dd 0 ;Depth - same as above
framebuffer_tag_end:
In this case we let the bootloader decide for us the framebuffer configuration. Width
and Heigth
field are self explanatory, while the depth
field indicates the number of bits per pixel in a graphic mode.
Once the framebuffer is set in the multiboot header, when grub loads the kernel it should add a new tag: the framebuffer_info
tag. As explained in the Multiboot paragraph, if using the header provided in the documentation, there should already be a struct multiboot_tag_framebuffer, otherwise we should create our own.
The basic structure of the framebuffer info tag is:
Size | Description |
---|---|
u32 | type = 8 |
u32 | size |
u64 | framebuffer_addr |
u32 | framebuffer_pitch |
u32 | framebuffer_width |
u32 | framebuffer_height |
u8 | framebuffer_bpp |
u8 | framebuffer_type |
u8 | reserved |
varies | color_info |
Where:
color_index
.Pitch is the number of bytes on each row. bpp is same as depths.
Depending on the framebuffer_type
value there can be different values for color_info
field.
color-info
field has the following values:Size | Description |
---|---|
u32 | framebuffer_palette_num_colors |
varies | framebuffer_palette |
The framebuffer_palette_num_colors
is the number of colors available in the palette, and the framebuffer palette is an array of colour descriptors, where every colour has the following structure:
Size | Description |
---|---|
u8 | red_val |
u8 | green_val |
u8 | blue_val |
color_type
is defined as follows:Size | Description |
---|---|
u8 | framebuffer_red_field_position |
u8 | framebuffer_red_mask_size |
u8 | framebuffer_green_field_position |
u8 | framebuffer_green_mask_size |
u8 | framebuffer_blue_field_position |
u8 | framebuffer_blue_mask_size |
Where framebuffer_XXX_field_position
is the starting bit of the color XXX, and the framebuffer_XXX_mask_size
is the size in bits of the color XXX. Usually the format is 0xRRGGBB (is the same format used in HTML).
framebuffer-bpp = 16
and framebuffer_pitch
is expressed in byte text per line.Everything that we see on the screen with the framebuffer enabled will be done by the function that plot pixels.
Plotting a pixel is pretty easy, we just need to fill the value of a specific address with the colour we want for it. What we need for drawing a pixel then is:
x
and y
coordinates)The first thing we need to do when we want to plot a pixel is to compute the address of the pixel at row y and column x. To do it we first need to know how many bytes are in one row, and how many bytes are in one pixel. These information are present in the multiboot framebuffer info tag:
If we want to know the actual row offset we need then to:
\[row = y * framebuffer_{pitch}\]and similarly for the column we need to:
\[column = x * bpp\]Now we have the offset in byte for both the row and column byte, to compute the absolute address of our pixel we need just need to add row and column to the base address:
\[pixel_{position} = base_{address} + column + row\]This address is the location where we are going to write a colour value and it will be displayed on our screen.
Be aware that grub is giving us a physical address for the framebuffer_base. When enabling virtual memory (refer to the Memory Management part) be sure to map the framebuffer somewhere so that it can be still accessible, otherwise a Page Fault Exception will be triggered!
Now that we have a plot pixel function is time to draw something nice on the screen. Usually to do this we should have a file system supported, and at least an image format implemented. But some graphic tools, like The Gimp provide an option to save an image into C source code header
, or C source code
.
If we save the image as C source header code, we get a .h
file with a variable static char* header_data
, and few extra attribute variables that contains the width and height of the image, and also a helper function called HEADER_PIXEL
that extract the pixel and move to the next at every call:
The helper function is called in the following way:
HEADER_PIXEL(logo_data, pixel)
where logo_data
is a pointer to the image content and pixel
is an array of 4 chars, that will contain the pixel values.
Now since each pixel is identified by 3 colors and we have 4 elements into an array, we know that the last element (pixel[3]
) is always zero. The color is encoded in RGB format with Blue being the least significant byte, and to plot that pixel we need to fill a 32 bit address, so the array need to be converted into a uint32_t
variable, this can easily be done with some bitwise operations:
unsigned char pixel[4];
HEADER_PIXEL(logo_data, pixel)
pixel[3] = 0;
uint32_t num = (uint32_t) pixel[3] << 24 |
(uint32_t)pixel[0] << 16 |
(uint32_t)pixel[1] << 8 |
(uint32_t)pixel[2];
Note that we used a unsigned char
type, while gimp is providing for a static char
type, the reason is because char
type can be signed or unsigned depending on the platform. So if values are greater than 127 are used, they may be intended as negative values, if char
is signed, when they are cast to uint32_t
the sign can be extended, leading unexpected results.
In the code above we are making sure that the value of pixel[3]
is zero, since the HEADER_PIXEL
function is not touching it. Now the value of num
will be the colour of the pixel to be plotted.
With this value we can call the function we have created to plot the pixel with the color indicated by num
.
Using width and height given by the gimp header, and a given starting position x, y to draw an image we just need to iterate through the pixels using a nested for loop, to iterate through rows (x) and columns (y) using height and width as limits.