hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile40
-rw-r--r--README.md12
-rw-r--r--boot/boot.zig43
-rw-r--r--boot/idt.zig115
-rw-r--r--boot/keyboard.zig42
-rw-r--r--boot/pic.zig22
-rw-r--r--boot/port.zig25
-rw-r--r--boot/serial.zig18
-rw-r--r--bridge/bridge.zig243
-rw-r--r--build.zig13
-rw-r--r--build.zig.zon1
-rw-r--r--kernel/.gitkeep0
-rw-r--r--kernel/main.rv240
13 files changed, 691 insertions, 123 deletions
diff --git a/Makefile b/Makefile
index e732382..4d95142 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,8 @@
-# Revel — build the kernel, assemble a Limine ISO, boot it in QEMU.
-#
-# make build the ISO (default)
-# make run build and boot it headless (serial on stdout)
-# make kernel just compile the kernel ELF
-# make clean remove build output (kernel, ISO, caches)
-# make distclean also remove Limine's built host tool
+# make : build the ISO (default)
+# make run : build and boot it headless (serial on stdout)
+# make kernel : just compile the kernel ELF
+# make clean : remove build output (kernel, ISO, caches)
+# make distclean : also remove Limine's built host tool
ZIG ?= zig
QEMU ?= qemu-system-x86_64
@@ -22,26 +20,28 @@ kernel:
limine/limine:
$(MAKE) -C limine
+# WARNING: DO NOT FUCK W/ THE \ DELIMETERS THEY ARE MADE TO BE PRETTY
+# ON OUTPUT, NOT PRETTY IN CODE. IF YOU DONT LIKE IT, GO
+# SHOKE ON A TIT
iso: kernel limine/limine
rm -rf iso_root
mkdir -p iso_root/boot/limine iso_root/EFI/BOOT
- cp $(KERNEL) iso_root/boot/revel
- cp boot/limine.conf iso_root/boot/limine/
+ cp $(KERNEL) iso_root/boot/revel
+ cp boot/limine.conf iso_root/boot/limine/
cp limine/limine-bios.sys \
- limine/limine-bios-cd.bin \
- limine/limine-uefi-cd.bin iso_root/boot/limine/
- cp limine/BOOTX64.EFI \
- limine/BOOTIA32.EFI iso_root/EFI/BOOT/
+ limine/limine-bios-cd.bin \
+ limine/limine-uefi-cd.bin iso_root/boot/limine/
+ cp limine/BOOTX64.EFI \
+ limine/BOOTIA32.EFI iso_root/EFI/BOOT/
xorriso -as mkisofs -R -r -J \
- -b boot/limine/limine-bios-cd.bin \
- -no-emul-boot -boot-load-size 4 -boot-info-table \
- -hfsplus -apm-block-size 2048 \
- --efi-boot boot/limine/limine-uefi-cd.bin \
- -efi-boot-part --efi-boot-image --protective-msdos-label \
- iso_root -o $(ISO)
+ -b boot/limine/limine-bios-cd.bin \
+ -no-emul-boot -boot-load-size 4 -boot-info-table \
+ -hfsplus -apm-block-size 2048 \
+ --efi-boot boot/limine/limine-uefi-cd.bin \
+ -efi-boot-part --efi-boot-image --protective-msdos-label \
+ iso_root -o $(ISO)
./limine/limine bios-install $(ISO)
-# boot it in a real QEMU window. serial goes back later.
run: iso
$(QEMU) -M q35 -m 256M -cdrom $(ISO) -boot d -no-reboot -no-shutdown
diff --git a/README.md b/README.md
index ec7ca33..1c859ff 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,16 @@
# revel
A Unix-like kernel, written in revo!
-## License
+## Build/run
+you need shit to get it to work, just run it and install whatever it doesnt find.
+
+yes ill rework docs later
+
+god so needy
+```
+$ make run
+```
+
+## License
MIT, see [LICENSE](LICENSE).
diff --git a/boot/boot.zig b/boot/boot.zig
index 1f50d83..e6342ce 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -6,6 +6,7 @@ const gdt = @import("gdt.zig");
const idt = @import("idt.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
+const keyboard = @import("keyboard.zig");
const bridge = @import("bridge");
// We need a panic handler otherwise zig won't be happy,
@@ -52,8 +53,8 @@ export fn _start() callconv(.c) noreturn {
gdt.load();
idt.init();
-
-
+ // NOTE: CPU interrupts stay off until the event loop's sti
+ // which is after the VM has run that\
serial.write("gdt + idt loaded\r\n");
// HHDM + memory map -> physical frame allocator -> kernel heap
@@ -84,23 +85,29 @@ export fn _start() callconv(.c) noreturn {
pmm.free(b);
pmm.free(c);
- // hand the kernel heap to revo and we're gucci
- bridge.run(heap.allocator(), serial.write);
+ // hand the kernel heap + raw framebuffer to revo and bring up the VM
+ const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
+ .ptr = s.address,
+ .width = s.width,
+ .height = s.height,
+ .pitch = s.pitch,
+ } else null;
+ bridge.boot(heap.allocator(), serial.write, fb);
- if (framebuffer.get()) |screen| {
- // XOR texture
- var y: usize = 0;
- while (y < screen.height) : (y += 1) {
- var x: usize = 0;
- while (x < screen.width) : (x += 1) {
- const v: u32 = @intCast((x ^ y) & 0xFF);
- framebuffer.putpixel(screen, x, y, (v << 16) | (v << 8) | v);
- }
+ // - sleep until an interrupt
+ // - drain the keyboard
+ // - repeat.
+ //
+ // the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key
+ // arriving between "queue empty" and hlt would otherwise sit until
+ // the next keypress)
+ while (true) {
+ asm volatile ("cli");
+ if (keyboard.pop()) |sc| {
+ asm volatile ("sti");
+ bridge.onKey(sc);
+ } else {
+ asm volatile ("sti; hlt");
}
- serial.write("drew to the framebuffer\r\n");
- } else {
- serial.write("limine was a piece of shit.\r\n");
}
-
- while (true) asm volatile ("hlt");
}
diff --git a/boot/idt.zig b/boot/idt.zig
index 127fd8e..a9898d6 100644
--- a/boot/idt.zig
+++ b/boot/idt.zig
@@ -6,6 +6,8 @@
const gdt = @import("gdt.zig");
const serial = @import("serial.zig");
+const pic = @import("pic.zig");
+const keyboard = @import("keyboard.zig");
// 64bit interrupt gate
const Gate = packed struct {
@@ -26,7 +28,7 @@ const Idtr = packed struct {
var idt = [_]Gate{@bitCast(@as(u128, 0))} ** 256;
var idtr: Idtr = undefined;
-// what the common stub leaves on the stack, low address -> high.
+// what the common stub leaves on the stack, low address -> high.
// `mov rdi, rsp` hands the handler a pointer to this
const Frame = extern struct {
r15: u64,
@@ -55,17 +57,17 @@ const Frame = extern struct {
};
const names = [_][]const u8{
- "divide by zero", "debug",
- "non-maskable interrupt", "breakpoint",
- "overflow", "bound range exceeded",
- "invalid opcode", "device not available",
- "double fault", "coprocessor segment overrun",
- "invalid TSS", "segment not present",
- "stack-segment fault", "general protection fault",
- "page fault", "reserved",
- "x87 floating-point", "alignment check",
- "machine check", "SIMD floating-point",
- "virtualization", "control protection",
+ "divide by zero", "debug",
+ "non-maskable interrupt", "breakpoint",
+ "overflow", "bound range exceeded",
+ "invalid opcode", "device not available",
+ "double fault", "coprocessor segment overrun",
+ "invalid TSS", "segment not present",
+ "stack-segment fault", "general protection fault",
+ "page fault", "reserved",
+ "x87 floating-point", "alignment check",
+ "machine check", "SIMD floating-point",
+ "virtualization", "control protection",
};
fn name(vector: u64) []const u8 {
@@ -134,23 +136,95 @@ fn writeHex(value: u64) void {
export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn {
serial.write("\r\n!!! CPU DID AN OOPSIE: ");
serial.write(name(frame.vector));
- serial.write("\r\n vector="); writeHex(frame.vector);
- serial.write(" err="); writeHex(frame.error_code);
- serial.write("\r\n rip="); writeHex(frame.rip);
- serial.write(" cs="); writeHex(frame.cs);
- serial.write("\r\n rflags="); writeHex(frame.rflags);
- serial.write(" rsp="); writeHex(frame.rsp);
+ serial.write("\r\n vector=");
+ writeHex(frame.vector);
+ serial.write(" err=");
+ writeHex(frame.error_code);
+ serial.write("\r\n rip=");
+ writeHex(frame.rip);
+ serial.write(" cs=");
+ writeHex(frame.cs);
+ serial.write("\r\n rflags=");
+ writeHex(frame.rflags);
+ serial.write(" rsp=");
+ writeHex(frame.rsp);
// page faults stash the offending address in CR2
if (frame.vector == 14) {
const cr2 = asm volatile ("mov %%cr2, %[out]"
: [out] "=r" (-> u64),
);
- serial.write("\r\n cr2="); writeHex(cr2);
+ serial.write("\r\n cr2=");
+ writeHex(cr2);
}
serial.write("\r\n");
while (true) asm volatile ("hlt");
}
+// hardware IRQ.
+// unlike exceptions these must return so we acknowledge the PIC and iretq
+// back to whatever we interrupted. the stub mirrors the exception one (dummy
+// error code + vector for a uniform Frame) but jumps to the returning path.
+fn irqStub(comptime vector: u8) fn () callconv(.naked) void {
+ return struct {
+ fn entry() callconv(.naked) void {
+ asm volatile ("pushq $0"); // dummy error code, keeps Frame uniform
+ asm volatile ("pushq %[v]\n jmp irqCommon"
+ :
+ : [v] "i" (@as(u32, vector)),
+ );
+ }
+ }.entry;
+}
+
+// same register save as isrCommon, but afterwards we restore everything, drop
+// the pushed vector + error code, and iretq instead of halting.
+export fn irqCommon() callconv(.naked) void {
+ asm volatile (
+ \\ push %rax
+ \\ push %rbx
+ \\ push %rcx
+ \\ push %rdx
+ \\ push %rsi
+ \\ push %rdi
+ \\ push %rbp
+ \\ push %r8
+ \\ push %r9
+ \\ push %r10
+ \\ push %r11
+ \\ push %r12
+ \\ push %r13
+ \\ push %r14
+ \\ push %r15
+ \\ mov %rsp, %rdi
+ \\ call irqDispatch
+ \\ pop %r15
+ \\ pop %r14
+ \\ pop %r13
+ \\ pop %r12
+ \\ pop %r11
+ \\ pop %r10
+ \\ pop %r9
+ \\ pop %r8
+ \\ pop %rbp
+ \\ pop %rdi
+ \\ pop %rsi
+ \\ pop %rdx
+ \\ pop %rcx
+ \\ pop %rbx
+ \\ pop %rax
+ \\ add $16, %rsp
+ \\ iretq
+ );
+}
+
+export fn irqDispatch(frame: *Frame) callconv(.c) void {
+ switch (frame.vector) {
+ pic.MASTER_OFFSET + 1 => keyboard.onIrq(), // IRQ1: keyboard
+ else => {},
+ }
+ pic.eoi(@intCast(frame.vector - pic.MASTER_OFFSET));
+}
+
fn setGate(vector: u8, handler: u64) void {
idt[vector] = .{
.offset_low = @truncate(handler),
@@ -166,6 +240,9 @@ fn setGate(vector: u8, handler: u64) void {
// fill the exception vectors and load the IDT
pub fn init() void {
inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)));
+ // IRQ1 (keyboard) -> vector 0x21
+ // the PIC is remapped separately in boot.
+ setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)));
idtr = .{ .limit = @sizeOf(@TypeOf(idt)) - 1, .base = @intFromPtr(&idt) };
asm volatile ("lidt (%[idtr])"
:
diff --git a/boot/keyboard.zig b/boot/keyboard.zig
new file mode 100644
index 0000000..8e3a58f
--- /dev/null
+++ b/boot/keyboard.zig
@@ -0,0 +1,42 @@
+// top half of the PS/2 keyboard driver.
+// the IRQ1 handler calls onIrq and reads the raw scancode out
+// of the controller and drops it in a ring buffer. thats ALLLLLL it does
+// no decoding, no allocation, nothing that could (should) fault or reenter
+// the only reason this exists is because the interrupt handler is not a safe
+// spot to execute revo code and shit can break easily and i am NOT going to debug
+// it so its just better to do it like this.
+//
+// since there is just one producer (ISR) and one consumer (the event loop)
+// i dont need to make a lock, the ISR just moves the tail and the loop just
+// moves the head
+
+const port = @import("port.zig");
+
+const DATA_PORT: u16 = 0x60;
+const SIZE: usize = 256; // u8 indices wrap mod 256 for free
+
+var buf: [SIZE]u8 = undefined;
+var head: u8 = 0; // next slot the loop will read
+var tail: u8 = 0; // next slot the ISR will write
+
+// called from the IRQ1 handler. read the scancode (which also ACKs the
+// controller so it'll send the next one) and enqueue it, dropping it on the
+// floor if the buffer is somehow full.
+pub fn onIrq() void {
+ const code = port.inb(DATA_PORT);
+ const t: *volatile u8 = &tail;
+ const h: *volatile u8 = &head;
+ if (t.* +% 1 == h.*) return; // full, drop
+ buf[t.*] = code;
+ t.* +%= 1;
+}
+
+// called from the event loop. next queued scancode, or null if empty.
+pub fn pop() ?u8 {
+ const t: *volatile u8 = &tail;
+ const h: *volatile u8 = &head;
+ if (h.* == t.*) return null;
+ const code = buf[h.*];
+ h.* +%= 1;
+ return code;
+}
diff --git a/boot/pic.zig b/boot/pic.zig
new file mode 100644
index 0000000..0bec4a8
--- /dev/null
+++ b/boot/pic.zig
@@ -0,0 +1,22 @@
+// this is just a 8259 PIC thing in zig, kinda defines only the constants the
+// IDT needs cause the actual remapping and masking is done in the kernel level
+// which is pretty cool.EOI has to stay here cause it runs inside the ISR tho,
+// so somewhere the VM cant really do much.
+//
+// see: https://wiki.osdev.org/8259_PIC
+
+const port = @import("port.zig");
+
+const MASTER_CMD: u16 = 0x20;
+const SLAVE_CMD: u16 = 0xA0;
+const EOI: u8 = 0x20; // end-of-interrupt command
+
+pub const MASTER_OFFSET: u8 = 0x20; // IRQ0..7 -> vectors 0x20..0x27
+pub const SLAVE_OFFSET: u8 = 0x28; // IRQ8..15 -> vectors 0x28..0x2F
+
+// tell the PIC we're done. the slave's interrupts also need the master EOI'd
+// since they arrive via the cascade.
+pub fn eoi(irq: u4) void {
+ if (irq >= 8) port.outb(SLAVE_CMD, EOI);
+ port.outb(MASTER_CMD, EOI);
+}
diff --git a/boot/port.zig b/boot/port.zig
new file mode 100644
index 0000000..05668d3
--- /dev/null
+++ b/boot/port.zig
@@ -0,0 +1,25 @@
+// x86 port I/O
+//
+// these are pretty important cause all drivers which rely
+// on serial rely on these 2 instructions so
+
+pub inline fn outb(port: u16, val: u8) void {
+ asm volatile ("outb %[val], %[port]"
+ :
+ : [val] "{al}" (val),
+ [port] "N{dx}" (port),
+ );
+}
+
+pub inline fn inb(port: u16) u8 {
+ return asm volatile ("inb %[port], %[result]"
+ : [result] "={al}" (-> u8),
+ : [port] "N{dx}" (port),
+ );
+}
+
+// a throwaway write to an unused port, ~1us. some 8259s need a beat between
+// back-to-back command writes on slow hardware; harmless on QEMU.
+pub inline fn wait() void {
+ outb(0x80, 0);
+}
diff --git a/boot/serial.zig b/boot/serial.zig
index 2e8c8d0..fab0e66 100644
--- a/boot/serial.zig
+++ b/boot/serial.zig
@@ -1,6 +1,6 @@
-// This is a minimal 16550 UART driver for COM1, this is mainly going
-// to be used for debugging since its really simple to setup.
-// Fun fact: i was drunk when i wrote this
+// this is a minimal 16550 UART driver for COM1, this is mainly going
+// to be used for debugging since its really simple to setup.
+// Fun fact: i was drunk when i wrote this
//
// see: https://wiki.osdev.org/Serial_Ports
@@ -38,15 +38,23 @@ pub fn init() void {
outb(COM1 + 4, 0x0B); // RTS/DSR set
}
-// COM1+5 (bit 5) = transmit holding register and
+// COM1+5 (bit 5) = transmit holding register and
// spin until it's clear to send
fn putc(c: u8) void {
while (inb(COM1 + 5) & 0x20 == 0) {}
outb(COM1, c);
}
+// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n.
+// inject the \r ourselves, skipping it when the \n already has one so existing
+// "\r\n" strings don't turn into "\r\r\n".
pub fn write(s: []const u8) void {
- for (s) |c| putc(c);
+ var prev: u8 = 0;
+ for (s) |c| {
+ if (c == '\n' and prev != '\r') putc('\r');
+ putc(c);
+ prev = c;
+ }
}
// dump a u64 as decimal. handy for "usable: N MiB" type logging.
diff --git a/bridge/bridge.zig b/bridge/bridge.zig
index 136cfa9..0c934eb 100644
--- a/bridge/bridge.zig
+++ b/bridge/bridge.zig
@@ -11,32 +11,35 @@ pub const Sink = *const fn ([]const u8) void;
fn noopSink(_: []const u8) void {}
var sink: Sink = noopSink;
-// fan a multi-slice write payload out to the sink. mirrors the shape of std.Io's
-// file_write_streaming request: an optional header, then `data`, whose LAST slice
-// is repeated `splat` times. returns the total byte count written.
+// fan a multi-slice write payload out to the sink.
+// mirrors the shape of std.Io's file_write_streaming request so
+// an optional header, then the data whose LAST slice is repeated
+// splat times.
+// idk why zig chose these names, they are funny af.
+// NOTE: returns the total byte count written.
fn writeToSink(
header: []const u8,
data: []const []const u8,
splat: usize,
) std.Io.Operation.FileWriteStreaming.Result {
- if (header.len > 0) sink(header);
- for (data[0 .. data.len -| 1]) |slice| {
- if (slice.len > 0) sink(slice);
+ if (header.len > 0) emit(header);
+ for (data[0..data.len -| 1]) |slice| {
+ if (slice.len > 0) emit(slice);
}
const last = data[data.len -| 1];
var i: usize = 0;
while (i < splat) : (i += 1) {
- if (last.len > 0) sink(last);
+ if (last.len > 0) emit(last);
}
var total: usize = header.len;
- for (data[0 .. data.len -| 1]) |slice| total += slice.len;
+ for (data[0..data.len -| 1]) |slice| total += slice.len;
total += last.len * splat;
return total;
}
-// every file write (stdout, stderr, whatever) routes to the serial port; we
-// don't have a filesystem or multiple sinks yet, so there's nowhere else to go.
+// every file write (stdout, stderr, whatever) routes to the serial port but we
+// don't have a filesystem or multiple sinks yet, so there's nowhere else to go
fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result {
return switch (operation) {
.file_write_streaming => |req| .{
@@ -48,7 +51,9 @@ fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.
};
}
-// no timer wired up yet, so time stands still. fine until we have the PIT/TSC.
+// no timer wired up yet, so time stands still,
+// call me the time bender the way i fuck shit up
+// TODO: fine until we have the PIT/TSC
fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp {
return .{ .nanoseconds = 0 };
}
@@ -57,13 +62,13 @@ fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionErr
return .{ .nanoseconds = 1_000_000 };
}
-// the scheduler parks fibers itself, it never needs us to actually block.
+// the scheduler parks fibers itself, it never needs us to actually block
fn ioSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void {}
fn ioCrashHandler(_: ?*anyopaque) void {}
-// eval captures its own errors into a buffer, so this is never reached; return
-// Canceled rather than recursing into a stderr lock we can't provide.
+// eval captures its own errors into a buffer, so this is never reached lol
+// return Canceled rather than recursing into a stderr lock we can't provide
fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
return error.Canceled;
}
@@ -87,8 +92,8 @@ const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable };
// A second IO used only as std's debug IO (for std.debug.print, which revo hits
// on a few error paths). std's default debug IO is a threaded stderr writer that
-// doesn't exist on freestanding, and std.Io.failing's stderr path is literally
-// `unreachable` -- so we give std a real one that drains to the same sink. Unlike
+// doesn't exist on freestandin, and std.Io.failing's stderr path is literally
+// unreachable, so we give std a real one that drains to the same sink. Unlike
// serial_io above, its lockStderr must return an actual File.Writer (std turns a
// Canceled lock into a panic), and swapCancelProtection must not be unreachable.
var dbg_fw: std.Io.File.Writer = undefined;
@@ -96,8 +101,8 @@ var dbg_fw_buf: [256]u8 = undefined;
fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
// File.stderr() reads posix.STDERR_FILENO, which doesn't exist on
- // freestanding. Build the File by hand instead; ioOperate ignores the handle
- // and routes every write to the sink regardless.
+ // freestanding so we build the file by hand instead. ioOperate ignores
+ // the handle and routes every write to the sink regardless
const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } };
dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf);
return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color };
@@ -125,7 +130,7 @@ const debug_io_vtable: std.Io.VTable = blk: {
pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable };
// an unbuffered std.Io.Writer that drains straight to the sink, so we can render
-// revo's own error reports (which write to a std.Io.Writer) onto the console.
+// revo's own error reports (which write to a std.Io.Writer) onto the console
const SinkWriter = struct {
interface: std.Io.Writer,
@@ -136,14 +141,14 @@ const SinkWriter = struct {
fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
_ = w;
var total: usize = 0;
- for (data[0 .. data.len -| 1]) |s| {
- if (s.len > 0) sink(s);
+ for (data[0..data.len -| 1]) |s| {
+ if (s.len > 0) emit(s);
total += s.len;
}
const last = data[data.len -| 1];
var i: usize = 0;
while (i < splat) : (i += 1) {
- if (last.len > 0) sink(last);
+ if (last.len > 0) emit(last);
}
total += last.len * splat;
return total;
@@ -152,23 +157,147 @@ const SinkWriter = struct {
fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {}
};
-// bring up the VM on the kernel heap and run a revo program. this is the "hello
-// world" that proves the whole stack: revo compiled for bare metal, our
-// allocator feeding it, its stdout coming out our serial port.
-pub fn run(alloc: std.mem.Allocator, out: Sink) void {
+const HostResult = revo.std_lib.HostResult;
+const Data = revo.Data;
+
+// revo's only number type is f64 which is really fucking annoying
+fn f64ToInt(comptime T: type, n: f64) ?T {
+ if (!std.math.isFinite(n)) return null;
+ // go through @floatFromInt for the bounds: maxInt(u64) isn't representable
+ // as an f64 literal, so comparing against the comptime_int directly won't
+ // even compile bruh
+ const min_f: f64 = @floatFromInt(std.math.minInt(T));
+ const max_f: f64 = @floatFromInt(std.math.maxInt(T));
+ if (n < min_f or n > max_f) return null;
+ return @intFromFloat(n);
+}
+
+// pass shit to revo
+fn hostOutb(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const port = f64ToInt(u16, args[0].asNum().?) orelse return HostResult.other("outb: port out of range");
+ const val = f64ToInt(u8, args[1].asNum().?) orelse return HostResult.other("outb: value out of range");
+ asm volatile ("outb %[v], %[p]"
+ :
+ : [v] "{al}" (val),
+ [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostInb(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const port = f64ToInt(u16, args[0].asNum().?) orelse return HostResult.other("inb: port out of range");
+ const val = asm volatile ("inb %[p], %[r]"
+ : [r] "={al}" (-> u8),
+ : [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.num(val));
+}
+
+// pass shit to revo
+fn hostPuts(args: []const revo.Data, vm: *revo.VM) anyerror!HostResult {
+ const s = args[0].asString() orelse return HostResult.other("puts: not a string");
+ emit(vm.stringValue(s));
+ return HostResult.data(Data.new.nil());
+}
+
+// the actual framebuffer, the data layout feels pretty self explanatory
+pub const Fb = struct {
+ ptr: [*]u8,
+ width: usize,
+ height: usize,
+ pitch: usize,
+};
+var g_fb: ?Fb = null;
+
+fn hostFbWidth(_: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0));
+}
+
+fn hostFbHeight(_: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.height else 0));
+}
+
+// color is 0xRRGGBB in the usual Limine 32-bpp layout
+fn hostFillRect(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no-op
+ const x = f64ToInt(usize, args[0].asNum().?) orelse return HostResult.other("fill_rect: bad x");
+ const y = f64ToInt(usize, args[1].asNum().?) orelse return HostResult.other("fill_rect: bad y");
+ const w = f64ToInt(usize, args[2].asNum().?) orelse return HostResult.other("fill_rect: bad w");
+ const h = f64ToInt(usize, args[3].asNum().?) orelse return HostResult.other("fill_rect: bad h");
+ const color = f64ToInt(u32, args[4].asNum().?) orelse return HostResult.other("fill_rect: bad color");
+
+ var yy = y;
+ const y_end = @min(y + h, fb.height);
+ const x_end = @min(x + w, fb.width);
+ while (yy < y_end) : (yy += 1) {
+ const row = fb.ptr + yy * fb.pitch;
+ var xx = x;
+ while (xx < x_end) : (xx += 1) {
+ const px: *align(1) u32 = @ptrCast(row + xx * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// mirror everything to serial
+fn emit(bytes: []const u8) void {
+ sink(bytes);
+}
+
+// shift the whole framebuffer up dy pixels and clear the freed rows
+fn hostFbScroll(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil());
+ const dy = f64ToInt(usize, args[0].asNum().?) orelse return HostResult.other("fb_scroll: bad dy");
+ const color = f64ToInt(u32, args[1].asNum().?) orelse return HostResult.other("fb_scroll: bad color");
+ if (dy == 0 or dy >= fb.height) return HostResult.data(Data.new.nil());
+ const move_bytes = dy * fb.pitch;
+ const total = fb.height * fb.pitch;
+ std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]);
+ var y: usize = fb.height - dy;
+ while (y < fb.height) : (y += 1) {
+ const line = fb.ptr + y * fb.pitch;
+ var x: usize = 0;
+ while (x < fb.width) : (x += 1) {
+ const px: *align(1) u32 = @ptrCast(line + x * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// install the primitives as globals the revo program can call by name
+// idk how else we should be doing this, lung said he'll do stdlib stuff
+fn registerPrimitives(vm: *revo.VM) !void {
+ const define = revo.std_lib.define;
+ const T = revo.std_lib.TypeSpec;
+ try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb)));
+ try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb)));
+ try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts)));
+ try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth)));
+ try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight)));
+ try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect)));
+ try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll)));
+}
+
+// the VM outlives boot() now, so we juts make this hoe static
+var g_vm: ?*revo.VM = null;
+
+const init_program = @embedFile("kernel_main");
+
+pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb) void {
sink = out;
+ g_fb = fb;
// NOTE: we drive the VM directly (compile -> run) instead of the high-level
- // Runtime.eval wrapper. that wrapper has a latent type error in revo (it
- // names module.EvalResult, which doesn't exist) that only trips when it's
- // actually analyzed -- we're the first caller. this is the same low-level
- // path revo's own wasm entry uses, so it's the well-trodden one anyway.
+ // Runtime.eval wrapper, which has a latent type error in revo that only
+ // trips when analyzed. same low-level path revo's own wasm entry uses.
const VM = revo.VM;
const vm = alloc.create(VM) catch {
out("bridge: OOM creating VM\r\n");
return;
};
- defer alloc.destroy(vm);
vm.* = VM.init(.{
.alloc = alloc,
.io = serial_io,
@@ -178,38 +307,29 @@ pub fn run(alloc: std.mem.Allocator, out: Sink) void {
out("bridge: VM.init failed\r\n");
return;
};
- defer vm.deinit();
out("bridge: revo VM up\r\n");
- // a proper workout, not just a print: recursion (call frames + soft-float
- // arithmetic), string methods (real stdlib), tables, tuples, destructuring.
- const src =
- \\fn fib(n) do
- \\ if n < 2 n
- \\ else fib(n - 1) + fib(n - 2)
- \\end
- \\print("hello from revo")
- \\print("fib(10) =", fib(10))
- \\print("arith =", 1 + 2 * 3, 10 / 4)
- \\print("string =", "revo":upper(), "KERNEL":lower())
- \\const arr = {10, 20, 30}
- \\print("table =", arr, arr[0])
- \\const (a, b) = (3, 4)
- \\print("tuple =", a + b)
- ;
- const build_result = revo.lang.build(vm, .{ .name = "kernel", .text = src }, .{}) catch {
+ registerPrimitives(vm) catch {
+ out("bridge: registering primitives failed\r\n");
+ return;
+ };
+
+ const build_result = revo.lang.build(vm, .{ .name = "kernel", .text = init_program }, .{}) catch {
out("bridge: compile threw\r\n");
return;
};
const artifact = switch (build_result) {
.ok => |art| art,
- .err => {
- out("bridge: compile error\r\n");
+ .err => |failure| {
+ out("bridge: compile error: ");
+ var sw = SinkWriter.init();
+ revo.lang.renderError(alloc, &sw.interface, .{ .name = "kernel", .text = init_program }, failure) catch {};
+ out("\r\n");
return;
},
};
- defer alloc.free(artifact.instructions);
- defer alloc.free(artifact.spans);
+ // NOTE: on purpose we don't free artifact.instructions/spans cause on_key's
+ // bytecode lives in there and we call it for the life of the kernel
vm.setProgramDebugInfo(artifact.spans, "", "kernel") catch {
out("bridge: setProgramDebugInfo failed\r\n");
@@ -221,12 +341,25 @@ pub fn run(alloc: std.mem.Allocator, out: Sink) void {
return;
};
switch (eval_result) {
- .ok => out("\r\nbridge: eval ok\r\n"),
+ .ok => g_vm = vm, // handler is defined so hand the VM to the event loop
.err => |failure| {
out("\r\nbridge: runtime error: ");
var sw = SinkWriter.init();
- failure.render(alloc, &sw.interface, src) catch {};
+ failure.render(alloc, &sw.interface, init_program) catch {};
out("\r\n");
},
}
}
+
+// called from the boot event loop for each raw scancode
+// hand it to revo's on_key
+// runs in normal context (not the ISR), so allocation is fine here
+// i think
+pub fn onKey(scancode: u8) void {
+ const vm = g_vm orelse return;
+ const cb = vm.getGlobal("on_key") orelse return;
+ if (cb.asFunction() == null) return;
+ _ = vm.callFunctionParts(cb, null, &[_]revo.Data{revo.Data.new.num(scancode)}, null) catch {
+ sink("bridge: on_key threw\r\n");
+ };
+}
diff --git a/build.zig b/build.zig
index ea919b7..46322a7 100644
--- a/build.zig
+++ b/build.zig
@@ -6,10 +6,10 @@ pub fn build(b: *std.Build) void {
// f128/SSE ops, which i don't provide.
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "optimize mode") orelse .ReleaseSafe;
- // The kernel must NOT touch SSE/MMX/AVX until it has
- // enabled them itself, so I strip those features and
- // let the compiler use a soft-float ABI.
- // `code_model = .kernel` keeps relocations valid for
+ // The kernel must NOT touch SSE/MMX/AVX until it has
+ // enabled them itself, so I strip those features and
+ // let the compiler use a soft-float ABI
+ // `code_model = .kernel` keeps relocations valid for
// the higher-half load address in boot/linker.ld.
//
// In other words. Going raw baby
@@ -51,7 +51,7 @@ pub fn build(b: *std.Build) void {
var visited: std.AutoHashMap(*std.Build.Module, void) = .init(b.allocator);
defer visited.deinit();
kernelize(revo_mod, &visited);
-
+
// the thin guns
const bridge_mod = b.createModule(.{
.root_source_file = b.path("bridge/bridge.zig"),
@@ -63,6 +63,9 @@ pub fn build(b: *std.Build) void {
.link_libc = false,
});
bridge_mod.addImport("revo", revo_mod);
+ // the OS source lives in kernel/*.rv; @embedFile("kernel_main") in the bridge
+ // pulls it in at build time (an anonymous import lets it cross the module dir)
+ bridge_mod.addAnonymousImport("kernel_main", .{ .root_source_file = b.path("kernel/main.rv") });
kernel.root_module.addImport("bridge", bridge_mod);
diff --git a/build.zig.zon b/build.zig.zon
index 0a70ccb..114983a 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -11,5 +11,6 @@
"build.zig.zon",
"boot",
"bridge",
+ "kernel",
},
}
diff --git a/kernel/.gitkeep b/kernel/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/kernel/.gitkeep
+++ /dev/null
diff --git a/kernel/main.rv b/kernel/main.rv
new file mode 100644
index 0000000..8fe3b81
--- /dev/null
+++ b/kernel/main.rv
@@ -0,0 +1,240 @@
+# font8x8_basic (printable ASCII 0x20..0x7f), one row of 8 glyphs per line
+# each glyph is 8 bytes; bit 0 (LSB) of a byte is the leftmost pixel
+global FONT = {
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 },
+ { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 },
+ { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 },
+ { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 },
+ { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 },
+ { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 },
+ { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 },
+ { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 },
+ { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06 },
+ { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 },
+ { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 },
+ { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 },
+ { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 },
+ { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 },
+ { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 },
+ { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 },
+ { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 },
+ { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 },
+ { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 },
+ { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 },
+ { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 },
+ { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 },
+ { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06 },
+ { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 },
+ { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 },
+ { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 },
+ { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 },
+ { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 },
+ { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 },
+ { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 },
+ { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 },
+ { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 },
+ { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 },
+ { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 },
+ { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 },
+ { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 },
+ { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 },
+ { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 },
+ { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 },
+ { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 },
+ { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 },
+ { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 },
+ { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 },
+ { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 },
+ { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 },
+ { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 },
+ { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 },
+ { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 },
+ { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 },
+ { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 },
+ { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 },
+ { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 },
+ { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 },
+ { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 },
+ { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00 },
+ { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00 },
+ { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00 },
+ { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF },
+ { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 },
+ { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 },
+ { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 },
+ { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 },
+ { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 },
+ { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 },
+ { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F },
+ { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 },
+ { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 },
+ { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E },
+ { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 },
+ { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 },
+ { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 },
+ { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 },
+ { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 },
+ { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F },
+ { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 },
+ { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 },
+ { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 },
+ { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 },
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 },
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 },
+ { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 },
+ { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 },
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F },
+ { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 },
+ { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00 },
+ { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00 },
+ { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00 },
+ { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+}
+
+const SCALE = 2
+const CW = 8 * SCALE
+const CH = 8 * SCALE
+const FG = 0xd0d0d0
+const CON_BG = 0x101018
+global con = { col = 0, row = 0 }
+
+fn con_cols() fb_width() / CW
+fn con_rows() fb_height() / CH
+
+fn draw_glyph(col, row, ch) do
+ const x0 = col * CW
+ const y0 = row * CH
+ fill_rect(x0, y0, CW, CH, CON_BG) # clear the cell
+ const code = ch:ascii()
+ if code < 32 return :nil
+ if code > 127 return :nil
+ const g = FONT[code - 32]
+ for fy in 0..8 do
+ const bits = g[fy]
+ for fx in 0..8 do
+ if (bits shr fx) band 1 == 1 fill_rect(x0 + fx * SCALE, y0 + fy * SCALE, SCALE, SCALE, FG)
+ end
+ end
+end
+
+fn con_newline() do
+ con.col = 0
+ con.row = con.row + 1
+ if con.row >= con_rows() do
+ fb_scroll(CH, CON_BG)
+ con.row = con_rows() - 1
+ end
+end
+
+# we dont do Shift/Cntrl
+# thats a job for future me lmao
+fn con_putc(ch) do
+ const code = ch:ascii()
+ if code == 10 do con_newline() return :nil end # \n
+ if code == 13 do con.col = 0 return :nil end # \r
+ if code == 8 do # backspace: move left
+ if con.col > 0 con.col = con.col - 1
+ return :nil
+ end
+ if code < 32 return :nil
+ draw_glyph(con.col, con.row, ch)
+ con.col = con.col + 1
+ if con.col >= con_cols() con_newline()
+end
+
+# render a string to the screen and mirror to serial
+fn puts(s) do
+ serial_puts(s)
+ for i in 0..s:len() do con_putc(s[i]) end
+end
+
+# keyboard scancode table
+# ts is NOT a good way to write a driver LMAO
+const KBD = " 1234567890-= qwertyuiop[] asdfghjkl;'` \\zxcvbnm,./ "
+
+# 8259 PIC
+fn pic_remap() do
+ outb(0x20, 0x11) outb(0xa0, 0x11)
+ outb(0x21, 0x20) outb(0xa1, 0x28)
+ outb(0x21, 0x04) outb(0xa1, 0x02)
+ outb(0x21, 0x01) outb(0xa1, 0x01)
+ outb(0x21, 0xff) outb(0xa1, 0xff)
+end
+fn pic_unmask(line) outb(0x21, inb(0x21) band (255 bxor (1 shl line)))
+
+# this is just a simple prompt to test that shit is working
+fn cmd_help() puts("commands: help, clear, bars, echo <text>, time\n")
+
+fn cmd_clear() do
+ fill_rect(0, 0, fb_width(), fb_height(), CON_BG)
+ con.col = 0
+ con.row = 0
+end
+
+fn cmd_bars() do
+ const bars = { 0xff5555, 0xffaa00, 0xffff55, 0x55ff55, 0x55ffff, 0x5599ff, 0xaa55ff, 0xffffff }
+ const bw = fb_width() / 8
+ for i in 0..8 do fill_rect(i * bw, 0, bw, 40, bars[i]) end
+ puts("painted 8 bars from revo\n")
+end
+
+fn bcd(b) (b shr 4) * 10 + (b band 15)
+fn rtc(reg) do
+ outb(0x70, reg)
+ inb(0x71)
+end
+fn cmd_time() puts("utc " ~ bcd(rtc(4)) ~ ":" ~ bcd(rtc(2)) ~ ":" ~ bcd(rtc(0)) ~ "\n")
+
+fn run(line) do
+ const words = line:split(" ")
+ const cmd = words[0]
+ if cmd == "help" do cmd_help() end
+ else if cmd == "clear" do cmd_clear() end
+ else if cmd == "bars" do cmd_bars() end
+ else if cmd == "time" do cmd_time() end
+ else if cmd == "echo" do
+ if line:len() > 5 puts(line:sub(5, line:len() - 5) ~ "\n") else puts("\n")
+ end
+ else if line:len() == 0 do :nil end
+ else do puts("unknown command: " ~ cmd ~ "\n") end
+ puts("> ")
+end
+
+# she stroke my keys till i handle
+# no fr this is just a key stroke handler
+global sh = { line = "" }
+global on_key = fn(code) do
+ if code >= 128 return :nil # key release
+ if code == 28 do # Enter
+ puts("\n")
+ run(sh.line)
+ sh.line = ""
+ return :nil
+ end
+ if code == 14 do # Backspace
+ if sh.line:len() > 0 do
+ sh.line = sh.line:sub(0, sh.line:len() - 1)
+ puts("\x08 \x08")
+ end
+ return :nil
+ end
+ if code < 2 return :nil
+ if code >= KBD:len() return :nil
+ const ch = KBD[code]
+ sh.line = sh.line ~ ch
+ puts(ch)
+end
+
+# kmain
+pic_remap()
+pic_unmask(1)
+cmd_clear()
+puts("revel shell, live in revo. type `help`\n> ")