Files
wipforth/emu.js
Camden Dixie O'Brien 2a3949e09f Make terminal xHEAD and xTAIL registers 32 bits
This enables waiting on them with memory.atomic.wait32 (there is no
wait8) which is needed to avoid spinning when waiting for a key.
2026-03-02 18:51:42 +00:00

329 lines
8.0 KiB
JavaScript

const TXBUF = 0x00;
const RXBUF = 0x20;
const TXHEAD = 0x40;
const TXTAIL = 0x44;
const RXHEAD = 0x48;
const RXTAIL = 0x4c;
const SYSREADY = 0x50;
const TXBUF_SIZE = 32;
const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const POLL_INTERVAL_MS = 20;
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.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.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
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) {
this.input_enable = true;
this.flush_output();
document.getElementById('cursor').classList.add('blinking');
}
}
}
fifo_next(idx) {
return (idx + 1) & 0x1f;
}
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);
}
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.flush_output();
document.getElementById('cursor').classList.remove('blinking');
if (this.idle_timer)
clearTimeout(this.idle_timer);
this.idle_timer = setTimeout(() => {
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)
return '<span id="cursor">' + 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', () => {
document.getElementById('output').innerText = '';
window.emu = new Emulator();
});