diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-09-12 17:20:19 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-09-12 17:28:44 +1200 |
commit | 19c5679b8be4fe879e165576e97f96b85f5b5044 (patch) | |
tree | de10dbc9a23514adbddfd563d38b1238dfce6241 /bedrock.js | |
parent | 4c67a10f265dde1c3aa430cb851062a38646d6f2 (diff) | |
download | bedrock-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.js | 2211 |
1 files changed, 1244 insertions, 967 deletions
@@ -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, -}; |