RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 04:30:07 -0700
commit2a105612c9f6eeb692b8d5862fb2b27ae68a3ee9
parent3d90f5acee1369df49034601dd4c252cfc6c9e3d
treebrowse 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.
MCargo.lock+1 -1
MCargo.toml+19 -1
MREADME.md+408 -42
Mboot.png+0 -0
Mbuild.rs+1 -0
Mdisk/scripts/selftest.sh+10 -0
Ascripts/gpu-check.sh+63 -0
Ascripts/hvc0-session.txt+5 -0
Mscripts/run-headless.sh+33 -5
Mscripts/screenshot.sh+19 -15
Asrc/dev.rs+252 -0
Msrc/errno.rs+3 -2
Asrc/fbcon.rs+196 -0
Asrc/font.rs+313 -0
Msrc/fs/exfat.rs+1 -1
Msrc/fs/ext2.rs+1 -1
Msrc/fs/file.rs+7 -0
Msrc/fs/mod.rs+9 -0
Msrc/fs/ramfs.rs+1 -1
Msrc/interrupts.rs+2 -0
Msrc/lib.rs+18 -1
Msrc/main.rs+6 -0
Msrc/memory.rs+26 -0
Msrc/net/mod.rs+7 -2
Msrc/pci.rs+58 -0
Msrc/serial.rs+2 -0
Msrc/syscall.rs+75 -3
Msrc/vga_buffer.rs+140 -30
Asrc/virtio/console.rs+245 -0
Asrc/virtio/gpu.rs+247 -0
Asrc/virtio/mod.rs+230 -0
Asrc/virtio/queue.rs+162 -0
Asrc/virtio/rng.rs+132 -0
Atests/hvc0-input.txt+1 -0
Mtests/shell.rs+1 -1
Mtests/vfs.rs+2 -2
Atests/virtio.rs+176 -0
Auser/getty.rs+54 -0
Muser/ls.rs+7 -4
Auser/random.rs+74 -0
Muser/sh.rs+1 -0
Auser/tile.rs+173 -0
Muser/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`). -![Rustbelt 0.12.0 (stage 12: networking): ifconfig shows eth0 with 10.0.2.15/24 from DHCP; ping 10.0.2.2 gets 3 replies; fetch prints a page from QEMU's test web server; wget saves 108894 bytes to D:/seq.txt](boot.png)+![Rustbelt 0.13.0 (stage 13: virtual drivers) on the 1024x768 virtio-gpu framebuffer: `tile` drew a gradient wallpaper and four tiled windows with gaps and borders; the text console now lives in the left window with the cyan focus border, showing ls -l /dev (fb0, hvc0, null, random, urandom), random bytes, dice rolls and a ping](boot.png)  ## 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",