Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/loon-kernel/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
target/
screenshot.png
11 changes: 10 additions & 1 deletion crates/loon-kernel/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ KERNEL := target/riscv64gc-unknown-none-elf/release/loon-kernel
QEMU := qemu-system-riscv64
QFLAGS := -machine virt -cpu rv64 -smp 1 -m 128M -nographic -bios default

.PHONY: build run check clean
.PHONY: build run gui screenshot check clean

build:
cargo build --release
Expand All @@ -15,6 +15,15 @@ build:
run: build
$(QEMU) $(QFLAGS) -serial mon:stdio -kernel $(KERNEL)

# Boot with a display. The kernel finds the ramfb, runs boot/gui.oo, and
# stays up so there is something to look at. Close the window to quit.
gui: build
$(QEMU) $(QFLAGS) -serial mon:stdio -device ramfb -display cocoa -kernel $(KERNEL)

# Boot headless with a display, grab the framebuffer over QMP as a PNG.
screenshot: build
@python3 tools/screenshot.py screenshot.png

# The same program on the host, for comparison. Run from the workspace root:
# this directory's .cargo/config.toml pins a bare-metal target that the host
# build must not inherit.
Expand Down
28 changes: 25 additions & 3 deletions crates/loon-kernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ the outermost handler is a UART driver rather than a call into Linux.
brew install qemu
rustup target add riscv64gc-unknown-none-elf

make run # boot it
make host # run the same program on the host
make check # boot it and diff the two
make run # boot it (serial console)
make gui # boot it with a display — the kernel paints a framebuffer
make screenshot # boot headless, grab the framebuffer over QMP as a PNG
make host # run the same program on the host
make check # boot it and diff the two
```

## What is here
Expand All @@ -22,9 +24,13 @@ make check # boot it and diff the two
| `src/mmio.rs` | the one place that touches device registers |
| `src/heap.rs` | first-fit free-list allocator over RAM above the image |
| `src/sbi.rs` | the slice of SBI we need (power off) |
| `src/fwcfg.rs` | QEMU fw_cfg, via its DMA interface — used to find and configure the ramfb |
| `src/ramfb.rs` | the display: a linear XRGB framebuffer in RAM that QEMU scans out |
| `tools/screenshot.py` | headless boot + QMP `screendump` → PNG |
| `src/eir/` | boot-image decoder and the EIR interpreter |
| `boot/init.oo` | the init program — ordinary Loon |
| `boot/mandel.oo` | a Mandelbrot set, because a kernel that boots should get to do one gratuitous thing |
| `boot/gui.oo` | first light: a Loon program painting the framebuffer through `Fb` effects |

The host toolchain is not in this crate's build graph. `build.rs` shells out
to `loon image`, which compiles `boot/init.oo` to a boot image; the kernel
Expand Down Expand Up @@ -89,6 +95,22 @@ What remains is roughly 500 ns/op against ~7 ns/op for a minimal native
dispatch loop under the same emulator. That gap is real and unexplained;
chasing it needs an idle machine and a profiler, not more guessing.

## The display

`Fb` is an effect (`width`, `height`, `clear`, `fill-rect`, `present`) that
falls through the Loon handler stack to the ramfb driver, exactly as
`Console.write` falls through to the UART. `boot/gui.oo` runs only when the
machine was booted with `-device ramfb`; without one, `Fb` ops raise a loud
error naming the missing device, and the headless boot never invokes them.

Raster stays in Rust behind rectangle-sized primitives on purpose: at the
interpreter's current speed, per-pixel Loon would be ~150 ms per 640×480
frame. What lives in Loon is the *what*, not the *how*.

Not yet: text (needs an embedded bitmap font), input (virtio-input — the next
real piece of work), a host-side `Fb` handler so `make check` can diff the
GUI the way it diffs the console, and any notion of time in the event loop.

## Known limits

- **Cooperative only.** No timer interrupt yet, so a pure loop owns the
Expand Down
31 changes: 31 additions & 0 deletions crates/loon-kernel/boot/gui.oo
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
; First light. A Loon program painting a framebuffer, as the kernel.
;
; `Fb` is an effect like any other: nothing here knows whether the pixels
; land in a ramfb, a virtio-gpu, a PPM file on the host, or a simulation
; that only checks the draw calls. This one runs when the machine has a
; display (`make gui`) and is skipped when it does not.

[effect Fb
[width [] Int]
[height [] Int]
[clear [Int] Unit]
[fill-rect [Int Int Int Int Int] Unit]
[present [] Unit]]

; A column of bars, each a little further along, each a little bluer.
[fn bar [i]
[let w [Fb.width]]
[let y [+ 40 [* i 36]]]
[let len [+ 120 [* i 44]]]
[let blue [+ 96 [* i 16]]]
[Fb.fill-rect 40 y len 24 [+ [* 40 65536] [+ [* 60 256] blue]]]]

[fn main []
[Fb.clear 1710618] ; 0x1a1a1a
[each [range 0 10] bar]
; the loon: a big off-white square with a dark eye
[let w [Fb.width]]
[Fb.fill-rect [- w 200] 60 140 140 15658734] ; 0xeeeeee
[Fb.fill-rect [- w 130] 100 24 24 1710618]
[Fb.present]
[println [str "painted " w "x" [Fb.height]]]]
1 change: 1 addition & 0 deletions crates/loon-kernel/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fn main() {
("bench", "LOON_BENCH_IMAGE"),
("loop", "LOON_LOOP_IMAGE"),
("mandel", "LOON_MANDEL_IMAGE"),
("gui", "LOON_GUI_IMAGE"),
] {
let src = manifest.join(format!("boot/{name}.oo"));
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join(format!("{name}.img"));
Expand Down
25 changes: 25 additions & 0 deletions crates/loon-kernel/src/eir/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ pub trait Host {
fn write(&mut self, s: &str);
/// Monotonic ticks since boot.
fn ticks(&mut self) -> i64;
/// Framebuffer, if the machine has one. Ops are named rather than
/// enumerated so the VM does not have to know what a display can do; the
/// host decides, and says loudly when it can't.
fn fb(&mut self, op: &str, args: &[i64]) -> Result<Option<i64>, String>;
}

/// Operands read out of registers for one instruction.
Expand Down Expand Up @@ -785,6 +789,27 @@ impl<'m, H: Host> Vm<'m, H> {
Ok(Val::Unit)
}
("Clock", "now") | ("Clock", "ticks") => Ok(Val::Int(self.host.ticks())),
("Fb", op) => {
// Everything a framebuffer takes is an integer: coordinates,
// sizes, 0xRRGGBB colours.
let mut ints = Vec::with_capacity(args.len());
for a in args {
match a {
Val::Int(n) => ints.push(*n),
Val::Float(f) => ints.push(*f as i64),
v => {
return Err(alloc::format!(
"Fb.{op}: expected an integer argument, got a {}",
v.type_name()
))
}
}
}
Ok(match self.host.fb(op, &ints)? {
Some(n) => Val::Int(n),
None => Val::Unit,
})
}
("Fail", "fail") => Err(alloc::format!(
"unhandled failure: {}",
args.first().map(show).unwrap_or_default()
Expand Down
115 changes: 115 additions & 0 deletions crates/loon-kernel/src/fwcfg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! QEMU fw_cfg — the firmware configuration channel.
//!
//! A tiny key/value store the emulator exposes to the guest; on `virt` it
//! sits at 0x1010_0000. We use exactly one thing from it: the `etc/ramfb`
//! file, whose contents tell QEMU where our framebuffer lives. Everything
//! goes through the DMA interface, which is byte-order-defined (big-endian)
//! and does not care about the register's access width.

use core::sync::atomic::{fence, Ordering};

use crate::mmio;

const BASE: usize = 0x1010_0000;
const SELECTOR: usize = BASE + 0x08;
const DMA: usize = BASE + 0x10;

const KEY_FILE_DIR: u16 = 0x0019;

const CTL_ERROR: u32 = 1 << 0;
const CTL_READ: u32 = 1 << 1;
const CTL_SELECT: u32 = 1 << 3;
const CTL_WRITE: u32 = 1 << 4;

/// One DMA descriptor, in memory, all fields big-endian.
#[repr(C)]
struct DmaAccess {
control: u32,
length: u32,
address: u64,
}

/// One entry of the file directory: `struct FWCfgFile`.
#[repr(C)]
struct File {
size: u32,
select: u16,
_reserved: u16,
name: [u8; 56],
}

pub struct FwCfg;

impl FwCfg {
/// Issue one DMA transfer and wait for it. `control` carries the op bits
/// (and, if selecting, the key in the upper half).
unsafe fn dma(&self, control: u32, buf: *mut u8, len: usize) -> Result<(), ()> {
let desc = DmaAccess {
control: control.to_be(),
length: (len as u32).to_be(),
address: (buf as u64).to_be(),
};
// The device reads the descriptor and the buffer straight from RAM.
fence(Ordering::SeqCst);
mmio::write64(DMA, (&desc as *const DmaAccess as u64).to_be());
// Completion is signalled by the device clearing `control`.
loop {
fence(Ordering::SeqCst);
let c = u32::from_be(core::ptr::read_volatile(&desc.control));
if c == 0 {
return Ok(());
}
if c & CTL_ERROR != 0 {
return Err(());
}
core::hint::spin_loop();
}
}

unsafe fn read(&self, key: u16, buf: &mut [u8]) -> Result<(), ()> {
self.dma(
((key as u32) << 16) | CTL_SELECT | CTL_READ,
buf.as_mut_ptr(),
buf.len(),
)
}

/// Continue reading the currently selected item.
unsafe fn read_more(&self, buf: &mut [u8]) -> Result<(), ()> {
self.dma(CTL_READ, buf.as_mut_ptr(), buf.len())
}

/// Find a named file and return its selector key.
pub fn find(&self, name: &str) -> Option<u16> {
unsafe {
let mut count = [0u8; 4];
self.read(KEY_FILE_DIR, &mut count).ok()?;
let count = u32::from_be_bytes(count);
for _ in 0..count {
let mut raw = [0u8; core::mem::size_of::<File>()];
self.read_more(&mut raw).ok()?;
let f: File = core::ptr::read_unaligned(raw.as_ptr() as *const File);
let n = f.name.iter().position(|&b| b == 0).unwrap_or(f.name.len());
if &f.name[..n] == name.as_bytes() {
return Some(u16::from_be(f.select));
}
}
None
}
}

/// Overwrite a file's contents (only meaningful for the few writable
/// ones, like `etc/ramfb`).
pub fn write(&self, key: u16, data: &[u8]) -> Result<(), ()> {
unsafe {
// Selecting via the register first is belt and braces: some
// firmware paths do it and it costs nothing.
mmio::write16(SELECTOR, key.to_be());
self.dma(
((key as u32) << 16) | CTL_SELECT | CTL_WRITE,
data.as_ptr() as *mut u8,
data.len(),
)
}
}
}
68 changes: 61 additions & 7 deletions crates/loon-kernel/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ static HEAP: heap::Heap = heap::Heap::new();
#[macro_use]
mod uart;
mod eir;
mod fwcfg;
mod heap;
mod mmio;
mod ramfb;
mod sbi;

// Set up a stack and clear .bss before anything Rust-shaped runs. `a0` holds
Expand Down Expand Up @@ -99,7 +101,26 @@ pub extern "C" fn kmain(hart: usize, dtb: usize) -> ! {
micros_since(t0),
micros_since(BOOT.load(core::sync::atomic::Ordering::Relaxed)),
);
sbi::shutdown(false)
// If the machine has a display, hand it to the GUI program and
// stay up so there is something to look at. Otherwise we are a
// headless run and the polite thing is to power off.
match ramfb::Ramfb::init(640, 480) {
Some(fb) => {
println!("framebuffer {}x{} — running gui", fb.width, fb.height);
if let Err(e) = run_on(
include_bytes!(env!("LOON_GUI_IMAGE")),
Machine { fb: Some(fb) },
) {
println!("gui failed: {e}");
sbi::shutdown(true);
}
println!("gui up — close the window or ^A x to quit");
loop {
unsafe { core::arch::asm!("wfi") };
}
}
None => sbi::shutdown(false),
}
}
Err(e) => {
println!();
Expand All @@ -111,17 +132,47 @@ pub extern "C" fn kmain(hart: usize, dtb: usize) -> ! {

/// The machine, as the VM sees it. Effects that no Loon handler caught
/// arrive here, which is the only place in the system that touches hardware.
struct Machine;
struct Machine {
fb: Option<ramfb::Ramfb>,
}

impl eir::vm::Host for Machine {
fn write(&mut self, s: &str) {
print!("{s}");
}

fn ticks(&mut self) -> i64 {
let t: u64;
unsafe { core::arch::asm!("rdtime {}", out(reg) t) };
t as i64
now() as i64
}

fn fb(&mut self, op: &str, a: &[i64]) -> Result<Option<i64>, alloc::string::String> {
let Some(fb) = self.fb.as_mut() else {
return Err(alloc::format!(
"Fb.{op}: this machine has no framebuffer (boot with -device ramfb)"
));
};
let arg = |i: usize| -> Result<i64, alloc::string::String> {
a.get(i)
.copied()
.ok_or_else(|| alloc::format!("Fb.{op}: missing argument {i}"))
};
match op {
"width" => Ok(Some(fb.width as i64)),
"height" => Ok(Some(fb.height as i64)),
"clear" => {
fb.clear(arg(0)? as u32);
Ok(None)
}
"fill-rect" => {
fb.fill_rect(arg(0)?, arg(1)?, arg(2)?, arg(3)?, arg(4)? as u32);
Ok(None)
}
"present" => {
fb.present();
Ok(None)
}
_ => Err(alloc::format!("Fb.{op}: no such framebuffer operation")),
}
}
}

Expand All @@ -140,8 +191,11 @@ fn micros_since(start: u64) -> u64 {
}

fn run_init(image: &[u8]) -> Result<(), alloc::string::String> {
run_on(image, Machine { fb: None })
}

fn run_on(image: &[u8], mut machine: Machine) -> Result<(), alloc::string::String> {
let module = eir::decode::decode(image)?;
let mut machine = Machine;
let mut vm = eir::vm::Vm::new(&module, &mut machine).with_fuel(500_000_000);
vm.run()?;
Ok(())
Expand All @@ -152,7 +206,7 @@ fn run_init(image: &[u8]) -> Result<(), alloc::string::String> {
/// interpreter rather than the console.
fn run_bench_named(name: &str, image: &[u8]) -> Result<(), alloc::string::String> {
let module = eir::decode::decode(image)?;
let mut machine = Machine;
let mut machine = Machine { fb: None };
let mut vm = eir::vm::Vm::new(&module, &mut machine).with_fuel(2_000_000_000);

let t = now();
Expand Down
Loading
Loading