diff options
| author | hachem <im@hachem.wtf> | 2026-09-12 10:59:26 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-12 10:59:26 +0200 |
| commit | d6d908e3386e107b41241e9eda7dc546581524df (patch) | |
| tree | 8219d02a7494152722ac7f32140d63c8a3b3daa7 | |
| parent | 4f540af34d7a88547d60bed7bd28850e6d0e18ab (diff) | |
feat: basic keyboard driver + tester shell
| -rw-r--r-- | Makefile | 40 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | boot/boot.zig | 43 | ||||
| -rw-r--r-- | boot/idt.zig | 115 | ||||
| -rw-r--r-- | boot/keyboard.zig | 42 | ||||
| -rw-r--r-- | boot/pic.zig | 22 | ||||
| -rw-r--r-- | boot/port.zig | 25 | ||||
| -rw-r--r-- | boot/serial.zig | 18 | ||||
| -rw-r--r-- | bridge/bridge.zig | 243 | ||||
| -rw-r--r-- | build.zig | 13 | ||||
| -rw-r--r-- | build.zig.zon | 1 | ||||
| -rw-r--r-- | kernel/.gitkeep | 0 | ||||
| -rw-r--r-- | kernel/main.rv | 240 |
13 files changed, 691 insertions, 123 deletions
@@ -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 @@ -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"); + }; +} @@ -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> ") |
