rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 04:30:07 -0700 |
|---|---|---|
| commit | 2a105612c9f6eeb692b8d5862fb2b27ae68a3ee9 | |
| parent | 3d90f5acee1369df49034601dd4c252cfc6c9e3d | |
| tree | browse files at this commit | |
Stage 13: virtual drivers (0.13.0) virtio-rng (getrandom syscall 318, /dev/random, seeds smoltcp and ports), virtio-console (hvc0, kernel log mirror, getty), virtio-gpu 2D with a 1024x768 framebuffer console (Spleen 8x16 font, VGA text fallback) and /dev/fb0 (ioctl + write blitting). New user programs: random, getty, tile. New tests/virtio.rs (9 tests), gpu-check.sh screendump pixel check, README chapter on virtio, framebuffers and the road to 3D.
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +19 -1 |
| M | README.md | +408 -42 |
| M | boot.png | +0 -0 |
| M | build.rs | +1 -0 |
| M | disk/scripts/selftest.sh | +10 -0 |
| A | scripts/gpu-check.sh | +63 -0 |
| A | scripts/hvc0-session.txt | +5 -0 |
| M | scripts/run-headless.sh | +33 -5 |
| M | scripts/screenshot.sh | +19 -15 |
| A | src/dev.rs | +252 -0 |
| M | src/errno.rs | +3 -2 |
| A | src/fbcon.rs | +196 -0 |
| A | src/font.rs | +313 -0 |
| M | src/fs/exfat.rs | +1 -1 |
| M | src/fs/ext2.rs | +1 -1 |
| M | src/fs/file.rs | +7 -0 |
| M | src/fs/mod.rs | +9 -0 |
| M | src/fs/ramfs.rs | +1 -1 |
| M | src/interrupts.rs | +2 -0 |
| M | src/lib.rs | +18 -1 |
| M | src/main.rs | +6 -0 |
| M | src/memory.rs | +26 -0 |
| M | src/net/mod.rs | +7 -2 |
| M | src/pci.rs | +58 -0 |
| M | src/serial.rs | +2 -0 |
| M | src/syscall.rs | +75 -3 |
| M | src/vga_buffer.rs | +140 -30 |
| A | src/virtio/console.rs | +245 -0 |
| A | src/virtio/gpu.rs | +247 -0 |
| A | src/virtio/mod.rs | +230 -0 |
| A | src/virtio/queue.rs | +162 -0 |
| A | src/virtio/rng.rs | +132 -0 |
| A | tests/hvc0-input.txt | +1 -0 |
| M | tests/shell.rs | +1 -1 |
| M | tests/vfs.rs | +2 -2 |
| A | tests/virtio.rs | +176 -0 |
| A | user/getty.rs | +54 -0 |
| M | user/ls.rs | +7 -4 |
| A | user/random.rs | +74 -0 |
| M | user/sh.rs | +1 -0 |
| A | user/tile.rs | +173 -0 |
| M | user/ulib.rs | +16 -0 |
43 files changed, 3197 insertions, 112 deletions
@@ -261,7 +261,7 @@ dependencies = [ [[package]] name = "rustbelt"-version = "0.12.0"+version = "0.13.0" dependencies = [ "bootloader", "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.12.0"+version = "0.13.0" edition = "2024" [dependencies]@@ -105,6 +105,18 @@ run-args = [ # `httpd` in Rustbelt answers `curl http://127.0.0.1:5580/`. "-netdev", "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat,hostfwd=tcp:127.0.0.1:5580-:80", "-device", "virtio-net-pci,netdev=net0",+ # Stage 13's virtual devices:+ # virtio-vga: virtio-gpu that also does the old VGA modes (so the+ # BIOS and the early VGA text console still work). -vga none drops+ # the default "std" VGA card it replaces.+ # virtio-rng-pci: random numbers from the host.+ # virtio-serial-pci + virtconsole: a second terminal (hvc0) on a unix+ # socket: `socat - UNIX-CONNECT:target/hvc0.sock` on the host, and+ # `getty /dev/hvc0 &` in Rustbelt to get a shell there.+ "-vga", "none", "-device", "virtio-vga",+ "-device", "virtio-rng-pci",+ "-device", "virtio-serial-pci", "-device", "virtconsole,chardev=hvc",+ "-chardev", "socket,id=hvc,path=target/hvc0.sock,server=on,wait=off", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", # lets the kernel exit QEMU "-serial", "stdio", # serial port -> your terminal ]@@ -135,6 +147,12 @@ test-args = [ # test). No hostfwd: tests must not depend on a free host port. "-netdev", "user,id=net0,tftp=disk,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat", "-device", "virtio-net-pci,netdev=net0",+ # The stage-13 devices (see run-args). The console's input comes from+ # tests/hvc0-input.txt and its output goes to target/hvc0-test.out.+ "-vga", "none", "-device", "virtio-vga",+ "-device", "virtio-rng-pci",+ "-device", "virtio-serial-pci", "-device", "virtconsole,chardev=hvc",+ "-chardev", "file,id=hvc,path=target/hvc0-test.out,input-path=tests/hvc0-input.txt", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", "-display", "none",
@@ -9,8 +9,11 @@ redirection, plus `dir`, `type`, `ver`, and drive letters from `A:` to `E:`. Under the shell:-- **VGA text and serial output.** The kernel prints to the VGA text screen and- to the serial port (COM1).+- **A framebuffer console and serial output.** The kernel draws its text+ console with a built-in 8x16 bitmap font on a 1024x768 **virtio-gpu**+ framebuffer (falling back to the old VGA text screen when there's no+ virtio-gpu), and mirrors everything to the serial port (COM1) and a+ second console, **hvc0**. - **CPU exceptions.** It handles breakpoints, page faults, and double faults (on their own stack). - **Several CPUs.** It starts every CPU core QEMU gives it (`-smp 4`)@@ -29,9 +32,9 @@ Under the shell: (wait queues) without using any CPU while they wait. - **User programs.** Small Rust ELF binaries run in **ring 3** as processes, each with its own address space. Their memory is freed when they exit. They- talk to the kernel through about 38 `syscall`s, numbered like Linux's:+ talk to the kernel through about 39 `syscall`s, numbered like Linux's: `read`, `write`, `open`, `stat`, `getdents64`, `pipe`, `uname`, `socket`,- `connect`... A few are our own: `spawn`, `wait`, `mounts`, `sysinfo`,+ `connect`, `getrandom`... A few are our own: `spawn`, `wait`, `mounts`, `sysinfo`, `resolve` and `netinfo`. - **Files.** Files live behind a Linux-style **VFS**: a ramfs at `/` and `/tmp`, a **FAT32** partition mounted at `/mnt/disk0`, an **ext2**@@ -52,6 +55,12 @@ Under the shell: BSD-style sockets (`socket`, `connect`, `bind`, `listen`, `accept`, `sendto`, `recvfrom`) and come with `ifconfig`, `ping`, `host`, `fetch` (a tiny `wget`) and `httpd` (a tiny web server you can reach from the host).+- **Virtual devices.** Hand-written drivers for the modern (virtio 1.0)+ **virtio-rng** (entropy: the `getrandom` syscall, `/dev/random`, smoltcp's+ seed and random TCP ports), **virtio-console** (`/dev/hvc0`, a second+ terminal the host reaches through a socket; `getty` runs a shell on it)+ and **virtio-gpu** in 2D mode (`/dev/fb0` with ioctls; `tile` draws a+ tiling-window-manager mock-up on it). It has a `cargo test` suite that runs inside QEMU, and a headless self-test that drives the shell with a script.@@ -61,9 +70,12 @@ comments. Everything else is new: the serial mirroring, uptime clock, demo features and scripts, and everything past chapter 12. That covers preemptive threads, user mode, syscalls, ELF loading, the VFS, PCI, virtio-blk, partitions, FAT32, the shell, the ext2, exFAT and btrfs drivers,-multi-core (SMP) support, and the virtio-net driver and socket layer.+multi-core (SMP) support, the virtio-net driver and socket layer, and the+virtio-rng, virtio-console and virtio-gpu drivers with the framebuffer+console. The 8x16 font is [Spleen](https://github.com/fcambus/spleen) by+Frederic Cambus (BSD 2-clause, license in `src/font.rs`). -+ ## New to Rust and C? Start here @@ -87,6 +99,9 @@ for. Read them roughly in the order the kernel uses them: 6. `src/process.rs`, `src/syscall.rs`, `src/elf.rs` -- user programs. 7. `user/hello.rs`, then `user/sh.rs` -- what a program on Rustbelt looks like. 8. `src/fs/` -- the file systems, once the rest makes sense.+9. `src/virtio/` -- devices: `mod.rs` and `queue.rs` (how a driver talks to+ a virtio device), then `rng.rs` (the smallest driver), `console.rs` and+ `gpu.rs`; `src/fbcon.rs` draws text on the framebuffer. **Rust in kernel code, in five ideas:** @@ -123,7 +138,8 @@ first one to boot is the *BSP*, the others *APs*. **Try things.** `cargo run`, then in the shell: `help`, `ver`, `sysinfo`, `spin 200 > A:/tmp/spin.txt &` followed by `sysinfo` (watch one CPU go to 100%), `overflow`-(a stack overflow, caught). Change something small -- the message in+(a stack overflow, caught), `tile` (the console moves into a window;+`tile -r` moves it back), `random -d 6 5`. Change something small -- the message in `user/overflow.rs`, the bar width in `user/sysinfo.rs` -- and run `cargo test` to check nothing broke. When something crashes, the `[kernel]` lines on the serial output tell you which handler caught it and where.@@ -196,9 +212,28 @@ forwarder at `10.0.2.3`. Two extras are set up in `Cargo.toml`: Try `ifconfig`, `ping 10.0.2.2`, `host example.com` or `wget -o D:/page.html http://example.com/`. +**The second console (hvc0):** `cargo run` connects the guest's+virtio-console to a UNIX socket, `target/hvc0.sock`. The kernel log is+mirrored there; to get a shell on it, type `getty /dev/hvc0 &` in the+QEMU window, then on the host:++```sh+socat - UNIX-CONNECT:target/hvc0.sock # or: nc -U target/hvc0.sock+```++Type commands and press Enter (that shell reads whole lines, like a+script, and echoes each one after its prompt; `exit` ends it and getty+starts a new one). The headless runs use a file instead of the socket+(`-chardev file,...,input-path=`), see below.++**The display** is virtio-gpu (`-vga none -device virtio-vga`): the guest+sets up a 1024x768 framebuffer and QEMU shows it scaled in its window. Leave+those two options out and the kernel falls back to the VGA text console.+ To get the mouse and keyboard back from QEMU, press Ctrl+Alt+G. To quit, close the window or press Ctrl+C in the terminal. Over SSH without X, use-`cargo run -- -display curses` to see the VGA screen right in your terminal,+`cargo run -- -display curses -vga std` to see the screen right in your terminal+(curses can only show text mode, so this switches back to the VGA console), and quit from another terminal with `pkill qemu-system`. Headless (no window, good for SSH / scripts):@@ -206,7 +241,8 @@ Headless (no window, good for SSH / scripts): ```sh ./scripts/run-headless.sh # boot, the shell runs disk/scripts/selftest.sh, QEMU exits; # passes if the shell exits 0 AND the expected lines appear- # (and the host's curl got a page from the guest's httpd)+ # (and the host's curl got a page from the guest's httpd,+ # and the shell on hvc0 answered in target/headless-hvc0.txt) ./scripts/run-headless.sh stack-overflow # deliberately overflow the stack; passes if the double-fault handler catches it ./scripts/run-headless.sh page-fault # write to unmapped 0xdeadbeaf; passes if the page-fault handler catches it EXT2_IMG=copy.img ./scripts/run-headless.sh # use copy.img as the ext2 disk, writable (no snapshot)@@ -223,8 +259,10 @@ cargo run --features demos # the earlier stages' demos first, the ./scripts/screenshot.sh [out.png] [session.txt] [expected...] # boot, type a shell session via QEMU monitor `sendkey` # (one command per line; {BS} = Backspace, {UP} = up arrow),- # check the expected texts appear, save the VGA screen+ # check the expected texts appear, save the screen (.png, or .ppm) # (default: the session in boot.png); exits 1 if text is missing+./scripts/gpu-check.sh # type `tile`, dump the virtio-gpu screen, compare 12 pixels+ # against the colours tile.rs draws (see "Virtual drivers") ``` Cargo features (in `Cargo.toml`):@@ -245,11 +283,15 @@ only, so the VGA screen shows just the shell session (and kernel messages such as a killed process). ```-Rustbelt 0.12.0 -- a hobby x86_64 kernel in Rust+Rustbelt 0.13.0 -- a hobby x86_64 kernel in Rust [smp] 4 CPUs online [pci] 00:00.0 8086:1237 host bridge-... (10 PCI devices, one of them the network card)-[pci] 10 devices on the PCI bus+... (12 PCI devices: the network card, the GPU, the rng, the console, 4 disks)+[pci] 00:03.0 1af4:1050 display controller+[pci] 00:04.0 1af4:1005 unclassified+[pci] 00:05.0 1af4:1003 communication controller+...+[pci] 12 devices on the PCI bus [disk] vda: virtio-blk, 131072 sectors (64 MiB) [disk] vda: GPT partition table, 1 partition(s) [disk] vda1: start 2048 sectors 126976 type Microsoft basic data (FAT/exFAT/NTFS) name "fat32"@@ -265,17 +307,22 @@ Rustbelt 0.12.0 -- a hobby x86_64 kernel in Rust [disk] vdd: btrfs volume "TYLERBTRFS" (16 KiB nodes, metadata DUP, data single, 2 subvolumes, read-only) mounted at /mnt/disk3 [vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp+[vfs] mounted devfs at /dev [vfs] mounted fat32 at /mnt/disk0 [vfs] mounted ext2 at /mnt/disk1 [vfs] mounted exfat at /mnt/disk2 [vfs] mounted btrfs at /mnt/disk3-[net] eth0: virtio-net, MAC 52:54:00:12:34:56, IRQ 11+[virtio] rng: virtio-rng at 00:04.0+[virtio] hvc0: virtio-console at 00:05.0, IRQ 10+[virtio] gpu: virtio-gpu 2D at 00:03.0, display 1280x800, framebuffer 1024x768 (3072 KiB)+[virtio] fbcon: text console on the framebuffer, 128x48 characters+[net] eth0: virtio-net, MAC 52:54:00:12:34:56, IRQ 10 [net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3-[process] started pid 1 (sh), entry 0x8000406b60+[process] started pid 1 (sh), entry 0x8000407500 A:/ $ # Rustbelt shell self-test: `cargo run --features headless` runs this with A:/ $ # `sh /mnt/disk0/scripts/selftest.sh` (B:/scripts/selftest.sh) and scripts/run-headless.sh checks the output. A:/ $ ver-Rustbelt 0.12.0 (stage 12: networking) x86_64+Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64 A:/ $ drives A: / ramfs B: /mnt/disk0 fat32@@ -311,7 +358,7 @@ B:/ $ dir bin | wc -l [process] started pid 8 (ls), entry 0x8000401880 [process] started pid 9 (wc), entry 0x8000400260 [process] pid 8 (ls) exited with code 0-22+25 [process] pid 9 (wc) exited with code 0 B:/ $ echo first line > A:/tmp/out.txt B:/ $ echo second line >> /tmp/out.txt@@ -613,6 +660,25 @@ E:/ $ httpd -n 1 -t 20 [httpd] 10.0.2.2:38550 GET / -> 200 (134 bytes) [httpd] served 1 request, bye ...+E:/ $ ls -l /dev+crw-rw-rw- <DEV> fb0+crw-rw-rw- <DEV> hvc0+crw-rw-rw- <DEV> null+crw-rw-rw- <DEV> random+crw-rw-rw- <DEV> urandom+E:/ $ random -n 8+7f1178e9b80be627+E:/ $ random -d 6 5+4 4 6 1 2+E:/ $ getty -1 /dev/hvc0+[process] started pid 69 (getty), entry 0x8000400460+[process] started pid 70 (sh), entry 0x8000407500+... (that shell's output goes to hvc0, see below)+[process] pid 69 (getty) exited with code 0+E:/ $ tile+tile: 4 windows on 1024x768, console 61x46+E:/ $ tile -r+console: full screen, 128x48 characters E:/ $ nosuchprogram sh: nosuchprogram: command not found (try `help`) E:/ $ cd Q:@@ -623,25 +689,54 @@ A:/tmp $ pwd A:/tmp $ exit 0 [process] pid 1 (sh) exited with code 0 [ok] shell self-test finished: Exited(0)-[memory] 235 frames in use before the shell, 235 after it and all its children exited+[memory] 1015 frames in use before the shell, 1015 after it and all its children exited [run-headless] frames in use returned to the baseline-[run-headless] host curl got httpd's page: Hello from Rustbelt! Rustbelt 0.12.0 (stage 12: networking)-[run-headless] all 52+5 expected shell lines found+[run-headless] host curl got httpd's page: Hello from Rustbelt! Rustbelt 0.13.0 (stage 13: virtual drivers)+[run-headless] all 60+12 expected shell and hvc0 lines found [run-headless] kernel exited OK ``` +And `target/headless-hvc0.txt`, what the host got from hvc0: the kernel+log up to the moment getty opened the port, then the shell running the+commands in `scripts/hvc0-session.txt`:++```+...+E:/ $ getty -1 /dev/hvc0+[process] started pid 69 (getty), entry 0x8000400460++Rustbelt getty: shell on this terminal (type `exit` to end it)+E:/ $ ver+Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64+E:/ $ echo hi over hvc0+hi over hvc0+E:/ $ random -d 20 3+3 12 8+E:/ $ ls /dev+fb0+hvc0+null+random+urandom+E:/ $ exit 0+[process] pid 69 (getty) exited with code 0+E:/ $ tile+...+```+ (Which CPU each program lands on, the exact order of lines printed by programs running at the same time, ping times and download speeds change-from run to run. `example.com`'s address is whatever the host's DNS says;+from run to run, and so do the random numbers. `example.com`'s address is whatever the host's DNS says; without DNS that line is `host: example.com: ...` and the run still passes.) The run ends with the same number of frames in use before and after the-shell, even though 65 processes have come and gone; `run-headless.sh`+shell, even though 74 processes have come and gone; `run-headless.sh` fails if they differ. Exited processes give back their page tables, code,-user stacks and kernel stacks. (The 235 at boot are mostly the four virtio-disks' and the network card's queues and buffers, the kernel stack area-and the other CPUs' stacks.)+user stacks and kernel stacks. (Of the 1015 at boot, 768 are the 3 MiB+framebuffer; most of the rest are the virtio queues and buffers of the+four disks, the network card, the rng, the console and the GPU, the kernel+stack area and the other CPUs' stacks.) The kernel boots from the bootimage on the first (IDE) disk; the scripts and `Cargo.toml` attach a **second** disk, `target/disk.img` (GPT + FAT32), a@@ -690,7 +785,7 @@ heap_allocation::simple_allocation... [ok] | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) | | `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `B:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged | | `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter |-| `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` |+| `tests/vfs.rs` | the root holds `dev`, `etc`, `hello.txt`, `mnt`, `tmp`; read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts (4 with `/dev`), sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk | | `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files (`bin`, `notes`, `readme.txt`, `scripts`); read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes | | `tests/ext2.rs` | the three ext2 images are mounted (`/mnt/disk1`, `disk4`, `disk5`); permissions and case-sensitive names; `big.txt` line by line and in one read (direct, single and double indirect blocks); `sparse.bin`'s data at 0 / 40 MiB / 65 MiB (triple indirect) and its holes; fast and slow symlinks, `readlink`, removing a link keeps its target; 4 KiB blocks and revision 0; on each image: mkdir, create, a 300 KB write, append, overwrite across a block boundary, a write at 70 MiB (hole + triple indirect), truncate, `ENOTEMPTY`/`EISDIR`/`ENOTDIR`, unlink, rmdir, and the superblock's free counts are back where they started; 200 files grow a directory; copy from FAT to ext2. `scripts/ext2-check.sh` then checks the same kernel's writes with the host's `e2fsck` |@@ -699,6 +794,7 @@ heap_allocation::simple_allocation... [ok] | `tests/threads.rs` | two busy-looping threads that never yield both make progress while the main thread busy-waits too (only possible with preemption); a thread can exit and the others keep running | | `tests/smp.rs` | **multi-core and the scheduler**: all 4 CPUs are online and take timer ticks; 4 busy threads run on 2+ CPUs *at the same time* (per-CPU busy ticks); 3 `spin` processes run on 2+ CPUs; 4 threads on 4 CPUs add 200,000 to a locked counter without losing one; `sleep_ticks(10)` lasts 10-12 ticks; sleepers wake in deadline order (3, 6, 9); while 4 threads sleep the CPUs are idle (<10% busy, so no busy-polling); two threads ping-pong 1,000 times through a `WaitQueue`; **leak checks**: 200 threads and 40 processes spawned and exited leave `frames_in_use`, the thread count, the process count and the number of kernel stacks exactly where they were | | `tests/net.rs` | **networking**, all against things QEMU itself provides, so it's deterministic and works offline: DHCP gives 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3 and the MAC is QEMU's default; 3 ICMP echoes to the gateway come back with their payload; a UDP **TFTP** read of `readme.txt` from slirp's TFTP server (`tftp=disk` in `test-args`); a 64 KiB TCP round trip through the `cat` echo server (guestfwd 10.0.2.100:7); an HTTP GET of 108,894 bytes through the socket API; `ECONNREFUSED` from a closed host port; a UDP `recv` with a 300 ms timeout returns `EAGAIN` after 250-1000 ms; the `ping` and `fetch` programs (saving to a file, and exit 1 on a 404); 20 TCP + 20 UDP sockets opened and closed leave the socket count and `frames_in_use` unchanged; DNS for `example.com`, **skipped** (not failed) if the host has no DNS |+| `tests/virtio.rs` | **the virtual drivers**: 2 x 4 KiB from virtio-rng differ, aren't constant, have 15384-17384 one bits out of 32768 and contain all 256 byte values; the `random` program (hex, dice, a usage error); hvc0 receives exactly the bytes QEMU feeds in from `tests/hvc0-input.txt`, and 8 KiB written to it go out with nothing dropped; virtio-gpu answers `GET_DISPLAY_INFO`, refuses a flush of a resource that doesn't exist with `ERR_INVALID_RESOURCE_ID`, full and clipped flushes work, a pattern written into the framebuffer reads back; the text console is on the framebuffer (128x48) and printing lights up pixels; `/dev` lists `fb0 hvc0 null random urandom`; `tile` draws the expected colours at known pixels, moves the console to 61x46 and `tile -r` back, with no frames leaked. `scripts/gpu-check.sh` checks what QEMU actually displays | | `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed("stack overflow")` and leaves no frames behind | How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`@@ -800,17 +896,30 @@ src/net/mod.rs networking: the smoltcp Interface, DHCP + DNS, the `ne src/net/virtio_net.rs legacy virtio-net driver: RX/TX virtqueues, MAC, smoltcp's Device trait src/net/socket.rs BSD-style sockets (TCP, UDP, ICMP) on smoltcp, blocking on a wait queue user/{ifconfig,ping,host,fetch,httpd}.rs the network programs+user/{random,getty,tile}.rs stage 13: getrandom, a shell on hvc0, the framebuffer tiling demo+scripts/hvc0-session.txt the commands run-headless.sh feeds into hvc0 (for `getty -1 /dev/hvc0`)+scripts/gpu-check.sh types `tile`, dumps the virtio-gpu screen (PPM), compares pixels scripts/http-reply.sh the host side of `fetch http://10.0.2.100/`: QEMU runs it per connection-src/vga_buffer.rs VGA text-mode writer, print!/println!, print_at (status line), backspace+src/vga_buffer.rs the console writer, print!/println!, print_at (status line), backspace: VGA text+ mode, or an FbConsole once virtio-gpu is up (use_framebuffer, set_viewport)+src/virtio/mod.rs modern (1.0) virtio over PCI: capabilities, feature negotiation, the+ Transport (common config, notify, ISR, device config), IRQ fan-out+src/virtio/queue.rs split virtqueue: descriptor table, available and used rings+src/virtio/rng.rs virtio-rng: fill(buf) for getrandom, /dev/random and seeds (+ fallback)+src/virtio/console.rs virtio-console (hvc0): 8 RX + 8 TX buffers, IRQ-driven input, log mirroring+src/virtio/gpu.rs virtio-gpu 2D: display info, resource, backing, scanout, transfer + flush+src/fbcon.rs text console on the framebuffer: cells, cursor, scrolling, dirty rows, a viewport+src/font.rs the 8x16 bitmap font (Spleen, BSD-2), Latin-1 + box drawing+src/dev.rs devfs at /dev: null, random, urandom, hvc0, fb0 (the Framebuffer file) src/serial.rs COM1 UART writer + serial_print!/serial_println! tests/ integration tests, one QEMU-booted kernel each scripts/runner.sh `cargo run`/`cargo test` runner: `bootimage runner` + stale-image fix scripts/run-headless.sh headless boot + shell self-test, timeout, exit-code and output checks-scripts/screenshot.sh headless boot + a typed shell session + PNG screenshot via QEMU monitor+scripts/screenshot.sh headless boot + a typed shell session + PNG/PPM screenshot via QEMU monitor scripts/ext2-check.sh Rustbelt writes to copies of the ext2 images, then host e2fsck -fn + debugfs scripts/exfat-check.sh Rustbelt writes to copies of the exFAT images, then host fsck.exfat -n + dump.exfat scripts/btrfs-check.sh btrfs check --readonly, then tests/btrfs.rs's file CRCs vs. the host's files-boot.png screenshot of the VGA screen (a shell session)+boot.png screenshot of the virtio-gpu screen (tile + a shell session) ``` ## What the interrupt code does (chapters 5-7)@@ -1208,7 +1317,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 4 | `stat` | path, path_len, *Stat | follows symlinks | | 5 | `fstat` | fd, *Stat | | | 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes |-| 16 | `ioctl` | fd, request, arg | only `TTY_SET_RAW` (1): console raw mode on (arg 1) / off (0); returns the old mode, `ENOTTY` if fd isn't the console |+| 16 | `ioctl` | fd, request, arg | console: `TTY_SET_RAW` (1): raw mode on (arg 1) / off (0); returns the old mode. `/dev/fb0`: `FB_GET_INFO` (0x4600) writes `[width, height, bytes per row, 32]` to `*arg`; `FB_FLUSH` (0x46f0) shows `*[x, y, w, h]` (arg 0: the rows this fd wrote); `FB_FILL` (0x46f1) fills `*[x, y, w, h, 0xRRGGBB]`; `FB_SET_CONSOLE` (0x46f2) moves the text console into `*[x, y, w, h]` (arg 0: full screen) and returns `cols << 16 \| rows`. Others: `ENOTTY` | | 22 | `pipe` | *[u32; 2] | [read end, write end] | | 24 | `sched_yield` | | | | 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` |@@ -1223,7 +1332,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`) | | 60 | `exit` | code | | | 61 | `wait` | pid, flags | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child (freeing its memory); `ECHILD` if not your child; flags `WNOHANG` (1): don't block, `EAGAIN` while it's still running |-| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.12.0", "stage 12: networking", "x86_64", "(none)" |+| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.13.0", "stage 13: virtual drivers", "x86_64", "(none)" | | 79 | `getcwd` | buf, len | returns the length (no NUL); `ERANGE` if too small | | 80 | `chdir` | path, path_len | per-process cwd, inherited by spawned children | | 83 | `mkdir` | path, path_len | |@@ -1232,6 +1341,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 89 | `readlink` | path, path_len, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink | | 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) | | 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT); any clock id counts from boot |+| 318 | `getrandom` | buf, len, flags | random bytes from virtio-rng (the host's `/dev/urandom`), up to 64 KiB per call; never blocks, flags ignored. Without virtio-rng: a TSC-seeded xorshift (not secure) | | 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid | | 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it | | 502 | `sysinfo` | *SysInfo | **ours**: CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) |@@ -1245,7 +1355,7 @@ Deviations from Linux worth knowing: the process's cwd; `.` and `..` are resolved in the kernel (`fs::normalize`). - **`Stat` is tiny**: `#[repr(C)] struct Stat { kind: u64, size: u64, mode: u64 }`, kind 1 = file, 2 = directory, 3 = console, 4 = pipe, 5 = symlink,- 6 = socket; `mode`+ 6 = socket, 7 = device (`/dev/*`; `getdents64` says `DT_CHR`); `mode` is the permission bits (`0o644`), or 0 on file systems without them (ramfs, FAT). No inode numbers, owners or times yet. - **spawn instead of fork + exec.** `fork` copies the whole address space (or@@ -1569,6 +1679,9 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `host name` (`nslookup`) | look up a name's IPv4 addresses | | `fetch [-o file] http://host[:port]/path` (`wget`) | HTTP/1.0 GET; prints the body, or saves it (`-o D:/page.html`) with a size and speed report; exit 1 on non-200 | | `httpd [-p port] [-n count] [-t secs]` | a tiny web server: `/` is a greeting with a visitor count, other paths serve files from the current directory |+| `random [-n bytes]` / `random -d sides [count]` | random bytes as hex (default 16), or fair dice rolls; uses `getrandom` |+| `getty [-1] device` | run a shell on another terminal, e.g. `getty /dev/hvc0 &`; restarts it when it exits (`-1`: once) |+| `tile [1-6]` / `tile -r` | draw a tiling-window-manager mock-up on `/dev/fb0` and move the console into the first window; `-r` gives the console the whole screen back | | `hello`, `demo`, `evil`, `spin`, `child` | the stage 5/6 test programs: `run demo` runs all the stage 6 syscall checks | Built-in aliases are the DOS names: `dir` = `ls`, `type` = `cat`,@@ -2423,6 +2536,225 @@ sockets / `poll`/`select`, `shutdown`, `getsockname`, checksum offload or multi-queue, more than one card, a loopback interface (127.0.0.1), or TLS (so `fetch` is plain `http://` only). +## Virtual drivers (stage 13)++Stage 13 adds the last three virtual devices Rustbelt wants from QEMU,+with no physical hardware involved:++- **virtio-rng** (`src/virtio/rng.rs`): random bytes from the host. They+ feed the `getrandom` syscall (Linux number 318), `/dev/random` and+ `/dev/urandom`, the `random` program, smoltcp's random seed (which it+ uses for things like TCP initial sequence numbers) and the first local TCP/UDP port,+ which used to be a constant 49152.+- **virtio-console** (`src/virtio/console.rs`): a second terminal, `hvc0`,+ like Linux's `/dev/hvc0` in a VM. The kernel log is mirrored to it, and+ `getty /dev/hvc0` runs a shell on it. On the host it's a QEMU+ *chardev*: a UNIX socket for `cargo run`, files for the tests.+- **virtio-gpu, 2D** (`src/virtio/gpu.rs`): a 1024x768 framebuffer. The+ text console moves onto it (`src/fbcon.rs`, with a built-in bitmap+ font, `src/font.rs`), and user programs get `/dev/fb0` (`src/dev.rs`).+ Without a virtio-gpu, the VGA text console carries on as before.++```+A:/ $ ls -l /dev+crw-rw-rw- <DEV> fb0+crw-rw-rw- <DEV> hvc0+crw-rw-rw- <DEV> null+crw-rw-rw- <DEV> random+crw-rw-rw- <DEV> urandom+A:/ $ random+82461427e1b4012e64cd289b0db180fe+A:/ $ random -d 6 5+6 1 2 6 1+A:/ $ tile+tile: 4 windows on 1024x768, console 61x46+```++### What virtio is, for a learner++Real hardware is complicated because it's real: a network chip has+registers, timing rules and errata. An emulator like QEMU *can* copy such+a chip register by register (that's how it emulates the e1000 network+card or the old VGA card), but it's slow and both sides get complicated.+**Virtio** is the alternative: device types designed for virtual machines,+where guest and host just agree on a simple protocol in shared memory. The+same drivers work in QEMU, KVM, cloud VMs and Firecracker.++Every virtio device has the same three parts:++1. **Feature bits.** The device offers a set of features and the driver+ accepts the ones it understands. Rustbelt only ever accepts+ `VERSION_1` (bit 32: "I speak virtio 1.0"), which the modern interface+ requires.+2. **Virtqueues.** Ring buffers in *guest* memory (`src/virtio/queue.rs`).+ A *descriptor table* lists buffers (physical address, length, and+ whether the device writes it). The driver puts the index of a request+ on the *available ring* and writes the queue number to a *notify*+ register (the "doorbell"). The device does the work, then puts the same+ index on the *used ring* together with how many bytes it wrote, and+ maybe raises an interrupt. The driver either sleeps until that+ interrupt or polls the used ring. That's the whole protocol: what a+ request *means* is up to the device type.+3. **Config space.** A few device-specific registers: the GPU's number of+ displays, the console's size, a network card's MAC address.++The three drivers show three ways of using queues:++| Device | Queues | A request is | Waiting |+|---|---|---|---|+| rng | 1 | one empty buffer; the device fills it with random bytes | polled (it answers at once) |+| console | 2 (receive, transmit) | receive: 8 empty 512-byte buffers always posted; transmit: bytes to print | receive: **interrupt** (input can come at any time); transmit: polled |+| gpu | 2 (control, cursor; only control used) | a command (24-byte header + arguments) plus a buffer for the response | polled |++**Legacy and modern.** The virtio-blk and virtio-net drivers from stages 6+and 12 speak the *legacy* (pre-1.0) interface: every register at a fixed+offset in one I/O-port BAR. That's simpler, but virtio-gpu doesn't support+it. The modern interface (`src/virtio/mod.rs`) puts register blocks+(common config, notify, interrupt status, device config) in memory BARs.+**PCI capabilities** (a linked list in PCI config space, vendor id 0x09)+say which BAR and offset each block is at. `Transport::new` walks that+list, maps the BARs uncached (`memory::map_mmio`) and does the start-up+dance: reset, ACKNOWLEDGE, DRIVER, features, FEATURES_OK, set up the+queues, DRIVER_OK. All three new drivers share it. The rng and console+cards are "transitional" (they'd speak both), so the modern code could+replace the legacy one in virtio-blk and virtio-net later.++**Interrupts.** The console's receive queue uses the card's PCI interrupt+line, which it shares with the network card (IRQ 10 here). PCI interrupts+are level-triggered and can be shared, so `interrupts::pci_irq` asks every+driver "was it you?" (`net::handle_irq`, then `virtio::handle_irq`).+Reading a device's interrupt-status register says yes or no *and* lowers+its line. A driver that forgot that read would leave the line high, and+the CPU would take the same interrupt forever.++### Framebuffers and scanout++A **framebuffer** is memory with one number per pixel: here a 32-bit+`0x00RRGGBB` for each of 1024 x 768 pixels, row after row (3 MiB). Drawing+is just writing to memory. **Scanout** is the display hardware reading that+memory 60 times a second and sending it to the monitor. A real GPU scans+out straight from video memory. virtio-gpu adds a step, because the image+the host shows lives on the host:++```+ guest RAM (our framebuffer) host (QEMU) window+ +-------------------------+ TRANSFER_TO_HOST_2D +-----------+ RESOURCE_FLUSH+ | 0x00RRGGBB pixels | -------------------> | resource 1 | --------------> screen+ +-------------------------+ (copy a rectangle) +-----------+ (show it)+ ^ ATTACH_BACKING ties these pages to resource 1; SET_SCANOUT shows resource 1+```++So setting up (`gpu::init`) is: `GET_DISPLAY_INFO` (QEMU prefers+1280x800; we ask for 1024x768 and QEMU scales the window),+`RESOURCE_CREATE_2D` (an image with id 1, format B8G8R8X8, which makes a+`u32` read as `0x00RRGGBB`), `RESOURCE_ATTACH_BACKING` (its pixels are+these 768 contiguous guest pages) and `SET_SCANOUT` (show resource 1 on+display 0). After drawing, `gpu::flush(x, y, w, h)` sends+`TRANSFER_TO_HOST_2D` and `RESOURCE_FLUSH` together. Nothing reaches the+screen without a flush, so the console keeps a **dirty range**: the pixel+rows it changed since the last flush. It flushes once per `print!`, not per+character.++**The framebuffer console** (`src/fbcon.rs`) keeps the old console's+model: a grid of cells, each a character and a VGA colour attribute, and+the same 16-colour palette. Drawing a cell copies a glyph from+`src/font.rs` (8x16 pixels, one byte per row, one bit per pixel) into the+framebuffer. 1024x768 gives 128x48 characters instead of VGA's 80x25.+Scrolling moves the pixel rows up with one `memmove` (row by row when+the console is in a viewport narrower than the screen). The font is Spleen (BSD-2): ASCII and Latin-1, so `é` now+shows up instead of a `■`, plus box-drawing characters. The console can be+confined to a **viewport** (`FB_SET_CONSOLE`): that's how `tile` puts it in+a window.++**/dev/fb0 for programs.** Linux programs `mmap` `/dev/fb0` and draw into+it directly. Rustbelt has no `mmap` yet (the next stage), so `/dev/fb0` is+a file you `lseek` + `write` pixels into, plus ioctls for what doesn't fit+in a write:++| ioctl | arg | does |+|---|---|---|+| `FB_GET_INFO` (0x4600) | `*[u32; 4]` | width, height, bytes per row, bits per pixel |+| `FB_FLUSH` (0x46f0) | `*[x, y, w, h]`, or 0 | show a rectangle (0: the rows this fd wrote since the last flush) |+| `FB_FILL` (0x46f1) | `*[x, y, w, h, color]` | fill a rectangle in the kernel (much faster than writing it) |+| `FB_SET_CONSOLE` (0x46f2) | `*[x, y, w, h]`, or 0 | move the text console into the rectangle (0: the whole screen); returns `cols << 16 \| rows` |++`user/tile.rs` uses all of them. It writes a gradient wallpaper 8 rows at+a time, splits the screen "dwindle"-style (each window takes half of what's+left, alternating left/right and top/bottom), fills borders and windows+with `FB_FILL`, flushes, then moves the console into the first window.+That's a first sketch of the tiling compositor on the roadmap; the+compositor itself will need `mmap` (to draw without a syscall per+rectangle), input events and windows owned by other processes.++### Why 2D first, and what 3D would take++2D is all a console, a tiling compositor and a software renderer need: the+CPU draws the pixels, the GPU just shows them. It's six commands, and+every later step builds on it, because even 3D output ends with+`SET_SCANOUT` and `RESOURCE_FLUSH`.++3D through **virgl** is a different scale of project. The guest doesn't+send pixels but *rendering commands*. virtio-gpu adds 3D contexts, 3D+resources (textures, buffers) and `SUBMIT_3D`, which carries a command+stream in Gallium's format. On the host, **virglrenderer** turns that+stream into OpenGL calls on the real GPU. In the guest, the stream comes+from **Mesa**, the open-source OpenGL/Vulkan implementation, through its+virgl driver. So Rustbelt would need to run Mesa: that means a C library+(the musl port), `mmap`, threads in user space, a way to load+shared libraries or a big static build, and kernel support for 3D+resources and fences. Newer QEMU versions also offer **Venus** (Vulkan+over virtio-gpu) and **virtio-gpu rutabaga/gfxstream**; they need the same+user-space groundwork. For now, 2D and software rendering are plenty for a+tiling compositor.++### The second console, and testing it headless++`virtconsole` is a port on a `virtio-serial-pci` card. Rustbelt doesn't+negotiate the multi-port feature, so there's exactly one port, port 0,+which needs no control messages. Bytes the host sends land in the receive+buffers; the interrupt handler copies them into a 4 KiB queue and wakes any+reader. `read` on `/dev/hvc0` blocks until there's input. Writes go+straight to the transmit queue. While a program has `/dev/hvc0` open, the+kernel stops mirroring its log there, so a shell on hvc0 isn't interrupted+by `[process]` lines.++QEMU decides where the other end goes: `-chardev socket,...,server=on`+for `cargo run` (connect with `socat`), and `-chardev file,path=OUT,input-path=IN`+for tests. The file chardev sends all of `IN` once at start-up and writes+everything the guest sends into `OUT`, so a headless run can feed hvc0 a+script and check the reply afterwards. `run-headless.sh` feeds+`scripts/hvc0-session.txt` to `getty -1 /dev/hvc0` and checks+`target/headless-hvc0.txt`; `tests/virtio.rs` checks the exact bytes of+`tests/hvc0-input.txt` arrive.++### Testing the GPU++Inside the guest, `tests/virtio.rs` checks the protocol (an error comes+back for a bad resource id) and reads pixels back from the framebuffer.+That only proves the guest's memory is right, though. To check what QEMU+actually *displays*, `scripts/gpu-check.sh` boots the kernel, types `tile`+through the QEMU monitor, saves the screen with the monitor's+`screendump` command as a PPM file (a tiny header, then raw RGB bytes, so+a few lines of Python can read it) and compares 12 pixels with the+colours `tile.rs` draws: the wallpaper gradient at three heights, the gap+between windows, the cyan and gray borders, three window fills, a "text"+bar, and the console window's black background with text on it.++### What it does and doesn't do++- virtio-rng: 4 KiB per request, a 1 s timeout; if the device is missing or+ silent, a TSC-seeded xorshift64* takes over (fine for port numbers, not+ for cryptography).+- virtio-console: one port, no resize events, no multiport; TX waits at+ most 10 ms for a free buffer and counts what it drops. The shell on it+ isn't interactive (no line editing); the host terminal does the echo.+- virtio-gpu: one display, one resource, a fixed 1024x768 (the host's+ preferred size is read and logged, not used), no hardware cursor, no+ resize, no 3D. Flushes are synchronous (polling for each answer).+- `/dev/fb0`: `write`/`lseek`/ioctls; `mmap` comes with the next stage.+- Every new user program's heap is still 64 KiB, so `tile` writes the+ wallpaper in 32 KiB pieces.+ ## Roadmap Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -2430,7 +2762,9 @@ user mode + syscalls, ELF loading, a VFS with a ramfs, PCI, virtio-blk, MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait, process memory cleanup, the shell, hand-written ext2, exFAT and (read-only) btrfs drivers, multi-core support with a sleep/wake scheduler and-guard pages, and networking (virtio-net + smoltcp + sockets).+guard pages, networking (virtio-net + smoltcp + sockets), and the+virtual drivers (virtio-rng, virtio-console, virtio-gpu 2D with a+framebuffer console). - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -2463,6 +2797,9 @@ guard pages, and networking (virtio-net + smoltcp + sockets). - [x] SMP (stage 11): ACPI MADT, local APIC + timer, INIT/SIPI trampoline, per-CPU GDT/TSS/`swapgs` data, a scheduler for 4 CPUs with sleep/wake and wait queues, frames freed on every exit, guard pages under kernel and user stacks — [OSDev: SMP](https://wiki.osdev.org/Symmetric_Multiprocessing), [OSDev: APIC](https://wiki.osdev.org/APIC), [OSDev: MADT](https://wiki.osdev.org/MADT), [OSDev: APIC timer](https://wiki.osdev.org/APIC_Timer) - [x] btrfs, hand-written, read-only: chunk tree (single/DUP), root/FS/csum trees, crc32c everywhere, zlib/lzo/zstd, holes, subvolumes, checked against `btrfs check` and the host's files — [btrfs on-disk format](https://btrfs.readthedocs.io/en/latest/dev/On-disk-format.html) - [x] Networking (stage 12): virtio-net (legacy, interrupt-driven), smoltcp for ARP/IPv4/ICMP/UDP/TCP/DHCP/DNS, BSD-style socket syscalls with blocking on wait queues, `ifconfig`/`ping`/`host`/`fetch`/`httpd` — [OSDev: Virtio](https://wiki.osdev.org/Virtio), [smoltcp](https://docs.rs/smoltcp), [OSDev: Networking](https://wiki.osdev.org/Category:Networking), [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)+- [x] Virtual drivers (stage 13): a modern (1.0) virtio transport shared by virtio-rng (`getrandom`, `/dev/random`, smoltcp's seed), virtio-console (`/dev/hvc0`, `getty`) and virtio-gpu 2D (a 1024x768 framebuffer, a bitmap-font console, `/dev/fb0`, `tile`) — [virtio 1.2 spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html), [OSDev: Virtio](https://wiki.osdev.org/Virtio), [Linux virtio-gpu driver](https://github.com/torvalds/linux/tree/master/drivers/gpu/drm/virtio)+- [ ] `fork`, `exec`, `mmap`, signals, then a musl port (next)+- [ ] A tiling compositor on the framebuffer (later) ### Filesystem roadmap (Tyler's plan) @@ -2509,19 +2846,24 @@ Next, in this order: [smoltcp](https://github.com/smoltcp-rs/smoltcp), sockets, and `ifconfig`, `ping`, `host`, `fetch`, `httpd`. Left over for later: non-blocking sockets and `poll`, IPv6, a loopback interface, TLS.-3. **More virtio devices** (next): virtio-rng (entropy, which TCP sequence- numbers and later TLS want), virtio-console, and virtio-gpu (a- framebuffer instead of VGA text mode).-4. **Running Linux software**: `fork`, `exec`, `mmap` and signals, then a- [musl](https://musl.libc.org/) port, so ordinary C programs recompiled- against Rustbelt's syscalls (numbered like Linux's already, sockets- included) can run.+3. ~~**More virtio devices**~~ — done (stage 13): virtio-rng,+ virtio-console (hvc0) and virtio-gpu in 2D mode, with the text console+ on a framebuffer and `/dev/fb0` for programs. Left over for later:+ moving virtio-blk and virtio-net to the modern transport, resizing the+ display, a hardware cursor, virtio-input (keyboard/mouse events for the+ compositor), 3D (virgl, see above).+4. **Running Linux software** (next): `fork`, `exec`, `mmap` and signals,+ then a [musl](https://musl.libc.org/) port, so ordinary C programs+ recompiled against Rustbelt's syscalls (numbered like Linux's already,+ sockets and `getrandom` included) can run. `mmap` of `/dev/fb0` comes+ with it, so programs can draw without a syscall per rectangle.+5. **The compositor** (after that): see below; `tile` is its first sketch. **Long term: a graphical desktop.** The goal is a **Hyprland-style tiling compositor**: windows tile automatically, with keyboard-driven layouts and smooth animations. It will draw **natively on the virtio-gpu framebuffer** first, with Rustbelt's own programs as clients, so it needs nothing beyond-step 3. **Wayland client support** comes later, once step 4 can run+step 3 plus `mmap` and an input device. **Wayland client support** comes later, once step 4 can run ordinary Linux programs: the compositor then speaks the Wayland protocol, so existing Wayland apps can open windows on it. @@ -2639,7 +2981,31 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi - *Slirp's TFTP server answers from port 69*, not from a fresh port like RFC 1350 says. `tests/net.rs` replies to wherever the data came from. - QEMU's default network card is an e1000. Adding `-netdev`/`-device- virtio-net-pci` replaces it, so the PCI device count stays 10.+ virtio-net-pci` replaces it, so the PCI device count stayed 10 (stage 13's+ GPU, rng and console make it 12).+- **Pitfalls hit in stage 13:**+ - *`memory allocation of 65536 bytes failed` in `tile`.* User programs+ have a fixed 64 KiB heap (`user/ulib.rs`), so a 64 KiB row buffer can+ never fit. The wallpaper now goes out 8 rows (32 KiB) at a time; a+ growable heap needs `mmap` or `brk`, next stage.+ - *virtio-rng's PCI class is 0x00 ("unclassified") and the console's is+ 0x07 (communication)*; `pci::class_name` didn't know them. With the GPU,+ rng and console the PCI bus now has 12 devices instead of 10.+ - *virtio-gpu has no legacy interface*, so the stage-6 style "one I/O+ port BAR" driver couldn't work: the modern transport (capabilities,+ memory BARs, a 64-bit BAR for the GPU) had to come first.+ - *The log mirror must never deadlock.* Every `serial_print!` also goes+ to hvc0. `serial::_print` releases the serial lock first and calls the+ console with interrupts off, so an interrupt handler that prints can't+ find the hvc0 lock already held on its own CPU.+ - *A test counted mounts*: `/dev` is a new mount, so `tests/vfs.rs`+ expects 4, and the root listing has `dev` in it.+ - `-vga none -device virtio-vga` keeps the old VGA text memory (virtio-vga+ *is* VGA-compatible), so the console tests that read `0xb8000` still+ pass, and the bootloader's text output still shows until the switch.+ - *A boot that sat at `A:/ $` for 90 s* turned out to be a stale,+ non-headless image (the headless build had failed to compile), waiting+ for keyboard input; not a driver bug. Check the build before the boot. - **The `\ |` key**: pc-keyboard 0.7's scancode set 1 decodes it (scancode 0x2B) as `KeyCode::Oem7`, but its US 104-key layout only maps `Oem5` to `\` and `|`. So that key typed nothing, which went unnoticed until the
Binary file changed.
@@ -74,6 +74,7 @@ const PROGRAMS: &[&str] = &[ "hello", "evil", "spin", "demo", "child", // stage 5/6 demos and tests "sysinfo", "overflow", // stage 11: CPU load, and a stack overflow (guard pages) "ifconfig", "ping", "host", "fetch", "httpd", // stage 12: networking+ "random", "getty", "tile", // stage 13: virtual drivers ]; const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -79,6 +79,16 @@ wget -o D:/seq.txt http://10.0.2.100/big cat D:/seq.txt | wc -l host example.com httpd -n 1 -t 20+# Stage 13: virtual drivers. The devices in /dev; random bytes from+# virtio-rng; a shell on the second console (getty -1 runs the commands+# QEMU feeds into hvc0 from a file; its output goes to another file that+# run-headless.sh checks); the tiling demo on the virtio-gpu framebuffer.+ls -l /dev+random -n 8+random -d 6 5+getty -1 /dev/hvc0+tile+tile -r nosuchprogram cd Q: cd A:/tmp
@@ -0,0 +1,63 @@+#!/usr/bin/env bash+# virtio-gpu read-back test: boot the interactive kernel (screenshot.sh),+# type `tile`, dump the virtio-gpu scanout as a PPM image and check pixels+# at known places against the colours user/tile.rs draws. This tests the+# whole path: user program -> /dev/fb0 ioctls -> framebuffer memory ->+# TRANSFER_TO_HOST_2D + RESOURCE_FLUSH -> QEMU's display.+#+# Usage: ./scripts/gpu-check.sh+set -euo pipefail+cd "$(dirname "$0")/.."++PPM=target/gpu-check.ppm+SESSION=$(mktemp /tmp/gpu-session.XXXXXX)+echo tile > "$SESSION"+rm -f "$PPM"+./scripts/screenshot.sh "$PPM" "$SESSION" "tile: 4 windows on 1024x768, console 61x46" > target/gpu-check.log 2>&1 \+ || { tail -20 target/gpu-check.log; echo "[gpu-check] screenshot session failed"; exit 1; }+rm -f "$SESSION"++python3 - "$PPM" <<'PY'+import sys+data = open(sys.argv[1], 'rb').read()+# PPM "P6": a text header (magic, width, height, max value), then RGB bytes.+magic, w, h, maxval, pixels = data.split(maxsplit=4)+w, h = int(w), int(h)+assert magic == b'P6' and maxval == b'255', 'not a 24-bit PPM'+def px(x, y):+ i = (y * w + x) * 3+ return tuple(pixels[i:i + 3])+def rgb(c):+ return (c >> 16, (c >> 8) & 0xff, c & 0xff)+# The layout of `tile` on 1024x768 (gaps 10, borders 3, dwindle split):+# window 0 = x 10..507 (console), window 1 = x 517..1014, y 10..379,+# window 2 = x 517..760, y 389..758, window 3 = x 770..1014, y 389..758.+def wallpaper(y): # tile.rs: gradient()+ mix = lambda a, b: a + (b - a) * y // 768+ return (mix(0x1e, 0x50), mix(0x1e, 0x2a), mix(0x2e, 0x78))+checks = [+ ((w, h), (1024, 768), 'screen size'),+ (px(2, 2), wallpaper(2), 'wallpaper, top left corner'),+ (px(5, 700), wallpaper(700), 'wallpaper, left edge'),+ (px(512, 300), wallpaper(300), 'gap between windows'),+ (px(11, 300), rgb(0x33ccff), 'window 0 border (focused, cyan)'),+ (px(518, 100), rgb(0x595959), 'window 1 border (gray)'),+ (px(525, 18), rgb(0x89b4fa), 'window 1 fill (blue)'),+ (px(540, 31), rgb(0x445a7d), 'window 1 "text" bar (darker blue)'),+ (px(525, 397), rgb(0xa6e3a1), 'window 2 fill (green)'),+ (px(778, 397), rgb(0xf9e2af), 'window 3 fill (yellow)'),+ (px(1020, 760), wallpaper(760), 'wallpaper, bottom right'),+]+bad = 0+for got, want, what in checks:+ ok = got == want+ bad += not ok+ print(f"[gpu-check] {'ok ' if ok else 'FAIL'} {what}: {got}" + ('' if ok else f", expected {want}"))+# The console window: black background with text in some other colour.+console = {px(x, y) for x in range(13, 505, 3) for y in range(13, 756, 3)}+ok = (0, 0, 0) in console and len(console) > 1+bad += not ok+print(f"[gpu-check] {'ok ' if ok else 'FAIL'} console window has black background and text ({len(console)} colours)")+sys.exit(1 if bad else 0)+PY+echo "[gpu-check] virtio-gpu output matches"
@@ -0,0 +1,5 @@+ver+echo hi over hvc0+random -d 20 3+ls /dev+exit 0
@@ -56,6 +56,16 @@ rm -f "$CURL_OUT" done ) & CURL_PID=$! +# Stage 13 virtual drivers: virtio-gpu (instead of the VGA card),+# virtio-rng, and virtio-console whose hvc0 port reads the commands in+# scripts/hvc0-session.txt and writes into target/headless-hvc0.txt (the+# self-test runs `getty -1 /dev/hvc0`, a shell on that port).+HVC_OUT=target/headless-hvc0.txt+rm -f "$HVC_OUT"+VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci+ -device virtio-serial-pci -device virtconsole,chardev=hvc+ -chardev "file,id=hvc,path=$HVC_OUT,input-path=scripts/hvc0-session.txt")+ LOG=target/headless-serial.log set +e timeout 90 qemu-system-x86_64 -smp 4 \@@ -65,6 +75,7 @@ timeout 90 qemu-system-x86_64 -smp 4 \ -drive "$EXFAT_DRIVE" \ -drive "$BTRFS_DRIVE" \ -netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \+ "${VIRTIO_DEVICES[@]}" \ -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ -serial stdio -display none -no-reboot | tee "$LOG" code=${PIPESTATUS[0]}@@ -74,7 +85,7 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then expect=(- "Rustbelt 0.12.0 (stage 12: networking) x86_64" # ver+ "Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64" # ver "[smp] 4 CPUs online" # smp.rs started 3 more cores "B: /mnt/disk0 fat32" # drives "C: /mnt/disk1 ext2"@@ -123,6 +134,14 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then "Hello from the host, via QEMU guestfwd!" # fetch (TCP + HTTP) "fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt" # wget -o D: "[httpd] served 1 request, bye" # httpd, reached via hostfwd+ "[virtio] rng: virtio-rng at 00:04.0" # stage 13: virtio/+ "[virtio] hvc0: virtio-console at 00:05.0"+ "[virtio] gpu: virtio-gpu 2D at 00:03.0, display 1280x800, framebuffer 1024x768"+ "[virtio] fbcon: text console on the framebuffer, 128x48 characters"+ "crw-rw-rw- <DEV> fb0" # ls -l /dev+ "crw-rw-rw- <DEV> hvc0"+ "tile: 4 windows on 1024x768, console 61x46" # the tiling demo+ "console: full screen, 128x48 characters" # tile -r "sh: nosuchprogram: command not found" "cd: there is no drive Q:" "A:/tmp $ pwd"@@ -131,11 +150,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then for line in "${expect[@]}"; do grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; } done- # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 22,+ # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 25, # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3, # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.- for n in 1 22 3 4 20000; do+ for n in 1 25 3 4 20000; do grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; } done # No memory leaks: once the shell and all ~60 programs it ran have@@ -146,12 +165,21 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1 fi # The other direction: the host fetched a page from httpd in the guest.- if grep -qF "Hello from Rustbelt! Rustbelt 0.12.0" "$CURL_OUT" 2>/dev/null; then+ if grep -qF "Hello from Rustbelt! Rustbelt 0.13.0" "$CURL_OUT" 2>/dev/null; then echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")" else echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1 fi- [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+5 expected shell lines found" || code=35+ # random -n 8: 16 hex digits; random -d 6 5: five numbers 1-6.+ grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }+ grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }+ # The shell on hvc0 ran the host's commands and answered over hvc0.+ for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.13.0 (stage 13" \+ "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do+ grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }+ done+ grep -qxE -- "[0-9]+ [0-9]+ [0-9]+" "$HVC_OUT" || { echo "[run-headless] MISSING on hvc0: random -d 20 3"; missing=1; }+ [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+12 expected shell and hvc0 lines found" || code=35 fi # isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33.
@@ -1,11 +1,12 @@ #!/usr/bin/env bash # Boot the normal (interactive) kernel with no window, type a shell session-# into it through the QEMU monitor's `sendkey` command, then dump the VGA-# screen to a PNG. Doubles as an automated test of the keyboard, the raw+# into it through the QEMU monitor's `sendkey` command, then dump the+# screen (the virtio-gpu framebuffer) to a PNG, or a PPM if the file name+# ends in .ppm (scripts/gpu-check.sh reads its pixels). Doubles as an automated test of the keyboard, the raw # console mode and the shell's line editor (Backspace, up-arrow history): # afterwards the serial copy of the console is checked for expected text. #-# Usage: ./scripts/screenshot.sh [out.png] [session.txt] [expected text...]+# Usage: ./scripts/screenshot.sh [out.png|out.ppm] [session.txt] [expected text...] # session.txt: one command per line, typed key by key followed by Enter. # In it, {BS} is Backspace and {UP} is the up arrow. # Exits 1 if any expected text doesn't show up.@@ -21,21 +22,21 @@ OUT="${1:-$PWD/boot.png}" SESSION="${2:-}" shift $(( $# > 2 ? 2 : $# )) if [ -z "$SESSION" ]; then- # The default session (what boot.png shows): the network address from- # DHCP, a ping to QEMU's gateway, and two HTTP downloads from the test- # server that QEMU runs for us (guestfwd, see below), one printed and one- # saved to D:. "vx{BS}er" types a typo and fixes it.+ # The default session (what boot.png shows): the tiling demo moves the+ # text console into the left window of the virtio-gpu framebuffer, then+ # a few commands run in it: the devices, random numbers from virtio-rng,+ # and a ping. "vx{BS}er" types a typo and fixes it. SESSION=$(mktemp /tmp/session.XXXXXX) cat > "$SESSION" <<'TXT'+tile vx{BS}er-ifconfig-ping -c 3 -i 200 10.0.2.2-fetch http://10.0.2.100/-wget -o D:/seq.txt http://10.0.2.100/big+ls -l /dev+random+random -d 6 5+ping -c 2 -i 200 10.0.2.2 TXT- set -- "Rustbelt 0.12.0 (stage 12: networking) x86_64" "inet 10.0.2.15/24 gateway 10.0.2.2" \- "3 packets transmitted, 3 received, 0% packet loss" "Hello from the host, via QEMU guestfwd!" \- "saved 108894 bytes to /mnt/disk2/seq.txt"+ set -- "Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+ "<DEV> fb0" "2 packets transmitted, 2 received, 0% packet loss" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)@@ -47,6 +48,8 @@ qemu-system-x86_64 -smp 4 -drive format=raw,file="$IMG" \ -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \ -netdev "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh" \ -device virtio-net-pci,netdev=net0 \+ -vga none -device virtio-vga -device virtio-rng-pci \+ -device virtio-serial-pci -device virtconsole,chardev=hvc -chardev null,id=hvc \ -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$! sleep 4 # boot until the shell prompt@@ -70,7 +73,8 @@ for line in open(session): s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.08) time.sleep(0.8) # let the command run time.sleep(0.5)-s.sendall(f"screendump {out} -f png\n".encode()); time.sleep(1)+fmt = 'ppm' if out.endswith('.ppm') else 'png'+s.sendall(f"screendump {out} -f {fmt}\n".encode()); time.sleep(1) s.sendall(b"quit\n"); time.sleep(0.5) PY wait $QPID || true
@@ -0,0 +1,252 @@+//! /dev: device files. (Unix's big idea: devices look like files, so `cat`,+//! `>` and every program that reads or writes files can use them.)+//!+//! /dev/null reads as empty, swallows writes+//! /dev/random random bytes (virtio-rng; /dev/urandom is the same --+//! /dev/urandom on modern Linux they only differ before boot-time seeding)+//! /dev/hvc0 the virtio console: a second terminal+//! /dev/fb0 the virtio-gpu framebuffer: pixels at byte offsets,+//! plus ioctls (see syscall.rs) for its size, flushing,+//! filling rectangles and moving the text console+//!+//! A tiny file system (`DevFs`) mounted at /dev lists them; opening one+//! gives a device-specific `File` (via `Inode::open_device`) instead of the+//! usual read-the-inode file. Linux does the same with "character device"+//! inodes whose `open` is routed to a driver.++use crate::errno::Errno;+use crate::fs::file::{File, KIND_DEVICE, SEEK_CUR, SEEK_END, SEEK_SET, Stat};+use crate::fs::{DirEntry, FileSystem, Inode, InodeKind};+use crate::virtio::{console, gpu, rng};+use alloc::{string::String, sync::Arc, vec::Vec};+use core::sync::atomic::Ordering;+use spin::Mutex;++pub struct DevFs;++impl FileSystem for DevFs {+ fn name(&self) -> &'static str {+ "devfs"+ }+ fn root(&self) -> Arc<dyn Inode> {+ Arc::new(DevDir)+ }+}++/// The devices that exist on this machine.+fn names() -> Vec<&'static str> {+ let mut names = alloc::vec!["null", "random", "urandom"];+ if console::present() {+ names.push("hvc0");+ }+ if gpu::framebuffer().is_some() {+ names.push("fb0");+ }+ names.sort();+ names+}++struct DevDir;++impl Inode for DevDir {+ fn kind(&self) -> InodeKind {+ InodeKind::Directory+ }+ fn size(&self) -> usize {+ names().len()+ }+ fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+ let name = names().into_iter().find(|n| *n == name).ok_or(Errno::ENOENT)?;+ Ok(Arc::new(DevNode(name)))+ }+ fn list(&self) -> Result<Vec<DirEntry>, Errno> {+ Ok(names().into_iter().map(|n| DirEntry { name: String::from(n), kind: InodeKind::Device }).collect())+ }+}++/// One device's directory entry.+struct DevNode(&'static str);++impl Inode for DevNode {+ fn kind(&self) -> InodeKind {+ InodeKind::Device+ }+ fn size(&self) -> usize {+ match self.0 {+ "fb0" => gpu::framebuffer().map_or(0, |f| f.size_bytes()),+ _ => 0,+ }+ }+ fn mode(&self) -> u32 {+ 0o666+ }+ fn open_device(&self) -> Option<Arc<dyn File>> {+ Some(match self.0 {+ "null" => Arc::new(Null),+ "random" | "urandom" => Arc::new(Random),+ "hvc0" => {+ console::OPEN_COUNT.fetch_add(1, Ordering::Relaxed);+ Arc::new(Hvc)+ }+ "fb0" => Arc::new(Framebuffer::new(gpu::framebuffer()?)),+ _ => return None,+ })+ }+}++fn device_stat(size: usize) -> Result<Stat, Errno> {+ Ok(Stat { kind: KIND_DEVICE, size: size as u64, mode: 0o666 })+}++struct Null;++impl File for Null {+ fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+ Ok(0)+ }+ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ Ok(buf.len())+ }+ fn stat(&self) -> Result<Stat, Errno> {+ device_stat(0)+ }+}++struct Random;++impl File for Random {+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ let n = buf.len().min(65536);+ rng::fill(&mut buf[..n]);+ Ok(n)+ }+ /// Linux mixes written bytes into its pool; we have no pool, so we+ /// just accept them.+ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ Ok(buf.len())+ }+ fn stat(&self) -> Result<Stat, Errno> {+ device_stat(0)+ }+}++struct Hvc;++impl File for Hvc {+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ Ok(console::read(buf))+ }+ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ console::write(buf);+ Ok(buf.len()) // what didn't fit is dropped, like a serial line+ }+ fn stat(&self) -> Result<Stat, Errno> {+ device_stat(0)+ }+}++impl Drop for Hvc {+ fn drop(&mut self) {+ console::OPEN_COUNT.fetch_sub(1, Ordering::Relaxed);+ }+}++/// /dev/fb0: the framebuffer as a file of `stride * height * 4` bytes.+/// Writing changes pixels in memory; they reach the screen on the FLUSH+/// ioctl or when the file is closed (we remember which rows changed).+pub struct Framebuffer {+ pub info: gpu::FbInfo,+ offset: Mutex<usize>,+ dirty: Mutex<Option<(u32, u32)>>, // pixel rows [first, last)+}++impl Framebuffer {+ fn new(info: gpu::FbInfo) -> Framebuffer {+ Framebuffer { info, offset: Mutex::new(0), dirty: Mutex::new(None) }+ }++ fn bytes(&self) -> &'static mut [u8] {+ unsafe { core::slice::from_raw_parts_mut(self.info.base as *mut u8, self.info.size_bytes()) }+ }++ /// Remember that pixel rows [y0, y1) changed.+ pub fn mark_dirty(&self, y0: u32, y1: u32) {+ let mut dirty = self.dirty.lock();+ *dirty = Some(match *dirty {+ Some((a, b)) => (a.min(y0), b.max(y1)),+ None => (y0, y1),+ });+ }++ /// Send the changed rows to the screen.+ pub fn flush(&self) -> bool {+ match self.dirty.lock().take() {+ Some((y0, y1)) => gpu::flush(0, y0, self.info.width, y1 - y0),+ None => true,+ }+ }++ /// Fill a rectangle with one colour (clipped to the screen).+ pub fn fill(&self, x: u32, y: u32, w: u32, h: u32, color: u32) {+ let (x0, y0) = (x.min(self.info.width), y.min(self.info.height));+ let (x1, y1) = (x.saturating_add(w).min(self.info.width), y.saturating_add(h).min(self.info.height));+ for row in y0..y1 {+ let start = (row * self.info.stride) as usize;+ let line = unsafe { core::slice::from_raw_parts_mut(self.info.pixels().add(start), self.info.width as usize) };+ line[x0 as usize..x1 as usize].fill(color);+ }+ if y1 > y0 {+ self.mark_dirty(y0, y1);+ }+ }+}++impl File for Framebuffer {+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ let mut offset = self.offset.lock();+ let src = self.bytes();+ let n = buf.len().min(src.len().saturating_sub(*offset));+ buf[..n].copy_from_slice(&src[*offset..*offset + n]);+ *offset += n;+ Ok(n)+ }+ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ let mut offset = self.offset.lock();+ let dst = self.bytes();+ if *offset >= dst.len() && !buf.is_empty() {+ return Err(Errno::ENOSPC); // past the end of the screen+ }+ let n = buf.len().min(dst.len() - *offset);+ dst[*offset..*offset + n].copy_from_slice(&buf[..n]);+ let row_bytes = self.info.stride as usize * 4;+ if n > 0 {+ self.mark_dirty((*offset / row_bytes) as u32, ((*offset + n - 1) / row_bytes + 1) as u32);+ }+ *offset += n;+ Ok(n)+ }+ fn seek(&self, offset: i64, whence: u64) -> Result<u64, Errno> {+ let mut pos = self.offset.lock();+ let base = match whence {+ SEEK_SET => 0,+ SEEK_CUR => *pos as i64,+ SEEK_END => self.info.size_bytes() as i64,+ _ => return Err(Errno::EINVAL),+ };+ let new = base.checked_add(offset).filter(|&p| p >= 0).ok_or(Errno::EINVAL)?;+ *pos = new as usize;+ Ok(new as u64)+ }+ fn stat(&self) -> Result<Stat, Errno> {+ device_stat(self.info.size_bytes())+ }+ fn as_framebuffer(&self) -> Option<&Framebuffer> {+ Some(self)+ }+}++impl Drop for Framebuffer {+ fn drop(&mut self) {+ self.flush(); // like close() on Linux's deferred-io framebuffers+ }+}
@@ -15,6 +15,7 @@ pub enum Errno { EFAULT = 14, // bad address (pointer not in user space / not mapped) EBUSY = 16, // busy (e.g. removing a mount point) EEXIST = 17, // file exists+ ENODEV = 19, // no such device (e.g. no GPU for a framebuffer ioctl) ENOTDIR = 20, // a path component isn't a directory EISDIR = 21, // is a directory EINVAL = 22, // invalid argument@@ -51,8 +52,8 @@ impl Errno { /// The errno for a negative syscall result like -2, if we know it. pub fn from_ret(ret: i64) -> Option<Errno> { use Errno::*;- const ALL: [Errno; 40] = [- ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST,+ const ALL: [Errno; 41] = [+ ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV, ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE, ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE, EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,
@@ -0,0 +1,196 @@+//! The text console, drawn into the virtio-gpu framebuffer.+//!+//! VGA text mode gave us character cells for free: write a byte, the video+//! card draws the letter. A framebuffer is just pixels, so we keep the same+//! grid of (character, colour) cells ourselves and *render* each cell from+//! the bitmap font (font.rs, 8x16 pixels per glyph). 1024x768 pixels /+//! 8x16 = 128 columns x 48 rows.+//!+//! Scrolling moves the pixels up one text row (a big memmove) instead of+//! re-rendering every glyph. After a print we tell the GPU which pixel rows+//! changed (`flush`) -- only those get copied to the host.+//!+//! The console can be confined to a *viewport* (a rectangle of the screen),+//! which the `tile` demo uses to put the shell in one tile of its layout.+//! vga_buffer.rs decides whether text goes here or to the VGA buffer.++use crate::font::{FONT, GLYPH_HEIGHT, GLYPH_WIDTH};+use crate::virtio::gpu::{self, FbInfo};+use alloc::{vec, vec::Vec};++/// The 16 VGA colours as 0x00RRGGBB, so colour attributes mean the same+/// on both consoles.+pub const PALETTE: [u32; 16] = [+ 0x000000, 0x0000aa, 0x00aa00, 0x00aaaa, 0xaa0000, 0xaa00aa, 0xaa5500, 0xaaaaaa,+ 0x555555, 0x5555ff, 0x55ff55, 0x55ffff, 0xff5555, 0xff55ff, 0xffff55, 0xffffff,+];++/// A rectangle in pixels.+#[derive(Clone, Copy, Debug, PartialEq, Eq)]+pub struct Rect {+ pub x: u32,+ pub y: u32,+ pub w: u32,+ pub h: u32,+}++pub struct FbConsole {+ fb: FbInfo,+ view: Rect,+ cols: usize,+ rows: usize,+ cells: Vec<(u8, u8)>, // (glyph, colour attribute), row by row+ dirty: Option<(u32, u32)>, // pixel rows [first, last) that changed+ cursor: Option<(usize, usize)>,+}++impl FbConsole {+ pub fn new(fb: FbInfo) -> FbConsole {+ let view = Rect { x: 0, y: 0, w: fb.width, h: fb.height };+ let mut con = FbConsole { fb, view, cols: 0, rows: 0, cells: Vec::new(), dirty: None, cursor: None };+ con.set_view(view, 0x0a);+ con+ }++ pub fn cols(&self) -> usize { self.cols }+ pub fn rows(&self) -> usize { self.rows }+ pub fn view(&self) -> Rect { self.view }++ /// Move the console to `view` (clipped to the screen; at least 16x4+ /// cells). Keeps the bottom-most text, clears the new area with+ /// background attribute `attr`.+ pub fn set_view(&mut self, view: Rect, attr: u8) {+ let x = view.x.min(self.fb.width - 16 * GLYPH_WIDTH as u32);+ let y = view.y.min(self.fb.height - 4 * GLYPH_HEIGHT as u32);+ let w = view.w.clamp(16 * GLYPH_WIDTH as u32, self.fb.width - x);+ let h = view.h.clamp(4 * GLYPH_HEIGHT as u32, self.fb.height - y);+ let (cols, rows) = (w as usize / GLYPH_WIDTH, h as usize / GLYPH_HEIGHT);+ let mut cells = vec![(b' ', attr); cols * rows];+ // Copy the old text, bottom rows to bottom rows.+ for r in 0..rows.min(self.rows) {+ let (old_r, new_r) = (self.rows - 1 - r, rows - 1 - r);+ for c in 0..cols.min(self.cols) {+ cells[new_r * cols + c] = self.cells[old_r * self.cols + c];+ }+ }+ self.view = Rect { x, y, w, h };+ (self.cols, self.rows, self.cells, self.cursor) = (cols, rows, cells, None);+ self.fill_px(self.view, PALETTE[(attr >> 4) as usize & 15]);+ for r in 0..rows {+ for c in 0..cols {+ self.draw_cell(r, c);+ }+ }+ self.mark_dirty(0, h);+ }++ fn fill_px(&mut self, r: Rect, color: u32) {+ for y in r.y..r.y + r.h {+ let row = unsafe { self.pixel_row(y) };+ row[r.x as usize..(r.x + r.w) as usize].fill(color);+ }+ }++ /// Pixel row `y` of the framebuffer.+ unsafe fn pixel_row(&self, y: u32) -> &'static mut [u32] {+ let start = unsafe { self.fb.pixels().add((y * self.fb.stride) as usize) };+ unsafe { core::slice::from_raw_parts_mut(start, self.fb.width as usize) }+ }++ /// Mark pixel rows [y0, y1) of the view as changed.+ fn mark_dirty(&mut self, y0: u32, y1: u32) {+ let (y0, y1) = (self.view.y + y0, self.view.y + y1);+ self.dirty = Some(match self.dirty {+ Some((a, b)) => (a.min(y0), b.max(y1)),+ None => (y0, y1),+ });+ }++ /// Render one cell from the font: a set bit is foreground colour, a+ /// clear one background. The cursor is an underline in the last 2 rows.+ fn draw_cell(&mut self, row: usize, col: usize) {+ let (glyph, attr) = self.cells[row * self.cols + col];+ let fg = PALETTE[attr as usize & 15];+ let bg = PALETTE[(attr >> 4) as usize & 15];+ let x0 = self.view.x as usize + col * GLYPH_WIDTH;+ let y0 = self.view.y + (row * GLYPH_HEIGHT) as u32;+ let cursor_here = self.cursor == Some((row, col));+ for (dy, &bits) in FONT[glyph as usize].iter().enumerate() {+ let line = unsafe { self.pixel_row(y0 + dy as u32) };+ let bits = if cursor_here && dy >= GLYPH_HEIGHT - 2 { 0xff } else { bits };+ for dx in 0..GLYPH_WIDTH {+ line[x0 + dx] = if bits & (0x80 >> dx) != 0 { fg } else { bg };+ }+ }+ let y = (row * GLYPH_HEIGHT) as u32;+ self.mark_dirty(y, y + GLYPH_HEIGHT as u32);+ }++ pub fn put(&mut self, row: usize, col: usize, glyph: u8, attr: u8) {+ if row < self.rows && col < self.cols {+ self.cells[row * self.cols + col] = (glyph, attr);+ self.draw_cell(row, col);+ }+ }++ pub fn cell(&self, row: usize, col: usize) -> (u8, u8) {+ self.cells[row * self.cols + col]+ }++ /// Show the cursor at (row, col) (None: hide it).+ pub fn set_cursor(&mut self, at: Option<(usize, usize)>) {+ let at = at.filter(|&(r, c)| r < self.rows && c < self.cols);+ if at == self.cursor {+ return;+ }+ let old = core::mem::replace(&mut self.cursor, at);+ for (r, c) in [old, at].into_iter().flatten() {+ self.draw_cell(r, c);+ }+ }++ /// Everything up one text row; the bottom row becomes blank (`attr`).+ pub fn scroll_up(&mut self, attr: u8) {+ self.set_cursor(None); // or its underline would move up too+ self.cells.copy_within(self.cols.., 0);+ let last = (self.rows - 1) * self.cols;+ self.cells[last..].fill((b' ', attr));+ // Move the pixels: row y+16 -> row y, for the whole text area.+ let (x0, x1) = (self.view.x as usize, self.view.x as usize + self.cols * GLYPH_WIDTH);+ let text_h = (self.rows * GLYPH_HEIGHT) as u32;+ let stride = self.fb.stride as usize;+ if x0 == 0 && x1 == stride {+ // Full-width view: one big contiguous move.+ let base = self.view.y as usize * stride;+ let len = text_h as usize * stride;+ let all = unsafe { core::slice::from_raw_parts_mut(self.fb.pixels().add(base), len) };+ all.copy_within(GLYPH_HEIGHT * stride.., 0);+ } else {+ for y in self.view.y..self.view.y + text_h - GLYPH_HEIGHT as u32 {+ let src = unsafe { self.pixel_row(y + GLYPH_HEIGHT as u32) };+ let dst = unsafe { self.pixel_row(y) };+ dst[x0..x1].copy_from_slice(&src[x0..x1]);+ }+ }+ for c in 0..self.cols {+ self.draw_cell(self.rows - 1, c);+ }+ self.mark_dirty(0, text_h);+ }++ /// Blank every cell (rows `from..`).+ pub fn clear_rows(&mut self, from: usize, attr: u8) {+ for r in from..self.rows {+ for c in 0..self.cols {+ self.put(r, c, b' ', attr);+ }+ }+ }++ /// Send the changed pixel rows to the screen.+ pub fn flush(&mut self) {+ if let Some((y0, y1)) = self.dirty.take() {+ gpu::flush(self.view.x, y0, self.view.w, y1 - y0);+ }+ }+}
@@ -0,0 +1,313 @@+//! An 8x16 bitmap font for the framebuffer console: 256 glyphs, one byte+//! per pixel row, bit 7 = leftmost pixel. Generated from Spleen 8x16 2.2.0+//! (by Frederic Cambus, https://github.com/fcambus/spleen) -- printable+//! ASCII and Latin-1 at their own codes, a few box-drawing and arrow+//! glyphs in the otherwise unused slots 0x80-0x9f (see `glyph_index`), and+//! slot 0x7f is a hollow box for "a character we don't have".+//!+//! Spleen's license (BSD 2-Clause), as it requires:+//!+//! Copyright (c) 2018-2026, Frederic Cambus+//! All rights reserved.+//!+//! Redistribution and use in source and binary forms, with or without+//! modification, are permitted provided that the following conditions are met:+//!+//! * Redistributions of source code must retain the above copyright+//! notice, this list of conditions and the following disclaimer.+//!+//! * Redistributions in binary form must reproduce the above copyright+//! notice, this list of conditions and the following disclaimer in the+//! documentation and/or other materials provided with the distribution.+//!+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+//! ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE+//! LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+//! CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+//! SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+//! INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+//! CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+//! ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+//! POSSIBILITY OF SUCH DAMAGE.++pub const GLYPH_WIDTH: usize = 8;+pub const GLYPH_HEIGHT: usize = 16;+/// Slot of the "missing character" box.+pub const MISSING: u8 = 0x7f;++/// Unicode characters drawn from the 0x80.. slots, in order.+const EXTRA: [char; 22] = ['\u{2500}', '\u{2502}', '\u{250c}', '\u{2510}', '\u{2514}', '\u{2518}', '\u{251c}', '\u{2524}', '\u{252c}', '\u{2534}', '\u{253c}', '\u{2588}', '\u{2591}', '\u{2592}', '\u{2593}', '\u{25a0}', '\u{2022}', '\u{2026}', '\u{2190}', '\u{2191}', '\u{2192}', '\u{2193}'];++/// Which glyph shows character `c`.+pub fn glyph_index(c: char) -> u8 {+ match c as u32 {+ 0x20..=0x7e | 0xa0..=0xff => c as u8,+ _ => match EXTRA.iter().position(|&e| e == c) {+ Some(i) => 0x80 + i as u8,+ None => MISSING,+ },+ }+}++/// The glyphs (index = slot).+#[rustfmt::skip]+pub static FONT: [[u8; 16]; 256] = [+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 20 ' '+ [0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 21 '!'+ [0x00, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 22 '"'+ [0x00, 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00], // 23 '#'+ [0x00, 0x10, 0x7e, 0xd0, 0xd0, 0xd0, 0x7c, 0x16, 0x16, 0x16, 0x16, 0xfc, 0x10, 0x00, 0x00, 0x00], // 24 '$'+ [0x00, 0x00, 0x06, 0x66, 0x6c, 0x0c, 0x18, 0x18, 0x30, 0x36, 0x66, 0x60, 0x00, 0x00, 0x00, 0x00], // 25 '%'+ [0x00, 0x00, 0x38, 0x6c, 0x6c, 0x6c, 0x38, 0x70, 0xda, 0xcc, 0xcc, 0x7a, 0x00, 0x00, 0x00, 0x00], // 26 '&'+ [0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 27 "'"+ [0x00, 0x0e, 0x18, 0x30, 0x30, 0x60, 0x60, 0x60, 0x60, 0x30, 0x30, 0x18, 0x0e, 0x00, 0x00, 0x00], // 28 '('+ [0x00, 0x70, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x70, 0x00, 0x00, 0x00], // 29 ')'+ [0x00, 0x00, 0x00, 0x00, 0x66, 0x3c, 0x18, 0xff, 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00], // 2a '*'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 2b '+'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // 2c ','+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 2d '-'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 2e '.'+ [0x00, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x00, 0x00, 0x00], // 2f '/'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 30 '0'+ [0x00, 0x00, 0x18, 0x38, 0x78, 0x58, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // 31 '1'+ [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, 0x00, 0x00, 0x00], // 32 '2'+ [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 33 '3'+ [0x00, 0x00, 0xc0, 0xc0, 0xcc, 0xcc, 0xcc, 0xcc, 0xfe, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00], // 34 '4'+ [0x00, 0x00, 0xfe, 0xc6, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 35 '5'+ [0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 36 '6'+ [0x00, 0x00, 0xfe, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // 37 '7'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 38 '8'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 39 '9'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 3a ':'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // 3b ';'+ [0x00, 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00], // 3c '<'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 3d '='+ [0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00], // 3e '>'+ [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // 3f '?'+ [0x00, 0x00, 0x00, 0x7c, 0xc2, 0xda, 0xda, 0xda, 0xda, 0xde, 0xc0, 0x7c, 0x00, 0x00, 0x00, 0x00], // 40 '@'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 41 'A'+ [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 42 'B'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 43 'C'+ [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 44 'D'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 45 'E'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 46 'F'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xde, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 47 'G'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 48 'H'+ [0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // 49 'I'+ [0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf0, 0x00, 0x00, 0x00, 0x00], // 4a 'J'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 4b 'K'+ [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 4c 'L'+ [0x00, 0x00, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 4d 'M'+ [0x00, 0x00, 0xc6, 0xc6, 0xe6, 0xe6, 0xd6, 0xd6, 0xce, 0xce, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 4e 'N'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 4f 'O'+ [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 50 'P'+ [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xd6, 0x7c, 0x18, 0x0c, 0x00, 0x00], // 51 'Q'+ [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 52 'R'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 53 'S'+ [0x00, 0x00, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 54 'T'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 55 'U'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 56 'V'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00, 0x00, 0x00, 0x00], // 57 'W'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 58 'X'+ [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 59 'Y'+ [0x00, 0x00, 0xfe, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xc0, 0xfe, 0x00, 0x00, 0x00, 0x00], // 5a 'Z'+ [0x00, 0x3e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3e, 0x00, 0x00, 0x00], // 5b '['+ [0x00, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x00, 0x00, 0x00], // 5c '\\'+ [0x00, 0x7c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x7c, 0x00, 0x00, 0x00], // 5d ']'+ [0x00, 0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 5e '^'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00], // 5f '_'+ [0x00, 0x30, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 60 '`'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 61 'a'+ [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 62 'b'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 63 'c'+ [0x00, 0x00, 0x06, 0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 64 'd'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 65 'e'+ [0x00, 0x00, 0x1e, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // 66 'f'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06, 0x06, 0xfc, 0x00], // 67 'g'+ [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 68 'h'+ [0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // 69 'i'+ [0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00], // 6a 'j'+ [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xf0, 0xd8, 0xcc, 0xc6, 0x00, 0x00, 0x00, 0x00], // 6b 'k'+ [0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1c, 0x00, 0x00, 0x00, 0x00], // 6c 'l'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0xd6, 0xd6, 0xd6, 0xd6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 6d 'm'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 6e 'n'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 6f 'o'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // 70 'p'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x00], // 71 'q'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 72 'r'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 73 's'+ [0x00, 0x00, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1e, 0x00, 0x00, 0x00, 0x00], // 74 't'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 75 'u'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 76 'v'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xd6, 0xd6, 0xd6, 0x6e, 0x00, 0x00, 0x00, 0x00], // 77 'w'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 78 'x'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00], // 79 'y'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xfe, 0x00, 0x00, 0x00, 0x00], // 7a 'z'+ [0x00, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x00, 0x00, 0x00], // 7b '{'+ [0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00], // 7c '|'+ [0x00, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00], // 7d '}'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x7e, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 7e '~'+ [0x00, 0x00, 0x00, 0x7e, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7e, 0x00, 0x00, 0x00, 0x00], // 7f missing+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 80 U+2500 ─+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 81 U+2502 │+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 82 U+250C ┌+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 83 U+2510 ┐+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 84 U+2514 └+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 85 U+2518 ┘+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1f, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 86 U+251C ├+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 87 U+2524 ┤+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 88 U+252C ┬+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 89 U+2534 ┴+ [0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18], // 8a U+253C ┼+ [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], // 8b U+2588 █+ [0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44], // 8c U+2591 ░+ [0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa], // 8d U+2592 ▒+ [0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77], // 8e U+2593 ▓+ [0x00, 0x00, 0x00, 0x00, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00], // 8f U+25A0 ■+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 90 U+2022 •+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdb, 0xdb, 0x00, 0x00, 0x00, 0x00], // 91 U+2026 …+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0xfe, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 92 U+2190 ←+ [0x00, 0x00, 0x18, 0x3c, 0x5a, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 93 U+2191 ↑+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x04, 0xfe, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 94 U+2192 →+ [0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x5a, 0x3c, 0x18, 0x00, 0x00, 0x00, 0x00], // 95 U+2193 ↓+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // a0 nbsp+ [0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // a1 '¡'+ [0x00, 0x00, 0x00, 0x00, 0x08, 0x7e, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0x7e, 0x08, 0x00, 0x00, 0x00], // a2 '¢'+ [0x00, 0x00, 0x38, 0x6c, 0x60, 0x60, 0x60, 0xf8, 0x60, 0x60, 0xc0, 0xfe, 0x00, 0x00, 0x00, 0x00], // a3 '£'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00], // a4 '¤'+ [0x00, 0x00, 0xc3, 0xc3, 0x66, 0x3c, 0x18, 0x3c, 0x18, 0x3c, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // a5 '¥'+ [0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00], // a6 '¦'+ [0x00, 0x3c, 0x66, 0x60, 0x30, 0x3c, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x0c, 0x06, 0x66, 0x3c, 0x00], // a7 '§'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // a8 '¨'+ [0x00, 0x00, 0x00, 0x7c, 0x82, 0x9a, 0xa2, 0xa2, 0xa2, 0x9a, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00], // a9 '©'+ [0x00, 0x38, 0x0c, 0x3c, 0x4c, 0x3c, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // aa 'ª'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x66, 0xcc, 0x66, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00], // ab '«'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ac '¬'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ad '\xad'+ [0x00, 0x00, 0x00, 0x7c, 0x82, 0xba, 0xaa, 0xb2, 0xaa, 0xaa, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00], // ae '®'+ [0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // af '¯'+ [0x00, 0x38, 0x6c, 0x6c, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b0 '°'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00], // b1 '±'+ [0x00, 0x38, 0x4c, 0x0c, 0x38, 0x60, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b2 '²'+ [0x00, 0x1c, 0x26, 0x0c, 0x06, 0x26, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b3 '³'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b4 '´'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xf6, 0xc0, 0xc0, 0xc0, 0x00], // b5 'µ'+ [0x00, 0x00, 0x7e, 0xd6, 0xd6, 0xd6, 0x76, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00], // b6 '¶'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b7 '·'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00], // b8 '¸'+ [0x00, 0x18, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // b9 '¹'+ [0x00, 0x38, 0x6c, 0x6c, 0x38, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ba 'º'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcc, 0x66, 0x33, 0x66, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00], // bb '»'+ [0x00, 0x40, 0xc0, 0x40, 0x42, 0x46, 0xec, 0x18, 0x30, 0x70, 0xd4, 0x94, 0x1e, 0x04, 0x04, 0x00], // bc '¼'+ [0x00, 0x40, 0xc0, 0x40, 0x42, 0x46, 0xec, 0x18, 0x30, 0x6c, 0xd2, 0x82, 0x0c, 0x10, 0x1e, 0x00], // bd '½'+ [0x00, 0x60, 0x90, 0x20, 0x12, 0x96, 0x6c, 0x18, 0x30, 0x70, 0xd4, 0x94, 0x1e, 0x04, 0x04, 0x00], // be '¾'+ [0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x60, 0xc0, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // bf '¿'+ [0x30, 0x18, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c0 'À'+ [0x18, 0x30, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c1 'Á'+ [0x38, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c2 'Â'+ [0x32, 0x4c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c3 'Ã'+ [0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c4 'Ä'+ [0x38, 0x6c, 0x38, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // c5 'Å'+ [0x00, 0x00, 0x7e, 0xd8, 0xd8, 0xd8, 0xfe, 0xd8, 0xd8, 0xd8, 0xd8, 0xde, 0x00, 0x00, 0x00, 0x00], // c6 'Æ'+ [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x18, 0x18, 0x30, 0x00], // c7 'Ç'+ [0x30, 0x18, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // c8 'È'+ [0x18, 0x30, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // c9 'É'+ [0x38, 0x6c, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // ca 'Ê'+ [0x6c, 0x6c, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // cb 'Ë'+ [0x30, 0x18, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // cc 'Ì'+ [0x0c, 0x18, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // cd 'Í'+ [0x38, 0x6c, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // ce 'Î'+ [0x66, 0x66, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // cf 'Ï'+ [0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0xf6, 0x66, 0x66, 0x66, 0x66, 0x7c, 0x00, 0x00, 0x00, 0x00], // d0 'Ð'+ [0x32, 0x4c, 0x00, 0xc6, 0xe6, 0xe6, 0xd6, 0xd6, 0xce, 0xce, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // d1 'Ñ'+ [0x30, 0x18, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // d2 'Ò'+ [0x18, 0x30, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // d3 'Ó'+ [0x38, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // d4 'Ô'+ [0x32, 0x4c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // d5 'Õ'+ [0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // d6 'Ö'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00], // d7 '×'+ [0x00, 0x02, 0x7c, 0xc6, 0xce, 0xce, 0xd6, 0xd6, 0xe6, 0xe6, 0xc6, 0x7c, 0x80, 0x00, 0x00, 0x00], // d8 'Ø'+ [0x30, 0x18, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // d9 'Ù'+ [0x18, 0x30, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // da 'Ú'+ [0x38, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // db 'Û'+ [0x6c, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // dc 'Ü'+ [0x18, 0x30, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // dd 'Ý'+ [0x00, 0x00, 0x00, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0x00, 0x00], // de 'Þ'+ [0x00, 0x00, 0x78, 0xcc, 0xcc, 0xcc, 0xd8, 0xcc, 0xc6, 0xc6, 0xd6, 0xdc, 0x00, 0x00, 0x00, 0x00], // df 'ß'+ [0x00, 0x60, 0x30, 0x18, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e0 'à'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e1 'á'+ [0x00, 0x10, 0x38, 0x6c, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e2 'â'+ [0x00, 0x32, 0x7e, 0x4c, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e3 'ã'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e4 'ä'+ [0x00, 0x38, 0x6c, 0x38, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // e5 'å'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x16, 0x16, 0x7e, 0xd0, 0xd0, 0x6e, 0x00, 0x00, 0x00, 0x00], // e6 'æ'+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x18, 0x18, 0x30, 0x00], // e7 'ç'+ [0x00, 0x60, 0x30, 0x18, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // e8 'è'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // e9 'é'+ [0x00, 0x10, 0x38, 0x6c, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // ea 'ê'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // eb 'ë'+ [0x00, 0x30, 0x18, 0x0c, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // ec 'ì'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // ed 'í'+ [0x00, 0x18, 0x3c, 0x66, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // ee 'î'+ [0x00, 0x00, 0x66, 0x66, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // ef 'ï'+ [0x00, 0xd8, 0x70, 0xd8, 0x0c, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f0 'ð'+ [0x00, 0x32, 0x7e, 0x4c, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // f1 'ñ'+ [0x00, 0x60, 0x30, 0x18, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f2 'ò'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f3 'ó'+ [0x00, 0x10, 0x38, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f4 'ô'+ [0x00, 0x32, 0x7e, 0x4c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f5 'õ'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // f6 'ö'+ [0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x7e, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00], // f7 '÷'+ [0x00, 0x00, 0x00, 0x00, 0x02, 0x7c, 0xc6, 0xce, 0xd6, 0xe6, 0xc6, 0x7c, 0x80, 0x00, 0x00, 0x00], // f8 'ø'+ [0x00, 0x60, 0x30, 0x18, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // f9 'ù'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // fa 'ú'+ [0x00, 0x10, 0x38, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // fb 'û'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // fc 'ü'+ [0x00, 0x0c, 0x18, 0x30, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00], // fd 'ý'+ [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // fe 'þ'+ [0x00, 0x00, 0x6c, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00], // ff 'ÿ'+];
@@ -727,7 +727,7 @@ impl Volume { let attrs = match kind { InodeKind::File => ATTR_ARCHIVE, InodeKind::Directory => ATTR_DIRECTORY,- InodeKind::Symlink => return Err(Errno::EOPNOTSUPP), // exFAT has no symlinks+ InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // exFAT has neither }; let mut dir = self.resolve(dir_path)?; if !dir.is_dir() {
@@ -827,7 +827,7 @@ impl Volume { let dir = match kind { InodeKind::File => false, InodeKind::Directory => true,- InodeKind::Symlink => return Err(Errno::EOPNOTSUPP), // no symlink syscall yet+ InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // no symlink/mknod syscall yet }; let mut inode = self.alloc_inode(self.group_of(parent_ino), dir)?; let now = self.now();
@@ -28,6 +28,7 @@ pub const KIND_CONSOLE: u64 = 3; pub const KIND_PIPE: u64 = 4; pub const KIND_SYMLINK: u64 = 5; pub const KIND_SOCKET: u64 = 6;+pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ... impl Stat { pub fn of(inode: &dyn Inode) -> Stat {@@ -35,6 +36,7 @@ impl Stat { InodeKind::File => KIND_FILE, InodeKind::Directory => KIND_DIR, InodeKind::Symlink => KIND_SYMLINK,+ InodeKind::Device => KIND_DEVICE, }; Stat { kind, size: inode.size() as u64, mode: inode.mode() as u64 } }@@ -65,6 +67,10 @@ pub trait File: Send + Sync { fn getdents(&self, _buf: &mut [u8]) -> Result<usize, Errno> { Err(Errno::ENOTDIR) }+ /// The framebuffer ioctls need /dev/fb0 behind the fd.+ fn as_framebuffer(&self) -> Option<&crate::dev::Framebuffer> {+ None+ } } /// A file or directory opened through the VFS: inode + current position.@@ -149,6 +155,7 @@ impl File for InodeFile { InodeKind::Directory => 4, // DT_DIR InodeKind::File => 8, // DT_REG InodeKind::Symlink => 10, // DT_LNK+ InodeKind::Device => 2, // DT_CHR (a "character device") }; rec[19..19 + entry.name.len()].copy_from_slice(entry.name.as_bytes()); used += reclen;
@@ -47,6 +47,8 @@ pub enum InodeKind { Directory, /// A symbolic link: a small file holding a path ([`Inode::readlink`]). Symlink,+ /// A device file in /dev (opening it gives the driver's own `File`).+ Device, } /// One entry of a directory listing.@@ -102,6 +104,11 @@ pub trait Inode: Send + Sync { fn remove(&self, _name: &str) -> Result<(), Errno> { Err(Errno::ENOTDIR) }+ /// Device files: the `File` that `open` should return (None = an+ /// ordinary inode, opened as an `InodeFile`).+ fn open_device(&self) -> Option<Arc<dyn file::File>> {+ None+ } } /// A mounted file system.@@ -298,8 +305,10 @@ pub fn init() { motd.write_at(0, b"Welcome to Tyler's kernel. Next up: SMP, then networking.\n").unwrap(); dir.create("tmp", InodeKind::Directory).unwrap(); // mount point dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN+ dir.create("dev", InodeKind::Directory).unwrap(); // device files (dev.rs) mount("/", root).unwrap(); mount("/tmp", ramfs::RamFs::new()).unwrap();+ mount("/dev", Arc::new(crate::dev::DevFs)).unwrap(); } /// Try to mount every registered partition (and unpartitioned disk) at
@@ -86,7 +86,7 @@ impl Inode for RamInode { let node = Arc::new(match kind { InodeKind::File => RamInode::File(Mutex::new(Vec::new())), InodeKind::Directory => RamInode::Dir(Mutex::new(BTreeMap::new())),- InodeKind::Symlink => return Err(Errno::EOPNOTSUPP), // ramfs has no symlinks+ InodeKind::Symlink | InodeKind::Device => return Err(Errno::EOPNOTSUPP), // ramfs has neither }); entries.insert(String::from(name), node.clone()); Ok(node)
@@ -296,7 +296,9 @@ pub fn enable_pci_irq(irq: u8) { /// A PCI device interrupted. Only the network card uses interrupts (the /// disks are polled), so ask it; then tell the PIC we're done. fn pci_irq(irq: u8) {+ // Lines are shared: ask every driver whether it was its device. crate::net::handle_irq();+ crate::virtio::handle_irq(); unsafe { PICS.lock().notify_end_of_interrupt(PIC_1_OFFSET + irq); }
@@ -25,8 +25,11 @@ pub mod acpi; // ACPI tables: just enough to list the CPUs (MADT) pub mod allocator; // heap: maps heap pages + the #[global_allocator] pub mod apic; // local APIC: per-CPU timer, inter-processor interrupts pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions+pub mod dev; // /dev: null, random, hvc0, fb0 pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers+pub mod fbcon; // text console drawn on the framebuffer+pub mod font; // 8x16 bitmap font (Spleen) for fbcon pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard@@ -42,7 +45,8 @@ pub mod task; // cooperative multitasking: async tasks + executor pub mod time; // microsecond clock (TSC calibrated against the PIT) pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues) pub mod tty; // console: keyboard line discipline + fd 0/1/2-pub mod vga_buffer; // 80x25 VGA text mode screen+pub mod vga_buffer; // the console: VGA text mode, or fbcon on a GPU+pub mod virtio; // modern virtio transport + virtio-rng, -console, -gpu /// Set up everything interrupt-related. Order matters: /// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the@@ -82,6 +86,19 @@ pub fn init_storage() -> (alloc::vec::Vec<pci::PciDevice>, alloc::vec::Vec<alloc (devices, log) } +/// The stage-13 virtual devices: virtio-rng, virtio-console (hvc0) and+/// virtio-gpu. With a GPU the text console moves onto its framebuffer.+/// Call before `init_net` (smoltcp's random seed comes from the rng).+/// Returns boot log lines.+pub fn init_drivers() -> alloc::vec::Vec<alloc::string::String> {+ let mut log = virtio::init(&pci::scan());+ if let Some(fb) = virtio::gpu::framebuffer() {+ let (cols, rows) = vga_buffer::use_framebuffer(fb);+ log.push(alloc::format!("fbcon: text console on the framebuffer, {}x{} characters", cols, rows));+ }+ log+}+ /// Networking: find the virtio-net card, start smoltcp, DHCP and the netd /// thread. Needs the scheduler (call after `init_smp`). Returns the boot /// log line, or None without a network card.
@@ -92,6 +92,12 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { serial_println!("[vfs] mounted {} at {}", fs_type, path); } + // --- Virtual devices (virtio/): rng, second console, GPU framebuffer ---+ // Before the network: smoltcp gets its random seed from virtio-rng.+ for line in rustbelt::init_drivers() {+ kprintln!("[virtio] {}", line);+ }+ // --- Network (net/): virtio-net + smoltcp; DHCP runs in the background -- match rustbelt::init_net() { Some(line) => kprintln!("[net] {}", line),
@@ -428,3 +428,29 @@ pub fn unmap_kernel_page(page: Page) { } }) }++/// Make device registers at physical `phys..phys + len` (a PCI memory BAR)+/// reachable and return their virtual address. The bootloader's physical+/// memory map usually covers them already (it maps up to 4 GiB, holes+/// included); BARs placed above that get mapped here, page by page, with+/// caching off (a device register must be read from the device every time).+pub fn map_mmio(phys: u64, len: u64) -> VirtAddr {+ let virt = phys_to_virt(PhysAddr::new(phys));+ without_interrupts(|| {+ let _lock = KERNEL_TABLES.lock();+ let mut mapper = unsafe { mapper_for(kernel_l4_frame()) };+ let first = phys & !0xfff;+ for page_phys in (first..phys + len).step_by(4096) {+ let page: Page = Page::containing_address(phys_to_virt(PhysAddr::new(page_phys)));+ if let TranslateResult::Mapped { .. } = mapper.translate(page.start_address()) {+ continue;+ }+ let frame = PhysFrame::containing_address(PhysAddr::new(page_phys));+ let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_CACHE;+ if let Ok(flush) = unsafe { mapper.map_to(page, frame, flags, &mut GlobalFrameAllocator) } {+ flush.flush();+ }+ }+ });+ virt+}
@@ -89,15 +89,20 @@ pub fn init(devices: &[crate::pci::PciDevice]) -> Option<String> { let mac = device.mac; let mut config = Config::new(HardwareAddress::Ethernet(EthernetAddress(mac))); // Random seed: TCP's initial sequence numbers and DHCP transaction ids.- config.random_seed = unsafe { core::arch::x86_64::_rdtsc() };+ // From virtio-rng (rng.rs) -- guessable ones would let an attacker+ // inject packets into our connections.+ config.random_seed = crate::virtio::rng::u64(); let iface = Interface::new(config, &mut device, now()); let mut sockets = SocketSet::new(Vec::new()); let dhcp = sockets.add(dhcpv4::Socket::new()); let dns = sockets.add(dns::Socket::new(&[], vec![None, None, None, None])); let irq = device.irq;+ // Ephemeral (client) ports start at a random spot in 49152..65000, so+ // they're not predictable either.+ let first_port = 49152 + (crate::virtio::rng::u64() % (65000 - 49152)) as u16; IO_BASE.store(device.io_base, Ordering::Relaxed); without_interrupts(|| {- *NET.lock() = Some(Net { iface, device, sockets, dhcp, dns, config: None, closing: Vec::new(), next_port: 49152 });+ *NET.lock() = Some(Net { iface, device, sockets, dhcp, dns, config: None, closing: Vec::new(), next_port: first_port }); }); crate::interrupts::enable_pci_irq(irq); crate::thread::spawn("netd", netd);
@@ -71,6 +71,62 @@ impl PciDevice { write_config(self.bus, self.device, self.function, 0x04, command | 0b101); } + /// Same for devices whose registers are memory-mapped (MMIO): command+ /// bit 1 = memory space, bit 2 = bus master.+ pub fn enable_memory_and_bus_master(&self) {+ let command = self.read(0x04);+ write_config(self.bus, self.device, self.function, 0x04, command | 0b110);+ }++ /// Physical address of memory BAR `n`. Bits 1-2 of a memory BAR give its+ /// type: 0b10 = 64-bit, and then BAR n+1 holds the upper half. The low 4+ /// bits are flags, not address.+ pub fn bar_address(&self, n: u8) -> u64 {+ let low = self.bar(n);+ if low & 1 != 0 {+ return (low & !0x3) as u64; // an I/O port BAR+ }+ let mut addr = (low & !0xf) as u64;+ if (low >> 1) & 0b11 == 0b10 {+ addr |= (self.bar(n + 1) as u64) << 32;+ }+ addr+ }++ /// One byte of configuration space (any offset).+ pub fn read_u8(&self, offset: u8) -> u8 {+ (self.read(offset & 0xfc) >> ((offset & 3) * 8)) as u8+ }++ /// Read 32 bits at any 4-byte aligned config offset.+ pub fn read_u32(&self, offset: u8) -> u32 {+ self.read(offset)+ }++ /// The *capability list*: a linked list in config space that newer+ /// devices use to describe extra features (MSI, MSI-X, power+ /// management, and -- what we need -- where modern virtio devices put+ /// their register blocks). Status bit 4 says the list exists; register+ /// 0x34 points at the first entry; each entry is [id, next, data...].+ /// Returns the config-space offsets of the entries with id `cap_id`.+ pub fn capabilities(&self, cap_id: u8) -> Vec<u8> {+ let mut found = Vec::new();+ let status = (self.read(0x04) >> 16) as u16;+ if status & (1 << 4) == 0 {+ return found;+ }+ let mut offset = self.read_u8(0x34) & 0xfc;+ let mut guard = 0; // a broken list must not loop forever+ while offset != 0 && guard < 48 {+ if self.read_u8(offset) == cap_id {+ found.push(offset);+ }+ offset = self.read_u8(offset + 1) & 0xfc;+ guard += 1;+ }+ found+ }+ /// Human-readable class (just the common ones). pub fn class_name(&self) -> &'static str { match (self.class, self.subclass) {@@ -80,6 +136,8 @@ impl PciDevice { (0x01, _) => "storage controller", (0x02, _) => "network controller", (0x03, _) => "display controller",+ (0x00, _) => "unclassified",+ (0x07, _) => "communication controller", (0x06, 0x00) => "host bridge", (0x06, 0x01) => "ISA bridge", (0x06, 0x80) => "bridge (other)",
@@ -24,6 +24,8 @@ pub fn _print(args: core::fmt::Arguments) { x86_64::instructions::interrupts::without_interrupts(|| { SERIAL1.lock().write_fmt(args).expect("printing to serial failed"); });+ // And to the virtio console (hvc0), if there is one.+ x86_64::instructions::interrupts::without_interrupts(|| crate::virtio::console::log(args)); } /// Like `print!`, but to the serial port.
@@ -18,7 +18,7 @@ //! | 4 | stat | path, path_len, *Stat | our own small `Stat {kind, size, mode}`; follows symlinks | //! | 5 | fstat | fd, *Stat | | //! | 8 | lseek | fd, offset, whence | SEEK_SET/CUR/END = 0/1/2 |-//! | 16 | ioctl | fd, request, arg | only TTY_SET_RAW (ours): console raw/cooked mode |+//! | 16 | ioctl | fd, request, arg | console: TTY_SET_RAW (ours); /dev/fb0: FB_GET_INFO, FB_FLUSH, FB_FILL, FB_SET_CONSOLE (below) | //! | 22 | pipe | *[u32; 2] | [read end, write end] | //! | 24 | sched_yield | | | //! | 35 | sleep_ms | milliseconds | Linux's nr 35 is nanosleep(timespec); ours is simpler |@@ -42,6 +42,7 @@ //! | 54 | setsockopt | fd, level, name, *value, len | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval` | //! | 217 | getdents64 | fd, buf, len | Linux `struct linux_dirent64` records | //! | 228 | clock_gettime | clock, *timespec | CLOCK_REALTIME/MONOTONIC both = time since boot (we have no RTC driver) |+//! | 318 | getrandom | buf, len, flags | random bytes (virtio-rng); up to 64 KiB per call; flags ignored | //! | 500 | spawn | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) | //! | 501 | mounts | buf, len | ours: "path fstype\n" per mount (Linux: read /proc/mounts) | //! | 502 | sysinfo | *SysInfo | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) |@@ -53,6 +54,16 @@ //! the defaults) picks the child's stdin/stdout/stderr from the caller's fds //! (-1 = same as mine) and flags (SPAWN_TRACE = print its syscalls). //!+//! The framebuffer ioctls (on an fd of /dev/fb0; numbers in Linux's 'F'+//! range, but the structs are our own -- Linux's fb_var_screeninfo has 40+//! fields):+//!+//! FB_GET_INFO 0x4600 *FbInfo {width, height, stride_bytes, bpp} (u32s)+//! FB_FLUSH 0x46f0 *[x, y, w, h] or 0 = what `write` changed: show it+//! FB_FILL 0x46f1 *[x, y, w, h, 0x00RRGGBB]: fill (not flushed)+//! FB_SET_CONSOLE 0x46f2 *[x, y, w, h] or 0 = full screen: move the text+//! console there; returns columns << 16 | rows+//! //! What happens on `syscall` (the CPU does steps 1-3 in one instruction): //! 1. rcx = user RIP (next instruction), r11 = user RFLAGS //! 2. RFLAGS &= !SFMASK (we mask IF -> interrupts off)@@ -114,6 +125,7 @@ pub const SYS_BIND: u64 = 49; pub const SYS_LISTEN: u64 = 50; pub const SYS_SETSOCKOPT: u64 = 54; pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_GETRANDOM: u64 = 318; pub const SYS_CLOCK_GETTIME: u64 = 228; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501;@@ -135,6 +147,11 @@ pub const O_APPEND: u64 = 0o2000; /// ioctl request (ours): arg 1 = raw mode, 0 = cooked; returns the old mode. pub const TTY_SET_RAW: u64 = 1; pub const SPAWN_TRACE: u64 = 1;+/// Framebuffer ioctls (see the table at the top).+pub const FB_GET_INFO: u64 = 0x4600;+pub const FB_FLUSH: u64 = 0x46f0;+pub const FB_FILL: u64 = 0x46f1;+pub const FB_SET_CONSOLE: u64 = 0x46f2; /// Name of a syscall number (for tracing). pub fn name(nr: u64) -> &'static str {@@ -161,6 +178,7 @@ pub fn name(nr: u64) -> &'static str { SYS_UNLINK => "unlink", SYS_READLINK => "readlink", SYS_GETDENTS64 => "getdents64",+ SYS_GETRANDOM => "getrandom", SYS_SPAWN => "spawn", SYS_MOUNTS => "mounts", SYS_SYSINFO => "sysinfo",@@ -324,6 +342,7 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: SYS_CLOCK_GETTIME => sys_clock_gettime(a2), SYS_RESOLVE => sys_resolve(a1, a2, a3, a4), SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),+ SYS_GETRANDOM => sys_getrandom(a1, a2), _ => Err(Errno::ENOSYS), }; let ret = match result {@@ -436,12 +455,26 @@ fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> { Ok(()) } +/// Copy `N` u32s in from user memory (ioctl arguments).+fn read_user_u32s<const N: usize>(ptr: u64) -> Result<[u32; N], Errno> {+ let src = user_slice(ptr, 4 * N as u64)?;+ let mut out = [0u32; N];+ for (i, word) in out.iter_mut().enumerate() {+ *word = u32::from_le_bytes(src[4 * i..4 * i + 4].try_into().unwrap());+ }+ Ok(out)+}+ fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> { let path = user_path(path_ptr, path_len)?; let inode = match fs::lookup(&path) { Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(&path, InodeKind::File)?, other => other?, };+ // Device files (/dev/...) come with their own File.+ if let Some(device) = inode.open_device() {+ return current()?.add_file(device);+ } if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File { inode.truncate()?; }@@ -540,7 +573,11 @@ fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_pt fn sys_ioctl(fd: u64, request: u64, arg: u64) -> Result<u64, Errno> { let process = current()?;- if process.file(fd)?.stat()?.kind != crate::fs::file::KIND_CONSOLE {+ let file = process.file(fd)?;+ if let Some(fb) = file.as_framebuffer() {+ return fb_ioctl(fb, request, arg);+ }+ if file.stat()?.kind != crate::fs::file::KIND_CONSOLE { return Err(Errno::ENOTTY); } match request {@@ -549,13 +586,48 @@ fn sys_ioctl(fd: u64, request: u64, arg: u64) -> Result<u64, Errno> { } } +/// The /dev/fb0 ioctls (table at the top of this file).+fn fb_ioctl(fb: &crate::dev::Framebuffer, request: u64, arg: u64) -> Result<u64, Errno> {+ let info = fb.info;+ match request {+ FB_GET_INFO => {+ copy_to_user(arg, &[info.width, info.height, info.stride * 4, 32u32])?;+ Ok(0)+ }+ FB_FLUSH if arg == 0 => fb.flush().then_some(0).ok_or(Errno::EIO),+ FB_FLUSH => {+ let [x, y, w, h] = read_user_u32s::<4>(arg)?;+ crate::virtio::gpu::flush(x, y, w, h).then_some(0).ok_or(Errno::EIO)+ }+ FB_FILL => {+ let [x, y, w, h, color] = read_user_u32s::<5>(arg)?;+ fb.fill(x, y, w, h, color);+ Ok(0)+ }+ FB_SET_CONSOLE => {+ let [x, y, w, h] = if arg == 0 { [0; 4] } else { read_user_u32s::<4>(arg)? };+ let (cols, rows) = crate::vga_buffer::set_viewport(crate::fbcon::Rect { x, y, w, h }).ok_or(Errno::ENODEV)?;+ Ok((cols as u64) << 16 | rows as u64)+ }+ _ => Err(Errno::EINVAL),+ }+}++/// getrandom(buf, len, flags): Linux 318. We always have "enough entropy"+/// (the host's), so the flags (GRND_NONBLOCK, GRND_RANDOM) change nothing.+fn sys_getrandom(buf: u64, len: u64) -> Result<u64, Errno> {+ let buf = user_slice_mut(buf, len.min(65536))?;+ crate::virtio::rng::fill(buf);+ Ok(buf.len() as u64)+}+ /// Linux's `struct utsname`: six NUL-terminated strings of 65 bytes each. fn sys_uname(buf: u64) -> Result<u64, Errno> { let fields = [ "Rustbelt", // sysname (`uname -s`) "qemu", // nodename (hostname) env!("CARGO_PKG_VERSION"), // release (`uname -r`)- "stage 12: networking", // version+ "stage 13: virtual drivers", // version "x86_64", // machine "(none)", // domainname ];
@@ -4,7 +4,12 @@ //! physical address 0xb8000: byte 0 = ASCII char, byte 1 = colour attribute //! (low nibble foreground, high nibble background). //! The bootloader identity-maps that address, so we can write to it directly.+//!+//! Since stage 13 the same `Writer` can draw on the virtio-gpu framebuffer+//! instead (fbcon.rs): `use_framebuffer` switches over at boot when there+//! is a GPU; without one, this VGA text mode stays the console. +use crate::fbcon::{FbConsole, Rect}; use core::fmt; use lazy_static::lazy_static; use spin::Mutex;@@ -49,57 +54,96 @@ struct Buffer { } /// Always writes on the bottom row and scrolls everything up on newline.+///+/// Two possible screens behind it: the VGA text buffer (always there in+/// QEMU, and what the test kernels use), or -- once virtio-gpu is up and+/// `use_framebuffer` was called -- the framebuffer console (fbcon.rs),+/// which has more and smaller cells. Everything below goes through+/// `put`/`scroll`/`width`/`height`, so it works the same on both. pub struct Writer { column_position: usize, color_code: ColorCode, buffer: &'static mut Buffer,+ fb: Option<FbConsole>, } impl Writer {+ fn width(&self) -> usize {+ self.fb.as_ref().map_or(BUFFER_WIDTH, |f| f.cols())+ }++ fn height(&self) -> usize {+ self.fb.as_ref().map_or(BUFFER_HEIGHT, |f| f.rows())+ }++ /// Set one cell.+ fn put(&mut self, row: usize, col: usize, ascii_character: u8, color_code: ColorCode) {+ match &mut self.fb {+ Some(fb) => fb.put(row, col, ascii_character, color_code.0),+ None => self.buffer.chars[row][col].write(ScreenChar { ascii_character, color_code }),+ }+ }+ pub fn write_byte(&mut self, byte: u8) { match byte { b'\n' => self.new_line(), byte => {- if self.column_position >= BUFFER_WIDTH {+ if self.column_position >= self.width() { self.new_line(); }- let row = BUFFER_HEIGHT - 1;+ let row = self.height() - 1; let col = self.column_position;- self.buffer.chars[row][col].write(ScreenChar {- ascii_character: byte,- color_code: self.color_code,- });+ self.put(row, col, byte, self.color_code); self.column_position += 1; } } } pub fn write_string(&mut self, s: &str) {- for byte in s.bytes() {- match byte {- // printable ASCII or newline- 0x20..=0x7e | b'\n' => self.write_byte(byte),+ for c in s.chars() {+ let byte = match (c, self.fb.is_some()) {+ ('\n', _) => b'\n',+ // printable ASCII is the same everywhere+ (' '..='~', _) => c as u8,+ // the framebuffer font has Latin-1 and a few more+ (c, true) => crate::font::glyph_index(c), // VGA only knows code page 437; show anything else as a block- _ => self.write_byte(0xfe),- }+ (_, false) => 0xfe,+ };+ self.write_byte(byte); } } fn new_line(&mut self) {- for row in 1..BUFFER_HEIGHT {- for col in 0..BUFFER_WIDTH {- let c = self.buffer.chars[row][col].read();- self.buffer.chars[row - 1][col].write(c);+ match &mut self.fb {+ Some(fb) => fb.scroll_up(self.color_code.0),+ None => {+ for row in 1..BUFFER_HEIGHT {+ for col in 0..BUFFER_WIDTH {+ let c = self.buffer.chars[row][col].read();+ self.buffer.chars[row - 1][col].write(c);+ }+ }+ self.clear_row(BUFFER_HEIGHT - 1); } }- self.clear_row(BUFFER_HEIGHT - 1); self.column_position = 0; } fn clear_row(&mut self, row: usize) {- let blank = ScreenChar { ascii_character: b' ', color_code: self.color_code };- for col in 0..BUFFER_WIDTH {- self.buffer.chars[row][col].write(blank);+ for col in 0..self.width() {+ self.put(row, col, b' ', self.color_code);+ }+ }++ /// After a print: framebuffer only -- draw the cursor and send the+ /// changed pixels to the screen. (VGA needs nothing: the card reads its+ /// buffer itself, 70 times a second.)+ fn finish(&mut self) {+ let (row, col) = (self.height() - 1, self.column_position);+ if let Some(fb) = &mut self.fb {+ fb.set_cursor(Some((row, col)));+ fb.flush(); } } }@@ -119,9 +163,69 @@ lazy_static! { column_position: 0, color_code: ColorCode::new(Color::LightGreen, Color::Black), buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },+ fb: None, }); } +/// Switch the console to the framebuffer (called once virtio-gpu is up).+/// The text already on the VGA screen comes along, so the boot messages+/// stay visible. Returns the new size in characters.+pub fn use_framebuffer(fb: crate::virtio::gpu::FbInfo) -> (usize, usize) {+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ let mut con = FbConsole::new(fb);+ let (cols, rows) = (con.cols(), con.rows());+ // VGA's 25 rows go to the bottom 25 rows of the new screen.+ for row in 0..BUFFER_HEIGHT.min(rows) {+ for col in 0..BUFFER_WIDTH.min(cols) {+ let c = writer.buffer.chars[row][col].read();+ con.put(rows - BUFFER_HEIGHT.min(rows) + row, col, c.ascii_character, c.color_code.0);+ }+ }+ writer.fb = Some(con);+ writer.finish();+ (cols, rows)+ })+}++/// Is the console on the framebuffer?+pub fn on_framebuffer() -> bool {+ x86_64::instructions::interrupts::without_interrupts(|| WRITER.lock().fb.is_some())+}++/// Confine the framebuffer console to a rectangle of the screen (the+/// `tile` demo puts it in one tile); a zero-sized rect = the whole screen.+/// Returns the new (columns, rows), or None on the VGA console.+pub fn set_viewport(mut view: Rect) -> Option<(usize, usize)> {+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ let attr = writer.color_code.0;+ let fb = writer.fb.as_mut()?;+ if view.w == 0 || view.h == 0 {+ let info = crate::virtio::gpu::framebuffer()?;+ view = Rect { x: 0, y: 0, w: info.width, h: info.height };+ }+ fb.set_view(view, attr);+ let size = (fb.cols(), fb.rows());+ writer.column_position = writer.column_position.min(size.0);+ writer.finish();+ Some(size)+ })+}++/// The console's current rectangle in pixels (framebuffer only).+pub fn viewport() -> Option<Rect> {+ x86_64::instructions::interrupts::without_interrupts(|| WRITER.lock().fb.as_ref().map(|f| f.view()))+}++/// Text size of the console in characters (columns, rows).+pub fn size() -> (usize, usize) {+ x86_64::instructions::interrupts::without_interrupts(|| {+ let writer = WRITER.lock();+ (writer.width(), writer.height())+ })+}+ /// Write formatted text at a fixed screen position (row, col) in black on /// light grey, without moving the normal cursor. Used for status lines. /// Text that runs past the end of the row is cut off.@@ -136,10 +240,8 @@ pub fn print_at(row: usize, col: usize, args: fmt::Arguments) { fn write_str(&mut self, s: &str) -> fmt::Result { let color_code = ColorCode::new(Color::Black, Color::LightGray); for byte in s.bytes() {- if self.col < BUFFER_WIDTH {- let ascii_character = byte;- self.writer.buffer.chars[self.row][self.col]- .write(ScreenChar { ascii_character, color_code });+ if self.col < self.writer.width() && self.row < self.writer.height() {+ self.writer.put(self.row, self.col, byte, color_code); self.col += 1; } }@@ -150,13 +252,17 @@ pub fn print_at(row: usize, col: usize, args: fmt::Arguments) { x86_64::instructions::interrupts::without_interrupts(|| { let mut writer = WRITER.lock(); let _ = At { writer: &mut writer, row, col }.write_fmt(args);+ if let Some(fb) = &mut writer.fb {+ fb.flush();+ } }); } /// Draw " uptime: N s " right-aligned on the top row. Called from the timer /// interrupt handler about once a second. pub fn show_uptime(seconds: u64) {- print_at(0, BUFFER_WIDTH - 15, format_args!(" uptime: {:>4}s ", seconds));+ let width = size().0;+ print_at(0, width.saturating_sub(15), format_args!(" uptime: {:>4}s ", seconds)); } /// Erase the character before the cursor on the bottom row (Backspace).@@ -166,10 +272,11 @@ pub fn backspace() { let mut writer = WRITER.lock(); if writer.column_position > 0 { writer.column_position -= 1;- let col = writer.column_position;- let blank = ScreenChar { ascii_character: b' ', color_code: writer.color_code };- writer.buffer.chars[BUFFER_HEIGHT - 1][col].write(blank);+ let (row, col) = (writer.height() - 1, writer.column_position);+ let color_code = writer.color_code;+ writer.put(row, col, b' ', color_code); }+ writer.finish(); }); } @@ -178,10 +285,11 @@ pub fn backspace() { pub fn clear_screen() { x86_64::instructions::interrupts::without_interrupts(|| { let mut writer = WRITER.lock();- for row in 1..BUFFER_HEIGHT {+ for row in 1..writer.height() { writer.clear_row(row); } writer.column_position = 0;+ writer.finish(); }); } @@ -192,7 +300,9 @@ pub fn _print(args: fmt::Arguments) { // handler that also prints would spin forever waiting for a lock held by // the code it interrupted (a deadlock). x86_64::instructions::interrupts::without_interrupts(|| {- WRITER.lock().write_fmt(args).unwrap();+ let mut writer = WRITER.lock();+ writer.write_fmt(args).unwrap();+ writer.finish(); }); }
@@ -0,0 +1,245 @@+//! virtio-console: a second terminal, `hvc0` ("hypervisor console", the+//! name Linux gives it). QEMU connects it to anything a *chardev* can be:+//!+//! -device virtio-serial-pci -device virtconsole,chardev=hvc+//! -chardev socket,id=hvc,path=target/hvc0.sock,server=on,wait=off+//! (then on the host: socat - UNIX-CONNECT:target/hvc0.sock)+//! -chardev file,id=hvc,path=out.txt,input-path=in.txt (tests)+//!+//! Two queues: 0 = receive (host -> us), 1 = transmit (us -> host). (With+//! the MULTIPORT feature there'd be more ports and a control queue; we+//! don't ask for it, so it's exactly one port, which QEMU opens for us.)+//!+//! * **Receiving**: we keep 8 empty 512-byte buffers in the receive queue.+//! When the host sends bytes the device fills one, puts it on the used+//! ring and raises an interrupt; we copy the bytes into INPUT and put the+//! buffer straight back. Readers of /dev/hvc0 wait on INPUT.+//! * **Sending**: 8 buffers too. A write copies into a free one and kicks+//! the device. If all 8 are still in flight (host not reading), we wait a+//! few milliseconds and then *drop* the output: kernel logging must never+//! hang because nobody is listening.+//!+//! Every kernel log line (`serial_print!`) also goes here -- except while+//! a program has /dev/hvc0 open (e.g. a shell runs on it via `getty`), so+//! that session isn't interleaved with log noise.++use super::{Transport, Virtqueue};+use crate::memory;+use crate::pci::PciDevice;+use crate::thread::WaitQueue;+use alloc::{format, string::String, vec::Vec};+use conquer_once::spin::OnceCell;+use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};+use crossbeam_queue::ArrayQueue;+use spin::Mutex;+use x86_64::{PhysAddr, instructions::interrupts::without_interrupts};++const SLOTS: u16 = 8; // buffers per direction+const SLOT_SIZE: usize = 512; // 8 x 512 = one page per direction++struct Hvc {+ transport: Transport,+ rx: Virtqueue,+ tx: Virtqueue,+ rx_phys: u64,+ tx_phys: u64,+}++static HVC: Mutex<Option<Hvc>> = Mutex::new(None);+static PRESENT: AtomicBool = AtomicBool::new(false);+/// Bytes received from the host, not read yet.+static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();+static INPUT_READY: WaitQueue = WaitQueue::new();+/// How many open /dev/hvc0 files there are (kernel log mirroring pauses+/// while there are any).+pub static OPEN_COUNT: AtomicUsize = AtomicUsize::new(0);+/// Bytes we had to drop because the host wasn't reading.+pub static DROPPED: AtomicUsize = AtomicUsize::new(0);++fn input() -> &'static ArrayQueue<u8> {+ INPUT.get_or_init(|| ArrayQueue::new(4096))+}++/// virtio-serial: transitional id 0x1003, modern 0x1043.+pub fn is_virtio_console(d: &PciDevice) -> bool {+ d.vendor_id == super::VENDOR && matches!(d.device_id, 0x1003 | 0x1043)+}++pub fn present() -> bool {+ PRESENT.load(Ordering::Relaxed)+}++fn slot_ptr(base_phys: u64, slot: u16) -> *mut u8 {+ memory::phys_to_virt(PhysAddr::new(base_phys + slot as u64 * SLOT_SIZE as u64)).as_mut_ptr()+}++pub fn init(devices: &[PciDevice]) -> Option<String> {+ let pci = devices.iter().find(|d| is_virtio_console(d))?;+ let setup = || -> Option<Hvc> {+ let transport = Transport::new(pci)?;+ transport.begin_init(0)?;+ // Receive interrupts on; transmit is polled (we reap finished+ // buffers whenever we write).+ let mut rx = transport.setup_queue(0, SLOTS, true)?;+ let tx = transport.setup_queue(1, SLOTS, false)?;+ let rx_phys = memory::alloc_contiguous_frames(1)?.as_u64();+ let tx_phys = memory::alloc_contiguous_frames(1)?.as_u64();+ while let Some(slot) = rx.next_free() {+ rx.push(&[(rx_phys + slot as u64 * SLOT_SIZE as u64, SLOT_SIZE as u32, true)])?;+ }+ transport.driver_ok();+ rx.notify();+ Some(Hvc { transport, rx, tx, rx_phys, tx_phys })+ };+ let Some(hvc) = setup() else {+ return Some(String::from("hvc0: virtio-console found but setup failed"));+ };+ input();+ without_interrupts(|| *HVC.lock() = Some(hvc));+ PRESENT.store(true, Ordering::Relaxed);+ let irq = pci.interrupt_line();+ crate::interrupts::enable_pci_irq(irq);+ Some(format!("hvc0: virtio-console at {:02x}:{:02x}.{}, IRQ {}", pci.bus, pci.device, pci.function, irq))+}++/// Move received bytes from the used ring into INPUT and recycle the+/// buffers. Called from the interrupt handler, and by readers (in case the+/// interrupt line isn't one we handle).+fn poll_rx(hvc: &mut Hvc) -> bool {+ let mut got = false;+ while let Some((slot, len)) = hvc.rx.pop_used() {+ let data = unsafe { core::slice::from_raw_parts(slot_ptr(hvc.rx_phys, slot), (len as usize).min(SLOT_SIZE)) };+ for &b in data {+ let _ = input().push(b); // full: drop (4 KiB nobody reads)+ }+ got |= len > 0;+ let _ = hvc.rx.push(&[(hvc.rx_phys + slot as u64 * SLOT_SIZE as u64, SLOT_SIZE as u32, true)]);+ }+ if got {+ hvc.rx.notify();+ }+ got+}++pub fn handle_irq() {+ let mut got = false;+ if let Some(mut guard) = HVC.try_lock() {+ if let Some(hvc) = guard.as_mut() {+ hvc.transport.read_isr();+ got = poll_rx(hvc);+ }+ }+ if got {+ INPUT_READY.wake_all();+ }+}++/// Send bytes to the host. Never blocks for long: drops what doesn't fit+/// within ~10 ms. Returns how many bytes were sent.+pub fn write(bytes: &[u8]) -> usize {+ if !present() {+ return 0;+ }+ without_interrupts(|| {+ let mut guard = HVC.lock();+ let Some(hvc) = guard.as_mut() else { return 0 };+ let mut sent = 0;+ for chunk in bytes.chunks(SLOT_SIZE) {+ // Reap finished sends; wait a little if none is free.+ let deadline = crate::time::now_us() + 10_000;+ let slot = loop {+ while hvc.tx.pop_used().is_some() {}+ if let Some(slot) = hvc.tx.next_free() {+ break Some(slot);+ }+ if crate::time::now_us() > deadline {+ break None;+ }+ core::hint::spin_loop();+ };+ let Some(slot) = slot else {+ DROPPED.fetch_add(bytes.len() - sent, Ordering::Relaxed);+ break;+ };+ unsafe { core::ptr::copy_nonoverlapping(chunk.as_ptr(), slot_ptr(hvc.tx_phys, slot), chunk.len()) };+ hvc.tx.push(&[(hvc.tx_phys + slot as u64 * SLOT_SIZE as u64, chunk.len() as u32, false)]);+ hvc.tx.notify();+ sent += chunk.len();+ }+ sent+ })+}++/// Kernel log mirror (called by serial.rs for every `serial_print!`).+pub fn log(args: core::fmt::Arguments) {+ if !present() || OPEN_COUNT.load(Ordering::Relaxed) > 0 {+ return;+ }+ /// Collect the formatted text in a small stack buffer (no heap needed).+ struct Buf {+ data: [u8; 256],+ len: usize,+ }+ impl core::fmt::Write for Buf {+ fn write_str(&mut self, s: &str) -> core::fmt::Result {+ for &b in s.as_bytes() {+ if self.len == self.data.len() {+ write(&self.data);+ self.len = 0;+ }+ self.data[self.len] = b;+ self.len += 1;+ }+ Ok(())+ }+ }+ let mut buf = Buf { data: [0; 256], len: 0 };+ let _ = core::fmt::Write::write_fmt(&mut buf, args);+ write(&buf.data[..buf.len]);+}++/// Read received bytes into `buf`; blocks until at least one is there.+pub fn read(buf: &mut [u8]) -> usize {+ if buf.is_empty() {+ return 0;+ }+ loop {+ let take = |buf: &mut [u8]| {+ let mut n = 0;+ while n < buf.len() {+ match input().pop() {+ Some(b) => { buf[n] = b; n += 1; }+ None => break,+ }+ }+ n+ };+ without_interrupts(|| {+ if let Some(hvc) = HVC.lock().as_mut() {+ poll_rx(hvc);+ }+ });+ let n = take(buf);+ if n > 0 {+ return n;+ }+ // Sleep until the interrupt handler has data (or 1 tick, then poll).+ let deadline = crate::interrupts::ticks() + 1;+ INPUT_READY.wait_until_deadline(deadline, || (!input().is_empty()).then_some(()));+ }+}++/// Bytes waiting in the input buffer (tests).+pub fn pending_input() -> usize {+ input().len()+}++/// Read all buffered input without blocking (tests).+pub fn drain_input() -> Vec<u8> {+ without_interrupts(|| {+ if let Some(hvc) = HVC.lock().as_mut() {+ poll_rx(hvc);+ }+ });+ core::iter::from_fn(|| input().pop()).collect()+}
@@ -0,0 +1,247 @@+//! virtio-gpu, 2D only: a framebuffer the host shows on its screen.+//! `-vga none -device virtio-vga` (virtio-gpu plus the old VGA modes, so+//! the BIOS and our VGA text console keep working until we switch over).+//!+//! The GPU doesn't scan out of our memory directly. The protocol has+//! *resources* (images living on the host) and *backing* (guest pages+//! holding the pixels). Setting up a framebuffer, every step one command on+//! the control queue:+//!+//! 1. GET_DISPLAY_INFO which outputs ("scanouts") exist, what size+//! 2. RESOURCE_CREATE_2D a 1024x768 image, format B8G8R8X8, id 1+//! 3. RESOURCE_ATTACH_BACKING its pixels live in these guest pages+//! 4. SET_SCANOUT show resource 1 on scanout 0+//!+//! Then, whenever we've drawn something:+//!+//! 5. TRANSFER_TO_HOST_2D copy a rectangle from our pages to the host+//! 6. RESOURCE_FLUSH and update the screen there+//!+//! Each command is a request buffer (a 24-byte header + arguments) and a+//! response buffer the device writes (a header whose type says OK or which+//! error). We wait for each answer by polling -- it's a function call into+//! QEMU, microseconds.+//!+//! 3D (virgl) would add contexts, 3D resources and a command stream of+//! Gallium/OpenGL commands that Mesa in the guest produces and+//! virglrenderer on the host executes. That's a lot of protocol; 2D is the+//! foundation it builds on (3D still ends in SET_SCANOUT + RESOURCE_FLUSH).++use super::{Transport, Virtqueue};+use crate::memory;+use crate::pci::PciDevice;+use alloc::{format, string::String, vec::Vec};+use conquer_once::spin::OnceCell;+use spin::Mutex;+use x86_64::{PhysAddr, instructions::interrupts::without_interrupts};++/// The framebuffer size we ask for (the device's preferred size is only+/// logged: fixed is predictable for tests and screenshots).+pub const WIDTH: u32 = 1024;+pub const HEIGHT: u32 = 768;++// Command types (virtio spec 5.7.6.7) and response types.+pub const CMD_GET_DISPLAY_INFO: u32 = 0x0100;+pub const CMD_RESOURCE_CREATE_2D: u32 = 0x0101;+pub const CMD_SET_SCANOUT: u32 = 0x0103;+pub const CMD_RESOURCE_FLUSH: u32 = 0x0104;+pub const CMD_TRANSFER_TO_HOST_2D: u32 = 0x0105;+pub const CMD_RESOURCE_ATTACH_BACKING: u32 = 0x0106;+pub const RESP_OK_NODATA: u32 = 0x1100;+pub const RESP_OK_DISPLAY_INFO: u32 = 0x1101;+pub const RESP_ERR_INVALID_RESOURCE_ID: u32 = 0x1203;+/// Pixel format: bytes B, G, R, unused -- i.e. a little-endian u32 is+/// 0x00RRGGBB, the usual way to write colours.+const FORMAT_B8G8R8X8: u32 = 2;+const RESOURCE_ID: u32 = 1;++/// Where the pixels are. Pixel (x, y) is the u32 at `base + y * stride + x`.+#[derive(Clone, Copy, Debug)]+pub struct FbInfo {+ pub base: u64, // kernel virtual address+ pub phys: u64,+ pub width: u32,+ pub height: u32,+ pub stride: u32, // in pixels+}++impl FbInfo {+ pub fn pixels(&self) -> *mut u32 {+ self.base as *mut u32+ }+ pub fn size_bytes(&self) -> usize {+ (self.stride * self.height * 4) as usize+ }+}++struct Gpu {+ transport: Transport,+ control: Virtqueue,+ cmd_phys: u64, // one page: requests at 0 and 512, responses at 2048 and 2560+}++static GPU: Mutex<Option<Gpu>> = Mutex::new(None);+static FB: OnceCell<FbInfo> = OnceCell::uninit();+/// The host's preferred display size, as reported by GET_DISPLAY_INFO.+static PREFERRED: Mutex<(u32, u32)> = Mutex::new((0, 0));++/// virtio-gpu: modern-only, id 0x1040 + 16. (virtio-vga is the same id+/// with the VGA display class.)+pub fn is_virtio_gpu(d: &PciDevice) -> bool {+ d.vendor_id == super::VENDOR && d.device_id == 0x1050+}++/// The framebuffer, once the GPU is set up.+pub fn framebuffer() -> Option<FbInfo> {+ FB.get().copied()+}++impl Gpu {+ /// Copy `words` into request slot `slot`, as a command with a zeroed+ /// header (flags, fence, context) after the type.+ fn put_request(&self, slot: u64, cmd: u32, args: &[u32]) -> u32 {+ let base: *mut u32 = memory::phys_to_virt(PhysAddr::new(self.cmd_phys + 512 * slot)).as_mut_ptr();+ let header = [cmd, 0, 0, 0, 0, 0]; // type, flags, fence_id (u64), ctx_id, ring_idx+padding+ for (i, w) in header.iter().chain(args).enumerate() {+ unsafe { base.add(i).write_volatile(*w) };+ }+ ((header.len() + args.len()) * 4) as u32+ }++ fn response(&self, slot: u64) -> *const u32 {+ memory::phys_to_virt(PhysAddr::new(self.cmd_phys + 2048 + 512 * slot)).as_ptr()+ }++ /// Send commands (at most 2) and wait for all answers. Returns the+ /// response type of each.+ fn run(&mut self, cmds: &[(u32, &[u32])], resp_len: u32) -> [u32; 2] {+ let mut types = [0u32; 2];+ for (slot, (cmd, args)) in cmds.iter().enumerate().take(2) {+ let len = self.put_request(slot as u64, *cmd, args);+ unsafe { (self.response(slot as u64) as *mut u32).write_volatile(0) };+ let req = self.cmd_phys + 512 * slot as u64;+ let resp = self.cmd_phys + 2048 + 512 * slot as u64;+ if self.control.push(&[(req, len, false), (resp, resp_len, true)]).is_none() {+ return types;+ }+ }+ self.control.notify();+ for _ in 0..cmds.len().min(2) {+ if self.control.wait_used(1_000_000).is_none() {+ return types; // a dead device: 0 = no answer+ }+ }+ for slot in 0..cmds.len().min(2) {+ types[slot] = unsafe { self.response(slot as u64).read_volatile() };+ }+ types+ }+}++/// Send one raw command and return the response type (0 = no answer).+/// Exposed for the tests, which check that errors come back as errors.+pub fn command(cmd: u32, args: &[u32]) -> u32 {+ without_interrupts(|| match GPU.lock().as_mut() {+ Some(gpu) => gpu.run(&[(cmd, args)], 24)[0],+ None => 0,+ })+}++/// Push the rectangle (x, y, w, h) of the framebuffer to the screen:+/// TRANSFER_TO_HOST_2D + RESOURCE_FLUSH, both queued before we wait.+/// Clipped to the framebuffer; returns false if the GPU said no.+pub fn flush(x: u32, y: u32, w: u32, h: u32) -> bool {+ let Some(fb) = framebuffer() else { return false };+ let (x, y) = (x.min(fb.width), y.min(fb.height));+ let (w, h) = (w.min(fb.width - x), h.min(fb.height - y));+ if w == 0 || h == 0 {+ return true;+ }+ let offset = (y as u64 * fb.stride as u64 + x as u64) * 4; // where the rect starts in the backing+ without_interrupts(|| {+ let mut guard = GPU.lock();+ let Some(gpu) = guard.as_mut() else { return false };+ let transfer = [x, y, w, h, offset as u32, (offset >> 32) as u32, RESOURCE_ID, 0];+ let flush = [x, y, w, h, RESOURCE_ID, 0];+ let r = gpu.run(&[(CMD_TRANSFER_TO_HOST_2D, &transfer), (CMD_RESOURCE_FLUSH, &flush)], 24);+ r == [RESP_OK_NODATA, RESP_OK_NODATA]+ })+}++/// The host's preferred display size (from GET_DISPLAY_INFO).+pub fn preferred_size() -> (u32, u32) {+ *PREFERRED.lock()+}++pub fn init(devices: &[PciDevice]) -> Option<String> {+ let pci = devices.iter().find(|d| is_virtio_gpu(d))?;+ match setup(pci) {+ Ok(line) => Some(line),+ Err(why) => Some(format!("gpu: virtio-gpu found but {}", why)),+ }+}++fn setup(pci: &PciDevice) -> Result<String, String> {+ let transport = Transport::new(pci).ok_or("has no modern virtio registers")?;+ transport.begin_init(0).ok_or("feature negotiation failed")?; // no VIRGL, no EDID: plain 2D+ let control = transport.setup_queue(0, 16, false).ok_or("has no control queue")?;+ transport.driver_ok();+ let cmd_phys = memory::alloc_contiguous_frames(1).ok_or("out of memory")?.as_u64();+ let mut gpu = Gpu { transport, control, cmd_phys };++ // 1. Display info: 16 scanouts x {x, y, width, height, enabled, flags}.+ let [t, _] = gpu.run(&[(CMD_GET_DISPLAY_INFO, &[])], 24 + 16 * 24);+ if t != RESP_OK_DISPLAY_INFO {+ return Err(format!("GET_DISPLAY_INFO answered {:#x}", t));+ }+ let info = gpu.response(0);+ let (pw, ph, enabled) = unsafe { (info.add(6 + 2).read(), info.add(6 + 3).read(), info.add(6 + 4).read()) };+ *PREFERRED.lock() = (pw, ph);++ // The pixels: 1024 x 768 x 4 bytes = 3 MiB of contiguous frames, so one+ // backing entry covers it. (Pages anywhere would do, with one entry per+ // page; contiguous is just less bookkeeping.)+ let bytes = WIDTH * HEIGHT * 4;+ let phys = memory::alloc_contiguous_frames((bytes / 4096) as usize).ok_or("out of memory for the framebuffer")?.as_u64();++ // 2-4. Create the resource, give it its pages, show it.+ let steps: [(u32, Vec<u32>); 3] = [+ (CMD_RESOURCE_CREATE_2D, alloc::vec![RESOURCE_ID, FORMAT_B8G8R8X8, WIDTH, HEIGHT]),+ // resource, number of entries, then {addr u64, length u32, padding}+ (CMD_RESOURCE_ATTACH_BACKING, alloc::vec![RESOURCE_ID, 1, phys as u32, (phys >> 32) as u32, bytes, 0]),+ // rect {x, y, w, h}, scanout id, resource id+ (CMD_SET_SCANOUT, alloc::vec![0, 0, WIDTH, HEIGHT, 0, RESOURCE_ID]),+ ];+ for (cmd, args) in &steps {+ let [t, _] = gpu.run(&[(*cmd, args)], 24);+ if t != RESP_OK_NODATA {+ return Err(format!("command {:#x} answered {:#x}", cmd, t));+ }+ }+ let fb = FbInfo {+ base: memory::phys_to_virt(PhysAddr::new(phys)).as_u64(),+ phys,+ width: WIDTH,+ height: HEIGHT,+ stride: WIDTH,+ };+ without_interrupts(|| *GPU.lock() = Some(gpu));+ FB.init_once(|| fb);+ flush(0, 0, WIDTH, HEIGHT); // show the (black) picture+ Ok(format!(+ "gpu: virtio-gpu 2D at {:02x}:{:02x}.{}, display {}x{}{}, framebuffer {}x{} ({} KiB)",+ pci.bus, pci.device, pci.function, pw, ph,+ if enabled != 0 { "" } else { " (disabled)" },+ WIDTH, HEIGHT, bytes / 1024+ ))+}++/// Acknowledge interrupts (e.g. "display config changed" when the QEMU+/// window is resized); we poll, so nothing else to do.+pub fn handle_irq() {+ if let Some(guard) = GPU.try_lock() {+ if let Some(gpu) = guard.as_ref() {+ gpu.transport.read_isr();+ }+ }+}
@@ -0,0 +1,230 @@+//! Virtio: the "virtual hardware" standard QEMU, KVM, cloud VMs and+//! Firecracker all speak. (Stage 13; virtio-blk and virtio-net from stages+//! 6 and 12 predate this module and talk the older *legacy* interface on+//! their own -- see block/virtio_blk.rs.)+//!+//! A virtio device is a PCI card that doesn't pretend to be real hardware.+//! Instead of emulating, say, a 1990s graphics chip register by register,+//! guest and host agree on a simple shared-memory protocol:+//!+//! * **Feature bits.** The device offers features, the driver accepts the+//! subset it understands. (We always accept VERSION_1, bit 32: "I speak+//! the modern, little-endian, spec-1.0 interface".)+//! * **Virtqueues** (queue.rs): ring buffers in guest RAM. The driver puts+//! buffer addresses into a queue and *notifies* the device (one register+//! write); the device does the work and puts the buffer back on the+//! "used" ring. A request is just bytes in a buffer whose format each+//! device type defines (a GPU command, a block read, random bytes...).+//! * **Config space**: a few device-specific registers (the console's size,+//! the GPU's number of scanouts...).+//!+//! **Legacy vs modern.** Old ("legacy", virtio 0.9.5) devices put all that in+//! one I/O-port BAR with fixed offsets -- simple, and what virtio_blk.rs and+//! virtio_net.rs use. Modern (virtio 1.0+) devices put several register+//! blocks into memory BARs and describe *where* with PCI capabilities+//! (vendor-specific, id 0x09). QEMU's rng, console and net cards are+//! "transitional" (they speak both); virtio-gpu is modern-only, so this+//! module does the modern way, and the three stage-13 drivers share it:+//!+//! rng.rs virtio-rng: entropy for getrandom(), /dev/random, smoltcp+//! console.rs virtio-console: a second terminal, /dev/hvc0+//! gpu.rs virtio-gpu (2D): a framebuffer, /dev/fb0, the fb console+//!+//! Spec: https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html++pub mod console;+pub mod gpu;+pub mod queue;+pub mod rng;++use crate::pci::PciDevice;+use core::ptr::{read_volatile, write_volatile};+pub use queue::Virtqueue;++/// Virtio's PCI vendor id (Red Hat's). Modern device ids are 0x1040 + type+/// (gpu = type 16 -> 0x1050); transitional ones are 0x1000.. (rng 0x1005,+/// console 0x1003).+pub const VENDOR: u16 = 0x1af4;++// Device status bits: the driver walks through these while starting up.+const STATUS_ACKNOWLEDGE: u8 = 1; // "I noticed you"+const STATUS_DRIVER: u8 = 2; // "and I have a driver for you"+const STATUS_DRIVER_OK: u8 = 4; // "set up, go"+const STATUS_FEATURES_OK: u8 = 8; // "I accept these features"++/// Feature bit 32: the modern interface. Required for modern-only devices.+const F_VERSION_1: u64 = 1 << 32;++// Capability types (the `cfg_type` byte of a virtio PCI capability).+const CAP_COMMON: u8 = 1; // the common config: features, status, queues+const CAP_NOTIFY: u8 = 2; // doorbells: write a queue number to kick it+const CAP_ISR: u8 = 3; // interrupt status (reading it acknowledges)+const CAP_DEVICE: u8 = 4; // device-specific config++// Offsets inside the common config block (virtio spec 4.1.4.3).+const DEVICE_FEATURE_SELECT: u64 = 0x00;+const DEVICE_FEATURE: u64 = 0x04;+const DRIVER_FEATURE_SELECT: u64 = 0x08;+const DRIVER_FEATURE: u64 = 0x0c;+const DEVICE_STATUS: u64 = 0x14;+const QUEUE_SELECT: u64 = 0x16;+const QUEUE_SIZE: u64 = 0x18;+const QUEUE_ENABLE: u64 = 0x1c;+const QUEUE_NOTIFY_OFF: u64 = 0x1e;+const QUEUE_DESC: u64 = 0x20;+const QUEUE_DRIVER: u64 = 0x28; // the "available" ring+const QUEUE_DEVICE: u64 = 0x30; // the "used" ring++/// A modern virtio PCI device: virtual addresses of its register blocks.+pub struct Transport {+ pub pci: PciDevice,+ common: u64,+ notify: u64,+ notify_multiplier: u32,+ isr: u64,+ device: u64,+}++// Plain MMIO helpers. Volatile: every access must really reach the device.+fn r8(a: u64) -> u8 { unsafe { read_volatile(a as *const u8) } }+fn r16(a: u64) -> u16 { unsafe { read_volatile(a as *const u16) } }+fn r32(a: u64) -> u32 { unsafe { read_volatile(a as *const u32) } }+fn w8(a: u64, v: u8) { unsafe { write_volatile(a as *mut u8, v) } }+fn w16(a: u64, v: u16) { unsafe { write_volatile(a as *mut u16, v) } }+fn w32(a: u64, v: u32) { unsafe { write_volatile(a as *mut u32, v) } }+fn w64(a: u64, v: u64) { w32(a, v as u32); w32(a + 4, (v >> 32) as u32) }++impl Transport {+ /// Find the register blocks of a modern virtio device by walking its+ /// vendor-specific capabilities. Each one says: block type, which BAR,+ /// offset inside the BAR, length.+ pub fn new(pci: &PciDevice) -> Option<Transport> {+ if pci.vendor_id != VENDOR {+ return None;+ }+ pci.enable_memory_and_bus_master();+ let (mut common, mut notify, mut isr, mut device, mut multiplier) = (0, 0, 0, 0, 0);+ for cap in pci.capabilities(0x09) {+ let cfg_type = pci.read_u8(cap + 3);+ let bar = pci.read_u8(cap + 4);+ let offset = pci.read_u32(cap + 8) as u64;+ let length = pci.read_u32(cap + 12) as u64;+ if bar > 5 || pci.bar(bar) & 1 != 0 {+ continue; // we only do memory BARs (there's also a PCI-config window type)+ }+ let base = pci.bar_address(bar);+ if base == 0 {+ continue;+ }+ let virt = crate::memory::map_mmio(base + offset, length.max(1)).as_u64();+ match cfg_type {+ CAP_COMMON if common == 0 => common = virt,+ CAP_NOTIFY if notify == 0 => {+ notify = virt;+ multiplier = pci.read_u32(cap + 16); // extra field of this cap+ }+ CAP_ISR if isr == 0 => isr = virt,+ CAP_DEVICE if device == 0 => device = virt,+ _ => {}+ }+ }+ if common == 0 || notify == 0 || isr == 0 {+ return None; // a legacy-only device+ }+ Some(Transport { pci: *pci, common, notify, notify_multiplier: multiplier, isr, device })+ }++ /// Reset, then negotiate features: we accept `wanted` (plus VERSION_1)+ /// as far as the device offers them. Returns the agreed set.+ pub fn begin_init(&self, wanted: u64) -> Option<u64> {+ let status = self.common + DEVICE_STATUS;+ w8(status, 0); // reset+ while r8(status) != 0 {+ core::hint::spin_loop();+ }+ w8(status, STATUS_ACKNOWLEDGE);+ w8(status, STATUS_ACKNOWLEDGE | STATUS_DRIVER);+ // Features are 64 bits read/written as two 32-bit halves.+ let mut offered = 0u64;+ for half in 0..2u32 {+ w32(self.common + DEVICE_FEATURE_SELECT, half);+ offered |= (r32(self.common + DEVICE_FEATURE) as u64) << (32 * half);+ }+ let agreed = offered & (wanted | F_VERSION_1);+ if agreed & F_VERSION_1 == 0 {+ return None;+ }+ for half in 0..2u32 {+ w32(self.common + DRIVER_FEATURE_SELECT, half);+ w32(self.common + DRIVER_FEATURE, (agreed >> (32 * half)) as u32);+ }+ w8(status, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK);+ // The device clears FEATURES_OK again if it can't live with our choice.+ if r8(status) & STATUS_FEATURES_OK == 0 {+ return None;+ }+ Some(agreed)+ }++ /// Set up virtqueue number `index` with at most `max_size` entries.+ /// `interrupts`: false = we'll poll it, tell the device not to bother.+ pub fn setup_queue(&self, index: u16, max_size: u16, interrupts: bool) -> Option<Virtqueue> {+ w16(self.common + QUEUE_SELECT, index);+ let device_max = r16(self.common + QUEUE_SIZE);+ if device_max == 0 {+ return None; // no such queue+ }+ // Split queues need a power of two; QEMU's sizes are, so min() keeps it.+ let size = device_max.min(max_size);+ w16(self.common + QUEUE_SIZE, size);+ let notify_off = r16(self.common + QUEUE_NOTIFY_OFF) as u64;+ let doorbell = self.notify + notify_off * self.notify_multiplier as u64;+ let queue = Virtqueue::new(index, size, doorbell, interrupts)?;+ w64(self.common + QUEUE_DESC, queue.desc_phys());+ w64(self.common + QUEUE_DRIVER, queue.avail_phys());+ w64(self.common + QUEUE_DEVICE, queue.used_phys());+ w16(self.common + QUEUE_ENABLE, 1);+ Some(queue)+ }++ /// Last step: tell the device we're ready.+ pub fn driver_ok(&self) {+ let status = self.common + DEVICE_STATUS;+ w8(status, r8(status) | STATUS_DRIVER_OK);+ }++ /// Read (and so acknowledge) the interrupt status: bit 0 = a queue was+ /// used, bit 1 = the device config changed.+ pub fn read_isr(&self) -> u8 {+ r8(self.isr)+ }++ /// 32-bit word of the device-specific config.+ pub fn config_u32(&self, offset: u64) -> u32 {+ if self.device == 0 { 0 } else { r32(self.device + offset) }+ }+ pub fn set_config_u32(&self, offset: u64, value: u32) {+ if self.device != 0 {+ w32(self.device + offset, value)+ }+ }+}++/// Interrupt handler part, called by interrupts.rs for shared PCI lines.+/// Every device on a shared line must have its ISR read, or (level+/// triggered) the line stays asserted forever. Only the console uses+/// interrupts; the rng and the GPU are polled.+pub fn handle_irq() {+ console::handle_irq();+ gpu::handle_irq();+ rng::handle_irq();+}++/// Bring up all three stage-13 drivers. Returns boot log lines.+pub fn init(devices: &[PciDevice]) -> alloc::vec::Vec<alloc::string::String> {+ let mut log = alloc::vec::Vec::new();+ log.push(rng::init(devices));+ log.extend(console::init(devices));+ log.extend(gpu::init(devices));+ log+}
@@ -0,0 +1,162 @@+//! A *split virtqueue*: three arrays in guest RAM shared with the device.+//!+//! ```text+//! descriptor table [addr u64 | len u32 | flags u16 | next u16] x size+//! available ring flags u16, idx u16, ring[size] u16 (driver -> device)+//! used ring flags u16, idx u16, ring[size] {id u32, len u32}+//! (device -> driver)+//! ```+//!+//! To send a request the driver fills one or more descriptors (chained with+//! the NEXT flag; WRITE marks buffers the *device* writes into, like a+//! response), puts the first one's index in the available ring, bumps+//! `avail.idx` and rings the doorbell. When done, the device puts that index+//! (and how many bytes it wrote) into the used ring and bumps `used.idx`.+//! Both indexes only ever count up (wrapping at 65536); position in the ring+//! is `idx % size`. That's the whole protocol.++use crate::memory;+use alloc::vec::Vec;+use core::ptr::{read_volatile, write_volatile};+use core::sync::atomic::{Ordering, fence};+use x86_64::PhysAddr;++const DESC_NEXT: u16 = 1; // the chain continues at `next`+const DESC_WRITE: u16 = 2; // the device writes this buffer+const AVAIL_NO_INTERRUPT: u16 = 1; // "don't interrupt me when you use buffers"++#[repr(C)]+struct Descriptor {+ addr: u64,+ len: u32,+ flags: u16,+ next: u16,+}++pub struct Virtqueue {+ index: u16,+ size: u16,+ phys: u64, // start of the descriptor table (all three parts are contiguous)+ avail_off: u64, // byte offsets of the available and used rings+ used_off: u64,+ doorbell: u64, // where to write `index` to notify the device+ free: Vec<u16>, // descriptors not in use+ avail_idx: u16, // our copy of avail.idx+ last_used: u16, // used ring entries we've already processed+}++// The raw addresses are only touched under the driver's lock.+unsafe impl Send for Virtqueue {}++impl Virtqueue {+ /// Allocate and zero the rings: descriptors and the available ring+ /// first, then the used ring on its own page-aligned spot.+ pub fn new(index: u16, size: u16, doorbell: u64, interrupts: bool) -> Option<Virtqueue> {+ let n = size as u64;+ let avail_off = 16 * n;+ let used_off = (avail_off + 6 + 2 * n).next_multiple_of(4096);+ let bytes = used_off + (6 + 8 * n).next_multiple_of(4096);+ let phys = memory::alloc_contiguous_frames((bytes / 4096) as usize)?.as_u64();+ let queue = Virtqueue {+ index, size, phys, avail_off, used_off, doorbell,+ free: (0..size).rev().collect(),+ avail_idx: 0,+ last_used: 0,+ };+ if !interrupts {+ unsafe { write_volatile(queue.ptr::<u16>(avail_off), AVAIL_NO_INTERRUPT) };+ }+ Some(queue)+ }++ pub fn desc_phys(&self) -> u64 { self.phys }+ pub fn avail_phys(&self) -> u64 { self.phys + self.avail_off }+ pub fn used_phys(&self) -> u64 { self.phys + self.used_off }+ pub fn free_descriptors(&self) -> usize { self.free.len() }+ /// The descriptor the next one-buffer `push` will use (drivers that+ /// keep one data slot per descriptor want to know it beforehand).+ pub fn next_free(&self) -> Option<u16> { self.free.last().copied() }++ fn ptr<T>(&self, offset: u64) -> *mut T {+ memory::phys_to_virt(PhysAddr::new(self.phys + offset)).as_mut_ptr()+ }++ /// Queue one request made of `buffers` (physical address, length,+ /// device-writable?). Returns the head descriptor's index (the request's+ /// id), or None if the queue is full. Doesn't notify: call `notify`.+ pub fn push(&mut self, buffers: &[(u64, u32, bool)]) -> Option<u16> {+ // (A fixed array, not a Vec: the console calls this for every+ // kernel log line, so keep it cheap.)+ if buffers.is_empty() || buffers.len() > 4 || self.free.len() < buffers.len() {+ return None;+ }+ let mut ids = [0u16; 4];+ for id in ids.iter_mut().take(buffers.len()) {+ *id = self.free.pop().unwrap();+ }+ let ids = &ids[..buffers.len()];+ for (i, &(addr, len, device_writes)) in buffers.iter().enumerate() {+ let mut flags = if device_writes { DESC_WRITE } else { 0 };+ let next = if i + 1 < ids.len() { flags |= DESC_NEXT; ids[i + 1] } else { 0 };+ let desc = Descriptor { addr, len, flags, next };+ unsafe { write_volatile(self.ptr::<Descriptor>(16 * ids[i] as u64), desc) };+ }+ let slot = (self.avail_idx % self.size) as u64;+ unsafe { write_volatile(self.ptr::<u16>(self.avail_off + 4 + 2 * slot), ids[0]) };+ // The descriptors and the ring entry must be visible before the+ // device can see the new index.+ fence(Ordering::SeqCst);+ self.avail_idx = self.avail_idx.wrapping_add(1);+ unsafe { write_volatile(self.ptr::<u16>(self.avail_off + 2), self.avail_idx) };+ fence(Ordering::SeqCst);+ Some(ids[0])+ }++ /// Ring the doorbell: "queue `index` has new requests".+ pub fn notify(&self) {+ unsafe { write_volatile(self.doorbell as *mut u16, self.index) };+ }++ /// Take one finished request off the used ring: (head id, bytes the+ /// device wrote). Its descriptors become free again.+ pub fn pop_used(&mut self) -> Option<(u16, u32)> {+ fence(Ordering::SeqCst);+ let used_idx = unsafe { read_volatile(self.ptr::<u16>(self.used_off + 2)) };+ if used_idx == self.last_used {+ return None;+ }+ let slot = (self.last_used % self.size) as u64;+ let entry = self.used_off + 4 + 8 * slot;+ let (id, len) = unsafe { (read_volatile(self.ptr::<u32>(entry)), read_volatile(self.ptr::<u32>(entry + 4))) };+ self.last_used = self.last_used.wrapping_add(1);+ // Give the chain back to the free list.+ let mut d = id as u16;+ loop {+ self.free.push(d);+ let desc = unsafe { read_volatile(self.ptr::<Descriptor>(16 * d as u64)) };+ if desc.flags & DESC_NEXT == 0 {+ break;+ }+ d = desc.next;+ }+ Some((id as u16, len))+ }++ /// Wait (spinning) for the device to finish one request, at most+ /// `timeout_us` microseconds. For the polled devices (rng, gpu).+ pub fn wait_used(&mut self, timeout_us: u64) -> Option<(u16, u32)> {+ let deadline = crate::time::now_us() + timeout_us;+ // Also count spins, in case the clock isn't running (interrupts off+ // before the TSC is calibrated).+ for spins in 0u64.. {+ if let Some(done) = self.pop_used() {+ return Some(done);+ }+ if crate::time::now_us() > deadline || spins > 200_000_000 {+ return None;+ }+ core::hint::spin_loop();+ }+ None+ }+}
@@ -0,0 +1,132 @@+//! virtio-rng: random bytes from the host (QEMU reads the host's+//! /dev/urandom). `-device virtio-rng-pci`.+//!+//! The simplest virtio device there is: one queue, and a request is just an+//! empty buffer the device fills with random bytes. We poll for the answer+//! (it takes microseconds) instead of waiting for an interrupt.+//!+//! Why bother? A computer is deterministic; good randomness has to come+//! from outside (hardware noise -- or here, the host). TCP needs+//! unpredictable sequence numbers and ports, or an attacker can guess them+//! and inject packets; programs need random keys and seeds. Without the+//! device we fall back to a generator seeded from the CPU's time stamp+//! counter -- fine for games, *not* for secrets, and we say so in the log.+//! (Real kernels also mix in the CPU's RDRAND and interrupt timings.)++use super::{Transport, Virtqueue};+use crate::memory;+use crate::pci::PciDevice;+use alloc::{format, string::String};+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};+use spin::Mutex;+use x86_64::{PhysAddr, instructions::interrupts::without_interrupts};++struct Rng {+ transport: Transport,+ queue: Virtqueue,+ buf_phys: u64, // one page the device writes random bytes into+}++static RNG: Mutex<Option<Rng>> = Mutex::new(None);+static HAVE_DEVICE: AtomicBool = AtomicBool::new(false);+/// State of the fallback generator (xorshift64*).+static FALLBACK: AtomicU64 = AtomicU64::new(0);++/// Is this the virtio entropy device? (transitional id 0x1005, modern 0x1044)+pub fn is_virtio_rng(d: &PciDevice) -> bool {+ d.vendor_id == super::VENDOR && matches!(d.device_id, 0x1005 | 0x1044)+}++pub fn init(devices: &[PciDevice]) -> String {+ let Some(pci) = devices.iter().find(|d| is_virtio_rng(d)) else {+ return String::from("rng: no virtio-rng, using a TSC-seeded generator (weak!)");+ };+ let setup = || -> Option<Rng> {+ let transport = Transport::new(pci)?;+ transport.begin_init(0)?;+ let queue = transport.setup_queue(0, 8, false)?;+ transport.driver_ok();+ let buf_phys = memory::alloc_contiguous_frames(1)?.as_u64();+ Some(Rng { transport, queue, buf_phys })+ };+ match setup() {+ Some(rng) => {+ without_interrupts(|| *RNG.lock() = Some(rng));+ HAVE_DEVICE.store(true, Ordering::Relaxed);+ format!("rng: virtio-rng at {:02x}:{:02x}.{}", pci.bus, pci.device, pci.function)+ }+ None => String::from("rng: virtio-rng found but setup failed, using a TSC-seeded generator (weak!)"),+ }+}++/// Is the host's entropy device in use (vs the weak fallback)?+pub fn available() -> bool {+ HAVE_DEVICE.load(Ordering::Relaxed)+}++/// Fill `buf` with random bytes. Returns false if the bytes came from the+/// weak fallback generator.+pub fn fill(buf: &mut [u8]) -> bool {+ let mut done = 0;+ while done < buf.len() && available() {+ match device_read(&mut buf[done..]) {+ Some(n) => done += n,+ None => {+ // No answer within a second: stop trusting the device.+ HAVE_DEVICE.store(false, Ordering::Relaxed);+ crate::serial_println!("[rng] virtio-rng stopped answering, using the fallback");+ }+ }+ }+ if done < buf.len() {+ fallback_fill(&mut buf[done..]);+ return false;+ }+ true+}++/// One request: up to a page of random bytes. Returns how many we got.+fn device_read(buf: &mut [u8]) -> Option<usize> {+ without_interrupts(|| {+ let mut guard = RNG.lock();+ let rng = guard.as_mut()?;+ let want = buf.len().min(4096);+ rng.queue.push(&[(rng.buf_phys, want as u32, true)])?;+ rng.queue.notify();+ let (_, len) = rng.queue.wait_used(1_000_000)?;+ let n = (len as usize).min(want);+ let src: *const u8 = memory::phys_to_virt(PhysAddr::new(rng.buf_phys)).as_ptr();+ buf[..n].copy_from_slice(unsafe { core::slice::from_raw_parts(src, n) });+ Some(n)+ })+}++/// A random u64 (for seeds and port numbers).+pub fn u64() -> u64 {+ let mut bytes = [0u8; 8];+ fill(&mut bytes);+ u64::from_le_bytes(bytes)+}++/// xorshift64* (Marsaglia / Vigna), seeded and stirred with the TSC.+fn fallback_fill(buf: &mut [u8]) {+ let tsc = unsafe { core::arch::x86_64::_rdtsc() };+ let mut x = FALLBACK.load(Ordering::Relaxed) ^ tsc.rotate_left(17) | 1;+ for chunk in buf.chunks_mut(8) {+ x ^= x >> 12;+ x ^= x << 25;+ x ^= x >> 27;+ let out = x.wrapping_mul(0x2545_f491_4f6c_dd1d).to_le_bytes();+ chunk.copy_from_slice(&out[..chunk.len()]);+ }+ FALLBACK.store(x, Ordering::Relaxed);+}++/// Acknowledge a (shared-line) interrupt; we poll, so nothing else to do.+pub fn handle_irq() {+ if let Some(guard) = RNG.try_lock() {+ if let Some(rng) = guard.as_ref() {+ rng.transport.read_isr();+ }+ }+}
@@ -0,0 +1 @@+hello from the host over hvc0
@@ -176,7 +176,7 @@ fn scripted_shell_session() { "piped text\n", "wc -l\n2\n", "saved\nmore\n",- "Rustbelt 0.12.0",+ "Rustbelt 0.13.0", "B:/ $ exit 3\n", ] { assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -62,7 +62,7 @@ fn mounts_are_separate_file_systems() { fs::mount("/tmp/deeper", RamFs::new()).unwrap(); assert_eq!(fs::lookup("/tmp/deeper").unwrap().list().unwrap().len(), 0); assert_eq!(fs::mount("/tmp/deeper", RamFs::new()).err(), Some(Errno::EEXIST));- assert_eq!(fs::mounts().len(), 3);+ assert_eq!(fs::mounts().len(), 4); // "/", /dev (devfs), /tmp, /tmp/deeper } #[test_case]@@ -73,7 +73,7 @@ fn write_grow_and_list() { assert_eq!(inode.read_at(0, &mut buf), Ok(7)); assert_eq!(&buf[..7], b"\0\0\0\0abc"); let names: Vec<String> = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();- assert_eq!(names, vec!["etc", "hello.txt", "mnt", "tmp"]);+ assert_eq!(names, vec!["dev", "etc", "hello.txt", "mnt", "tmp"]); } #[test_case]
@@ -0,0 +1,176 @@+//! Stage 13: the virtual drivers (virtio-rng, virtio-console, virtio-gpu).+//! Deterministic: QEMU provides every device (see test-args in Cargo.toml).+//! - rng: the bytes are not constant and look random (bit balance), the+//! getrandom path works for a user program+//! - console: the bytes QEMU feeds in from tests/hvc0-input.txt arrive,+//! and writes to the host complete without drops+//! - gpu: GET_DISPLAY_INFO works, a bad resource id is refused with the+//! right error, the framebuffer reads back what was written, the text+//! console moved onto it, `tile` draws through /dev/fb0+//! - /dev lists the devices, and nothing leaks frames+//! (scripts/gpu-check.sh checks the actual screen through QEMU.)+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{string::String, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::virtio::{console, gpu, rng};+use rustbelt::{fs, memory, serial_println, time, vga_buffer};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rustbelt::init();+ rustbelt::init_memory(boot_info);+ rustbelt::init_smp();+ fs::init();+ rustbelt::init_storage();+ for line in rustbelt::init_drivers() {+ serial_println!("[virtio] {}", line);+ }+ test_main();+ rustbelt::hlt_loop();+}++fn run(name: &str, args: &[&str]) -> ExitStatus {+ let mut spawn = SpawnArgs::kernel(name);+ spawn.args.extend(args.iter().map(|a| String::from(*a)));+ let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+ process::wait(&p)+}++#[test_case]+fn rng_bytes_are_random() {+ assert!(rng::available(), "no virtio-rng: test-args need -device virtio-rng-pci");+ let mut a = [0u8; 4096];+ let mut b = [0u8; 4096];+ assert!(rng::fill(&mut a) && rng::fill(&mut b), "virtio-rng gave no bytes");+ assert_ne!(a, b, "two reads gave the same bytes");+ assert!(a.iter().any(|&x| x != a[0]), "constant bytes");+ // 32768 random bits: the number of ones should be close to half+ // (the standard deviation is ~90, so +-1000 never fails by chance).+ let ones: u32 = a.iter().map(|x| x.count_ones()).sum();+ assert!((15384..=17384).contains(&ones), "{} one bits out of 32768", ones);+ // All 256 byte values should show up in 8 KiB (each ~32 times).+ let mut seen = [false; 256];+ a.iter().chain(b.iter()).for_each(|&x| seen[x as usize] = true);+ assert!(seen.iter().all(|&s| s), "some byte values never appeared");+ assert_ne!(rng::u64(), rng::u64());+}++#[test_case]+fn random_program_uses_getrandom() {+ assert_eq!(run("random", &["-n", "32"]), ExitStatus::Exited(0));+ assert_eq!(run("random", &["-d", "6", "10"]), ExitStatus::Exited(0));+ assert_eq!(run("random", &["-n", "x"]), ExitStatus::Exited(2)); // usage error+}++#[test_case]+fn console_receives_host_input() {+ assert!(console::present(), "no virtio-console: test-args need virtio-serial-pci + virtconsole");+ // QEMU sends the input file as soon as the port is ready; wait up to 5 s.+ let expected = b"hello from the host over hvc0\n";+ let mut got: Vec<u8> = Vec::new();+ let deadline = time::now_us() + 5_000_000;+ while got.len() < expected.len() && time::now_us() < deadline {+ got.extend(console::drain_input());+ rustbelt::thread::yield_now();+ }+ assert_eq!(&got[..], &expected[..], "hvc0 input: {:?}", core::str::from_utf8(&got));+}++#[test_case]+fn console_writes_complete() {+ let dropped = console::DROPPED.load(core::sync::atomic::Ordering::Relaxed);+ // 8 KiB: more than all 8 transmit slots at once, so slots get reused.+ let line = b"Rustbelt hvc0 output test: 0123456789 abcdefghijklmnopqrstuvwxyz\n";+ for _ in 0..128 {+ assert_eq!(console::write(line), line.len());+ }+ assert_eq!(console::DROPPED.load(core::sync::atomic::Ordering::Relaxed), dropped);+}++#[test_case]+fn gpu_display_info_and_errors() {+ let fb = gpu::framebuffer().expect("no virtio-gpu: test-args need -device virtio-vga");+ assert_eq!((fb.width, fb.height), (gpu::WIDTH, gpu::HEIGHT));+ assert_eq!(fb.size_bytes(), 1024 * 768 * 4);+ assert_eq!(gpu::command(gpu::CMD_GET_DISPLAY_INFO, &[]), gpu::RESP_OK_DISPLAY_INFO);+ let (w, h) = gpu::preferred_size();+ assert!(w >= 640 && h >= 480, "display {}x{}", w, h);+ // Flushing a resource that doesn't exist must fail, not hang or crash.+ let bad_flush = [0, 0, 16, 16, 999, 0];+ assert_eq!(gpu::command(gpu::CMD_RESOURCE_FLUSH, &bad_flush), gpu::RESP_ERR_INVALID_RESOURCE_ID);+ assert!(gpu::flush(0, 0, 1024, 768), "a full-screen flush failed");+ assert!(gpu::flush(1000, 700, 500, 500), "a clipped flush failed");+}++#[test_case]+fn framebuffer_reads_back() {+ let fb = gpu::framebuffer().unwrap();+ let px = |x: u32, y: u32| unsafe { fb.pixels().add((y * fb.stride + x) as usize) };+ // Write a pattern into a corner, flush it, read it back.+ for y in 700..768 {+ for x in 900..1024 {+ unsafe { px(x, y).write_volatile(x * 7 ^ y * 13) };+ }+ }+ assert!(gpu::flush(900, 700, 124, 68));+ for y in 700..768 {+ for x in 900..1024 {+ assert_eq!(unsafe { px(x, y).read_volatile() }, x * 7 ^ y * 13);+ }+ }+}++#[test_case]+fn text_console_is_on_the_framebuffer() {+ assert!(vga_buffer::on_framebuffer());+ assert_eq!(vga_buffer::size(), (128, 48));+ // Print a line: some pixels in the bottom text row must be lit.+ rustbelt::println!("framebuffer console test");+ let fb = gpu::framebuffer().unwrap();+ let row_y = 768 - 2 * 16; // the line above the (empty) cursor line+ let lit = (row_y..row_y + 16)+ .flat_map(|y| (0..24 * 8).map(move |x| (x, y)))+ .filter(|&(x, y)| unsafe { fb.pixels().add((y * fb.stride + x) as usize).read_volatile() } != 0)+ .count();+ assert!(lit > 200, "only {} lit pixels where the text is", lit);+}++#[test_case]+fn dev_lists_the_devices() {+ let dev = fs::lookup("/dev").expect("no /dev");+ let mut names: Vec<String> = dev.list().expect("list /dev").into_iter().map(|e| e.name).collect();+ names.sort();+ assert_eq!(names, ["fb0", "hvc0", "null", "random", "urandom"]);+ assert_eq!(fs::lookup("/dev/fb0").unwrap().size(), 1024 * 768 * 4);+}++#[test_case]+fn tile_draws_through_dev_fb0() {+ let frames = memory::frames_in_use();+ assert_eq!(run("tile", &[]), ExitStatus::Exited(0));+ let fb = gpu::framebuffer().unwrap();+ let px = |x: u32, y: u32| unsafe { fb.pixels().add((y * fb.stride + x) as usize).read_volatile() };+ assert_eq!(px(11, 300), 0x33ccff, "focused border");+ assert_eq!(px(525, 18), 0x89b4fa, "window 1");+ assert_eq!(px(778, 397), 0xf9e2af, "window 3");+ assert_eq!(px(2, 2), 0x1e1e2e, "wallpaper");+ assert_eq!(vga_buffer::size(), (61, 46), "console moved into window 0");+ assert_eq!(run("tile", &["-r"]), ExitStatus::Exited(0));+ assert_eq!(vga_buffer::size(), (128, 48));+ assert_eq!(memory::frames_in_use(), frames, "frames leaked");+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rustbelt::test_panic_handler(info)+}
@@ -0,0 +1,54 @@+//! getty [-1] DEVICE -- run a shell on another terminal, e.g. /dev/hvc0.+//!+//! Real Unix systems run one "getty" per terminal line: it opens the line+//! and starts a login shell on it. Ours skips the login: it opens DEVICE+//! and spawns `sh` with stdin, stdout and stderr all on it. When the shell+//! exits (`exit`) a new one starts, unless `-1` was given.+//!+//! Try it: `getty /dev/hvc0 &` here, then on the host+//! `socat - UNIX-CONNECT:target/hvc0.sock` and type commands. That shell+//! is not "interactive" (no line editing): it reads one line at a time and+//! prints each line after the prompt, like a script.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++const SHELL: &str = "/mnt/disk0/bin/sh";++fn main() -> i64 {+ let (once, dev) = match (arg(1), arg(2)) {+ (Some("-1"), Some(dev)) => (true, dev),+ (Some(dev), None) if dev != "-1" => (false, dev),+ _ => {+ eprintln!("usage: getty [-1] DEVICE (e.g. getty /dev/hvc0 &)");+ return 2;+ }+ };+ loop {+ let fd = open(dev, 0);+ if fd < 0 {+ eprintln!("getty: {}: {}", dev, strerror(fd));+ return 1;+ }+ let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";+ write(fd, greeting.as_bytes());+ let opts = SpawnOptions { stdin: fd, stdout: fd, stderr: fd, flags: 0 };+ let pid = spawn(SHELL, "sh", &opts);+ if pid < 0 {+ eprintln!("getty: {}: {}", SHELL, strerror(pid));+ close(fd);+ return 1;+ }+ let status = wait(pid);+ close(fd);+ if once {+ return status;+ }+ sleep_ms(500); // don't spin if the shell keeps dying at once+ }+}
@@ -21,6 +21,7 @@ struct Entry { name: String, dir: bool, link: bool,+ dev: bool, // a device like /dev/fb0 (shown as <DEV>) size: u64, mode: u64, // 0 = no permissions on this file system target: String, // symlinks (with -l): where they point@@ -51,7 +52,7 @@ fn main() -> i64 { continue; } if st.kind != KIND_DIR {- let entry = Entry { name: String::from(*path), dir: false, link: false, size: st.size, mode: st.mode, target: String::new() };+ let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.kind == KIND_DEVICE, size: st.size, mode: st.mode, target: String::new() }; print_entries(&[entry], long, columns); continue; }@@ -85,8 +86,8 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> { } for_each_dirent(&buf, n as usize, |name, d_type| { if name != "." && name != ".." {- let (dir, link) = (d_type == 4, d_type == 10); // DT_DIR, DT_LNK- entries.push(Entry { name: String::from(name), dir, link, size: 0, mode: 0, target: String::new() });+ let (dir, link, dev) = (d_type == 4, d_type == 10, d_type == 2); // DT_DIR, DT_LNK, DT_CHR+ entries.push(Entry { name: String::from(name), dir, link, dev, size: 0, mode: 0, target: String::new() }); } }); }@@ -120,6 +121,8 @@ fn print_entries(entries: &[Entry], long: bool, columns: bool) { println!("{} <DIR> {}/", perms, e.name); } else if e.link { println!("{} {} -> {}", perms, e.name, e.target);+ } else if e.dev {+ println!("{} <DEV> {}", perms, e.name); } else { println!("{}{:>10} {}", perms, e.size, e.name); }@@ -153,7 +156,7 @@ fn mode_string(e: &Entry) -> String { if e.link { return String::from("lrwxrwxrwx"); // symlink permissions are never used }- let mut s = String::from(if e.dir { "d" } else { "-" });+ let mut s = String::from(if e.dir { "d" } else if e.dev { "c" } else { "-" }); for shift in [6, 3, 0] { let bits = (e.mode >> shift) & 7; s.push(if bits & 4 != 0 { 'r' } else { '-' });
@@ -0,0 +1,74 @@+//! random -- print random bytes from the kernel's entropy source.+//!+//! random 16 random bytes as hex (a fresh 128-bit id)+//! random -n 32 32 bytes as hex+//! random -d 6 [N] roll N dice with 6 sides (default 1 die)+//!+//! The bytes come from the `getrandom` system call (Linux number 318),+//! which the kernel fills from the virtio-rng device, i.e. from the host's+//! /dev/urandom. /dev/random and /dev/urandom read the same source.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ match (arg(1), arg(2), arg(3)) {+ (None, ..) => hex(16),+ (Some("-n"), Some(n), None) => match n.parse::<usize>() {+ Ok(n) if n <= 4096 => hex(n),+ _ => usage(),+ },+ (Some("-d"), Some(sides), count) => {+ let sides = sides.parse::<u64>().unwrap_or(0);+ let count = count.map_or(Some(1), |c| c.parse::<u64>().ok()).unwrap_or(0);+ if sides < 2 || count == 0 || count > 100 {+ return usage();+ }+ for i in 0..count {+ print!("{}{}", if i > 0 { " " } else { "" }, roll(sides));+ }+ println!();+ 0+ }+ _ => usage(),+ }+}++/// Print `n` random bytes as hex digits.+fn hex(n: usize) -> i64 {+ let mut buf = [0u8; 4096];+ let r = getrandom(&mut buf[..n]);+ if r < 0 {+ eprintln!("random: getrandom: {}", strerror(r));+ return 1;+ }+ for b in &buf[..n] {+ print!("{:02x}", b);+ }+ println!();+ 0+}++/// A fair number in 1..=sides. Plain `x % sides` would favour small+/// numbers slightly, so throw away the values from the uneven top part.+fn roll(sides: u64) -> u64 {+ let limit = u64::MAX - u64::MAX % sides;+ loop {+ let mut b = [0u8; 8];+ getrandom(&mut b);+ let x = u64::from_le_bytes(b);+ if x < limit {+ return x % sides + 1;+ }+ }+}++fn usage() -> i64 {+ eprintln!("usage: random | random -n BYTES (max 4096) | random -d SIDES [COUNT]");+ 2+}
@@ -43,6 +43,7 @@ Rustbelt shell commands programs ls [-l] cat wc grep [-i] [-v] mkdir rm touch cp sleep sysinfo network ifconfig (ip) ping [-c n] host host name (nslookup) fetch [-o file] http://... (wget) httpd [-p port] [-n count]+ devices random [-n bytes | -d sides [n]] getty [-1] /dev/hvc0 tile [n] | tile -r (bin dirs) hello demo evil spin [rounds] overflow child sh run [--trace] prog args run a program (--trace prints its syscalls) start prog args run it in the background (or: prog args &)
@@ -0,0 +1,173 @@+//! tile [N] -- draw a tiling-window-manager mock-up on /dev/fb0.+//!+//! tile 4 "windows" in a dwindle layout, the text console in the first+//! tile 6 1 to 6 windows+//! tile -r give the text console the whole screen back+//!+//! A hint of the compositor planned for later (think Hyprland): a gradient+//! wallpaper, windows with gaps between them and a border, the focused one+//! cyan. The "windows" are only coloured rectangles, except the first one,+//! which is where the kernel's text console moves to (FB_SET_CONSOLE), so+//! the shell keeps working inside it.+//!+//! How it talks to the framebuffer (no mmap yet, so no direct pointer):+//! ioctl(FB_GET_INFO) width, height, bytes per row, bits per pixel+//! lseek + write raw 0x00RRGGBB pixels (the wallpaper rows)+//! ioctl(FB_FILL) fill a rectangle with one colour (fast)+//! ioctl(FB_FLUSH) send the changed part to the screen+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::vec;+use alloc::vec::Vec;+use ulib::*;++const GAP: u32 = 10; // space between windows and around the screen edge+const BORDER: u32 = 3;+const ACTIVE: u32 = 0x33ccff; // focused window border (cyan)+const INACTIVE: u32 = 0x595959; // the other borders (gray)+/// Window contents: soft pastels (the first window is the console).+const COLORS: [u32; 5] = [0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf38ba8, 0xcba6f7];++#[derive(Clone, Copy)]+struct Rect {+ x: u32,+ y: u32,+ w: u32,+ h: u32,+}++fn main() -> i64 {+ let fd = open("/dev/fb0", 0);+ if fd < 0 {+ eprintln!("tile: /dev/fb0: {} (no virtio-gpu?)", strerror(fd));+ return 1;+ }+ let status = match arg(1) {+ Some("-r") => reset(fd),+ Some(n) => match n.parse::<usize>() {+ Ok(n @ 1..=6) => draw(fd, n),+ _ => {+ eprintln!("usage: tile [1-6] | tile -r");+ 2+ }+ },+ None => draw(fd, 4),+ };+ close(fd);+ status+}++fn reset(fd: i64) -> i64 {+ let r = ioctl(fd, FB_SET_CONSOLE, 0);+ if r < 0 {+ eprintln!("tile: {}", strerror(r));+ return 1;+ }+ println!("console: full screen, {}x{} characters", r >> 16, r & 0xffff);+ 0+}++fn draw(fd: i64, n: usize) -> i64 {+ let mut info = [0u32; 4];+ if ioctl(fd, FB_GET_INFO, info.as_mut_ptr() as u64) < 0 {+ eprintln!("tile: FB_GET_INFO failed");+ return 1;+ }+ let [width, height, stride, _bpp] = info;+ wallpaper(fd, width, height, stride);++ let screen = Rect { x: GAP, y: GAP, w: width - 2 * GAP, h: height - 2 * GAP };+ let windows = dwindle(screen, n);+ for (i, &win) in windows.iter().enumerate() {+ fill(fd, win, if i == 0 { ACTIVE } else { INACTIVE });+ let inside = shrink(win, BORDER);+ if i > 0 {+ fake_content(fd, inside, COLORS[(i - 1) % COLORS.len()]);+ }+ }+ let flush = [0, 0, width, height];+ ioctl(fd, FB_FLUSH, flush.as_ptr() as u64);++ // Move the console into window 0 (inside its border).+ let c = shrink(windows[0], BORDER);+ let r = ioctl(fd, FB_SET_CONSOLE, [c.x, c.y, c.w, c.h].as_ptr() as u64);+ if r < 0 {+ eprintln!("tile: FB_SET_CONSOLE: {}", strerror(r));+ return 1;+ }+ println!("tile: {} windows on {}x{}, console {}x{}", n, width, height, r >> 16, r & 0xffff);+ 0+}++/// Split the screen like a "dwindle" layout: each window takes half of+/// what is left, alternating left|right and top/bottom splits.+fn dwindle(mut area: Rect, n: usize) -> Vec<Rect> {+ let mut out = Vec::new();+ for i in 0..n {+ if i + 1 == n {+ out.push(area); // the last window gets the rest+ break;+ }+ if i % 2 == 0 {+ let w = (area.w - GAP) / 2;+ out.push(Rect { w, ..area });+ area = Rect { x: area.x + w + GAP, w: area.w - w - GAP, ..area };+ } else {+ let h = (area.h - GAP) / 2;+ out.push(Rect { h, ..area });+ area = Rect { y: area.y + h + GAP, h: area.h - h - GAP, ..area };+ }+ }+ out+}++/// A vertical gradient, written 8 rows (32 KiB) at a time with lseek ++/// write. (Programs only have a 64 KiB heap until mmap exists.)+fn wallpaper(fd: i64, width: u32, height: u32, stride: u32) {+ const ROWS: u32 = 8;+ let mut buf = vec![0u8; (stride * ROWS) as usize];+ for y0 in (0..height).step_by(ROWS as usize) {+ let rows = ROWS.min(height - y0);+ for dy in 0..rows {+ let color = gradient(y0 + dy, height).to_le_bytes();+ let row = &mut buf[(dy * stride) as usize..][..(width * 4) as usize];+ row.chunks_exact_mut(4).for_each(|px| px.copy_from_slice(&color));+ }+ lseek(fd, (y0 * stride) as i64, SEEK_SET);+ write(fd, &buf[..(rows * stride) as usize]);+ }+}++/// Dark blue at the top to purple at the bottom.+fn gradient(y: u32, height: u32) -> u32 {+ let mix = |a: u32, b: u32| a + (b - a) * y / height;+ mix(0x1e, 0x50) << 16 | mix(0x1e, 0x2a) << 8 | mix(0x2e, 0x78)+}++/// Fill a window with `color` and draw some darker bars, like lines of text.+fn fake_content(fd: i64, r: Rect, color: u32) {+ fill(fd, r, color);+ let dark = (color >> 1) & 0x7f7f7f; // every channel halved+ let mut y = r.y + 16;+ let mut line = 0;+ while y + 6 < r.y + r.h - 8 {+ let w = (r.w - 32) * [9, 6, 8, 4, 7][line % 5] / 10;+ fill(fd, Rect { x: r.x + 16, y, w, h: 6 }, dark);+ y += 16;+ line += 1;+ }+}++fn shrink(r: Rect, by: u32) -> Rect {+ Rect { x: r.x + by, y: r.y + by, w: r.w - 2 * by, h: r.h - 2 * by }+}++fn fill(fd: i64, r: Rect, color: u32) {+ ioctl(fd, FB_FILL, [r.x, r.y, r.w, r.h, color].as_ptr() as u64);+}
@@ -53,11 +53,17 @@ pub const SYS_SETSOCKOPT: u64 = 54; pub const SYS_CLOCK_GETTIME: u64 = 228; pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504;+pub const SYS_GETRANDOM: u64 = 318; pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const TTY_SET_RAW: u64 = 1;+// /dev/fb0 ioctls (see the table in the kernel's syscall.rs).+pub const FB_GET_INFO: u64 = 0x4600;+pub const FB_FLUSH: u64 = 0x46f0;+pub const FB_FILL: u64 = 0x46f1;+pub const FB_SET_CONSOLE: u64 = 0x46f2; pub const WNOHANG: u64 = 1; pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1;@@ -90,6 +96,7 @@ pub const KIND_DIR: u64 = 2; pub const KIND_CONSOLE: u64 = 3; pub const KIND_PIPE: u64 = 4; pub const KIND_SYMLINK: u64 = 5;+pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ... /// Same layout as the kernel's `syscall::SpawnOptions`. #[repr(C)]@@ -208,6 +215,14 @@ pub fn uname(buf: &mut [u8; 6 * 65]) -> [&str; 6] { core::str::from_utf8(&field[..len]).unwrap_or("?") }) }+/// Fill `buf` with random bytes from the kernel (virtio-rng). Linux 318.+pub fn getrandom(buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETRANDOM, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+/// A raw ioctl: what `request` means depends on the device.+pub fn ioctl(fd: i64, request: u64, arg: u64) -> i64 {+ syscall3(SYS_IOCTL, fd as u64, request, arg)+} /// The mount table as text ("path fstype" lines); returns the length. pub fn mounts(buf: &mut [u8]) -> i64 { syscall3(SYS_MOUNTS, buf.as_mut_ptr() as u64, buf.len() as u64, 0)@@ -360,6 +375,7 @@ pub fn strerror(errno: i64) -> &'static str { 12 => "Out of memory", 14 => "Bad address", 16 => "Device or resource busy",+ 19 => "No such device", 17 => "File exists", 20 => "Not a directory", 21 => "Is a directory",