Files
wipforth/emu.js

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 '&quot;';
case "'":
return '&apos;';
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
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();
}
});