363 lines
9.1 KiB
JavaScript
363 lines
9.1 KiB
JavaScript
const TXBUF = 0x000;
|
|
const RXBUF = 0x080;
|
|
const TXHEAD = 0x100;
|
|
const TXTAIL = 0x104;
|
|
const RXHEAD = 0x108;
|
|
const RXTAIL = 0x10c;
|
|
|
|
const SYSREADY = 0x110;
|
|
const SYSINTER = 0x114;
|
|
|
|
const PERIPHS_SIZE = 0x200;
|
|
|
|
const POLL_INTERVAL_MS = 5;
|
|
const DOT_INTERVAL_MS = 25;
|
|
|
|
const COLS = 80;
|
|
const TAB_WIDTH = 8;
|
|
|
|
const CURSOR_IDLE_TIME_MS = 1000;
|
|
|
|
class Emulator {
|
|
constructor() {
|
|
this.mem = new WebAssembly.Memory({
|
|
initial: 1,
|
|
maximum: 1,
|
|
shared: true
|
|
});
|
|
this.mem_u8 = new Uint8Array(this.mem.buffer);
|
|
for (let i = 0; i < PERIPHS_SIZE; ++i)
|
|
this.mem_u8[i] = 0;
|
|
this.mem_i32 = new Int32Array(this.mem.buffer);
|
|
|
|
this.decoder = new TextDecoder('utf-8');
|
|
this.encoder = new TextEncoder('utf-8');
|
|
|
|
this.output = document.getElementById('output');
|
|
this.rx_queue = [];
|
|
this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
|
|
|
|
this.rows = this.max_rows();
|
|
this.grid = Array.from(
|
|
{ length: this.rows },
|
|
() => new Array(COLS).fill(' '));
|
|
this.cursor = { x: 0, y: 0 };
|
|
this.range = {
|
|
start: { x: 0, y: 0 },
|
|
end: { x: 0, y: 0 }
|
|
};
|
|
this.idle_timer = null;
|
|
this.input_enable = false;
|
|
document.addEventListener('keydown', (e) => this.handle_keydown(e));
|
|
window.addEventListener('resize', () => this.handle_resize());
|
|
|
|
this.forth = new Worker('boot.js', { type: 'module' });
|
|
|
|
this.print("Assembling kernel ");
|
|
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
|
|
this.forth.postMessage({ type: "load", mem: this.mem });
|
|
this.forth.onmessage = (e) => {
|
|
clearInterval(this.dots);
|
|
this.print(" done\n");
|
|
|
|
this.print("Loading prelude ");
|
|
this.forth.postMessage({ type: "boot" });
|
|
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
|
|
};
|
|
|
|
fetch('prelude.f')
|
|
.then(res => res.text())
|
|
.then(text => {
|
|
for (const cu of this.encoder.encode(text))
|
|
this.rx_queue.push(cu);
|
|
});
|
|
}
|
|
|
|
poll() {
|
|
const txhead = Atomics.load(this.mem_u8, TXHEAD);
|
|
const txtail = Atomics.load(this.mem_u8, TXTAIL);
|
|
if (txhead != txtail)
|
|
this.handle_tx_data(txhead, txtail);
|
|
|
|
if (this.rx_queue.length != 0) {
|
|
const rxhead = Atomics.load(this.mem_u8, RXHEAD);
|
|
const rxtail = Atomics.load(this.mem_u8, RXTAIL);
|
|
if (this.fifo_next(rxtail) != rxhead)
|
|
this.handle_rx_data(rxhead, rxtail);
|
|
}
|
|
|
|
if (!this.input_enable) {
|
|
const sysready = Atomics.load(this.mem_u8, SYSREADY);
|
|
if (sysready != 0) {
|
|
clearInterval(this.dots);
|
|
this.print(" done\n");
|
|
|
|
Atomics.store(this.mem_u8, SYSINTER, 1);
|
|
Atomics.notify(this.mem_i32, SYSINTER / 4);
|
|
|
|
this.input_enable = true;
|
|
this.blink = true;
|
|
this.flush_output();
|
|
}
|
|
}
|
|
}
|
|
|
|
fifo_next(idx) {
|
|
return (idx + 1) & 0x7f;
|
|
}
|
|
|
|
handle_tx_data(head, tail) {
|
|
const data = [];
|
|
do {
|
|
data.push(this.mem_u8[TXBUF + head]);
|
|
head = this.fifo_next(head);
|
|
} while (head != tail);
|
|
Atomics.store(this.mem_u8, TXHEAD, head);
|
|
this.print(this.decoder.decode(new Uint8Array(data)));
|
|
}
|
|
|
|
handle_rx_data(head, tail) {
|
|
do {
|
|
this.mem_u8[RXBUF + tail] = this.rx_queue.shift();
|
|
tail = this.fifo_next(tail);
|
|
} while (this.fifo_next(tail) != head && this.rx_queue.length != 0);
|
|
Atomics.store(this.mem_u8, RXTAIL, tail);
|
|
Atomics.notify(this.mem_i32, RXTAIL / 4);
|
|
}
|
|
|
|
print(str) {
|
|
for (const cp of str) {
|
|
switch (cp) {
|
|
case '\n':
|
|
do
|
|
this.print_character(' ');
|
|
while (this.range.start.x != 0);
|
|
break;
|
|
case '\t':
|
|
do
|
|
this.print_character(' ');
|
|
while (this.range.start.x % TAB_WIDTH != 0);
|
|
break;
|
|
default:
|
|
this.print_character(cp);
|
|
break;
|
|
}
|
|
}
|
|
this.flush_output();
|
|
}
|
|
|
|
print_character(c) {
|
|
this.shift_up(this.range.start);
|
|
this.grid[this.range.start.y][this.range.start.x] = c;
|
|
this.cursor = this.next_cell(this.cursor);
|
|
this.range.start = this.next_cell(this.range.start);
|
|
}
|
|
|
|
handle_keydown(e) {
|
|
if (!this.input_enable)
|
|
return;
|
|
|
|
if (e.key.length == 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
this.shift_up(this.cursor);
|
|
this.grid[this.cursor.y][this.cursor.x] = e.key;
|
|
this.cursor_move(1, 0);
|
|
} else if (e.key == 'Enter') {
|
|
this.cursor.y = this.range.end.y + 1;
|
|
while (this.cursor.y >= this.rows)
|
|
this.scroll();
|
|
this.cursor.x = 0;
|
|
this.submit_line();
|
|
Object.assign(this.range.start, this.cursor);
|
|
Object.assign(this.range.end, this.cursor);
|
|
} else if (e.key == 'ArrowLeft' || e.key == 'b' && e.ctrlKey) {
|
|
this.cursor_move(-1, 0);
|
|
} else if (e.key == 'ArrowRight' || e.key == 'f' && e.ctrlKey) {
|
|
this.cursor_move(1, 0);
|
|
} else if (e.key == 'ArrowUp' || e.key == 'p' && e.ctrlKey) {
|
|
this.cursor_move(0, -1);
|
|
} else if (e.key == 'ArrowDown' || e.key == 'n' && e.ctrlKey) {
|
|
this.cursor_move(0, 1);
|
|
} else if (e.key == 'Backspace') {
|
|
if (this.cursor_move(-1, 0))
|
|
this.shift_down(this.cursor);
|
|
} else if (e.key == 'Delete' || e.key == 'd' && e.ctrlKey) {
|
|
this.shift_down(this.cursor);
|
|
} else if (e.key == 'Home' || e.key == 'a' && e.ctrlKey) {
|
|
Object.assign(this.cursor, this.range.start);
|
|
} else if (e.key == 'End' || e.key == 'e' && e.ctrlKey) {
|
|
Object.assign(this.cursor, this.range.end);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
this.blink = false;
|
|
this.flush_output();
|
|
|
|
document.getElementById('cursor').classList.remove('blinking');
|
|
if (this.idle_timer)
|
|
clearTimeout(this.idle_timer);
|
|
this.idle_timer = setTimeout(() => {
|
|
this.blink = true;
|
|
document.getElementById('cursor').classList.add('blinking');
|
|
}, CURSOR_IDLE_TIME_MS);
|
|
}
|
|
|
|
submit_line() {
|
|
let line = '';
|
|
let cell = this.range.start;
|
|
while (cell.x != this.range.end.x || cell.y != this.range.end.y) {
|
|
line += this.grid[cell.y][cell.x];
|
|
cell = this.next_cell(cell);
|
|
}
|
|
line += '\n';
|
|
for (let cu of this.encoder.encode(line)) {
|
|
this.rx_queue.push(cu);
|
|
}
|
|
}
|
|
|
|
cursor_move(dx, dy) {
|
|
let x = this.cursor.x + dx;
|
|
let y = this.cursor.y + dy;
|
|
if (x < 0) {
|
|
--y;
|
|
x += COLS;
|
|
} else if (x >= COLS) {
|
|
++y;
|
|
x -= COLS;
|
|
}
|
|
|
|
if (this.range.start.y > y || this.range.end.y < y)
|
|
return false;
|
|
if (y == this.range.start.y && this.range.start.x > x)
|
|
return false;
|
|
if (y == this.range.end.y && this.range.end.x < x)
|
|
return false;
|
|
|
|
this.cursor.x = x;
|
|
this.cursor.y = y;
|
|
return true;
|
|
}
|
|
|
|
prev_cell(cell) {
|
|
if (cell.x > 0)
|
|
return { x: cell.x - 1, y: cell.y };
|
|
else if (cell.y > 0)
|
|
return { x: COLS - 1, y: cell.y - 1 };
|
|
else
|
|
return null;
|
|
}
|
|
|
|
next_cell(cell) {
|
|
if (cell.x < COLS - 1)
|
|
return { x: cell.x + 1, y: cell.y };
|
|
else if (cell.y < this.rows - 1)
|
|
return { x: 0, y: cell.y + 1 };
|
|
else
|
|
return null;
|
|
}
|
|
|
|
shift_up(start) {
|
|
let cell = { x: this.range.end.x, y: this.range.end.y };
|
|
while (cell.x != start.x || cell.y != start.y) {
|
|
let prev = this.prev_cell(cell);
|
|
this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x];
|
|
cell = prev;
|
|
}
|
|
if (this.next_cell(this.range.end) == null)
|
|
this.scroll();
|
|
this.range.end = this.next_cell(this.range.end);
|
|
}
|
|
|
|
shift_down(start) {
|
|
let cell = { x: start.x, y: start.y };
|
|
while (cell.x != this.range.end.x || cell.y != this.range.end.y) {
|
|
let next = this.next_cell(cell);
|
|
this.grid[cell.y][cell.x] = this.grid[next.y][next.x];
|
|
cell = next;
|
|
}
|
|
this.grid[this.range.end.y][this.range.end.x] = ' ';
|
|
this.range.end = this.prev_cell(this.range.end);
|
|
}
|
|
|
|
flush_output() {
|
|
const html = this.grid.map((row, y) => {
|
|
return row.map((c, x) => {
|
|
const ec = this.html_escape(c);
|
|
if (this.input_enable
|
|
&& x == this.cursor.x && y == this.cursor.y) {
|
|
const cl = this.blink ? 'class="blinking"' : '';
|
|
return `<span id="cursor" ${cl}>` + ec + '</span>';
|
|
} else {
|
|
return ec;
|
|
}
|
|
}).join('').trimEnd();
|
|
}).join('\n');
|
|
this.output.innerHTML = html;
|
|
}
|
|
|
|
html_escape(c) {
|
|
switch (c) {
|
|
case '"':
|
|
return '"';
|
|
case "'":
|
|
return ''';
|
|
case '&':
|
|
return '&';
|
|
case '<':
|
|
return '<';
|
|
case '>':
|
|
return '>';
|
|
default:
|
|
return c;
|
|
}
|
|
}
|
|
|
|
scroll() {
|
|
this.grid.shift()
|
|
this.grid.push(new Array(COLS).fill(' '));
|
|
this.cursor.y -= 1;
|
|
this.range.start.y -= 1;
|
|
this.range.end.y -= 1;
|
|
}
|
|
|
|
max_rows() {
|
|
const style = getComputedStyle(this.output);
|
|
const line_height = parseFloat(style.lineHeight);
|
|
const margin_top = parseFloat(style.marginTop);
|
|
const margin_bottom = parseFloat(style.marginBottom);
|
|
|
|
const viewport_height = window.innerHeight;
|
|
const output_height = viewport_height - margin_top - margin_bottom;
|
|
return Math.floor(output_height / line_height) - 1;
|
|
}
|
|
|
|
handle_resize() {
|
|
this.rows = this.max_rows();
|
|
while (this.grid.length < this.rows)
|
|
this.grid.push(new Array(COLS).fill(' '));
|
|
while (this.grid.length > this.rows) {
|
|
this.grid.shift()
|
|
this.cursor.y -= 1;
|
|
this.range.start.y -= 1;
|
|
this.range.end.y -= 1;
|
|
}
|
|
this.flush_output();
|
|
}
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const output = document.getElementById('output');
|
|
output.innerText = '';
|
|
|
|
if (!self.crossOriginIsolated) {
|
|
output.innerText = "Yeah so there's this thing where Chromium ends "
|
|
+ "up ignoring COOP/COEP\nheaders after a hard reload "
|
|
+ "sometimes, and I haven't been able to\nfigure out how to "
|
|
+ "work around it yet. If you just wait a little while\nand "
|
|
+ "then reload normally then hopefully it should work haha";
|
|
} else {
|
|
window.emu = new Emulator();
|
|
}
|
|
});
|