summaryrefslogtreecommitdiff
path: root/bedrock.js
diff options
context:
space:
mode:
authorBen Bridle <ben@derelict.engineering>2025-09-12 17:20:19 +1200
committerBen Bridle <ben@derelict.engineering>2025-09-12 17:28:44 +1200
commit19c5679b8be4fe879e165576e97f96b85f5b5044 (patch)
treede10dbc9a23514adbddfd563d38b1238dfce6241 /bedrock.js
parent4c67a10f265dde1c3aa430cb851062a38646d6f2 (diff)
downloadbedrock-js-19c5679b8be4fe879e165576e97f96b85f5b5044.zip
Implement a faster core in WebAssembly
This is a massive commit that restructures a lot of the library. The primary change is the implementation of a second Bedrock core using WebAssembly, which performs much better than the existing JavaScript core. The JavaScript core has been retained as a fallback for browsers that don't support WebAssembly. Benchmarking both cores using the numbers benchmark and with all of the devices stubbed out in the emulator demonstrates a 40x speedup for the WebAssembly implementation (going from 4800ms to 120ms).
Diffstat (limited to 'bedrock.js')
-rw-r--r--bedrock.js2211
1 files changed, 1244 insertions, 967 deletions
diff --git a/bedrock.js b/bedrock.js
index 0022346..2130283 100644
--- a/bedrock.js
+++ b/bedrock.js
@@ -3,52 +3,138 @@
let systemName = "bedrock-js";
let systemVersion = "1.0.1";
let systemAuthors = "Ben Bridle";
-let initialScreenScale = 2;
+let defaultToWasm = true; // default to using WASM core for emulators
+let maxFrameTime = 500; // milliseconds before a render is forced
+let initialScreenScale = 2; // screen pixel scale
+let tabSize = 2; // width of tabs in assembler
+let maxTransmissions = 100; // maximum number of transmissions to retain
-// Upgrade every pre.bedrock element to a full assembler, and every
-// bedrock to a full emulator.
-window.addEventListener('DOMContentLoaded', function() {
- injectStyles();
+// Number of emulator cycles to run between updates. This value massively affects
+// performance: if it's too high, the UI will lag, and if it's too low, the
+// emulator will run slow. We must take the middle way.
+// TODO: Find a way to dynamically calculate the optimal value at runtime,
+// based on time taken to run each batch.
+let cyclesPerBatch = defaultToWasm ? 800000 : 50000 ;
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: WEBASSEMBLY MODULE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+// WebAssembly module bytecode embedded as a base64 string.
+let wasmBytecode = Uint8Array.from(atob(
+'AGFzbQEAAAABIAdgAX8Bf2ACf38Bf2AAAGAAAX5gAAF/YAF/AGACf38AAiECB2JlZHJvY2sFZGdldDEAAAdiZWRyb2NrBWRzZXQxAAEDJCMCAwQEBAUFBQUFBQQEBAQEBAQEBAQAAAAGBgEFBQUBAQEB' +
+'AAUDAQACBmMSfgFCAAt/AUEAC38BQYCCBAt/AUGAhAQLfwBBAAt/AEGAggQLfwBBgIQEC38AQQALfwBBAQt/AEECC38AQQMLfwBBBAt/AEEFC38AQQYLfwBBBwt/AEEIC38AQQkLfwBBCgsHkQIkBm1l' +
+'bW9yeQIABXJlc2V0AAICY2MAAwJpcAAEAndwAAUCcnAABgV3cHNoMQAHBXdwc2gyAAgFcnBzaDEACQVycHNoMgAKBXdwc2hiAAsFcnBzaGIADAV3cG9wMQANBXdwb3AyAA4FcnBvcDEADwVycG9wMgAQ' +
+'BW1wb3AxABEFbXBvcDIAEgV3Z2V0MQATBXdnZXQyABQFcmdldDEAFQVyZ2V0MgAWBW1nZXQxABcFbWdldDIAGAVkZ2V0MgAZBW1zZXQxABoFbXNldDIAGwVkc2V0MgAcBWlwbW92AB0Fd3Btb3YAHgVy' +
+'cG1vdgAfBHJvbDEAIARyb3IxACEEcm9sMgAiBHJvcjIAIwRldmFsACQK6CMjDgAjBCQBIwUkAiMGJAMLBAAjAAsEACMBCwcAIwUjAmsLBwAjBiMDawsQACMCQQFrJAIjAiAAOgAACxAAIwJBAmskAiMC' +
+'IAA7AQALEAAjA0EBayQDIwMgADoAAAsQACMDQQJrJAMjAyAAOwEACwwAQf8BQQAgABsQBwsMAEH/AUEAIAAbEAkLDgAjAi0AACMCQQFqJAILDgAjAi8BACMCQQJqJAILDgAjAy0AACMDQQFqJAMLDgAj' +
+'Ay8BACMDQQJqJAMLDgAjAS0AACMBQQFqJAELGgAjAS0AAEEIdCMBQQFqLQAAciMBQQJqJAELBwAjAi0AAAsHACMCLwEACwcAIwMtAAALBwAjAy8BAAsHACAALQAACxMAIAAtAABBCHQgAEEBai0AAHIL' +
+'EQAgABAAQQh0IABBAWoQAHILCQAgACABOgAACxoAIAAgAUEIdjoAACAAQQFqIAFB/wFxOgAACxkAIAAgAUEIdhABIABBAWogAUH/AXEQAXILCQAjASAAaiQBCwkAIwIgAGskAgsJACMDIABrJAMLFwAg' +
+'AUEHcSEBIAAgAXQgAEEIIAFrdnILFwAgAUEHcSEBIABBCCABa3QgACABdnILFwAgAUEPcSEBIAAgAXQgAEEQIAFrdnILFwAgAUEPcSEBIABBECABa3QgACABdnIL7R8CAX4BfyMAIACtfCEBA0ACQCAB' +
+'IwBRBEAMAQsjAEIBfCQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' +
+'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' +
+'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' +
+'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' +
+'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAQEQ7/AQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQl' +
+'JicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsB' +
+'jAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQB' +
+'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'), c => c.charCodeAt(0));
+
+// WebAssembly module compiled and ready to be instantiated.
+let wasmModule;
+
+async function loadWasmModule() {
+ // // Uncomment this to load the WebAssembly module bytecode from a separate file.
+ // wasmBytecode = await fetch('/bedrock.wasm')
+ // .then((r) => { return r.ok ? r.arrayBuffer() : null })
+ // .then((a) => { return new Uint8Array(a) })
+ // .catch(() => { return null });
+ wasmModule = await WebAssembly.compile(wasmBytecode).then((m) => m);
+}
+
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: ASSEMBLER AND EMULATOR INSTANTIATION :::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+// Automatically upgrade all eligible elements to assemblers or emulators.
+window.addEventListener('DOMContentLoaded', ()=>{
+ // Attempt to load and compile the WebAssembly module, then start upgrading.
+ loadWasmModule().finally(upgradeAll);
+})
+
+
+// Upgrade every `pre.bedrock`/`bedrock` element to an assembler/emulator.
+function upgradeAll() {
+ // Upgrade every `pre.bedrock` element to an assembler.
for (let element of document.querySelectorAll('pre.bedrock')) {
- // Click on the element to upgrade to an assembler.
+ // Defer the upgrade until the element is clicked.
element.style.cursor = 'pointer';
- element.addEventListener('click', function(e) {
+ element.addEventListener('click', (e)=>{
e.preventDefault();
upgradeToAssembler(element);
})
- // Prevent text selection on the element.
- element.addEventListener('selectstart', function(e) {
+ // Prevent the click from selecting any text.
+ element.addEventListener('selectstart', (e)=>{
e.preventDefault();
})
}
+ // Upgrade every `bedrock` element to an emulator.
for (let element of document.querySelectorAll('bedrock')) {
upgradeToEmulator(element);
}
-})
-
-
-// Insert the styles for the assembler and emulator into the page.
-function injectStyles() {
+ // Insert the CSS for the assembler and emulator into the page head.
let css = document.createElement('style');
- css.textContent = bedrockStyles;
+ css.textContent = bedrockCSS;
document.head.appendChild(css);
}
-// Upgrade a text element to a full assembler.
+// Upgrade a text-containing element to a full assembler.
function upgradeToAssembler(element) {
let assembler = new AssemblerElement(element);
// HACK 2: Track the top of the original element relative to the viewport.
let topStart = element.getBoundingClientRect().top;
let scrollStart = window.scrollY;
-
// HACK 1: Insert a fake spacer element following the original element to
// prevent the page from jumping when the original element is replaced.
- // Spacer ensures that there is at least element height below the
- // viewport, filling the gap created while element is being replaced.
+ // Spacer ensures that there is at minimum an element-sized span below
+ // the viewport, filling the gap created when the element momentarily
+ // disappears while being replaced.
let windowHeight = document.documentElement.clientHeight;
let documentHeight = document.documentElement.scrollHeight;
let belowViewport = documentHeight - (window.scrollY + windowHeight);
@@ -58,18 +144,17 @@ function upgradeToAssembler(element) {
spacer.style.height = spacerHeight + 'px';
element.parentElement.insertBefore(spacer, element.nextSibling);
- element.replaceWith(assembler);
+ element.replaceWith(assembler.el);
// HACK 1: Remove the spacer after a short delay.
setTimeout(()=>spacer.remove());
-
// HACK 2: Check if the page jumped as the assembler was inserted.
- let topEnd = assembler.getBoundingClientRect().top;
+ let topEnd = assembler.el.getBoundingClientRect().top;
if (topStart != topEnd) {
window.scrollTo({top: scrollStart, behavior: 'instant'});
// Wait for the document layout to settle.
setTimeout(() => {
- topEnd = assembler.getBoundingClientRect().top;
+ topEnd = assembler.el.getBoundingClientRect().top;
let topTarget = (topEnd - topStart) + window.scrollY;
window.scrollTo({top: topTarget, behavior: 'instant'});
});
@@ -79,708 +164,47 @@ function upgradeToAssembler(element) {
}
-// Upgrade a bedrock element with attributes to a full emulator.
+// Upgrade a bedrock element to a full emulator. Supported attributes are:
+// src: URL to the Bedrock program to run
+// controls: if present, show frame with controls around emulator
+// nocursor: if present, hide the regular mouse cursor when hovering
+// scale: initial screen scale factor (1-10)
function upgradeToEmulator(element) {
+ // Extract attribute values.
let attr = element.attributes;
- if (!attr.src || !attr.src.value) {
- console.error("No valid src attribute provided for <bedrock> element");
- return;
- }
- let scale = 1;
- try { scale = clamp(parseInt(attr.scale.value), 1, 10) } catch {}
- let options = {
- scale,
- controls: 'controls' in attr,
- nocursor: 'nocursor' in attr,
- autoplay: true,
- };
- let url = URL.parse(attr.src.value, window.location.href);
- if (!url) {
- console.error(`Invalid src attribute ${attr.src.value} provided for <bedrock> element`);
- return;
- }
- let request = new XMLHttpRequest();
- request.open("GET", url, true);
- request.responseType = "arraybuffer";
- request.onreadystatechange = function() {
- if (request.readyState == 4) {
- if (request.status == 200) {
- options.bytecode = new Uint8Array(request.response);
- let emulator = new EmulatorElement(options);
- element.replaceWith(emulator);
- } else {
- console.error(`Could not load Bedrock program from ${url.href}`);
- }
- }
- }
- request.send();
-}
-
-
-// ----------------------------------------------------------------------------------------------- +
-// :::::: ELEMENTS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
-// ----------------------------------------------------------------------------------------------- +
-
-
-// Constructor for an interactive assembler element.
-function AssemblerElement(element) {
- let assembler = elementFromHTML(assemblerTemplate);
- let textarea = assembler.querySelector('textarea');
- let viewer = assembler.querySelector('.viewer');
- let editor = assembler.querySelector('.editor');
- let errorPanel = assembler.querySelector('.panel.errors');
- let bytecodePanel = assembler.querySelector('.panel.bytecode');
- let status = assembler.querySelector('.status');
- let checkButton = assembler.querySelector('button[name="check"]');
- let runButton = assembler.querySelector('button[name="run"]');
- let fullscreenButton = assembler.querySelector('button[name="fullscreen"]');
- assembler.autoGrow = true;
- assembler.emulator = null;
-
-
- // Show and hide the error and bytecode panels.
- assembler.showErrorPanel = function() {
- errorPanel.classList.remove('hidden'); }
- assembler.hideErrorPanel = function() {
- errorPanel.classList.add('hidden'); }
- assembler.showBytecodePanel = function() {
- bytecodePanel.classList.remove('hidden'); }
- assembler.hideBytecodePanel = function() {
- bytecodePanel.classList.add('hidden'); }
-
-
- // Place focus on the editor.
- assembler.focus = function() {
- textarea.focus();
- }
-
- // Lock the viewer to the textarea by syncing their scroll positions.
- assembler.syncScroll = function() {
- viewer.scrollTop = textarea.scrollTop;
- viewer.scrollLeft = textarea.scrollLeft;
- }
-
- // Lock the viewer to the textarea by syncing their sizes.
- assembler.syncSize = function() {
- // Disable auto-grow if editor was made smaller by the user.
- if (textarea.clientHeight < viewer.clientHeight) {
- assembler.autoGrow = false; }
- viewer.style.height = textarea.clientHeight + 'px';
- editor.style.height = textarea.clientHeight + 'px';
- }
-
- // Copy text from textarea to viewer with syntax highlighting.
- assembler.renderText = function() {
- // Force viewer to show trailing newlines.
- let text = textarea.value;
- if (text.slice(-1) == "\n") text += ' ';
- // Replace contents of viewer.
- viewer.innerHTML = '';
- for (let child of highlightSource(text)) {
- viewer.appendChild(child); }
- // Auto-grow the text area as text is entered.
- if (assembler.autoGrow && textarea.scrollHeight > textarea.clientHeight) {
- textarea.style.height = textarea.scrollHeight + 'px'; }
- // Lock viewer to textarea.
- assembler.syncScroll();
- }
-
- // Assemble the program and show bytecode or errors.
- assembler.checkProgram = function() {
- let { bytecode, symbols, errors } = assembleProgram(textarea.value);
- errorPanel.innerHTML = '';
- bytecodePanel.innerHTML = '';
- if (errors.length) {
- let list = document.createElement('ul');
- for (let error of errors) {
- let item = document.createElement('li');
- let line = error.start.line + 1;
- let column = error.start.column + 1;
- item.textContent = `[${line}:${column}] ${error.text}`;
- list.appendChild(item); }
- errorPanel.appendChild(list);
- let unit = errors.length == 1 ? 'error' : 'errors';
- status.textContent = `${errors.length} ${unit}`;
- assembler.hideBytecodePanel();
- assembler.showErrorPanel();
- } else {
- let unit = bytecode.length == 1 ? 'byte' : 'bytes';
- status.textContent = `no errors (${bytecode.length} ${unit})`;
- let hexString = '';
- for (let byte of bytecode) hexString += hex(byte, 2) + ' ';
- let programListing = document.createElement('div');
- programListing.textContent = hexString;
- bytecodePanel.appendChild(programListing);
- assembler.hideErrorPanel();
- assembler.hideBytecodePanel();
- if (bytecode.length) assembler.showBytecodePanel();
- return { bytecode, symbols };
- }
- }
-
- assembler.runProgram = function() {
- // Create an emulator if none exists.
- if (!assembler.emulator) {
- assembler.emulator = new EmulatorElement();
- assembler.emulator.assembler = assembler;
- assembler.parentElement.insertBefore(assembler.emulator, assembler.nextSibling); }
- let program = assembler.checkProgram();
- if (program) {
- assembler.hideBytecodePanel();
- let { bytecode, symbols } = program;
- assembler.emulator.showStatePanel();
- assembler.emulator.startProgram(bytecode, symbols);
- }
- }
-
- // Change the fullscreen state of the whole assembler.
- assembler.toggleFullscreen = function() {
- if (document.fullscreenElement != assembler) {
- assembler.requestFullscreen();
- } else {
- document.exitFullscreen();
- }
- }
-
- // Handle keypresses on the whole assembler.
- assembler.onKeyPress = function(event) {
- if (event.key == "Escape") {
- event.preventDefault();
- assembler.hideErrorPanel();
- assembler.hideBytecodePanel();
- } else if (event.key == "s" && event.ctrlKey) {
- event.preventDefault();
- assembler.checkProgram();
- }
- }
-
- // Handle keypresses on just the editor.
- assembler.onEditorKeyPress = function(event) {
- let size = 2; let indent = ' ';
- let text = textarea.value;
- let selectionStart = textarea.selectionStart;
- let selectionEnd = textarea.selectionEnd;
-
- // Find the character index following the previous newline character.
- function findLineStart(i) {
- while (i>0 && text[i-1] != '\n') i -= 1;
- return i; }
-
- if (event.key == "Enter" && event.ctrlKey) {
- event.preventDefault();
- assembler.runProgram();
- } else if (event.key == "Tab") {
- event.preventDefault();
- // Find the start index of every line in the selection.
- let i = findLineStart(selectionStart);
- let lineStarts = [i];
- for (; i<selectionEnd; i++) if (text[i] == '\n') lineStarts.push(i+1);
- if (event.shiftKey) {
- // Find length of the leading whitespace in each line.
- let whitespaceLengths = [];
- for (let start of lineStarts) {
- let end = start;
- while ('\t '.includes(text[end])) end++;
- whitespaceLengths.push(end-start); }
- // Unindent each line in reverse order.
- selectionStart -= Math.min(size, whitespaceLengths[0]);
- for (let i=lineStarts.length-1; i>=0; i--) {
- let start = lineStarts[i];
- let trim = Math.min(size, whitespaceLengths[i]);
- text = text.slice(0, start) + text.slice(start+trim);
- selectionEnd -= trim; }
- } else {
- // Indent each line in reverse order.
- selectionStart += size;
- for (let i=lineStarts.length-1; i>=0; i--) {
- let start = lineStarts[i];
- text = text.slice(0, start) + indent + text.slice(start);
- selectionEnd += size; }
- }
- // Update textarea content and viewer.
- textarea.value = text;
- textarea.selectionStart = selectionStart;
- textarea.selectionEnd = selectionEnd;
- assembler.renderText();
- }
- }
-
- textarea.addEventListener('input', assembler.renderText);
- textarea.addEventListener('scroll', assembler.syncScroll);
- textarea.addEventListener('keydown', assembler.onEditorKeyPress);
- assembler.addEventListener('keydown', assembler.onKeyPress);
- checkButton.addEventListener('click', assembler.checkProgram);
- runButton.addEventListener('click', assembler.runProgram);
- fullscreenButton.addEventListener('click', assembler.toggleFullscreen);
-
- assembler.resizeObserver = new ResizeObserver(assembler.syncSize).observe(textarea);
- textarea.value = element.textContent.trim();
- textarea.selectionStart = 0;
- textarea.selectionEnd = 0;
- textarea.scrollLeft = 0;
- textarea.style.height = element.clientHeight + 'px';
- assembler.renderText();
-
- return assembler;
-}
-
-
-/// Constructor for a interactive emulator element.
-function EmulatorElement(options) {
- let emulator = elementFromHTML(emulatorTemplate);
- // Menu bar
- let menuBar = emulator.querySelector('.menubar');
- let fullscreenButton = menuBar.querySelector('button[name="fullscreen"]');
- let stateButton = menuBar.querySelector('button[name="state"]');
- let runButton = menuBar.querySelector('button[name="run"]');
- let pauseButton = menuBar.querySelector('button[name="pause"]');
- let stopButton = menuBar.querySelector('button[name="stop"]');
- let stepButton = menuBar.querySelector('button[name="step"]');
- let status = menuBar.querySelector('.status');
- // Screen panel
- let screenPanel = emulator.querySelector('.screen');
- let canvas = screenPanel.querySelector('canvas');
- // Stream panel
- let streamPanel = emulator.querySelector('.stream');
- let transmissions = emulator.querySelector('.transmissions');
- // State panel
- let statePanel = emulator.querySelector('.panel.state');
- let pcState = statePanel.querySelector('.pc');
- let labelState = statePanel.querySelector('.label');
- let wstState = statePanel.querySelector('.wst');
- let rstState = statePanel.querySelector('.rst');
-
- let loadedBytecode = options ? options.bytecode : new Uint8Array(0);
- let loadedSymbols = new SymbolTable();
- let loadedMetadata = parseMetadata(loadedBytecode);
- let currentTransmission = null;
- let transmissionParser = new Utf8StreamParser();
-
- emulator.screen = screenPanel;
- emulator.canvas = canvas;
- let br = new Bedrock(emulator);
- br.reset();
- br.onUpdate = () => emulator.updateDOM();
-
- emulator.showStatePanel = function() {
- statePanel.classList.remove('hidden'); }
- emulator.toggleStatePanel = function() {
- statePanel.classList.toggle('hidden'); }
- emulator.showStreamPanel = function() {
- streamPanel.classList.remove('hidden'); }
- emulator.hideStreamPanel = function() {
- streamPanel.classList.add('hidden'); }
- emulator.showScreenPanel = function() {
- screenPanel.classList.remove('hidden'); }
- emulator.hideScreenPanel = function() {
- screenPanel.classList.add('hidden'); }
- emulator.hideMenuBar = function() {
- menuBar.classList.add('hidden'); }
-
- // Reset the emulator and load a new program.
- emulator.startProgram = function(bytecode, symbols) {
- emulator.stopProgram();
- loadedBytecode = bytecode;
- loadedSymbols = symbols;
- loadedMetadata = parseMetadata(bytecode);
- br.loadProgram(bytecode);
- br.run();
- }
- // Fires when the play button is pressed.
- emulator.runProgram = function() {
- br.blank ? emulator.startProgram(loadedBytecode, loadedSymbols) : br.run();
- }
- // Fires when the pause button is pressed.
- emulator.pauseProgram = function() {
- br.pause();
- }
- // Fires when the stop button is pressed.
- emulator.stopProgram = function() {
- br.stop();
- emulator.updateDOM();
- transmissions.innerHTML = '';
- emulator.hideStreamPanel();
- emulator.hideScreenPanel();
- currentTransmission = null;
- }
-
- emulator.updateDOM = function() {
- function renderStack(stack) {
- let string = '';
- for (let i=0; i<stack.p && i<stack.mem.length; i++) {
- string += hex(stack.mem[i], 2) + ' ';
- }
- return string;
- }
- pcState.textContent = hex(br.p, 4);
- labelState.textContent = loadedSymbols.findSymbol(br.p);
- wstState.textContent = renderStack(br.wst);
- rstState.textContent = renderStack(br.rst);
- if (br.halted) {
- status.textContent = 'ended';
- } else if (br.blank) {
- status.textContent = '';
- } else if (br.paused) {
- status.textContent = 'paused';
- } else if (br.asleep) {
- let wakeMask = br.dev.system.wakeMask;
- status.textContent = `sleeping / 0x${hex(wakeMask, 4)}`;
- } else {
- status.textContent = `running / ${br.cycle}`;
- }
- emulator.flushTransmission();
- }
-
- emulator.updateScreenSize = function() {
- if (!screenPanel.classList.contains('hidden')) {
- if (document.fullscreenElement == emulator) {
- let width = screenPanel.clientWidth;
- let height = screenPanel.clientHeight;
- br.dev.screen.resizeToFill(width, height);
- } else {
- let width = screenPanel.clientWidth;
- let height = width * 9/16;
- br.dev.screen.resizeToFill(width, height);
- }
- }
- }
-
- // Receive an incoming byte from the local stream.
- emulator.receiveTransmissionByte = function(byte) {
- transmissionParser.push(byte);
- }
-
- // Push all received bytes to the DOM.
- emulator.flushTransmission = function() {
- let string = transmissionParser.read();
- if (string) {
- if (!currentTransmission) {
- emulator.showStreamPanel();
- let element = document.createElement('li');
- element.addEventListener('click', function() {
- copyText(element.textContent); })
- currentTransmission = element;
- transmissions.appendChild(element); }
- currentTransmission.textContent += string;
- streamPanel.scrollTop = streamPanel.scrollHeight;
- }
- }
-
- // End the current incoming transmission.
- emulator.endTransmission = function() {
- emulator.flushTransmission();
- streamPanel.scrollTop = streamPanel.scrollHeight;
- currentTransmission = null;
-
- }
-
- // Change the fullscreen state of the whole emulator.
- emulator.toggleFullscreen = function() {
- if (document.fullscreenElement != emulator) {
- emulator.requestFullscreen();
- } else {
- document.exitFullscreen();
- }
- }
-
- emulator.mouseMove = function(e) {
- let bounds = canvas.getBoundingClientRect();
- let scale = br.dev.screen.scale;
- let x = ((e.clientX - bounds.left) / scale) & 0xFFFF;
- let y = ((e.clientY - bounds.top ) / scale) & 0xFFFF;
- br.dev.input.applyPosition(x, y);
- }
- emulator.mouseDown = function(e) {
- br.dev.input.applyButtons(e.buttons);
- }
- emulator.mouseUp = function(e) {
- br.dev.input.applyButtons(e.buttons);
- }
- emulator.mouseEnter = function(e) {
- br.dev.input.applyActive(true);
- emulator.mouseMove(e);
- }
- emulator.mouseExit = function(e) {
- br.dev.input.applyActive(false);
- emulator.mouseMove(e);
- }
- emulator.mouseScroll = function(e) {
- // Only capture scroll events if canvas is focused.
- if (document.activeElement == canvas) {
- e.preventDefault();
- let scale;
- // TODO: Dial these numbers in a bit.
- switch (e.deltaMode) {
- case (e.DOM_DELTA_PIXEL): scale = 1/10; break;
- case (e.DOM_DELTA_LINE): scale = 1; break;
- case (e.DOM_DELTA_PAGE): scale = 20; break;
- };
- br.dev.input.applyHScroll(e.deltaX * scale);
- br.dev.input.applyVScroll(e.deltaY * scale);
- }
- }
- emulator.keyInput = function(e, pressed) {
- e.preventDefault();
- br.dev.input.applyModifiers(e);
- if (!e.repeat && !e.isComposing) {
- br.dev.input.applyKey(e, pressed);
- }
- }
-
- emulator.touchStart = function(e) {
- if (e.changedTouches.length) {
- emulator.mouseMove(e.changedTouches[0]); }
- br.dev.input.applyActive(true);
- br.dev.input.applyButtons(0x01);
- }
- emulator.touchEnd = function(e) {
- br.dev.input.applyActive(false);
- br.dev.input.applyButtons(0x00);
- }
-
- fullscreenButton.addEventListener('click', emulator.toggleFullscreen);
- stateButton.addEventListener('click', emulator.toggleStatePanel);
- runButton.addEventListener('click', emulator.runProgram);
- pauseButton.addEventListener('click', emulator.pauseProgram);
- stopButton.addEventListener('click', emulator.stopProgram);
- stepButton.addEventListener('click', br.step);
-
- canvas.addEventListener('mousemove', emulator.mouseMove);
- canvas.addEventListener('pointermove', emulator.mouseMove);
- canvas.addEventListener('mousedown', emulator.mouseDown);
- canvas.addEventListener('mouseup', emulator.mouseUp);
- canvas.addEventListener('touchstart', emulator.touchStart);
- canvas.addEventListener('touchend', emulator.touchEnd);
- canvas.addEventListener('touchcancel', emulator.touchEnd);
- canvas.addEventListener('mouseenter', emulator.mouseEnter);
- canvas.addEventListener('mouseleave', emulator.mouseExit);
- canvas.addEventListener('wheel', emulator.mouseScroll);
- canvas.addEventListener('contextmenu', (e)=>{e.preventDefault()});
- canvas.addEventListener('keydown', (e)=>emulator.keyInput(e, true));
- canvas.addEventListener('keyup', (e)=>emulator.keyInput(e, false));
- // Make canvas focusable to be able to receive keydown and keyup events.
- canvas.tabIndex = 0;
-
- emulator.resizeObserver = new ResizeObserver(emulator.updateScreenSize).observe(screenPanel);
-
- if (options && !options.controls) emulator.hideMenuBar();
- if (options && options.autoplay) emulator.runProgram();
- if (options && options.nocursor) canvas.style.cursor = 'none';
-
- return emulator;
-}
-
-
-// Create an element from an HTML string.
-function elementFromHTML(html) {
- let template = document.createElement('template');
- template.innerHTML = html.trim();
- return template.content.firstChild;
-}
-
-
-// Convert an integer to a hexadecimal string.
-function hex(value, pad) {
- return value.toString(16).toUpperCase().padStart(pad, '0');
-}
-
-
-function parseMetadata(bytecode) {
- // Test identifier to see if program has metadata.
- let id = [0xE8,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B];
- for (let i=0; i<10; i++) if (bytecode[i] != id[i]) return;
- // Parse each metadata item.
- let name = getString(0x000A);
- let authors = getString(0x000C);
- let description = getString(0x000E);
- let bgColour = getColour(0x0010);
- let fgColour = getColour(0x0012);
- let smallIcon = getIcon(0x0014, 24);
- let largeIcon = getIcon(0x0016, 64);
- return { name, authors, description, bgColour, fgColour, smallIcon, largeIcon };
-
- function getByte(addr) {
- return bytecode[addr] || 0;
- }
- function getDouble(addr) {
- return getByte(addr) << 8 | getByte(addr+1);
- }
- function getString(addr) {
- addr = getDouble(addr);
- if (addr) {
- let parser = new Utf8StreamParser();
- while (true) {
- let byte = bytecode[addr++];
- if (!byte) break;
- parser.push(byte);
- }
- return parser.read();
- }
- }
- function getColour(addr) {
- addr = getDouble(addr);
- if (addr) {
- let colour = getDouble(addr);
- let r = (colour >> 8 & 0xF) * 17;
- let g = (colour >> 8 & 0xF) * 17;
- let b = (colour >> 8 & 0xF) * 17;
- return [r, g, b];
- }
- }
- function getIcon(addr, size) {
- // TODO: Return a custom type containing the dimensions and raw
- // pixel data, and a colourise method to generate a coloured ImageData.
- }
-}
-
-
-// Copy a string to the clipboard.
-function copyText(text) {
- navigator.clipboard.writeText(text);
-}
-
-let encoder = new TextEncoder();
-function encodeUtf8(text) {
- return encoder.encode(text);
-}
-
-// Incrementally parse a UTF-8 string.
-function Utf8StreamParser() {
- // Full characters parsed so far.
- this.string = '';
- // Current partial code point.
- this.codePoint;
- // True if the current code point is being dropped.
- this.dropBytes = false;
- // Number of bytes received of the current code point.
- this.i = 0;
- // Expected byte length of the current code point.
- this.len = 0;
-
- // Push a code point to the string.
- let pushCodePoint = (codePoint) => {
- this.string += String.fromCodePoint(codePoint);
- }
- // Drop the current partial code point, if any.
- let dropCodePoint = () => {
- // Push a 'replacement character' if there is a partial code point
- // that hasn't already been dropped.
- if (this.i && !this.dropBytes) pushCodePoint(0xFFFD);
- this.codePoint = 0;
- this.dropBytes = false;
- this.i = 0;
- this.len = 0;
- }
- // Parse the start of an encoded code point.
- let parseStartByte = (byte, length) => {
- dropCodePoint();
- this.len = length;
- parseContinuationByte(byte);
- }
- // Parse the remaining bytes of an encoded code point.
- let parseContinuationByte = (byte) => {
- if (!this.dropBytes) {
- if (this.len) {
- this.codePoint = this.codePoint << 6 | (byte & 0x3F);
- this.i += 1;
- if (this.i == this.len) {
- pushCodePoint(this.codePoint);
- this.len = 0;
- }
- } else {
- this.dropBytes = true;
- pushCodePoint(0xFFFD);
- }
- }
- }
+ let getString = (key) => attr[key] ? attr[key].value : null;
+ let getBool = (key) => attr[key] ? true : false;
+ let src = getString("src");
+ let scale = getString("scale") || "1";
+ let controls = getBool("controls");
+ let nocursor = getBool("nocursor");
- // Parse a UTF-8 encoded byte.
- this.push = (byte) => {
- if (byte < 0x80) { // start of a 1-byte code point
- dropCodePoint();
- pushCodePoint(byte);
- } else if (byte < 0xC0) { // not a start byte
- parseContinuationByte(byte);
- } else if (byte < 0xE0) { // start of a 2-byte code point
- parseStartByte(byte, 2);
- } else if (byte < 0xF0) { // start of a 3-byte code point
- parseStartByte(byte, 3);
- } else if (byte < 0xF8) { // start of a 4-byte code point
- parseStartByte(byte, 4);
+ // Validate options.
+ if (!src) { console.error("No src attribute provided for emulator", element); return; }
+ try { scale = clamp(parseInt(attr.scale.value), 1, 10) } catch { scale = 1 }
+ let options = { scale, controls, nocursor, autoplay: true };
+ let url = new URL(src, window.location.href).href;
+ // Instantiate emulator once program has loaded in.
+ fetch(url).then((r) => {
+ if (r.ok) { r.arrayBuffer().then((a) => {
+ options.bytecode = new Uint8Array(a);
+ let emulator = new EmulatorElement(options);
+ emulator.init().then(() => { element.replaceWith(emulator.el) });
+ })} else {
+ console.log(`Could not load Bedrock program from ${url}`);
}
- }
- // Parse an array of bytes.
- this.pushList = (list) => {
- for (let byte of list) this.push(byte);
- }
-
- // Take the characters that have been parsed so far.
- this.read = () => {
- let output = this.string;
- this.string = '';
- return output;
- }
+ });
}
-
-
-
// ----------------------------------------------------------------------------------------------- +
// :::::: ASSEMBLER:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
// ----------------------------------------------------------------------------------------------- +
-// Convert a string of Bedrock source code into syntax-highlighted nodes.
-function highlightSource(source) {
- let currentText = '', currentClass = '';
- let items = [];
- let typeLookup = {
- 'comment': 'comment',
- 'markOpen': 'comment',
- 'markClose': 'comment',
- 'blockOpen': '',
- 'blockClose': '',
- 'rawString': 'value',
- 'terminatedString': 'value',
- 'macroDefinition': 'definition',
- 'globalLabel': 'definition',
- 'localLabel': 'definition',
- 'macroTerminator': 'definition',
- 'spacer': '',
- 'symbol': 'reference',
- 'byteLiteral': 'value',
- 'doubleLiteral': 'value',
- 'instruction': '',
- 'whitespace': '',
- };
- function pushItem(text, className) {
- if (currentClass != className) {
- if (currentClass) {
- let span = document.createElement('span');
- span.textContent = currentText;
- span.className = currentClass;
- items.push(span);
- } else {
- items.push(document.createTextNode(currentText));
- }
- currentClass = className;
- currentText = text;
- } else {
- currentText += text;
- }
- }
- for (let token of tokeniseProgram(source).tokens) {
- pushItem(token.text, typeLookup[token.type]);
- }
- pushItem(' ');
- return items;
-}
-
-
// Tokenise a string of Bedrock source code.
-function tokeniseProgram(source) {
+function tokeniseSource(source) {
let tokens = [], errors = [];
let currentText = '', endChar = '', label = '';
let currentLine = 0, currentColumn = 0; // position of next character
@@ -845,10 +269,10 @@ function tokeniseProgram(source) {
} else if (firstChar == ';') {
token.type = 'macroTerminator';
} else if (firstChar == '#') {
- token.type = 'spacer';
+ token.type = 'padding';
token.value = parseLiteral(currentText.slice(1));
if (token.value == undefined) {
- error(`Invalid value for spacer: ${currentText.slice(1)}`)
+ error(`Invalid value for padding: ${currentText.slice(1)}`)
}
} else if (firstChar == '~') {
token.type = 'symbol';
@@ -926,9 +350,56 @@ function tokeniseProgram(source) {
}
+// Convert a string of Bedrock source code into syntax-highlighted nodes.
+function highlightSource(source) {
+ let currentText = '', currentClass = '';
+ let items = [];
+ let typeLookup = {
+ 'comment': 'comment',
+ 'markOpen': 'comment',
+ 'markClose': 'comment',
+ 'blockOpen': '',
+ 'blockClose': '',
+ 'rawString': 'value',
+ 'terminatedString': 'value',
+ 'macroDefinition': 'definition',
+ 'globalLabel': 'definition',
+ 'localLabel': 'definition',
+ 'macroTerminator': 'definition',
+ 'padding': '',
+ 'symbol': 'reference',
+ 'byteLiteral': 'value',
+ 'doubleLiteral': 'value',
+ 'instruction': '',
+ 'whitespace': '',
+ };
+ function pushItem(text, className) {
+ if (currentClass != className) {
+ if (currentClass) {
+ let span = document.createElement('span');
+ span.textContent = currentText;
+ span.className = currentClass;
+ items.push(span);
+ } else {
+ items.push(document.createTextNode(currentText));
+ }
+ currentClass = className;
+ currentText = text;
+ } else {
+ currentText += text;
+ }
+ }
+ for (let token of tokeniseSource(source).tokens) {
+ pushItem(token.text, typeLookup[token.type]);
+ }
+ pushItem(' ');
+ return items;
+}
+
+
// Assemble a string of Bedrock source code.
-function assembleProgram(source) {
- let { tokens, errors } = tokeniseProgram(source);
+function assembleSource(source) {
+ let { tokens, errors } = tokeniseSource(source);
let program = new Uint8Array(65536);
// Populated up front with all names.
let macros = {}, labels = {}, address = 0;
@@ -971,7 +442,7 @@ function assembleProgram(source) {
} else if (['rawString', 'terminatedString'].includes(token.type)) {
for (let byte of encodeUtf8(token.value)) program[address++] = byte;
if (token.type == 'terminatedString') pushByte(0);
- } else if (token.type == 'spacer') {
+ } else if (token.type == 'padding') {
for (let i=0; i<token.value; i++) pushByte(0);
} else if (token.type == 'blockOpen') {
stack.push({ token, address });
@@ -1082,6 +553,7 @@ function assembleProgram(source) {
}
+// Efficient lookup table for finding the closest label to a given address.
function SymbolTable() {
this.names = [];
this.addresses = [];
@@ -1117,177 +589,144 @@ function SymbolTable() {
}
+// Map the predefined instruction names to bytes for the assembler.
+const instructionTable = {
+ 'HLT': 0x00, 'NOP' : 0x20, 'DB1' : 0x40, 'DB2' : 0x60, 'DB3' : 0x80, 'DB4' : 0xA0, 'DB5' : 0xC0, 'DB6' : 0xE0,
+ 'PSH': 0x01, 'PSH:': 0x21, 'PSH*': 0x41, 'PSH*:': 0x61, 'PSHr': 0x81, 'PSHr:': 0xA1, 'PSHr*': 0xC1, 'PSHr*:': 0xE1,
+ ':': 0x21, '*:': 0x61, 'r:': 0xA1, 'r*:': 0xE1,
+ 'POP': 0x02, 'POP:': 0x22, 'POP*': 0x42, 'POP*:': 0x62, 'POPr': 0x82, 'POPr:': 0xA2, 'POPr*': 0xC2, 'POPr*:': 0xE2,
+ 'CPY': 0x03, 'CPY:': 0x23, 'CPY*': 0x43, 'CPY*:': 0x63, 'CPYr': 0x83, 'CPYr:': 0xA3, 'CPYr*': 0xC3, 'CPYr*:': 0xE3,
+ 'DUP': 0x04, 'DUP:': 0x24, 'DUP*': 0x44, 'DUP*:': 0x64, 'DUPr': 0x84, 'DUPr:': 0xA4, 'DUPr*': 0xC4, 'DUPr*:': 0xE4,
+ 'OVR': 0x05, 'OVR:': 0x25, 'OVR*': 0x45, 'OVR*:': 0x65, 'OVRr': 0x85, 'OVRr:': 0xA5, 'OVRr*': 0xC5, 'OVRr*:': 0xE5,
+ 'SWP': 0x06, 'SWP:': 0x26, 'SWP*': 0x46, 'SWP*:': 0x66, 'SWPr': 0x86, 'SWPr:': 0xA6, 'SWPr*': 0xC6, 'SWPr*:': 0xE6,
+ 'ROT': 0x07, 'ROT:': 0x27, 'ROT*': 0x47, 'ROT*:': 0x67, 'ROTr': 0x87, 'ROTr:': 0xA7, 'ROTr*': 0xC7, 'ROTr*:': 0xE7,
+ 'JMP': 0x08, 'JMP:': 0x28, 'JMP*': 0x48, 'JMP*:': 0x68, 'JMPr': 0x88, 'JMPr:': 0xA8, 'JMPr*': 0xC8, 'JMPr*:': 0xE8,
+ 'JMS': 0x09, 'JMS:': 0x29, 'JMS*': 0x49, 'JMS*:': 0x69, 'JMSr': 0x89, 'JMSr:': 0xA9, 'JMSr*': 0xC9, 'JMSr*:': 0xE9,
+ 'JCN': 0x0A, 'JCN:': 0x2A, 'JCN*': 0x4A, 'JCN*:': 0x6A, 'JCNr': 0x8A, 'JCNr:': 0xAA, 'JCNr*': 0xCA, 'JCNr*:': 0xEA,
+ 'JCS': 0x0B, 'JCS:': 0x2B, 'JCS*': 0x4B, 'JCS*:': 0x6B, 'JCSr': 0x8B, 'JCSr:': 0xAB, 'JCSr*': 0xCB, 'JCSr*:': 0xEB,
+ 'LDA': 0x0C, 'LDA:': 0x2C, 'LDA*': 0x4C, 'LDA*:': 0x6C, 'LDAr': 0x8C, 'LDAr:': 0xAC, 'LDAr*': 0xCC, 'LDAr*:': 0xEC,
+ 'STA': 0x0D, 'STA:': 0x2D, 'STA*': 0x4D, 'STA*:': 0x6D, 'STAr': 0x8D, 'STAr:': 0xAD, 'STAr*': 0xCD, 'STAr*:': 0xED,
+ 'LDD': 0x0E, 'LDD:': 0x2E, 'LDD*': 0x4E, 'LDD*:': 0x6E, 'LDDr': 0x8E, 'LDDr:': 0xAE, 'LDDr*': 0xCE, 'LDDr*:': 0xEE,
+ 'STD': 0x0F, 'STD:': 0x2F, 'STD*': 0x4F, 'STD*:': 0x6F, 'STDr': 0x8F, 'STDr:': 0xAF, 'STDr*': 0xCF, 'STDr*:': 0xEF,
+ 'ADD': 0x10, 'ADD:': 0x30, 'ADD*': 0x50, 'ADD*:': 0x70, 'ADDr': 0x90, 'ADDr:': 0xB0, 'ADDr*': 0xD0, 'ADDr*:': 0xF0,
+ 'SUB': 0x11, 'SUB:': 0x31, 'SUB*': 0x51, 'SUB*:': 0x71, 'SUBr': 0x91, 'SUBr:': 0xB1, 'SUBr*': 0xD1, 'SUBr*:': 0xF1,
+ 'INC': 0x12, 'INC:': 0x32, 'INC*': 0x52, 'INC*:': 0x72, 'INCr': 0x92, 'INCr:': 0xB2, 'INCr*': 0xD2, 'INCr*:': 0xF2,
+ 'DEC': 0x13, 'DEC:': 0x33, 'DEC*': 0x53, 'DEC*:': 0x73, 'DECr': 0x93, 'DECr:': 0xB3, 'DECr*': 0xD3, 'DECr*:': 0xF3,
+ 'LTH': 0x14, 'LTH:': 0x34, 'LTH*': 0x54, 'LTH*:': 0x74, 'LTHr': 0x94, 'LTHr:': 0xB4, 'LTHr*': 0xD4, 'LTHr*:': 0xF4,
+ 'GTH': 0x15, 'GTH:': 0x35, 'GTH*': 0x55, 'GTH*:': 0x75, 'GTHr': 0x95, 'GTHr:': 0xB5, 'GTHr*': 0xD5, 'GTHr*:': 0xF5,
+ 'EQU': 0x16, 'EQU:': 0x36, 'EQU*': 0x56, 'EQU*:': 0x76, 'EQUr': 0x96, 'EQUr:': 0xB6, 'EQUr*': 0xD6, 'EQUr*:': 0xF6,
+ 'NQK': 0x17, 'NQK:': 0x37, 'NQK*': 0x57, 'NQK*:': 0x77, 'NQKr': 0x97, 'NQKr:': 0xB7, 'NQKr*': 0xD7, 'NQKr*:': 0xF7,
+ 'SHL': 0x18, 'SHL:': 0x38, 'SHL*': 0x58, 'SHL*:': 0x78, 'SHLr': 0x98, 'SHLr:': 0xB8, 'SHLr*': 0xD8, 'SHLr*:': 0xF8,
+ 'SHR': 0x19, 'SHR:': 0x39, 'SHR*': 0x59, 'SHR*:': 0x79, 'SHRr': 0x99, 'SHRr:': 0xB9, 'SHRr*': 0xD9, 'SHRr*:': 0xF9,
+ 'ROL': 0x1A, 'ROL:': 0x3A, 'ROL*': 0x5A, 'ROL*:': 0x7A, 'ROLr': 0x9A, 'ROLr:': 0xBA, 'ROLr*': 0xDA, 'ROLr*:': 0xFA,
+ 'ROR': 0x1B, 'ROR:': 0x3B, 'ROR*': 0x5B, 'ROR*:': 0x7B, 'RORr': 0x9B, 'RORr:': 0xBB, 'RORr*': 0xDB, 'RORr*:': 0xFB,
+ 'IOR': 0x1C, 'IOR:': 0x3C, 'IOR*': 0x5C, 'IOR*:': 0x7C, 'IORr': 0x9C, 'IORr:': 0xBC, 'IORr*': 0xDC, 'IORr*:': 0xFC,
+ 'XOR': 0x1D, 'XOR:': 0x3D, 'XOR*': 0x5D, 'XOR*:': 0x7D, 'XORr': 0x9D, 'XORr:': 0xBD, 'XORr*': 0xDD, 'XORr*:': 0xFD,
+ 'AND': 0x1E, 'AND:': 0x3E, 'AND*': 0x5E, 'AND*:': 0x7E, 'ANDr': 0x9E, 'ANDr:': 0xBE, 'ANDr*': 0xDE, 'ANDr*:': 0xFE,
+ 'NOT': 0x1F, 'NOT:': 0x3F, 'NOT*': 0x5F, 'NOT*:': 0x7F, 'NOTr': 0x9F, 'NOTr:': 0xBF, 'NOTr*': 0xDF, 'NOTr*:': 0xFF,
+};
+
+
// ----------------------------------------------------------------------------------------------- +
-// :::::: EMULATOR::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// :::::: EMULATOR CORE (WEBASSEMBLY) ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
// ----------------------------------------------------------------------------------------------- +
-// The core Bedrock emulator. Receives an EmulatorElement.
-function Bedrock(e) {
- this.e = e;
- this.mem = new Uint8Array(65536);
- this.wst = new Stack();
- this.rst = new Stack();
- this.dev = new DeviceBus(this);
- this.mark = performance.now();
- // Callback functions.
- this.onUpdate;
+// WebAssembly implementation of the Bedrock core.
+function BedrockWasm() {
+ let wasm, wst, rst;
+ this.mem;
+ this.ldd;
+ this.std;
- // Reset the emulator, preserving the contents of program memory.
- this.reset = () => {
- this.p = 0;
- this.wst.reset();
- this.rst.reset();
- this.dev.reset();
- this.cycle = 0;
- this.blank = true; // true when emulator has been reset.
- this.paused = true; // true when program is paused for the user.
- this.asleep = false; // true when program is waiting for input.
- this.halted = false; // true when program has halted.
- this.frameLag = 0; // time since runLoop started
+ this.init = async (dget1, dset1) => {
+ const imported = { bedrock: { dget1, dset1 } };
+ wasm = (await WebAssembly.instantiate(wasmModule, imported)).exports;
+ this.mem = new Uint8Array(wasm.memory.buffer, 0, 0x10000);
+ wst = new Uint8Array(wasm.memory.buffer, 0x10000, 0x100);
+ rst = new Uint8Array(wasm.memory.buffer, 0x10100, 0x100);
+ this.ldd = dget1;
+ this.std = dset1;
+ this.cc = wasm.cc;
+ this.ip = wasm.ip;
+ this.wp = wasm.wp;
+ this.rp = wasm.rp;
+ this.wst = (i) => { return wst[0xFF-i]; }
+ this.rst = (i) => { return rst[0xFF-i]; }
+ this.eval = wasm.eval;
+ this.reset = wasm.reset;
}
- // Reset the emulator and load in a new program.
- this.loadProgram = (bytecode) => {
+ // Load an assembled program into program memory.
+ this.load = (bytes) => {
this.reset();
this.mem.fill(0);
- this.mem.set(bytecode.slice(0, 65536));
- this.blank = false;
- this.mark = performance.now();
- console.log("Started new run");
+ this.mem.set(bytes.slice(0, 65536));
}
+}
- this.run = () => {
- this.paused = false;
- this.update();
- this.runLoop();
- }
- this.step = () => {
- if (this.paused && !this.halted) {
- if (this.asleep) {
- this.sleepLoop()
- } else {
- this.blank = false;
- switch (this.evaluate(1)) {
- case 5: this.halt(); break; // HALT
- case 4: this.reset(); break; // RESET
- case 3: this.reset(); break; // FORK
- case 2: this.sleep(); break; // SLEEP
- case 1: this.pause(); break; // DEBUG
- default: break; // NORMAL
- }
- this.update();
- this.render();
- }
- }
- }
- this.pause = () => {
- this.paused = true;
- this.update();
- this.render();
- }
-
- this.stop = () => {
- this.reset();
- this.update();
- this.render();
- }
+// ----------------------------------------------------------------------------------------------- +
+// :::::: EMULATOR CORE (JAVASCRIPT) :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
- this.halt = () => {
- this.halted = true;
- this.update();
- this.render();
- console.log("Run completed in", performance.now() - this.mark, "milliseconds");
- }
- this.sleep = () => {
- this.asleep = true;
- this.update();
- this.render();
- this.sleepLoop();
- }
+// Pure JavaScript implementation of the Bedrock core.
+function BedrockJs() {
+ this.mem;
+ this.ldd;
+ this.std;
+ let cc, ip, wst, rst;
- this.update = () => {
- if (this.onUpdate) this.onUpdate(this);
+ this.init = async (dget1, dset1) => {
+ this.mem = new Uint8Array(65536);
+ this.ldd = dget1;
+ this.std = dset1;
+ cc = 0;
+ ip = 0;
+ wst = new Stack();
+ rst = new Stack();
}
- // Render the screen contents to the canvas.
- this.render = () => {
- if (this.dev.input.accessed || this.dev.screen.accessed) {
- e.showScreenPanel();
- }
- let scr = this.dev.screen;
- if (scr.dirty()) {
- scr.render();
- let image = new ImageData(scr.pixels, scr.width, scr.height);
- scr.ctx.putImageData(image, 0, 0, scr.dx0, scr.dy0, scr.dx1, scr.dy1);
- scr.unmark();
- this.frameLag = performance.now();
- }
- }
+ this.cc = () => { return cc };
+ this.ip = () => { return ip };
+ this.wp = () => { return wst.p };
+ this.rp = () => { return rst.p };
+ this.wst = (i) => { return wst.mem[i] };
+ this.rst = (i) => { return rst.mem[i] };
- this.runLoop = () => {
- if (!this.halted && !this.paused) {
- if (this.asleep) {
- setTimeout(this.sleepLoop.bind(this));
- } else {
- // Tail-recursive with timeout to allow other code to run.
- switch (this.evaluate(100000)) {
- case 5: this.halt(); return; // HALT
- case 4: this.reset(); break; // RESET
- case 3: this.reset(); break; // FORK
- case 2: this.sleep(); return; // SLEEP
- case 1: this.pause(); break; // DEBUG
- default: // NORMAL
- }
- this.update();
- // Force a render if the frame has been running for >500ms.
- if (performance.now() - this.frameLag > 500) {
- this.render(); }
- setTimeout(this.runLoop.bind(this), 0);
- }
- }
+ // Load an assembled program into program memory.
+ this.load = (bytes) => {
+ this.reset();
+ this.mem.fill(0);
+ this.mem.set(bytes.slice(0, 65536));
}
- this.sleepLoop = () => {
- this.frameLag = performance.now();
- if (!this.halted) {
- if (!this.asleep) {
- setTimeout(this.runLoop.bind(this), 0);
- } else {
- let queue = this.dev.queue.get(this.dev.system.wakeMask);
- for (let slot of queue) {
- if (this.dev.devices[slot].wake()) {
- this.asleep = false;
- this.dev.system.wakeSlot = slot;
- setTimeout(this.runLoop.bind(this));
- return;
- }
- }
- setTimeout(this.sleepLoop.bind(this), 0);
- }
- }
+ // Reset all pointers and counters to zero.
+ this.reset = () => {
+ cc = 0;
+ ip = 0;
+ wst.reset();
+ rst.reset();
}
- // Evaluate at most n processor cycles, returning a signal value.
- this.evaluate = (n) => {
- for (let i=0; i<n; i++) {
- this.cycle += 1;
- let instr = this.mem[this.p++];
+ this.eval = (maxCycles) => {
+ for (let i=0; i<maxCycles; i++) {
+ cc += 1;
+ let instr = this.mem[ip++];
let rMode = instr & 0x80;
let wMode = instr & 0x40;
let iMode = instr & 0x20;
- let w = rMode ? this.rst : this.wst;
- let r = rMode ? this.wst : this.rst;
+ let w = rMode ? rst : wst;
+ let r = rMode ? wst : rst;
// Memory access functions.
- let mpop1 = ( ) => { return this.mem[this.p++]; }
+ let mpop1 = ( ) => { return this.mem[ip++]; }
let mpop2 = ( ) => { return mpop1() << 8 | mpop1(); }
let mpopx = ( ) => { return wMode ? mpop2() : mpop1(); }
- let mpsh1 = (v) => { this.mem[this.p++] = v; }
+ let mpsh1 = (v) => { this.mem[ip++] = v; }
let mpsh2 = (v) => { this.mpsh1(v >> 8); this.mpsh1(v); }
let mpshx = (v) => { wMode ? mpsh2(v) : mpsh1(v); }
let mget1 = (a) => { return this.mem[a]; }
@@ -1297,18 +736,18 @@ function Bedrock(e) {
let mset2 = (a,v) => { mset1(a, v >> 8); mset1(a+1, v); }
let msetx = (a,v) => { wMode ? mset2(a,v) : mset1(a,v); }
// Device access functions.
- let dget1 = (p) => { return this.dev.read(p); }
+ let dget1 = this.ldd;
let dget2 = (p) => { return dget1(p) << 8 | dget1(p+1); }
let dgetx = (p) => { return wMode ? dget2(p) : dget1(p); }
- let dset1 = (p,v) => { return this.dev.write(p,v); }
+ let dset1 = this.std;
let dset2 = (p,v) => { let s1 = dset1(p, v >> 8); let s2 = dset1(p+1, v & 0xFF); return s1 || s2; }
let dsetx = (p,v) => { if (wMode) { return dset2(p,v) } else { return dset1(p,v) } }
// Stack access functions.
- let wpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : w.pop1(); }
- let wpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : w.pop2(); }
+ let wpop1 = ( ) => { return w.pop1(); }
+ let wpop2 = ( ) => { return w.pop2(); }
let wpopx = ( ) => { return wMode ? wpop2() : wpop1(); }
- let rpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : r.pop1(); }
- let rpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : r.pop2(); }
+ let rpop1 = ( ) => { return r.pop1(); }
+ let rpop2 = ( ) => { return r.pop2(); }
let rpopx = ( ) => { return wMode ? rpop2() : rpop1(); }
let wpsh1 = (v) => { w.psh1(v); }
let wpsh2 = (v) => { w.psh2(v); }
@@ -1316,6 +755,13 @@ function Bedrock(e) {
let rpsh1 = (v) => { r.psh1(v); }
let rpsh2 = (v) => { r.psh2(v); }
let rpshx = (v) => { wMode ? r.psh2(v) : r.psh1(v); }
+ // Immediate-mode aware stack access functions.
+ let iwpop1 = ( ) => { return iMode ? mpop1() : w.pop1(); }
+ let iwpop2 = ( ) => { return iMode ? mpop2() : w.pop2(); }
+ let iwpopx = ( ) => { return wMode ? iwpop2() : iwpop1(); }
+ let irpop1 = ( ) => { return iMode ? mpop1() : r.pop1(); }
+ let irpop2 = ( ) => { return iMode ? mpop2() : r.pop2(); }
+ let irpopx = ( ) => { return wMode ? irpop2() : irpop1(); }
// Shifting operations.
let shl1 = (v,d) => { return d < 8 ? v << d & 0xFF : 0; }
let shl2 = (v,d) => { return d < 16 ? v << d & 0xFFFF : 0; }
@@ -1332,45 +778,55 @@ function Bedrock(e) {
let a, p, s, t, v, x, y, z;
switch (instr & 0x1F) {
- /* HLT */ case 0x00: switch(instr) { case 0x00: return 5; case 0x40: return 1; default: } break;
- /* PSH */ case 0x01: x=rpopx(); wpshx(x); break;
- /* POP */ case 0x02: wpopx(); break;
- /* CPY */ case 0x03: x=rpopx(); rpshx(x); wpshx(x); break;
- /* DUP */ case 0x04: x=wpopx(); wpshx(x); wpshx(x); break;
- /* OVR */ case 0x05: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpshx(x); break;
- /* SWP */ case 0x06: y=wpopx(); x=wpopx(); wpshx(y); wpshx(x); break;
- /* ROT */ case 0x07: z=wpopx(); y=wpopx(); x=wpopx(); wpshx(y); wpshx(z); wpshx(x); break;
- /* JMP */ case 0x08: a=wpop2(); this.p = a; break;
- /* JMS */ case 0x09: a=wpop2(); rpsh2(this.p); this.p = a; break;
- /* JCN */ case 0x0A: a=wpop2(); t=wpopx(); if (t) { this.p = a; } break;
- /* JCS */ case 0x0B: a=wpop2(); t=wpopx(); if (t) { rpsh2(this.p); this.p = a; } break;
- /* LDA */ case 0x0C: a=wpop2(); v=mgetx(a); wpshx(v); break;
- /* STA */ case 0x0D: a=wpop2(); v=wpopx(); msetx(a,v); break;
- /* LDD */ case 0x0E: p=wpop1(); v=dgetx(p); wpshx(v); break;
- /* STD */ case 0x0F: p=wpop1(); v=wpopx(); s=dsetx(p,v); if (s) { return s; } break;
- /* ADD */ case 0x10: y=wpopx(); x=wpopx(); wpshx(x+y); break;
- /* SUB */ case 0x11: y=wpopx(); x=wpopx(); wpshx(x-y); break;
- /* INC */ case 0x12: x=wpopx(); wpshx(x+1); break;
- /* DEC */ case 0x13: x=wpopx(); wpshx(x-1); break;
- /* LTH */ case 0x14: y=wpopx(); x=wpopx(); wpsh1(0-(x <y)); break;
- /* GTH */ case 0x15: y=wpopx(); x=wpopx(); wpsh1(0-(x >y)); break;
- /* EQU */ case 0x16: y=wpopx(); x=wpopx(); wpsh1(0-(x==y)); break;
- /* NQK */ case 0x17: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpsh1(0-(x!=y)); break;
- /* SHL */ case 0x18: y=wpop1(); x=wpopx(); wpshx(shlx(x,y)); break;
- /* SHR */ case 0x19: y=wpop1(); x=wpopx(); wpshx(shrx(x,y)); break;
- /* ROL */ case 0x1A: y=wpop1(); x=wpopx(); wpshx(rolx(x,y)); break;
- /* ROR */ case 0x1B: y=wpop1(); x=wpopx(); wpshx(rorx(x,y)); break;
- /* IOR */ case 0x1C: y=wpopx(); x=wpopx(); wpshx(x|y); break;
- /* XOR */ case 0x1D: y=wpopx(); x=wpopx(); wpshx(x^y); break;
- /* AND */ case 0x1E: y=wpopx(); x=wpopx(); wpshx(x&y); break;
- /* NOT */ case 0x1F: x=wpopx(); wpshx(~x ); break;
+ /* HLT */ case 0x00: switch(instr) {
+ case 0x00: return Signal.HALT;
+ case 0x20: continue;
+ case 0x40: return Signal.DB1;
+ case 0x60: return Signal.DB2;
+ case 0x80: return Signal.DB3;
+ case 0xA0: return Signal.DB4;
+ case 0xC0: return Signal.DB5;
+ case 0xE0: return Signal.DB6;
+ }
+ /* PSH */ case 0x01: x=irpopx(); wpshx(x); break;
+ /* POP */ case 0x02: iwpopx(); break;
+ /* CPY */ case 0x03: x=irpopx(); rpshx(x); wpshx(x); break;
+ /* DUP */ case 0x04: x=iwpopx(); wpshx(x); wpshx(x); break;
+ /* OVR */ case 0x05: y=iwpopx(); x=wpopx(); wpshx(x); wpshx(y); wpshx(x); break;
+ /* SWP */ case 0x06: y=iwpopx(); x=wpopx(); wpshx(y); wpshx(x); break;
+ /* ROT */ case 0x07: z=iwpopx(); y=wpopx(); x=wpopx(); wpshx(y); wpshx(z); wpshx(x); break;
+ /* JMP */ case 0x08: a=iwpop2(); ip = a; break;
+ /* JMS */ case 0x09: a=iwpop2(); rpsh2(ip); ip = a; break;
+ /* JCN */ case 0x0A: a=iwpop2(); t=wpopx(); if (t) { ip = a; } break;
+ /* JCS */ case 0x0B: a=iwpop2(); t=wpopx(); if (t) { rpsh2(ip); ip = a; } break;
+ /* LDA */ case 0x0C: a=iwpop2(); v=mgetx(a); wpshx(v); break;
+ /* STA */ case 0x0D: a=iwpop2(); v=wpopx(); msetx(a,v); break;
+ /* LDD */ case 0x0E: p=iwpop1(); v=dgetx(p); wpshx(v); break;
+ /* STD */ case 0x0F: p=iwpop1(); v=wpopx(); s=dsetx(p,v); if (s) { return s; } break;
+ /* ADD */ case 0x10: y=iwpopx(); x=wpopx(); wpshx(x+y); break;
+ /* SUB */ case 0x11: y=iwpopx(); x=wpopx(); wpshx(x-y); break;
+ /* INC */ case 0x12: x=iwpopx(); wpshx(x+1); break;
+ /* DEC */ case 0x13: x=iwpopx(); wpshx(x-1); break;
+ /* LTH */ case 0x14: y=iwpopx(); x=wpopx(); wpsh1(0-(x <y)); break;
+ /* GTH */ case 0x15: y=iwpopx(); x=wpopx(); wpsh1(0-(x >y)); break;
+ /* EQU */ case 0x16: y=iwpopx(); x=wpopx(); wpsh1(0-(x==y)); break;
+ /* NQK */ case 0x17: y=iwpopx(); x=wpopx(); wpshx(x); wpshx(y); wpsh1(0-(x!=y)); break;
+ /* SHL */ case 0x18: y=iwpop1(); x=wpopx(); wpshx(shlx(x,y)); break;
+ /* SHR */ case 0x19: y=iwpop1(); x=wpopx(); wpshx(shrx(x,y)); break;
+ /* ROL */ case 0x1A: y=iwpop1(); x=wpopx(); wpshx(rolx(x,y)); break;
+ /* ROR */ case 0x1B: y=iwpop1(); x=wpopx(); wpshx(rorx(x,y)); break;
+ /* IOR */ case 0x1C: y=iwpopx(); x=wpopx(); wpshx(x|y); break;
+ /* XOR */ case 0x1D: y=iwpopx(); x=wpopx(); wpshx(x^y); break;
+ /* AND */ case 0x1E: y=iwpopx(); x=wpopx(); wpshx(x&y); break;
+ /* NOT */ case 0x1F: x=iwpopx(); wpshx(~x ); break;
}
}
- };
+ return Signal.BREAK;
+ }
}
-// Stack for the core system.
+// Stack implementation for the JavaScript core.
function Stack() {
this.mem = new Uint8Array(256);
this.p = 0;
@@ -1380,9 +836,222 @@ function Stack() {
this.psh2 = (v) => { this.psh1(v >> 8); this.psh1(v); }
this.pop1 = ( ) => { return this.mem[--this.p]; }
this.pop2 = ( ) => { return this.pop1() | this.pop1() << 8; }
+
+}
+
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: EMULATOR :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+// Signal enum for devices.
+const Signal = Object.freeze({
+ BREAK: 0,
+ HALT: 1,
+ DB1: 2,
+ DB2: 3,
+ DB3: 4,
+ DB4: 5,
+ DB5: 6,
+ DB6: 7,
+ SLEEP: 8,
+ RESET: 9,
+ FORK: 10,
+});
+
+
+// Main Bedrock emulator implementation.
+function Bedrock(emulatorElement, wasm=defaultToWasm) {
+ let core;
+ if (wasmModule && wasm) {
+ console.log('Instantiating Bedrock with WASM core');
+ core = new BedrockWasm();
+ } else {
+ console.log('Instantiating Bedrock with JS core');
+ core = new BedrockJs();
+ }
+
+ this.e = emulatorElement; // the linked EmulatorElement
+ this.dev = new DeviceBus(this);
+
+ this.init = async () => {
+ await core.init(this.dev.read, this.dev.write);
+ }
+
+ // Fully reset the emulator, and load in a new program.
+ this.load = (bytecode) => {
+ this.reset();
+ core.load(bytecode);
+ this.blank = false;
+ }
+
+ // Fully reset the emulator.
+ this.reset = () => {
+ core.reset();
+ this.dev.reset();
+ this.blank = true; // emulator has been fully reset, screen is blank.
+ this.paused = true; // program is not currently executing.
+ this.asleep = false; // program is waiting for input.
+ this.halted = false; // current program has ended.
+ this.frameMark = 0; // time when previous frame was rendered.
+ }
+
+ this.update = () => {
+ if (this.e) {
+ this.e.updateDOM();
+ }
+ }
+
+ this.render = () => {
+ if (this.e) {
+ if (this.dev.input.accessed || this.dev.screen.accessed) {
+ this.e.showScreenPanel();
+ }
+ let scr = this.dev.screen;
+ if (scr.dirty()) {
+ scr.render();
+ let image = new ImageData(scr.pixels, scr.width, scr.height);
+ scr.ctx.putImageData(image, 0, 0, scr.dx0, scr.dy0, scr.dx1, scr.dy1);
+ scr.unmark();
+ this.frameMark = performance.now();
+ }
+ }
+ }
+
+ // Start or unpause the program.
+ this.run = () => {
+ this.paused = false;
+ this.update();
+ this.runLoop();
+ }
+
+ // Pause the program, hold the current emulator state.
+ this.pause = () => {
+ this.paused = true;
+ this.update();
+ this.render();
+ }
+
+ // End and reset the program, allow it to run from the beginning.
+ this.stop = () => {
+ this.reset();
+ this.update();
+ this.render();
+ }
+
+ this.sleep = () => {
+ this.asleep = true;
+ this.update();
+ this.render();
+ this.sleepLoop();
+ }
+
+ // End the program, hold the final emulator state.
+ this.halt = () => {
+ this.halted = true;
+ this.update();
+ this.render();
+ }
+
+ // Run the program for a single cycle.
+ this.step = () => {
+ this.blank = false;
+ if (this.paused && !this.halted) {
+ if (this.asleep) {
+ setTimeout(this.sleepLoop.bind(this), 0);
+ return;
+ }
+ switch (core.eval(1)) {
+ case Signal.HALT: this.halt(); return;
+ case Signal.DB4: this.assert(); break;
+ // case Signal.RESET: this.reset(); break;
+ // case Signal.FORK: this.reset(); break;
+ case Signal.SLEEP: this.sleep(); return;
+ case Signal.DB1: this.pause(); break;
+ default: // NORMAL
+ }
+ this.update();
+ this.render();
+ }
+ }
+
+ // Run the program indefinitely.
+ this.runLoop = () => {
+ if (!this.halted && !this.paused) {
+ if (this.asleep) {
+ setTimeout(this.sleepLoop.bind(this), 0);
+ return;
+ }
+ // Tail-recursive with timeout to allow other code to run.
+ switch (core.eval(cyclesPerBatch)) {
+ case Signal.HALT: this.halt(); return;
+ case Signal.DB4: this.assert(); break;
+ // case Signal.RESET: this.reset(); break;
+ // case Signal.FORK: this.reset(); break;
+ case Signal.SLEEP: this.sleep(); return;
+ case Signal.DB1: this.pause(); break;
+ default: // NORMAL
+ }
+ this.update();
+ // Force a render if the frame has been running for too long.
+ if (performance.now() - this.frameMark > maxFrameTime) {
+ this.render();
+ }
+ setTimeout(this.runLoop.bind(this), 0);
+ }
+ }
+
+ // Wait for input before returning to runLoop.
+ this.sleepLoop = () => {
+ this.frameMark = performance.now();
+ if (!this.halted) {
+ if (!this.asleep) {
+ setTimeout(this.runLoop.bind(this), 0);
+ return;
+ }
+ let queue = this.dev.queue.get(this.dev.system.wakeMask);
+ for (let slot of queue) {
+ if (this.dev.devices[slot].wake()) {
+ this.asleep = false;
+ this.dev.system.wakeSlot = slot;
+ setTimeout(this.runLoop.bind(this));
+ return;
+ }
+ }
+ setTimeout(this.sleepLoop.bind(this), 0);
+ }
+ }
+
+ this.debugState = () => {
+ let wst = []; let wp = core.wp();
+ let rst = []; let rp = core.rp();
+ for (let i=0; i<wp; i++) wst.push(core.wst(i));
+ for (let i=0; i<rp; i++) rst.push(core.rst(i));
+ return {
+ cc: core.cc(),
+ ip: core.ip(),
+ wst, rst,
+ };
+ }
+
+ this.assert = () => {
+ if (core.wp() == 1 && core.rp() == 0 && core.wst(0) == 0xFF) {
+ core.std(0x86, 0x2E); // period
+ } else {
+ core.std(0x86, 0x58); // capital X
+ }
+ }
}
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: EMULATOR DEVICES :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
// Device bus for the core system.
function DeviceBus(br) {
this.system = new SystemDevice(br);
@@ -1414,12 +1083,13 @@ function DeviceBus(br) {
this.queue = new WakeQueue();
- this.reset = function() {
+ this.reset = () => {
for (let d of this.devices) d.reset(); }
- this.read = function(p) {
+ this.read = (p) => {
return this.devices[p >> 4].read(p & 0x0F); }
- this.write = function(p,v) {
- return this.devices[p >> 4].write(p & 0x0F, v); }
+ this.write = (p,v) => {
+ return this.devices[p >> 4].write(p & 0x0F, v);
+ }
}
@@ -1474,8 +1144,8 @@ function SystemDevice(br) {
this.write = function(p,v) {
switch (p) {
case 0x0: this.wakeMask = setH(this.wakeMask, v); break;
- case 0x1: this.wakeMask = setL(this.wakeMask, v); return 2;
- case 0x3: let s = v ? 3 : 4; return s;
+ case 0x1: this.wakeMask = setL(this.wakeMask, v); return Signal.SLEEP;
+ case 0x3: let s = v ? Signal.FORK : Signal.RESET; return s;
case 0x8: this.nameBuffer.reset(); break;
case 0x9: this.authorsBuffer.reset(); break;
default: return;
@@ -1772,8 +1442,8 @@ function InputDevice(br) {
this.write = function(p,v) {
this.accessed = true;
switch (p) {
- case 0xA: this.characterBytes = []; return;
- default: return;
+ case 0xA: this.characterBytes.clear(); return;
+ default: return;
}
}
this.wake = function() {
@@ -1882,7 +1552,7 @@ function InputDevice(br) {
function ScreenDevice(br) {
this.init = function() {
- this.ctx = br.e.canvas.getContext("2d", {
+ this.ctx = br.e.el.canvas.getContext("2d", {
alpha: false,
willReadFrequently: false,
});
@@ -1903,7 +1573,7 @@ function ScreenDevice(br) {
this.paletteW = 0;
this.selection = [0, 0, 0, 0];
this.sprite = new SpriteBuffer();
- let initialWidth = Math.trunc(br.e.clientWidth / initialScreenScale);
+ let initialWidth = Math.trunc(br.e.el.clientWidth / initialScreenScale);
let initialHeight = Math.trunc(initialWidth * 9/16);
this.width = -1; // screen won't resize if dimensions aren't different
this.resize(initialWidth, initialHeight, initialScreenScale);
@@ -2173,10 +1843,10 @@ function StreamDevice(br) {
}
this.write = function(p,v) {
switch (p) {
- case 0x3: br.e.endTransmission(); break;
+ case 0x3: br.e.endTransmission(); break;
case 0x6:
- case 0x7: br.e.receiveTransmissionByte(v); break;
- default: return;
+ case 0x7: br.e.receiveTransmissionByte(v); break;
+ default: return;
}
}
this.wake = function() {
@@ -2201,6 +1871,7 @@ function ClipboardDevice(br) {
}
this.write = function(p,v) {
// HACK: The -1 is to break the run loop to allow the br.pause() to take hold.
+ // A 0 wouldn't be raised, and the higher signals have other meanings.
switch (p) {
case 0x2: this.readEntry(v); return -1;
case 0x3: this.writeEntry(v); return -1;
@@ -2217,7 +1888,6 @@ function ClipboardDevice(br) {
this.readEntry = function(v) {
this.readQueue = [];
if (v) {
- // TODO: Figure out how to make this synchronous.
// HACK: Pause the emulator until promise resolves.
if (navigator.clipboard.readText) {
br.pause();
@@ -2238,20 +1908,13 @@ function ClipboardDevice(br) {
}
this.writeEntry = function(v) {
if (v) {
- // TODO: Figure out how to make this synchronous.
// HACK: Pause the emulator until promise resolves.
if (navigator.clipboard.writeText) {
br.pause();
let parser = new Utf8StreamParser();
parser.pushList(this.writeQueue);
- navigator.clipboard.writeText(parser.read()).then(
- () => {
- br.run();
- },
- () => {
- br.run();
- }
- );
+ navigator.clipboard.writeText(parser.read())
+ .finally(() => { br.run() });
}
} else {
// TODO: Look into valid clipboard binary formats.
@@ -2436,12 +2099,667 @@ function signed(v) { return v << 16 >> 16; }
function clamp(v,a,b) { return v>=a ? v<b ? v : b : a; } // inclusive a and b
+// Incrementally parse a UTF-8 string.
+function Utf8StreamParser() {
+ // Full characters parsed so far.
+ this.string = '';
+ // Current partial code point.
+ this.codePoint;
+ // True if the current code point is being dropped.
+ this.dropBytes = false;
+ // Number of bytes received of the current code point.
+ this.i = 0;
+ // Expected byte length of the current code point.
+ this.len = 0;
+
+ // Push a code point to the string.
+ let pushCodePoint = (codePoint) => {
+ this.string += String.fromCodePoint(codePoint);
+ }
+ // Drop the current partial code point, if any.
+ let dropCodePoint = () => {
+ // Push a 'replacement character' if there is a partial code point
+ // that hasn't already been dropped.
+ if (this.i && !this.dropBytes) pushCodePoint(0xFFFD);
+ this.codePoint = 0;
+ this.dropBytes = false;
+ this.i = 0;
+ this.len = 0;
+ }
+ // Parse the start of an encoded code point.
+ let parseStartByte = (byte, length) => {
+ dropCodePoint();
+ this.len = length;
+ parseContinuationByte(byte);
+ }
+ // Parse the remaining bytes of an encoded code point.
+ let parseContinuationByte = (byte) => {
+ if (!this.dropBytes) {
+ if (this.len) {
+ this.codePoint = this.codePoint << 6 | (byte & 0x3F);
+ this.i += 1;
+ if (this.i == this.len) {
+ pushCodePoint(this.codePoint);
+ this.len = 0;
+ }
+ } else {
+ this.dropBytes = true;
+ pushCodePoint(0xFFFD);
+ }
+ }
+ }
+
+ // Parse a UTF-8 encoded byte.
+ this.push = (byte) => {
+ if (byte < 0x80) { // start of a 1-byte code point
+ dropCodePoint();
+ pushCodePoint(byte);
+ } else if (byte < 0xC0) { // not a start byte
+ parseContinuationByte(byte);
+ } else if (byte < 0xE0) { // start of a 2-byte code point
+ parseStartByte(byte, 2);
+ } else if (byte < 0xF0) { // start of a 3-byte code point
+ parseStartByte(byte, 3);
+ } else if (byte < 0xF8) { // start of a 4-byte code point
+ parseStartByte(byte, 4);
+ }
+ }
+ // Parse an array of bytes.
+ this.pushList = (list) => {
+ for (let byte of list) this.push(byte);
+ }
+
+ // Take the characters that have been parsed so far.
+ this.read = () => {
+ let output = this.string;
+ this.string = '';
+ return output;
+ }
+}
+
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: ASSEMBLER ELEMENT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+// Constructor for an interactive assembler element.
+function AssemblerElement(element) {
+ this.el = instantiateFromTemplate(assemblerTemplate);
+ this.el.textarea = this.el.querySelector('textarea');
+ this.el.viewer = this.el.querySelector('.viewer');
+ this.el.editor = this.el.querySelector('.editor');
+ this.el.errorPanel = this.el.querySelector('.panel.errors');
+ this.el.bytecodePanel = this.el.querySelector('.panel.bytecode');
+ this.el.status = this.el.querySelector('.status');
+ this.el.checkButton = this.el.querySelector('button[name="check"]');
+ this.el.runButton = this.el.querySelector('button[name="run"]');
+ this.el.fullscreenButton = this.el.querySelector('button[name="fullscreen"]');
+
+ // Copy the text content of a passed element into the editor.
+ if (element) {
+ this.el.textarea.value = element.textContent.trim();
+ this.el.textarea.style.height = element.clientHeight + 'px';
+ }
+
+ this.autoGrow = true; // increase height as text is entered
+ this.emulator = null; // linked emulator element, if any
+
+ // Show and hide the error and bytecode panels.
+ this.showErrorPanel = () => {
+ this.hideBytecodePanel();
+ this.el.errorPanel.classList.remove('hidden'); }
+ this.hideErrorPanel = () => {
+ this.el.errorPanel.classList.add('hidden'); }
+ this.showBytecodePanel = () => {
+ this.hideErrorPanel();
+ this.el.bytecodePanel.classList.remove('hidden'); }
+ this.hideBytecodePanel = () => {
+ this.el.bytecodePanel.classList.add('hidden'); }
+
+ // Re-align the viewer to the textarea by setting the viewer scroll position.
+ this.syncScroll = () => {
+ this.el.viewer.scrollTop = this.el.textarea.scrollTop;
+ this.el.viewer.scrollLeft = this.el.textarea.scrollLeft;
+ }
+
+ // Re-align the viewer to the textarea by setting the viewer height.
+ this.syncHeight = () => {
+ // Disable auto-grow if editor was shrunk by the user.
+ if (this.el.textarea.clientHeight < this.el.viewer.clientHeight) {
+ this.autoGrow = false; }
+ let height = this.el.textarea.clientHeight + 'px';
+ this.el.viewer.style.height = height;
+ this.el.editor.style.height = height;
+ }
+
+ // Copy text from textarea to viewer with syntax highlighting.
+ this.renderText = () => {
+ // HACK: Force viewer to show trailing newlines.
+ let text = this.el.textarea.value;
+ if (text.slice(-1) == "\n") text += ' ';
+ // Replace contents of viewer.
+ this.el.viewer.innerHTML = '';
+ for (let node of highlightSource(text)) {
+ this.el.viewer.appendChild(node); }
+ // Auto-grow the text area as text is entered.
+ if (this.autoGrow && this.el.textarea.scrollHeight > this.el.textarea.clientHeight) {
+ this.el.textarea.style.height = this.el.textarea.scrollHeight + 'px'; }
+ // Lock viewer to textarea.
+ this.syncScroll();
+ }
+
+ // Assemble the program and show bytecode or errors.
+ this.checkProgram = () => {
+ let { bytecode, symbols, errors } = assembleSource(this.el.textarea.value);
+ this.el.errorPanel.innerHTML = '';
+ this.el.bytecodePanel.innerHTML = '';
+ if (errors.length) {
+ let list = document.createElement('ul');
+ for (let error of errors) {
+ let item = document.createElement('li');
+ let line = error.start.line + 1;
+ let column = error.start.column + 1;
+ item.textContent = `[${line}:${column}] ${error.text}`;
+ list.appendChild(item); }
+ this.el.errorPanel.appendChild(list);
+ let unit = errors.length == 1 ? 'error' : 'errors';
+ this.el.status.textContent = `${errors.length} ${unit}`;
+ this.showErrorPanel();
+ } else {
+ let unit = bytecode.length == 1 ? 'byte' : 'bytes';
+ this.el.status.textContent = `no errors (${bytecode.length} ${unit})`;
+ let programListing = document.createElement('div');
+ programListing.textContent = hexArray(bytecode);
+ this.el.bytecodePanel.appendChild(programListing);
+ this.hideErrorPanel();
+ this.hideBytecodePanel();
+ if (bytecode.length) this.showBytecodePanel();
+ return { bytecode, symbols };
+ }
+ }
+
+ // Assemble the program and run in an emulator.
+ this.runProgram = () => {
+ // Check and assemble program.
+ let program = this.checkProgram();
+ if (program) {
+ this.hideBytecodePanel();
+ let { bytecode, symbols } = program;
+ if (this.emulator) {
+ this.emulator.showStatePanel();
+ this.emulator.startProgram(bytecode, symbols);
+ } else {
+ // Instantiate a new emulator element if none exists.
+ this.emulator = new EmulatorElement();
+ this.emulator.assembler = this;
+ this.el.parentElement.insertBefore(this.emulator.el, this.el.nextSibling);
+ this.emulator.init().then(() => {
+ this.emulator.showStatePanel();
+ this.emulator.startProgram(bytecode, symbols);
+ })
+ }
+ }
+ }
+
+ // Toggle the fullscreen state of the whole assembler element.
+ this.toggleFullscreen = () => {
+ if (document.fullscreenElement != this.el) {
+ this.el.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+ }
+
+ // Handle keypresses on the whole assembler.
+ this.onKeyPress = (event) => {
+ if (event.key == "Escape") {
+ event.preventDefault();
+ this.hideErrorPanel();
+ this.hideBytecodePanel();
+ } else if (event.key == "s" && event.ctrlKey) {
+ event.preventDefault();
+ this.checkProgram();
+ }
+ }
+
+ // Handle keypresses on just the editor.
+ this.onEditorKeyPress = (event) => {
+ let indent = ' ';
+ let text = this.el.textarea.value;
+ let selectionStart = this.el.textarea.selectionStart;
+ let selectionEnd = this.el.textarea.selectionEnd;
+
+ // Find the character index following the previous newline character.
+ function findLineStart(i) {
+ while (i>0 && text[i-1] != '\n') i -= 1;
+ return i; }
+
+ if (event.key == "Enter" && event.ctrlKey) {
+ event.preventDefault();
+ this.runProgram();
+ } else if (event.key == "Tab") {
+ event.preventDefault();
+ // Find the start index of every line in the selection.
+ let i = findLineStart(selectionStart);
+ let lineStarts = [i];
+ for (; i<selectionEnd; i++) if (text[i] == '\n') lineStarts.push(i+1);
+ if (event.shiftKey) {
+ // Find length of the leading whitespace in each line.
+ let whitespaceLengths = [];
+ for (let start of lineStarts) {
+ let end = start;
+ while ('\t '.includes(text[end])) end++;
+ whitespaceLengths.push(end-start); }
+ // Unindent each line in reverse order.
+ selectionStart -= Math.min(tabSize, whitespaceLengths[0]);
+ for (let i=lineStarts.length-1; i>=0; i--) {
+ let start = lineStarts[i];
+ let trim = Math.min(tabSize, whitespaceLengths[i]);
+ text = text.slice(0, start) + text.slice(start+trim);
+ selectionEnd -= trim; }
+ } else {
+ // Indent each line in reverse order.
+ selectionStart += tabSize;
+ for (let i=lineStarts.length-1; i>=0; i--) {
+ let start = lineStarts[i];
+ text = text.slice(0, start) + indent + text.slice(start);
+ selectionEnd += tabSize; }
+ }
+ // Update textarea content and viewer.
+ this.el.textarea.value = text;
+ this.el.textarea.selectionStart = selectionStart;
+ this.el.textarea.selectionEnd = selectionEnd;
+ this.renderText();
+ }
+ }
+
+ this.el.textarea.addEventListener('input', this.renderText);
+ this.el.textarea.addEventListener('scroll', this.syncScroll);
+ this.el.textarea.addEventListener('keydown', this.onEditorKeyPress);
+ this.el.addEventListener('keydown', this.onKeyPress);
+ this.el.checkButton.addEventListener('click', this.checkProgram);
+ this.el.runButton.addEventListener('click', this.runProgram);
+ this.el.fullscreenButton.addEventListener('click', this.toggleFullscreen);
+
+ this.el.resizeObserver = new ResizeObserver(this.syncHeight).observe(this.el.textarea);
+ this.el.textarea.selectionStart = 0;
+ this.el.textarea.selectionEnd = 0;
+ this.el.textarea.scrollLeft = 0;
+ this.renderText();
+}
+
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: EMULATOR ELEMENT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+/// Constructor for a interactive emulator element.
+function EmulatorElement(options) {
+ this.el = instantiateFromTemplate(emulatorTemplate);
+ // Menu bar
+ this.el.menuBar = this.el.querySelector('.menubar');
+ this.el.fullscreenButton = this.el.menuBar.querySelector('button[name="fullscreen"]');
+ this.el.stateButton = this.el.menuBar.querySelector('button[name="state"]');
+ this.el.runButton = this.el.menuBar.querySelector('button[name="run"]');
+ this.el.pauseButton = this.el.menuBar.querySelector('button[name="pause"]');
+ this.el.stopButton = this.el.menuBar.querySelector('button[name="stop"]');
+ this.el.stepButton = this.el.menuBar.querySelector('button[name="step"]');
+ this.el.status = this.el.menuBar.querySelector('.status');
+ // Screen panel
+ this.el.screenPanel = this.el.querySelector('.screen');
+ this.el.canvas = this.el.screenPanel.querySelector('canvas');
+ // Stream panel
+ this.el.streamPanel = this.el.querySelector('.stream');
+ this.el.transmissions = this.el.querySelector('.transmissions');
+ // State panel
+ this.el.statePanel = this.el.querySelector('.panel.state');
+ this.el.ipValue = this.el.statePanel.querySelector('#ip .value');
+ this.el.ipNote = this.el.statePanel.querySelector('#ip .note');
+ this.el.ccValue = this.el.statePanel.querySelector('#cc .value');
+ this.el.ccNote = this.el.statePanel.querySelector('#cc .note');
+ this.el.wstValue = this.el.statePanel.querySelector('#wst .value');
+ this.el.rstValue = this.el.statePanel.querySelector('#rst .value');
+
+ let withWasm = options ? options.wasm : undefined;
+ this.br = new Bedrock(this, withWasm);
+ this.bytecode = options ? options.bytecode : new Uint8Array(0);
+ this.symbols = new SymbolTable();
+
+ // Buffered transmission handling for stream device.
+ let currentTransmission = null;
+ let transmissionParser = new Utf8StreamParser();
+
+ // Initialise emulator and emulator element.
+ this.init = async () => {
+ await this.br.init();
+ this.br.reset();
+ this.br.onUpdate = () => this.updateDOM();
+
+ // Apply options.
+ if (options && !options.controls) this.hideMenuBar();
+ if (options && options.autoplay) this.runProgram();
+ if (options && options.nocursor) this.el.canvas.style.cursor = 'none';
+
+ // Make canvas focusable to be able to receive keydown and keyup events.
+ this.el.canvas.tabIndex = 0;
+
+ this.el.fullscreenButton.addEventListener('click', this.toggleFullscreen);
+ this.el.stateButton.addEventListener('click', this.toggleStatePanel);
+ this.el.runButton.addEventListener('click', this.runProgram);
+ this.el.pauseButton.addEventListener('click', this.pauseProgram);
+ this.el.stopButton.addEventListener('click', this.stopProgram);
+ this.el.stepButton.addEventListener('click', this.br.step);
+ this.resizeObserver = new ResizeObserver(this.updateScreenSize).observe(this.el.screenPanel);
+
+ this.el.canvas.addEventListener('mousemove', this.mouseMove);
+ this.el.canvas.addEventListener('pointermove', this.mouseMove);
+ this.el.canvas.addEventListener('mousedown', this.mouseDown);
+ this.el.canvas.addEventListener('mouseup', this.mouseUp);
+ this.el.canvas.addEventListener('touchstart', this.touchStart);
+ this.el.canvas.addEventListener('touchend', this.touchEnd);
+ this.el.canvas.addEventListener('touchcancel', this.touchEnd);
+ this.el.canvas.addEventListener('mouseenter', this.mouseEnter);
+ this.el.canvas.addEventListener('mouseleave', this.mouseExit);
+ this.el.canvas.addEventListener('wheel', this.mouseScroll);
+ this.el.canvas.addEventListener('contextmenu', (e)=>{e.preventDefault()});
+ this.el.canvas.addEventListener('keydown', (e)=>this.keyInput(e, true));
+ this.el.canvas.addEventListener('keyup', (e)=>this.keyInput(e, false));
+ }
+
+ this.showStatePanel = () => {
+ this.el.statePanel.classList.remove('hidden'); }
+ this.toggleStatePanel = () => {
+ this.el.statePanel.classList.toggle('hidden'); }
+ this.showStreamPanel = () => {
+ this.el.streamPanel.classList.remove('hidden'); }
+ this.hideStreamPanel = () => {
+ this.el.streamPanel.classList.add('hidden'); }
+ this.showScreenPanel = () => {
+ this.el.screenPanel.classList.remove('hidden'); }
+ this.hideScreenPanel = () => {
+ this.el.screenPanel.classList.add('hidden'); }
+ this.hideMenuBar = () => {
+ this.el.menuBar.classList.add('hidden'); }
+
+ // Reset the emulator, load and run a new program.
+ this.startProgram = (bytecode, symbols) => {
+ this.stopProgram();
+ this.bytecode = bytecode;
+ this.symbols = symbols;
+ this.br.load(bytecode);
+ this.br.run();
+ }
+
+ // Fires when the stop button is pressed.
+ this.stopProgram = () => {
+ this.br.stop();
+ this.updateDOM();
+ this.el.transmissions.innerHTML = '';
+ this.hideStreamPanel();
+ this.hideScreenPanel();
+ currentTransmission = null;
+ }
+
+ // Fires when the play button is pressed.
+ this.runProgram = () => {
+ if (this.br.blank) {
+ this.startProgram(this.bytecode, this.symbols);
+ } else {
+ this.br.run();
+ }
+ }
+
+ // Fires when the pause button is pressed.
+ this.pauseProgram = () => {
+ this.br.pause();
+ }
+
+ // Update information displayed in the emulator frame.
+ this.updateDOM = () => {
+ let { cc, ip, wst, rst } = this.br.debugState();
+ this.el.ipValue.textContent = hex(ip, 4);
+ this.el.ipNote.textContent = this.symbols.findSymbol(ip);
+ this.el.ccValue.textContent = cc;
+ this.el.wstValue.textContent = hexArray(wst);
+ this.el.rstValue.textContent = hexArray(rst);
+ if (this.br.halted) {
+ this.el.status.textContent = 'ended';
+ } else if (this.br.blank) {
+ this.el.status.textContent = '';
+ } else if (this.br.paused) {
+ this.el.status.textContent = 'paused';
+ } else if (this.br.asleep) {
+ let wakeMask = this.br.dev.system.wakeMask;
+ this.el.status.textContent = `sleeping / 0x${hex(wakeMask, 4)}`;
+ } else {
+ this.el.status.textContent = `running / ${cc}`;
+ }
+ this.flushTransmission();
+ }
+
+ this.updateScreenSize = () => {
+ if (!this.el.screenPanel.classList.contains('hidden')) {
+ if (document.fullscreenElement == this.el) {
+ let width = this.el.screenPanel.clientWidth;
+ let height = this.el.screenPanel.clientHeight;
+ this.br.dev.screen.resizeToFill(width, height);
+ } else {
+ let width = this.el.screenPanel.clientWidth;
+ let height = width * 9/16;
+ this.br.dev.screen.resizeToFill(width, height);
+ }
+ }
+ }
+
+ // Receive an incoming byte from the local stream.
+ this.receiveTransmissionByte = (byte) => {
+ transmissionParser.push(byte);
+ }
+
+ // Push all received bytes to the DOM.
+ this.flushTransmission = () => {
+ let string = transmissionParser.read();
+ if (string) {
+ if (!currentTransmission) {
+ this.showStreamPanel();
+ let element = document.createElement('li');
+ element.addEventListener('click', () => {
+ copyText(element.textContent); })
+ currentTransmission = element;
+ this.el.transmissions.appendChild(element);
+ // Prune old transmissions.
+ let count = Math.max(this.el.transmissions.children.length - maxTransmissions, 0);
+ for (let i=0; i<count; i++) this.el.transmissions.children[0].remove();
+ }
+ currentTransmission.textContent += string;
+ this.el.streamPanel.scrollTop = this.el.streamPanel.scrollHeight;
+ }
+ }
+
+ // End the current incoming transmission.
+ this.endTransmission = () => {
+ this.flushTransmission();
+ currentTransmission = null;
+ }
+
+ // Change the fullscreen state of the whole emulator.
+ this.toggleFullscreen = () => {
+ if (document.fullscreenElement != this.el) {
+ this.el.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+ }
+
+ this.mouseMove = (e) => {
+ let bounds = this.el.canvas.getBoundingClientRect();
+ let scale = this.br.dev.screen.scale;
+ let x = ((e.clientX - bounds.left) / scale) & 0xFFFF;
+ let y = ((e.clientY - bounds.top ) / scale) & 0xFFFF;
+ this.br.dev.input.applyPosition(x, y);
+ }
+
+ this.mouseDown = (e) => { this.br.dev.input.applyButtons(e.buttons); }
+ this.mouseUp = (e) => { this.br.dev.input.applyButtons(e.buttons); }
+
+ this.mouseEnter = (e) => {
+ this.br.dev.input.applyActive(true);
+ this.mouseMove(e);
+ }
+
+ this.mouseExit = (e) => {
+ // HACK: Force buttons to release when the mouse leaves the screen,
+ // otherwise any buttons that were held down when the mouse leaves
+ // will be stuck down when the mouse re-enters, even if they were
+ // released off-screen.
+ // TODO: Figure out how to attach a listener to the window as a whole
+ // that can tell each emulator how the mouse behaves off-screen, so
+ // that the mouse can be dragged off and onto a window in one
+ // continuous stroke.
+ this.br.dev.input.applyButtons(0x00);
+ this.br.dev.input.applyActive(false);
+ this.mouseMove(e);
+ }
+
+ this.mouseScroll = (e) => {
+ // Only capture scroll events if canvas is focused.
+ if (document.activeElement == this.el.canvas) {
+ e.preventDefault();
+ let scale;
+ // TODO: Dial these numbers in a bit.
+ switch (e.deltaMode) {
+ case (e.DOM_DELTA_PIXEL): scale = 1/10; break;
+ case (e.DOM_DELTA_LINE): scale = 1; break;
+ case (e.DOM_DELTA_PAGE): scale = 20; break;
+ };
+ this.br.dev.input.applyHScroll(e.deltaX * scale);
+ this.br.dev.input.applyVScroll(e.deltaY * scale);
+ }
+ }
+
+ this.keyInput = (e, pressed) => {
+ e.preventDefault();
+ this.br.dev.input.applyModifiers(e);
+ if (!e.repeat && !e.isComposing) {
+ this.br.dev.input.applyKey(e, pressed);
+ }
+ }
+
+ this.touchStart = (e) => {
+ if (e.changedTouches.length) {
+ this.mouseMove(e.changedTouches[0]); }
+ this.br.dev.input.applyActive(true);
+ this.br.dev.input.applyButtons(0x01);
+ }
+
+ this.touchEnd = (e) => {
+ this.br.dev.input.applyActive(false);
+ this.br.dev.input.applyButtons(0x00);
+ }
+}
+
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: METADATA :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+function parseMetadata(bytecode) {
+ // Test identifier to see if program has metadata.
+ let id = [0xE8,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B];
+ for (let i=0; i<10; i++) if (bytecode[i] != id[i]) return;
+ // Parse each metadata item.
+ let name = getString(0x000A);
+ let authors = getString(0x000C);
+ let description = getString(0x000E);
+ let bgColour = getColour(0x0010);
+ let fgColour = getColour(0x0012);
+ let smallIcon = getIcon(0x0014, 24);
+ let largeIcon = getIcon(0x0016, 64);
+ return { name, authors, description, bgColour, fgColour, smallIcon, largeIcon };
+
+ function getByte(addr) {
+ return bytecode[addr] || 0;
+ }
+ function getDouble(addr) {
+ return getByte(addr) << 8 | getByte(addr+1);
+ }
+ function getString(addr) {
+ addr = getDouble(addr);
+ if (addr) {
+ let parser = new Utf8StreamParser();
+ while (true) {
+ let byte = bytecode[addr++];
+ if (!byte) break;
+ parser.push(byte);
+ }
+ return parser.read();
+ }
+ }
+ function getColour(addr) {
+ addr = getDouble(addr);
+ if (addr) {
+ let colour = getDouble(addr);
+ let r = (colour >> 8 & 0xF) * 17;
+ let g = (colour >> 8 & 0xF) * 17;
+ let b = (colour >> 8 & 0xF) * 17;
+ return [r, g, b];
+ }
+ }
+ function getIcon(addr, size) {
+ // TODO: Return a custom type containing the dimensions and raw
+ // pixel data, and a colourise method to generate a coloured ImageData.
+ }
+}
+
+
// ----------------------------------------------------------------------------------------------- +
-// :::::: CONSTANTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// :::::: UTILITIES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
// ----------------------------------------------------------------------------------------------- +
+// Convert an integer to a hexadecimal string.
+function hex(value, pad=2) {
+ return value.toString(16).toUpperCase().padStart(pad, '0');
+}
+
+// Convert a whole byte array to a hexadecimal string.
+function hexArray(array) {
+ let string = '';
+ for (let byte of array) string += hex(byte) + ' ';
+ return string;
+}
+
+// Instantiate an element from an HTML string.
+function instantiateFromTemplate(html) {
+ let template = document.createElement('template');
+ template.innerHTML = html.trim();
+ return template.content.firstChild;
+}
+
+// Copy a string to the clipboard.
+function copyText(text) {
+ navigator.clipboard.writeText(text);
+}
+
+let encoder = new TextEncoder();
+function encodeUtf8(text) {
+ return encoder.encode(text);
+}
+
+
+// ----------------------------------------------------------------------------------------------- +
+// :::::: HTML CSS SVG :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +
+// ----------------------------------------------------------------------------------------------- +
+
+
+// Link to the bedrock-js project page on benbridle.com. Includes the bedrock logo as an SVG.
const projectLink = `
<a href='https://benbridle.com/bedrock-js' target='_blank'>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8' height='8'>
@@ -2507,7 +2825,6 @@ const assemblerTemplate = `
</div>
`;
-
const emulatorTemplate = `
<div class='bedrock emulator'>
<div class='menubar'>
@@ -2521,17 +2838,17 @@ const emulatorTemplate = `
${projectLink}
</div>
<div class='hidden panel state'><ul>
- <li>PC: <span class='pc'></span> <span class='label'></span></li>
- <li>WST: <span class='wst'></span></li>
- <li>RST: <span class='rst'></span></li>
+ <li id='ip' > IP: <span class='value'></span> <span class='note'></span></li>
+ <li id='cc' > CC: <span class='value'></span> <span class='note'></span></li>
+ <li id='wst'>WST: <span class='value'></span></li>
+ <li id='rst'>RST: <span class='value'></span></li>
</ul></div>
<div class='hidden stream'><ul class='transmissions'></ul></div>
<div class='hidden screen'><canvas></canvas></div>
</div>
`;
-
-const bedrockStyles = `
+const bedrockCSS = `
div.bedrock {
--br-fg:var(--fg,#222); --br-bg:var(--bg,#eed); --br-accent:var(--accent,#467); }
div.bedrock * {
@@ -2586,7 +2903,7 @@ const bedrockStyles = `
overflow:scroll; margin:0; list-style:none; }
.bedrock .panel li {
white-space:pre; }
- .bedrock .panel .label {
+ .bedrock .panel .note {
opacity:60%; }
.bedrock .stream {
@@ -2606,43 +2923,3 @@ const bedrockStyles = `
.bedrock:fullscreen .stream {
max-height:none; }
`;
-
-
-const instructionTable = {
- 'HLT': 0x00, 'NOP' : 0x20, 'DB1' : 0x40, 'DB2' : 0x60, 'DB3' : 0x80, 'DB4' : 0xA0, 'DB5' : 0xC0, 'DB6' : 0xE0,
- 'PSH': 0x01, 'PSH:': 0x21, 'PSH*': 0x41, 'PSH*:': 0x61, 'PSHr': 0x81, 'PSHr:': 0xA1, 'PSHr*': 0xC1, 'PSHr*:': 0xE1,
- ':': 0x21, '*:': 0x61, 'r:': 0xA1, 'r*:': 0xE1,
- 'POP': 0x02, 'POP:': 0x22, 'POP*': 0x42, 'POP*:': 0x62, 'POPr': 0x82, 'POPr:': 0xA2, 'POPr*': 0xC2, 'POPr*:': 0xE2,
- 'CPY': 0x03, 'CPY:': 0x23, 'CPY*': 0x43, 'CPY*:': 0x63, 'CPYr': 0x83, 'CPYr:': 0xA3, 'CPYr*': 0xC3, 'CPYr*:': 0xE3,
- 'DUP': 0x04, 'DUP:': 0x24, 'DUP*': 0x44, 'DUP*:': 0x64, 'DUPr': 0x84, 'DUPr:': 0xA4, 'DUPr*': 0xC4, 'DUPr*:': 0xE4,
- 'OVR': 0x05, 'OVR:': 0x25, 'OVR*': 0x45, 'OVR*:': 0x65, 'OVRr': 0x85, 'OVRr:': 0xA5, 'OVRr*': 0xC5, 'OVRr*:': 0xE5,
- 'SWP': 0x06, 'SWP:': 0x26, 'SWP*': 0x46, 'SWP*:': 0x66, 'SWPr': 0x86, 'SWPr:': 0xA6, 'SWPr*': 0xC6, 'SWPr*:': 0xE6,
- 'ROT': 0x07, 'ROT:': 0x27, 'ROT*': 0x47, 'ROT*:': 0x67, 'ROTr': 0x87, 'ROTr:': 0xA7, 'ROTr*': 0xC7, 'ROTr*:': 0xE7,
- 'JMP': 0x08, 'JMP:': 0x28, 'JMP*': 0x48, 'JMP*:': 0x68, 'JMPr': 0x88, 'JMPr:': 0xA8, 'JMPr*': 0xC8, 'JMPr*:': 0xE8,
- 'JMS': 0x09, 'JMS:': 0x29, 'JMS*': 0x49, 'JMS*:': 0x69, 'JMSr': 0x89, 'JMSr:': 0xA9, 'JMSr*': 0xC9, 'JMSr*:': 0xE9,
- 'JCN': 0x0A, 'JCN:': 0x2A, 'JCN*': 0x4A, 'JCN*:': 0x6A, 'JCNr': 0x8A, 'JCNr:': 0xAA, 'JCNr*': 0xCA, 'JCNr*:': 0xEA,
- 'JCS': 0x0B, 'JCS:': 0x2B, 'JCS*': 0x4B, 'JCS*:': 0x6B, 'JCSr': 0x8B, 'JCSr:': 0xAB, 'JCSr*': 0xCB, 'JCSr*:': 0xEB,
- 'LDA': 0x0C, 'LDA:': 0x2C, 'LDA*': 0x4C, 'LDA*:': 0x6C, 'LDAr': 0x8C, 'LDAr:': 0xAC, 'LDAr*': 0xCC, 'LDAr*:': 0xEC,
- 'STA': 0x0D, 'STA:': 0x2D, 'STA*': 0x4D, 'STA*:': 0x6D, 'STAr': 0x8D, 'STAr:': 0xAD, 'STAr*': 0xCD, 'STAr*:': 0xED,
- 'LDD': 0x0E, 'LDD:': 0x2E, 'LDD*': 0x4E, 'LDD*:': 0x6E, 'LDDr': 0x8E, 'LDDr:': 0xAE, 'LDDr*': 0xCE, 'LDDr*:': 0xEE,
- 'STD': 0x0F, 'STD:': 0x2F, 'STD*': 0x4F, 'STD*:': 0x6F, 'STDr': 0x8F, 'STDr:': 0xAF, 'STDr*': 0xCF, 'STDr*:': 0xEF,
- 'ADD': 0x10, 'ADD:': 0x30, 'ADD*': 0x50, 'ADD*:': 0x70, 'ADDr': 0x90, 'ADDr:': 0xB0, 'ADDr*': 0xD0, 'ADDr*:': 0xF0,
- 'SUB': 0x11, 'SUB:': 0x31, 'SUB*': 0x51, 'SUB*:': 0x71, 'SUBr': 0x91, 'SUBr:': 0xB1, 'SUBr*': 0xD1, 'SUBr*:': 0xF1,
- 'INC': 0x12, 'INC:': 0x32, 'INC*': 0x52, 'INC*:': 0x72, 'INCr': 0x92, 'INCr:': 0xB2, 'INCr*': 0xD2, 'INCr*:': 0xF2,
- 'DEC': 0x13, 'DEC:': 0x33, 'DEC*': 0x53, 'DEC*:': 0x73, 'DECr': 0x93, 'DECr:': 0xB3, 'DECr*': 0xD3, 'DECr*:': 0xF3,
- 'LTH': 0x14, 'LTH:': 0x34, 'LTH*': 0x54, 'LTH*:': 0x74, 'LTHr': 0x94, 'LTHr:': 0xB4, 'LTHr*': 0xD4, 'LTHr*:': 0xF4,
- 'GTH': 0x15, 'GTH:': 0x35, 'GTH*': 0x55, 'GTH*:': 0x75, 'GTHr': 0x95, 'GTHr:': 0xB5, 'GTHr*': 0xD5, 'GTHr*:': 0xF5,
- 'EQU': 0x16, 'EQU:': 0x36, 'EQU*': 0x56, 'EQU*:': 0x76, 'EQUr': 0x96, 'EQUr:': 0xB6, 'EQUr*': 0xD6, 'EQUr*:': 0xF6,
- 'NQK': 0x17, 'NQK:': 0x37, 'NQK*': 0x57, 'NQK*:': 0x77, 'NQKr': 0x97, 'NQKr:': 0xB7, 'NQKr*': 0xD7, 'NQKr*:': 0xF7,
- 'SHL': 0x18, 'SHL:': 0x38, 'SHL*': 0x58, 'SHL*:': 0x78, 'SHLr': 0x98, 'SHLr:': 0xB8, 'SHLr*': 0xD8, 'SHLr*:': 0xF8,
- 'SHR': 0x19, 'SHR:': 0x39, 'SHR*': 0x59, 'SHR*:': 0x79, 'SHRr': 0x99, 'SHRr:': 0xB9, 'SHRr*': 0xD9, 'SHRr*:': 0xF9,
- 'ROL': 0x1A, 'ROL:': 0x3A, 'ROL*': 0x5A, 'ROL*:': 0x7A, 'ROLr': 0x9A, 'ROLr:': 0xBA, 'ROLr*': 0xDA, 'ROLr*:': 0xFA,
- 'ROR': 0x1B, 'ROR:': 0x3B, 'ROR*': 0x5B, 'ROR*:': 0x7B, 'RORr': 0x9B, 'RORr:': 0xBB, 'RORr*': 0xDB, 'RORr*:': 0xFB,
- 'IOR': 0x1C, 'IOR:': 0x3C, 'IOR*': 0x5C, 'IOR*:': 0x7C, 'IORr': 0x9C, 'IORr:': 0xBC, 'IORr*': 0xDC, 'IORr*:': 0xFC,
- 'XOR': 0x1D, 'XOR:': 0x3D, 'XOR*': 0x5D, 'XOR*:': 0x7D, 'XORr': 0x9D, 'XORr:': 0xBD, 'XORr*': 0xDD, 'XORr*:': 0xFD,
- 'AND': 0x1E, 'AND:': 0x3E, 'AND*': 0x5E, 'AND*:': 0x7E, 'ANDr': 0x9E, 'ANDr:': 0xBE, 'ANDr*': 0xDE, 'ANDr*:': 0xFE,
- 'NOT': 0x1F, 'NOT:': 0x3F, 'NOT*': 0x5F, 'NOT*:': 0x7F, 'NOTr': 0x9F, 'NOTr:': 0xBF, 'NOTr*': 0xDF, 'NOTr*:': 0xFF,
-
- // TODO: Remove.
- '?':0x40,
-};