1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
|
// so the thing is right, revo isn't really that low level and we need to have a
// thin bridge to translate shit over. Its just to setup the VM, serial ports,
// heap shit like that.
const std = @import("std");
const revo = @import("revo");
// all the VM's output goes here, boot injects this at runtime so the
// bridge doesn't need to pull a serial port from its ass
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 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) 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) emit(last);
}
var total: usize = header.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 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| .{
.file_write_streaming = writeToSink(req.header, req.data, req.splat),
},
.file_read_streaming => .{ .file_read_streaming = error.InputOutput },
.device_io_control => .{ .device_io_control = -1 },
.net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } },
};
}
// 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 };
}
fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration {
return .{ .nanoseconds = 1_000_000 };
}
// 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 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;
}
fn ioUnlockStderr(_: ?*anyopaque) void {}
// start from std's all-failing vtable and override only what the VM touches.
const io_vtable: std.Io.VTable = blk: {
var v = std.Io.failing.vtable.*;
v.operate = ioOperate;
v.now = ioNow;
v.clockResolution = ioClockResolution;
v.sleep = ioSleep;
v.crashHandler = ioCrashHandler;
v.lockStderr = ioLockStderr;
v.unlockStderr = ioUnlockStderr;
break :blk v;
};
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 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;
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 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 };
}
fn dbgUnlockStderr(_: ?*anyopaque) void {
dbg_fw.interface.flush() catch {};
}
fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection {
return new;
}
const debug_io_vtable: std.Io.VTable = blk: {
var v = std.Io.failing.vtable.*;
v.operate = ioOperate;
v.now = ioNow;
v.clockResolution = ioClockResolution;
v.sleep = ioSleep;
v.crashHandler = ioCrashHandler;
v.lockStderr = dbgLockStderr;
v.unlockStderr = dbgUnlockStderr;
v.swapCancelProtection = dbgSwapCancel;
break :blk v;
};
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
const SinkWriter = struct {
interface: std.Io.Writer,
fn init() SinkWriter {
return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } };
}
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) emit(s);
total += s.len;
}
const last = data[data.len -| 1];
var i: usize = 0;
while (i < splat) : (i += 1) {
if (last.len > 0) emit(last);
}
total += last.len * splat;
return total;
}
fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {}
};
const HostResult = revo.baselib.host.HostResult;
const Data = revo.Value;
// 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.Value, _: *revo.VM) anyerror!HostResult {
const port = f64ToInt(u16, args[0].asNumOpt().?) orelse return HostResult.other("outb: port out of range");
const val = f64ToInt(u8, args[1].asNumOpt().?) 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.Value, _: *revo.VM) anyerror!HostResult {
const port = f64ToInt(u16, args[0].asNumOpt().?) 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.Value, 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.Value, _: *revo.VM) anyerror!HostResult {
return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0));
}
fn hostFbHeight(_: []const revo.Value, _: *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.Value, _: *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].asNumOpt().?) orelse return HostResult.other("fill_rect: bad x");
const y = f64ToInt(usize, args[1].asNumOpt().?) orelse return HostResult.other("fill_rect: bad y");
const w = f64ToInt(usize, args[2].asNumOpt().?) orelse return HostResult.other("fill_rect: bad w");
const h = f64ToInt(usize, args[3].asNumOpt().?) orelse return HostResult.other("fill_rect: bad h");
const color = f64ToInt(u32, args[4].asNumOpt().?) 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.Value, _: *revo.VM) anyerror!HostResult {
const fb = g_fb orelse return HostResult.data(Data.new.nil());
const dy = f64ToInt(usize, args[0].asNumOpt().?) orelse return HostResult.other("fb_scroll: bad dy");
const color = f64ToInt(u32, args[1].asNumOpt().?) 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());
}
pub const KernelOps = struct {
phys_to_virt: *const fn (u64) u64,
alloc_frame: *const fn () u64, // 0 on failure
create_addrspace: *const fn () u64, // pml4 phys, 0 on failure (raw 64-bit entry copy incl. possible NX bit -> not f64-representable)
run_process: *const fn (u64, u64, u64) void, // pml4, entry, ustack
serial_next: *const fn () i64, // next mirrored serial byte, or -1 if empty
elf_phys: u64,
elf_size: u64,
};
var g_kops: ?KernelOps = null;
// mem_read(phys, size) -> value: read 1/2/4/8 bytes of physical memory
fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
const phys = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_read: bad addr");
const size = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_read: bad size");
const v = ops.phys_to_virt(phys);
const val: u64 = switch (size) {
1 => @as(*const u8, @ptrFromInt(v)).*,
2 => @as(*align(1) const u16, @ptrFromInt(v)).*,
4 => @as(*align(1) const u32, @ptrFromInt(v)).*,
8 => @as(*align(1) const u64, @ptrFromInt(v)).*,
else => return HostResult.other("mem_read: size must be 1/2/4/8"),
};
return HostResult.data(Data.new.num(val));
}
// mem_copy(dst_phys, src_phys, len): raw copy between physical regions
fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
const dst = ops.phys_to_virt(f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_copy: bad dst"));
const src = ops.phys_to_virt(f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_copy: bad src"));
const len = f64ToInt(usize, args[2].asNumOpt().?) orelse return HostResult.other("mem_copy: bad len");
@memcpy(@as([*]u8, @ptrFromInt(dst))[0..len], @as([*]const u8, @ptrFromInt(src))[0..len]);
return HostResult.data(Data.new.nil());
}
// mem_zero(phys, len)
fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
const dst = ops.phys_to_virt(f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_zero: bad addr"));
const len = f64ToInt(usize, args[1].asNumOpt().?) orelse return HostResult.other("mem_zero: bad len");
@memset(@as([*]u8, @ptrFromInt(dst))[0..len], 0);
return HostResult.data(Data.new.nil());
}
// mem_write(phys, val, size): write 1/2/4/8 bytes of physical memory
fn hostMemWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
const phys = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_write: bad addr");
const val = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_write: bad val");
const size = f64ToInt(u64, args[2].asNumOpt().?) orelse return HostResult.other("mem_write: bad size");
const v = ops.phys_to_virt(phys);
switch (size) {
1 => @as(*u8, @ptrFromInt(v)).* = @truncate(val),
2 => @as(*align(1) u16, @ptrFromInt(v)).* = @truncate(val),
4 => @as(*align(1) u32, @ptrFromInt(v)).* = @truncate(val),
8 => @as(*align(1) u64, @ptrFromInt(v)).* = val,
else => return HostResult.other("mem_write: size must be 1/2/4/8"),
}
return HostResult.data(Data.new.nil());
}
// invlpg(virt): flush one page from the TLB after remapping it
fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const virt = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("invlpg: bad addr");
asm volatile ("invlpg (%[v])"
:
: [v] "r" (virt),
: .{ .memory = true });
return HostResult.data(Data.new.nil());
}
fn hostFrameAlloc(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
return HostResult.data(Data.new.num(ops.alloc_frame()));
}
fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
return HostResult.data(Data.new.num(ops.create_addrspace()));
}
fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
const as = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("proc_run: bad as");
const entry = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("proc_run: bad entry");
const ustack = f64ToInt(u64, args[2].asNumOpt().?) orelse return HostResult.other("proc_run: bad ustack");
ops.run_process(as, entry, ustack);
return HostResult.data(Data.new.nil());
}
// serial_next() -> byte or -1
fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
return HostResult.data(Data.new.num(ops.serial_next()));
}
fn hostElfPhys(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
return HostResult.data(Data.new.num(ops.elf_phys));
}
fn hostElfSize(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
const ops = g_kops orelse return HostResult.other("no kernel ops");
return HostResult.data(Data.new.num(ops.elf_size));
}
fn registerPrimitives(vm: *revo.VM) !void {
const define = revo.baselib.host.define;
const T = revo.baselib.host.ParamType;
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)));
// low-level: memory + address spaces + process launch (revo's ELF loader)
try vm.registerGlobal("mem_read", try vm.installHost("mem_read", define(&[_]T{ .number, .number }, hostMemRead)));
try vm.registerGlobal("mem_copy", try vm.installHost("mem_copy", define(&[_]T{ .number, .number, .number }, hostMemCopy)));
try vm.registerGlobal("mem_zero", try vm.installHost("mem_zero", define(&[_]T{ .number, .number }, hostMemZero)));
try vm.registerGlobal("mem_write", try vm.installHost("mem_write", define(&[_]T{ .number, .number, .number }, hostMemWrite)));
try vm.registerGlobal("invlpg", try vm.installHost("invlpg", define(&[_]T{.number}, hostInvlpg)));
try vm.registerGlobal("frame_alloc", try vm.installHost("frame_alloc", define(&[_]T{}, hostFrameAlloc)));
try vm.registerGlobal("as_create", try vm.installHost("as_create", define(&[_]T{}, hostAsCreate)));
try vm.registerGlobal("proc_run", try vm.installHost("proc_run", define(&[_]T{ .number, .number, .number }, hostProcRun)));
try vm.registerGlobal("serial_next", try vm.installHost("serial_next", define(&[_]T{}, hostSerialNext)));
try vm.registerGlobal("elf_phys", try vm.installHost("elf_phys", define(&[_]T{}, hostElfPhys)));
try vm.registerGlobal("elf_size", try vm.installHost("elf_size", define(&[_]T{}, hostElfSize)));
}
// the VM outlives boot() now, so we juts make this hoe static
var g_vm: ?*revo.VM = null;
// revo freestanding bug (fork @ fd0ff06): the module's very first top-level
// statement must not be a table literal, or module eval throws "want table, got
// atom" while building it. a leading no-op statement sidesteps it. remove once
// revo fixes the freestanding codegen upstream.
const init_program = "const _revel_boot = 0\n" ++
@embedFile("k_font") ++ "\n" ++
@embedFile("k_console") ++ "\n" ++
@embedFile("k_vmm") ++ "\n" ++
@embedFile("k_proc") ++ "\n" ++
@embedFile("k_shell") ++ "\n" ++
@embedFile("k_input") ++ "\n" ++
@embedFile("k_main");
pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void {
sink = out;
g_fb = fb;
g_kops = ops;
// NOTE: we drive the VM directly (compile -> run) instead of the high-level
// 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;
};
vm.* = VM.init(.{
.alloc = alloc,
.io = serial_io,
.argv = &.{},
.diag_alloc = alloc,
}) catch {
out("bridge: VM.init failed\r\n");
return;
};
out("bridge: revo VM up\r\n");
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 => |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;
},
};
// 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");
return;
};
const eval_result = revo.run.runBytecodeReport(vm, "kernel", artifact.instructions) catch {
out("bridge: eval threw\r\n");
return;
};
switch (eval_result) {
.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, 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.Value{revo.Value.new.num(scancode)}, null) catch {
sink("bridge: on_key threw\r\n");
};
flushConsole(vm);
}
// render any serial output produced while handling the key
fn flushConsole(vm: *revo.VM) void {
const f = vm.getGlobal("flush_console") orelse return;
if (f.asFunction() == null) return;
_ = vm.callFunctionParts(f, null, &.{}, null) catch {};
}
|