mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-20 16:07:49 +00:00
Compare commits
23 Commits
2676d69c31
...
edition-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3550ea87 | ||
|
|
ecb60ec326 | ||
|
|
8a1267477a | ||
|
|
ce01059620 | ||
|
|
4d0c3ac188 | ||
|
|
d565cd125b | ||
|
|
ca86085360 | ||
|
|
5f3d38884c | ||
|
|
f557d1c698 | ||
|
|
0c248d027e | ||
|
|
2cf0675a2d | ||
|
|
916ad36e78 | ||
|
|
3c2e91fa4e | ||
|
|
c9683a2cd9 | ||
|
|
514736b1d8 | ||
|
|
647b509971 | ||
|
|
1118350b16 | ||
|
|
fb096a7484 | ||
|
|
8a41fd65bf | ||
|
|
50802c8332 | ||
|
|
ba410f40ba | ||
|
|
a119d36cc9 | ||
|
|
9080e69a09 |
@@ -445,7 +445,7 @@ Afterwards, you can run the tools through `rust-nm`, `rust-objdump`, and `rust-s
|
|||||||
|
|
||||||
[`cargo-binutils`]: https://github.com/rust-embedded/cargo-binutils
|
[`cargo-binutils`]: https://github.com/rust-embedded/cargo-binutils
|
||||||
|
|
||||||
### `nm`
|
### List Symbols using `nm`
|
||||||
|
|
||||||
We defined a `_start` function as the entry point of our kernel.
|
We defined a `_start` function as the entry point of our kernel.
|
||||||
To verify that it is properly exposed in the executable, we can run `nm` to list all the symbols defined in the executable:
|
To verify that it is properly exposed in the executable, we can run `nm` to list all the symbols defined in the executable:
|
||||||
@@ -463,7 +463,7 @@ If we comment out the `_start` function or if we remove the `#[no_mangle]` attri
|
|||||||
|
|
||||||
This way we can ensure that we set the `_start` function correctly.
|
This way we can ensure that we set the `_start` function correctly.
|
||||||
|
|
||||||
### `objdump`
|
### Inspect ELF File using `objdump`
|
||||||
|
|
||||||
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
|
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ UEFI, in contrast, is more modern and has much more features, but also more comp
|
|||||||
### BIOS
|
### BIOS
|
||||||
|
|
||||||
Almost all x86 systems have support for BIOS booting, including most UEFI-based machines that support an emulated BIOS.
|
Almost all x86 systems have support for BIOS booting, including most UEFI-based machines that support an emulated BIOS.
|
||||||
This is great, because you can use the same boot logic across all machines from the last centuries.
|
This is great, because you can use the same boot logic across all machines from the last century.
|
||||||
The drawback is that the standard is very old, for example the CPU is put into a 16-bit compatibility mode called [real mode] before booting so that archaic bootloaders from the 1980s would still work.
|
The drawback is that the standard is very old, for example the CPU is put into a 16-bit compatibility mode called [real mode] before booting so that archaic bootloaders from the 1980s would still work.
|
||||||
Also, BIOS-compatibility will be slowly removed on newer UEFI machines over the next years (see below).
|
Also, BIOS-compatibility will be slowly removed on newer UEFI machines over the next years (see below).
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ This structure has the following general format:
|
|||||||
| 446 | partition entry 1 | 16 |
|
| 446 | partition entry 1 | 16 |
|
||||||
| 462 | partition entry 2 | 16 |
|
| 462 | partition entry 2 | 16 |
|
||||||
| 478 | partition entry 3 | 16 |
|
| 478 | partition entry 3 | 16 |
|
||||||
| 444 | partition entry 4 | 16 |
|
| 494 | partition entry 4 | 16 |
|
||||||
| 510 | boot signature | 2 |
|
| 510 | boot signature | 2 |
|
||||||
|
|
||||||
The bootstrap code is commonly called the _bootloader_ and responsible for loading and starting the operating system kernel.
|
The bootstrap code is commonly called the _bootloader_ and responsible for loading and starting the operating system kernel.
|
||||||
@@ -292,7 +292,7 @@ There are a few notable things:
|
|||||||
|
|
||||||
To verify that the `entry_point` macro worked as expected, we can use the `objdump` tool as [described in the previous post][objdump-prev]. First, we recompile using `cargo build --target x86_64-unknown-none`, then we inspect the section headers using `objdump` or `rust-objdump`:
|
To verify that the `entry_point` macro worked as expected, we can use the `objdump` tool as [described in the previous post][objdump-prev]. First, we recompile using `cargo build --target x86_64-unknown-none`, then we inspect the section headers using `objdump` or `rust-objdump`:
|
||||||
|
|
||||||
[objdump-prev]: @/edition-3/posts/01-minimal-kernel/index.md#objdump
|
[objdump-prev]: @/edition-3/posts/01-minimal-kernel/index.md#inspect-elf-file-using-objdump
|
||||||
|
|
||||||
```bash,hl_lines=8
|
```bash,hl_lines=8
|
||||||
❯ rust-objdump -h target/x86_64-unknown-none/debug/kernel
|
❯ rust-objdump -h target/x86_64-unknown-none/debug/kernel
|
||||||
@@ -491,7 +491,7 @@ profile = "default"
|
|||||||
targets = ["x86_64-unknown-none"]
|
targets = ["x86_64-unknown-none"]
|
||||||
```
|
```
|
||||||
|
|
||||||
The `channel` field specifies which [toolchain`] to use.
|
The `channel` field specifies which [`toolchain`] to use.
|
||||||
In our case, we want to use the latest nightly compiler.
|
In our case, we want to use the latest nightly compiler.
|
||||||
We could also specify a specific nightly here, e.g. `nightly-2023-04-30`, which can be useful when there is some breakage in the newest nightly.
|
We could also specify a specific nightly here, e.g. `nightly-2023-04-30`, which can be useful when there is some breakage in the newest nightly.
|
||||||
In the `targets` list, we can specify additional targets that we want to compile to.
|
In the `targets` list, we can specify additional targets that we want to compile to.
|
||||||
@@ -706,7 +706,7 @@ We then use the the `create_uefi_image` and `create_bios_image` methods to creat
|
|||||||
[requires build scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
|
[requires build scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
|
||||||
[`join`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.join
|
[`join`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.join
|
||||||
|
|
||||||
We can now use use a simple `cargo build` to cross-compile our kernel, build the bootloader, and combine them to create a bootable disk image:
|
We can now use a simple `cargo build` to cross-compile our kernel, build the bootloader, and combine them to create a bootable disk image:
|
||||||
|
|
||||||
```
|
```
|
||||||
❯ cargo build
|
❯ cargo build
|
||||||
@@ -1011,27 +1011,35 @@ Now we can create our `qemu-uefi` executable at `src/bin/qemu-uefi.rs`:
|
|||||||
|
|
||||||
```rust ,hl_lines=3-15
|
```rust ,hl_lines=3-15
|
||||||
// src/bin/qemu-uefi.rs
|
// src/bin/qemu-uefi.rs
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env, process::{self, Command}
|
||||||
process::{self, Command},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use ovmf_prebuilt::{Arch, FileType, Prebuilt, Source};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let prebuilt =
|
||||||
|
Prebuilt::fetch(Source::LATEST, "target/ovmf").unwrap();
|
||||||
|
let ovmf_code = prebuilt.get_file(Arch::X64, FileType::Code);
|
||||||
|
let ovmf_vars = prebuilt.get_file(Arch::X64, FileType::Vars);
|
||||||
let mut qemu = Command::new("qemu-system-x86_64");
|
let mut qemu = Command::new("qemu-system-x86_64");
|
||||||
qemu.arg("-drive");
|
qemu.args([
|
||||||
qemu.arg(format!("format=raw,file={}", env!("UEFI_IMAGE")));
|
"-drive",
|
||||||
qemu.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi());
|
&format!("format=raw,if=pflash,readonly=on,file={}", ovmf_code.display()),
|
||||||
|
"-drive",
|
||||||
|
&format!("format=raw,if=pflash,file={}", ovmf_vars.display()),
|
||||||
|
"-drive",
|
||||||
|
&format!("format=raw,file={}", env!("UEFI_IMAGE")),
|
||||||
|
]);
|
||||||
let exit_status = qemu.status().unwrap();
|
let exit_status = qemu.status().unwrap();
|
||||||
process::exit(exit_status.code().unwrap_or(-1));
|
process::exit(exit_status.code().unwrap_or(-1));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
It's very similar to our `qemu-bios` executable.
|
It's very similar to our `qemu-bios` executable.
|
||||||
The only two differences are that it passes an additional `-bios` argument and that it uses the `UEFI_IMAGE` instead of the `BIOS_IMAGE`.
|
The only two differences are that it passes two additional `-drive if=pflash,..` arguments to load UEFI firmware (`OVMF_CODE.fd`) and writable NVRAM (`OVMF_VARS.fd`), and that it uses the `UEFI_IMAGE` instead of the `BIOS_IMAGE`.
|
||||||
Using a quick `cargo run --bin qemu-uefi`, we can confirm that it works as intended.
|
Using a quick `cargo run --bin qemu-uefi`, we can confirm that it works as intended.
|
||||||
|
|
||||||
|
|
||||||
### Screen Output
|
### Screen Output
|
||||||
|
|
||||||
While we see some screen output from the bootloader, our kernel still does nothing.
|
While we see some screen output from the bootloader, our kernel still does nothing.
|
||||||
@@ -1047,7 +1055,7 @@ Since the size, pixel format, and memory location of the framebuffer can vary be
|
|||||||
The easiest way to do this is to read it from the [boot information structure][`BootInfo`] that the bootloader passes as argument to our kernel entry point:
|
The easiest way to do this is to read it from the [boot information structure][`BootInfo`] that the bootloader passes as argument to our kernel entry point:
|
||||||
|
|
||||||
```rust ,hl_lines=3 7-13
|
```rust ,hl_lines=3 7-13
|
||||||
// in src/kernel/main.rs
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
use bootloader_api::BootInfo;
|
use bootloader_api::BootInfo;
|
||||||
|
|
||||||
@@ -1083,7 +1091,7 @@ For now, let's just try setting the whole screen to some color.
|
|||||||
For this, we just set every pixel in the byte slice to some fixed value:
|
For this, we just set every pixel in the byte slice to some fixed value:
|
||||||
|
|
||||||
```rust ,hl_lines=5-7
|
```rust ,hl_lines=5-7
|
||||||
// in src/kernel/main.rs
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ Using the [`BootInfo`] provided by the bootloader, we were able to access a spec
|
|||||||
We wrote some example code to display a gray background:
|
We wrote some example code to display a gray background:
|
||||||
|
|
||||||
[previous post]: @/edition-3/posts/02-booting/index.md
|
[previous post]: @/edition-3/posts/02-booting/index.md
|
||||||
[`BootInfo`]: https://docs.rs/bootloader_api/latest/bootloader_api/info/struct.BootInfo.html
|
[`BootInfo`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.BootInfo.html
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/kernel/main.rs
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||||
@@ -65,22 +65,315 @@ The pixels are laid out line by line, typically starting at the top.
|
|||||||
|
|
||||||
For example, the pixels of an image with width 10 and height 3 would be typically stored in this order:
|
For example, the pixels of an image with width 10 and height 3 would be typically stored in this order:
|
||||||
|
|
||||||
<table><tbody>
|
<table style = "width: fit-content;"><tbody>
|
||||||
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr>
|
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr>
|
||||||
<tr><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr>
|
<tr><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr>
|
||||||
<tr><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td></tr>
|
<tr><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td></tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
|
|
||||||
So top left pixel is stored at offset 0 in the bitmap array.
|
So top left pixel is stored at offset 0 in the bitmap array.
|
||||||
The pixel on its right is at offset `pixel_size`.
|
The pixel on its right is at pixel offset 1.
|
||||||
The first pixel of the next line starts at offset `line_length * pixel_size`.
|
The first pixel of the next line starts at pixel offset `line_length`, which is 10 in this case.
|
||||||
|
The last line starts at pixel offset 20, which is `line_length * 2`.
|
||||||
|
|
||||||
### Padding
|
### Padding
|
||||||
|
|
||||||
Depending on the hardware and GPU firmware, it is often more efficient
|
Depending on the hardware and GPU firmware, it is often more efficient to make lines start at well-aligned offsets.
|
||||||
|
Because of this, there is often some additional padding at the end of each line.
|
||||||
|
So the actual memory layout of the 10x3 example image might look like this, with the padding marked as yellow:
|
||||||
|
|
||||||
|
<table style = "width: fit-content;"><tbody>
|
||||||
|
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td style="background-color:yellow;">10</td><td style="background-color:yellow;">11</td></tr>
|
||||||
|
<tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td><td>20</td><td>21</td><td style="background-color:yellow;">22</td><td style="background-color:yellow;">23</td></tr>
|
||||||
|
<tr><td>24</td><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td><td>30</td><td>31</td><td>32</td><td>33</td><td style="background-color:yellow;">34</td><td style="background-color:yellow;">35</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
So now the second line starts at pixel offset 12.
|
||||||
|
The two pixels at the end of each line are considered as padding and ignored.
|
||||||
|
So if we want to set the first pixel of the second line, we need to be aware of the additional padding and set the pixel at offset 12 instead of offset 10.
|
||||||
|
|
||||||
|
The line length plus the padding bytes is typically called the _stride_ or _pitch_ of the buffer.
|
||||||
|
In the example above, the stride is 12 and the line length is 10.
|
||||||
|
|
||||||
|
Since the amount of padding depends on the hardware, the stride is only known at runtime.
|
||||||
|
The `bootloader` crate queries the framebuffer parameters from the UEFI or BIOS firmware and reports them as part of the `BootInfo`.
|
||||||
|
It provides the stride of the framebuffer, among other parameters, in form of a [`FrameBufferInfo`] struct that can be created using the [`FrameBuffer::info`] method.
|
||||||
|
|
||||||
|
[`FrameBufferInfo`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBufferInfo.html
|
||||||
|
[`FrameBuffer::info`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBuffer.html#method.info
|
||||||
|
|
||||||
### Color formats
|
### Color formats
|
||||||
|
|
||||||
|
The [`FrameBufferInfo`] also specifies the [`PixelFormat`] of the framebuffer, which also depends on the underlying hardware.
|
||||||
|
Using this information, we can set pixels to different colors.
|
||||||
|
For example, the [`PixelFormat::Rgb`] variant specifies that each pixel is represented in the [RGB color space], which stores the red, green, and blue parts of the pixel as separate bytes.
|
||||||
|
In this model, the color red would be represented as the three bytes `[255, 0, 0]`, or `0xff0000` in [hexadecimal representation].
|
||||||
|
The color yellow is represented the addition of red and green, which results in `[255, 255, 0]` (or `0xffff00` in hexadecimal representation).
|
||||||
|
|
||||||
|
[`PixelFormat`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/enum.PixelFormat.html
|
||||||
|
[`PixelFormat::Rgb`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/enum.PixelFormat.html#variant.Rgb
|
||||||
|
[RGB color space]: https://en.wikipedia.org/wiki/RGB_color_spaces
|
||||||
|
[hexadecimal representation]: https://en.wikipedia.org/wiki/RGB_color_model#Numeric_representations
|
||||||
|
|
||||||
|
While the `Rgb` format is most common, there are also framebuffers that use a different color format.
|
||||||
|
For example, the [`PixelFormat::Bgr`] stores the three colors in inverted order, i.e. blue first and red last.
|
||||||
|
There are also buffers that don't support colors at all and can represent only grayscale pixels.
|
||||||
|
The `bootloader_api` crate reports such buffers as [`PixelFormat::U8`].
|
||||||
|
|
||||||
|
[`PixelFormat::Bgr`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/enum.PixelFormat.html#variant.Bgr
|
||||||
|
[`PixelFormat::U8`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/enum.PixelFormat.html#variant.U8
|
||||||
|
|
||||||
|
Note that there might be some additional padding at the pixel-level as well.
|
||||||
|
For example, an `Rgb` pixel might be stored as 4 bytes instead of 3 to ensure 32-bit alignment.
|
||||||
|
The number of bytes per pixel is reported by the bootloader in the [`FrameBufferInfo::bytes_per_pixel`] field.
|
||||||
|
|
||||||
|
[`FrameBufferInfo::bytes_per_pixel`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBufferInfo.html#structfield.bytes_per_pixel
|
||||||
|
|
||||||
|
## Setting specific Pixels
|
||||||
|
|
||||||
|
Based on this above details, we can now create a function to set a specific pixel to a certain color.
|
||||||
|
We start by creating a new `framebuffer` [module]:
|
||||||
|
|
||||||
|
[module]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
|
||||||
|
|
||||||
|
```rust ,hl_lines=3-5
|
||||||
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
|
// declare a submodule -> the compiler will automatically look
|
||||||
|
// for a file named `framebuffer.rs` or `framebuffer/mod.rs`
|
||||||
|
mod framebuffer;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the new module, we create basic structs for representing pixel positions and colors:
|
||||||
|
|
||||||
|
```rust ,hl_lines=3-16
|
||||||
|
// in new kernel/src/framebuffer.rs file
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Position {
|
||||||
|
pub x: usize,
|
||||||
|
pub y: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Color {
|
||||||
|
pub red: u8,
|
||||||
|
pub green: u8,
|
||||||
|
pub blue: u8,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By marking the structs and their fields as `pub`, we make them accessible from the parent `kernel` module.
|
||||||
|
We use the `#[derive]` attribute to implement the [`Debug`], [`Clone`], [`Copy`], [`PartialEq`], and [`Eq`] traits of Rust's core library.
|
||||||
|
These traits allow us to duplicate, compare, and print the structs.
|
||||||
|
|
||||||
|
[`Debug`]: https://doc.rust-lang.org/stable/core/fmt/trait.Debug.html
|
||||||
|
[`Clone`]: https://doc.rust-lang.org/stable/core/clone/trait.Clone.html
|
||||||
|
[`Copy`]: https://doc.rust-lang.org/stable/core/marker/trait.Copy.html
|
||||||
|
[`PartialEq`]: https://doc.rust-lang.org/stable/core/cmp/trait.PartialEq.html
|
||||||
|
[`Eq`]: https://doc.rust-lang.org/stable/core/cmp/trait.Eq.html
|
||||||
|
|
||||||
|
Next, we create a function for setting a specific pixel in the framebuffer to a given color:
|
||||||
|
|
||||||
|
```rust ,hl_lines=3 5-39
|
||||||
|
// in new kernel/src/framebuffer.rs file
|
||||||
|
|
||||||
|
use bootloader_api::info::{FrameBuffer, PixelFormat};
|
||||||
|
|
||||||
|
pub fn set_pixel_in(framebuffer: &mut FrameBuffer, position: Position, color: Color) {
|
||||||
|
let info = framebuffer.info();
|
||||||
|
|
||||||
|
// calculate offset to first byte of pixel
|
||||||
|
let byte_offset = {
|
||||||
|
// use stride to calculate pixel offset of target line
|
||||||
|
let line_offset = position.y * info.stride;
|
||||||
|
// add x position to get the absolute pixel offset in buffer
|
||||||
|
let pixel_offset = line_offset + position.x;
|
||||||
|
// convert to byte offset
|
||||||
|
pixel_offset * info.bytes_per_pixel
|
||||||
|
};
|
||||||
|
|
||||||
|
// set pixel based on color format
|
||||||
|
let pixel_buffer = &mut framebuffer.buffer_mut()[byte_offset..];
|
||||||
|
match info.pixel_format {
|
||||||
|
PixelFormat::Rgb => {
|
||||||
|
pixel_buffer[0] = color.red;
|
||||||
|
pixel_buffer[1] = color.green;
|
||||||
|
pixel_buffer[2] = color.blue;
|
||||||
|
}
|
||||||
|
PixelFormat::Bgr => {
|
||||||
|
pixel_buffer[0] = color.blue;
|
||||||
|
pixel_buffer[1] = color.green;
|
||||||
|
pixel_buffer[2] = color.red;
|
||||||
|
}
|
||||||
|
PixelFormat::U8 => {
|
||||||
|
// use a simple average-based grayscale transform
|
||||||
|
let gray = color.red / 3 + color.green / 3 + color.blue / 3;
|
||||||
|
pixel_buffer[0] = gray;
|
||||||
|
}
|
||||||
|
other => panic!("unknown pixel format {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first step is to calculate the byte offset within the framebuffer slice at which the pixel starts.
|
||||||
|
For this, we first calculate the pixel offset of the line by multiplying the `y` position with the stride of the framebuffer, i.e. its line width plus the line padding.
|
||||||
|
We then add the `x` position to get the absolute index of the pixel.
|
||||||
|
As the framebuffer slice is a byte slice, we need to transform the pixel index to a byte offset by multiplying it with the number of `bytes_per_pixel`.
|
||||||
|
|
||||||
|
[`FrameBuffer::buffer_mut`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/struct.FrameBuffer.html#method.buffer_mut
|
||||||
|
|
||||||
|
The second step is to set the pixel to the desired color.
|
||||||
|
We first use the [`FrameBuffer::buffer_mut`] method to get access to the actual bytes of the framebuffer in form of a slice.
|
||||||
|
Then, we use the slicing operator `[byte_offset..]` to get a sub-slice starting at the `byte_offset` of the target pixel.
|
||||||
|
As the write operation depends on the pixel format, we use a [`match`] statement:
|
||||||
|
|
||||||
|
[`match`]: https://doc.rust-lang.org/stable/std/keyword.match.html
|
||||||
|
|
||||||
|
- For `Rgb` framebuffers, we write three bytes; first red, then green, then blue.
|
||||||
|
- For `Bgr` framebuffers, we also write three bytes, but blue first and red last.
|
||||||
|
- For `U8` framebuffers, we first convert the color to grayscale by taking the average of the three color channels.
|
||||||
|
Note that there are multiple [different ways to convert colors to grayscale], so you can also use different factors here.
|
||||||
|
- For all other framebuffer formats, we [panic] for now.
|
||||||
|
|
||||||
|
[different ways to convert colors to grayscale]: https://www.baeldung.com/cs/convert-rgb-to-grayscale#bd-convert-rgb-to-grayscale
|
||||||
|
[panic]: https://doc.rust-lang.org/stable/core/macro.panic.html
|
||||||
|
|
||||||
|
Let's try to use our new function to write a blue pixel in our `kernel_main` function:
|
||||||
|
|
||||||
|
```rust ,hl_lines=5-11
|
||||||
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
|
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||||
|
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||||
|
let position = framebuffer::Position { x: 20, y: 100 };
|
||||||
|
let color = framebuffer::Color {
|
||||||
|
red: 0,
|
||||||
|
green: 0,
|
||||||
|
blue: 255,
|
||||||
|
};
|
||||||
|
framebuffer::set_pixel_in(framebuffer, position, color);
|
||||||
|
}
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When we run our code in QEMU using `cargo run --bin qemu-bios` (or `--bin qemu-uefi`) and look _very closely_, we can see the blue pixel.
|
||||||
|
It's really difficult to see, so I marked with an arrow below:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As this single pixel is too difficult to see, let's draw a filled square of 100x100 pixels instead:
|
||||||
|
|
||||||
|
```rust ,hl_lines=10-18
|
||||||
|
// in kernel/src/main.rs
|
||||||
|
|
||||||
|
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||||
|
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||||
|
let color = framebuffer::Color {
|
||||||
|
red: 0,
|
||||||
|
green: 0,
|
||||||
|
blue: 255,
|
||||||
|
};
|
||||||
|
for x in 0..100 {
|
||||||
|
for y in 0..100 {
|
||||||
|
let position = framebuffer::Position {
|
||||||
|
x: 20 + x,
|
||||||
|
y: 100 + y,
|
||||||
|
};
|
||||||
|
framebuffer::set_pixel_in(framebuffer, position, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we clearly see that our code works as intended:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Feel free to experiment with different positions and colors if you like.
|
||||||
|
You can also try to draw a circle instead of a square, or a line with a certain thickness.
|
||||||
|
|
||||||
|
As you can probably imagine, it would be a lot of work to draw more complex shapes this way.
|
||||||
|
One example for such complex shapes is _text_, i.e. the rendering of letters and punctuation.
|
||||||
|
Fortunately, there is the nice `no_std`-compatible [`embedded-graphics`] crate, which provides draw functions for text, various shapes, and image data.
|
||||||
|
|
||||||
|
[`embedded-graphics`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/index.html
|
||||||
|
|
||||||
|
## The `embedded-graphics` crate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Implementing `DrawTarget`
|
||||||
|
|
||||||
|
```rust ,hl_lines=3
|
||||||
|
// in kernel/src/framebuffer.rs
|
||||||
|
use embedded_graphics::{
|
||||||
|
Pixel,
|
||||||
|
draw_target::DrawTarget,
|
||||||
|
geometry::{OriginDimensions, Size},
|
||||||
|
pixelcolor::{Rgb888, RgbColor},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Display<'f> {
|
||||||
|
framebuffer: &'f mut FrameBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'f> Display<'f> {
|
||||||
|
pub fn new(framebuffer: &'f mut FrameBuffer) -> Display {
|
||||||
|
Display { framebuffer }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_pixel(&mut self, Pixel(coordinates, color): Pixel<Rgb888>) {
|
||||||
|
// ignore any out of bounds pixels
|
||||||
|
let (width, height) = {
|
||||||
|
let info = self.framebuffer.info();
|
||||||
|
|
||||||
|
(info.width, info.height)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (x, y) = {
|
||||||
|
let c: (i32, i32) = coordinates.into();
|
||||||
|
(c.0 as usize, c.1 as usize)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (0..width).contains(&x) && (0..height).contains(&y) {
|
||||||
|
let color = Color { red: color.r(), green: color.g(), blue: color.b() };
|
||||||
|
|
||||||
|
set_pixel_in(self.framebuffer, Position { x, y }, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<'f> DrawTarget for Display<'f> {
|
||||||
|
type Color = Rgb888;
|
||||||
|
|
||||||
|
/// Drawing operations can never fail.
|
||||||
|
type Error = core::convert::Infallible;
|
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||||
|
{
|
||||||
|
for pixel in pixels.into_iter() {
|
||||||
|
self.draw_pixel(pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'f> OriginDimensions for Display<'f> {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
let info = self.framebuffer.info();
|
||||||
|
|
||||||
|
Size::new(info.width as u32, info.height as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,7 +382,7 @@ Depending on the hardware and GPU firmware, it is often more efficient
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
draw shapes and pixels directly onto the framebuffer. That's fine and all, but how is one able to go from that to displaying text on the screen? Understanding this requires taking a deep dive into how characters are rendered behind the scenes.
|
So far, we have drawn shapes and pixels directly onto the framebuffer. That's fine and all, but how is one able to go from that to displaying text on the screen? Understanding this requires taking a deep dive into how characters are rendered behind the scenes.
|
||||||
|
|
||||||
When a key is pressed on the keyboard, it sends a character code to the CPU. It's the CPU's job at that point to then interpret the character code and match it with an image to draw on the screen. The image is then sent to either the GPU or the framebuffer (the latter in our case) to be drawn on the screen, and the user sees that image as a letter, number, CJK character, emoji, or whatever else he or she wanted to have displayed by pressing that key.
|
When a key is pressed on the keyboard, it sends a character code to the CPU. It's the CPU's job at that point to then interpret the character code and match it with an image to draw on the screen. The image is then sent to either the GPU or the framebuffer (the latter in our case) to be drawn on the screen, and the user sees that image as a letter, number, CJK character, emoji, or whatever else he or she wanted to have displayed by pressing that key.
|
||||||
|
|
||||||
@@ -179,7 +472,7 @@ fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! {
|
|||||||
let frame_buffer_struct = frame_buffer_option.unwrap();
|
let frame_buffer_struct = frame_buffer_option.unwrap();
|
||||||
|
|
||||||
// extract the framebuffer info and, to satisfy the borrow checker, clone it
|
// extract the framebuffer info and, to satisfy the borrow checker, clone it
|
||||||
let frame_buffer_info = frame_buffer.info().clone();
|
let frame_buffer_info = frame_buffer_struct.info().clone();
|
||||||
|
|
||||||
// get the framebuffer's mutable raw byte slice
|
// get the framebuffer's mutable raw byte slice
|
||||||
let raw_frame_buffer = frame_buffer_struct.buffer_mut();
|
let raw_frame_buffer = frame_buffer_struct.buffer_mut();
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -76,7 +76,7 @@ html {
|
|||||||
--background-color: #fff;
|
--background-color: #fff;
|
||||||
--text-color: #515151;
|
--text-color: #515151;
|
||||||
--heading-color: #313131;
|
--heading-color: #313131;
|
||||||
--heading-code-color: #a0565c;
|
--heading-code-color: #313131;
|
||||||
--link-color: #268bd2;
|
--link-color: #268bd2;
|
||||||
--hr-color-top: #eee;
|
--hr-color-top: #eee;
|
||||||
--hr-color-bottom: #fff;
|
--hr-color-bottom: #fff;
|
||||||
@@ -181,11 +181,11 @@ h6 {
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2rem;
|
font-size: 2.25rem;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
@@ -196,6 +196,7 @@ h5,
|
|||||||
h6 {
|
h6 {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body text */
|
/* Body text */
|
||||||
|
|||||||
Reference in New Issue
Block a user