diff options
Diffstat (limited to 'bedrock.js')
-rw-r--r-- | bedrock.js | 2604 |
1 files changed, 2604 insertions, 0 deletions
diff --git a/bedrock.js b/bedrock.js new file mode 100644 index 0000000..e3a2041 --- /dev/null +++ b/bedrock.js @@ -0,0 +1,2604 @@ +"use strict"; + +let systemName = "bedrock-js"; +let systemVersion = "0.1.0"; +let systemAuthors = "Ben Bridle"; +let initialScreenScale = 2; + + +// Upgrade every pre.bedrock element to a full assembler, and every +// bedrock to a full emulator. +window.addEventListener('DOMContentLoaded', function() { + injectStyles(); + + for (let element of document.querySelectorAll('pre.bedrock')) { + // Click on the element to upgrade to an assembler. + element.style.cursor = 'pointer'; + element.addEventListener('click', function(e) { + e.preventDefault(); + upgradeToAssembler(element); + }) + // Prevent text selection on the element. + element.addEventListener('selectstart', function(e) { + e.preventDefault(); + }) + } + for (let element of document.querySelectorAll('bedrock')) { + upgradeToEmulator(element); + } +}) + + +// Insert the styles for the assembler and emulator into the page. +function injectStyles() { + let css = document.createElement('style'); + css.textContent = bedrockStyles; + document.head.appendChild(css); +} + + +// Upgrade a text 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. + let windowHeight = document.documentElement.clientHeight; + let documentHeight = document.documentElement.scrollHeight; + let belowViewport = documentHeight - (window.scrollY + windowHeight); + let elementHeight = element.scrollHeight; + let spacerHeight = Math.max(0, elementHeight - belowViewport); + let spacer = document.createElement('div'); + spacer.style.height = spacerHeight + 'px'; + element.parentElement.insertBefore(spacer, element.nextSibling); + + element.replaceWith(assembler); + + // 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; + if (topStart != topEnd) { + window.scrollTo({top: scrollStart, behavior: 'instant'}); + // Wait for the document layout to settle. + setTimeout(() => { + topEnd = assembler.getBoundingClientRect().top; + let topTarget = (topEnd - topStart) + window.scrollY; + window.scrollTo({top: topTarget, behavior: 'instant'}); + }); + } + + return assembler; +} + + +// Upgrade a bedrock element with attributes to a full emulator. +function upgradeToEmulator(element) { + 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, + 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) { + 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.updateStatePanel(); + + 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.updateStatePanel(); + transmissions.innerHTML = ''; + emulator.hideStreamPanel(); + emulator.hideScreenPanel(); + currentTransmission = null; + } + + emulator.updateStatePanel = function() { + function renderStack(stack) { + let string = ''; + for (let i=0; i<stack.p; 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.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) { + if (!currentTransmission) { + emulator.showStreamPanel(); + let element = document.createElement('li'); + element.addEventListener('click', function() { + copyText(element.textContent); }) + currentTransmission = element; + transmissions.appendChild(element); } + transmissionParser.push(byte); + currentTransmission.textContent += transmissionParser.read(); + streamPanel.scrollTop = streamPanel.scrollHeight; + } + + // End the current incoming transmission. + emulator.endTransmission = function() { + 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; break; + case (e.DOM_DELTA_LINE): scale = 5; break; + case (e.DOM_DELTA_PAGE): scale = 30; 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); + } + } + + 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('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(); + + 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); + } + } + } + + // 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:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// 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) { + let tokens = [], errors = []; + let currentText = '', endChar = '', label = ''; + let currentLine = 0, currentColumn = 0; // position of next character + let prevLine = 0, prevColumn = 0; // position before current + let rewindLine = 0, rewindColumn = 0; // position before prev + let startLine = 0, startColumn = 0; // position of token start + + // Rewind the prev position by one character. + function rewind() { + prevLine = rewindLine; prevColumn = rewindColumn; + } + // Attach a start and end location to a string. + function trackText(text) { + let start = { line: startLine, column: startColumn }; + let end = { line: prevLine, column: prevColumn }; + return { text, start, end }; + } + // Report an error. + function error(text) { + errors.push(trackText(text)); + } + // Parse a literal string. + function parseLiteral(string) { + if (string.length == 2 || string.length == 4) { + const hexDigits = "0123456789ABCDEFabcdef"; + for (let c of string) if (!hexDigits.includes(c)) return; + let value = parseInt(string, 16); + if (!isNaN(value)) return value; + } + }; + // Commit the current token. + function commit() { + if (currentText) { + let firstChar = currentText[0]; + let token = trackText(currentText); + if ('()'.includes(firstChar)) { + token.type = 'comment'; + } else if (firstChar == '[') { + token.type = 'markOpen'; + } else if (firstChar == ']') { + token.type = 'markClose'; + } else if (firstChar == '{') { + token.type = 'blockOpen'; + } else if (firstChar == '}') { + token.type = 'blockClose'; + } else if (firstChar == '\'') { + token.type = 'rawString'; + token.value = currentText.slice(1,-1); + } else if (firstChar == '"') { + token.type = 'terminatedString'; + token.value = currentText.slice(1,-1); + } else if (firstChar == '%') { + token.type = 'macroDefinition'; + token.name = currentText.slice(1); + } else if (firstChar == '@') { + token.type = 'globalLabel'; + label = currentText.slice(1); + token.name = label; + } else if (firstChar == '&') { + token.type = 'localLabel'; + token.name = label + '/' + currentText.slice(1); + } else if (firstChar == ';') { + token.type = 'macroTerminator'; + } else if (firstChar == '#') { + token.type = 'spacer'; + token.value = parseLiteral(currentText.slice(1)); + if (token.value == undefined) { + error(`Invalid value for spacer: ${currentText.slice(1)}`) + } + } else if (firstChar == '~') { + token.type = 'symbol'; + token.name = label + '/' + currentText.slice(1); + } else { + let value = parseLiteral(currentText); + if (value != undefined) { + token.type = currentText.length == 2 ? 'byteLiteral' : 'doubleLiteral'; + token.value = value; + } else { + let value = instructionTable[currentText]; + if (value != undefined) { + token.type = 'instruction'; + token.value = value; + } else { + token.type = 'symbol'; + token.name = currentText; + } + } + } + tokens.push(token); + currentText = ''; + startLine = currentLine; startColumn = currentColumn; + } + } + + for (let c of source) { + // Advance position. + rewindLine = prevLine; rewindColumn = prevColumn; + prevLine = currentLine; prevColumn = currentColumn; + currentColumn += 1; + if (c == '\n') { currentLine += 1; currentColumn = 0; } + + if (endChar) { + currentText += c; + if (c == endChar) { + commit(); endChar = ''; + } + } else if ('()'.includes(c)) { + commit(); currentText += c; + if (c == '(') { + endChar = ')'; + } else { + error("Unmatched ')', no comment was in progress"); + commit(); + } + } else if (!currentText && '"\''.includes(c)) { + currentText += c; endChar = c; + } else if ('\r\n\t '.includes(c)) { + let token = { + text: c, type: 'whitespace', + start: { line: prevLine, column: prevColumn }, + end: { line: prevLine, column: prevColumn }, + }; + rewind(); commit(); + tokens.push(token); + } else if ('[]{};'.includes(c)) { + rewind(); commit(); currentText += c; commit(); + } else if (c == ':') { + currentText += c; commit(); + } else { + currentText += c; + } + } + // Report unclosed token. + if (endChar == ')') { + error("Unclosed comment"); + } else if (endChar) { + error("Unclosed string"); + } + + // Commit final token. + commit(); + return { tokens, errors }; +} + + +// Assemble a string of Bedrock source code. +function assembleProgram(source) { + let { tokens, errors } = tokeniseProgram(source); + let program = new Uint8Array(65536); + // Populated up front with all names. + let macros = {}, labels = {}, address = 0; + for (let token of tokens) { + if (['globalLabel', 'localLabel'].includes(token.type)) { + labels[token.name] = { refs:[], address:0 }; + } else if ('macroDefinition' == token.type) { + macros[token.name] = []; + } + } + // Populated gradually as each definition is reached. + let macroNames = {}, labelNames = {}; + + // Assemble a byte value. + function pushByte(byte) { + program[address++] = byte; + } + // Assemble a double value. + function pushDouble(double) { + program[address++] = double >> 8; + program[address++] = double & 0xFF; + } + // Report an error message. + function error(text, token) { + let start = { line: token.start.line, column: token.start.column }; + let end = { line: token.end.line, column: token.end.column }; + errors.push({ text, start, end }); + } + // Returns true if the name has been defined at this point in the program. + function nameTaken(name) { + return name in macroNames || name in labelNames || name in instructionTable; + } + // Recursively assemble an array of tokens. + function assembleTokens(tokens) { + let stack = []; + for (let i=0; i<tokens.length; i++) { + let token = tokens[i]; + if (['comment', 'markOpen', 'markClose', 'whitespace'].includes(token.type)) { + // ignore comments and whitespace. + } 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') { + for (let i=0; i<token.value; i++) pushByte(0); + } else if (token.type == 'blockOpen') { + stack.push({ token, address }); + pushDouble(0); + } else if (token.type == 'blockClose') { + let stash = address; + let pop = stack.pop(); + if (pop != undefined) { + address = pop.address; + pushDouble(stash); + if (stash > 0xFFFF) { + error(`Block address too high`, token); + } + } else { + error("Unmatched '}', no block was in progress", token); + } + address = stash; + } else if (['globalLabel', 'localLabel'].includes(token.type)) { + if (nameTaken(token.name)) { + error(`Label name already taken: ${token.name}`, token); + } else { + labelNames[token.name] = true; + labels[token.name].address = address; + if (address > 0xFFFF) { + error(`Label address too high: ${token.name}`, token); + } + } + } else if (token.type == 'macroDefinition') { + if (nameTaken(token.name)) { + error(`Macro name already taken: ${token.name}`, token); + } else { + let body = [], terminated = false; + i += 1; + while (i<tokens.length) { + let bodyToken = tokens[i++] + let name = bodyToken.name; + let type = bodyToken.type; + if (['whitespace', 'comment', 'markOpen', 'markClose'].includes(type)) { + // ignore comments and whitespace + } else if (type == 'macroTerminator') { + terminated = true; break; + } else if (['globalLabel', 'localLabel'].includes(type)) { + error(`Label definition inside macro definition: ${name}`, bodyToken); + } else if (type == 'macroDefinition') { + error(`Macro definition inside macro definition: ${name}`, bodyToken); + } else if (type == 'symbol') { + if (name in labels) { + body.push(bodyToken); + } else if (name in macroNames) { + body.push(bodyToken); + } else if (name in macros) { + error(`Macro cannot reference itself: ${name}`, bodyToken); + } else if (name in macros) { + error(`Macro referenced before definition: ${name}`, bodyToken); + } else { + error(`Undefined symbol: ${name}`, bodyToken); + } + } else { + body.push(bodyToken); + } + } + if (terminated) { + macroNames[token.name] = true; + macros[token.name] = body; + } else { + error('Unterminated macro definition', token); + } + } + } else if (token.type == 'macroTerminator') { + error("Unmatched ';', no macro definition was in progress", token); + } else if (['byteLiteral', 'instruction'].includes(token.type)) { + pushByte(token.value); + } else if (token.type == 'doubleLiteral') { + pushDouble(token.value); + } else if (token.type == 'symbol') { + if (token.name in labels) { + labels[token.name].refs.push(address); + pushDouble(0); + } else if (token.name in macroNames) { + assembleTokens(macros[token.name]); + } else if (token.name in macros) { + error('Macro referenced before definition', token); + } else { + error(`Undefined symbol: ${token.name}`, token); + } + } else { + console.log("Unknown token type: " + token.type); + } + } + for (let { token, address } of stack) { + error('Unclosed block', token); + } + } + + assembleTokens(tokens); + let endAddress = address; + let symbols = new SymbolTable(); + // Backfill label references with real label addresses. + for (let [name, label] of Object.entries(labels)) { + for (let ref of label.refs) { + address = ref; pushDouble(label.address); + } + // Add label and address to symbols table. + symbols.addSymbol(label.address, name); + } + let bytecode = program.slice(0, endAddress); + return { bytecode, symbols, errors }; +} + + +function SymbolTable() { + this.names = []; + this.addresses = []; + + // Add a symbol to the table. Symbols must be added in address order. + this.addSymbol = function(address, name) { + this.names.push(name); + this.addresses.push(address); + } + + // Get the symbol that is at or comes before this address. + this.findSymbol = function(address) { + // Perform a binary search, based on Rust's slice::binary_search_by. + let size = this.addresses.length; + let base = 0; + while (size > 1) { + let half = size >> 1; + let mid = base + half; + base = this.addresses[mid] > address ? base : mid; + size -= half; + } + if (this.addresses[base] == address) { + return '@' + this.names[base]; + } else { + if (address >= this.addresses[0]) { + let i = base + (this.addresses[base] > address); + return '@' + this.names[i]; + } else { + return ''; + } + } + } +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: EMULATOR::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// 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); + // Callback functions. + this.onUpdate; + + // 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. + } + + // Reset the emulator and load in a new program. + this.loadProgram = (bytecode) => { + this.reset(); + this.mem.fill(0); + this.mem.set(bytecode.slice(0, 65536)); + this.blank = false; + } + + 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(); + } + + this.halt = () => { + this.halted = true; + this.update(); + this.render(); + } + + this.sleep = () => { + this.asleep = true; + this.update(); + this.render(); + this.sleepLoop(); + } + + this.update = () => { + if (this.onUpdate) this.onUpdate(this); + } + + // 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.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(); + setTimeout(this.runLoop.bind(this), 0); + } + } + } + + this.sleepLoop = () => { + 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); + } + } + } + + // 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++]; + 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; + + // Memory access functions. + let mpop1 = ( ) => { return this.mem[this.p++]; } + let mpop2 = ( ) => { return mpop1() << 8 | mpop1(); } + let mpopx = ( ) => { return wMode ? mpop2() : mpop1(); } + let mpsh1 = (v) => { this.mem[this.p++] = 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]; } + let mget2 = (a) => { return mget1(a) << 8 | mget1(a+1); } + let mgetx = (a) => { return wMode ? mget2(a) : mget1(a); } + let mset1 = (a,v) => { this.mem[a] = v; } + 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 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 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 wpopx = ( ) => { return wMode ? wpop2() : wpop1(); } + let rpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : r.pop1(); } + let rpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : r.pop2(); } + let rpopx = ( ) => { return wMode ? rpop2() : rpop1(); } + let wpsh1 = (v) => { w.psh1(v); } + let wpsh2 = (v) => { w.psh2(v); } + let wpshx = (v) => { wMode ? w.psh2(v) : w.psh1(v); } + let rpsh1 = (v) => { r.psh1(v); } + let rpsh2 = (v) => { r.psh2(v); } + let rpshx = (v) => { wMode ? r.psh2(v) : r.psh1(v); } + // Shifting operations. + let shl1 = (v,d) => { return d < 8 ? v << d & 0xFF : 0; } + let shl2 = (v,d) => { return d < 16 ? v << d & 0xFFFF : 0; } + let shlx = (v,d) => { return wMode ? shl2(v,d) : shl1(v,d); } + let shr1 = (v,d) => { return d < 8 ? v >> d & 0xFF : 0; } + let shr2 = (v,d) => { return d < 16 ? v >> d & 0xFFFF : 0; } + let shrx = (v,d) => { return wMode ? shr2(v,d) : shr1(v,d); } + let rol1 = (v,d) => { d %= 8; return shl1(v, d) | shr1(v, 8-d); } + let rol2 = (v,d) => { d %= 16; return shl2(v, d) | shr2(v, 16-d); } + let rolx = (v,d) => { return wMode ? rol2(v,d) : rol1(v,d); } + let ror1 = (v,d) => { d %= 8; return shr1(v, d) | shl1(v, 8-d); } + let ror2 = (v,d) => { d %= 16; return shr2(v, d) | shl2(v, 16-d); } + let rorx = (v,d) => { return wMode ? ror2(v,d) : ror1(v,d); } + + 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=wpop1(); if (t) { this.p = a; } break; + /* JCS */ case 0x0B: a=wpop2(); t=wpop1(); 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; + } + } + }; +} + + +// Stack for the core system. +function Stack() { + this.mem = new Uint8Array(256); + this.p = 0; + + this.reset = ( ) => { this.mem.fill(0); this.p = 0; } + this.psh1 = (v) => { this.mem[this.p++] = v; } + 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; } +} + + +// Device bus for the core system. +function DeviceBus(br) { + this.system = new SystemDevice(br); + this.memory = new MemoryDevice(br); + this.math = new MathDevice(br); + this.clock = new ClockDevice(br); + this.input = new InputDevice(br); + this.screen = new ScreenDevice(br); + this.tone = new NullDevice(); + this.sampler = new NullDevice(); + this.stream = new StreamDevice(br); + this.file = new NullDevice(); + this.clipboard = new ClipboardDevice(br); + this.registry = new NullDevice(); + this.custom1 = new NullDevice(); + this.custom2 = new NullDevice(); + this.custom3 = new NullDevice(); + this.custom4 = new NullDevice(); + + this.devices = [ + this.system, this.memory, this.math, this.clock, + this.input, this.screen, this.tone, this.sampler, + this.stream, this.file, this.clipboard, this.registry, + this.custom1, this.custom2, this.custom3, this.custom4, + ]; + for (let device of this.devices) { + if (device.init) device.init(); + } + + this.queue = new WakeQueue(); + + this.reset = function() { + for (let d of this.devices) d.reset(); } + this.read = function(p) { + return this.devices[p >> 4].read(p & 0x0F); } + this.write = function(p,v) { + return this.devices[p >> 4].write(p & 0x0F, v); } +} + + +function WakeQueue() { + this.queue = [0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF]; + + // Return the queue after applying a wake mask. + this.get = function(wakeMask) { + let masked = []; + for (let slot of this.queue) + if (wakeMask & 0x8000 >> slot) + masked.push(slot); + // Add slot zero last. + if (wakeMask & 0x8000) masked.push(0); + return masked; + } + // Move slot to the back of the queue. + this.wake = function(slot) { + let i = this.queue.indexOf(slot); + if (i > -1) this.queue.splice(i, 1); + this.queue.push(slot); + } +} + + +// Disconnected device. +function NullDevice() { + this.reset = function() { } + this.read = function(p) { return 0; } + this.write = function(p,v) { return; } + this.wake = function() { return false; } +} + + +function SystemDevice(br) { + this.reset = function() { + this.wakeMask = 0; + this.wakeSlot = 0; + this.nameBuffer = new TextBuffer(`${systemName}/${systemVersion}`); + this.authorsBuffer = new TextBuffer(systemAuthors); + } + this.read = function(p) { + switch (p) { + case 0x2: return this.wakeSlot; + case 0x8: return this.nameBuffer.read(); + case 0x9: return this.authorsBuffer.read(); + case 0xE: return 0b1111_1100; + case 0xF: return 0b1010_0000; + default: return 0x00; + } + } + 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 0x8: this.nameBuffer.reset(); break; + case 0x9: this.authorsBuffer.reset(); break; + default: return; + } + } + this.wake = function() { + return true; + } +} + + +function MemoryDevice(br) { + this.reset = function() { + this.mem = new Uint8Array(); + this.count = 0; + this.countW = 0; + this.copyW = 0; + this.page1 = 0; + this.addr1 = 0; + this.page2 = 0; + this.addr2 = 0; + } + this.read = function(p) { + switch (p) { + case 0x0: return getH(this.count); + case 0x1: return getL(this.count); + case 0x2: return getH(this.page1); + case 0x3: return getL(this.page1); + case 0x4: return getH(this.addr1); + case 0x5: return getL(this.addr1); + case 0x6: + case 0x7: return this.read1(); + case 0xA: return getH(this.page2); + case 0xB: return getL(this.page2); + case 0xC: return getH(this.addr2); + case 0xD: return getL(this.addr2); + case 0xE: + case 0xF: return this.read2(); + default: return 0x00; + } + } + this.write = function(p,v) { + switch (p) { + case 0x0: this.countW = setH(this.countW, v); return; + case 0x1: this.countW = setL(this.countW, v); this.allocate(); return; + case 0x2: this.page1 = setH(this.page1, v); return; + case 0x3: this.page1 = setL(this.page1, v); return; + case 0x4: this.addr1 = setH(this.addr1, v); return; + case 0x5: this.addr1 = setL(this.addr1, v); return; + case 0x6: + case 0x7: this.write1(v); return; + case 0x8: this.copyW = setH(this.copyW, v); return; + case 0x9: this.copyW = setL(this.copyW, v); this.copy(); return; + case 0xA: this.page2 = setH(this.page2, v); return; + case 0xB: this.page2 = setL(this.page2, v); return; + case 0xC: this.addr2 = setH(this.addr2, v); return; + case 0xD: this.addr2 = setL(this.addr2, v); return; + case 0xE: + case 0xF: this.write2(v); return; + default: return; + } + } + this.wake = function() { + return false; + } + + // Update number of allocated pages to equal count. + this.allocate = function() { + // Allocate memory. + if (this.countW > this.count) { + let old = this.mem; + this.mem = new Uint8Array(this.countW * 256); + this.mem.set(old); + // Deallocate memory. + } else if (this.countW < this.count) { + this.mem = this.mem.slice(0, this.countW); + } + this.count = this.countW; + } + // Read a byte from head 1. + this.read1 = function() { + let i = (this.page1 * 256) + this.addr1; + this.addr1 = (this.addr1 + 1) & 0xFFFF; + return this.mem.length > i ? this.mem[i] : 0; + } + // Read a byte from head 2. + this.read2 = function() { + let i = (this.page2 * 256) + this.addr2; + this.addr2 = (this.addr2 + 1) & 0xFFFF; + return this.mem.length > i ? this.mem[i] : 0; + } + // Write a byte to head 1. + this.write1 = function(v) { + let i = (this.page1 * 256) + this.addr1; + this.addr1 = (this.addr1 + 1) & 0xFFFF; + if (this.mem.length > i) this.mem[i] = v; + } + // Write a byte to head 2. + this.write2 = function(v) { + let i = (this.page2 * 256) + this.addr2; + this.addr2 = (this.addr2 + 1) & 0xFFFF; + if (this.mem.length > i) this.mem[i] = v; + } + // Copy pages from head 2 to head 1. + this.copy = function() { + let src = this.page2; + let dst = this.page1; + let remaining = this.copyW + while (remaining && this.count > src && this.count > dst) { + let index = dst++ * 256; + let start = src++ * 256; + let end = start + 256; + this.mem.copyWithin(index, start, end); + } + } +} + + +function MathDevice() { + this.reset = function() { + this.x = 0; + this.y = 0; + this.r = 0; + this.t = 0; + } + this.read = function(p) { + switch (p) { + case 0x0: return getH(this.px()); + case 0x1: return getL(this.px()); + case 0x2: return getH(this.py()); + case 0x3: return getL(this.py()); + case 0x4: return getH(this.cr()); + case 0x5: return getL(this.cr()); + case 0x6: return getH(this.ct()); + case 0x7: return getL(this.ct()); + case 0x8: return getHH(this.prod()); + case 0x9: return getHL(this.prod()); + case 0xA: return getH(this.prod()); + case 0xB: return getL(this.prod()); + case 0xC: return getH(this.quot()); + case 0xD: return getL(this.quot()); + case 0xE: return getH(this.rem()); + case 0xF: return getL(this.rem()); + default: return 0x00; + } + } + this.write = function(p,v) { + switch (p) { + case 0x0: this.x = setH(this.x, v); return; + case 0x1: this.x = setL(this.x, v); return; + case 0x2: this.y = setH(this.y, v); return; + case 0x3: this.y = setL(this.y, v); return; + case 0x4: this.r = setH(this.r, v); return; + case 0x5: this.r = setL(this.r, v); return; + case 0x6: this.t = setH(this.t, v); return; + case 0x7: this.t = setL(this.t, v); return; + default: return; + } + } + this.wake = function() { + return false; + } + + // Return x coordinate of polar point. + this.px = function() { + let x = Math.trunc(Math.cos((2 * Math.PI * this.t) / 65536) * this.r); + return x <= 32767 && x >= -32768 ? x : 0; }; + // Return y coordinate of polar point. + this.py = function() { + let y = Math.trunc(Math.sin((2 * Math.PI * this.t) / 65536) * this.r); + return y <= 32767 && y >= -32768 ? y : 0; }; + // Return r coordinate of cartesian point. + this.cr = function() { + return Math.trunc(Math.sqrt(Math.pow(signed(this.x), 2) + Math.pow(signed(this.y), 2))); }; + // Return t coordinate of cartesian point. + this.ct = function() { + return Math.trunc(Math.atan2(signed(this.y), signed(this.x)) * 65536 / (2 * Math.PI)); }; + // Return product of x and y operands. + this.prod = function() { + return this.x * this.y; }; + this.quot = function() { + return this.y ? this.x / this.y : 0; }; + this.rem = function() { + return this.y ? this.x % this.y : 0; }; +} + + +function ClockDevice(br) { + this.reset = function() { + this.start = performance.now(); + this.uptime = 0; + this.t1 = new CountdownTimer(); + this.t2 = new CountdownTimer(); + this.t3 = new CountdownTimer(); + this.t4 = new CountdownTimer(); + } + this.read = function(p) { + switch (p) { + case 0x0: return new Date().getFullYear()-2000; + case 0x1: return new Date().getMonth(); + case 0x2: return new Date().getDate()-1; + case 0x3: return new Date().getHours(); + case 0x4: return new Date().getMinutes(); + case 0x5: return new Date().getSeconds(); + case 0x6: this.updateUptime(); return getH(this.uptime); + case 0x7: return getL(this.uptime); + case 0x8: this.t1.update(); return getH(this.t1.read); + case 0x9: return getL(this.t1.read); + case 0xA: this.t2.update(); return getH(this.t2.read); + case 0xB: return getL(this.t2.read); + case 0xC: this.t3.update(); return getH(this.t3.read); + case 0xD: return getL(this.t3.read); + case 0xE: this.t4.update(); return getH(this.t4.read); + case 0xF: return getL(this.t4.read); + default: return 0x00; + } + } + this.write = function(p,v) { + switch (p) { + case 0x8: this.t1.write = setH(this.t1.write, v); return; + case 0x9: this.t1.write = setL(this.t1.write, v); this.t1.commit(); return; + case 0xA: this.t2.write = setH(this.t2.write, v); return; + case 0xB: this.t2.write = setL(this.t2.write, v); this.t2.commit(); return; + case 0xC: this.t3.write = setH(this.t3.write, v); return; + case 0xD: this.t3.write = setL(this.t3.write, v); this.t3.commit(); return; + case 0xE: this.t4.write = setH(this.t4.write, v); return; + case 0xF: this.t4.write = setL(this.t4.write, v); this.t4.commit(); return; + default: return; + } + } + this.wake = function() { + let t1 = this.t1.wake(); + let t2 = this.t2.wake(); + let t3 = this.t3.wake(); + let t4 = this.t4.wake(); + return t1 || t2 || t3 || t4; + } + + this.updateUptime = function() { + let delta = performance.now() - this.start; + this.uptime = delta * 0.256 & 0xFFFF; + } +} + + +function InputDevice(br) { + this.reset = function() { + this.x = 0; + this.y = 0; + this.xR = 0; + this.yR = 0; + this.hScroll = 0; + this.vScroll = 0; + this.hScrollR = 0; + this.vScrollR = 0; + this.pointerButtons = 0; + this.pointerActive = false; + // Navigation state + this.navigation = 0; + this.navUp = false; + this.navDown = false; + this.navLeft = false; + this.navRight = false; + this.navConfirm = false; + this.navCancel = false; + this.navTab = false; + this.navShift = false; + // + this.modifiers = 0; + this.characterBytes = new ByteQueue(1024); + // Flags + this.wakeFlag = false; + this.accessed = false; + } + this.read = function(p) { + this.accessed = true; + switch (p) { + case 0x0: this.xR = this.x; return getH(this.xR); + case 0x1: return getL(this.xR); + case 0x2: this.yR = this.y; return getH(this.yR); + case 0x3: return getL(this.yR); + case 0x4: this.updateHScroll(); return getH(this.hScrollR); + case 0x5: return getL(this.hScrollR); + case 0x6: this.updateVScroll(); return getH(this.vScrollR); + case 0x7: return getL(this.vScrollR); + case 0x8: return bool(this.pointerActive); + case 0x9: return this.pointerButtons; + case 0xA: return this.characterBytes.pop(); + case 0xB: return this.modifiers; + case 0xC: return this.navigation; + default: return 0x00; + } + } + this.write = function(p,v) { + this.accessed = true; + switch (p) { + case 0xA: this.characterBytes = []; return; + default: return; + } + } + this.wake = function() { + let wake = this.wakeFlag; this.wakeFlag = false; + return wake; + } + + this.updateHScroll = function() { + let delta = Math.trunc(this.hScroll); + this.hScroll -= delta; + this.hScrollR = delta; + } + this.updateVScroll = function() { + let delta = Math.trunc(this.vScroll); + this.vScroll -= delta; + this.vScrollR = delta; + } + this.applyPosition = function(x, y) { + let wake = x != this.x || y != this.y; + this.x = x; this.y = y; + if (wake) this.wakeFlag = true; + } + this.applyButtons = function(jsButtons) { + let pointerButtons = 0; + pointerButtons |= jsButtons & 0x01 ? 0x80 : 0x00; + pointerButtons |= jsButtons & 0x02 ? 0x40 : 0x00; + pointerButtons |= jsButtons & 0x04 ? 0x20 : 0x00; + let wake = pointerButtons != this.pointerButtons; + this.pointerButtons = pointerButtons; + if (wake) this.wakeFlag = true; + } + this.applyActive = function(active) { + let wake = active != this.pointerActive; + this.pointerActive = active; + if (wake) this.wakeFlag = true; + } + this.applyKey = function(event, pressed) { + // Handle navigation keys. + switch (event.key) { + case "ArrowUp": this.navUp = pressed; break; + case "ArrowDown": this.navDown = pressed; break; + case "ArrowLeft": this.navLeft = pressed; break; + case "ArrowRight": this.navRight = pressed; break; + case "Enter": this.navConfirm = pressed; break; + case "Escape": this.navCancel = pressed; break; + case "Tab": this.navTab = pressed; break; + case "Shift": this.navShift = pressed; break; + } + let navigation = 0; + navigation |= this.navUp ? 0x80 : 0x00; + navigation |= this.navDown ? 0x40 : 0x00; + navigation |= this.navLeft ? 0x20 : 0x00; + navigation |= this.navRight ? 0x10 : 0x00; + navigation |= this.navConfirm ? 0x08 : 0x00; + navigation |= this.navCancel ? 0x04 : 0x00; + navigation |= this.navTab && !this.navShift ? 0x02 : 0x00; + navigation |= this.navTab && this.navShift ? 0x01 : 0x00; + if (navigation != this.navigation) { + this.wakeFlag = true; + this.navigation = navigation; + } + // Handle character input. + if (pressed) { + if (event.key.length == 1) { + let byteList = encodeUtf8(event.key); + this.characterBytes.pushList(byteList); + this.wakeFlag = true; + } else { + switch (event.key) { + case "Backspace": this.characterBytes.push(0x08); break; + case "Tab": this.characterBytes.push(0x09); break; + case "Enter": this.characterBytes.push(0x0A); break; + case "Spacebar": this.characterBytes.push(0x20); break; + default: return; + } + this.wakeFlag = true; + } + } + } + this.applyModifiers = function(event) { + let modifiers = 0; + modifiers |= event.ctrlKey ? 0x80 : 0x00; + modifiers |= event.shiftKey ? 0x40 : 0x00; + modifiers |= event.altKey ? 0x20 : 0x00; + modifiers |= event.metaKey ? 0x10 : 0x00; + let wake = modifiers != this.modifiers; + this.modifiers = modifiers; + if (wake) this.wakeFlag = true; + } + this.applyHScroll = function(delta) { + if (delta) { + this.wakeFlag = true; + this.hScroll += delta; + this.hScroll = clamp(this.hScroll, -32768, 32767); + } + } + this.applyVScroll = function(delta) { + if (delta) { + this.wakeFlag = true; + this.vScroll += delta; + this.vScroll = clamp(this.vScroll, -32768, 32767); + } + } +} + + +function ScreenDevice(br) { + this.init = function() { + this.ctx = br.e.canvas.getContext("2d", { + alpha: false, + willReadFrequently: false, + }); + } + this.reset = function() { + this.x = 0; + this.y = 0; + this.px = 0; // previous x + this.py = 0; // previous y + this.widthW = 0; + this.heightW = 0; + this.palette = [ + [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], + [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], + [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], + [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], + ]; + this.paletteW = 0; + this.selection = [0, 0, 0, 0]; + this.sprite = new SpriteBuffer(); + let initialWidth = Math.trunc(br.e.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); + this.wakeFlag = false; + this.accessed = false; + } + this.read = function(p) { + this.accessed = true; + switch (p) { + case 0x0: return getH(this.x); + case 0x1: return getL(this.x); + case 0x2: return getH(this.y); + case 0x3: return getL(this.y); + case 0x4: return getH(this.width); + case 0x5: return getL(this.width); + case 0x6: return getH(this.height); + case 0x7: return getL(this.height); + default: return 0x00; + } + } + this.write = function(p,v) { + this.accessed = true; + switch (p) { + case 0x0: this.x = setH(this.x, v); return; + case 0x1: this.x = setL(this.x, v); return; + case 0x2: this.y = setH(this.y, v); return; + case 0x3: this.y = setL(this.y, v); return; + case 0x4: this.widthW = setH(this.widthW, v); return; + case 0x5: this.widthW = setL(this.widthW, v); this.commitWidth(); return; + case 0x6: this.heightW = setH(this.heightW, v); return; + case 0x7: this.heightW = setL(this.heightW, v); this.commitHeight(); return; + case 0x8: this.paletteW = setH(this.paletteW, v); return; + case 0x9: this.paletteW = setL(this.paletteW, v); this.commitPalette(); return; + case 0xA: this.selection[0] = v >> 4; this.selection[1] = v & 0xF; return; + case 0xB: this.selection[2] = v >> 4; this.selection[3] = v & 0xF; return; + case 0xC: + case 0xD: this.sprite.push(v); return; + case 0xE: this.draw(v); return; + case 0xF: this.move(v); return; + default: return; + } + } + this.wake = function() { + let wake = this.wakeFlag; this.wakeFlag = false; + return wake; + } + + // Mark a region as dirty, with 0 inclusive and 1 exclusive. + this.mark = function(x0, y0, x1, y1) { + if(x0 < this.dx0) this.dx0 = x0; + if(y0 < this.dy0) this.dy0 = y0; + if(x1 > this.dx1) this.dx1 = x1; + if(y1 > this.dy1) this.dy1 = y1; + } + // Unmark the dirty region. + this.unmark = function() { + this.dx0 = 32767; + this.dy0 = 32767; + this.dx1 = -32768; + this.dy1 = -32768; + } + // Check if the screen has a visible dirty region. + this.dirty = function() { + // TODO: Does clamp pull a distant dirty region into the viewport? + this.dx0 = clamp(this.dx0, 0, this.width); + this.dx1 = clamp(this.dx1, 0, this.width); + this.dy0 = clamp(this.dy0, 0, this.height); + this.dy1 = clamp(this.dy1, 0, this.height); + return this.dx1 > this.dx0 && this.dy1 > this.dy0; + } + // Update the dirty region of this.pixels from fg and bg. + this.render = function() { + let dw = this.dx1 - this.dx0; + for (let y=this.dy0; y<this.dy1; y++) { + let i0 = y * this.width + this.dx0; + let i1 = i0 + dw; + let j = i0 * 4; + for (let i=i0; i<i1; i++) { + let index = this.fg[i] || this.bg[i]; + let [r, g, b] = this.palette[index]; + this.pixels[j++] = r; + this.pixels[j++] = g; + this.pixels[j++] = b; + this.pixels[j++] = 0xFF; + } + } + } + // Resize to fill a container, dimensions given in real pixels. + this.resizeToFill = function(width, height) { + width = width / this.scale; + height = height / this.scale; + if (this.resize(width, height)) { + this.wakeFlag = true; + // TODO: Preserve current canvas contents when resizing. + let [r, g, b] = this.palette[0]; + this.ctx.fillStyle = '#'+hex(r,2)+hex(g,2)+hex(b,2); + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + }; + } + // Resize to an absolute size. + this.resize = function(width, height, scale) { + width = Math.trunc(width); + height = Math.trunc(height); + if (width != this.width || height != this.height) { + if (width != undefined) this.width = width; + if (height != undefined) this.height = height; + if (scale != undefined) this.scale = scale; + this.fg = new Uint8Array(this.width * this.height); + this.bg = new Uint8Array(this.width * this.height); + this.pixels = new Uint8ClampedArray(this.width * this.height * 4); + this.ctx.canvas.width = this.width; + this.ctx.canvas.height = this.height; + this.ctx.canvas.style.width = (this.width * this.scale) + 'px'; + this.ctx.canvas.style.height = (this.height * this.scale) + 'px'; + this.mark(0, 0, this.width, this.height); + return true; + } else { + return false; + } + } + this.commitWidth = function() { + console.log("Resizing the canvas has not yet been implemented.") + // this.width = this.widthW; + // this.resize(this.width, this.height); + } + this.commitHeight = function() { + console.log("Resizing the canvas has not yet been implemented.") + // this.height = this.heightW; + // this.resize(this.width, this.height); + } + this.commitPalette = function() { + let r = (this.paletteW >> 8 & 0x0F) * 17; + let g = (this.paletteW >> 4 & 0x0F) * 17; + let b = (this.paletteW & 0x0F) * 17; + this.palette[this.paletteW>>12] = [r,g,b]; + this.mark(0, 0, this.width, this.height); + } + this.draw = function(v) { + let layer = v & 0x80 ? this.fg : this.bg; + let lower = v & 0x0F; + switch (v >> 4 & 0x07) { + case 0: this.drawPixel(layer, this.x, this.y, lower); break; + case 1: this.draw1BitSprite(layer, lower); break; + case 2: this.fillLayer(layer, lower); break; + case 3: this.draw2BitSprite(layer, lower); break; + case 4: this.drawSolidLine(layer, lower); break; + case 5: this.drawTexturedLine(layer, lower); break; + case 6: this.drawSolidRect(layer, lower); break; + case 7: this.drawTexturedRect(layer, lower); break; + } + this.px = this.x; + this.py = this.y; + } + this.move = function(v) { + let dist = v & 0x3F; + switch (v & 0xC0) { + case 0x00: this.x += dist; this.x &= 0xFFFF; return; + case 0x40: this.y += dist; this.y &= 0xFFFF; return; + case 0x80: this.x -= dist; this.x &= 0xFFFF; return; + case 0xC0: this.y -= dist; this.y &= 0xFFFF; return; + } + } + + this.drawPixel = function(layer, x, y, colour) { + x = x & 0xFFFF; y = y & 0xFFFF; + if (x < this.width && y < this.height) { + layer[x + (y * this.width)] = colour; + this.mark(x, y, x+1, y+1); + } + } + this.fillLayer = function(layer, colour) { + layer.fill(colour); + this.mark(0, 0, this.width, this.height); + } + this.draw1BitSprite = function(layer, transform) { + let sprite = this.sprite.get1Bit(transform); + this.drawSprite(layer, sprite, transform); + } + this.draw2BitSprite = function(layer, transform) { + let sprite = this.sprite.get2Bit(transform); + this.drawSprite(layer, sprite, transform); + } + this.drawSprite = function(layer, sprite, transform) { + let opaque = !(transform & 0x8); + for (let y=0; y<8; y++) { + for (let x=0; x<8; x++) { + let index = sprite[y*8+x]; + if (opaque || index) { + let colour = this.selection[index]; + this.drawPixel(layer, this.x+x, this.y+y, colour) + }; + } + } + } + this.drawSolidLine = function(layer, colour) { + this.drawLine((x, y) => this.drawPixel(layer, x, y, colour)); + } + this.drawSolidRect = function(layer, colour) { + this.drawRect((x, y) => this.drawPixel(layer, x, y, colour)); + } + this.drawTexturedLine = function(layer, transform) { + let sprite = this.sprite.get1Bit(transform); + let opaque = !(transform & 0x8); + this.drawLine((x, y) => { + let index = sprite[(y%8)*8+(x%8)]; + if (opaque || index) { + let colour = this.selection[index]; + this.drawPixel(layer, x, y, colour); + } + }) + } + this.drawTexturedRect = function(layer, transform) { + let sprite = this.sprite.get1Bit(transform); + let opaque = !(transform & 0x8); + this.drawRect((x, y) => { + let index = sprite[(y%8)*8+(x%8)]; + if (opaque || index) { + let colour = this.selection[index]; + this.drawPixel(layer, x, y, colour); + } + }) + } + this.drawLine = function(drawFn) { + let xEnd = signed(this.x); + let yEnd = signed(this.y); + let x = signed(this.px); + let y = signed(this.py); + let dx = Math.abs(xEnd - x); + let dy = -Math.abs(yEnd - y); + let sx = x < xEnd ? 1 : -1; + let sy = y < yEnd ? 1 : -1; + let e1 = dx + dy; + while (true) { + drawFn(x, y); + if (x == xEnd && y == yEnd) break; + let e2 = e1 << 1; + if (e2 >= dy) { e1 += dy; x += sx; } + if (e2 <= dx) { e1 += dx; y += sy; } + } + } + this.drawRect = function(drawFn) { + let x = signed(this.x); let px = signed(this.px); + let y = signed(this.y); let py = signed(this.py); + let x0 = Math.min(x, px); let x1 = Math.max(x, px); + let y0 = Math.min(y, py); let y1 = Math.max(y, py); + if (x1 >= 0 && y1 >= 0 && x0 < this.width && y0 < this.height) { + for (let y=y0; y<=y1; y++) { + for (let x=x0; x<=x1; x++) { + drawFn(x, y); + } + } + } + } +} + + +function StreamDevice(br) { + this.reset = function() { + } + this.read = function(p) { + switch (p) { + case 0x1: return 0xFF; + case 0x3: return 0xFF; + case 0x5: return 0xFF; + default: return 0x00; + } + } + this.write = function(p,v) { + switch (p) { + case 0x3: br.e.endTransmission(); break; + case 0x6: + case 0x7: br.e.receiveTransmissionByte(v); break; + default: return; + } + } +} + + +function ClipboardDevice(br) { + this.reset = function() { + this.readQueue = []; // reverse order + this.writeQueue = []; // normal order + } + this.read = function(p) { + switch (p) { + case 0x4: return clamp(this.readQueue.length, 0, 0xFF); + case 0x5: return clamp(this.writeQueue.length, 0, 0xFF); + case 0x6: + case 0x7: return this.readQueue.pop() || 0x00; + default: return 0x00; + } + } + this.write = function(p,v) { + // HACK: The -1 is to break the run loop to allow the br.pause() to take hold. + switch (p) { + case 0x2: this.readEntry(v); return -1; + case 0x3: this.writeEntry(v); return -1; + case 0x5: this.writeQueue = []; break; + case 0x6: + case 0x7: this.writeQueue.push(v); break; + default: return; + } + } + + 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(); + navigator.clipboard.readText().then( + (text) => { + this.readQueue = Array.from(encodeUtf8(text)); + this.readQueue.reverse(); + br.run(); + }, + () => { + br.run(); + }, + ); + } + } else { + // TODO: Look into valid clipboard binary formats. + } + } + 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(); + } + ); + } + } else { + // TODO: Look into valid clipboard binary formats. + // let array = new Uint8Array(this.writeQueue); + // let type = "application/octet-stream"; + // let blob = new Blob([array.buffer], {type}); + // let items = [new ClipboardItem({ [type]: blob })]; + // navigator.clipboard.write(items); + } + this.writeQueue = 0; + } +} + + + +// Read-only buffer for reading a string from a device. +function TextBuffer(text) { + this.mem = new Uint8Array(text.length); + this.p = 0; + + // Populate the buffer. + for (let i=0; i<text.length; i++) this.mem[i] = text.codePointAt(i); + + this.reset = function() { + this.p = 0; + } + this.read = function() { + return this.mem[this.p++] ?? 0; + } +} + + +/// Countdown timer for the clock device. +function CountdownTimer() { + this.reset = function() { + this.end = 0; + this.read = 0; + this.write = 0; + this.wakeFlag = false; + } + this.reset(); + + this.wake = function() { + if (this.end && this.end <= performance.now()) { + this.end = 0; + this.wakeFlag = true; + } + let wake = this.wakeFlag; this.wakeFlag = false; + return wake; + } + this.update = function() { + if (this.end) { + let remaining = this.end - performance.now(); + if (remaining > 0) { + this.read = remaining * 0.256; + } else { + this.read = 0; + this.end = 0; + this.wakeFlag = true; + } + } else { + this.read = 0; + } + } + this.commit = function() { + this.end = performance.now() + (this.write / 0.256); + } +} + + +// Byte queue implemented as a circular buffer. +function ByteQueue(capacity) { + this.mem = new Uint8Array(capacity); + this.length = 0; + this.head = 0; + this.tail = 0; + + this.clear = function() { + this.length = 0; + this.head = 0; + this.tail = 0; + } + this.push = function(v) { + // Only push if there is sufficient remaining capacity. + if (this.length < this.mem.length) { + this.mem[this.head] = v; + this.head = (this.head + 1) % this.mem.length; + this.length += 1; + } + } + this.pushList = function(list) { + // Only push if there is sufficient remaining capacity. + if (this.mem.length - this.length >= list.length) { + for (let byte of list) this.push(byte); + } + } + this.pop = function() { + if (this.length) { + let byte = this.mem[this.tail]; + this.tail = (this.tail + 1) % this.mem.length; + this.length -= 1; + return byte; + } else { + return 0x00; + } + } +} + + +// Circular buffer that holds sprite data for the screen device. +function SpriteBuffer() { + this.mem = new Uint8Array(16); + this.p = 0; + this.cache1 = new Uint8Array(64); // cached 1-bit sprite + this.cache2 = new Uint8Array(64); // cached 2-bit sprite + this.dirty1 = true; // dirty flag for 1-bit sprite + this.dirty2 = true; // dirty flag for 2-bit sprite + + this.push = function(v) { + this.mem[this.p] = v; + this.p = (this.p + 1) & 0xF; + this.dirty1 = true; + this.dirty2 = true; + } + this.get1Bit = function(transform) { + if (this.dirty1) { + for (let i=0; i<8; i++) { + let rowL = this.mem[this.p+i+8 & 0xF]; + for (let j=0; j<8; j++) { + let bit = rowL >> (7-j) & 0x1; + this.cache1[i*8+j] = bit; + } + } + this.dirty1 = false; + } + return this.transform(this.cache1, transform); + } + this.get2Bit = function(transform) { + if (this.dirty2) { + for (let i=0; i<8; i++) { + let rowH = this.mem[this.p+i & 0xF]; + let rowL = this.mem[this.p+i+8 & 0xF]; + for (let j=0; j<8; j++) { + let bitH = rowH >> (7-j) & 0x1; + let bitL = rowL >> (7-j) & 0x1; + this.cache2[i*8+j] = bitH << 1 | bitL; + } + } + this.dirty2 = false; + } + return this.transform(this.cache2, transform); + } + + this.transform = function(sprite, transform) { + let x, y; let t = new Uint8Array(64); + switch (transform & 0x7) { + case 0: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ y *8+ x ] } }; break; + case 1: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ y *8+(7-x)] } }; break; + case 2: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-y)*8+ x ] } }; break; + case 3: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-y)*8+(7-x)] } }; break; + case 4: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ x *8+ y ] } }; break; + case 5: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ x *8+(7-y)] } }; break; + case 6: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-x)*8+ y ] } }; break; + case 7: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-x)*8+(7-y)] } }; break; + } + return t; + } +} + + +function setHH(v, byte) { return byte << 24 | v & 0x00FFFFFF; } +function setHL(v, byte) { return byte << 16 | v & 0xFF00FFFF; } +function setH (v, byte) { return byte << 8 | v & 0xFFFF00FF; } +function setL (v, byte) { return byte | v & 0xFFFFFF00; } +function getHH(v) { return v >> 24 & 0x000000FF; } +function getHL(v) { return v >> 16 & 0x000000FF; } +function getH (v) { return v >> 8 & 0x000000FF; } +function getL (v) { return v & 0x000000FF; } + +function bool (b) { return b ? 0xFF : 0x00; } +function signed(v) { return v << 16 >> 16; } +function clamp(v,a,b) { return v>=a ? v<b ? v : b : a; } // inclusive a and b + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: CONSTANTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +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'> + <g fill='currentColor'> + <rect x='0' y='0' width='8' height='1' /> + <rect x='0' y='2' width='8' height='1' /> + <rect x='0' y='4' width='8' height='4' /> + </g> + </svg><span style='margin:0 4px'>bedrock-js</span> + </a> +`; + +const fullscreenIcon = ` + <svg xmlns='http://www.w3.org/2000/svg' viewBox='8 8 20 20' width='1em' fill='currentColor'> + <path d='m 10,16 2,0 0,-4 4,0 0,-2 L 10,10 l 0,6 0,0 z'></path> + <path d='m 20,10 0,2 4,0 0,4 2,0 L 26,10 l -6,0 0,0 z'></path> + <path d='m 24,24 -4,0 0,2 L 26,26 l 0,-6 -2,0 0,4 0,0 z'></path> + <path d='M 12,20 10,20 10,26 l 6,0 0,-2 -4,0 0,-4 0,0 z'></path> + </svg> +`; + +const runIcon = ` + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1em' fill='currentColor'> + <path d='m 5,2 6,6 -6,6 z'></path> + </svg> +`; + +const pauseIcon = ` + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1em' fill='currentColor'> + <path d='m 2.5,2.5 4.5,0 0,11 -4.5,0 z'></path> + <path d='m 9.5,2.5 4.5,0 0,11 -4.5,0 z'></path> + </svg> +`; + +const stopIcon = ` + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1em' fill='currentColor'> + <path d='m 2.5,2.5 11,0 0,11 -11,0 z'></path> + </svg> +`; + +const stepIcon = ` + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1em' fill='currentColor'> + <path d='m 3,2 6,6 -6,6 z'></path> + <path d='m 9,2.5 2.5,0 0,11 -2.5,0 z'></path> + </svg> +`; + +const assemblerTemplate = ` + <div class='bedrock assembler'> + <div class='editor'> + <div class='viewer' aria-hidden=true></div> + <textarea spellcheck=false></textarea> + </div> + <div class='menubar'> + <button name='fullscreen'>${fullscreenIcon}</button> + <button name='check'>CHECK</button> + <button name='run'>RUN</button> + <span class='status'></span> + ${projectLink} + </div> + <div class='hidden panel errors'></div> + <div class='hidden panel bytecode'></div> + </div> +`; + + +const emulatorTemplate = ` + <div class='bedrock emulator'> + <div class='menubar'> + <button name='fullscreen'>${fullscreenIcon}</button> + <button name='state'>STATE</button> + <button name='run'>${runIcon}</button> + <button name='pause'>${pauseIcon}</button> + <button name='stop'>${stopIcon}</button> + <button name='step'>${stepIcon}</button> + <span class='status'></span> + ${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> + </ul></div> + <div class='hidden stream'><ul class='transmissions'></ul></div> + <div class='hidden screen'><canvas></canvas></div> + </div> +`; + + +const bedrockStyles = ` + div.bedrock { + --br-fg:var(--fg,#222); --br-bg:var(--bg,#eed); --br-accent:var(--accent,#467); } + div.bedrock * { + font-family:var(--font-family),monospace; font-weight:var(--font-weight); font-size:var(--font-size); + box-sizing:border-box; color:var(--br-bg); } + div.bedrock { + display:flex; flex-direction:column; overflow:hidden; border-radius:2px; } + div.bedrock .hidden { + display:none } + + .bedrock.assembler .editor { + position:relative; flex-grow:1; } + .bedrock.assembler textarea, .bedrock.assembler .viewer { + position:absolute; top:0; left:0; width:100%; height:100%; margin:0; padding:0.5rem 0.7rem; + max-height:calc(100vh); overflow:auto; border:none; white-space:pre; tab-size:2; } + .bedrock.assembler textarea { + z-index:1; background:transparent; color:transparent; caret-color:var(--br-fg); resize:vertical } + .bedrock.assembler textarea:focus { + outline:none; } + .bedrock:fullscreen textarea, .bedrock:fullscreen .screen { + resize:none; height:100% !important; } + + .bedrock.assembler .viewer { + z-index:0; background:var(--br-bg); color:var(--br-fg) } + .bedrock.assembler .viewer .comment { color:var(--comment, #998) } + .bedrock.assembler .viewer .value { color:var(--value, #532) } + .bedrock.assembler .viewer .definition { color:var(--definition, #245) } + .bedrock.assembler .viewer .reference { color:var(--reference ) } + + .bedrock .menubar { + background:var(--br-accent); display:flex; padding:3px; gap:3px } + .bedrock button { + display:flex; align-items:center; background:#0004; border:none; border-radius:2px; cursor:pointer; } + .bedrock .menubar button:hover { + background:#0006; } + .bedrock .menubar button:active { + background:#000c; } + .bedrock .menubar .status { + font-size:85%; } + .bedrock .menubar a { + text-decoration:none; white-space:nowrap; padding-left:0.4em } + .bedrock .menubar a:hover { + text-decoration:underline } + .bedrock .menubar .status { + flex-grow:1; white-space:nowrap; overflow-x:hidden; align-self:center; padding-left:0.4rem; } + + .bedrock .panel { + color:var(--br-bg); background:var(--br-accent); } + .bedrock .panel > * { + background:#0006; padding:0.3rem 0.5rem; } + .bedrock .panel ul { + overflow:scroll; margin:0; list-style:none; } + .bedrock .panel li { + white-space:pre; } + .bedrock .panel .label { + opacity:60%; } + + .bedrock .stream { + overflow:auto; background:var(--br-bg); padding:0.3rem; flex-grow:1; max-height:10rem; } + .bedrock .stream ul { + margin:0; padding:0; padding-left:0.5rem } + .bedrock .stream li { + color:var(--br-fg); padding:0 0.3rem; cursor:pointer; white-space:break-spaces } + .bedrock .stream li::marker { + content:'>'; } + .bedrock .stream li:hover { + text-decoration:underline } + .bedrock .screen { + background:#111; } + .bedrock .screen canvas { + display:block; margin:0 auto; image-rendering:pixelated; touch-action:none; } + .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, +}; |