diff --git a/Gruntfile.js b/Gruntfile.js index 19ad99668..74d3dbdee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -201,8 +201,8 @@ module.exports = function(grunt) { return "/**\n * @copyright " + filepath.replace(/^\./, "http://pcjs.org") + " (C) Jeff Parsons 2012-2016\n */\n\n" + src.replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, '') .replace(/^(import|export)[ \t]+[^\n]*\n/gm, '') - .replace(/^[ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);/gm, '') - .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;/gm, '') + .replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, '') + .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, '') .replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, '') .replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') .replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') @@ -219,8 +219,8 @@ module.exports = function(grunt) { return "/**\n * @copyright " + filepath.replace(/^\./, "http://pcjs.org") + " (C) Jeff Parsons 2012-2016\n */\n\n" + src.replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, '') .replace(/^(import|export)[ \t]+[^\n]*\n/gm, '') - .replace(/^[ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);/gm, '') - .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;/gm, '') + .replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, '') + .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, '') .replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, '') .replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') .replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') @@ -237,8 +237,8 @@ module.exports = function(grunt) { return "/**\n * @copyright " + filepath.replace(/^\./, "http://pcjs.org") + " (C) Jeff Parsons 2012-2016\n */\n\n" + src.replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, '') .replace(/^(import|export)[ \t]+[^\n]*\n/gm, '') - .replace(/^[ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);/gm, '') - .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;/gm, '') + .replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, '') + .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, '') .replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, '') .replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') .replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') @@ -255,8 +255,8 @@ module.exports = function(grunt) { return "/**\n * @copyright " + filepath.replace(/^\./, "http://pcjs.org") + " (C) Jeff Parsons 2012-2016\n */\n\n" + src.replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, '') .replace(/^(import|export)[ \t]+[^\n]*\n/gm, '') - .replace(/^[ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);/gm, '') - .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;/gm, '') + .replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, '') + .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, '') .replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, '') .replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') .replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') diff --git a/gulpfile.js b/gulpfile.js index ed41a2f65..f326621d1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -92,8 +92,8 @@ gulp.task('mktmp', function() { .pipe(header('/**\n * @copyright ' + file.path.replace(/.*\/(modules\/.*)/, "http://pcjs.org/$1") + ' (C) Jeff Parsons 2012-2016\n */\n\n')) .pipe(replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, "")) .pipe(replace(/^(import|export)[ \t]+[^\n]*\n/gm, "")) - .pipe(replace(/^[ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);/gm, "")) - .pipe(replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;/gm, "")) + .pipe(replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, "")) + .pipe(replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, "")) .pipe(replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, "")) .pipe(replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, "")) .pipe(replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, "")) diff --git a/modules/htmlout/lib/htmlout.js b/modules/htmlout/lib/htmlout.js index 2a9c86571..0012ea5d7 100644 --- a/modules/htmlout/lib/htmlout.js +++ b/modules/htmlout/lib/htmlout.js @@ -514,8 +514,8 @@ HTMLOut.filter = function(req, res, next) } else { sData = sData.replace(/^([ \t]*import\s+\S+\s+from\s+(['"]).*?\1;)/gm, "// $1"); sData = sData.replace(/^([ \t]*export\s+default\s+\S+;)/gm, "// $1"); - sData = sData.replace(/^([ \t]*var\s+\S+\s+=\s+require\((['"]).*?\1\);)/gm, "// $1"); - sData = sData.replace(/^([ \t]*(if\s+\(NODE\)\s*|)module\.exports\s+=\s+\S+;)/gm, "// $1"); + sData = sData.replace(/^([ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);)/gm, "// $1"); + sData = sData.replace(/^([ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;)/gm, "// $1"); res.set("Content-Type", "application/javascript"); res.status(200).send(sData); } diff --git a/modules/pc8080/lib/bus.js b/modules/pc8080/lib/bus.js index fcadb3a09..e28385f1c 100644 --- a/modules/pc8080/lib/bus.js +++ b/modules/pc8080/lib/bus.js @@ -28,124 +28,992 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var Memory8080 = require("./memory"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Usr = require("../../shared/es6/usrlib"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var Memory8080 = require("./memory"); +var Messages8080 = require("./messages"); /** - * Bus8080(cpu, dbg) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The Bus8080 component manages physical memory and I/O address spaces. - * - * The Bus8080 component has no UI elements, so it does not require an init() handler, - * but it still inherits from the Component class and must be allocated like any - * other device component. It's currently allocated by the Computer's init() handler, - * which then calls the initBus() method of all the other components. - * - * For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped - * devices), the address space must still be allocated through the Bus8080 component via - * addMemory(). If the component needs something more than simple read/write storage, - * it must provide a custom controller. - * - * All port (I/O) operations are defined by external handlers; they register with us, - * and we manage those registrations and provide support for I/O breakpoints, but the - * only default I/O behavior we provide is ignoring writes to any unregistered output - * ports and returning 0xff from any unregistered input ports. - * - * @constructor - * @extends Component - * @param {Object} parmsBus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg + * @unrestricted */ -function Bus8080(parmsBus, cpu, dbg) -{ - Component.call(this, "Bus", parmsBus, Bus8080); - - this.cpu = cpu; - this.dbg = dbg; - - this.nBusWidth = parmsBus['busWidth'] || 16; - - /* - * Compute all Bus8080 memory block parameters, based on the width of the bus. The entire - * address space is divided into blocks, using a block size that is (hopefully) appropriate to - * the bus width. The following table summarizes our simplistic calculations. +class Bus8080 extends Component { + /** + * Bus8080(cpu, dbg) * - * Bus Width Block Shift Block Size - * --------- ----------- ---------- - * 16 bits (64Kb address space): 10 1Kb (64 maximum blocks) - * 18 bits (256Kb address space): 11 2Kb (128 maximum blocks) - * 20 bits (1Mb address space): 12 4Kb (256 maximum blocks) - * 22 bits (4Mb address space): 13 8Kb (512 maximum blocks) - * 24 bits (16Mb address space): 14 16Kb (1K maximum blocks) - * 32 bits (4Gb address space); 15 32Kb (128K maximum blocks) + * The Bus8080 component manages physical memory and I/O address spaces. * - * The coarser block granularities (ie, 16Kb and 32Kb) may cause problems for certain RAM and/or ROM - * allocations that are contiguous but are allocated out of order, or that have different controller - * requirements. Your choices, for the moment, are either to ensure the allocations are performed in - * order, or to choose smaller nBlockShift values (at the expense of a generating a larger block array). + * The Bus8080 component has no UI elements, so it does not require an init() handler, + * but it still inherits from the Component class and must be allocated like any + * other device component. It's currently allocated by the Computer's init() handler, + * which then calls the initBus() method of all the other components. + * + * For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped + * devices), the address space must still be allocated through the Bus8080 component via + * addMemory(). If the component needs something more than simple read/write storage, + * it must provide a custom controller. + * + * All port (I/O) operations are defined by external handlers; they register with us, + * and we manage those registrations and provide support for I/O breakpoints, but the + * only default I/O behavior we provide is ignoring writes to any unregistered output + * ports and returning 0xff from any unregistered input ports. + * + * @this {Bus8080} + * @param {Object} parmsBus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg */ - this.addrTotal = Math.pow(2, this.nBusWidth); - this.nBusLimit = this.nBusMask = (this.addrTotal - 1) | 0; - this.nBlockShift = (this.nBusWidth >> 1) + 2; - if (this.nBlockShift < 10) this.nBlockShift = 10; - if (this.nBlockShift > 15) this.nBlockShift = 15; - this.nBlockSize = 1 << this.nBlockShift; - this.nBlockLen = this.nBlockSize >> 2; - this.nBlockLimit = this.nBlockSize - 1; - this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0; - this.nBlockMask = this.nBlockTotal - 1; - this.assert(this.nBlockMask <= Bus8080.BlockInfo.num.mask); + constructor(parmsBus, cpu, dbg) + { + super("Bus", parmsBus, Bus8080); - /* - * Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by - * port, of sub-arrays which contain: + this.cpu = cpu; + this.dbg = dbg; + + this.nBusWidth = parmsBus['busWidth'] || 16; + + /* + * Compute all Bus8080 memory block parameters, based on the width of the bus. The entire + * address space is divided into blocks, using a block size that is (hopefully) appropriate to + * the bus width. The following table summarizes our simplistic calculations. + * + * Bus Width Block Shift Block Size + * --------- ----------- ---------- + * 16 bits (64Kb address space): 10 1Kb (64 maximum blocks) + * 18 bits (256Kb address space): 11 2Kb (128 maximum blocks) + * 20 bits (1Mb address space): 12 4Kb (256 maximum blocks) + * 22 bits (4Mb address space): 13 8Kb (512 maximum blocks) + * 24 bits (16Mb address space): 14 16Kb (1K maximum blocks) + * 32 bits (4Gb address space); 15 32Kb (128K maximum blocks) + * + * The coarser block granularities (ie, 16Kb and 32Kb) may cause problems for certain RAM and/or ROM + * allocations that are contiguous but are allocated out of order, or that have different controller + * requirements. Your choices, for the moment, are either to ensure the allocations are performed in + * order, or to choose smaller nBlockShift values (at the expense of a generating a larger block array). + */ + this.addrTotal = Math.pow(2, this.nBusWidth); + this.nBusLimit = this.nBusMask = (this.addrTotal - 1) | 0; + this.nBlockShift = (this.nBusWidth >> 1) + 2; + if (this.nBlockShift < 10) this.nBlockShift = 10; + if (this.nBlockShift > 15) this.nBlockShift = 15; + this.nBlockSize = 1 << this.nBlockShift; + this.nBlockLen = this.nBlockSize >> 2; + this.nBlockLimit = this.nBlockSize - 1; + this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0; + this.nBlockMask = this.nBlockTotal - 1; + this.assert(this.nBlockMask <= Bus8080.BlockInfo.num.mask); + + /* + * Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by + * port, of sub-arrays which contain: + * + * [0]: registered function to call for every I/O access + * + * The registered function is called with the port address, and if the access was triggered by the CPU, + * the instruction pointer (IP) at the point of access. + * + * WARNING: Unlike the (old) read and write memory notification functions, these support only one + * pair of input/output functions per port. A more sophisticated architecture could support a list + * of chained functions across multiple components, but I doubt that will be necessary here. + * + * UPDATE: The Debugger now piggy-backs on these arrays to indicate ports for which it wants notification + * of I/O. In those cases, the registered component/function elements may or may not be set, but the + * following additional element will be set: + * + * [1]: true to break on I/O, false to ignore I/O + * + * The false case is important if fPortInputBreakAll and/or fPortOutputBreakAll is set, because it allows the + * Debugger to selectively ignore specific ports. + */ + this.aPortInputNotify = []; + this.aPortOutputNotify = []; + this.fPortInputBreakAll = this.fPortOutputBreakAll = false; + + /* + * By default, all I/O ports are 1 byte wide; ports that are wider must add themselves to one or both of + * these lists, using addPortInputWidth() and/or addPortOutputWidth(). + */ + this.aPortInputWidth = []; + this.aPortOutputWidth = []; + + /* + * Allocate empty Memory blocks to span the entire physical address space. + */ + this.initMemory(); + + this.setReady(); + } + + /** + * initMemory() * - * [0]: registered function to call for every I/O access + * Allocate enough (empty) Memory blocks to span the entire physical address space. * - * The registered function is called with the port address, and if the access was triggered by the CPU, - * the instruction pointer (IP) at the point of access. - * - * WARNING: Unlike the (old) read and write memory notification functions, these support only one - * pair of input/output functions per port. A more sophisticated architecture could support a list - * of chained functions across multiple components, but I doubt that will be necessary here. - * - * UPDATE: The Debugger now piggy-backs on these arrays to indicate ports for which it wants notification - * of I/O. In those cases, the registered component/function elements may or may not be set, but the - * following additional element will be set: - * - * [1]: true to break on I/O, false to ignore I/O - * - * The false case is important if fPortInputBreakAll and/or fPortOutputBreakAll is set, because it allows the - * Debugger to selectively ignore specific ports. + * @this {Bus8080} */ - this.aPortInputNotify = []; - this.aPortOutputNotify = []; - this.fPortInputBreakAll = this.fPortOutputBreakAll = false; + initMemory() + { + var block = new Memory8080(); + block.copyBreakpoints(this.dbg); + this.aMemBlocks = new Array(this.nBlockTotal); + for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { + this.aMemBlocks[iBlock] = block; + } + } - /* - * By default, all I/O ports are 1 byte wide; ports that are wider must add themselves to one or both of - * these lists, using addPortInputWidth() and/or addPortOutputWidth(). + /** + * reset() + * + * @this {Bus8080} */ - this.aPortInputWidth = []; - this.aPortOutputWidth = []; + reset() + { + } - /* - * Allocate empty Memory blocks to span the entire physical address space. + /** + * powerUp(data, fRepower) + * + * We don't need a powerDown() handler, because for largely historical reasons, our state is saved by saveMemory(), + * which called by the CPU. + * + * However, we do need a powerUp() handler, because on resumable machines, the Computer's onReset() function calls + * everyone's powerUp() handler rather than their reset() handler. + * + * TODO: Perhaps Computer should be smarter: if there's no powerUp() handler, then fallback to the reset() handler. + * In that case, however, we'd either need to remove the powerUp() stub in Component, or detect the existence of the stub. + * + * @this {Bus8080} + * @param {Object|null} data (always null because we supply no powerDown() handler) + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure */ - this.initMemory(); + powerUp(data, fRepower) + { + if (!fRepower) this.reset(); + return true; + } - this.setReady(); + /** + * addMemory(addr, size, type) + * + * Adds new Memory blocks to the specified address range. Any Memory blocks previously + * added to that range must first be removed via removeMemory(); otherwise, you'll get + * an allocation conflict error. This helps prevent address calculation errors, redundant + * allocations, etc. + * + * We've relaxed some of the original requirements (ie, that addresses must start at a + * block-granular address, or that sizes must be equal to exactly one or more blocks), + * because machines with large block sizes can make it impossible to load certain ROMs at + * their required addresses. Every allocation still allocates a whole number of blocks. + * + * Even so, Bus8080 memory management does NOT provide a general-purpose heap. Most memory + * allocations occur during machine initialization and never change. In particular, there + * is NO support for removing partial-block allocations. + * + * Each Memory block keeps track of a start address (addr) and length (used), indicating + * the used space within the block; any free space that precedes or follows that used space + * can be allocated later, by simply extending the beginning or ending of the previously used + * space. However, any holes that might have existed between the original allocation and an + * extension are subsumed by the extension. + * + * @this {Bus8080} + * @param {number} addr is the starting physical address of the request + * @param {number} size of the request, in bytes + * @param {number} type is one of the Memory8080.TYPE constants + * @return {boolean} true if successful, false if not + */ + addMemory(addr, size, type) + { + var addrNext = addr; + var sizeLeft = size; + var iBlock = addrNext >>> this.nBlockShift; + + while (sizeLeft > 0 && iBlock < this.aMemBlocks.length) { + + var block = this.aMemBlocks[iBlock]; + var addrBlock = iBlock * this.nBlockSize; + var sizeBlock = this.nBlockSize - (addrNext - addrBlock); + if (sizeBlock > sizeLeft) sizeBlock = sizeLeft; + + if (block && block.size) { + if (block.type == type) { + /* + * Where there is already a similar block with a non-zero size, we allow the allocation only if: + * + * 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or + * 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block) + */ + if (addrNext + sizeLeft <= block.addr) { + block.used += (block.addr - addrNext); + block.addr = addrNext; + return true; + } + if (addrNext >= block.addr + block.used) { + var sizeAvail = block.size - (addrNext - addrBlock); + if (sizeAvail > sizeLeft) sizeAvail = sizeLeft; + block.used = addrNext - block.addr + sizeAvail; + addrNext = addrBlock + this.nBlockSize; + sizeLeft -= sizeAvail; + iBlock++; + continue; + } + } + return this.reportError(Bus8080.ERROR.ADD_MEM_INUSE, addrNext, sizeLeft); + } + + var blockNew = new Memory8080(addrNext, sizeBlock, this.nBlockSize, type); + blockNew.copyBreakpoints(this.dbg, block); + this.aMemBlocks[iBlock++] = blockNew; + + addrNext = addrBlock + this.nBlockSize; + sizeLeft -= sizeBlock; + } + + if (sizeLeft <= 0) { + this.status(Math.floor(size / 1024) + "Kb " + Memory8080.TYPE.NAMES[type] + " at " + Str.toHexWord(addr)); + return true; + } + + return this.reportError(Bus8080.ERROR.ADD_MEM_BADRANGE, addr, size); + } + + /** + * cleanMemory(addr, size) + * + * @this {Bus8080} + * @param {number} addr + * @param {number} size + * @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process + */ + cleanMemory(addr, size) + { + var fClean = true; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aMemBlocks.length) { + if (this.aMemBlocks[iBlock].fDirty) { + this.aMemBlocks[iBlock].fDirty = fClean = false; + this.aMemBlocks[iBlock].fDirtyEver = true; + } + size -= this.nBlockSize; + iBlock++; + } + return fClean; + } + + /** + * scanMemory(info, addr, size) + * + * Returns a BusInfo8080 object for the specified address range. + * + * @this {Bus8080} + * @param {Object} [info] previous BusInfo8080, if any + * @param {number} [addr] starting address of range (0 if none provided) + * @param {number} [size] size of range, in bytes (up to end of address space if none provided) + * @return {Object} updated info (or new info if no previous info provided) + */ + scanMemory(info, addr, size) + { + if (addr == null) addr = 0; + if (size == null) size = (this.addrTotal - addr) | 0; + if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []}; + + var iBlock = addr >>> this.nBlockShift; + var iBlockMax = ((addr + size - 1) >>> this.nBlockShift); + + info.cbTotal = 0; + info.cBlocks = 0; + while (iBlock <= iBlockMax) { + var block = this.aMemBlocks[iBlock]; + info.cbTotal += block.size; + if (block.size) { + info.aBlocks.push(Usr.initBitFields(Bus8080.BlockInfo, iBlock, 0, 0, block.type)); + info.cBlocks++ + } + iBlock++; + } + return info; + } + + /** + * getWidth() + * + * @this {Bus8080} + * @return {number} + */ + getWidth() + { + return this.nBusWidth; + } + + /** + * removeMemory(addr, size) + * + * Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes. + * + * TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface. + * + * @this {Bus8080} + * @param {number} addr + * @param {number} size + * @return {boolean} true if successful, false if not + */ + removeMemory(addr, size) + { + if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) { + var iBlock = addr >>> this.nBlockShift; + while (size > 0) { + var blockOld = this.aMemBlocks[iBlock]; + var blockNew = new Memory8080(addr); + blockNew.copyBreakpoints(this.dbg, blockOld); + this.aMemBlocks[iBlock++] = blockNew; + addr = iBlock * this.nBlockSize; + size -= this.nBlockSize; + } + return true; + } + return this.reportError(Bus8080.ERROR.REM_MEM_BADRANGE, addr, size); + } + + /** + * getMemoryBlocks(addr, size) + * + * @this {Bus8080} + * @param {number} addr is the starting physical address + * @param {number} size of the request, in bytes + * @return {Array} of Memory blocks + */ + getMemoryBlocks(addr, size) + { + var aBlocks = []; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aMemBlocks.length) { + aBlocks.push(this.aMemBlocks[iBlock++]); + size -= this.nBlockSize; + } + return aBlocks; + } + + /** + * setMemoryBlocks(addr, size, aBlocks, type) + * + * If no type is specified, then specified address range uses all the provided blocks as-is; + * this form of setMemoryBlocks() is used for complete physical aliases. + * + * Otherwise, new blocks are allocated with the specified type; the underlying memory from the + * provided blocks is still used, but the new blocks may have different access to that memory. + * + * @this {Bus8080} + * @param {number} addr is the starting physical address + * @param {number} size of the request, in bytes + * @param {Array} aBlocks as returned by getMemoryBlocks() + * @param {number} [type] is one of the Memory8080.TYPE constants + */ + setMemoryBlocks(addr, size, aBlocks, type) + { + var i = 0; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aMemBlocks.length) { + var block = aBlocks[i++]; + this.assert(block); + if (!block) break; + if (type !== undefined) { + var blockNew = new Memory8080(addr); + blockNew.clone(block, type, this.dbg); + block = blockNew; + } + this.aMemBlocks[iBlock++] = block; + size -= this.nBlockSize; + } + } + + /** + * getByte(addr) + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @return {number} byte (8-bit) value at that address + */ + getByte(addr) + { + return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByte(addr & this.nBlockLimit, addr); + } + + /** + * getByteDirect(addr) + * + * This is useful for the Debugger and other components that want to bypass getByte() breakpoint detection. + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @return {number} byte (8-bit) value at that address + */ + getByteDirect(addr) + { + return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByteDirect(addr & this.nBlockLimit, addr); + } + + /** + * getShort(addr) + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ + getShort(addr) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + if (off != this.nBlockLimit) { + return this.aMemBlocks[iBlock].readShort(off, addr); + } + return this.aMemBlocks[iBlock++].readByte(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByte(0, addr + 1) << 8); + } + + /** + * getShortDirect(addr) + * + * This is useful for the Debugger and other components that want to bypass getShort() breakpoint detection. + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ + getShortDirect(addr) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + if (off != this.nBlockLimit) { + return this.aMemBlocks[iBlock].readShortDirect(off, addr); + } + return this.aMemBlocks[iBlock++].readByteDirect(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByteDirect(0, addr + 1) << 8); + } + + /** + * setByte(addr, b) + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) + */ + setByte(addr, b) + { + this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByte(addr & this.nBlockLimit, b & 0xff, addr); + } + + /** + * setByteDirect(addr, b) + * + * This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only + * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) + */ + setByteDirect(addr, b) + { + this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr); + } + + /** + * setShort(addr, w) + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) + */ + setShort(addr, w) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + if (off != this.nBlockLimit) { + this.aMemBlocks[iBlock].writeShort(off, w & 0xffff, addr); + return; + } + this.aMemBlocks[iBlock++].writeByte(off, w & 0xff, addr); + this.aMemBlocks[iBlock & this.nBlockMask].writeByte(0, (w >> 8) & 0xff, addr + 1); + } + + /** + * setShortDirect(addr, w) + * + * This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only + * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). + * + * @this {Bus8080} + * @param {number} addr is a physical address + * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) + */ + setShortDirect(addr, w) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + if (off != this.nBlockLimit) { + this.aMemBlocks[iBlock].writeShortDirect(off, w & 0xffff, addr); + return; + } + this.aMemBlocks[iBlock++].writeByteDirect(off, w & 0xff, addr); + this.aMemBlocks[iBlock & this.nBlockMask].writeByteDirect(0, (w >> 8) & 0xff, addr + 1); + } + + /** + * addMemBreak(addr, fWrite) + * + * @this {Bus8080} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ + addMemBreak(addr, fWrite) + { + if (DEBUGGER) { + var iBlock = addr >>> this.nBlockShift; + this.aMemBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite); + } + } + + /** + * removeMemBreak(addr, fWrite) + * + * @this {Bus8080} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ + removeMemBreak(addr, fWrite) + { + if (DEBUGGER) { + var iBlock = addr >>> this.nBlockShift; + this.aMemBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite); + } + } + + /** + * saveMemory(fAll) + * + * The only memory blocks we save are those marked as dirty, but most likely all of RAM will have been marked dirty, + * and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed + * what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least + * all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them). + * + * All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so: + * + * [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...] + * + * In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit + * integer (because they are formed using bit-wise operator rather than floating-point math operators), so don't be + * surprised to see negative numbers in the data. + * + * The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays + * will look like: + * + * [count0, dw0, count1, dw1, ...] + * + * where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates + * that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State + * helper methods compress() and decompress() to create and expand the compressed data arrays. + * + * @this {Bus8080} + * @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags) + * @return {Array} a + */ + saveMemory(fAll) + { + var i = 0; + var a = []; + + for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { + var block = this.aMemBlocks[iBlock]; + /* + * We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of + * the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag, + * it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine. + */ + if (fAll && block.type != Memory8080.TYPE.ROM || block.fDirty || block.fDirtyEver) { + a[i++] = iBlock; + a[i++] = State.compress(block.save()); + } + } + + return a; + } + + /** + * restoreMemory(a) + * + * This restores the contents of all Memory blocks; called by CPUState.restore(). + * + * In theory, we ONLY have to save/restore block contents. Other block attributes, + * like the type, the memory controller (if any), and the active memory access functions, + * should already be restored, since every component (re)allocates all the memory blocks + * it was using when it's restored. And since the CPU is guaranteed to be the last + * component to be restored, all those blocks (and their attributes) should be in place now. + * + * See saveMemory() for more information on how the memory block contents are saved. + * + * @this {Bus8080} + * @param {Array} a + * @return {boolean} true if successful, false if not + */ + restoreMemory(a) + { + var i; + for (i = 0; i < a.length - 1; i += 2) { + var iBlock = a[i]; + var adw = a[i+1]; + if (adw && adw.length < this.nBlockLen) { + adw = State.decompress(adw, this.nBlockLen); + } + var block = this.aMemBlocks[iBlock]; + if (!block || !block.restore(adw)) { + /* + * Either the block to restore hasn't been allocated, indicating a change in the machine + * configuration since it was last saved (the most likely explanation) or there's some internal + * inconsistency (eg, the block size is wrong). + */ + Component.error("Unable to restore memory block " + iBlock); + return false; + } + } + return true; + } + + /** + * addPortInputBreak(port) + * + * @this {Bus8080} + * @param {number} [port] + * @return {boolean} true if break on port input enabled, false if disabled + */ + addPortInputBreak(port) + { + if (port === undefined) { + this.fPortInputBreakAll = !this.fPortInputBreakAll; + return this.fPortInputBreakAll; + } + if (this.aPortInputNotify[port] === undefined) { + this.aPortInputNotify[port] = [null, false]; + } + this.aPortInputNotify[port][1] = !this.aPortInputNotify[port][1]; + return this.aPortInputNotify[port][1]; + } + + /** + * addPortInputNotify(start, end, fn) + * + * Add a port input-notification handler to the list of such handlers. + * + * @this {Bus8080} + * @param {number} start port address + * @param {number} end port address + * @param {function(number,number)} fn is called with the port and IP values at the time of the input + */ + addPortInputNotify(start, end, fn) + { + if (fn !== undefined) { + for (var port = start; port <= end; port++) { + if (this.aPortInputNotify[port] !== undefined) { + Component.warning("Input port " + Str.toHexWord(port) + " already registered"); + continue; + } + this.aPortInputNotify[port] = [fn, false]; + if (MAXDEBUG) this.log("addPortInputNotify(" + Str.toHexWord(port) + ")"); + } + } + } + + /** + * addPortInputTable(component, table, offset) + * + * Add port input-notification handlers from the specified table (a batch version of addPortInputNotify) + * + * @this {Bus8080} + * @param {Component} component + * @param {Object} table + * @param {number} [offset] is an optional port offset + */ + addPortInputTable(component, table, offset) + { + if (offset === undefined) offset = 0; + if (table) { + for (var port in table) { + this.addPortInputNotify(+port + offset, +port + offset, table[port].bind(component)); + } + } + } + + /** + * addPortInputWidth(port, size) + * + * By default, all input ports are 1 byte wide; ports that are wider must call this function. + * + * @this {Bus8080} + * @param {number} port + * @param {number} size (1, 2 or 4) + */ + addPortInputWidth(port, size) + { + this.aPortInputWidth[port] = size; + } + + /** + * checkPortInputNotify(port, size, addrIP) + * + * @this {Bus8080} + * @param {number} port + * @param {number} size (1, 2 or 4) + * @param {number} [addrIP] is the IP value at the time of the input + * @return {number} simulated port data + * + * NOTE: It seems that parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS) + * assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below. + */ + checkPortInputNotify(port, size, addrIP) + { + var data = 0, shift = 0; + + while (size > 0) { + + var aNotify = this.aPortInputNotify[port]; + var sizePort = this.aPortInputWidth[port] || 1; + var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1)); + var dataPort = maskPort; + + /* + * TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O + * to a 32-bit port). We probably should pass the size through to the aNotify[0] handler, + * and let it decide what to do, but I don't feel like changing all the I/O handlers right now. + * The good news, at least, is that the 8-bit handlers would not have to do anything special. + * This assert will warn us if this is a pressing need. + */ + this.assert(size >= sizePort); + + if (aNotify !== undefined) { + if (aNotify[0]) { + dataPort = aNotify[0](port, addrIP); + if (dataPort === undefined) { + dataPort = maskPort; + } else { + dataPort &= maskPort; + } + } + if (DEBUGGER && this.dbg && this.fPortInputBreakAll != aNotify[1]) { + this.dbg.checkPortInput(port, size, dataPort); + } + } + else { + if (DEBUGGER && this.dbg) { + this.dbg.messageIO(this, port, null, addrIP); + if (this.fPortInputBreakAll) this.dbg.checkPortInput(port, size, dataPort); + } + } + + data |= dataPort << shift; + shift += (sizePort << 3); + port += sizePort; + size -= sizePort; + } + + this.assert(!size); + return data; + } + + /** + * addPortOutputBreak(port) + * + * @this {Bus8080} + * @param {number} [port] + * @return {boolean} true if break on port output enabled, false if disabled + */ + addPortOutputBreak(port) + { + if (port === undefined) { + this.fPortOutputBreakAll = !this.fPortOutputBreakAll; + return this.fPortOutputBreakAll; + } + if (this.aPortOutputNotify[port] === undefined) { + this.aPortOutputNotify[port] = [null, false]; + } + this.aPortOutputNotify[port][1] = !this.aPortOutputNotify[port][1]; + return this.aPortOutputNotify[port][1]; + } + + /** + * addPortOutputNotify(start, end, fn) + * + * Add a port output-notification handler to the list of such handlers. + * + * @this {Bus8080} + * @param {number} start port address + * @param {number} end port address + * @param {function(number,number)} fn is called with the port and IP values at the time of the output + */ + addPortOutputNotify(start, end, fn) + { + if (fn !== undefined) { + for (var port = start; port <= end; port++) { + if (this.aPortOutputNotify[port] !== undefined) { + Component.warning("Output port " + Str.toHexWord(port) + " already registered"); + continue; + } + this.aPortOutputNotify[port] = [fn, false]; + if (MAXDEBUG) this.log("addPortOutputNotify(" + Str.toHexWord(port) + ")"); + } + } + } + + /** + * addPortOutputTable(component, table, offset) + * + * Add port output-notification handlers from the specified table (a batch version of addPortOutputNotify) + * + * @this {Bus8080} + * @param {Component} component + * @param {Object} table + * @param {number} [offset] is an optional port offset + */ + addPortOutputTable(component, table, offset) + { + if (offset === undefined) offset = 0; + if (table) { + for (var port in table) { + this.addPortOutputNotify(+port + offset, +port + offset, table[port].bind(component)); + } + } + } + + /** + * addPortOutputWidth(port, size) + * + * By default, all output ports are 1 byte wide; ports that are wider must call this function. + * + * @this {Bus8080} + * @param {number} port + * @param {number} size (1, 2 or 4) + */ + addPortOutputWidth(port, size) + { + this.aPortOutputWidth[port] = size; + } + + /** + * checkPortOutputNotify(port, size, data, addrIP) + * + * @this {Bus8080} + * @param {number} port + * @param {number} size + * @param {number} data + * @param {number} [addrIP] is the IP value at the time of the output + */ + checkPortOutputNotify(port, size, data, addrIP) + { + var shift = 0; + + while (size > 0) { + + var aNotify = this.aPortOutputNotify[port]; + var sizePort = this.aPortOutputWidth[port] || 1; + var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1)); + var dataPort = (data >>>= shift) & maskPort; + + /* + * TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O + * to a 32-bit port). We probably should pass the size through to the aNotify[0] handler, + * and let it decide what to do, but I don't feel like changing all the I/O handlers right now. + * The good news, at least, is that the 8-bit handlers would not have to do anything special. + * This assert will warn us if this is a pressing need. + */ + this.assert(size >= sizePort); + + if (aNotify !== undefined) { + if (aNotify[0]) { + aNotify[0](port, dataPort, addrIP); + } + if (DEBUGGER && this.dbg && this.fPortOutputBreakAll != aNotify[1]) { + this.dbg.checkPortOutput(port, size, dataPort); + } + } + else { + if (DEBUGGER && this.dbg) { + this.dbg.messageIO(this, port, dataPort, addrIP); + if (this.fPortOutputBreakAll) this.dbg.checkPortOutput(port, size, dataPort); + } + } + + shift += (sizePort << 3); + port += sizePort; + size -= sizePort; + } + this.assert(!size); + } + + /** + * removePortInputNotify(start, end) + * + * Remove port input-notification handler(s) (to be ENABLED later if needed) + * + * @this {Bus8080} + * @param {number} start address + * @param {number} end address + * + removePortInputNotify(start, end) + { + for (var port = start; port < end; port++) { + if (this.aPortInputNotify[port]) { + delete this.aPortInputNotify[port]; + } + } + } + */ + + /** + * removePortOutputNotify(start, end) + * + * Remove port output-notification handler(s) (to be ENABLED later if needed) + * + * @this {Bus8080} + * @param {number} start address + * @param {number} end address + * + removePortOutputNotify(start, end) + { + for (var port = start; port < end; port++) { + if (this.aPortOutputNotify[port]) { + delete this.aPortOutputNotify[port]; + } + } + } + */ + + /** + * reportError(op, addr, size, fQuiet) + * + * @this {Bus8080} + * @param {number} op + * @param {number} addr + * @param {number} size + * @param {boolean} [fQuiet] (true if any error should be quietly logged) + * @return {boolean} false + */ + reportError(op, addr, size, fQuiet) + { + var sError = "Memory block error (" + op + ": " + Str.toHex(addr) + "," + Str.toHex(size) + ")"; + if (fQuiet) { + if (this.dbg) { + this.dbg.message(sError); + } else { + this.log(sError); + } + } else { + Component.error(sError); + } + return false; + } } -Component.subclass(Bus8080); - Bus8080.ERROR = { ADD_MEM_INUSE: 1, ADD_MEM_BADRANGE: 2, @@ -153,162 +1021,6 @@ Bus8080.ERROR = { REM_MEM_BADRANGE: 5 }; -/** - * initMemory() - * - * Allocate enough (empty) Memory blocks to span the entire physical address space. - * - * @this {Bus8080} - */ -Bus8080.prototype.initMemory = function() -{ - var block = new Memory8080(); - block.copyBreakpoints(this.dbg); - this.aMemBlocks = new Array(this.nBlockTotal); - for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { - this.aMemBlocks[iBlock] = block; - } -}; - -/** - * reset() - * - * @this {Bus8080} - */ -Bus8080.prototype.reset = function() -{ -}; - -/** - * powerUp(data, fRepower) - * - * We don't need a powerDown() handler, because for largely historical reasons, our state is saved by saveMemory(), - * which called by the CPU. - * - * However, we do need a powerUp() handler, because on resumable machines, the Computer's onReset() function calls - * everyone's powerUp() handler rather than their reset() handler. - * - * TODO: Perhaps Computer should be smarter: if there's no powerUp() handler, then fallback to the reset() handler. - * In that case, however, we'd either need to remove the powerUp() stub in Component, or detect the existence of the stub. - * - * @this {Bus8080} - * @param {Object|null} data (always null because we supply no powerDown() handler) - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -Bus8080.prototype.powerUp = function(data, fRepower) -{ - if (!fRepower) this.reset(); - return true; -}; - -/** - * addMemory(addr, size, type) - * - * Adds new Memory blocks to the specified address range. Any Memory blocks previously - * added to that range must first be removed via removeMemory(); otherwise, you'll get - * an allocation conflict error. This helps prevent address calculation errors, redundant - * allocations, etc. - * - * We've relaxed some of the original requirements (ie, that addresses must start at a - * block-granular address, or that sizes must be equal to exactly one or more blocks), - * because machines with large block sizes can make it impossible to load certain ROMs at - * their required addresses. Every allocation still allocates a whole number of blocks. - * - * Even so, Bus8080 memory management does NOT provide a general-purpose heap. Most memory - * allocations occur during machine initialization and never change. In particular, there - * is NO support for removing partial-block allocations. - * - * Each Memory block keeps track of a start address (addr) and length (used), indicating - * the used space within the block; any free space that precedes or follows that used space - * can be allocated later, by simply extending the beginning or ending of the previously used - * space. However, any holes that might have existed between the original allocation and an - * extension are subsumed by the extension. - * - * @this {Bus8080} - * @param {number} addr is the starting physical address of the request - * @param {number} size of the request, in bytes - * @param {number} type is one of the Memory8080.TYPE constants - * @return {boolean} true if successful, false if not - */ -Bus8080.prototype.addMemory = function(addr, size, type) -{ - var addrNext = addr; - var sizeLeft = size; - var iBlock = addrNext >>> this.nBlockShift; - - while (sizeLeft > 0 && iBlock < this.aMemBlocks.length) { - - var block = this.aMemBlocks[iBlock]; - var addrBlock = iBlock * this.nBlockSize; - var sizeBlock = this.nBlockSize - (addrNext - addrBlock); - if (sizeBlock > sizeLeft) sizeBlock = sizeLeft; - - if (block && block.size) { - if (block.type == type) { - /* - * Where there is already a similar block with a non-zero size, we allow the allocation only if: - * - * 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or - * 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block) - */ - if (addrNext + sizeLeft <= block.addr) { - block.used += (block.addr - addrNext); - block.addr = addrNext; - return true; - } - if (addrNext >= block.addr + block.used) { - var sizeAvail = block.size - (addrNext - addrBlock); - if (sizeAvail > sizeLeft) sizeAvail = sizeLeft; - block.used = addrNext - block.addr + sizeAvail; - addrNext = addrBlock + this.nBlockSize; - sizeLeft -= sizeAvail; - iBlock++; - continue; - } - } - return this.reportError(Bus8080.ERROR.ADD_MEM_INUSE, addrNext, sizeLeft); - } - - var blockNew = new Memory8080(addrNext, sizeBlock, this.nBlockSize, type); - blockNew.copyBreakpoints(this.dbg, block); - this.aMemBlocks[iBlock++] = blockNew; - - addrNext = addrBlock + this.nBlockSize; - sizeLeft -= sizeBlock; - } - - if (sizeLeft <= 0) { - this.status(Math.floor(size / 1024) + "Kb " + Memory8080.TYPE.NAMES[type] + " at " + str.toHexWord(addr)); - return true; - } - - return this.reportError(Bus8080.ERROR.ADD_MEM_BADRANGE, addr, size); -}; - -/** - * cleanMemory(addr, size) - * - * @this {Bus8080} - * @param {number} addr - * @param {number} size - * @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process - */ -Bus8080.prototype.cleanMemory = function(addr, size) -{ - var fClean = true; - var iBlock = addr >>> this.nBlockShift; - while (size > 0 && iBlock < this.aMemBlocks.length) { - if (this.aMemBlocks[iBlock].fDirty) { - this.aMemBlocks[iBlock].fDirty = fClean = false; - this.aMemBlocks[iBlock].fDirtyEver = true; - } - size -= this.nBlockSize; - iBlock++; - } - return fClean; -}; - /* * Data types used by scanMemory() */ @@ -328,7 +1040,7 @@ var BlockInfo; * type: BitField * }} */ -Bus8080.BlockInfo = usr.defineBitFields({num:20, count:8, btmod:1, type:3}); +Bus8080.BlockInfo = Usr.defineBitFields({num:20, count:8, btmod:1, type:3}); /** * BusInfo8080 object definition (returned by scanMemory()) @@ -345,712 +1057,4 @@ Bus8080.BlockInfo = usr.defineBitFields({num:20, count:8, btmod:1, type:3}); */ var BusInfo8080; -/** - * scanMemory(info, addr, size) - * - * Returns a BusInfo8080 object for the specified address range. - * - * @this {Bus8080} - * @param {Object} [info] previous BusInfo8080, if any - * @param {number} [addr] starting address of range (0 if none provided) - * @param {number} [size] size of range, in bytes (up to end of address space if none provided) - * @return {Object} updated info (or new info if no previous info provided) - */ -Bus8080.prototype.scanMemory = function(info, addr, size) -{ - if (addr == null) addr = 0; - if (size == null) size = (this.addrTotal - addr) | 0; - if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []}; - - var iBlock = addr >>> this.nBlockShift; - var iBlockMax = ((addr + size - 1) >>> this.nBlockShift); - - info.cbTotal = 0; - info.cBlocks = 0; - while (iBlock <= iBlockMax) { - var block = this.aMemBlocks[iBlock]; - info.cbTotal += block.size; - if (block.size) { - info.aBlocks.push(usr.initBitFields(Bus8080.BlockInfo, iBlock, 0, 0, block.type)); - info.cBlocks++ - } - iBlock++; - } - return info; -}; - -/** - * getWidth() - * - * @this {Bus8080} - * @return {number} - */ -Bus8080.prototype.getWidth = function() -{ - return this.nBusWidth; -}; - -/** - * removeMemory(addr, size) - * - * Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes. - * - * TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface. - * - * @this {Bus8080} - * @param {number} addr - * @param {number} size - * @return {boolean} true if successful, false if not - */ -Bus8080.prototype.removeMemory = function(addr, size) -{ - if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) { - var iBlock = addr >>> this.nBlockShift; - while (size > 0) { - var blockOld = this.aMemBlocks[iBlock]; - var blockNew = new Memory8080(addr); - blockNew.copyBreakpoints(this.dbg, blockOld); - this.aMemBlocks[iBlock++] = blockNew; - addr = iBlock * this.nBlockSize; - size -= this.nBlockSize; - } - return true; - } - return this.reportError(Bus8080.ERROR.REM_MEM_BADRANGE, addr, size); -}; - -/** - * getMemoryBlocks(addr, size) - * - * @this {Bus8080} - * @param {number} addr is the starting physical address - * @param {number} size of the request, in bytes - * @return {Array} of Memory blocks - */ -Bus8080.prototype.getMemoryBlocks = function(addr, size) -{ - var aBlocks = []; - var iBlock = addr >>> this.nBlockShift; - while (size > 0 && iBlock < this.aMemBlocks.length) { - aBlocks.push(this.aMemBlocks[iBlock++]); - size -= this.nBlockSize; - } - return aBlocks; -}; - -/** - * setMemoryBlocks(addr, size, aBlocks, type) - * - * If no type is specified, then specified address range uses all the provided blocks as-is; - * this form of setMemoryBlocks() is used for complete physical aliases. - * - * Otherwise, new blocks are allocated with the specified type; the underlying memory from the - * provided blocks is still used, but the new blocks may have different access to that memory. - * - * @this {Bus8080} - * @param {number} addr is the starting physical address - * @param {number} size of the request, in bytes - * @param {Array} aBlocks as returned by getMemoryBlocks() - * @param {number} [type] is one of the Memory8080.TYPE constants - */ -Bus8080.prototype.setMemoryBlocks = function(addr, size, aBlocks, type) -{ - var i = 0; - var iBlock = addr >>> this.nBlockShift; - while (size > 0 && iBlock < this.aMemBlocks.length) { - var block = aBlocks[i++]; - this.assert(block); - if (!block) break; - if (type !== undefined) { - var blockNew = new Memory8080(addr); - blockNew.clone(block, type, this.dbg); - block = blockNew; - } - this.aMemBlocks[iBlock++] = block; - size -= this.nBlockSize; - } -}; - -/** - * getByte(addr) - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @return {number} byte (8-bit) value at that address - */ -Bus8080.prototype.getByte = function(addr) -{ - return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByte(addr & this.nBlockLimit, addr); -}; - -/** - * getByteDirect(addr) - * - * This is useful for the Debugger and other components that want to bypass getByte() breakpoint detection. - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @return {number} byte (8-bit) value at that address - */ -Bus8080.prototype.getByteDirect = function(addr) -{ - return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByteDirect(addr & this.nBlockLimit, addr); -}; - -/** - * getShort(addr) - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @return {number} word (16-bit) value at that address - */ -Bus8080.prototype.getShort = function(addr) -{ - var off = addr & this.nBlockLimit; - var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; - if (off != this.nBlockLimit) { - return this.aMemBlocks[iBlock].readShort(off, addr); - } - return this.aMemBlocks[iBlock++].readByte(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByte(0, addr + 1) << 8); -}; - -/** - * getShortDirect(addr) - * - * This is useful for the Debugger and other components that want to bypass getShort() breakpoint detection. - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @return {number} word (16-bit) value at that address - */ -Bus8080.prototype.getShortDirect = function(addr) -{ - var off = addr & this.nBlockLimit; - var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; - if (off != this.nBlockLimit) { - return this.aMemBlocks[iBlock].readShortDirect(off, addr); - } - return this.aMemBlocks[iBlock++].readByteDirect(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByteDirect(0, addr + 1) << 8); -}; - -/** - * setByte(addr, b) - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) - */ -Bus8080.prototype.setByte = function(addr, b) -{ - this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByte(addr & this.nBlockLimit, b & 0xff, addr); -}; - -/** - * setByteDirect(addr, b) - * - * This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only - * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) - */ -Bus8080.prototype.setByteDirect = function(addr, b) -{ - this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr); -}; - -/** - * setShort(addr, w) - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) - */ -Bus8080.prototype.setShort = function(addr, w) -{ - var off = addr & this.nBlockLimit; - var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; - if (off != this.nBlockLimit) { - this.aMemBlocks[iBlock].writeShort(off, w & 0xffff, addr); - return; - } - this.aMemBlocks[iBlock++].writeByte(off, w & 0xff, addr); - this.aMemBlocks[iBlock & this.nBlockMask].writeByte(0, (w >> 8) & 0xff, addr + 1); -}; - -/** - * setShortDirect(addr, w) - * - * This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only - * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). - * - * @this {Bus8080} - * @param {number} addr is a physical address - * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) - */ -Bus8080.prototype.setShortDirect = function(addr, w) -{ - var off = addr & this.nBlockLimit; - var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; - if (off != this.nBlockLimit) { - this.aMemBlocks[iBlock].writeShortDirect(off, w & 0xffff, addr); - return; - } - this.aMemBlocks[iBlock++].writeByteDirect(off, w & 0xff, addr); - this.aMemBlocks[iBlock & this.nBlockMask].writeByteDirect(0, (w >> 8) & 0xff, addr + 1); -}; - -/** - * addMemBreak(addr, fWrite) - * - * @this {Bus8080} - * @param {number} addr - * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint - */ -Bus8080.prototype.addMemBreak = function(addr, fWrite) -{ - if (DEBUGGER) { - var iBlock = addr >>> this.nBlockShift; - this.aMemBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite); - } -}; - -/** - * removeMemBreak(addr, fWrite) - * - * @this {Bus8080} - * @param {number} addr - * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint - */ -Bus8080.prototype.removeMemBreak = function(addr, fWrite) -{ - if (DEBUGGER) { - var iBlock = addr >>> this.nBlockShift; - this.aMemBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite); - } -}; - -/** - * saveMemory(fAll) - * - * The only memory blocks we save are those marked as dirty, but most likely all of RAM will have been marked dirty, - * and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed - * what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least - * all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them). - * - * All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so: - * - * [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...] - * - * In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit - * integer (because they are formed using bit-wise operator rather than floating-point math operators), so don't be - * surprised to see negative numbers in the data. - * - * The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays - * will look like: - * - * [count0, dw0, count1, dw1, ...] - * - * where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates - * that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State - * helper methods compress() and decompress() to create and expand the compressed data arrays. - * - * @this {Bus8080} - * @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags) - * @return {Array} a - */ -Bus8080.prototype.saveMemory = function(fAll) -{ - var i = 0; - var a = []; - - for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { - var block = this.aMemBlocks[iBlock]; - /* - * We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of - * the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag, - * it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine. - */ - if (fAll && block.type != Memory8080.TYPE.ROM || block.fDirty || block.fDirtyEver) { - a[i++] = iBlock; - a[i++] = State.compress(block.save()); - } - } - - return a; -}; - -/** - * restoreMemory(a) - * - * This restores the contents of all Memory blocks; called by CPUState.restore(). - * - * In theory, we ONLY have to save/restore block contents. Other block attributes, - * like the type, the memory controller (if any), and the active memory access functions, - * should already be restored, since every component (re)allocates all the memory blocks - * it was using when it's restored. And since the CPU is guaranteed to be the last - * component to be restored, all those blocks (and their attributes) should be in place now. - * - * See saveMemory() for more information on how the memory block contents are saved. - * - * @this {Bus8080} - * @param {Array} a - * @return {boolean} true if successful, false if not - */ -Bus8080.prototype.restoreMemory = function(a) -{ - var i; - for (i = 0; i < a.length - 1; i += 2) { - var iBlock = a[i]; - var adw = a[i+1]; - if (adw && adw.length < this.nBlockLen) { - adw = State.decompress(adw, this.nBlockLen); - } - var block = this.aMemBlocks[iBlock]; - if (!block || !block.restore(adw)) { - /* - * Either the block to restore hasn't been allocated, indicating a change in the machine - * configuration since it was last saved (the most likely explanation) or there's some internal - * inconsistency (eg, the block size is wrong). - */ - Component.error("Unable to restore memory block " + iBlock); - return false; - } - } - return true; -}; - -/** - * addPortInputBreak(port) - * - * @this {Bus8080} - * @param {number} [port] - * @return {boolean} true if break on port input enabled, false if disabled - */ -Bus8080.prototype.addPortInputBreak = function(port) -{ - if (port === undefined) { - this.fPortInputBreakAll = !this.fPortInputBreakAll; - return this.fPortInputBreakAll; - } - if (this.aPortInputNotify[port] === undefined) { - this.aPortInputNotify[port] = [null, false]; - } - this.aPortInputNotify[port][1] = !this.aPortInputNotify[port][1]; - return this.aPortInputNotify[port][1]; -}; - -/** - * addPortInputNotify(start, end, fn) - * - * Add a port input-notification handler to the list of such handlers. - * - * @this {Bus8080} - * @param {number} start port address - * @param {number} end port address - * @param {function(number,number)} fn is called with the port and IP values at the time of the input - */ -Bus8080.prototype.addPortInputNotify = function(start, end, fn) -{ - if (fn !== undefined) { - for (var port = start; port <= end; port++) { - if (this.aPortInputNotify[port] !== undefined) { - Component.warning("Input port " + str.toHexWord(port) + " already registered"); - continue; - } - this.aPortInputNotify[port] = [fn, false]; - if (MAXDEBUG) this.log("addPortInputNotify(" + str.toHexWord(port) + ")"); - } - } -}; - -/** - * addPortInputTable(component, table, offset) - * - * Add port input-notification handlers from the specified table (a batch version of addPortInputNotify) - * - * @this {Bus8080} - * @param {Component} component - * @param {Object} table - * @param {number} [offset] is an optional port offset - */ -Bus8080.prototype.addPortInputTable = function(component, table, offset) -{ - if (offset === undefined) offset = 0; - if (table) { - for (var port in table) { - this.addPortInputNotify(+port + offset, +port + offset, table[port].bind(component)); - } - } -}; - -/** - * addPortInputWidth(port, size) - * - * By default, all input ports are 1 byte wide; ports that are wider must call this function. - * - * @this {Bus8080} - * @param {number} port - * @param {number} size (1, 2 or 4) - */ -Bus8080.prototype.addPortInputWidth = function(port, size) -{ - this.aPortInputWidth[port] = size; -}; - -/** - * checkPortInputNotify(port, size, addrIP) - * - * @this {Bus8080} - * @param {number} port - * @param {number} size (1, 2 or 4) - * @param {number} [addrIP] is the IP value at the time of the input - * @return {number} simulated port data - * - * NOTE: It seems that parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS) - * assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below. - */ -Bus8080.prototype.checkPortInputNotify = function(port, size, addrIP) -{ - var data = 0, shift = 0; - - while (size > 0) { - - var aNotify = this.aPortInputNotify[port]; - var sizePort = this.aPortInputWidth[port] || 1; - var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1)); - var dataPort = maskPort; - - /* - * TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O - * to a 32-bit port). We probably should pass the size through to the aNotify[0] handler, - * and let it decide what to do, but I don't feel like changing all the I/O handlers right now. - * The good news, at least, is that the 8-bit handlers would not have to do anything special. - * This assert will warn us if this is a pressing need. - */ - this.assert(size >= sizePort); - - if (aNotify !== undefined) { - if (aNotify[0]) { - dataPort = aNotify[0](port, addrIP); - if (dataPort === undefined) { - dataPort = maskPort; - } else { - dataPort &= maskPort; - } - } - if (DEBUGGER && this.dbg && this.fPortInputBreakAll != aNotify[1]) { - this.dbg.checkPortInput(port, size, dataPort); - } - } - else { - if (DEBUGGER && this.dbg) { - this.dbg.messageIO(this, port, null, addrIP); - if (this.fPortInputBreakAll) this.dbg.checkPortInput(port, size, dataPort); - } - } - - data |= dataPort << shift; - shift += (sizePort << 3); - port += sizePort; - size -= sizePort; - } - - this.assert(!size); - return data; -}; - -/** - * removePortInputNotify(start, end) - * - * Remove port input-notification handler(s) (to be ENABLED later if needed) - * - * @this {Bus8080} - * @param {number} start address - * @param {number} end address - * -Bus8080.prototype.removePortInputNotify = function(start, end) -{ - for (var port = start; port < end; port++) { - if (this.aPortInputNotify[port]) { - delete this.aPortInputNotify[port]; - } - } -}; - */ - -/** - * addPortOutputBreak(port) - * - * @this {Bus8080} - * @param {number} [port] - * @return {boolean} true if break on port output enabled, false if disabled - */ -Bus8080.prototype.addPortOutputBreak = function(port) -{ - if (port === undefined) { - this.fPortOutputBreakAll = !this.fPortOutputBreakAll; - return this.fPortOutputBreakAll; - } - if (this.aPortOutputNotify[port] === undefined) { - this.aPortOutputNotify[port] = [null, false]; - } - this.aPortOutputNotify[port][1] = !this.aPortOutputNotify[port][1]; - return this.aPortOutputNotify[port][1]; -}; - -/** - * addPortOutputNotify(start, end, fn) - * - * Add a port output-notification handler to the list of such handlers. - * - * @this {Bus8080} - * @param {number} start port address - * @param {number} end port address - * @param {function(number,number)} fn is called with the port and IP values at the time of the output - */ -Bus8080.prototype.addPortOutputNotify = function(start, end, fn) -{ - if (fn !== undefined) { - for (var port = start; port <= end; port++) { - if (this.aPortOutputNotify[port] !== undefined) { - Component.warning("Output port " + str.toHexWord(port) + " already registered"); - continue; - } - this.aPortOutputNotify[port] = [fn, false]; - if (MAXDEBUG) this.log("addPortOutputNotify(" + str.toHexWord(port) + ")"); - } - } -}; - -/** - * addPortOutputTable(component, table, offset) - * - * Add port output-notification handlers from the specified table (a batch version of addPortOutputNotify) - * - * @this {Bus8080} - * @param {Component} component - * @param {Object} table - * @param {number} [offset] is an optional port offset - */ -Bus8080.prototype.addPortOutputTable = function(component, table, offset) -{ - if (offset === undefined) offset = 0; - if (table) { - for (var port in table) { - this.addPortOutputNotify(+port + offset, +port + offset, table[port].bind(component)); - } - } -}; - -/** - * addPortOutputWidth(port, size) - * - * By default, all output ports are 1 byte wide; ports that are wider must call this function. - * - * @this {Bus8080} - * @param {number} port - * @param {number} size (1, 2 or 4) - */ -Bus8080.prototype.addPortOutputWidth = function(port, size) -{ - this.aPortOutputWidth[port] = size; -}; - -/** - * checkPortOutputNotify(port, size, data, addrIP) - * - * @this {Bus8080} - * @param {number} port - * @param {number} size - * @param {number} data - * @param {number} [addrIP] is the IP value at the time of the output - */ -Bus8080.prototype.checkPortOutputNotify = function(port, size, data, addrIP) -{ - var shift = 0; - - while (size > 0) { - - var aNotify = this.aPortOutputNotify[port]; - var sizePort = this.aPortOutputWidth[port] || 1; - var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1)); - var dataPort = (data >>>= shift) & maskPort; - - /* - * TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O - * to a 32-bit port). We probably should pass the size through to the aNotify[0] handler, - * and let it decide what to do, but I don't feel like changing all the I/O handlers right now. - * The good news, at least, is that the 8-bit handlers would not have to do anything special. - * This assert will warn us if this is a pressing need. - */ - this.assert(size >= sizePort); - - if (aNotify !== undefined) { - if (aNotify[0]) { - aNotify[0](port, dataPort, addrIP); - } - if (DEBUGGER && this.dbg && this.fPortOutputBreakAll != aNotify[1]) { - this.dbg.checkPortOutput(port, size, dataPort); - } - } - else { - if (DEBUGGER && this.dbg) { - this.dbg.messageIO(this, port, dataPort, addrIP); - if (this.fPortOutputBreakAll) this.dbg.checkPortOutput(port, size, dataPort); - } - } - - shift += (sizePort << 3); - port += sizePort; - size -= sizePort; - } - this.assert(!size); -}; - -/** - * removePortOutputNotify(start, end) - * - * Remove port output-notification handler(s) (to be ENABLED later if needed) - * - * @this {Bus8080} - * @param {number} start address - * @param {number} end address - * -Bus8080.prototype.removePortOutputNotify = function(start, end) -{ - for (var port = start; port < end; port++) { - if (this.aPortOutputNotify[port]) { - delete this.aPortOutputNotify[port]; - } - } -}; - */ - -/** - * reportError(op, addr, size, fQuiet) - * - * @this {Bus8080} - * @param {number} op - * @param {number} addr - * @param {number} size - * @param {boolean} [fQuiet] (true if any error should be quietly logged) - * @return {boolean} false - */ -Bus8080.prototype.reportError = function(op, addr, size, fQuiet) -{ - var sError = "Memory block error (" + op + ": " + str.toHex(addr) + "," + str.toHex(size) + ")"; - if (fQuiet) { - if (this.dbg) { - this.dbg.message(sError); - } else { - this.log(sError); - } - } else { - Component.error(sError); - } - return false; -}; - -if (NODE) module.exports = Bus8080; +module.exports = Bus8080; diff --git a/modules/pc8080/lib/chipset.js b/modules/pc8080/lib/chipset.js index 169304612..c19a93fd8 100644 --- a/modules/pc8080/lib/chipset.js +++ b/modules/pc8080/lib/chipset.js @@ -28,75 +28,748 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var CPUDef8080 = require("./cpudef"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Usr = require("../../shared/es6/usrlib"); +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var CPUDef8080 = require("./cpudef"); +var Messages8080 = require("./messages"); /** - * ChipSet8080(parmsChipSet) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The ChipSet8080 component has the following component-specific (parmsChipSet) properties: - * - * model: eg, "SI1978" (should be a member of ChipSet8080.MODELS) - * swDIP: eg, "00000000", where swDIP[0] is DIP0, swDIP[1] is DIP1, etc. - * - * @constructor - * @extends Component - * @param {Object} parmsChipSet + * @unrestricted */ -function ChipSet8080(parmsChipSet) -{ - Component.call(this, "ChipSet", parmsChipSet, ChipSet8080, Messages8080.CHIPSET); - - var model = parmsChipSet['model']; - - if (model && !ChipSet8080.MODELS[model]) { - Component.notice("Unrecognized ChipSet model: " + model); - } - - this.config = ChipSet8080.MODELS[model] || {}; - - this.bSwitches = this.parseDIPSwitches(parmsChipSet['swDIP']); - - /* - * Here, I'm finally getting around to trying the Web Audio API. Fortunately, based on what little I know about - * sound generation, using the API to make the same noises as the IBM PC speaker seems straightforward. +class ChipSet8080 extends Component { + /** + * ChipSet8080(parmsChipSet) * - * To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false. + * The ChipSet8080 component has the following component-specific (parmsChipSet) properties: * - * From: + * model: eg, "SI1978" (should be a member of ChipSet8080.MODELS) + * swDIP: eg, "00000000", where swDIP[0] is DIP0, swDIP[1] is DIP1, etc. * - * http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html - * - * "Similar to how HTML5 canvas requires a context on which lines and curves are drawn, Web Audio requires an audio context - * on which sounds are played and manipulated. This context will be the parent object of further audio objects to come.... - * Your audio context is typically created when your page initializes and should be long-lived. You can play multiple sounds - * coming from multiple sources within the same context, so it is unnecessary to create more than one audio context per page." + * @this {ChipSet8080} + * @param {Object} parmsChipSet */ - this.fSpeaker = false; - if (parmsChipSet['sound']) { - this.classAudio = this.contextAudio = null; - if (window) { - this.classAudio = window['AudioContext'] || window['webkitAudioContext']; + constructor(parmsChipSet) + { + super("ChipSet", parmsChipSet, ChipSet8080, Messages8080.CHIPSET); + + var model = parmsChipSet['model']; + + if (model && !ChipSet8080.MODELS[model]) { + Component.notice("Unrecognized ChipSet model: " + model); } - if (this.classAudio) { - this.contextAudio = new this.classAudio(); - } else { - if (DEBUG) this.log("AudioContext not available"); + + this.config = ChipSet8080.MODELS[model] || {}; + + this.bSwitches = this.parseDIPSwitches(parmsChipSet['swDIP']); + + /* + * Here, I'm finally getting around to trying the Web Audio API. Fortunately, based on what little I know about + * sound generation, using the API to make the same noises as the IBM PC speaker seems straightforward. + * + * To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false. + * + * From: + * + * http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html + * + * "Similar to how HTML5 canvas requires a context on which lines and curves are drawn, Web Audio requires an audio context + * on which sounds are played and manipulated. This context will be the parent object of further audio objects to come.... + * Your audio context is typically created when your page initializes and should be long-lived. You can play multiple sounds + * coming from multiple sources within the same context, so it is unnecessary to create more than one audio context per page." + */ + this.fSpeaker = false; + if (parmsChipSet['sound']) { + this.classAudio = this.contextAudio = null; + if (window) { + this.classAudio = window['AudioContext'] || window['webkitAudioContext']; + } + if (this.classAudio) { + this.contextAudio = new this.classAudio(); + } else { + if (DEBUG) this.log("AudioContext not available"); + } + } + + this.setReady(); + } + + /** + * parseDIPSwitches(sBits, bDefault) + * + * @this {ChipSet8080} + * @param {string} sBits describing switch settings + * @param {number} [bDefault] + * @return {number|undefined} + */ + parseDIPSwitches(sBits, bDefault) + { + var b = bDefault; + if (sBits) { + /* + * NOTE: We can't use parseInt() with a base of 2, because both bit order and bit sense are reversed. + */ + b = 0; + var bit = 0x1; + for (var i = 0; i < sBits.length; i++) { + if (sBits.charAt(i) == "0") b |= bit; + bit <<= 1; + } + } + return b; + } + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {ChipSet8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "sw1") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + return false; + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {ChipSet8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.cmp = cmp; + this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard")); + this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort")); + this.video = /** @type {Video8080} */ (cmp.getMachineComponent("Video")); + bus.addPortInputTable(this, this.config.portsInput); + bus.addPortOutputTable(this, this.config.portsOutput); + + if (DEBUGGER) { + if (dbg) { + var chipset = this; + dbg.messageDump(Messages8080.NVR, function onDumpNVR() { + chipset.dumpNVR(); + }); + } } } - this.setReady(); -} + /** + * powerUp(data, fRepower) + * + * @this {ChipSet8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + if (!data) { + this.reset(); + } else { + if (!this.restore(data)) return false; + } + } + return true; + } -Component.subclass(ChipSet8080); + /** + * powerDown(fSave, fShutdown) + * + * @this {ChipSet8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * dumpNVR() + * + * @this {ChipSet8080} + */ + dumpNVR() + { + if (DEBUGGER) { + var sDump = ""; + for (var iWord = 0; iWord < this.aNVRWords.length; iWord++) { + if (sDump) { + sDump += (iWord && (iWord % 10)? ", " : ",\n"); + } + sDump += Str.toHexWord(this.aNVRWords[iWord]); + } + this.dbg.println(sDump); + } + } + + /** + * reset() + * + * @this {ChipSet8080} + */ + reset() + { + if (this.config.INIT && !this.restore(this.config.INIT)) { + this.notice("reset error"); + } + } + + /** + * save() + * + * This implements save support for the ChipSet component. + * + * @this {ChipSet8080} + * @return {Object} + */ + save() + { + var state = new State(this); + switch(this.config.MODEL) { + case ChipSet8080.SI1978.MODEL: + state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.wShiftData, this.bShiftCount, this.bSound1, this.bSound2]); + break; + case ChipSet8080.VT100.MODEL: + state.set(0, [this.bBrightness, this.bFlags]); + state.set(1, [this.bDC011Cols, this.bDC011Rate]); + state.set(2, [this.bDC012Scroll, this.bDC012Blink, this.bDC012Reverse, this.bDC012Attr]); + state.set(3, [this.dNVRAddr, this.wNVRData, this.bNVRLatch, this.bNVROut, this.aNVRWords]); + break; + } + return state.data(); + } + + /** + * restore(data) + * + * This implements restore support for the ChipSet component. + * + * @this {ChipSet8080} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + var a; + if (data && (a = data[0]) && a.length) { + switch(this.config.MODEL) { + case ChipSet8080.SI1978.MODEL: + this.bStatus0 = a[0]; + this.bStatus1 = a[1]; + this.bStatus2 = a[2]; + this.wShiftData = a[3]; + this.bShiftCount = a[4]; + this.bSound1 = a[5]; + this.bSound2 = a[6]; + return true; + case ChipSet8080.VT100.MODEL: + this.bBrightness = a[0]; + this.bFlags = a[1]; + a = data[1]; + this.bDC011Cols = a[0]; + this.bDC011Rate = a[1]; + a = data[2]; + this.bDC012Scroll = a[0]; + this.bDC012Blink = a[1]; + this.bDC012Reverse = a[2]; + this.bDC012Attr = a[3]; + a = data[3]; + this.dNVRAddr = a[0]; // 20-bit address + this.wNVRData = a[1]; // 14-bit word + this.bNVRLatch = a[2]; // 1 byte + this.bNVROut = a[3]; // 1 bit + this.aNVRWords = a[4]; // 100 14-bit words + return true; + } + } + return false; + } + + /** + * start() + * + * Notification from the CPU that it's starting. + * + * @this {ChipSet8080} + */ + start() + { + /* + * Currently, all we (may) do with this notification is allow the speaker to make noise. + */ + } + + /** + * stop() + * + * Notification from the CPU that it's stopping. + * + * @this {ChipSet8080} + */ + stop() + { + /* + * Currently, all we (may) do with this notification is prevent the speaker from making noise. + */ + } + + /** + * updateStatus0(bit, fSet) + * + * @this {ChipSet8080} + * @param {number} bit + * @param {boolean} fSet + */ + updateStatus0(bit, fSet) + { + this.bStatus0 &= ~bit; + if (fSet) this.bStatus0 |= bit; + } + + /** + * updateStatus1(bit, fSet) + * + * @this {ChipSet8080} + * @param {number} bit + * @param {boolean} fSet + */ + updateStatus1(bit, fSet) + { + this.bStatus1 &= ~bit; + if (fSet) this.bStatus1 |= bit; + } + + /** + * updateStatus2(bit, fSet) + * + * @this {ChipSet8080} + * @param {number} bit + * @param {boolean} fSet + */ + updateStatus2(bit, fSet) + { + this.bStatus2 &= ~bit; + if (fSet) this.bStatus2 |= bit; + } + + /** + * inSIStatus0(port, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x00) + * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) + * @return {number} simulated port value + */ + inSIStatus0(port, addrFrom) + { + var b = this.bStatus0; + this.printMessageIO(port, null, addrFrom, "STATUS0", b, true); + return b; + } + + /** + * inSIStatus1(port, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x01) + * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) + * @return {number} simulated port value + */ + inSIStatus1(port, addrFrom) + { + var b = this.bStatus1; + this.printMessageIO(port, null, addrFrom, "STATUS1", b, true); + return b; + } + + /** + * inSIStatus2(port, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x02) + * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) + * @return {number} simulated port value + */ + inSIStatus2(port, addrFrom) + { + var b = this.bStatus2; + this.printMessageIO(port, null, addrFrom, "STATUS2", b, true); + return b; + } + + /** + * inSIShiftResult(port, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x03) + * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) + * @return {number} simulated port value + */ + inSIShiftResult(port, addrFrom) + { + var b = (this.wShiftData >> (8 - this.bShiftCount)) & 0xff; + this.printMessageIO(port, null, addrFrom, "SHIFT.RESULT", b, true); + return b; + } + + /** + * outSIShiftCount(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x02) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outSIShiftCount(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "SHIFT.COUNT", null, true); + this.bShiftCount = b; + } + + /** + * outSISound1(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x03) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outSISound1(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "SOUND1", null, true); + this.bSound1 = b; + } + + /** + * outSIShiftData(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x04) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outSIShiftData(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "SHIFT.DATA", null, true); + this.wShiftData = (b << 8) | (this.wShiftData >> 8); + } + + /** + * outSISound2(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x05) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outSISound2(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "SOUND2", null, true); + this.bSound2 = b; + } + + /** + * outSIWatchdog(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x06) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outSIWatchdog(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "WATCHDOG", null, true); + } + + /** + * getVT100LBA(iBit) + * + * Returns the state of the requested (simulated) LBA bit. + * + * NOTE: This is currently only used to obtain LBA7, which we approximate with the slightly faster approach + * of masking bit 6 of the CPU cycle count (see the DC011 discussion above). This will result in a shorter LBA7 + * period than if we divided the cycle count by 88, but a shorter LBA7 period is probably helpful in terms of + * overall performance. + * + * @param {number} iBit + * @return {number} + */ + getVT100LBA(iBit) + { + return (this.cpu.getCycles() & (1 << (iBit - 1))) << 1; + } + + /** + * getNVRAddr() + * + * @return {number} + */ + getNVRAddr() + { + var i; + var tens = 0, ones = 0; + var addr = ~this.dNVRAddr; + for (i = 0; i < 10; i++) { + if (addr & 0x1) tens = 9-i; + addr >>= 1; + } + for (i = 0; i < 10; i++) { + if (addr & 0x1) ones = 9-i; + addr >>= 1; + } + addr = tens*10 + ones; + this.assert(addr >= 0 && addr < this.aNVRWords.length); + return addr; + } + + /** + * doNVRCommand() + */ + doNVRCommand() + { + var addr, data; + var bit = this.bNVRLatch & 0x1; + var bCmd = (this.bNVRLatch >> 1) & 0x7; + + switch(bCmd) { + case ChipSet8080.VT100.NVR.CMD.STANDBY: + break; + + case ChipSet8080.VT100.NVR.CMD.ACCEPT_ADDR: + this.dNVRAddr = (this.dNVRAddr << 1) | bit; + break; + + case ChipSet8080.VT100.NVR.CMD.ERASE: + addr = this.getNVRAddr(); + this.aNVRWords[addr] = ChipSet8080.VT100.NVR.WORDMASK; + this.printMessage("doNVRCommand(): erase data at addr " + Str.toHexWord(addr)); + break; + + case ChipSet8080.VT100.NVR.CMD.ACCEPT_DATA: + this.wNVRData = (this.wNVRData << 1) | bit; + break; + + case ChipSet8080.VT100.NVR.CMD.WRITE: + addr = this.getNVRAddr(); + data = this.wNVRData & ChipSet8080.VT100.NVR.WORDMASK; + this.aNVRWords[addr] = data; + this.printMessage("doNVRCommand(): write data " + Str.toHexWord(data) + " to addr " + Str.toHexWord(addr)); + break; + + case ChipSet8080.VT100.NVR.CMD.READ: + addr = this.getNVRAddr(); + data = this.aNVRWords[addr]; + /* + * If we don't explicitly initialize aNVRWords[], pretend any uninitialized words contains WORDMASK. + */ + if (data == null) data = ChipSet8080.VT100.NVR.WORDMASK; + this.wNVRData = data; + this.printMessage("doNVRCommand(): read data " + Str.toHexWord(data) + " from addr " + Str.toHexWord(addr)); + break; + + case ChipSet8080.VT100.NVR.CMD.SHIFT_OUT: + this.wNVRData <<= 1; + /* + * Since WORDMASK is 0x3fff, this will mask the shifted data with 0x4000, which is the bit we want to isolate. + */ + this.bNVROut = this.wNVRData & (ChipSet8080.VT100.NVR.WORDMASK + 1); + break; + + default: + this.printMessage("doNVRCommand(): unrecognized command " + Str.toHexByte(bCmd)); + break; + } + } + + /** + * inVT100Flags(port, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x42) + * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) + * @return {number} simulated port value + */ + inVT100Flags(port, addrFrom) + { + var b = this.bFlags; + + /* + * The NVR_CLK bit is driven by LBA7 (ie, bit 7 from Line Buffer Address generation); see the DC011 discussion above. + */ + b &= ~ChipSet8080.VT100.FLAGS.NVR_CLK; + if (this.getVT100LBA(7)) { + b |= ChipSet8080.VT100.FLAGS.NVR_CLK; + if (b != this.bFlags) { + this.doNVRCommand(); + } + } + + b &= ~ChipSet8080.VT100.FLAGS.NVR_DATA; + if (this.bNVROut) { + b |= ChipSet8080.VT100.FLAGS.NVR_DATA; + } + + b &= ~ChipSet8080.VT100.FLAGS.KBD_XMIT; + if (this.kbd && this.kbd.isVT100TransmitterReady()) { + b |= ChipSet8080.VT100.FLAGS.KBD_XMIT; + } + + b &= ~ChipSet8080.VT100.FLAGS.UART_XMIT; + if (this.serial && this.serial.isTransmitterReady()) { + b |= ChipSet8080.VT100.FLAGS.UART_XMIT; + } + + this.bFlags = b; + this.printMessageIO(port, null, addrFrom, "FLAGS", b); + return b; + } + + /** + * outVT100Brightness(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x42) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outVT100Brightness(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "BRIGHTNESS"); + this.bBrightness = b; + } + + /** + * outVT100NVRLatch(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0x62) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outVT100NVRLatch(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "NVR.LATCH"); + this.bNVRLatch = b; + } + + /** + * outVT100DC012(port, b, addrFrom) + * + * TODO: Consider whether we should disable any interrupts (eg, vertical retrace) until + * this port is initialized at runtime. + * + * @this {ChipSet8080} + * @param {number} port (0xA2) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outVT100DC012(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "DC012"); + + var bOpt = b & 0x3; + var bCmd = (b >> 2) & 0x3; + switch(bCmd) { + case 0x0: + this.bDC012Scroll = (this.bDC012Scroll & ~0x3) | bOpt; + break; + case 0x1: + this.bDC012Scroll = (this.bDC012Scroll & ~0xC) | (bOpt << 2); + if (this.video) this.video.updateScrollOffset(this.bDC012Scroll); + break; + case 0x2: + switch(bOpt) { + case 0x0: + this.bDC012Blink = ~this.bDC012Blink; + break; + case 0x1: + // TODO: Clear vertical frequency interrupt? + break; + case 0x2: + case 0x3: + this.bDC012Reverse = 0x3 - bOpt; + break; + } + break; + case 0x3: + this.bDC012Attr = bOpt; + break; + } + } + + /** + * outVT100DC011(port, b, addrFrom) + * + * @this {ChipSet8080} + * @param {number} port (0xC2) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outVT100DC011(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "DC011"); + if (b & ChipSet8080.VT100.DC011.RATE60) { + b &= ChipSet8080.VT100.DC011.RATE50; + if (this.bDC011Rate != b) { + this.bDC011Rate = b; + if (this.video) { + this.video.updateRate(this.bDC011Rate == ChipSet8080.VT100.DC011.RATE50? 50 : 60); + } + } + } else { + b &= ChipSet8080.VT100.DC011.COLS132; + if (this.bDC011Cols != b) { + this.bDC011Cols = b; + if (this.video) { + var nCols = (this.bDC011Cols == ChipSet8080.VT100.DC011.COLS132? 132 : 80); + var nRows = (nCols > 80 && (this.bFlags & ChipSet8080.VT100.FLAGS.NO_AVO)? 14 : 24); + this.video.updateDimensions(nCols, nRows); + } + } + } + } + + /** + * ChipSet8080.init() + * + * This function operates on every HTML element of class "chipset", extracting the + * JSON-encoded parameters for the ChipSet constructor from the element's "data-value" + * attribute, invoking the constructor to create a ChipSet component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeChipSet = Component.getElementsByClass(document, PC8080.APPCLASS, "chipset"); + for (var iChip = 0; iChip < aeChipSet.length; iChip++) { + var eChipSet = aeChipSet[iChip]; + var parmsChipSet = Component.getComponentParms(eChipSet); + var chipset = new ChipSet8080(parmsChipSet); + Component.bindComponentControls(chipset, eChipSet, PC8080.APPCLASS); + } + } +} /* * NOTE: The STATUS1 port could have been handled entirely by the Keyboard component, but it was just as easy @@ -351,110 +1024,6 @@ ChipSet8080.MODELS = { "VT100": ChipSet8080.VT100 }; -/** - * parseDIPSwitches(sBits, bDefault) - * - * @this {ChipSet8080} - * @param {string} sBits describing switch settings - * @param {number} [bDefault] - * @return {number|undefined} - */ -ChipSet8080.prototype.parseDIPSwitches = function(sBits, bDefault) -{ - var b = bDefault; - if (sBits) { - /* - * NOTE: We can't use parseInt() with a base of 2, because both bit order and bit sense are reversed. - */ - b = 0; - var bit = 0x1; - for (var i = 0; i < sBits.length; i++) { - if (sBits.charAt(i) == "0") b |= bit; - bit <<= 1; - } - } - return b; -}; - -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {ChipSet8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "sw1") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -ChipSet8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - return false; -}; - -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {ChipSet8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ -ChipSet8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.bus = bus; - this.cpu = cpu; - this.dbg = dbg; - this.cmp = cmp; - this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard")); - this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort")); - this.video = /** @type {Video8080} */ (cmp.getMachineComponent("Video")); - bus.addPortInputTable(this, this.config.portsInput); - bus.addPortOutputTable(this, this.config.portsOutput); - - if (DEBUGGER) { - if (dbg) { - var chipset = this; - dbg.messageDump(Messages8080.NVR, function onDumpNVR() { - chipset.dumpNVR(); - }); - } - } -}; - -/** - * powerUp(data, fRepower) - * - * @this {ChipSet8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -ChipSet8080.prototype.powerUp = function(data, fRepower) -{ - if (!fRepower) { - if (!data) { - this.reset(); - } else { - if (!this.restore(data)) return false; - } - } - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * @this {ChipSet8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -ChipSet8080.prototype.powerDown = function(fSave, fShutdown) -{ - return fSave? this.save() : true; -}; - ChipSet8080.SI1978.INIT = [ [ ChipSet8080.SI1978.STATUS0.ALWAYS_SET, @@ -532,552 +1101,6 @@ ChipSet8080.VT100.INIT = [ ] ]; -/** - * dumpNVR() - * - * @this {ChipSet8080} - */ -ChipSet8080.prototype.dumpNVR = function() -{ - if (DEBUGGER) { - var sDump = ""; - for (var iWord = 0; iWord < this.aNVRWords.length; iWord++) { - if (sDump) { - sDump += (iWord && (iWord % 10)? ", " : ",\n"); - } - sDump += str.toHexWord(this.aNVRWords[iWord]); - } - this.dbg.println(sDump); - } -}; - -/** - * reset() - * - * @this {ChipSet8080} - */ -ChipSet8080.prototype.reset = function() -{ - if (this.config.INIT && !this.restore(this.config.INIT)) { - this.notice("reset error"); - } -}; - -/** - * save() - * - * This implements save support for the ChipSet component. - * - * @this {ChipSet8080} - * @return {Object} - */ -ChipSet8080.prototype.save = function() -{ - var state = new State(this); - switch(this.config.MODEL) { - case ChipSet8080.SI1978.MODEL: - state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.wShiftData, this.bShiftCount, this.bSound1, this.bSound2]); - break; - case ChipSet8080.VT100.MODEL: - state.set(0, [this.bBrightness, this.bFlags]); - state.set(1, [this.bDC011Cols, this.bDC011Rate]); - state.set(2, [this.bDC012Scroll, this.bDC012Blink, this.bDC012Reverse, this.bDC012Attr]); - state.set(3, [this.dNVRAddr, this.wNVRData, this.bNVRLatch, this.bNVROut, this.aNVRWords]); - break; - } - return state.data(); -}; - -/** - * restore(data) - * - * This implements restore support for the ChipSet component. - * - * @this {ChipSet8080} - * @param {Object} data - * @return {boolean} true if successful, false if failure - */ -ChipSet8080.prototype.restore = function(data) -{ - var a; - if (data && (a = data[0]) && a.length) { - switch(this.config.MODEL) { - case ChipSet8080.SI1978.MODEL: - this.bStatus0 = a[0]; - this.bStatus1 = a[1]; - this.bStatus2 = a[2]; - this.wShiftData = a[3]; - this.bShiftCount = a[4]; - this.bSound1 = a[5]; - this.bSound2 = a[6]; - return true; - case ChipSet8080.VT100.MODEL: - this.bBrightness = a[0]; - this.bFlags = a[1]; - a = data[1]; - this.bDC011Cols = a[0]; - this.bDC011Rate = a[1]; - a = data[2]; - this.bDC012Scroll = a[0]; - this.bDC012Blink = a[1]; - this.bDC012Reverse = a[2]; - this.bDC012Attr = a[3]; - a = data[3]; - this.dNVRAddr = a[0]; // 20-bit address - this.wNVRData = a[1]; // 14-bit word - this.bNVRLatch = a[2]; // 1 byte - this.bNVROut = a[3]; // 1 bit - this.aNVRWords = a[4]; // 100 14-bit words - return true; - } - } - return false; -}; - -/** - * start() - * - * Notification from the CPU that it's starting. - * - * @this {ChipSet8080} - */ -ChipSet8080.prototype.start = function() -{ - /* - * Currently, all we (may) do with this notification is allow the speaker to make noise. - */ -}; - -/** - * stop() - * - * Notification from the CPU that it's stopping. - * - * @this {ChipSet8080} - */ -ChipSet8080.prototype.stop = function() -{ - /* - * Currently, all we (may) do with this notification is prevent the speaker from making noise. - */ -}; - -/** - * updateStatus0(bit, fSet) - * - * @this {ChipSet8080} - * @param {number} bit - * @param {boolean} fSet - */ -ChipSet8080.prototype.updateStatus0 = function(bit, fSet) -{ - this.bStatus0 &= ~bit; - if (fSet) this.bStatus0 |= bit; -}; - -/** - * updateStatus1(bit, fSet) - * - * @this {ChipSet8080} - * @param {number} bit - * @param {boolean} fSet - */ -ChipSet8080.prototype.updateStatus1 = function(bit, fSet) -{ - this.bStatus1 &= ~bit; - if (fSet) this.bStatus1 |= bit; -}; - -/** - * updateStatus2(bit, fSet) - * - * @this {ChipSet8080} - * @param {number} bit - * @param {boolean} fSet - */ -ChipSet8080.prototype.updateStatus2 = function(bit, fSet) -{ - this.bStatus2 &= ~bit; - if (fSet) this.bStatus2 |= bit; -}; - -/** - * inSIStatus0(port, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x00) - * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) - * @return {number} simulated port value - */ -ChipSet8080.prototype.inSIStatus0 = function(port, addrFrom) -{ - var b = this.bStatus0; - this.printMessageIO(port, null, addrFrom, "STATUS0", b, true); - return b; -}; - -/** - * inSIStatus1(port, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x01) - * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) - * @return {number} simulated port value - */ -ChipSet8080.prototype.inSIStatus1 = function(port, addrFrom) -{ - var b = this.bStatus1; - this.printMessageIO(port, null, addrFrom, "STATUS1", b, true); - return b; -}; - -/** - * inSIStatus2(port, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x02) - * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) - * @return {number} simulated port value - */ -ChipSet8080.prototype.inSIStatus2 = function(port, addrFrom) -{ - var b = this.bStatus2; - this.printMessageIO(port, null, addrFrom, "STATUS2", b, true); - return b; -}; - -/** - * inSIShiftResult(port, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x03) - * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) - * @return {number} simulated port value - */ -ChipSet8080.prototype.inSIShiftResult = function(port, addrFrom) -{ - var b = (this.wShiftData >> (8 - this.bShiftCount)) & 0xff; - this.printMessageIO(port, null, addrFrom, "SHIFT.RESULT", b, true); - return b; -}; - -/** - * outSIShiftCount(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x02) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outSIShiftCount = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "SHIFT.COUNT", null, true); - this.bShiftCount = b; -}; - -/** - * outSISound1(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x03) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outSISound1 = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "SOUND1", null, true); - this.bSound1 = b; -}; - -/** - * outSIShiftData(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x04) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outSIShiftData = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "SHIFT.DATA", null, true); - this.wShiftData = (b << 8) | (this.wShiftData >> 8); -}; - -/** - * outSISound2(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x05) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outSISound2 = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "SOUND2", null, true); - this.bSound2 = b; -}; - -/** - * outSIWatchdog(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x06) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outSIWatchdog = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "WATCHDOG", null, true); -}; - -/** - * getVT100LBA(iBit) - * - * Returns the state of the requested (simulated) LBA bit. - * - * NOTE: This is currently only used to obtain LBA7, which we approximate with the slightly faster approach - * of masking bit 6 of the CPU cycle count (see the DC011 discussion above). This will result in a shorter LBA7 - * period than if we divided the cycle count by 88, but a shorter LBA7 period is probably helpful in terms of - * overall performance. - * - * @param {number} iBit - * @return {number} - */ -ChipSet8080.prototype.getVT100LBA = function(iBit) -{ - return (this.cpu.getCycles() & (1 << (iBit - 1))) << 1; -}; - -/** - * getNVRAddr() - * - * @return {number} - */ -ChipSet8080.prototype.getNVRAddr = function() -{ - var i; - var tens = 0, ones = 0; - var addr = ~this.dNVRAddr; - for (i = 0; i < 10; i++) { - if (addr & 0x1) tens = 9-i; - addr >>= 1; - } - for (i = 0; i < 10; i++) { - if (addr & 0x1) ones = 9-i; - addr >>= 1; - } - addr = tens*10 + ones; - this.assert(addr >= 0 && addr < this.aNVRWords.length); - return addr; -}; - -/** - * doNVRCommand() - */ -ChipSet8080.prototype.doNVRCommand = function() -{ - var addr, data; - var bit = this.bNVRLatch & 0x1; - var bCmd = (this.bNVRLatch >> 1) & 0x7; - - switch(bCmd) { - case ChipSet8080.VT100.NVR.CMD.STANDBY: - break; - - case ChipSet8080.VT100.NVR.CMD.ACCEPT_ADDR: - this.dNVRAddr = (this.dNVRAddr << 1) | bit; - break; - - case ChipSet8080.VT100.NVR.CMD.ERASE: - addr = this.getNVRAddr(); - this.aNVRWords[addr] = ChipSet8080.VT100.NVR.WORDMASK; - this.printMessage("doNVRCommand(): erase data at addr " + str.toHexWord(addr)); - break; - - case ChipSet8080.VT100.NVR.CMD.ACCEPT_DATA: - this.wNVRData = (this.wNVRData << 1) | bit; - break; - - case ChipSet8080.VT100.NVR.CMD.WRITE: - addr = this.getNVRAddr(); - data = this.wNVRData & ChipSet8080.VT100.NVR.WORDMASK; - this.aNVRWords[addr] = data; - this.printMessage("doNVRCommand(): write data " + str.toHexWord(data) + " to addr " + str.toHexWord(addr)); - break; - - case ChipSet8080.VT100.NVR.CMD.READ: - addr = this.getNVRAddr(); - data = this.aNVRWords[addr]; - /* - * If we don't explicitly initialize aNVRWords[], pretend any uninitialized words contains WORDMASK. - */ - if (data == null) data = ChipSet8080.VT100.NVR.WORDMASK; - this.wNVRData = data; - this.printMessage("doNVRCommand(): read data " + str.toHexWord(data) + " from addr " + str.toHexWord(addr)); - break; - - case ChipSet8080.VT100.NVR.CMD.SHIFT_OUT: - this.wNVRData <<= 1; - /* - * Since WORDMASK is 0x3fff, this will mask the shifted data with 0x4000, which is the bit we want to isolate. - */ - this.bNVROut = this.wNVRData & (ChipSet8080.VT100.NVR.WORDMASK + 1); - break; - - default: - this.printMessage("doNVRCommand(): unrecognized command " + str.toHexByte(bCmd)); - break; - } -}; - -/** - * inVT100Flags(port, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x42) - * @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port) - * @return {number} simulated port value - */ -ChipSet8080.prototype.inVT100Flags = function(port, addrFrom) -{ - var b = this.bFlags; - - /* - * The NVR_CLK bit is driven by LBA7 (ie, bit 7 from Line Buffer Address generation); see the DC011 discussion above. - */ - b &= ~ChipSet8080.VT100.FLAGS.NVR_CLK; - if (this.getVT100LBA(7)) { - b |= ChipSet8080.VT100.FLAGS.NVR_CLK; - if (b != this.bFlags) { - this.doNVRCommand(); - } - } - - b &= ~ChipSet8080.VT100.FLAGS.NVR_DATA; - if (this.bNVROut) { - b |= ChipSet8080.VT100.FLAGS.NVR_DATA; - } - - b &= ~ChipSet8080.VT100.FLAGS.KBD_XMIT; - if (this.kbd && this.kbd.isVT100TransmitterReady()) { - b |= ChipSet8080.VT100.FLAGS.KBD_XMIT; - } - - b &= ~ChipSet8080.VT100.FLAGS.UART_XMIT; - if (this.serial && this.serial.isTransmitterReady()) { - b |= ChipSet8080.VT100.FLAGS.UART_XMIT; - } - - this.bFlags = b; - this.printMessageIO(port, null, addrFrom, "FLAGS", b); - return b; -}; - -/** - * outVT100Brightness(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x42) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outVT100Brightness = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "BRIGHTNESS"); - this.bBrightness = b; -}; - -/** - * outVT100NVRLatch(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0x62) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outVT100NVRLatch = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "NVR.LATCH"); - this.bNVRLatch = b; -}; - -/** - * outVT100DC012(port, b, addrFrom) - * - * TODO: Consider whether we should disable any interrupts (eg, vertical retrace) until - * this port is initialized at runtime. - * - * @this {ChipSet8080} - * @param {number} port (0xA2) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outVT100DC012 = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "DC012"); - - var bOpt = b & 0x3; - var bCmd = (b >> 2) & 0x3; - switch(bCmd) { - case 0x0: - this.bDC012Scroll = (this.bDC012Scroll & ~0x3) | bOpt; - break; - case 0x1: - this.bDC012Scroll = (this.bDC012Scroll & ~0xC) | (bOpt << 2); - if (this.video) this.video.updateScrollOffset(this.bDC012Scroll); - break; - case 0x2: - switch(bOpt) { - case 0x0: - this.bDC012Blink = ~this.bDC012Blink; - break; - case 0x1: - // TODO: Clear vertical frequency interrupt? - break; - case 0x2: - case 0x3: - this.bDC012Reverse = 0x3 - bOpt; - break; - } - break; - case 0x3: - this.bDC012Attr = bOpt; - break; - } -}; - -/** - * outVT100DC011(port, b, addrFrom) - * - * @this {ChipSet8080} - * @param {number} port (0xC2) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -ChipSet8080.prototype.outVT100DC011 = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "DC011"); - if (b & ChipSet8080.VT100.DC011.RATE60) { - b &= ChipSet8080.VT100.DC011.RATE50; - if (this.bDC011Rate != b) { - this.bDC011Rate = b; - if (this.video) { - this.video.updateRate(this.bDC011Rate == ChipSet8080.VT100.DC011.RATE50? 50 : 60); - } - } - } else { - b &= ChipSet8080.VT100.DC011.COLS132; - if (this.bDC011Cols != b) { - this.bDC011Cols = b; - if (this.video) { - var nCols = (this.bDC011Cols == ChipSet8080.VT100.DC011.COLS132? 132 : 80); - var nRows = (nCols > 80 && (this.bFlags & ChipSet8080.VT100.FLAGS.NO_AVO)? 14 : 24); - this.video.updateDimensions(nCols, nRows); - } - } - } -}; - /* * Port notification tables */ @@ -1107,28 +1130,9 @@ ChipSet8080.VT100.portsOutput = { 0xC2: ChipSet8080.prototype.outVT100DC011 }; -/** - * ChipSet8080.init() - * - * This function operates on every HTML element of class "chipset", extracting the - * JSON-encoded parameters for the ChipSet constructor from the element's "data-value" - * attribute, invoking the constructor to create a ChipSet component, and then binding - * any associated HTML controls to the new component. - */ -ChipSet8080.init = function() -{ - var aeChipSet = Component.getElementsByClass(document, PC8080.APPCLASS, "chipset"); - for (var iChip = 0; iChip < aeChipSet.length; iChip++) { - var eChipSet = aeChipSet[iChip]; - var parmsChipSet = Component.getComponentParms(eChipSet); - var chipset = new ChipSet8080(parmsChipSet); - Component.bindComponentControls(chipset, eChipSet, PC8080.APPCLASS); - } -}; - /* * Initialize every ChipSet module on the page. */ -web.onInit(ChipSet8080.init); +Web.onInit(ChipSet8080.init); -if (NODE) module.exports = ChipSet8080; +module.exports = ChipSet8080; diff --git a/modules/pc8080/lib/computer.js b/modules/pc8080/lib/computer.js index 2dda85af3..36d88491c 100644 --- a/modules/pc8080/lib/computer.js +++ b/modules/pc8080/lib/computer.js @@ -28,252 +28,1592 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var web = require("../../shared/lib/weblib"); - var UserAPI = require("../../shared/lib/userapi"); - var ReportAPI = require("../../shared/lib/reportapi"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var Bus8080 = require("./bus"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Usr = require("../../shared/es6/usrlib"); +var Web = require("../../shared/es6/weblib"); +var UserAPI = require("../../shared/es6/userapi"); +var ReportAPI = require("../../shared/es6/reportapi"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var Bus8080 = require("./bus"); +var Messages8080 = require("./messages"); /** - * Computer8080(parmsComputer, parmsMachine, fSuspended) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * @constructor - * @extends Component - * @param {Object} parmsComputer - * @param {Object} [parmsMachine] - * @param {boolean} [fSuspended] - * - * The Computer8080 component has no required (parmsComputer) properties, but it does - * support the following: - * - * autoPower: true to automatically power the computer (default), false to wait; - * false is honored only if a "power" button binding exists. - * - * busWidth: number of memory address lines (address bits) on the computer's "bus"; - * 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing, - * while 24 is required for 80286 protected-mode addressing. This value is passed - * directly through to the Bus component; see that component for more details. - * - * resume: one of the Computer8080.RESUME constants, which are as follows: - * '0' if resume disabled (default) - * '1' if enabled without prompting - * '2' if enabled with prompting - * '3' if enabled with prompting and auto-delete - * or a string containing the path of a predefined JSON-encoded state - * - * state: the path to JSON-encoded state file (see details regarding 'state' below) - * - * The parmsMachine object, if provided, may contain any of: - * - * autoMount: if set, this should override any 'autoMount' property in the FDC's - * parmsFDC object. - * - * autoPower: if set, this should override any 'autoPower' property in the Computer8080's - * parmsComputer object. - * - * messages: if set, this should override any 'messages' property in the Debugger's - * parmsDbg object. - * - * state: if set, this should override any 'state' property in the Computer8080's - * parmsComputer object. - * - * url: the location of the machine XML file - * - * If a predefined state is supplied AND it's successfully loaded, then resume behavior - * defaults to '1' (ie, resume enabled without prompting). - * - * This component insures that all components are ready before "powering" them. - * - * Different components become ready at different times, and initialization order (ie, - * the order the scripts are combined on the page) only partially determines readiness. - * This is because components like ROM and Video must finish loading their resource files - * before they are ready. Other components become ready after we call their initBus() - * function, because they have a Bus or CPU dependency, such as access to memory management - * functions. And other components, like CPU and Panel, are ready as soon as their - * constructor finishes. - * - * Once a component has indicated it's ready, we call its powerUp() notification - * function (if it has one--it's optional). We call the CPU's powerUp() function last, - * so that the CPU is assured that all other components are ready and "powered". + * @unrestricted */ -function Computer8080(parmsComputer, parmsMachine, fSuspended) { - - Component.call(this, "Computer", parmsComputer, Computer8080, Messages8080.COMPUTER); - - this.flags.powered = false; - - this.setMachineParms(parmsMachine); - - this.fAutoPower = this.getMachineParm('autoPower', parmsComputer); - - /* - * nPowerChange is 0 while the power state is stable, 1 while power is transitioning - * to "on", and -1 while power is transitioning to "off". - */ - this.nPowerChange = 0; - - /* - * TODO: Deprecate 'buswidth' (it should have always used camelCase) - */ - this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth']; - - this.resume = Computer8080.RESUME_NONE; - this.sStateData = null; - this.fStateData = false; // remembers if sStateData was loaded - this.fServerState = false; - - this.url = this.getMachineParm('url') || ""; - - /* - * Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be - * non-zero, convert to base 36, and chop off the leading digit and "decimal" point. - */ - this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12); - this.sUserID = this.queryUserID(); - - /* - * Find the appropriate CPU (and Debugger and Control Panel, if any) +class Computer8080 extends Component { + /** + * Computer8080(parmsComputer, parmsMachine, fSuspended) * - * CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here, - * where we know getComponentByType() will only return an CPUState object or null), wrap the expression - * in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide". + * The Computer8080 component has no required (parmsComputer) properties, but it does + * support the following: + * + * autoPower: true to automatically power the computer (default), false to wait; + * false is honored only if a "power" button binding exists. + * + * busWidth: number of memory address lines (address bits) on the computer's "bus"; + * 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing, + * while 24 is required for 80286 protected-mode addressing. This value is passed + * directly through to the Bus component; see that component for more details. + * + * resume: one of the Computer8080.RESUME constants, which are as follows: + * '0' if resume disabled (default) + * '1' if enabled without prompting + * '2' if enabled with prompting + * '3' if enabled with prompting and auto-delete + * or a string containing the path of a predefined JSON-encoded state + * + * state: the path to JSON-encoded state file (see details regarding 'state' below) + * + * The parmsMachine object, if provided, may contain any of: + * + * autoMount: if set, this should override any 'autoMount' property in the FDC's + * parmsFDC object. + * + * autoPower: if set, this should override any 'autoPower' property in the Computer8080's + * parmsComputer object. + * + * messages: if set, this should override any 'messages' property in the Debugger's + * parmsDbg object. + * + * state: if set, this should override any 'state' property in the Computer8080's + * parmsComputer object. + * + * url: the location of the machine XML file + * + * If a predefined state is supplied AND it's successfully loaded, then resume behavior + * defaults to '1' (ie, resume enabled without prompting). + * + * This component insures that all components are ready before "powering" them. + * + * Different components become ready at different times, and initialization order (ie, + * the order the scripts are combined on the page) only partially determines readiness. + * This is because components like ROM and Video must finish loading their resource files + * before they are ready. Other components become ready after we call their initBus() + * function, because they have a Bus or CPU dependency, such as access to memory management + * functions. And other components, like CPU and Panel, are ready as soon as their + * constructor finishes. + * + * Once a component has indicated it's ready, we call its powerUp() notification + * function (if it has one--it's optional). We call the CPU's powerUp() function last, + * so that the CPU is assured that all other components are ready and "powered". + * + * @this {Computer8080} + * @param {Object} parmsComputer + * @param {Object} [parmsMachine] + * @param {boolean} [fSuspended] */ - this.cpu = /** @type {CPUState8080} */ (Component.getComponentByType("CPU", this.id)); - if (!this.cpu) { - Component.error("Unable to find CPU component"); - return; - } - this.dbg = /** @type {Debugger8080} */ (Component.getComponentByType("Debugger", this.id)); + constructor(parmsComputer, parmsMachine, fSuspended) + { + super("Computer", parmsComputer, Computer8080, Messages8080.COMPUTER); - /* - * Enumerate all Video components for future updateVideo() calls. - */ - this.aVideo = []; - for (var video = null; (video = this.getMachineComponent("Video", video));) { - this.aVideo.push(video); - } + this.flags.powered = false; - /* - * Initialize the Bus component - */ - this.bus = new Bus8080({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg); + this.setMachineParms(parmsMachine); - /* - * Iterate through all the components and connect them to the Control Panel, if any - */ - var iComponent, component; - var aComponents = Component.getComponents(this.id); - this.panel = /** @type {Panel8080} */ (Component.getComponentByType("Panel", this.id)); + this.fAutoPower = this.getMachineParm('autoPower', parmsComputer); - if (this.panel && this.panel.controlPrint) { + /* + * nPowerChange is 0 while the power state is stable, 1 while power is transitioning + * to "on", and -1 while power is transitioning to "off". + */ + this.nPowerChange = 0; + + /* + * TODO: Deprecate 'buswidth' (it should have always used camelCase) + */ + this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth']; + + this.resume = Computer8080.RESUME_NONE; + this.sStateData = null; + this.fStateData = false; // remembers if sStateData was loaded + this.fServerState = false; + + this.url = this.getMachineParm('url') || ""; + + /* + * Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be + * non-zero, convert to base 36, and chop off the leading digit and "decimal" point. + */ + this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12); + this.sUserID = this.queryUserID(); + + /* + * Find the appropriate CPU (and Debugger and Control Panel, if any) + * + * CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here, + * where we know getComponentByType() will only return an CPUState object or null), wrap the expression + * in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide". + */ + this.cpu = /** @type {CPUState8080} */ (Component.getComponentByType("CPU", this.id)); + if (!this.cpu) { + Component.error("Unable to find CPU component"); + return; + } + this.dbg = /** @type {Debugger8080} */ (Component.getComponentByType("Debugger", this.id)); + + /* + * Enumerate all Video components for future updateVideo() calls. + */ + this.aVideo = []; + for (var video = null; (video = this.getMachineComponent("Video", video));) { + this.aVideo.push(video); + } + + /* + * Initialize the Bus component + */ + this.bus = new Bus8080({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg); + + /* + * Iterate through all the components and connect them to the Control Panel, if any + */ + var iComponent, component; + var aComponents = Component.getComponents(this.id); + this.panel = /** @type {Panel8080} */ (Component.getComponentByType("Panel", this.id)); + + if (this.panel && this.panel.controlPrint) { + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + /* + * I can think of many "cleaner" ways for the Control Panel component to pass its + * notice(), println(), etc, overrides on to all the other components, but it's just + * too darn convenient to slam those overrides into the components directly. + */ + component.notice = this.panel.notice; + component.println = this.panel.println; + component.controlPrint = this.panel.controlPrint; + } + } + + this.println(PC8080.APPNAME + " v" + (XMLVERSION || PC8080.APPVERSION) + "\n" + COPYRIGHT + "\n" + LICENSE); + + if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS); + + /* + * Iterate through all the components again and call their initBus() handler, if any + */ for (iComponent = 0; iComponent < aComponents.length; iComponent++) { component = aComponents[iComponent]; + if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg); + } + + var sStatePath = null; + var sResume = parmsComputer['resume']; + if (sResume !== undefined) { /* - * I can think of many "cleaner" ways for the Control Panel component to pass its - * notice(), println(), etc, overrides on to all the other components, but it's just - * too darn convenient to slam those overrides into the components directly. + * DEPRECATE: This goofiness is a holdover from when the 'resume' property was a string (either a + * single-digit string or a path); now it's always a number, so it never has a 'length' property and + * the call to parseInt() is unnecessary. */ - component.notice = this.panel.notice; - component.println = this.panel.println; - component.controlPrint = this.panel.controlPrint; - } - } - - this.println(PC8080.APPNAME + " v" + (XMLVERSION || PC8080.APPVERSION) + "\n" + COPYRIGHT + "\n" + LICENSE); - - if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS); - - /* - * Iterate through all the components again and call their initBus() handler, if any - */ - for (iComponent = 0; iComponent < aComponents.length; iComponent++) { - component = aComponents[iComponent]; - if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg); - } - - var sStatePath = null; - var sResume = parmsComputer['resume']; - if (sResume !== undefined) { - /* - * DEPRECATE: This goofiness is a holdover from when the 'resume' property was a string (either a - * single-digit string or a path); now it's always a number, so it never has a 'length' property and - * the call to parseInt() is unnecessary. - */ - if (sResume.length > 1) { - sStatePath = this.sResumePath = sResume; - } else { - this.resume = parseInt(sResume, 10); - } - } - - /* - * The Computer 'state' property allows a state file to be specified independent of the 'resume' feature; - * previously, you could only use 'resume' to load a state file -- which we still support, but loading a state - * file that way prevents the machine's state from being saved, since we always resume from the 'resume' file. - * - * The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists. - * So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data, - * but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've - * already loaded. - * - * However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that - * OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in - * localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off). - */ - var fAllowResume; - var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state']; - - if (sState) { - sStatePath = this.sStatePath = sState; - if (!fAllowResume) { - this.fServerState = true; - this.resume = Computer8080.RESUME_NONE; - } - if (this.resume) { - this.stateComputer = new State(this, PC8080.APPVERSION); - if (this.stateComputer.load()) { - sStatePath = null; + if (sResume.length > 1) { + sStatePath = this.sResumePath = sResume; } else { - delete this.stateComputer; + this.resume = parseInt(sResume, 10); + } + } + + /* + * The Computer 'state' property allows a state file to be specified independent of the 'resume' feature; + * previously, you could only use 'resume' to load a state file -- which we still support, but loading a state + * file that way prevents the machine's state from being saved, since we always resume from the 'resume' file. + * + * The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists. + * So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data, + * but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've + * already loaded. + * + * However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that + * OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in + * localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off). + */ + var fAllowResume; + var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state']; + + if (sState) { + sStatePath = this.sStatePath = sState; + if (!fAllowResume) { + this.fServerState = true; + this.resume = Computer8080.RESUME_NONE; + } + if (this.resume) { + this.stateComputer = new State(this, PC8080.APPVERSION); + if (this.stateComputer.load()) { + sStatePath = null; + } else { + delete this.stateComputer; + } + } + } + + /* + * If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set, + * then we have the option of resuming from a server-side state, assuming a valid USERID. + */ + if (!sStatePath && this.resume) { + sStatePath = this.getServerStatePath(); + if (sStatePath) this.fServerState = true; + } + + if (!sStatePath) { + this.setReady(); + } else { + var cmp = this; + Web.getResource(sStatePath, null, true, function(sURL, sResource, nErrorCode) { + cmp.doneLoad(sURL, sResource, nErrorCode); + }); + } + + if (!this.bindings["power"]) this.fAutoPower = true; + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (!fSuspended && this.fAutoPower) this.wait(this.powerOn); + } + + /** + * getMachineID() + * + * @this {Computer8080} + * @return {string} + */ + getMachineID() + { + return this.sMachineID; + } + + /** + * setMachineParms(parmsMachine) + * + * If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any). + * + * @this {Computer8080} + * @param {Object} [parmsMachine] + */ + setMachineParms(parmsMachine) + { + if (!parmsMachine) { + var sParms; + if (typeof resources == 'object' && (sParms = resources['parms'])) { + try { + parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")")); + } catch(e) { + Component.error(e.message + " (" + sParms + ")"); + } + } + } + this.parmsMachine = parmsMachine; + } + + /** + * getMachineParm(sParm, parmsComponent) + * + * If the machine parameter doesn't exist, we check for a matching component parameter (if parmsComponent is provided), + * and failing that, we check the bundled resources (if any). + * + * At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, then we return + * 'state' back to the caller (ie, the name of the resource), so that the caller will then attempt to load the 'state' + * resource to obtain the actual state. + * + * @this {Computer8080} + * @param {string} sParm + * @param {Object} [parmsComponent] + * @return {string|undefined} + */ + getMachineParm(sParm, parmsComponent) + { + /* + * When using getURLParm(), the check is allowed be a bit looser, because URL parameters are + * user-supplied, whereas most other parameters are developer-supplied. Granted, a developer + * may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'), + * but there are limits to my paranoia. + */ + var sParmLC = sParm.toLowerCase(); + var value = Web.getURLParm(sParm) || Web.getURLParm(sParmLC); + + if (value === undefined && this.parmsMachine) { + value = this.parmsMachine[sParm]; + } + if (value === undefined && parmsComponent) { + value = parmsComponent[sParm]; + } + if (value === undefined && typeof resources == 'object' && resources[sParm]) { + value = sParm; + } + return value; + } + + /** + * saveMachineParms() + * + * @this {Computer8080} + * @return {string|null} + */ + saveMachineParms() + { + return this.parmsMachine? JSON.stringify(this.parmsMachine) : null; + } + + /** + * getUserID() + * + * @this {Computer8080} + * @return {string} + */ + getUserID() + { + return this.sUserID || ""; + } + + /** + * doneLoad(sURL, sStateData, nErrorCode) + * + * @this {Computer8080} + * @param {string} sURL + * @param {string} sStateData + * @param {number} nErrorCode + */ + doneLoad(sURL, sStateData, nErrorCode) + { + if (!nErrorCode) { + this.sStateData = sStateData; + this.fStateData = true; + if (DEBUG && this.messageEnabled()) { + this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx")); + } + } else { + this.sResumePath = null; + this.fServerState = false; + this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + Str.trim(sStateData) : '') + ')'); + } + this.setReady(); + } + + /** + * wait(fn, parms) + * + * wait() waits until every component is ready (including ourselves, the last component we check), then calls the + * specified Computer method. + * + * TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases; + * sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that + * takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists + * that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped + * function declaration, instead of something stricter like: + * + * param {function(this:Computer, (number|Array|undefined)): undefined} fn + * + * @this {Computer8080} + * @param {function(...)} fn + * @param {number|Array} [parms] optional parameters + */ + wait(fn, parms) + { + var computer = this; + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) { + var component = (iComponent < aComponents.length ? aComponents[iComponent] : this); + if (!component.isReady()) { + component.isReady(function onComponentReady() { + computer.wait(fn, parms); + }); + return; + } + } + if (DEBUG && this.messageEnabled()) this.printMessage("Computer8080.wait(ready)"); + fn.call(this, parms); + } + + /** + * validateState(stateComputer) + * + * NOTE: We clear() stateValidate only when there's no stateComputer. + * + * @this {Computer8080} + * @param {State|null} [stateComputer] + * @return {boolean} true if state passes validation, false if not + */ + validateState(stateComputer) + { + var fValid = true; + var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE); + if (stateValidate.load() && stateValidate.parse()) { + var sTimestampValidate = stateValidate.get(Computer8080.STATE_TIMESTAMP); + var sTimestampComputer = stateComputer? stateComputer.get(Computer8080.STATE_TIMESTAMP) : "unknown"; + if (sTimestampValidate != sTimestampComputer) { + this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits"); + fValid = false; + if (!stateComputer) stateValidate.clear(); + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")"); + } + } + } + return fValid; + } + + /** + * powerOn(resume) + * + * Power every component "up", applying any previously available state information. + * + * @this {Computer8080} + * @param {number} [resume] is a valid RESUME value; default is this.resume + */ + powerOn(resume) + { + if (resume === undefined) { + resume = this.resume || (this.sStateData? Computer8080.RESUME_AUTO : Computer8080.RESUME_NONE); + } + + if (DEBUG && this.messageEnabled()) { + this.printMessage("Computer8080.powerOn(" + (resume == Computer8080.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")"); + } + + if (this.nPowerChange) { + return; + } + this.nPowerChange++; + + var fRepower = false; + var fRestore = false; + this.fRestoreError = false; + var stateComputer = this.stateComputer || new State(this, PC8080.APPVERSION); + + if (resume == Computer8080.RESUME_REPOWER) { + fRepower = true; + } + else if (resume > Computer8080.RESUME_NONE) { + if (stateComputer.load(this.sStateData)) { + /* + * Since we're resuming something (either a predefined state or a state from localStorage), let's + * create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn(). + * Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad + * may have happened the last time around. + */ + this.stateFailSafe = new State(this, PC8080.APPVERSION, Computer8080.STATE_FAILSAFE); + if (this.stateFailSafe.load()) { + this.powerReport(stateComputer); + /* + * We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it + * all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to + * restore, the state will be removed. + */ + resume = Computer8080.RESUME_PROMPT; + /* + * To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail + * with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'"). + */ + this.stateFailSafe.unload(); + } + + this.stateFailSafe.set(Computer8080.STATE_TIMESTAMP, Usr.getTimestamp()); + this.stateFailSafe.store(); + + var fValidate = this.resume && !this.fServerState; + if (resume == Computer8080.RESUME_AUTO || Web.confirmUser("Click OK to restore the previous " + PC8080.APPNAME + " machine state, or CANCEL to reset the machine.")) { + fRestore = stateComputer.parse(); + if (fRestore) { + var sCode = /** @type {string} */ (stateComputer.get(UserAPI.RES.CODE)); + var sData = /** @type {string} */ (stateComputer.get(UserAPI.RES.DATA)); + if (sCode) { + if (sCode == UserAPI.CODE.OK) { + stateComputer.load(sData); + } else { + /* + * A missing (or not yet created) state file is no cause for alarm, but other errors might be + */ + if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) { + this.notice("Error: " + sData); + if (sData == UserAPI.FAIL.VERIFY) this.resetUserID(); + } else { + this.println(sCode + ": " + sData); + } + /* + * Try falling back to the state that we should have saved in localStorage, as a backup to the + * server-side state. + */ + stateComputer.unload(); // discard the invalid server-side state first + if (stateComputer.load()) { + fRestore = stateComputer.parse(); + fValidate = true; + } else { + fRestore = false; // hmmm, there was nothing in localStorage either + } + } + } + } + /* + * If the load/parse was successful, and it was from localStorage (not sStateData), + * then we should to try verify that localStorage snapshot is current. One reason it may + * NOT be current is if localStorage was full and we got a quota error during the last + * powerOff(). + */ + if (fValidate) this.validateState(fRestore? stateComputer : null); + } else { + /* + * RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above. + */ + if (resume == Computer8080.RESUME_PROMPT) stateComputer.clear(); + } + } else { + /* + * If there's no state, then there should also be no validation timestamp; if there is, then once again, + * we're probably dealing with a quota error. + */ + this.validateState(); + } + delete this.sStateData; + delete this.stateComputer; + } + + /* + * Start powering all components, including any data they may need to restore their state; + * we restore power to the CPU last. + */ + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component !== this && component != this.cpu) { + fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore); + } + } + + /* + * Assuming this is not a repower, we must perform another wait, because some components may + * have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it + * to mount one or more additional disk images). + */ + var aParms = [stateComputer, resume, fRestore]; + + if (resume != Computer8080.RESUME_REPOWER) { + this.wait(this.donePowerOn, aParms); + return; + } + this.donePowerOn(aParms); + } + + /** + * powerRestore(component, stateComputer, fRepower, fRestore) + * + * @this {Computer8080} + * @param {Component} component + * @param {State} stateComputer + * @param {boolean} fRepower + * @param {boolean} fRestore + * @return {boolean} true if restore should continue, false if not + */ + powerRestore(component, stateComputer, fRepower, fRestore) + { + if (!component.flags.powered) { + + component.flags.powered = true; + + if (component.powerUp) { + + var data = null; + if (fRestore) { + data = stateComputer.get(component.id); + if (!data) { + /* + * This is a hack that makes it possible for a machine whose ID has been + * supplemented with a suffix (a single letter or digit) to find object IDs + * in states created from a machine without the suffix. + * + * For example, if a state file was created from a machine with ID "ibm5160" + * but the current machine is "ibm5160a", this attempts a second lookup with + * "ibm5160", enabling us to find objects that match the original machine ID + * (eg, "ibm5160.romEGA"). + */ + data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.')); + } + } + + /* + * State.get() will return whatever was originally passed to State.set() (eg, an + * Object or a string), but components are supposed to store only Objects, so if a + * string comes back, something went wrong. By explicitly eliminating "string" data, + * the Closure Compiler stops complaining that we might be passing strings to our + * powerUp() functions (even though we know we're not). + * + * TODO: Determine if there's some way to coerce the Closure Compiler into treating + * data as Object or null, without having to include this runtime check. An assert + * would be a good idea, but this is overkill. + */ + if (typeof data === "string") data = null; + + /* + * If computer is null, this is simply a repower notification, which most components + * don't do anything with. Exceptions include: CPU (since it may be halted) and Video + * (since its screen may be "turned off"). + */ + if (!component.powerUp(data, fRepower) && data) { + + Component.error("Unable to restore state for " + component.type); + /* + * If this is a resume error for a machine that also has a predefined state + * AND we're not restoring from that state, then throw away the current state, + * prevent any new state from being created, and then force a reload, which will + * hopefully restore us to the functioning predefined state. + * + * TODO: Considering doing this in ALL cases, not just in situations where a + * 'state' exists but we're not actually resuming from it. + */ + if (this.sStatePath && !this.fStateData) { + stateComputer.clear(); + this.resume = Computer8080.RESUME_NONE; + Web.reloadPage(); + } else { + /* + * In all other cases, we set fRestoreError, which should trigger a call to + * powerReport() and then delete the offending state. + */ + this.fRestoreError = true; + } + /* + * Any failure triggers an automatic to call powerUp() again, without any state, + * in the hopes that the component can recover by performing a reset. + */ + component.powerUp(null); + /* + * We also disable the rest of the restore operation, because it's not clear + * the remaining state information can be trusted; the machine is already in an + * inconsistent state, so we're not likely to make things worse, and the only + * alternative (starting over and performing a state-less reset) isn't likely to make + * the user any happier. But, we'll see... we need some experience with the code. + */ + fRestore = false; + } + } + + if (!fRepower && component.comment) { + var asComments = component.comment.split("|"); + for (var i = 0; i < asComments.length; i++) { + component.status(asComments[i]); + } + } + } + return fRestore; + } + + /** + * donePowerOn(aParms) + * + * This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time. + * + * @this {Computer8080} + * @param {Array} aParms containing [stateComputer, resume, fRestore] + */ + donePowerOn(aParms) + { + var stateComputer = aParms[0]; + var fRepower = (aParms[1] < 0); + var fRestore = aParms[2]; + + if (DEBUG && this.flags.powered && this.messageEnabled()) { + this.printMessage("Computer8080.donePowerOn(): redundant"); + } + + this.fInitialized = true; + this.flags.powered = true; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "Shutdown"; + + /* + * Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU; + * the CPU should begin executing immediately, unless a debugger is attached. + */ + if (this.cpu) { + /* + * TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)? + */ + this.powerRestore(this.cpu, stateComputer, fRepower, fRestore); + this.cpu.autoStart(); + } + + /* + * If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new + * state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same. + */ + if (this.fRestoreError) { + this.powerReport(stateComputer); + stateComputer.clear(); + } + + if (!fRepower && this.stateFailSafe) { + this.stateFailSafe.clear(); + delete this.stateFailSafe; + } + + this.nPowerChange = 0; + } + + /** + * checkPower() + * + * @this {Computer8080} + * @return {boolean} true if the computer is fully powered, false otherwise + */ + checkPower() + { + if (this.flags.powered) return true; + + var component = null, iComponent; + var aComponents = Component.getComponents(this.id); + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component !== this && !component.flags.ready) break; + } + if (iComponent == aComponents.length) { + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component !== this && !component.flags.powered) break; + } + } + if (iComponent == aComponents.length) component = this; + var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + "."; + Web.alertUser(s); + return false; + } + + /** + * powerReport(stateComputer) + * + * @this {Computer8080} + * @param {State} stateComputer + */ + powerReport(stateComputer) + { + if (Web.confirmUser("There may be a problem with your " + PC8080.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PC8080.APPNAME + " machine state to http://" + SITEHOST + ".")) { + Web.sendReport(PC8080.APPNAME, PC8080.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); + } + } + + /** + * powerOff(fSave, fShutdown) + * + * Power every component "down" and optionally save the machine state. + * + * There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when + * the user switches away while it's still being restored, causing Disk getResource() calls to fail. The + * Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't + * notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact + * it was only partially successful. + * + * Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to + * deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport() + * and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new + * state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future + * restores won't remount them either. + * + * We could perhaps solve this by having the Disk component notify us in those situations, set a new flag + * (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false, + * that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to + * do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons, + * making sure those failures don't interfere with the save process in the same way. + * + * As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted, + * which doesn't seem like a huge problem. + * + * @this {Computer8080} + * @param {boolean} [fSave] is true to request a saved state + * @param {boolean} [fShutdown] is true if the machine is being shut down + * @return {string|null} string representing the saved state (or null if error) + */ + powerOff(fSave, fShutdown) + { + var data; + var sState = "none"; + + if (DEBUG && this.messageEnabled()) { + this.printMessage("Computer8080.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); + } + + if (this.nPowerChange) { + return null; + } + this.nPowerChange--; + + var stateComputer = new State(this, PC8080.APPVERSION); + var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE); + + var sTimestamp = Usr.getTimestamp(); + stateValidate.set(Computer8080.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(Computer8080.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(Computer8080.STATE_VERSION, APPVERSION); + stateComputer.set(Computer8080.STATE_HOSTURL, Web.getHostURL()); + stateComputer.set(Computer8080.STATE_BROWSER, Web.getUserAgent()); + + /* + * Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything + * after they're no longer ready. + */ + if (this.cpu && this.cpu.powerDown) { + if (fShutdown) this.cpu.stopCPU(); + data = this.cpu.powerDown(fSave, fShutdown); + if (typeof data === "object") stateComputer.set(this.cpu.id, data); + if (fShutdown) { + this.cpu.flags.powered = false; + if (data === false) sState = null; + } + } + + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.flags.powered) { + if (component.powerDown) { + data = component.powerDown(fSave, fShutdown); + if (typeof data === "object") stateComputer.set(component.id, data); + } + if (fShutdown) { + component.flags.powered = false; + if (data === false) sState = null; + } + } + } + + if (sState) { + if (fShutdown) { + var fClear = false; + var fClearAll = false; + if (fSave) { + if (this.sUserID) { + this.saveServerState(this.sUserID, stateComputer.toString()); + } + if (!stateValidate.store() || !stateComputer.store()) { + sState = null; + /* + * New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away. + * Dedicated server-side storage is the only way we'll ever be able to reliably preserve a + * particular machine's state. Historically, attempting to limp along with whatever localStorage + * is left just generates the same useless and annoying warnings over and over. + */ + fClear = fClearAll = true; + } + } + else { + /* + * I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the + * current machine is "resumable", because there are situations where I have two configurations + * for the same machine -- one resumable and one not -- and I don't want the latter throwing away the + * state of the former. + * + * So this code is here now strictly for callers to delete the state of a "resumable" machine, not as + * some paranoid clean-up operation. + * + * An undocumented feature of this operation is that if your configuration uses the special 'resume="3"' + * value, and you click the "Reset" button, and then you click OK to reset the everything, this will + * actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed). + */ + if (this.resume) { + fClear = true; + fClearAll = (this.resume == Computer8080.RESUME_DELETE); + } + } + if (fClear) { + stateComputer.clear(fClearAll); + } + } else { + sState = stateComputer.toString(); + } + } + + if (fShutdown) { + this.flags.powered = false; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "Power"; + } + + this.nPowerChange = 0; + + return sState; + } + + /** + * reset() + * + * Notify all (other) components with a reset() method that the Computer is being reset. + * + * NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we + * allocated the Bus object ourselves, after all the other components were allocated, it ends + * up near the end of Component's list of components. Hence the special case for this.bus below. + * + * @this {Computer8080} + */ + reset() + { + if (this.bus && this.bus.reset) { + /* + * TODO: Why does WebStorm think that this.bus.type is undefined? The base class (Component) + * constructor defines it. + */ + this.printMessage("Resetting " + this.bus.type); + this.bus.reset(); + } + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component !== this && component !== this.bus && component.reset) { + this.printMessage("Resetting " + component.type); + component.reset(); } } } - /* - * If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set, - * then we have the option of resuming from a server-side state, assuming a valid USERID. + /** + * start(ms, nCycles) + * + * Notify all (other) components with a start() method that the CPU has started. + * + * Note that we're called by runCPU(), which is why we exclude the CPU component, + * as well as ourselves. + * + * @this {Computer8080} + * @param {number} ms + * @param {number} nCycles */ - if (!sStatePath && this.resume) { - sStatePath = this.getServerStatePath(); - if (sStatePath) this.fServerState = true; + start(ms, nCycles) + { + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.type == "CPU" || component === this) continue; + if (component.start) { + component.start(ms, nCycles); + } + } } - if (!sStatePath) { - this.setReady(); - } else { - var cmp = this; - web.getResource(sStatePath, null, true, function(sURL, sResource, nErrorCode) { - cmp.doneLoad(sURL, sResource, nErrorCode); - }); + /** + * stop(ms, nCycles) + * + * Notify all (other) components with a stop() method that the CPU has stopped. + * + * Note that we're called by runCPU(), which is why we exclude the CPU component, + * as well as ourselves. + * + * @this {Computer8080} + * @param {number} ms + * @param {number} nCycles + */ + stop(ms, nCycles) + { + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.type == "CPU" || component === this) continue; + if (component.stop) { + component.stop(ms, nCycles); + } + } } - if (!this.bindings["power"]) this.fAutoPower = true; - - /* - * Power on the computer, giving every component the opportunity to reset or restore itself. + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {Computer8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request */ - if (!fSuspended && this.fAutoPower) this.wait(this.powerOn); + setBinding(sHTMLType, sBinding, control, sValue) + { + var computer = this; + + switch (sBinding) { + case "power": + this.bindings[sBinding] = control; + control.onclick = function onClickPower() { + computer.onPower(); + }; + return true; + + case "reset": + this.bindings[sBinding] = control; + control.onclick = function onClickReset() { + computer.onReset(); + }; + return true; + + /* + * Technically, this binding should now be called "saveState", to clearly distinguish it from + * the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving + * an entire machine includes everything needed to start/restore the machine; eg, the machine + * XML configuration file(s) *and* the JSON-encoded machine state. + */ + case "save": + /* + * Since this feature depends on the server supporting the PCjs User API (see userapi.js), + * and since pcjs.org is no longer running a Node web server, we disable the feature for that + * particular host. + */ + if (Str.endsWith(Web.getHost(), "pcjs.org")) { + if (DEBUG) this.log("Remote user API not available"); + /* + * We could also simply hide the control; eg: + * + * control.style.display = "none"; + * + * but removing the control altogether seems better. + */ + control.parentNode.removeChild(/** @type {Node} */ (control)); + return false; + } + this.bindings[sBinding] = control; + control.onclick = function onClickSave() { + var sUserID = computer.queryUserID(true); + if (sUserID) { + /* + * I modified the test to include a check for sStatePath so that I could save new states + * for machines with existing states; otherwise, I'd have no (easy) way of capturing and + * updating their state. Making the machine (even temporarily) resumable would have been + * one work-around, but it's not appropriate for some machines, as their state is simply + * too large (for localStorage anyway, which is the default storage solution). + */ + var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath); + var sState = computer.powerOff(fSave); + if (fSave) { + computer.saveServerState(sUserID, sState); + } else { + computer.notice("Resume disabled, machine state not saved"); + } + } + /* + * This seemed like a handy alternative, but it turned out to be a no-go, at least for large states: + * + * var sState = computer.powerOff(true); + * if (sState) { + * sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState); + * window.open(sState); + * } + * + * Perhaps if I embedded the data in a link on the current page instead; eg: + * + * $('Download').appendTo('#container'); + */ + }; + return true; + + default: + break; + } + return false; + } + + /** + * resetUserID() + */ + resetUserID() + { + Web.setLocalStorageItem(Computer8080.STATE_USERID, ""); + this.sUserID = null; + } + + /** + * queryUserID(fPrompt) + * + * @param {boolean} [fPrompt] + * @returns {string|null|undefined} + */ + queryUserID(fPrompt) + { + var sUserID = this.sUserID; + if (!sUserID) { + sUserID = Web.getLocalStorageItem(Computer8080.STATE_USERID); + if (sUserID !== undefined) { + if (!sUserID && fPrompt) { + /* + * NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is + * merely a precaution, because ordinarily, setBinding() should have already determined if we are + * running from pcjs.org and disabled any "Save" button. + */ + sUserID = Web.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below."); + if (sUserID) { + sUserID = this.verifyUserID(sUserID); + if (!sUserID) this.notice("The user ID is invalid."); + } + } + } else if (fPrompt) { + this.notice("Browser local storage is not available"); + } + } + return sUserID; + } + + /** + * verifyUserID(sUserID) + * + * @this {Computer8080} + * @param {string} sUserID + * @return {string} validated user ID, or null if error + */ + verifyUserID(sUserID) + { + this.sUserID = null; + var fMessages = DEBUG && this.messageEnabled(); + if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")"); + var sRequest = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID; + var response = Web.getResource(sRequest); + var nErrorCode = response[0]; + var sResponse = response[1]; + if (!nErrorCode && sResponse) { + try { + response = eval("(" + sResponse + ")"); + if (response.code && response.code == UserAPI.CODE.OK) { + Web.setLocalStorageItem(Computer8080.STATE_USERID, response.data); + if (fMessages) this.printMessage(Computer8080.STATE_USERID + " updated: " + response.data); + this.sUserID = response.data; + } else { + if (fMessages) this.printMessage(response.code + ": " + response.data); + } + } catch (e) { + Component.error(e.message + " (" + sResponse + ")"); + } + } else { + if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")"); + } + return this.sUserID; + } + + /** + * getServerStatePath() + * + * @this {Computer8080} + * @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage) + */ + getServerStatePath() + { + var sStatePath = null; + if (this.sUserID) { + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer8080.STATE_USERID + " for load: " + this.sUserID); + } + sStatePath = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PC8080.APPVERSION); + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer8080.STATE_USERID + " unavailable"); + } + } + return sStatePath; + } + + /** + * saveServerState(sUserID, sState) + * + * @param {string} sUserID + * @param {string|null} sState + */ + saveServerState(sUserID, sState) + { + /* + * We must pass fSync == true, because (as I understand it) browsers will blow off any async + * requests when a page is being closed. Since our request is synchronous, storeServerState() + * should also return a result, but there's not much we can do with it, since browsers ALSO + * tend to blow off alerts() and the like when closing down. + */ + if (sState) { + if (DEBUG && this.messageEnabled()) { + this.printMessage("size of server state: " + sState.length + " bytes"); + } + var response = this.storeServerState(sUserID, sState, true); + if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) { + this.notice("Machine state saved to server"); + } else if (sState) { + var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE; + if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) { + sError = "Error: " + sError; + } else { + sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError; + } + this.notice(sError); + this.resetUserID(); + } + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage("no state to store"); + } + } + } + + /** + * storeServerState(sUserID, sState, fSync) + * + * @this {Computer8080} + * @param {string} sUserID + * @param {string} sState + * @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async) + * @return {*} server response if fSync is true and a response was received; otherwise null + */ + storeServerState(sUserID, sState, fSync) + { + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer8080.STATE_USERID + " for store: " + sUserID); + } + /* + * TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event, + * and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through). + */ + var dataPost = {}; + dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE; + dataPost[UserAPI.QUERY.USER] = sUserID; + dataPost[UserAPI.QUERY.STATE] = State.key(this, PC8080.APPVERSION); + dataPost[UserAPI.QUERY.DATA] = sState; + var sRequest = Web.getHost() + UserAPI.ENDPOINT; + if (!fSync) { + Web.getResource(sRequest, dataPost, true); + } else { + var response = Web.getResource(sRequest, dataPost); + var sResponse = response[0]; + if (response[1]) { + if (sResponse) { + var i = sResponse.indexOf('\n'); + if (i > 0) sResponse = sResponse.substr(0, i); + if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7); + } + sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}'; + } + if (DEBUG && this.messageEnabled()) this.printMessage(sResponse); + return JSON.parse(sResponse); + } + return null; + } + + /** + * onPower() + * + * This handles UI requests to toggle the computer's power (eg, see the "power" button binding). + * + * @this {Computer8080} + */ + onPower() + { + if (!this.nPowerChange) { + if (!this.flags.powered) { + this.wait(this.powerOn); + } else { + this.powerOff(false, true); + } + } + } + + /** + * onReset() + * + * This handles UI requests to reset the computer's state (eg, see the "reset" button binding). + * + * @this {Computer8080} + */ + onReset() + { + /* + * I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset"; + * ditto if the power state is currently being changed. + */ + if (!this.flags.powered || this.nPowerChange) return; + + /* + * If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset + * operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to + * discard a machine's state, including any disk changes. The traditional "reset" operation is still available + * for non-resumable machines. + * + * TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really + * wants to clutter the UI with confusing options. ;-) + */ + if (this.resume && !this.sResumePath) { + /* + * I used to bypass the prompt if this.resume == Computer8080.RESUME_AUTO, setting fSave to true automatically, + * but that gives the user no means of resetting a resumable machine that contains errors in its resume state. + */ + var fSave = (/* this.resume == Computer8080.RESUME_AUTO || */ Web.confirmUser("Click OK to save changes to this " + PC8080.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); + this.powerOff(fSave, true); + /* + * Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn() + * and rely on all the components to reset themselves to their default state. The components with + * the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether + * or not to unload/reload all their original auto-mounted disk images. + * + * However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because + * we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource() + * again); alternatively, we could avoid throwing that state away, but it seems better to save the memory. + * + * TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer. + */ + if (!fSave && this.sStatePath) { + Web.reloadPage(); + return; + } + if (!fSave) this.fReload = true; + this.powerOn(Computer8080.RESUME_NONE); + this.fReload = false; + } else { + this.reset(); + if (this.cpu) this.cpu.autoStart(); + } + } + + /** + * getMachineComponent(sType, componentPrev) + * + * @this {Computer8080} + * @param {string} sType + * @param {Component|null} [componentPrev] of previously returned component, if any + * @return {Component|null} + */ + getMachineComponent(sType, componentPrev) + { + var componentLast = componentPrev; + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (componentPrev) { + if (componentPrev == component) componentPrev = null; + continue; + } + if (component.type == sType) return component; + } + if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning"); + return null; + } + + /** + * updateFocus(fScroll) + * + * NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button), + * the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS, + * where the display is more constrained, so we no longer do it by default (fScroll must be true). + * + * @this {Computer8080} + * @param {boolean} [fScroll] (true if you really want the control scrolled into view) + */ + updateFocus(fScroll) + { + if (this.aVideo.length) { + /* + * This seems to be recommended work-around to prevent the browser from scrolling the focused element + * into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent + * is to ensure that keyboard input is fielded properly. + */ + var x = 0, y = 0; + if (!fScroll && window) { + x = window.scrollX; + y = window.scrollY; + } + + /* + * TODO: We need a mechanism to determine the "active" display, instead of hard-coding this to aVideo[0]. + */ + this.aVideo[0].setFocus(); + + if (!fScroll && window) { + window.scrollTo(x, y); + } + } + } + + /** + * updateStatus(fForce) + * + * If any DOM controls were bound to the CPU, then we need to call its updateStatus() handler; if there are no + * such bindings, then cpu.updateStatus() does nothing. + * + * Similarly, if there's a Panel, then we need to call its updateStatus() handler, in case it created its own canvas + * and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateStatus() also does nothing. + * + * In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or* + * neither. In theory, there could be BOTH, but that would be unusual. + * + * TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to + * separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately + * knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate, + * but Panel is an optional component; generally, only machines that include Debugger also include Panel. + * + * @this {Computer8080} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) + */ + updateStatus(fForce) + { + /* + * fForce is generally set to true whenever the CPU is transitioning to/from a running state, in which case + * cpu.updateStatus() will definitely want to hide/show register contents; however, at other times, when the + * CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance. + * + * So fForce serves as a hint to help cpu.updateStatus() make a more informed decision. panel.updateStatus() + * currently doesn't care, on the theory that canvas updates should be significantly faster than DOM updates, + * but we still pass fForce on. + */ + if (this.cpu) this.cpu.updateStatus(fForce); + if (this.panel) this.panel.updateStatus(fForce); + } + + /** + * updateVideo(fForced) + * + * Any high-frequency updates should be performed here (avoid updating DOM elements). + * + * @this {Computer8080} + * @param {boolean} [fForced] + */ + updateVideo(fForced) + { + for (var i = 0; i < this.aVideo.length; i++) { + this.aVideo[i].updateScreen(fForced); + } + } + + /** + * Computer8080.init() + * + * For every machine represented by an HTML element of class "pc8080-machine", this function + * locates the HTML element of class "computer", extracting the JSON-encoded parameters for the + * Computer constructor from the element's "data-value" attribute, invoking the constructor to + * create a Computer component, and then binding any associated HTML controls to the new component. + */ + static init() + { + /* + * In non-COMPILED builds, embedMachine() may have set XMLVERSION. + */ + if (!COMPILED && XMLVERSION) PC8080.APPVERSION = XMLVERSION; + + var aeMachines = Component.getElementsByClass(document, PC8080.APPCLASS + "-machine"); + + for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) { + + var eMachine = aeMachines[iMachine]; + var parmsMachine = Component.getComponentParms(eMachine); + + var aeComputers = Component.getElementsByClass(eMachine, PC8080.APPCLASS, "computer"); + + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + + /* + * We set fSuspended in the Computer constructor because we want to "power up" the + * computer ourselves, after any/all bindings are in place. + */ + var computer = new Computer8080(parmsComputer, parmsMachine, true); + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onInit(" + computer.flags.powered + ")"); + } + + /* + * Bind any "power", "reset" and "save" buttons. An "erase" button was also considered, + * but "reset" now provides a way to force the machine to start from scratch again, so "erase" + * may be redundant now. + */ + Component.bindComponentControls(computer, eComputer, PC8080.APPCLASS); + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (computer.fAutoPower) computer.wait(computer.powerOn); + } + } + } + + /** + * Computer8080.show() + * + * When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything, + * without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the + * computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process + * should be very quick, essentially just marking all components as powered again (so that, for example, the Video + * component will start drawing again) and firing the CPU up again. + */ + static show() + { + var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")"); + } + + /* + * Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs + * AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet. + * So, practically speaking, the first show() callback isn't all that useful. + */ + if (computer.fInitialized && !computer.flags.powered) { + /** + * Repower the computer, notifying every component to continue running as-is. + */ + computer.powerOn(Computer8080.RESUME_REPOWER); + } + } + } + } + + /** + * Computer8080.exit() + * + * The Computer is currently the only component that uses an "exit" handler, which web.onExit() defines as + * either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state, + * using our powerOff() function, before the page goes away. + * + * It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire + * page (and therefore this entire application) is retained in its current state by the browser (well, some + * browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing + * the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state. + * + * In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words, + * an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere + * presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns. + * + * However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower + * everything, without either resetting or restoring. Hence, the Computer8080.show() function, which calls powerOn() + * with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser + * didn't change anything. This "repower" process should be very quick, essentially just marking all components as + * powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again. + * + * Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use + * "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and + * localStorage means the simulation should be restored and become operational almost immediately. + */ + static exit() + { + var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onExit(" + computer.flags.powered + ")"); + } + + if (computer.flags.powered) { + /** + * Power off the computer, giving every component an opportunity to save its state, + * but only if 'resume' has been set AND there is no valid resume path (because if a valid resume + * path exists, we'll always load our state from there, and not from whatever we save here). + */ + computer.powerOff(!!(computer.resume && !computer.sResumePath), true); + } + } + } + } } -Component.subclass(Computer8080); - Computer8080.STATE_FAILSAFE = "failsafe"; Computer8080.STATE_VALIDATE = "validate"; Computer8080.STATE_TIMESTAMP = "timestamp"; @@ -293,1342 +1633,11 @@ Computer8080.RESUME_AUTO = 1; // automatically save/restore state Computer8080.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) Computer8080.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) -/** - * getMachineID() - * - * @return {string} - */ -Computer8080.prototype.getMachineID = function() -{ - return this.sMachineID; -}; - -/** - * setMachineParms(parmsMachine) - * - * If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any). - * - * @param {Object} [parmsMachine] - */ -Computer8080.prototype.setMachineParms = function(parmsMachine) -{ - if (!parmsMachine) { - var sParms; - if (typeof resources == 'object' && (sParms = resources['parms'])) { - try { - parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")")); - } catch(e) { - Component.error(e.message + " (" + sParms + ")"); - } - } - } - this.parmsMachine = parmsMachine; -}; - -/** - * getMachineParm(sParm, parmsComponent) - * - * If the machine parameter doesn't exist, we check for a matching component parameter (if parmsComponent is provided), - * and failing that, we check the bundled resources (if any). - * - * At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, then we return - * 'state' back to the caller (ie, the name of the resource), so that the caller will then attempt to load the 'state' - * resource to obtain the actual state. - * - * @param {string} sParm - * @param {Object} [parmsComponent] - * @return {string|undefined} - */ -Computer8080.prototype.getMachineParm = function(sParm, parmsComponent) -{ - /* - * When using getURLParm(), the check is allowed be a bit looser, because URL parameters are - * user-supplied, whereas most other parameters are developer-supplied. Granted, a developer - * may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'), - * but there are limits to my paranoia. - */ - var sParmLC = sParm.toLowerCase(); - var value = web.getURLParm(sParm) || web.getURLParm(sParmLC); - - if (value === undefined && this.parmsMachine) { - value = this.parmsMachine[sParm]; - } - if (value === undefined && parmsComponent) { - value = parmsComponent[sParm]; - } - if (value === undefined && typeof resources == 'object' && resources[sParm]) { - value = sParm; - } - return value; -}; - -/** - * saveMachineParms() - * - * @return {string|null} - */ -Computer8080.prototype.saveMachineParms = function() -{ - return this.parmsMachine? JSON.stringify(this.parmsMachine) : null; -}; - -/** - * getUserID() - * - * @return {string} - */ -Computer8080.prototype.getUserID = function() -{ - return this.sUserID || ""; -}; - -/** - * doneLoad(sURL, sStateData, nErrorCode) - * - * @this {Computer8080} - * @param {string} sURL - * @param {string} sStateData - * @param {number} nErrorCode - */ -Computer8080.prototype.doneLoad = function(sURL, sStateData, nErrorCode) -{ - if (!nErrorCode) { - this.sStateData = sStateData; - this.fStateData = true; - if (DEBUG && this.messageEnabled()) { - this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx")); - } - } else { - this.sResumePath = null; - this.fServerState = false; - this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + str.trim(sStateData) : '') + ')'); - } - this.setReady(); -}; - -/** - * wait(fn, parms) - * - * wait() waits until every component is ready (including ourselves, the last component we check), then calls the - * specified Computer method. - * - * TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases; - * sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that - * takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists - * that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped - * function declaration, instead of something stricter like: - * - * param {function(this:Computer, (number|Array|undefined)): undefined} fn - * - * @this {Computer8080} - * @param {function(...)} fn - * @param {number|Array} [parms] optional parameters - */ -Computer8080.prototype.wait = function(fn, parms) -{ - var computer = this; - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) { - var component = (iComponent < aComponents.length ? aComponents[iComponent] : this); - if (!component.isReady()) { - component.isReady(function onComponentReady() { - computer.wait(fn, parms); - }); - return; - } - } - if (DEBUG && this.messageEnabled()) this.printMessage("Computer8080.wait(ready)"); - fn.call(this, parms); -}; - -/** - * validateState(stateComputer) - * - * NOTE: We clear() stateValidate only when there's no stateComputer. - * - * @this {Computer8080} - * @param {State|null} [stateComputer] - * @return {boolean} true if state passes validation, false if not - */ -Computer8080.prototype.validateState = function(stateComputer) -{ - var fValid = true; - var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE); - if (stateValidate.load() && stateValidate.parse()) { - var sTimestampValidate = stateValidate.get(Computer8080.STATE_TIMESTAMP); - var sTimestampComputer = stateComputer? stateComputer.get(Computer8080.STATE_TIMESTAMP) : "unknown"; - if (sTimestampValidate != sTimestampComputer) { - this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits"); - fValid = false; - if (!stateComputer) stateValidate.clear(); - } else { - if (DEBUG && this.messageEnabled()) { - this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")"); - } - } - } - return fValid; -}; - -/** - * powerOn(resume) - * - * Power every component "up", applying any previously available state information. - * - * @this {Computer8080} - * @param {number} [resume] is a valid RESUME value; default is this.resume - */ -Computer8080.prototype.powerOn = function(resume) -{ - if (resume === undefined) { - resume = this.resume || (this.sStateData? Computer8080.RESUME_AUTO : Computer8080.RESUME_NONE); - } - - if (DEBUG && this.messageEnabled()) { - this.printMessage("Computer8080.powerOn(" + (resume == Computer8080.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")"); - } - - if (this.nPowerChange) { - return; - } - this.nPowerChange++; - - var fRepower = false; - var fRestore = false; - this.fRestoreError = false; - var stateComputer = this.stateComputer || new State(this, PC8080.APPVERSION); - - if (resume == Computer8080.RESUME_REPOWER) { - fRepower = true; - } - else if (resume > Computer8080.RESUME_NONE) { - if (stateComputer.load(this.sStateData)) { - /* - * Since we're resuming something (either a predefined state or a state from localStorage), let's - * create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn(). - * Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad - * may have happened the last time around. - */ - this.stateFailSafe = new State(this, PC8080.APPVERSION, Computer8080.STATE_FAILSAFE); - if (this.stateFailSafe.load()) { - this.powerReport(stateComputer); - /* - * We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it - * all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to - * restore, the state will be removed. - */ - resume = Computer8080.RESUME_PROMPT; - /* - * To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail - * with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'"). - */ - this.stateFailSafe.unload(); - } - - this.stateFailSafe.set(Computer8080.STATE_TIMESTAMP, usr.getTimestamp()); - this.stateFailSafe.store(); - - var fValidate = this.resume && !this.fServerState; - if (resume == Computer8080.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + PC8080.APPNAME + " machine state, or CANCEL to reset the machine.")) { - fRestore = stateComputer.parse(); - if (fRestore) { - var sCode = stateComputer.get(UserAPI.RES.CODE); - var sData = stateComputer.get(UserAPI.RES.DATA); - if (sCode) { - if (sCode == UserAPI.CODE.OK) { - stateComputer.load(sData); - } else { - /* - * A missing (or not yet created) state file is no cause for alarm, but other errors might be - */ - if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) { - this.notice("Error: " + sData); - if (sData == UserAPI.FAIL.VERIFY) this.resetUserID(); - } else { - this.println(sCode + ": " + sData); - } - /* - * Try falling back to the state that we should have saved in localStorage, as a backup to the - * server-side state. - */ - stateComputer.unload(); // discard the invalid server-side state first - if (stateComputer.load()) { - fRestore = stateComputer.parse(); - fValidate = true; - } else { - fRestore = false; // hmmm, there was nothing in localStorage either - } - } - } - } - /* - * If the load/parse was successful, and it was from localStorage (not sStateData), - * then we should to try verify that localStorage snapshot is current. One reason it may - * NOT be current is if localStorage was full and we got a quota error during the last - * powerOff(). - */ - if (fValidate) this.validateState(fRestore? stateComputer : null); - } else { - /* - * RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above. - */ - if (resume == Computer8080.RESUME_PROMPT) stateComputer.clear(); - } - } else { - /* - * If there's no state, then there should also be no validation timestamp; if there is, then once again, - * we're probably dealing with a quota error. - */ - this.validateState(); - } - delete this.sStateData; - delete this.stateComputer; - } - - /* - * Start powering all components, including any data they may need to restore their state; - * we restore power to the CPU last. - */ - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (component !== this && component != this.cpu) { - fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore); - } - } - - /* - * Assuming this is not a repower, we must perform another wait, because some components may - * have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it - * to mount one or more additional disk images). - */ - var aParms = [stateComputer, resume, fRestore]; - - if (resume != Computer8080.RESUME_REPOWER) { - this.wait(this.donePowerOn, aParms); - return; - } - this.donePowerOn(aParms); -}; - -/** - * powerRestore(component, stateComputer, fRepower, fRestore) - * - * @this {Computer8080} - * @param {Component} component - * @param {State} stateComputer - * @param {boolean} fRepower - * @param {boolean} fRestore - * @return {boolean} true if restore should continue, false if not - */ -Computer8080.prototype.powerRestore = function(component, stateComputer, fRepower, fRestore) -{ - if (!component.flags.powered) { - - component.flags.powered = true; - - if (component.powerUp) { - - var data = null; - if (fRestore) { - data = stateComputer.get(component.id); - if (!data) { - /* - * This is a hack that makes it possible for a machine whose ID has been - * supplemented with a suffix (a single letter or digit) to find object IDs - * in states created from a machine without the suffix. - * - * For example, if a state file was created from a machine with ID "ibm5160" - * but the current machine is "ibm5160a", this attempts a second lookup with - * "ibm5160", enabling us to find objects that match the original machine ID - * (eg, "ibm5160.romEGA"). - */ - data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.')); - } - } - - /* - * State.get() will return whatever was originally passed to State.set() (eg, an - * Object or a string), but components are supposed to store only Objects, so if a - * string comes back, something went wrong. By explicitly eliminating "string" data, - * the Closure Compiler stops complaining that we might be passing strings to our - * powerUp() functions (even though we know we're not). - * - * TODO: Determine if there's some way to coerce the Closure Compiler into treating - * data as Object or null, without having to include this runtime check. An assert - * would be a good idea, but this is overkill. - */ - if (typeof data === "string") data = null; - - /* - * If computer is null, this is simply a repower notification, which most components - * don't do anything with. Exceptions include: CPU (since it may be halted) and Video - * (since its screen may be "turned off"). - */ - if (!component.powerUp(data, fRepower) && data) { - - Component.error("Unable to restore state for " + component.type); - /* - * If this is a resume error for a machine that also has a predefined state - * AND we're not restoring from that state, then throw away the current state, - * prevent any new state from being created, and then force a reload, which will - * hopefully restore us to the functioning predefined state. - * - * TODO: Considering doing this in ALL cases, not just in situations where a - * 'state' exists but we're not actually resuming from it. - */ - if (this.sStatePath && !this.fStateData) { - stateComputer.clear(); - this.resume = Computer8080.RESUME_NONE; - web.reloadPage(); - } else { - /* - * In all other cases, we set fRestoreError, which should trigger a call to - * powerReport() and then delete the offending state. - */ - this.fRestoreError = true; - } - /* - * Any failure triggers an automatic to call powerUp() again, without any state, - * in the hopes that the component can recover by performing a reset. - */ - component.powerUp(null); - /* - * We also disable the rest of the restore operation, because it's not clear - * the remaining state information can be trusted; the machine is already in an - * inconsistent state, so we're not likely to make things worse, and the only - * alternative (starting over and performing a state-less reset) isn't likely to make - * the user any happier. But, we'll see... we need some experience with the code. - */ - fRestore = false; - } - } - - if (!fRepower && component.comment) { - var asComments = component.comment.split("|"); - for (var i = 0; i < asComments.length; i++) { - component.status(asComments[i]); - } - } - } - return fRestore; -}; - -/** - * donePowerOn(aParms) - * - * This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time. - * - * @this {Computer8080} - * @param {Array} aParms containing [stateComputer, resume, fRestore] - */ -Computer8080.prototype.donePowerOn = function(aParms) -{ - var stateComputer = aParms[0]; - var fRepower = (aParms[1] < 0); - var fRestore = aParms[2]; - - if (DEBUG && this.flags.powered && this.messageEnabled()) { - this.printMessage("Computer8080.donePowerOn(): redundant"); - } - - this.fInitialized = true; - this.flags.powered = true; - var controlPower = this.bindings["power"]; - if (controlPower) controlPower.textContent = "Shutdown"; - - /* - * Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU; - * the CPU should begin executing immediately, unless a debugger is attached. - */ - if (this.cpu) { - /* - * TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)? - */ - this.powerRestore(this.cpu, stateComputer, fRepower, fRestore); - this.cpu.autoStart(); - } - - /* - * If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new - * state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same. - */ - if (this.fRestoreError) { - this.powerReport(stateComputer); - stateComputer.clear(); - } - - if (!fRepower && this.stateFailSafe) { - this.stateFailSafe.clear(); - delete this.stateFailSafe; - } - - this.nPowerChange = 0; -}; - -/** - * checkPower() - * - * @this {Computer8080} - * @return {boolean} true if the computer is fully powered, false otherwise - */ -Computer8080.prototype.checkPower = function() -{ - if (this.flags.powered) return true; - - var component = null, iComponent; - var aComponents = Component.getComponents(this.id); - for (iComponent = 0; iComponent < aComponents.length; iComponent++) { - component = aComponents[iComponent]; - if (component !== this && !component.flags.ready) break; - } - if (iComponent == aComponents.length) { - for (iComponent = 0; iComponent < aComponents.length; iComponent++) { - component = aComponents[iComponent]; - if (component !== this && !component.flags.powered) break; - } - } - if (iComponent == aComponents.length) component = this; - var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + "."; - web.alertUser(s); - return false; -}; - -/** - * powerReport(stateComputer) - * - * @this {Computer8080} - * @param {State} stateComputer - */ -Computer8080.prototype.powerReport = function(stateComputer) -{ - if (web.confirmUser("There may be a problem with your " + PC8080.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PC8080.APPNAME + " machine state to http://" + SITEHOST + ".")) { - web.sendReport(PC8080.APPNAME, PC8080.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); - } -}; - -/** - * powerOff(fSave, fShutdown) - * - * Power every component "down" and optionally save the machine state. - * - * There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when - * the user switches away while it's still being restored, causing Disk getResource() calls to fail. The - * Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't - * notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact - * it was only partially successful. - * - * Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to - * deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport() - * and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new - * state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future - * restores won't remount them either. - * - * We could perhaps solve this by having the Disk component notify us in those situations, set a new flag - * (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false, - * that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to - * do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons, - * making sure those failures don't interfere with the save process in the same way. - * - * As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted, - * which doesn't seem like a huge problem. - * - * @this {Computer8080} - * @param {boolean} [fSave] is true to request a saved state - * @param {boolean} [fShutdown] is true if the machine is being shut down - * @return {string|null} string representing the saved state (or null if error) - */ -Computer8080.prototype.powerOff = function(fSave, fShutdown) -{ - var data; - var sState = "none"; - - if (DEBUG && this.messageEnabled()) { - this.printMessage("Computer8080.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); - } - - if (this.nPowerChange) { - return null; - } - this.nPowerChange--; - - var stateComputer = new State(this, PC8080.APPVERSION); - var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE); - - var sTimestamp = usr.getTimestamp(); - stateValidate.set(Computer8080.STATE_TIMESTAMP, sTimestamp); - stateComputer.set(Computer8080.STATE_TIMESTAMP, sTimestamp); - stateComputer.set(Computer8080.STATE_VERSION, APPVERSION); - stateComputer.set(Computer8080.STATE_HOSTURL, web.getHostURL()); - stateComputer.set(Computer8080.STATE_BROWSER, web.getUserAgent()); - - /* - * Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything - * after they're no longer ready. - */ - if (this.cpu && this.cpu.powerDown) { - if (fShutdown) this.cpu.stopCPU(); - data = this.cpu.powerDown(fSave, fShutdown); - if (typeof data === "object") stateComputer.set(this.cpu.id, data); - if (fShutdown) { - this.cpu.flags.powered = false; - if (data === false) sState = null; - } - } - - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (component.flags.powered) { - if (component.powerDown) { - data = component.powerDown(fSave, fShutdown); - if (typeof data === "object") stateComputer.set(component.id, data); - } - if (fShutdown) { - component.flags.powered = false; - if (data === false) sState = null; - } - } - } - - if (sState) { - if (fShutdown) { - var fClear = false; - var fClearAll = false; - if (fSave) { - if (this.sUserID) { - this.saveServerState(this.sUserID, stateComputer.toString()); - } - if (!stateValidate.store() || !stateComputer.store()) { - sState = null; - /* - * New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away. - * Dedicated server-side storage is the only way we'll ever be able to reliably preserve a - * particular machine's state. Historically, attempting to limp along with whatever localStorage - * is left just generates the same useless and annoying warnings over and over. - */ - fClear = fClearAll = true; - } - } - else { - /* - * I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the - * current machine is "resumable", because there are situations where I have two configurations - * for the same machine -- one resumable and one not -- and I don't want the latter throwing away the - * state of the former. - * - * So this code is here now strictly for callers to delete the state of a "resumable" machine, not as - * some paranoid clean-up operation. - * - * An undocumented feature of this operation is that if your configuration uses the special 'resume="3"' - * value, and you click the "Reset" button, and then you click OK to reset the everything, this will - * actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed). - */ - if (this.resume) { - fClear = true; - fClearAll = (this.resume == Computer8080.RESUME_DELETE); - } - } - if (fClear) { - stateComputer.clear(fClearAll); - } - } else { - sState = stateComputer.toString(); - } - } - - if (fShutdown) { - this.flags.powered = false; - var controlPower = this.bindings["power"]; - if (controlPower) controlPower.textContent = "Power"; - } - - this.nPowerChange = 0; - - return sState; -}; - -/** - * reset() - * - * Notify all (other) components with a reset() method that the Computer is being reset. - * - * NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we - * allocated the Bus object ourselves, after all the other components were allocated, it ends - * up near the end of Component's list of components. Hence the special case for this.bus below. - * - * @this {Computer8080} - */ -Computer8080.prototype.reset = function() -{ - if (this.bus && this.bus.reset) { - /* - * TODO: Why does WebStorm think that this.bus.type is undefined? The base class (Component) - * constructor defines it. - */ - this.printMessage("Resetting " + this.bus.type); - this.bus.reset(); - } - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (component !== this && component !== this.bus && component.reset) { - this.printMessage("Resetting " + component.type); - component.reset(); - } - } -}; - -/** - * start(ms, nCycles) - * - * Notify all (other) components with a start() method that the CPU has started. - * - * Note that we're called by runCPU(), which is why we exclude the CPU component, - * as well as ourselves. - * - * @this {Computer8080} - * @param {number} ms - * @param {number} nCycles - */ -Computer8080.prototype.start = function(ms, nCycles) -{ - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (component.type == "CPU" || component === this) continue; - if (component.start) { - component.start(ms, nCycles); - } - } -}; - -/** - * stop(ms, nCycles) - * - * Notify all (other) components with a stop() method that the CPU has stopped. - * - * Note that we're called by runCPU(), which is why we exclude the CPU component, - * as well as ourselves. - * - * @this {Computer8080} - * @param {number} ms - * @param {number} nCycles - */ -Computer8080.prototype.stop = function(ms, nCycles) -{ - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (component.type == "CPU" || component === this) continue; - if (component.stop) { - component.stop(ms, nCycles); - } - } -}; - -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {Computer8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -Computer8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - var computer = this; - - switch (sBinding) { - case "power": - this.bindings[sBinding] = control; - control.onclick = function onClickPower() { - computer.onPower(); - }; - return true; - - case "reset": - this.bindings[sBinding] = control; - control.onclick = function onClickReset() { - computer.onReset(); - }; - return true; - - /* - * Technically, this binding should now be called "saveState", to clearly distinguish it from - * the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving - * an entire machine includes everything needed to start/restore the machine; eg, the machine - * XML configuration file(s) *and* the JSON-encoded machine state. - */ - case "save": - /* - * Since this feature depends on the server supporting the PCjs User API (see userapi.js), - * and since pcjs.org is no longer running a Node web server, we disable the feature for that - * particular host. - */ - if (str.endsWith(web.getHost(), "pcjs.org")) { - if (DEBUG) this.log("Remote user API not available"); - /* - * We could also simply hide the control; eg: - * - * control.style.display = "none"; - * - * but removing the control altogether seems better. - */ - control.parentNode.removeChild(/** @type {Node} */ (control)); - return false; - } - this.bindings[sBinding] = control; - control.onclick = function onClickSave() { - var sUserID = computer.queryUserID(true); - if (sUserID) { - /* - * I modified the test to include a check for sStatePath so that I could save new states - * for machines with existing states; otherwise, I'd have no (easy) way of capturing and - * updating their state. Making the machine (even temporarily) resumable would have been - * one work-around, but it's not appropriate for some machines, as their state is simply - * too large (for localStorage anyway, which is the default storage solution). - */ - var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath); - var sState = computer.powerOff(fSave); - if (fSave) { - computer.saveServerState(sUserID, sState); - } else { - computer.notice("Resume disabled, machine state not saved"); - } - } - /* - * This seemed like a handy alternative, but it turned out to be a no-go, at least for large states: - * - * var sState = computer.powerOff(true); - * if (sState) { - * sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState); - * window.open(sState); - * } - * - * Perhaps if I embedded the data in a link on the current page instead; eg: - * - * $('Download').appendTo('#container'); - */ - }; - return true; - - default: - break; - } - return false; -}; - -/** - * resetUserID() - */ -Computer8080.prototype.resetUserID = function() -{ - web.setLocalStorageItem(Computer8080.STATE_USERID, ""); - this.sUserID = null; -}; - -/** - * queryUserID(fPrompt) - * - * @param {boolean} [fPrompt] - * @returns {string|null|undefined} - */ -Computer8080.prototype.queryUserID = function(fPrompt) -{ - var sUserID = this.sUserID; - if (!sUserID) { - sUserID = web.getLocalStorageItem(Computer8080.STATE_USERID); - if (sUserID !== undefined) { - if (!sUserID && fPrompt) { - /* - * NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is - * merely a precaution, because ordinarily, setBinding() should have already determined if we are - * running from pcjs.org and disabled any "Save" button. - */ - sUserID = web.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below."); - if (sUserID) { - sUserID = this.verifyUserID(sUserID); - if (!sUserID) this.notice("The user ID is invalid."); - } - } - } else if (fPrompt) { - this.notice("Browser local storage is not available"); - } - } - return sUserID; -}; - -/** - * verifyUserID(sUserID) - * - * @this {Computer8080} - * @param {string} sUserID - * @return {string} validated user ID, or null if error - */ -Computer8080.prototype.verifyUserID = function(sUserID) -{ - this.sUserID = null; - var fMessages = DEBUG && this.messageEnabled(); - if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")"); - var sRequest = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID; - var response = web.getResource(sRequest); - var nErrorCode = response[0]; - var sResponse = response[1]; - if (!nErrorCode && sResponse) { - try { - response = eval("(" + sResponse + ")"); - if (response.code && response.code == UserAPI.CODE.OK) { - web.setLocalStorageItem(Computer8080.STATE_USERID, response.data); - if (fMessages) this.printMessage(Computer8080.STATE_USERID + " updated: " + response.data); - this.sUserID = response.data; - } else { - if (fMessages) this.printMessage(response.code + ": " + response.data); - } - } catch (e) { - Component.error(e.message + " (" + sResponse + ")"); - } - } else { - if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")"); - } - return this.sUserID; -}; - -/** - * getServerStatePath() - * - * @this {Computer8080} - * @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage) - */ -Computer8080.prototype.getServerStatePath = function() -{ - var sStatePath = null; - if (this.sUserID) { - if (DEBUG && this.messageEnabled()) { - this.printMessage(Computer8080.STATE_USERID + " for load: " + this.sUserID); - } - sStatePath = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PC8080.APPVERSION); - } else { - if (DEBUG && this.messageEnabled()) { - this.printMessage(Computer8080.STATE_USERID + " unavailable"); - } - } - return sStatePath; -}; - -/** - * saveServerState(sUserID, sState) - * - * @param {string} sUserID - * @param {string|null} sState - */ -Computer8080.prototype.saveServerState = function(sUserID, sState) -{ - /* - * We must pass fSync == true, because (as I understand it) browsers will blow off any async - * requests when a page is being closed. Since our request is synchronous, storeServerState() - * should also return a result, but there's not much we can do with it, since browsers ALSO - * tend to blow off alerts() and the like when closing down. - */ - if (sState) { - if (DEBUG && this.messageEnabled()) { - this.printMessage("size of server state: " + sState.length + " bytes"); - } - var response = this.storeServerState(sUserID, sState, true); - if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) { - this.notice("Machine state saved to server"); - } else if (sState) { - var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE; - if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) { - sError = "Error: " + sError; - } else { - sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError; - } - this.notice(sError); - this.resetUserID(); - } - } else { - if (DEBUG && this.messageEnabled()) { - this.printMessage("no state to store"); - } - } -}; - -/** - * storeServerState(sUserID, sState, fSync) - * - * @this {Computer8080} - * @param {string} sUserID - * @param {string} sState - * @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async) - * @return {*} server response if fSync is true and a response was received; otherwise null - */ -Computer8080.prototype.storeServerState = function(sUserID, sState, fSync) -{ - if (DEBUG && this.messageEnabled()) { - this.printMessage(Computer8080.STATE_USERID + " for store: " + sUserID); - } - /* - * TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event, - * and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through). - */ - var dataPost = {}; - dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE; - dataPost[UserAPI.QUERY.USER] = sUserID; - dataPost[UserAPI.QUERY.STATE] = State.key(this, PC8080.APPVERSION); - dataPost[UserAPI.QUERY.DATA] = sState; - var sRequest = web.getHost() + UserAPI.ENDPOINT; - if (!fSync) { - web.getResource(sRequest, dataPost, true); - } else { - var response = web.getResource(sRequest, dataPost); - var sResponse = response[0]; - if (response[1]) { - if (sResponse) { - var i = sResponse.indexOf('\n'); - if (i > 0) sResponse = sResponse.substr(0, i); - if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7); - } - sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}'; - } - if (DEBUG && this.messageEnabled()) this.printMessage(sResponse); - return JSON.parse(sResponse); - } - return null; -}; - -/** - * onPower() - * - * This handles UI requests to toggle the computer's power (eg, see the "power" button binding). - * - * @this {Computer8080} - */ -Computer8080.prototype.onPower = function() -{ - if (!this.nPowerChange) { - if (!this.flags.powered) { - this.wait(this.powerOn); - } else { - this.powerOff(false, true); - } - } -}; - -/** - * onReset() - * - * This handles UI requests to reset the computer's state (eg, see the "reset" button binding). - * - * @this {Computer8080} - */ -Computer8080.prototype.onReset = function() -{ - /* - * I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset"; - * ditto if the power state is currently being changed. - */ - if (!this.flags.powered || this.nPowerChange) return; - - /* - * If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset - * operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to - * discard a machine's state, including any disk changes. The traditional "reset" operation is still available - * for non-resumable machines. - * - * TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really - * wants to clutter the UI with confusing options. ;-) - */ - if (this.resume && !this.sResumePath) { - /* - * I used to bypass the prompt if this.resume == Computer8080.RESUME_AUTO, setting fSave to true automatically, - * but that gives the user no means of resetting a resumable machine that contains errors in its resume state. - */ - var fSave = (/* this.resume == Computer8080.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + PC8080.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); - this.powerOff(fSave, true); - /* - * Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn() - * and rely on all the components to reset themselves to their default state. The components with - * the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether - * or not to unload/reload all their original auto-mounted disk images. - * - * However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because - * we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource() - * again); alternatively, we could avoid throwing that state away, but it seems better to save the memory. - * - * TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer. - */ - if (!fSave && this.sStatePath) { - web.reloadPage(); - return; - } - if (!fSave) this.fReload = true; - this.powerOn(Computer8080.RESUME_NONE); - this.fReload = false; - } else { - this.reset(); - if (this.cpu) this.cpu.autoStart(); - } -}; - -/** - * getMachineComponent(sType, componentPrev) - * - * @this {Computer8080} - * @param {string} sType - * @param {Component|null} [componentPrev] of previously returned component, if any - * @return {Component|null} - */ -Computer8080.prototype.getMachineComponent = function(sType, componentPrev) -{ - var componentLast = componentPrev; - var aComponents = Component.getComponents(this.id); - for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { - var component = aComponents[iComponent]; - if (componentPrev) { - if (componentPrev == component) componentPrev = null; - continue; - } - if (component.type == sType) return component; - } - if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning"); - return null; -}; - -/** - * updateFocus(fScroll) - * - * NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button), - * the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS, - * where the display is more constrained, so we no longer do it by default (fScroll must be true). - * - * @this {Computer8080} - * @param {boolean} [fScroll] (true if you really want the control scrolled into view) - */ -Computer8080.prototype.updateFocus = function(fScroll) -{ - if (this.aVideo.length) { - /* - * This seems to be recommended work-around to prevent the browser from scrolling the focused element - * into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent - * is to ensure that keyboard input is fielded properly. - */ - var x = 0, y = 0; - if (!fScroll && window) { - x = window.scrollX; - y = window.scrollY; - } - - /* - * TODO: We need a mechanism to determine the "active" display, instead of hard-coding this to aVideo[0]. - */ - this.aVideo[0].setFocus(); - - if (!fScroll && window) { - window.scrollTo(x, y); - } - } -}; - -/** - * updateStatus(fForce) - * - * If any DOM controls were bound to the CPU, then we need to call its updateStatus() handler; if there are no - * such bindings, then cpu.updateStatus() does nothing. - * - * Similarly, if there's a Panel, then we need to call its updateStatus() handler, in case it created its own canvas - * and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateStatus() also does nothing. - * - * In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or* - * neither. In theory, there could be BOTH, but that would be unusual. - * - * TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to - * separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately - * knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate, - * but Panel is an optional component; generally, only machines that include Debugger also include Panel. - * - * @this {Computer8080} - * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) - */ -Computer8080.prototype.updateStatus = function(fForce) -{ - /* - * fForce is generally set to true whenever the CPU is transitioning to/from a running state, in which case - * cpu.updateStatus() will definitely want to hide/show register contents; however, at other times, when the - * CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance. - * - * So fForce serves as a hint to help cpu.updateStatus() make a more informed decision. panel.updateStatus() - * currently doesn't care, on the theory that canvas updates should be significantly faster than DOM updates, - * but we still pass fForce on. - */ - if (this.cpu) this.cpu.updateStatus(fForce); - if (this.panel) this.panel.updateStatus(fForce); -}; - -/** - * updateVideo(fForced) - * - * Any high-frequency updates should be performed here (avoid updating DOM elements). - * - * @this {Computer8080} - * @param {boolean} [fForced] - */ -Computer8080.prototype.updateVideo = function(fForced) -{ - for (var i = 0; i < this.aVideo.length; i++) { - this.aVideo[i].updateScreen(fForced); - } -}; - -/** - * Computer8080.init() - * - * For every machine represented by an HTML element of class "pc8080-machine", this function - * locates the HTML element of class "computer", extracting the JSON-encoded parameters for the - * Computer constructor from the element's "data-value" attribute, invoking the constructor to - * create a Computer component, and then binding any associated HTML controls to the new component. - */ -Computer8080.init = function() -{ - /* - * In non-COMPILED builds, embedMachine() may have set XMLVERSION. - */ - if (!COMPILED && XMLVERSION) PC8080.APPVERSION = XMLVERSION; - - var aeMachines = Component.getElementsByClass(document, PC8080.APPCLASS + "-machine"); - - for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) { - - var eMachine = aeMachines[iMachine]; - var parmsMachine = Component.getComponentParms(eMachine); - - var aeComputers = Component.getElementsByClass(eMachine, PC8080.APPCLASS, "computer"); - - for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { - - var eComputer = aeComputers[iComputer]; - var parmsComputer = Component.getComponentParms(eComputer); - - /* - * We set fSuspended in the Computer constructor because we want to "power up" the - * computer ourselves, after any/all bindings are in place. - */ - var computer = new Computer8080(parmsComputer, parmsMachine, true); - - if (DEBUG && computer.messageEnabled()) { - computer.printMessage("onInit(" + computer.flags.powered + ")"); - } - - /* - * Bind any "power", "reset" and "save" buttons. An "erase" button was also considered, - * but "reset" now provides a way to force the machine to start from scratch again, so "erase" - * may be redundant now. - */ - Component.bindComponentControls(computer, eComputer, PC8080.APPCLASS); - - /* - * Power on the computer, giving every component the opportunity to reset or restore itself. - */ - if (computer.fAutoPower) computer.wait(computer.powerOn); - } - } -}; - -/** - * Computer8080.show() - * - * When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything, - * without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the - * computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process - * should be very quick, essentially just marking all components as powered again (so that, for example, the Video - * component will start drawing again) and firing the CPU up again. - */ -Computer8080.show = function() -{ - var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer"); - for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { - var eComputer = aeComputers[iComputer]; - var parmsComputer = Component.getComponentParms(eComputer); - var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id'])); - if (computer) { - - if (DEBUG && computer.messageEnabled()) { - computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")"); - } - - /* - * Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs - * AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet. - * So, practically speaking, the first show() callback isn't all that useful. - */ - if (computer.fInitialized && !computer.flags.powered) { - /** - * Repower the computer, notifying every component to continue running as-is. - */ - computer.powerOn(Computer8080.RESUME_REPOWER); - } - } - } -}; - -/** - * Computer8080.exit() - * - * The Computer is currently the only component that uses an "exit" handler, which web.onExit() defines as - * either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state, - * using our powerOff() function, before the page goes away. - * - * It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire - * page (and therefore this entire application) is retained in its current state by the browser (well, some - * browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing - * the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state. - * - * In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words, - * an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere - * presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns. - * - * However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower - * everything, without either resetting or restoring. Hence, the Computer8080.show() function, which calls powerOn() - * with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser - * didn't change anything. This "repower" process should be very quick, essentially just marking all components as - * powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again. - * - * Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use - * "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and - * localStorage means the simulation should be restored and become operational almost immediately. - */ -Computer8080.exit = function() -{ - var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer"); - for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { - var eComputer = aeComputers[iComputer]; - var parmsComputer = Component.getComponentParms(eComputer); - var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id'])); - if (computer) { - - if (DEBUG && computer.messageEnabled()) { - computer.printMessage("onExit(" + computer.flags.powered + ")"); - } - - if (computer.flags.powered) { - /** - * Power off the computer, giving every component an opportunity to save its state, - * but only if 'resume' has been set AND there is no valid resume path (because if a valid resume - * path exists, we'll always load our state from there, and not from whatever we save here). - */ - computer.powerOff(!!(computer.resume && !computer.sResumePath), true); - } - } - } -}; - /* * Initialize every Computer on the page. */ -web.onInit(Computer8080.init); -web.onShow(Computer8080.show); -web.onExit(Computer8080.exit); +Web.onInit(Computer8080.init); +Web.onShow(Computer8080.show); +Web.onExit(Computer8080.exit); -if (NODE) module.exports = Computer8080; +module.exports = Computer8080; diff --git a/modules/pc8080/lib/cpu.js b/modules/pc8080/lib/cpu.js index 0bd5ca757..b2fe53b7f 100644 --- a/modules/pc8080/lib/cpu.js +++ b/modules/pc8080/lib/cpu.js @@ -28,107 +28,1206 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var Component = require("../../shared/lib/component"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Usr = require("../../shared/es6/usrlib"); +var Component = require("../../shared/es6/component"); +var Messages8080 = require("./messages"); /** - * CPU8080(parmsCPU, nCyclesDefault) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The CPU8080 class supports the following (parmsCPU) properties: - * - * cycles: the machine's base cycles per second; the CPUState8080 constructor - * will provide us with a default (based on the CPU model) to use as a fallback. - * - * multiplier: base cycle multiplier; default is 1. - * - * autoStart: true to automatically start, false to not, or null if "it depends"; - * null is the default, which means do not autostart UNLESS there is no Debugger - * and no "Run" button (ie, no way to manually start the machine). - * - * csStart: the number of cycles that runCPU() must wait before generating - * checksum records; -1 if disabled. checksum records are a diagnostic aid - * used to help compare one CPU run to another. - * - * csInterval: the number of cycles that runCPU() must execute before generating - * a checksum record; -1 if disabled. - * - * csStop: the number of cycles to stop generating checksum records. - * - * This component is primarily responsible for interfacing the CPU with the outside - * world (eg, Panel and Debugger components), and managing overall CPU operation. - * - * It is extended by the CPUState8080 component, where the simulation control logic resides. - * - * @constructor - * @extends Component - * @param {Object} parmsCPU - * @param {number} nCyclesDefault + * @unrestricted */ -function CPU8080(parmsCPU, nCyclesDefault) -{ - Component.call(this, "CPU", parmsCPU, CPU8080, Messages8080.CPU); - - var nCycles = parmsCPU['cycles'] || nCyclesDefault; - - var nMultiplier = parmsCPU['multiplier'] || 1; - - this.nCyclesPerSecond = nCycles; - - /* - * nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for - * the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier - * until we've exceeded the host's speed limit and then starts the multiplier over at 1. - */ - this.nCyclesMultiplier = nMultiplier; - this.mhzDefault = Math.round(this.nCyclesPerSecond / 10000) / 100; - /* - * TODO: Take care of this with an initial setSpeed() call instead? - */ - this.mhzTarget = this.mhzDefault * this.nCyclesMultiplier; - - /* - * We add a number of flags to the set initialized by Component - */ - this.flags.running = false; - this.flags.starting = false; - this.flags.autoStart = parmsCPU['autoStart']; - - /* - * TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both) - */ - this.flags.displayLiveRegs = false; - - /* - * Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum - * is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval - * ("csInterval") set to a positive value. +class CPU8080 extends Component { + /** + * CPU8080(parmsCPU, nCyclesDefault) * - * As above, any of these parameters can also be set with the Debugger's execution options - * command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000 - * and call resetChecksum(). + * The CPU8080 class supports the following (parmsCPU) properties: + * + * cycles: the machine's base cycles per second; the CPUState8080 constructor + * will provide us with a default (based on the CPU model) to use as a fallback. + * + * multiplier: base cycle multiplier; default is 1. + * + * autoStart: true to automatically start, false to not, or null if "it depends"; + * null is the default, which means do not autostart UNLESS there is no Debugger + * and no "Run" button (ie, no way to manually start the machine). + * + * csStart: the number of cycles that runCPU() must wait before generating + * checksum records; -1 if disabled. checksum records are a diagnostic aid + * used to help compare one CPU run to another. + * + * csInterval: the number of cycles that runCPU() must execute before generating + * a checksum record; -1 if disabled. + * + * csStop: the number of cycles to stop generating checksum records. + * + * This component is primarily responsible for interfacing the CPU with the outside + * world (eg, Panel and Debugger components), and managing overall CPU operation. + * + * It is extended by the CPUState8080 component, where the simulation control logic resides. + * + * @this {CPU8080} + * @param {Object} parmsCPU + * @param {number} nCyclesDefault */ - this.flags.checksum = false; - this.nChecksum = this.nCyclesChecksumNext = 0; - this.nCyclesChecksumStart = parmsCPU["csStart"]; - this.nCyclesChecksumInterval = parmsCPU["csInterval"]; - this.nCyclesChecksumStop = parmsCPU["csStop"]; + constructor(parmsCPU, nCyclesDefault) + { + super("CPU", parmsCPU, CPU8080, Messages8080.CPU); - /* - * Array of countdown timers managed by addTimer() and setTimer(). + var nCycles = parmsCPU['cycles'] || nCyclesDefault; + + var nMultiplier = parmsCPU['multiplier'] || 1; + + this.nCyclesPerSecond = nCycles; + + /* + * nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for + * the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier + * until we've exceeded the host's speed limit and then starts the multiplier over at 1. + */ + this.nCyclesMultiplier = nMultiplier; + this.mhzDefault = Math.round(this.nCyclesPerSecond / 10000) / 100; + /* + * TODO: Take care of this with an initial setSpeed() call instead? + */ + this.mhzTarget = this.mhzDefault * this.nCyclesMultiplier; + + /* + * We add a number of flags to the set initialized by Component + */ + this.flags.running = false; + this.flags.starting = false; + this.flags.autoStart = parmsCPU['autoStart']; + + /* + * TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both) + */ + this.flags.displayLiveRegs = false; + + /* + * Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum + * is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval + * ("csInterval") set to a positive value. + * + * As above, any of these parameters can also be set with the Debugger's execution options + * command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000 + * and call resetChecksum(). + */ + this.flags.checksum = false; + this.nChecksum = this.nCyclesChecksumNext = 0; + this.nCyclesChecksumStart = parmsCPU["csStart"]; + this.nCyclesChecksumInterval = parmsCPU["csInterval"]; + this.nCyclesChecksumStop = parmsCPU["csStop"]; + + /* + * Array of countdown timers managed by addTimer() and setTimer(). + */ + this.aTimers = []; + + this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); }; + + this.setReady(); + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {CPU8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPU8080} cpu + * @param {Debugger8080} dbg */ - this.aTimers = []; + initBus(cmp, bus, cpu, dbg) + { + this.cmp = cmp; + this.bus = bus; + this.dbg = dbg; - this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); }; + for (var i = 0; i < CPU8080.BUTTONS.length; i++) { + var control = this.bindings[CPU8080.BUTTONS[i]]; + if (control) this.cmp.setBinding(null, CPU8080.BUTTONS[i], control); + } - this.setReady(); + /* + * Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts. + */ + this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet")); + + /* + * We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override. + */ + var sAutoStart = cmp.getMachineParm('autoStart'); + if (sAutoStart != null) { + this.flags.autoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart)); + } + + this.setReady(); + } + + /** + * reset() + * + * @this {CPU8080} + */ + reset() + { + } + + /** + * save() + * + * This is a placeholder for save support (overridden by the CPUState8080 component). + * + * @this {CPU8080} + * @return {Object|null} + */ + save() + { + return null; + } + + /** + * restore(data) + * + * This is a placeholder for restore support (overridden by the CPUState8080 component). + * + * @this {CPU8080} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ + restore(data) + { + return false; + } + + /** + * powerUp(data, fRepower) + * + * @this {CPU8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + if (!data || !this.restore) { + this.reset(); + } else { + this.resetCycles(); + if (!this.restore(data)) return false; + this.resetChecksum(); + } + /* + * Give the Debugger a chance to do/print something once we've powered up + */ + if (DEBUGGER && this.dbg) { + this.dbg.init(); + } else { + /* + * The Computer (this.cmp) knows if there's a Control Panel (this.cmp.panel), and the Control Panel + * knows if there's a "print" control (this.cmp.panel.controlPrint), and if there IS a "print" control + * but no debugger, the machine is probably misconfigured (most likely, the page simply neglected to + * load the Debugger component). + * + * However, we don't actually need to check all that; it's always safe use println(), regardless whether + * a Control Panel with a "print" control is present or not. + */ + this.println("No debugger detected"); + } + } + /* + * The Computer component (which is responsible for all powerDown and powerUp notifications) + * is now responsible for managing a component's fPowered flag, not us. + * + * this.flags.powered = true; + */ + this.updateCPU(); + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {CPU8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + /* + * The Computer component (which is responsible for all powerDown and powerUp notifications) + * is now responsible for managing a component's fPowered flag, not us. + * + * this.flags.powered = false; + */ + return fSave? this.save() : true; + } + + /** + * autoStart() + * + * @this {CPU8080} + * @return {boolean} true if started, false if not + */ + autoStart() + { + /* + * Start running automatically on power-up, assuming there's no Debugger and no "Run" button + */ + if (this.flags.autoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) { + /* + * We used to also set fUpdateFocus when calling runCPU(), on the assumption that in the "auto-starting" + * context, a machine without focus is like a day without sunshine, but in reality, focus should only be + * forced when the user takes some other machine-related action. + */ + this.runCPU(); + return true; + } + return false; + } + + /** + * isPowered() + * + * @this {CPU8080} + * @return {boolean} + */ + isPowered() + { + if (!this.flags.powered) { + this.println(this.toString() + " not powered"); + return false; + } + return true; + } + + /** + * isRunning() + * + * @this {CPU8080} + * @return {boolean} + */ + isRunning() + { + return this.flags.running; + } + + /** + * getChecksum() + * + * This will be implemented by the CPUState8080 component. + * + * @this {CPU8080} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ + getChecksum() + { + return 0; + } + + /** + * resetChecksum() + * + * If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the + * cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever + * the CPU is reset or restored. + * + * @this {CPU8080} + * @return {boolean} true if checksum generation enabled, false if not + */ + resetChecksum() + { + if (this.nCyclesChecksumStart === undefined) this.nCyclesChecksumStart = 0; + if (this.nCyclesChecksumInterval === undefined) this.nCyclesChecksumInterval = -1; + if (this.nCyclesChecksumStop === undefined) this.nCyclesChecksumStop = -1; + this.flags.checksum = (this.nCyclesChecksumStart >= 0 && this.nCyclesChecksumInterval > 0); + if (this.flags.checksum) { + this.nChecksum = 0; + this.nCyclesChecksumNext = this.nCyclesChecksumStart - this.nTotalCycles; + /* + * this.nCyclesChecksumNext = this.nCyclesChecksumStart + this.nCyclesChecksumInterval - + * (this.nTotalCycles % this.nCyclesChecksumInterval); + */ + return true; + } + return false; + } + + /** + * updateChecksum(nCycles) + * + * When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum + * number of cycles (1), effectively limiting execution to a single instruction, and then we're called with + * the exact number cycles that were actually executed. This should give us instruction-granular checksums + * at precise intervals that are 100% repeatable. + * + * @this {CPU8080} + * @param {number} nCycles + */ + updateChecksum(nCycles) + { + if (this.flags.checksum) { + /* + * Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum + */ + var fDisplay = false; + this.nChecksum = (this.nChecksum + this.getChecksum())|0; + this.nCyclesChecksumNext -= nCycles; + if (this.nCyclesChecksumNext <= 0) { + this.nCyclesChecksumNext += this.nCyclesChecksumInterval; + fDisplay = true; + } + if (this.nCyclesChecksumStop >= 0) { + if (this.nCyclesChecksumStop <= this.getCycles()) { + this.nCyclesChecksumInterval = this.nCyclesChecksumStop = -1; + this.resetChecksum(); + this.stopCPU(); + fDisplay = true; + } + } + if (fDisplay) this.displayChecksum(); + } + } + + /** + * displayChecksum() + * + * When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all + * checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU + * properties). + * + * @this {CPU8080} + */ + displayChecksum() + { + this.println(this.getCycles() + " cycles: " + "checksum=" + Str.toHex(this.nChecksum)); + } + + /** + * displayValue(sLabel, nValue, cch) + * + * This is principally for displaying register values, but in reality, it can be used to display any + * numeric (hex) value bound to the given label. + * + * @this {CPU8080} + * @param {string} sLabel + * @param {number} nValue + * @param {number} cch + */ + displayValue(sLabel, nValue, cch) + { + if (this.bindings[sLabel]) { + if (nValue === undefined) { + this.setError("Value for " + sLabel + " is invalid"); + this.stopCPU(); + } + var sVal; + if (!this.flags.running || this.flags.displayLiveRegs) { + sVal = Str.toHex(nValue, cch); + } else { + sVal = "--------".substr(0, cch); + } + /* + * TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if + * we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary + * string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being + * too obsessive. + */ + if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal; + } + } + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {CPU8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + var cpu = this; + var fBound = false; + + switch (sBinding) { + case "power": + case "reset": + /* + * The "power" and "reset" buttons are functions of the entire computer, not just the CPU, + * but it's not always convenient to stick a power button in the Computer component definition, + * so we record those bindings here and pass them on to the Computer component in initBus(). + */ + this.bindings[sBinding] = control; + fBound = true; + break; + + case "run": + this.bindings[sBinding] = control; + control.onclick = function onClickRun() { + if (!cpu.cmp || !cpu.cmp.checkPower()) return; + /* + * We no longer pass true to these runCPU()/stopCPU() calls, on the theory that if the "run" + * control is visible, then the computer is probably sufficiently visible as well; the problem + * with setting fUpdateFocus to true is that it can jerk the web page around in annoying ways. + */ + if (!cpu.flags.running) + cpu.runCPU(); + else + cpu.stopCPU(); + }; + fBound = true; + break; + + case "speed": + this.bindings[sBinding] = control; + fBound = true; + break; + + case "setSpeed": + this.bindings[sBinding] = control; + control.onclick = function onClickSetSpeed() { + cpu.setSpeed(cpu.nCyclesMultiplier << 1, true); + }; + control.textContent = this.getSpeedTarget(); + fBound = true; + break; + + default: + break; + } + return fBound; + } + + /** + * setBurstCycles(nCycles) + * + * This function is used by the ChipSet component whenever a very low timer count is set, + * in anticipation of the timer requiring an update sooner than the normal nCyclesPerYield + * period in runCPU() would normally provide. + * + * NOTE: In this context, "timer" refers to a timer chip (eg, an Intel 8253) being emulated by + * by the ChipSet component, not the timers managed by the CPU (eg, addTimer(), setTimer(), etc). + * + * @this {CPU8080} + * @param {number} nCycles is the target number of cycles to drop the current burst to + * @return {boolean} + */ + setBurstCycles(nCycles) + { + if (this.flags.running) { + var nDelta = this.nStepCycles - nCycles; + /* + * NOTE: If nDelta is negative, we will actually be increasing nStepCycles and nBurstCycles. + * Which is OK, but if we're also taking snapshots of the cycle counts, to make sure that instruction + * costs are being properly assessed, then we need to update nSnapCycles as well. + * + * TODO: If the delta is negative, we could simply ignore the request, but we must first carefully + * consider the impact on the ChipSet timers, if any. + */ + // if (DEBUG) this.nSnapCycles -= nDelta; + this.nStepCycles -= nDelta; + this.nBurstCycles -= nDelta; + return true; + } + return false; + } + + /** + * addCycles(nCycles, fEndStep) + * + * @this {CPU8080} + * @param {number} nCycles + * @param {boolean} [fEndStep] + */ + addCycles(nCycles, fEndStep) + { + this.nTotalCycles += nCycles; + if (fEndStep) { + this.nBurstCycles = this.nStepCycles = 0; + } + } + + /** + * calcCycles(fRecalc) + * + * Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst + * is driven by YIELDS_PER_SECOND (eg, 30). + * + * At the end of each burst, we subtract burst cycles from the yield cycle "threshold" counter. + * Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time to the time + * we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time remaining, + * we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU(). + * + * @this {CPU8080} + * @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent + * speed calculation (see calcSpeed). + */ + calcCycles(fRecalc) + { + /* + * Calculate "per" yield values. + */ + var vMultiplier = 1; + if (fRecalc) { + if (this.nCyclesMultiplier > 1 && this.mhz) { + vMultiplier = (this.mhz / this.mhzDefault); + } + } + + this.msPerYield = Math.round(1000 / CPU8080.YIELDS_PER_SECOND); + this.nCyclesPerYield = Math.floor(this.nCyclesPerSecond / CPU8080.YIELDS_PER_SECOND * vMultiplier); + + /* + * And initialize "next" yield values to the "per" values. + */ + if (!fRecalc) { + this.nCyclesNextYield = this.nCyclesPerYield; + } + this.nCyclesRecalc = 0; + } + + /** + * getCycles(fScaled) + * + * getCycles() returns the number of cycles executed so far. Note that we can be called after + * runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(), + * so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles), + * less the number of cycles it has yet to run (nStepCycles). + * + * nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also + * have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and + * nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on + * getCycles() returning steadily increasing values should also be prepared for a reset at any time. + * + * @this {CPU8080} + * @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1 + * @return {number} + */ + getCycles(fScaled) + { + var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles; + if (fScaled && this.nCyclesMultiplier > 1 && this.mhz > this.mhzDefault) { + /* + * We could scale the current cycle count by the current effective speed (this.mhz); eg: + * + * nCycles = Math.round(nCycles / (this.mhz / this.mhzDefault)); + * + * but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller + * fluctuations after each burst of instructions that runCPU() executes. + * + * Alternatively, we can scale the cycle count by the multiplier, which is good in that the + * multiplier doesn't vary once the user changes it, but a potential downside is that the + * multiplier might be set too high, resulting in a target speed that's higher than the effective + * speed is able to reach. + * + * Also, if multipliers were always limited to a power-of-two, then this could be calculated + * with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger + * interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's + * XML file). + */ + nCycles = Math.round(nCycles / this.nCyclesMultiplier); + } + return nCycles; + } + + /** + * getCyclesPerSecond() + * + * This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine) + * + * @this {CPU8080} + * @return {number} + */ + getCyclesPerSecond() + { + return this.nCyclesPerSecond; + } + + /** + * resetCycles() + * + * Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp(). + * It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(), + * which in turn assumes that all the cycle counts have been initialized to sensible values. + * + * @this {CPU8080} + */ + resetCycles() + { + this.mhz = 0; + this.nYieldsSinceStatusUpdate = 0; + this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = 0; + this.resetChecksum(); + this.setSpeed(1); + } + + /** + * getSpeed() + * + * @this {CPU8080} + * @return {number} the current speed multiplier + */ + getSpeed() + { + return this.nCyclesMultiplier; + } + + /** + * getSpeedCurrent() + * + * @this {CPU8080} + * @return {string} the current speed, in mhz, as a string formatted to two decimal places + */ + getSpeedCurrent() + { + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return ((this.flags.running && this.mhz)? (this.mhz.toFixed(2) + "Mhz") : "Stopped"); + } + + /** + * getSpeedTarget() + * + * @this {CPU8080} + * @return {string} the target speed, in mhz, as a string formatted to two decimal places + */ + getSpeedTarget() + { + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return this.mhzTarget.toFixed(2) + "Mhz"; + } + + /** + * setSpeed(nMultiplier, fUpdateFocus) + * + * NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point. + * + * @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset, + * so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls + * setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting). + * + * @this {CPU8080} + * @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high) + * @param {boolean} [fUpdateFocus] is true to update Computer focus + * @return {boolean} true if successful, false if not + */ + setSpeed(nMultiplier, fUpdateFocus) + { + var fSuccess = false; + if (nMultiplier !== undefined) { + /* + * If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1). + */ + if (this.mhz / this.mhzTarget < 0.8) { + nMultiplier = 1; + } else { + fSuccess = true; + } + this.nCyclesMultiplier = nMultiplier; + var mhz = this.mhzDefault * this.nCyclesMultiplier; + if (this.mhzTarget != mhz) { + this.mhzTarget = mhz; + var sSpeed = this.getSpeedTarget(); + var controlSpeed = this.bindings["setSpeed"]; + if (controlSpeed) controlSpeed.textContent = sSpeed; + this.println("target speed: " + sSpeed); + } + if (fUpdateFocus && this.cmp) this.cmp.updateFocus(); + } + this.addCycles(this.nRunCycles); + this.nRunCycles = 0; + this.msStartRun = Usr.getTime(); + this.msEndThisRun = 0; + this.calcCycles(); + return fSuccess; + } + + /** + * calcSpeed(nCycles, msElapsed) + * + * @this {CPU8080} + * @param {number} nCycles + * @param {number} msElapsed + */ + calcSpeed(nCycles, msElapsed) + { + if (msElapsed) { + this.mhz = Math.round(nCycles / (msElapsed * 10)) / 100; + if (msElapsed >= 86400000) { + this.nTotalCycles = 0; + this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow + } + } + } + + /** + * calcStartTime() + * + * @this {CPU8080} + */ + calcStartTime() + { + if (this.nCyclesRecalc >= this.nCyclesPerSecond) { + this.calcCycles(true); + } + this.nCyclesThisRun = 0; + this.msStartThisRun = Usr.getTime(); + + /* + * Try to detect situations where the browser may have throttled us, such as when the user switches + * to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks + * to roughly one per second. + * + * Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled, + * but there can still be enough of a lag between the callbacks that CPU speed will be noticeably + * erratic if we don't compensate for it here. + * + * We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the + * previous run and includes any requested sleep time) is comparable to the current msStartThisRun; + * if the delta is significant, we compensate by bumping msStartRun forward by that delta. + * + * This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called + * whenever the CPU starts running again -- zeroes msEndThisRun. + * + * This also won't do anything about other internal delays; for example, Debugger message() calls. + * By the time the message() function has called yieldCPU(), the cost of the message has already been + * incurred, so it will be end up being charged against the instruction(s) that triggered it. + * + * TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun + * "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability + * to hit its target speed, since you would expect any instruction that displays a message to be an + * EXTREMELY slow instruction. + */ + if (this.msEndThisRun) { + var msDelta = this.msStartThisRun - this.msEndThisRun; + if (msDelta > this.msPerYield) { + if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms"); + this.msStartRun += msDelta; + /* + * Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just + * in case, I make absolutely sure it cannot happen, since doing so could result in negative + * speed calculations. + */ + this.assert(this.msStartRun <= this.msStartThisRun); + if (this.msStartRun > this.msStartThisRun) { + this.msStartRun = this.msStartThisRun; + } + } + } + } + + /** + * calcRemainingTime() + * + * @this {CPU8080} + * @return {number} + */ + calcRemainingTime() + { + this.msEndThisRun = Usr.getTime(); + + var msYield = this.msPerYield; + if (this.nCyclesThisRun) { + /* + * Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU + * now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then + * the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed, + * and so applying that percentage to msPerYield should give us a better estimate of work vs. time. + */ + msYield = Math.round(msYield * this.nCyclesThisRun / this.nCyclesPerYield); + } + + var msElapsedThisRun = this.msEndThisRun - this.msStartThisRun; + var msRemainsThisRun = msYield - msElapsedThisRun; + + /* + * We could pass only "this run" results to calcSpeed(): + * + * nCycles = this.nCyclesThisRun; + * msElapsed = msElapsedThisRun; + * + * but it seems preferable to use longer time periods and hopefully get a more accurate speed. + * + * Also, if msRemainsThisRun >= 0 && this.nCyclesMultiplier == 1, we could pass these results instead: + * + * nCycles = this.nCyclesThisRun; + * msElapsed = this.msPerYield; + * + * to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations. + */ + var nCycles = this.nRunCycles; + var msElapsed = this.msEndThisRun - this.msStartRun; + + if (MAXDEBUG && msRemainsThisRun < 0 && this.nCyclesMultiplier > 1) { + this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)"); + } + + this.calcSpeed(nCycles, msElapsed); + + if (msRemainsThisRun < 0 || this.mhz < this.mhzTarget) { + /* + * Try "throwing out" the effects of large anomalies, by moving the overall run start time up; + * ordinarily, this should only happen when the someone is using an external Debugger or some other + * tool or feature that is interfering with our overall execution. + */ + if (msRemainsThisRun < -1000) { + this.msStartRun -= msRemainsThisRun; + } + /* + * If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate + * nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the + * simulation is at least usable. + */ + msRemainsThisRun = 0; + } + + /* + * Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(), + * it'll be ready to decide if calcCycles() should be called again. + */ + this.nCyclesRecalc += this.nCyclesThisRun; + + if (DEBUG && this.messageEnabled(Messages8080.LOG) && msRemainsThisRun) { + this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.msEndThisRun + "ms"); + } + + this.msEndThisRun += msRemainsThisRun; + return msRemainsThisRun; + } + + /** + * addTimer(callBack) + * + * Components that want to have timers that periodically fire after some number of milliseconds call + * addTimer() to create the timer, and then setTimer() every time they want to arm it. There is currently + * no removeTimer() because these are generally used for the entire lifetime of a component. + * + * Internally, each timer entry is a preallocated Array with two entries: a cycle countdown in element [0] + * and a callback function in element [1]. A timer is initially dormant; dormant timers have a countdown + * value of -1 (although any negative number will suffice) and active timers have a non-negative value. + * + * Why not use JavaScript's setTimeout() instead? Good question. For a good answer, see setTimer() below. + * + * @this {CPU8080} + * @param {function()} callBack + * @return {number} timer index + */ + addTimer(callBack) + { + var iTimer = this.aTimers.length; + this.aTimers.push([-1, callBack]); + return iTimer; + } + + /** + * setTimer(iTimer, ms, fReset) + * + * Using the timer index from a previous addTimer() call, this sets that timer to fire after the + * specified number of milliseconds. + * + * This is preferred over JavaScript's setTimeout(), because all our timers are effectively paused when + * the CPU is paused (eg, when the Debugger halts execution). Moreover, setTimeout() handlers only run after + * runCPU() yields, which is far too granular for some components (eg, when the SerialPort tries to simulate + * interrupts at 9600 baud). + * + * Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components + * use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness, + * that's currently not the case. TODO: Fix. + * + * @this {CPU8080} + * @param {number} iTimer + * @param {number} ms (converted into a cycle countdown internally) + * @param {boolean} [fReset] (true if the timer should be reset even if already armed) + * @return {number} (number of cycles used to arm timer, or -1 if error) + */ + setTimer(iTimer, ms, fReset) + { + var nCycles = -1; + if (iTimer >= 0 && iTimer < this.aTimers.length) { + if (fReset || this.aTimers[iTimer][0] < 0) { + nCycles = this.getMSCycles(ms); + /* + * We must now confront the following problem: if the CPU is currently executing a burst of cycles, + * the number of cycles it has executed in that burst so far must NOT be charged against the cycle + * timeout we're about to set. The simplest way to resolve that is to immediately call endBurst() + * and bias the above cycle timeout by the number of cycles that the burst executed. + */ + this.aTimers[iTimer][0] = nCycles + this.endBurst(); + } + } + return nCycles; + } + + /** + * getMSCycles(ms) + * + * @this {CPU8080} + * @param {number} ms + * @return {number} number of corresponding cycles + */ + getMSCycles(ms) + { + return (this.nCyclesPerSecond * this.nCyclesMultiplier) / 1000 * ms; + } + + /** + * getBurstCycles(nCycles) + * + * Used by runCPU() to get min(nCycles,[timer cycle counts]) + * + * @this {CPU8080} + * @param {number} nCycles (number of cycles about to execute) + * @return {number} (either nCycles or less if a timer needs to fire) + */ + getBurstCycles(nCycles) + { + for (var i = this.aTimers.length - 1; i >= 0; i--) { + var timer = this.aTimers[i]; + if (timer[0] < 0) continue; + if (nCycles > timer[0]) { + nCycles = timer[0]; + } + } + return nCycles; + } + + /** + * updateTimers(nCycles) + * + * Used by runCPU() to reduce all active timer countdown values by the number of cycles just executed; + * this is the function that actually "fires" any timer(s) whose countdown has reached (or dropped below) + * zero, invoking their callback function. + * + * @this {CPU8080} + * @param {number} nCycles (number of cycles actually executed) + */ + updateTimers(nCycles) + { + for (var i = this.aTimers.length - 1; i >= 0; i--) { + var timer = this.aTimers[i]; + if (timer[0] < 0) continue; + timer[0] -= nCycles; + if (timer[0] <= 0) { + timer[0] = -1; // zero is technically an "active" value, so ensure the timer is dormant now + timer[1](); // safe to invoke the callback function now + } + } + } + + /** + * endBurst(fReset) + * + * @this {CPU8080} + * @param {boolean} [fReset] + * @return {number} (number of cycles executed in the most recent burst) + */ + endBurst(fReset) + { + var nCycles = this.nBurstCycles -= this.nStepCycles; + this.nStepCycles = 0; + if (fReset) this.nBurstCycles = 0; + return nCycles; + } + + /** + * runCPU(fUpdateFocus) + * + * @this {CPU8080} + * @param {boolean} [fUpdateFocus] is true to update Computer focus + */ + runCPU(fUpdateFocus) + { + if (!this.setBusy(true)) { + this.updateCPU(); + if (this.cmp) this.cmp.stop(Usr.getTime(), this.getCycles()); + return; + } + + this.startCPU(fUpdateFocus); + + /* + * calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally + * recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached. + */ + this.calcStartTime(); + + try { + do { + /* + * nCycles is how many cycles we WANT to run on each iteration of stepCPU(), and may be as + * HIGH as nCyclesPerYield, but it may be significantly less. getBurstCycles() will adjust + * nCycles downward if any CPU timers need to fire during the next burst. + */ + var nCycles = this.getBurstCycles(this.flags.checksum? 1 : this.nCyclesPerYield); + + /* + * Execute the burst. + */ + this.stepCPU(nCycles); + + /* + * Terminate the burst, returning the number of cycles that stepCPU() actually ran. + */ + nCycles = this.endBurst(true); + + /* + * Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU first started). + */ + this.nCyclesThisRun += nCycles; + this.nRunCycles += nCycles; + this.updateChecksum(nCycles); + + /* + * Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero. + */ + this.updateTimers(nCycles); + + this.nCyclesNextYield -= nCycles; + if (this.nCyclesNextYield <= 0) { + this.nCyclesNextYield += this.nCyclesPerYield; + if (++this.nYieldsSinceStatusUpdate >= CPU8080.YIELDS_PER_STATUS) { + if (this.cmp) this.cmp.updateStatus(); + this.nYieldsSinceStatusUpdate = 0; + } + break; + } + } while (this.flags.running); + } + catch (e) { + this.stopCPU(); + this.updateCPU(); + if (this.cmp) this.cmp.stop(Usr.getTime(), this.getCycles()); + this.setBusy(false); + this.setError(e.stack || e.message); + return; + } + + setTimeout(this.onRunTimeout, this.calcRemainingTime()); + } + + /** + * startCPU(fUpdateFocus) + * + * WARNING: Other components must use runCPU() to get the CPU running; this is a runCPU() helper function only. + * + * @param {boolean} [fUpdateFocus] + */ + startCPU(fUpdateFocus) + { + if (!this.flags.running) { + /* + * setSpeed() without a speed parameter leaves the selected speed in place, but also resets the + * cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number + * of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc + * threshold counter. + */ + this.setSpeed(); + if (this.cmp) this.cmp.start(this.msStartRun, this.getCycles()); + this.flags.running = true; + this.flags.starting = true; + if (this.chipset) this.chipset.start(); + var controlRun = this.bindings["run"]; + if (controlRun) controlRun.textContent = "Halt"; + if (this.cmp) { + this.cmp.updateStatus(true); + if (fUpdateFocus) this.cmp.updateFocus(true); + } + } + } + + /** + * stepCPU(nMinCycles) + * + * This will be implemented by the CPUState8080 component. + * + * @this {CPU8080} + * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) + * @return {number} of cycles executed; 0 indicates that the last instruction was not executed + */ + stepCPU(nMinCycles) + { + return 0; + } + + /** + * stopCPU(fComplete) + * + * For use by any component that wants to stop the CPU. + * + * This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU(); + * it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....) + * + * @this {CPU8080} + * @param {boolean} [fComplete] + */ + stopCPU(fComplete) + { + this.isBusy(true); + this.endBurst(); + this.addCycles(this.nRunCycles); + this.nRunCycles = 0; + if (this.flags.running) { + this.flags.running = false; + if (this.chipset) this.chipset.stop(); + var controlRun = this.bindings["run"]; + if (controlRun) controlRun.textContent = "Run"; + } + this.flags.complete = fComplete; + } + + /** + * updateCPU(fForce) + * + * This used to be performed at the end of every stepCPU(), but runCPU() -- which relies upon + * stepCPU() -- needed to have more control over when these updates are performed. However, for + * other callers of stepCPU(), such as the Debugger, the combination of stepCPU() + updateCPU() + * provides the old behavior. + * + * @this {CPU8080} + * @param {boolean} [fForce] (true to force a video update; used by the Debugger) + */ + updateCPU(fForce) + { + if (this.cmp) { + this.cmp.updateVideo(fForce); + this.cmp.updateStatus(fForce); + } + } + + /** + * yieldCPU() + * + * Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU + * remains in a "running" state. + * + * @this {CPU8080} + */ + yieldCPU() + { + this.endBurst(); // this will break us out of stepCPU() + this.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU() + // if (DEBUG) this.nSnapCycles = this.nBurstCycles; + /* + * The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks + * odd for those messages to show CPU state changes but for the CPU's own status display to not (ditto + * for the Video display), so I've added this call to try to keep things looking synchronized. + */ + this.updateCPU(); + } } -Component.subclass(CPU8080); - /* * Constants that control the frequency at which various updates should occur. * @@ -148,1099 +1247,4 @@ CPU8080.YIELDS_PER_STATUS = 15; // every 15 yields (ie, twice pe CPU8080.BUTTONS = ["power", "reset"]; -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {CPU8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPU8080} cpu - * @param {Debugger8080} dbg - */ -CPU8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.cmp = cmp; - this.bus = bus; - this.dbg = dbg; - - for (var i = 0; i < CPU8080.BUTTONS.length; i++) { - var control = this.bindings[CPU8080.BUTTONS[i]]; - if (control) this.cmp.setBinding(null, CPU8080.BUTTONS[i], control); - } - - /* - * Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts. - */ - this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet")); - - /* - * We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override. - */ - var sAutoStart = cmp.getMachineParm('autoStart'); - if (sAutoStart != null) { - this.flags.autoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart)); - } - - this.setReady(); -}; - -/** - * reset() - * - * @this {CPU8080} - */ -CPU8080.prototype.reset = function() -{ -}; - -/** - * save() - * - * This is a placeholder for save support (overridden by the CPUState8080 component). - * - * @this {CPU8080} - * @return {Object|null} - */ -CPU8080.prototype.save = function() -{ - return null; -}; - -/** - * restore(data) - * - * This is a placeholder for restore support (overridden by the CPUState8080 component). - * - * @this {CPU8080} - * @param {Object} data - * @return {boolean} true if restore successful, false if not - */ -CPU8080.prototype.restore = function(data) -{ - return false; -}; - -/** - * powerUp(data, fRepower) - * - * @this {CPU8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -CPU8080.prototype.powerUp = function(data, fRepower) -{ - if (!fRepower) { - if (!data || !this.restore) { - this.reset(); - } else { - this.resetCycles(); - if (!this.restore(data)) return false; - this.resetChecksum(); - } - /* - * Give the Debugger a chance to do/print something once we've powered up - */ - if (DEBUGGER && this.dbg) { - this.dbg.init(); - } else { - /* - * The Computer (this.cmp) knows if there's a Control Panel (this.cmp.panel), and the Control Panel - * knows if there's a "print" control (this.cmp.panel.controlPrint), and if there IS a "print" control - * but no debugger, the machine is probably misconfigured (most likely, the page simply neglected to - * load the Debugger component). - * - * However, we don't actually need to check all that; it's always safe use println(), regardless whether - * a Control Panel with a "print" control is present or not. - */ - this.println("No debugger detected"); - } - } - /* - * The Computer component (which is responsible for all powerDown and powerUp notifications) - * is now responsible for managing a component's fPowered flag, not us. - * - * this.flags.powered = true; - */ - this.updateCPU(); - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * @this {CPU8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -CPU8080.prototype.powerDown = function(fSave, fShutdown) -{ - /* - * The Computer component (which is responsible for all powerDown and powerUp notifications) - * is now responsible for managing a component's fPowered flag, not us. - * - * this.flags.powered = false; - */ - return fSave? this.save() : true; -}; - -/** - * autoStart() - * - * @this {CPU8080} - * @return {boolean} true if started, false if not - */ -CPU8080.prototype.autoStart = function() -{ - /* - * Start running automatically on power-up, assuming there's no Debugger and no "Run" button - */ - if (this.flags.autoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) { - /* - * We used to also set fUpdateFocus when calling runCPU(), on the assumption that in the "auto-starting" - * context, a machine without focus is like a day without sunshine, but in reality, focus should only be - * forced when the user takes some other machine-related action. - */ - this.runCPU(); - return true; - } - return false; -}; - -/** - * isPowered() - * - * @this {CPU8080} - * @return {boolean} - */ -CPU8080.prototype.isPowered = function() -{ - if (!this.flags.powered) { - this.println(this.toString() + " not powered"); - return false; - } - return true; -}; - -/** - * isRunning() - * - * @this {CPU8080} - * @return {boolean} - */ -CPU8080.prototype.isRunning = function() -{ - return this.flags.running; -}; - -/** - * getChecksum() - * - * This will be implemented by the CPUState8080 component. - * - * @this {CPU8080} - * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) - */ -CPU8080.prototype.getChecksum = function() -{ - return 0; -}; - -/** - * resetChecksum() - * - * If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the - * cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever - * the CPU is reset or restored. - * - * @this {CPU8080} - * @return {boolean} true if checksum generation enabled, false if not - */ -CPU8080.prototype.resetChecksum = function() -{ - if (this.nCyclesChecksumStart === undefined) this.nCyclesChecksumStart = 0; - if (this.nCyclesChecksumInterval === undefined) this.nCyclesChecksumInterval = -1; - if (this.nCyclesChecksumStop === undefined) this.nCyclesChecksumStop = -1; - this.flags.checksum = (this.nCyclesChecksumStart >= 0 && this.nCyclesChecksumInterval > 0); - if (this.flags.checksum) { - this.nChecksum = 0; - this.nCyclesChecksumNext = this.nCyclesChecksumStart - this.nTotalCycles; - /* - * this.nCyclesChecksumNext = this.nCyclesChecksumStart + this.nCyclesChecksumInterval - - * (this.nTotalCycles % this.nCyclesChecksumInterval); - */ - return true; - } - return false; -}; - -/** - * updateChecksum(nCycles) - * - * When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum - * number of cycles (1), effectively limiting execution to a single instruction, and then we're called with - * the exact number cycles that were actually executed. This should give us instruction-granular checksums - * at precise intervals that are 100% repeatable. - * - * @this {CPU8080} - * @param {number} nCycles - */ -CPU8080.prototype.updateChecksum = function(nCycles) -{ - if (this.flags.checksum) { - /* - * Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum - */ - var fDisplay = false; - this.nChecksum = (this.nChecksum + this.getChecksum())|0; - this.nCyclesChecksumNext -= nCycles; - if (this.nCyclesChecksumNext <= 0) { - this.nCyclesChecksumNext += this.nCyclesChecksumInterval; - fDisplay = true; - } - if (this.nCyclesChecksumStop >= 0) { - if (this.nCyclesChecksumStop <= this.getCycles()) { - this.nCyclesChecksumInterval = this.nCyclesChecksumStop = -1; - this.resetChecksum(); - this.stopCPU(); - fDisplay = true; - } - } - if (fDisplay) this.displayChecksum(); - } -}; - -/** - * displayChecksum() - * - * When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all - * checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU - * properties). - * - * @this {CPU8080} - */ -CPU8080.prototype.displayChecksum = function() -{ - this.println(this.getCycles() + " cycles: " + "checksum=" + str.toHex(this.nChecksum)); -}; - -/** - * displayValue(sLabel, nValue, cch) - * - * This is principally for displaying register values, but in reality, it can be used to display any - * numeric (hex) value bound to the given label. - * - * @this {CPU8080} - * @param {string} sLabel - * @param {number} nValue - * @param {number} cch - */ -CPU8080.prototype.displayValue = function(sLabel, nValue, cch) -{ - if (this.bindings[sLabel]) { - if (nValue === undefined) { - this.setError("Value for " + sLabel + " is invalid"); - this.stopCPU(); - } - var sVal; - if (!this.flags.running || this.flags.displayLiveRegs) { - sVal = str.toHex(nValue, cch); - } else { - sVal = "--------".substr(0, cch); - } - /* - * TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if - * we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary - * string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being - * too obsessive. - */ - if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal; - } -}; - -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {CPU8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -CPU8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - var cpu = this; - var fBound = false; - - switch (sBinding) { - case "power": - case "reset": - /* - * The "power" and "reset" buttons are functions of the entire computer, not just the CPU, - * but it's not always convenient to stick a power button in the Computer component definition, - * so we record those bindings here and pass them on to the Computer component in initBus(). - */ - this.bindings[sBinding] = control; - fBound = true; - break; - - case "run": - this.bindings[sBinding] = control; - control.onclick = function onClickRun() { - if (!cpu.cmp || !cpu.cmp.checkPower()) return; - /* - * We no longer pass true to these runCPU()/stopCPU() calls, on the theory that if the "run" - * control is visible, then the computer is probably sufficiently visible as well; the problem - * with setting fUpdateFocus to true is that it can jerk the web page around in annoying ways. - */ - if (!cpu.flags.running) - cpu.runCPU(); - else - cpu.stopCPU(); - }; - fBound = true; - break; - - case "speed": - this.bindings[sBinding] = control; - fBound = true; - break; - - case "setSpeed": - this.bindings[sBinding] = control; - control.onclick = function onClickSetSpeed() { - cpu.setSpeed(cpu.nCyclesMultiplier << 1, true); - }; - control.textContent = this.getSpeedTarget(); - fBound = true; - break; - - default: - break; - } - return fBound; -}; - -/** - * setBurstCycles(nCycles) - * - * This function is used by the ChipSet component whenever a very low timer count is set, - * in anticipation of the timer requiring an update sooner than the normal nCyclesPerYield - * period in runCPU() would normally provide. - * - * NOTE: In this context, "timer" refers to a timer chip (eg, an Intel 8253) being emulated by - * by the ChipSet component, not the timers managed by the CPU (eg, addTimer(), setTimer(), etc). - * - * @this {CPU8080} - * @param {number} nCycles is the target number of cycles to drop the current burst to - * @return {boolean} - */ -CPU8080.prototype.setBurstCycles = function(nCycles) -{ - if (this.flags.running) { - var nDelta = this.nStepCycles - nCycles; - /* - * NOTE: If nDelta is negative, we will actually be increasing nStepCycles and nBurstCycles. - * Which is OK, but if we're also taking snapshots of the cycle counts, to make sure that instruction - * costs are being properly assessed, then we need to update nSnapCycles as well. - * - * TODO: If the delta is negative, we could simply ignore the request, but we must first carefully - * consider the impact on the ChipSet timers, if any. - */ - // if (DEBUG) this.nSnapCycles -= nDelta; - this.nStepCycles -= nDelta; - this.nBurstCycles -= nDelta; - return true; - } - return false; -}; - -/** - * addCycles(nCycles, fEndStep) - * - * @this {CPU8080} - * @param {number} nCycles - * @param {boolean} [fEndStep] - */ -CPU8080.prototype.addCycles = function(nCycles, fEndStep) -{ - this.nTotalCycles += nCycles; - if (fEndStep) { - this.nBurstCycles = this.nStepCycles = 0; - } -}; - -/** - * calcCycles(fRecalc) - * - * Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst - * is driven by YIELDS_PER_SECOND (eg, 30). - * - * At the end of each burst, we subtract burst cycles from the yield cycle "threshold" counter. - * Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time to the time - * we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time remaining, - * we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU(). - * - * @this {CPU8080} - * @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent - * speed calculation (see calcSpeed). - */ -CPU8080.prototype.calcCycles = function(fRecalc) -{ - /* - * Calculate "per" yield values. - */ - var vMultiplier = 1; - if (fRecalc) { - if (this.nCyclesMultiplier > 1 && this.mhz) { - vMultiplier = (this.mhz / this.mhzDefault); - } - } - - this.msPerYield = Math.round(1000 / CPU8080.YIELDS_PER_SECOND); - this.nCyclesPerYield = Math.floor(this.nCyclesPerSecond / CPU8080.YIELDS_PER_SECOND * vMultiplier); - - /* - * And initialize "next" yield values to the "per" values. - */ - if (!fRecalc) { - this.nCyclesNextYield = this.nCyclesPerYield; - } - this.nCyclesRecalc = 0; -}; - -/** - * getCycles(fScaled) - * - * getCycles() returns the number of cycles executed so far. Note that we can be called after - * runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(), - * so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles), - * less the number of cycles it has yet to run (nStepCycles). - * - * nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also - * have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and - * nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on - * getCycles() returning steadily increasing values should also be prepared for a reset at any time. - * - * @this {CPU8080} - * @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1 - * @return {number} - */ -CPU8080.prototype.getCycles = function(fScaled) -{ - var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles; - if (fScaled && this.nCyclesMultiplier > 1 && this.mhz > this.mhzDefault) { - /* - * We could scale the current cycle count by the current effective speed (this.mhz); eg: - * - * nCycles = Math.round(nCycles / (this.mhz / this.mhzDefault)); - * - * but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller - * fluctuations after each burst of instructions that runCPU() executes. - * - * Alternatively, we can scale the cycle count by the multiplier, which is good in that the - * multiplier doesn't vary once the user changes it, but a potential downside is that the - * multiplier might be set too high, resulting in a target speed that's higher than the effective - * speed is able to reach. - * - * Also, if multipliers were always limited to a power-of-two, then this could be calculated - * with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger - * interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's - * XML file). - */ - nCycles = Math.round(nCycles / this.nCyclesMultiplier); - } - return nCycles; -}; - -/** - * getCyclesPerSecond() - * - * This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine) - * - * @this {CPU8080} - * @return {number} - */ -CPU8080.prototype.getCyclesPerSecond = function() -{ - return this.nCyclesPerSecond; -}; - -/** - * resetCycles() - * - * Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp(). - * It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(), - * which in turn assumes that all the cycle counts have been initialized to sensible values. - * - * @this {CPU8080} - */ -CPU8080.prototype.resetCycles = function() -{ - this.mhz = 0; - this.nYieldsSinceStatusUpdate = 0; - this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = 0; - this.resetChecksum(); - this.setSpeed(1); -}; - -/** - * getSpeed() - * - * @this {CPU8080} - * @return {number} the current speed multiplier - */ -CPU8080.prototype.getSpeed = function() -{ - return this.nCyclesMultiplier; -}; - -/** - * getSpeedCurrent() - * - * @this {CPU8080} - * @return {string} the current speed, in mhz, as a string formatted to two decimal places - */ -CPU8080.prototype.getSpeedCurrent = function() -{ - /* - * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? - */ - return ((this.flags.running && this.mhz)? (this.mhz.toFixed(2) + "Mhz") : "Stopped"); -}; - -/** - * getSpeedTarget() - * - * @this {CPU8080} - * @return {string} the target speed, in mhz, as a string formatted to two decimal places - */ -CPU8080.prototype.getSpeedTarget = function() -{ - /* - * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? - */ - return this.mhzTarget.toFixed(2) + "Mhz"; -}; - -/** - * setSpeed(nMultiplier, fUpdateFocus) - * - * NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point. - * - * @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset, - * so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls - * setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting). - * - * @this {CPU8080} - * @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high) - * @param {boolean} [fUpdateFocus] is true to update Computer focus - * @return {boolean} true if successful, false if not - */ -CPU8080.prototype.setSpeed = function(nMultiplier, fUpdateFocus) -{ - var fSuccess = false; - if (nMultiplier !== undefined) { - /* - * If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1). - */ - if (this.mhz / this.mhzTarget < 0.8) { - nMultiplier = 1; - } else { - fSuccess = true; - } - this.nCyclesMultiplier = nMultiplier; - var mhz = this.mhzDefault * this.nCyclesMultiplier; - if (this.mhzTarget != mhz) { - this.mhzTarget = mhz; - var sSpeed = this.getSpeedTarget(); - var controlSpeed = this.bindings["setSpeed"]; - if (controlSpeed) controlSpeed.textContent = sSpeed; - this.println("target speed: " + sSpeed); - } - if (fUpdateFocus && this.cmp) this.cmp.updateFocus(); - } - this.addCycles(this.nRunCycles); - this.nRunCycles = 0; - this.msStartRun = usr.getTime(); - this.msEndThisRun = 0; - this.calcCycles(); - return fSuccess; -}; - -/** - * calcSpeed(nCycles, msElapsed) - * - * @this {CPU8080} - * @param {number} nCycles - * @param {number} msElapsed - */ -CPU8080.prototype.calcSpeed = function(nCycles, msElapsed) -{ - if (msElapsed) { - this.mhz = Math.round(nCycles / (msElapsed * 10)) / 100; - if (msElapsed >= 86400000) { - this.nTotalCycles = 0; - this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow - } - } -}; - -/** - * calcStartTime() - * - * @this {CPU8080} - */ -CPU8080.prototype.calcStartTime = function() -{ - if (this.nCyclesRecalc >= this.nCyclesPerSecond) { - this.calcCycles(true); - } - this.nCyclesThisRun = 0; - this.msStartThisRun = usr.getTime(); - - /* - * Try to detect situations where the browser may have throttled us, such as when the user switches - * to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks - * to roughly one per second. - * - * Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled, - * but there can still be enough of a lag between the callbacks that CPU speed will be noticeably - * erratic if we don't compensate for it here. - * - * We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the - * previous run and includes any requested sleep time) is comparable to the current msStartThisRun; - * if the delta is significant, we compensate by bumping msStartRun forward by that delta. - * - * This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called - * whenever the CPU starts running again -- zeroes msEndThisRun. - * - * This also won't do anything about other internal delays; for example, Debugger message() calls. - * By the time the message() function has called yieldCPU(), the cost of the message has already been - * incurred, so it will be end up being charged against the instruction(s) that triggered it. - * - * TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun - * "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability - * to hit its target speed, since you would expect any instruction that displays a message to be an - * EXTREMELY slow instruction. - */ - if (this.msEndThisRun) { - var msDelta = this.msStartThisRun - this.msEndThisRun; - if (msDelta > this.msPerYield) { - if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms"); - this.msStartRun += msDelta; - /* - * Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just - * in case, I make absolutely sure it cannot happen, since doing so could result in negative - * speed calculations. - */ - this.assert(this.msStartRun <= this.msStartThisRun); - if (this.msStartRun > this.msStartThisRun) { - this.msStartRun = this.msStartThisRun; - } - } - } -}; - -/** - * calcRemainingTime() - * - * @this {CPU8080} - * @return {number} - */ -CPU8080.prototype.calcRemainingTime = function() -{ - this.msEndThisRun = usr.getTime(); - - var msYield = this.msPerYield; - if (this.nCyclesThisRun) { - /* - * Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU - * now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then - * the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed, - * and so applying that percentage to msPerYield should give us a better estimate of work vs. time. - */ - msYield = Math.round(msYield * this.nCyclesThisRun / this.nCyclesPerYield); - } - - var msElapsedThisRun = this.msEndThisRun - this.msStartThisRun; - var msRemainsThisRun = msYield - msElapsedThisRun; - - /* - * We could pass only "this run" results to calcSpeed(): - * - * nCycles = this.nCyclesThisRun; - * msElapsed = msElapsedThisRun; - * - * but it seems preferable to use longer time periods and hopefully get a more accurate speed. - * - * Also, if msRemainsThisRun >= 0 && this.nCyclesMultiplier == 1, we could pass these results instead: - * - * nCycles = this.nCyclesThisRun; - * msElapsed = this.msPerYield; - * - * to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations. - */ - var nCycles = this.nRunCycles; - var msElapsed = this.msEndThisRun - this.msStartRun; - - if (MAXDEBUG && msRemainsThisRun < 0 && this.nCyclesMultiplier > 1) { - this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)"); - } - - this.calcSpeed(nCycles, msElapsed); - - if (msRemainsThisRun < 0 || this.mhz < this.mhzTarget) { - /* - * Try "throwing out" the effects of large anomalies, by moving the overall run start time up; - * ordinarily, this should only happen when the someone is using an external Debugger or some other - * tool or feature that is interfering with our overall execution. - */ - if (msRemainsThisRun < -1000) { - this.msStartRun -= msRemainsThisRun; - } - /* - * If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate - * nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the - * simulation is at least usable. - */ - msRemainsThisRun = 0; - } - - /* - * Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(), - * it'll be ready to decide if calcCycles() should be called again. - */ - this.nCyclesRecalc += this.nCyclesThisRun; - - if (DEBUG && this.messageEnabled(Messages8080.LOG) && msRemainsThisRun) { - this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.msEndThisRun + "ms"); - } - - this.msEndThisRun += msRemainsThisRun; - return msRemainsThisRun; -}; - -/** - * addTimer(callBack) - * - * Components that want to have timers that periodically fire after some number of milliseconds call - * addTimer() to create the timer, and then setTimer() every time they want to arm it. There is currently - * no removeTimer() because these are generally used for the entire lifetime of a component. - * - * Internally, each timer entry is a preallocated Array with two entries: a cycle countdown in element [0] - * and a callback function in element [1]. A timer is initially dormant; dormant timers have a countdown - * value of -1 (although any negative number will suffice) and active timers have a non-negative value. - * - * Why not use JavaScript's setTimeout() instead? Good question. For a good answer, see setTimer() below. - * - * @this {CPU8080} - * @param {function()} callBack - * @return {number} timer index - */ -CPU8080.prototype.addTimer = function(callBack) -{ - var iTimer = this.aTimers.length; - this.aTimers.push([-1, callBack]); - return iTimer; -}; - -/** - * setTimer(iTimer, ms, fReset) - * - * Using the timer index from a previous addTimer() call, this sets that timer to fire after the - * specified number of milliseconds. - * - * This is preferred over JavaScript's setTimeout(), because all our timers are effectively paused when - * the CPU is paused (eg, when the Debugger halts execution). Moreover, setTimeout() handlers only run after - * runCPU() yields, which is far too granular for some components (eg, when the SerialPort tries to simulate - * interrupts at 9600 baud). - * - * Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components - * use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness, - * that's currently not the case. TODO: Fix. - * - * @this {CPU8080} - * @param {number} iTimer - * @param {number} ms (converted into a cycle countdown internally) - * @param {boolean} [fReset] (true if the timer should be reset even if already armed) - * @return {number} (number of cycles used to arm timer, or -1 if error) - */ -CPU8080.prototype.setTimer = function(iTimer, ms, fReset) -{ - var nCycles = -1; - if (iTimer >= 0 && iTimer < this.aTimers.length) { - if (fReset || this.aTimers[iTimer][0] < 0) { - nCycles = this.getMSCycles(ms); - /* - * We must now confront the following problem: if the CPU is currently executing a burst of cycles, - * the number of cycles it has executed in that burst so far must NOT be charged against the cycle - * timeout we're about to set. The simplest way to resolve that is to immediately call endBurst() - * and bias the above cycle timeout by the number of cycles that the burst executed. - */ - this.aTimers[iTimer][0] = nCycles + this.endBurst(); - } - } - return nCycles; -}; - -/** - * getMSCycles(ms) - * - * @this {CPU8080} - * @param {number} ms - * @return {number} number of corresponding cycles - */ -CPU8080.prototype.getMSCycles = function(ms) -{ - return (this.nCyclesPerSecond * this.nCyclesMultiplier) / 1000 * ms; -}; - -/** - * getBurstCycles(nCycles) - * - * Used by runCPU() to get min(nCycles,[timer cycle counts]) - * - * @this {CPU8080} - * @param {number} nCycles (number of cycles about to execute) - * @return {number} (either nCycles or less if a timer needs to fire) - */ -CPU8080.prototype.getBurstCycles = function(nCycles) -{ - for (var i = this.aTimers.length - 1; i >= 0; i--) { - var timer = this.aTimers[i]; - if (timer[0] < 0) continue; - if (nCycles > timer[0]) { - nCycles = timer[0]; - } - } - return nCycles; -}; - -/** - * updateTimers(nCycles) - * - * Used by runCPU() to reduce all active timer countdown values by the number of cycles just executed; - * this is the function that actually "fires" any timer(s) whose countdown has reached (or dropped below) - * zero, invoking their callback function. - * - * @this {CPU8080} - * @param {number} nCycles (number of cycles actually executed) - */ -CPU8080.prototype.updateTimers = function(nCycles) -{ - for (var i = this.aTimers.length - 1; i >= 0; i--) { - var timer = this.aTimers[i]; - if (timer[0] < 0) continue; - timer[0] -= nCycles; - if (timer[0] <= 0) { - timer[0] = -1; // zero is technically an "active" value, so ensure the timer is dormant now - timer[1](); // safe to invoke the callback function now - } - } -}; - -/** - * endBurst(fReset) - * - * @this {CPU8080} - * @param {boolean} [fReset] - * @return {number} (number of cycles executed in the most recent burst) - */ -CPU8080.prototype.endBurst = function(fReset) -{ - var nCycles = this.nBurstCycles -= this.nStepCycles; - this.nStepCycles = 0; - if (fReset) this.nBurstCycles = 0; - return nCycles; -}; - -/** - * runCPU(fUpdateFocus) - * - * @this {CPU8080} - * @param {boolean} [fUpdateFocus] is true to update Computer focus - */ -CPU8080.prototype.runCPU = function(fUpdateFocus) -{ - if (!this.setBusy(true)) { - this.updateCPU(); - if (this.cmp) this.cmp.stop(usr.getTime(), this.getCycles()); - return; - } - - this.startCPU(fUpdateFocus); - - /* - * calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally - * recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached. - */ - this.calcStartTime(); - - try { - do { - /* - * nCycles is how many cycles we WANT to run on each iteration of stepCPU(), and may be as - * HIGH as nCyclesPerYield, but it may be significantly less. getBurstCycles() will adjust - * nCycles downward if any CPU timers need to fire during the next burst. - */ - var nCycles = this.getBurstCycles(this.flags.checksum? 1 : this.nCyclesPerYield); - - /* - * Execute the burst. - */ - this.stepCPU(nCycles); - - /* - * Terminate the burst, returning the number of cycles that stepCPU() actually ran. - */ - nCycles = this.endBurst(true); - - /* - * Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU first started). - */ - this.nCyclesThisRun += nCycles; - this.nRunCycles += nCycles; - this.updateChecksum(nCycles); - - /* - * Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero. - */ - this.updateTimers(nCycles); - - this.nCyclesNextYield -= nCycles; - if (this.nCyclesNextYield <= 0) { - this.nCyclesNextYield += this.nCyclesPerYield; - if (++this.nYieldsSinceStatusUpdate >= CPU8080.YIELDS_PER_STATUS) { - if (this.cmp) this.cmp.updateStatus(); - this.nYieldsSinceStatusUpdate = 0; - } - break; - } - } while (this.flags.running); - } - catch (e) { - this.stopCPU(); - this.updateCPU(); - if (this.cmp) this.cmp.stop(usr.getTime(), this.getCycles()); - this.setBusy(false); - this.setError(e.stack || e.message); - return; - } - - setTimeout(this.onRunTimeout, this.calcRemainingTime()); -}; - -/** - * startCPU(fUpdateFocus) - * - * WARNING: Other components must use runCPU() to get the CPU running; this is a runCPU() helper function only. - * - * @param {boolean} [fUpdateFocus] - */ -CPU8080.prototype.startCPU = function(fUpdateFocus) -{ - if (!this.flags.running) { - /* - * setSpeed() without a speed parameter leaves the selected speed in place, but also resets the - * cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number - * of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc - * threshold counter. - */ - this.setSpeed(); - if (this.cmp) this.cmp.start(this.msStartRun, this.getCycles()); - this.flags.running = true; - this.flags.starting = true; - if (this.chipset) this.chipset.start(); - var controlRun = this.bindings["run"]; - if (controlRun) controlRun.textContent = "Halt"; - if (this.cmp) { - this.cmp.updateStatus(true); - if (fUpdateFocus) this.cmp.updateFocus(true); - } - } -}; - -/** - * stepCPU(nMinCycles) - * - * This will be implemented by the CPUState8080 component. - * - * @this {CPU8080} - * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) - * @return {number} of cycles executed; 0 indicates that the last instruction was not executed - */ -CPU8080.prototype.stepCPU = function(nMinCycles) -{ - return 0; -}; - -/** - * stopCPU(fComplete) - * - * For use by any component that wants to stop the CPU. - * - * This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU(); - * it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....) - * - * @this {CPU8080} - * @param {boolean} [fComplete] - */ -CPU8080.prototype.stopCPU = function(fComplete) -{ - this.isBusy(true); - this.endBurst(); - this.addCycles(this.nRunCycles); - this.nRunCycles = 0; - if (this.flags.running) { - this.flags.running = false; - if (this.chipset) this.chipset.stop(); - var controlRun = this.bindings["run"]; - if (controlRun) controlRun.textContent = "Run"; - } - this.flags.complete = fComplete; -}; - -/** - * updateCPU(fForce) - * - * This used to be performed at the end of every stepCPU(), but runCPU() -- which relies upon - * stepCPU() -- needed to have more control over when these updates are performed. However, for - * other callers of stepCPU(), such as the Debugger, the combination of stepCPU() + updateCPU() - * provides the old behavior. - * - * @this {CPU8080} - * @param {boolean} [fForce] (true to force a video update; used by the Debugger) - */ -CPU8080.prototype.updateCPU = function(fForce) -{ - if (this.cmp) { - this.cmp.updateVideo(fForce); - this.cmp.updateStatus(fForce); - } -}; - -/** - * yieldCPU() - * - * Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU - * remains in a "running" state. - * - * @this {CPU8080} - */ -CPU8080.prototype.yieldCPU = function() -{ - this.endBurst(); // this will break us out of stepCPU() - this.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU() - // if (DEBUG) this.nSnapCycles = this.nBurstCycles; - /* - * The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks - * odd for those messages to show CPU state changes but for the CPU's own status display to not (ditto - * for the Video display), so I've added this call to try to keep things looking synchronized. - */ - this.updateCPU(); -}; - -if (NODE) module.exports = CPU8080; +module.exports = CPU8080; diff --git a/modules/pc8080/lib/cpudef.js b/modules/pc8080/lib/cpudef.js index a8afb8958..53d81f09f 100644 --- a/modules/pc8080/lib/cpudef.js +++ b/modules/pc8080/lib/cpudef.js @@ -124,4 +124,4 @@ CPUDef8080.PS.RESULT = (CPUDef8080.PS.CF | CPUDef8080.PS.PF | CPUDef8080.PS */ CPUDef8080.PS.SET = (CPUDef8080.PS.BIT1); -if (NODE) module.exports = CPUDef8080; +module.exports = CPUDef8080; diff --git a/modules/pc8080/lib/cpuops.js b/modules/pc8080/lib/cpuops.js index 41837f595..9ed573b66 100644 --- a/modules/pc8080/lib/cpuops.js +++ b/modules/pc8080/lib/cpuops.js @@ -28,11 +28,8 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var CPUDef8080 = require("./CPUDef"); - var Messages8080= require("./messages"); -} +var CPUDef8080 = require("./CPUDef"); +var Messages8080 = require("./messages"); /** * op=0x00 (NOP) diff --git a/modules/pc8080/lib/cpustate.js b/modules/pc8080/lib/cpustate.js index 65f953a95..938b5480d 100644 --- a/modules/pc8080/lib/cpustate.js +++ b/modules/pc8080/lib/cpustate.js @@ -28,1146 +28,1150 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var CPUDef8080 = require("./cpudef"); - var CPU8080 = require("./cpu"); - var Messages8080= require("./messages"); - var Memory8080 = require("./memory"); -} +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var CPUDef8080 = require("./cpudef"); +var CPU8080 = require("./cpu"); +var Memory8080 = require("./memory"); +var Messages8080 = require("./messages"); /** - * CPUState8080(parmsCPU) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The CPUState8080 class uses the following (parmsCPU) properties: - * - * model: a number (eg, 8080) that should match one of the CPUDef8080.MODEL_* values - * - * This extends the CPU class and passes any remaining parmsCPU properties to the CPU class - * constructor, along with a default speed (cycles per second) based on the specified (or default) - * CPU model number. - * - * The CPUState8080 class was initially written to simulate a 8080 microprocessor, although over time - * it may evolved to support other microprocessors (eg, the Zilog Z80). - * - * @constructor - * @extends CPU8080 - * @param {Object} parmsCPU + * @unrestricted */ -function CPUState8080(parmsCPU) -{ - this.model = +parmsCPU['model'] || CPUDef8080.MODEL_8080; - - var nCyclesDefault = 0; - switch(this.model) { - case CPUDef8080.MODEL_8080: - default: - nCyclesDefault = 1000000; - break; - } - - CPU8080.call(this, parmsCPU, nCyclesDefault); - - /* - * Initialize processor operation to match the requested model +class CPUState8080 extends CPU8080 { + /** + * CPUState8080(parmsCPU) + * + * The CPUState8080 class uses the following (parmsCPU) properties: + * + * model: a number (eg, 8080) that should match one of the CPUDef8080.MODEL_* values + * + * This extends the CPU class and passes any remaining parmsCPU properties to the CPU class + * constructor, along with a default speed (cycles per second) based on the specified (or default) + * CPU model number. + * + * The CPUState8080 class was initially written to simulate a 8080 microprocessor, although over time + * it may evolved to support other microprocessors (eg, the Zilog Z80). + * + * @this {CPUState8080} + * @param {Object} parmsCPU */ - this.initProcessor(); - - /* - * A variety of stepCPU() state variables that don't strictly need to be initialized before the first - * stepCPU() call, but it's good form to do so. - */ - this.resetCycles(); - this.flags.complete = this.flags.debugCheck = false; - - /* - * If there are no live registers to display, then updateStatus() can skip a bit.... - */ - this.cLiveRegs = 0; - - /* - * Array of halt handlers, if any (see addHaltCheck) - */ - this.afnHalt = []; - this.addrReset = 0x0000; - - /* - * This initial resetRegs() call is important to create all the registers, so that if/when we call restore(), - * it will have something to fill in. - */ - this.resetRegs(); -} - -Component.subclass(CPUState8080, CPU8080); - -/** - * addHaltCheck(fn) - * - * Records a function that will be called during HLT opcode processing. - * - * @this {CPUState8080} - * @param {function(number)} fn - */ -CPUState8080.prototype.addHaltCheck = function(fn) -{ - this.afnHalt.push(fn); -}; - -/** - * initProcessor() - * - * Interestingly, if I dynamically generate aOps as an array of functions bound to "this", using the bind() - * method, overall performance is worse. You would think that eliminating the need to use the call() method - * on every opcode function invocation would be helpful, but it's not. I'm not sure exactly why yet; perhaps - * a Closure Compiler optimization is defeated when generating the function array at run-time instead of at - * compile-time. - * - * @this {CPUState8080} - */ -CPUState8080.prototype.initProcessor = function() -{ - this.aOps = CPUDef8080.aOps8080; -}; - -/** - * reset() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.reset = function() -{ - if (this.flags.running) this.stopCPU(); - this.resetRegs(); - this.resetCycles(); - this.clearError(); // clear any fatal error/exception that setError() may have flagged - this.parent.reset.call(this); -}; - -/** - * resetRegs() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.resetRegs = function() -{ - this.regA = 0; - this.regB = 0; - this.regC = 0; - this.regD = 0; - this.regE = 0; - this.regH = 0; - this.regL = 0; - this.setSP(0); - this.setPC(this.addrReset); - - /* - * This resets the Processor Status flags (regPS), along with all the internal "result registers". - */ - this.setPS(0); - - /* - * intFlags contains some internal states we use to indicate whether a hardware interrupt (INTFLAG.INTR) or - * Trap software interrupt (INTR.TRAP) has been requested, as well as when we're in a "HLT" state (INTFLAG.HALT) - * that requires us to wait for a hardware interrupt (INTFLAG.INTR) before continuing execution. - */ - this.intFlags = CPUDef8080.INTFLAG.NONE; -}; - -/** - * setReset(addr) - * - * @this {CPUState8080} - * @param {number} addr - */ -CPUState8080.prototype.setReset = function(addr) -{ - this.addrReset = addr; - this.setPC(addr); -}; - -/** - * getChecksum() - * - * @this {CPUState8080} - * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) - */ -CPUState8080.prototype.getChecksum = function() -{ - var sum = (this.regA + this.regB + this.regC + this.regD + this.regE + this.regH + this.regL)|0; - sum = (sum + this.getSP() + this.getPC() + this.getPS())|0; - return sum; -}; - -/** - * save() - * - * This implements save support for the CPUState8080 component. - * - * @this {CPUState8080} - * @return {Object|null} - */ -CPUState8080.prototype.save = function() -{ - var state = new State(this); - state.set(0, [this.regA, this.regB, this.regC, this.regD, this.regE, this.regH, this.regL, this.getSP(), this.getPC(), this.getPS()]); - state.set(1, [this.intFlags, this.nTotalCycles, this.getSpeed()]); - state.set(2, this.bus.saveMemory()); - return state.data(); -}; - -/** - * restore(data) - * - * This implements restore support for the CPUState8080 component. - * - * @this {CPUState8080} - * @param {Object} data - * @return {boolean} true if restore successful, false if not - */ -CPUState8080.prototype.restore = function(data) -{ - var a = data[0]; - this.regA = a[0]; - this.regB = a[1]; - this.regC = a[2]; - this.regD = a[3]; - this.regE = a[4]; - this.regH = a[5]; - this.regL = a[6]; - this.setSP(a[7]); - this.setPC(a[8]); - this.setPS(a[9]); - a = data[1]; - this.intFlags = a[0]; - this.nTotalCycles = a[1]; - this.setSpeed(a[3]); - return this.bus.restoreMemory(data[2]); -}; - -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {CPUState8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "AX") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -CPUState8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - var fBound = false; - switch (sBinding) { - case "A": - case "B": - case "C": - case "BC": - case "D": - case "E": - case "DE": - case "H": - case "L": - case "HL": - case "SP": - case "PC": - case "PS": - case "IF": - case "SF": - case "ZF": - case "AF": - case "PF": - case "CF": - this.bindings[sBinding] = control; - this.cLiveRegs++; - fBound = true; - break; - default: - fBound = this.parent.setBinding.call(this, sHTMLType, sBinding, control); - break; - } - return fBound; -}; - -/** - * getBC() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getBC = function() -{ - return (this.regB << 8) | this.regC; -}; - -/** - * setBC(w) - * - * @this {CPUState8080} - * @param {number} w - */ -CPUState8080.prototype.setBC = function(w) -{ - this.regB = (w >> 8) & 0xff; - this.regC = w & 0xff; -}; - -/** - * getDE() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getDE = function() -{ - return (this.regD << 8) | this.regE; -}; - -/** - * setDE(w) - * - * @this {CPUState8080} - * @param {number} w - */ -CPUState8080.prototype.setDE = function(w) -{ - this.regD = (w >> 8) & 0xff; - this.regE = w & 0xff; -}; - -/** - * getHL() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getHL = function() -{ - return (this.regH << 8) | this.regL; -}; - -/** - * setHL(w) - * - * @this {CPUState8080} - * @param {number} w - */ -CPUState8080.prototype.setHL = function(w) -{ - this.regH = (w >> 8) & 0xff; - this.regL = w & 0xff; -}; - -/** - * getSP() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getSP = function() -{ - return this.regSP; -}; - -/** - * setSP(off) - * - * @this {CPUState8080} - * @param {number} off - */ -CPUState8080.prototype.setSP = function(off) -{ - this.regSP = off & 0xffff; -}; - -/** - * getPC() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getPC = function() -{ - return this.regPC; -}; - -/** - * offPC() - * - * @this {CPUState8080} - * @param {number} off - * @return {number} - */ -CPUState8080.prototype.offPC = function(off) -{ - return (this.regPC + off) & 0xffff; -}; - -/** - * setPC(off) - * - * @this {CPUState8080} - * @param {number} off - */ -CPUState8080.prototype.setPC = function(off) -{ - this.regPC = off & 0xffff; -}; - -/** - * clearCF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearCF = function() -{ - this.resultZeroCarry &= 0xff; -}; - -/** - * getCF() - * - * @this {CPUState8080} - * @return {number} 0 or 1 (CPUDef8080.PS.CF) - */ -CPUState8080.prototype.getCF = function() -{ - return (this.resultZeroCarry & 0x100)? CPUDef8080.PS.CF : 0; -}; - -/** - * setCF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setCF = function() -{ - this.resultZeroCarry |= 0x100; -}; - -/** - * updateCF(CF) - * - * @this {CPUState8080} - * @param {number} CF (0x000 or 0x100) - */ -CPUState8080.prototype.updateCF = function(CF) -{ - this.resultZeroCarry = (this.resultZeroCarry & 0xff) | CF; -}; - -/** - * clearPF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearPF = function() -{ - if (this.getPF()) this.resultParitySign ^= 0x1; -}; - -/** - * getPF() - * - * @this {CPUState8080} - * @return {number} 0 or CPUDef8080.PS.PF - */ -CPUState8080.prototype.getPF = function() -{ - return (CPUDef8080.PARITY[this.resultParitySign & 0xff])? CPUDef8080.PS.PF : 0; -}; - -/** - * setPF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setPF = function() -{ - if (!this.getPF()) this.resultParitySign ^= 0x1; -}; - -/** - * clearAF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearAF = function() -{ - this.resultAuxOverflow = (this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); -}; - -/** - * getAF() - * - * @this {CPUState8080} - * @return {number} 0 or CPUDef8080.PS.AF - */ -CPUState8080.prototype.getAF = function() -{ - return ((this.resultParitySign ^ this.resultAuxOverflow) & 0x10)? CPUDef8080.PS.AF : 0; -}; - -/** - * setAF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setAF = function() -{ - this.resultAuxOverflow = (~this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); -}; - -/** - * clearZF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearZF = function() -{ - this.resultZeroCarry |= 0xff; -}; - -/** - * getZF() - * - * @this {CPUState8080} - * @return {number} 0 or CPUDef8080.PS.ZF - */ -CPUState8080.prototype.getZF = function() -{ - return (this.resultZeroCarry & 0xff)? 0 : CPUDef8080.PS.ZF; -}; - -/** - * setZF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setZF = function() -{ - this.resultZeroCarry &= ~0xff; -}; - -/** - * clearSF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearSF = function() -{ - if (this.getSF()) this.resultParitySign ^= 0xc0; -}; - -/** - * getSF() - * - * @this {CPUState8080} - * @return {number} 0 or CPUDef8080.PS.SF - */ -CPUState8080.prototype.getSF = function() -{ - return (this.resultParitySign & 0x80)? CPUDef8080.PS.SF : 0; -}; - -/** - * setSF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setSF = function() -{ - if (!this.getSF()) this.resultParitySign ^= 0xc0; -}; - -/** - * clearIF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.clearIF = function() -{ - this.regPS &= ~CPUDef8080.PS.IF; -}; - -/** - * getIF() - * - * @this {CPUState8080} - * @return {number} 0 or CPUDef8080.PS.IF - */ -CPUState8080.prototype.getIF = function() -{ - return (this.regPS & CPUDef8080.PS.IF); -}; - -/** - * setIF() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.setIF = function() -{ - this.regPS |= CPUDef8080.PS.IF; -}; - -/** - * getPS() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getPS = function() -{ - return (this.regPS & ~CPUDef8080.PS.RESULT) | (this.getSF() | this.getZF() | this.getAF() | this.getPF() | this.getCF()); -}; - -/** - * setPS(regPS) - * - * @this {CPUState8080} - * @param {number} regPS - */ -CPUState8080.prototype.setPS = function(regPS) -{ - this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = 0; - if (regPS & CPUDef8080.PS.CF) this.resultZeroCarry |= 0x100; - if (!(regPS & CPUDef8080.PS.PF)) this.resultParitySign |= 0x01; - if (regPS & CPUDef8080.PS.AF) this.resultAuxOverflow |= 0x10; - if (!(regPS & CPUDef8080.PS.ZF)) this.resultZeroCarry |= 0xff; - if (regPS & CPUDef8080.PS.SF) this.resultParitySign ^= 0xc0; - this.regPS = (this.regPS & ~(CPUDef8080.PS.RESULT | CPUDef8080.PS.INTERNAL)) | (regPS & CPUDef8080.PS.INTERNAL) | CPUDef8080.PS.SET; - Component.assert((regPS & CPUDef8080.PS.RESULT) == (this.getPS() & CPUDef8080.PS.RESULT)); -}; - -/** - * getPSW() - * - * @this {CPUState8080} - * @return {number} - */ -CPUState8080.prototype.getPSW = function() -{ - return (this.getPS() & CPUDef8080.PS.MASK) | (this.regA << 8); -}; - -/** - * setPSW(w) - * - * @this {CPUState8080} - * @param {number} w - */ -CPUState8080.prototype.setPSW = function(w) -{ - this.setPS((w & CPUDef8080.PS.MASK) | (this.regPS & ~CPUDef8080.PS.MASK)); - this.regA = w >> 8; -}; - -/** - * addByte(src) - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA + src - */ -CPUState8080.prototype.addByte = function(src) -{ - this.resultAuxOverflow = this.regA ^ src; - return this.resultParitySign = (this.resultZeroCarry = this.regA + src) & 0xff; -}; - -/** - * addByteCarry(src) - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA + src + carry - */ -CPUState8080.prototype.addByteCarry = function(src) -{ - this.resultAuxOverflow = this.regA ^ src; - return this.resultParitySign = (this.resultZeroCarry = this.regA + src + ((this.resultZeroCarry & 0x100)? 1 : 0)) & 0xff; -}; - -/** - * andByte(src) - * - * Ordinarily, one would expect the Auxiliary Carry flag (AF) to be clear after this operation, - * but apparently the 8080 will set AF if bit 3 in either operand is set. - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA & src - */ -CPUState8080.prototype.andByte = function(src) -{ - this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = this.regA & src; - if ((this.regA | src) & 0x8) this.resultAuxOverflow ^= 0x10; // set AF by inverting bit 4 in resultAuxOverflow - return this.resultZeroCarry; -}; - -/** - * decByte(b) - * - * We perform this operation using 8-bit two's complement arithmetic, by negating and then adding - * the implied src of 1. This appears to mimic how the 8080 manages the Auxiliary Carry flag (AF). - * - * @this {CPUState8080} - * @param {number} b - * @return {number} - */ -CPUState8080.prototype.decByte = function(b) -{ - this.resultAuxOverflow = b ^ 0xff; - b = this.resultParitySign = (b + 0xff) & 0xff; - this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b; - return b; -}; - -/** - * incByte(b) - * - * @this {CPUState8080} - * @param {number} b - * @return {number} - */ -CPUState8080.prototype.incByte = function(b) -{ - this.resultAuxOverflow = b; - b = this.resultParitySign = (b + 1) & 0xff; - this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b; - return b; -}; - -/** - * orByte(src) - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA | src - */ -CPUState8080.prototype.orByte = function(src) -{ - return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA | src; -}; - -/** - * subByte(src) - * - * We perform this operation using 8-bit two's complement arithmetic, by inverting src, adding - * src + 1, and then inverting the resulting carry (resultZeroCarry ^ 0x100). This appears to mimic - * how the 8080 manages the Auxiliary Carry flag (AF). - * - * This function is also used as a cmpByte() function; compare instructions simply ignore the - * return value. - * - * Example: A=66, SUI $10 - * - * If we created the two's complement of 0x10 by negating it, there would just be one addition: - * - * 0110 0110 (0x66) - * + 1111 0000 (0xF0) (ie, -0x10) - * --------- - * 1 0101 0110 (0x56) - * - * But in order to mimic the 8080's AF flag, we must perform the two's complement of src in two steps, - * inverting it before the add, and then incrementing after the add; eg: - * - * 0110 0110 (0x66) - * + 1110 1111 (0xEF) (ie, ~0x10) - * --------- - * 1 0101 0101 (0x55) - * + 0000 0001 (0x01) - * --------- - * 1 0101 0110 (0x56) - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA - src - */ -CPUState8080.prototype.subByte = function(src) -{ - src ^= 0xff; - this.resultAuxOverflow = this.regA ^ src; - return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + 1) ^ 0x100) & 0xff; -}; - -/** - * subByteBorrow(src) - * - * We perform this operation using 8-bit two's complement arithmetic, using logic similar to subByte(), - * but changing the final increment to a conditional increment, because if the Carry flag (CF) is set, then - * we don't need to perform the increment at all. - * - * This mimics the behavior of subByte() when the Carry flag (CF) is clear, and hopefully also mimics how the - * 8080 manages the Auxiliary Carry flag (AF) when the Carry flag (CF) is set. - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA - src - carry - */ -CPUState8080.prototype.subByteBorrow = function(src) -{ - src ^= 0xff; - this.resultAuxOverflow = this.regA ^ src; - return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + ((this.resultZeroCarry & 0x100)? 0 : 1)) ^ 0x100) & 0xff; -}; - -/** - * xorByte(src) - * - * @this {CPUState8080} - * @param {number} src - * @return {number} regA ^ src - */ -CPUState8080.prototype.xorByte = function(src) -{ - return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA ^ src; -}; - -/** - * getByte(addr) - * - * @this {CPUState8080} - * @param {number} addr is a linear address - * @return {number} byte (8-bit) value at that address - */ -CPUState8080.prototype.getByte = function(addr) -{ - return this.bus.getByte(addr); -}; - -/** - * getWord(addr) - * - * @this {CPUState8080} - * @param {number} addr is a linear address - * @return {number} word (16-bit) value at that address - */ -CPUState8080.prototype.getWord = function(addr) -{ - return this.bus.getShort(addr); -}; - -/** - * setByte(addr, b) - * - * @this {CPUState8080} - * @param {number} addr is a linear address - * @param {number} b is the byte (8-bit) value to write (which we truncate to 8 bits; required by opSTOSb) - */ -CPUState8080.prototype.setByte = function(addr, b) -{ - this.bus.setByte(addr, b); -}; - -/** - * setWord(addr, w) - * - * @this {CPUState8080} - * @param {number} addr is a linear address - * @param {number} w is the word (16-bit) value to write (which we truncate to 16 bits to be safe) - */ -CPUState8080.prototype.setWord = function(addr, w) -{ - this.bus.setShort(addr, w); -}; - -/** - * getPCByte() - * - * @this {CPUState8080} - * @return {number} byte at the current PC; PC advanced by 1 - */ -CPUState8080.prototype.getPCByte = function() -{ - var b = this.getByte(this.regPC); - this.setPC(this.regPC + 1); - return b; -}; - -/** - * getPCWord() - * - * @this {CPUState8080} - * @return {number} word at the current PC; PC advanced by 2 - */ -CPUState8080.prototype.getPCWord = function() -{ - var w = this.getWord(this.regPC); - this.setPC(this.regPC + 2); - return w; -}; - -/** - * popWord() - * - * @this {CPUState8080} - * @return {number} word popped from the current SP; SP increased by 2 - */ -CPUState8080.prototype.popWord = function() -{ - var w = this.getWord(this.regSP); - this.setSP(this.regSP + 2); - return w; -}; - -/** - * pushWord(w) - * - * @this {CPUState8080} - * @param {number} w is the word (16-bit) value to push at current SP; SP decreased by 2 - */ -CPUState8080.prototype.pushWord = function(w) -{ - this.setSP(this.regSP - 2); - this.setWord(this.regSP, w); -}; - -/** - * checkINTR() - * - * @this {CPUState8080} - * @return {boolean} true if execution may proceed, false if not - */ -CPUState8080.prototype.checkINTR = function() -{ - /* - * If the Debugger is single-stepping, this.nStepCycles will always be zero, which we take - * advantage of here to avoid processing interrupts. The Debugger will have to issue a "g" - * command (or "p" command on a call instruction) if you want interrupts to be processed. - */ - if (this.nStepCycles) { - if ((this.intFlags & CPUDef8080.INTFLAG.INTR) && this.getIF()) { - for (var nLevel = 0; nLevel < 8; nLevel++) { - if (this.intFlags & (1 << nLevel)) break; - } - this.clearINTR(nLevel); - this.clearIF(); - this.intFlags &= ~CPUDef8080.INTFLAG.HALT; - this.aOps[CPUDef8080.OPCODE.RST0 | (nLevel << 3)].call(this); + constructor(parmsCPU) + { + var nCyclesDefault = 0; + var model = +parmsCPU['model'] || CPUDef8080.MODEL_8080; + + switch(model) { + case CPUDef8080.MODEL_8080: + default: + nCyclesDefault = 1000000; + break; } - } - if (this.intFlags & CPUDef8080.INTFLAG.HALT) { + + super(parmsCPU, nCyclesDefault); + + this.model = model; + /* - * As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead, opHLT() - * calls requestHALT(), which sets INTFLAG.HALT and signals to stepCPU() that it's free to end the - * current burst AND that it should not execute any more instructions until checkINTR() indicates - * that a hardware interrupt has been requested. + * Initialize processor operation to match the requested model */ - this.endBurst(); - return false; + this.initProcessor(); + + /* + * A variety of stepCPU() state variables that don't strictly need to be initialized before the first + * stepCPU() call, but it's good form to do so. + */ + this.resetCycles(); + this.flags.complete = this.flags.debugCheck = false; + + /* + * If there are no live registers to display, then updateStatus() can skip a bit.... + */ + this.cLiveRegs = 0; + + /* + * Array of halt handlers, if any (see addHaltCheck) + */ + this.afnHalt = []; + this.addrReset = 0x0000; + + /* + * This initial resetRegs() call is important to create all the registers, so that if/when we call restore(), + * it will have something to fill in. + */ + this.resetRegs(); } - return true; -}; -/** - * clearINTR(nLevel) - * - * Clear the corresponding interrupt level. - * - * nLevel can either be a valid interrupt level (0-7), or -1 to clear all pending interrupts - * (eg, in the event of a system-wide reset). - * - * @this {CPUState8080} - * @param {number} nLevel (0-7, or -1 for all) - */ -CPUState8080.prototype.clearINTR = function(nLevel) -{ - var bitsClear = nLevel < 0? 0xff : (1 << nLevel); - this.intFlags &= ~bitsClear; -}; + /** + * addHaltCheck(fn) + * + * Records a function that will be called during HLT opcode processing. + * + * @this {CPUState8080} + * @param {function(number)} fn + */ + addHaltCheck(fn) + { + this.afnHalt.push(fn); + } + /** + * initProcessor() + * + * Interestingly, if I dynamically generate aOps as an array of functions bound to "this", using the bind() + * method, overall performance is worse. You would think that eliminating the need to use the call() method + * on every opcode function invocation would be helpful, but it's not. I'm not sure exactly why yet; perhaps + * a Closure Compiler optimization is defeated when generating the function array at run-time instead of at + * compile-time. + * + * @this {CPUState8080} + */ + initProcessor() + { + this.aOps = CPUDef8080.aOps8080; + } -/** - * requestHALT() - * - * @this {CPUState8080} - */ -CPUState8080.prototype.requestHALT = function() -{ - this.intFlags |= CPUDef8080.INTFLAG.HALT; - this.endBurst(); -}; + /** + * reset() + * + * @this {CPUState8080} + */ + reset() + { + if (this.flags.running) this.stopCPU(); + this.resetRegs(); + this.resetCycles(); + this.clearError(); // clear any fatal error/exception that setError() may have flagged + super.reset(); + } -/** - * requestINTR(nLevel) - * - * Request the corresponding interrupt level. - * - * Each interrupt level (0-7) has its own intFlags bit (0-7). If the Interrupt Flag (IF) is also - * set, then we know that checkINTR() will want to issue the interrupt, so we end the current burst - * by setting nStepCycles to zero. But before we do, we subtract nStepCycles from nBurstCycles, - * so that the calculation of how many cycles were actually executed on this burst is correct. - * - * @this {CPUState8080} - * @param {number} nLevel (0-7) - */ -CPUState8080.prototype.requestINTR = function(nLevel) -{ - this.intFlags |= (1 << nLevel); - if (this.getIF()) { + /** + * resetRegs() + * + * @this {CPUState8080} + */ + resetRegs() + { + this.regA = 0; + this.regB = 0; + this.regC = 0; + this.regD = 0; + this.regE = 0; + this.regH = 0; + this.regL = 0; + this.setSP(0); + this.setPC(this.addrReset); + + /* + * This resets the Processor Status flags (regPS), along with all the internal "result registers". + */ + this.setPS(0); + + /* + * intFlags contains some internal states we use to indicate whether a hardware interrupt (INTFLAG.INTR) or + * Trap software interrupt (INTR.TRAP) has been requested, as well as when we're in a "HLT" state (INTFLAG.HALT) + * that requires us to wait for a hardware interrupt (INTFLAG.INTR) before continuing execution. + */ + this.intFlags = CPUDef8080.INTFLAG.NONE; + } + + /** + * setReset(addr) + * + * @this {CPUState8080} + * @param {number} addr + */ + setReset(addr) + { + this.addrReset = addr; + this.setPC(addr); + } + + /** + * getChecksum() + * + * @this {CPUState8080} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ + getChecksum() + { + var sum = (this.regA + this.regB + this.regC + this.regD + this.regE + this.regH + this.regL)|0; + sum = (sum + this.getSP() + this.getPC() + this.getPS())|0; + return sum; + } + + /** + * save() + * + * This implements save support for the CPUState8080 component. + * + * @this {CPUState8080} + * @return {Object|null} + */ + save() + { + var state = new State(this); + state.set(0, [this.regA, this.regB, this.regC, this.regD, this.regE, this.regH, this.regL, this.getSP(), this.getPC(), this.getPS()]); + state.set(1, [this.intFlags, this.nTotalCycles, this.getSpeed()]); + state.set(2, this.bus.saveMemory()); + return state.data(); + } + + /** + * restore(data) + * + * This implements restore support for the CPUState8080 component. + * + * @this {CPUState8080} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ + restore(data) + { + var a = data[0]; + this.regA = a[0]; + this.regB = a[1]; + this.regC = a[2]; + this.regD = a[3]; + this.regE = a[4]; + this.regH = a[5]; + this.regL = a[6]; + this.setSP(a[7]); + this.setPC(a[8]); + this.setPS(a[9]); + a = data[1]; + this.intFlags = a[0]; + this.nTotalCycles = a[1]; + this.setSpeed(a[3]); + return this.bus.restoreMemory(data[2]); + } + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {CPUState8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "AX") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + var fBound = false; + switch (sBinding) { + case "A": + case "B": + case "C": + case "BC": + case "D": + case "E": + case "DE": + case "H": + case "L": + case "HL": + case "SP": + case "PC": + case "PS": + case "IF": + case "SF": + case "ZF": + case "AF": + case "PF": + case "CF": + this.bindings[sBinding] = control; + this.cLiveRegs++; + fBound = true; + break; + default: + fBound = super.setBinding(sHTMLType, sBinding, control); + break; + } + return fBound; + } + + /** + * getBC() + * + * @this {CPUState8080} + * @return {number} + */ + getBC() + { + return (this.regB << 8) | this.regC; + } + + /** + * setBC(w) + * + * @this {CPUState8080} + * @param {number} w + */ + setBC(w) + { + this.regB = (w >> 8) & 0xff; + this.regC = w & 0xff; + } + + /** + * getDE() + * + * @this {CPUState8080} + * @return {number} + */ + getDE() + { + return (this.regD << 8) | this.regE; + } + + /** + * setDE(w) + * + * @this {CPUState8080} + * @param {number} w + */ + setDE(w) + { + this.regD = (w >> 8) & 0xff; + this.regE = w & 0xff; + } + + /** + * getHL() + * + * @this {CPUState8080} + * @return {number} + */ + getHL() + { + return (this.regH << 8) | this.regL; + } + + /** + * setHL(w) + * + * @this {CPUState8080} + * @param {number} w + */ + setHL(w) + { + this.regH = (w >> 8) & 0xff; + this.regL = w & 0xff; + } + + /** + * getSP() + * + * @this {CPUState8080} + * @return {number} + */ + getSP() + { + return this.regSP; + } + + /** + * setSP(off) + * + * @this {CPUState8080} + * @param {number} off + */ + setSP(off) + { + this.regSP = off & 0xffff; + } + + /** + * getPC() + * + * @this {CPUState8080} + * @return {number} + */ + getPC() + { + return this.regPC; + } + + /** + * offPC() + * + * @this {CPUState8080} + * @param {number} off + * @return {number} + */ + offPC(off) + { + return (this.regPC + off) & 0xffff; + } + + /** + * setPC(off) + * + * @this {CPUState8080} + * @param {number} off + */ + setPC(off) + { + this.regPC = off & 0xffff; + } + + /** + * clearCF() + * + * @this {CPUState8080} + */ + clearCF() + { + this.resultZeroCarry &= 0xff; + } + + /** + * getCF() + * + * @this {CPUState8080} + * @return {number} 0 or 1 (CPUDef8080.PS.CF) + */ + getCF() + { + return (this.resultZeroCarry & 0x100)? CPUDef8080.PS.CF : 0; + } + + /** + * setCF() + * + * @this {CPUState8080} + */ + setCF() + { + this.resultZeroCarry |= 0x100; + } + + /** + * updateCF(CF) + * + * @this {CPUState8080} + * @param {number} CF (0x000 or 0x100) + */ + updateCF(CF) + { + this.resultZeroCarry = (this.resultZeroCarry & 0xff) | CF; + } + + /** + * clearPF() + * + * @this {CPUState8080} + */ + clearPF() + { + if (this.getPF()) this.resultParitySign ^= 0x1; + } + + /** + * getPF() + * + * @this {CPUState8080} + * @return {number} 0 or CPUDef8080.PS.PF + */ + getPF() + { + return (CPUDef8080.PARITY[this.resultParitySign & 0xff])? CPUDef8080.PS.PF : 0; + } + + /** + * setPF() + * + * @this {CPUState8080} + */ + setPF() + { + if (!this.getPF()) this.resultParitySign ^= 0x1; + } + + /** + * clearAF() + * + * @this {CPUState8080} + */ + clearAF() + { + this.resultAuxOverflow = (this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); + } + + /** + * getAF() + * + * @this {CPUState8080} + * @return {number} 0 or CPUDef8080.PS.AF + */ + getAF() + { + return ((this.resultParitySign ^ this.resultAuxOverflow) & 0x10)? CPUDef8080.PS.AF : 0; + } + + /** + * setAF() + * + * @this {CPUState8080} + */ + setAF() + { + this.resultAuxOverflow = (~this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); + } + + /** + * clearZF() + * + * @this {CPUState8080} + */ + clearZF() + { + this.resultZeroCarry |= 0xff; + } + + /** + * getZF() + * + * @this {CPUState8080} + * @return {number} 0 or CPUDef8080.PS.ZF + */ + getZF() + { + return (this.resultZeroCarry & 0xff)? 0 : CPUDef8080.PS.ZF; + } + + /** + * setZF() + * + * @this {CPUState8080} + */ + setZF() + { + this.resultZeroCarry &= ~0xff; + } + + /** + * clearSF() + * + * @this {CPUState8080} + */ + clearSF() + { + if (this.getSF()) this.resultParitySign ^= 0xc0; + } + + /** + * getSF() + * + * @this {CPUState8080} + * @return {number} 0 or CPUDef8080.PS.SF + */ + getSF() + { + return (this.resultParitySign & 0x80)? CPUDef8080.PS.SF : 0; + } + + /** + * setSF() + * + * @this {CPUState8080} + */ + setSF() + { + if (!this.getSF()) this.resultParitySign ^= 0xc0; + } + + /** + * clearIF() + * + * @this {CPUState8080} + */ + clearIF() + { + this.regPS &= ~CPUDef8080.PS.IF; + } + + /** + * getIF() + * + * @this {CPUState8080} + * @return {number} 0 or CPUDef8080.PS.IF + */ + getIF() + { + return (this.regPS & CPUDef8080.PS.IF); + } + + /** + * setIF() + * + * @this {CPUState8080} + */ + setIF() + { + this.regPS |= CPUDef8080.PS.IF; + } + + /** + * getPS() + * + * @this {CPUState8080} + * @return {number} + */ + getPS() + { + return (this.regPS & ~CPUDef8080.PS.RESULT) | (this.getSF() | this.getZF() | this.getAF() | this.getPF() | this.getCF()); + } + + /** + * setPS(regPS) + * + * @this {CPUState8080} + * @param {number} regPS + */ + setPS(regPS) + { + this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = 0; + if (regPS & CPUDef8080.PS.CF) this.resultZeroCarry |= 0x100; + if (!(regPS & CPUDef8080.PS.PF)) this.resultParitySign |= 0x01; + if (regPS & CPUDef8080.PS.AF) this.resultAuxOverflow |= 0x10; + if (!(regPS & CPUDef8080.PS.ZF)) this.resultZeroCarry |= 0xff; + if (regPS & CPUDef8080.PS.SF) this.resultParitySign ^= 0xc0; + this.regPS = (this.regPS & ~(CPUDef8080.PS.RESULT | CPUDef8080.PS.INTERNAL)) | (regPS & CPUDef8080.PS.INTERNAL) | CPUDef8080.PS.SET; + Component.assert((regPS & CPUDef8080.PS.RESULT) == (this.getPS() & CPUDef8080.PS.RESULT)); + } + + /** + * getPSW() + * + * @this {CPUState8080} + * @return {number} + */ + getPSW() + { + return (this.getPS() & CPUDef8080.PS.MASK) | (this.regA << 8); + } + + /** + * setPSW(w) + * + * @this {CPUState8080} + * @param {number} w + */ + setPSW(w) + { + this.setPS((w & CPUDef8080.PS.MASK) | (this.regPS & ~CPUDef8080.PS.MASK)); + this.regA = w >> 8; + } + + /** + * addByte(src) + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA + src + */ + addByte(src) + { + this.resultAuxOverflow = this.regA ^ src; + return this.resultParitySign = (this.resultZeroCarry = this.regA + src) & 0xff; + } + + /** + * addByteCarry(src) + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA + src + carry + */ + addByteCarry(src) + { + this.resultAuxOverflow = this.regA ^ src; + return this.resultParitySign = (this.resultZeroCarry = this.regA + src + ((this.resultZeroCarry & 0x100)? 1 : 0)) & 0xff; + } + + /** + * andByte(src) + * + * Ordinarily, one would expect the Auxiliary Carry flag (AF) to be clear after this operation, + * but apparently the 8080 will set AF if bit 3 in either operand is set. + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA & src + */ + andByte(src) + { + this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = this.regA & src; + if ((this.regA | src) & 0x8) this.resultAuxOverflow ^= 0x10; // set AF by inverting bit 4 in resultAuxOverflow + return this.resultZeroCarry; + } + + /** + * decByte(b) + * + * We perform this operation using 8-bit two's complement arithmetic, by negating and then adding + * the implied src of 1. This appears to mimic how the 8080 manages the Auxiliary Carry flag (AF). + * + * @this {CPUState8080} + * @param {number} b + * @return {number} + */ + decByte(b) + { + this.resultAuxOverflow = b ^ 0xff; + b = this.resultParitySign = (b + 0xff) & 0xff; + this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b; + return b; + } + + /** + * incByte(b) + * + * @this {CPUState8080} + * @param {number} b + * @return {number} + */ + incByte(b) + { + this.resultAuxOverflow = b; + b = this.resultParitySign = (b + 1) & 0xff; + this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b; + return b; + } + + /** + * orByte(src) + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA | src + */ + orByte(src) + { + return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA | src; + } + + /** + * subByte(src) + * + * We perform this operation using 8-bit two's complement arithmetic, by inverting src, adding + * src + 1, and then inverting the resulting carry (resultZeroCarry ^ 0x100). This appears to mimic + * how the 8080 manages the Auxiliary Carry flag (AF). + * + * This function is also used as a cmpByte() function; compare instructions simply ignore the + * return value. + * + * Example: A=66, SUI $10 + * + * If we created the two's complement of 0x10 by negating it, there would just be one addition: + * + * 0110 0110 (0x66) + * + 1111 0000 (0xF0) (ie, -0x10) + * --------- + * 1 0101 0110 (0x56) + * + * But in order to mimic the 8080's AF flag, we must perform the two's complement of src in two steps, + * inverting it before the add, and then incrementing after the add; eg: + * + * 0110 0110 (0x66) + * + 1110 1111 (0xEF) (ie, ~0x10) + * --------- + * 1 0101 0101 (0x55) + * + 0000 0001 (0x01) + * --------- + * 1 0101 0110 (0x56) + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA - src + */ + subByte(src) + { + src ^= 0xff; + this.resultAuxOverflow = this.regA ^ src; + return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + 1) ^ 0x100) & 0xff; + } + + /** + * subByteBorrow(src) + * + * We perform this operation using 8-bit two's complement arithmetic, using logic similar to subByte(), + * but changing the final increment to a conditional increment, because if the Carry flag (CF) is set, then + * we don't need to perform the increment at all. + * + * This mimics the behavior of subByte() when the Carry flag (CF) is clear, and hopefully also mimics how the + * 8080 manages the Auxiliary Carry flag (AF) when the Carry flag (CF) is set. + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA - src - carry + */ + subByteBorrow(src) + { + src ^= 0xff; + this.resultAuxOverflow = this.regA ^ src; + return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + ((this.resultZeroCarry & 0x100)? 0 : 1)) ^ 0x100) & 0xff; + } + + /** + * xorByte(src) + * + * @this {CPUState8080} + * @param {number} src + * @return {number} regA ^ src + */ + xorByte(src) + { + return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA ^ src; + } + + /** + * getByte(addr) + * + * @this {CPUState8080} + * @param {number} addr is a linear address + * @return {number} byte (8-bit) value at that address + */ + getByte(addr) + { + return this.bus.getByte(addr); + } + + /** + * getWord(addr) + * + * @this {CPUState8080} + * @param {number} addr is a linear address + * @return {number} word (16-bit) value at that address + */ + getWord(addr) + { + return this.bus.getShort(addr); + } + + /** + * setByte(addr, b) + * + * @this {CPUState8080} + * @param {number} addr is a linear address + * @param {number} b is the byte (8-bit) value to write (which we truncate to 8 bits; required by opSTOSb) + */ + setByte(addr, b) + { + this.bus.setByte(addr, b); + } + + /** + * setWord(addr, w) + * + * @this {CPUState8080} + * @param {number} addr is a linear address + * @param {number} w is the word (16-bit) value to write (which we truncate to 16 bits to be safe) + */ + setWord(addr, w) + { + this.bus.setShort(addr, w); + } + + /** + * getPCByte() + * + * @this {CPUState8080} + * @return {number} byte at the current PC; PC advanced by 1 + */ + getPCByte() + { + var b = this.getByte(this.regPC); + this.setPC(this.regPC + 1); + return b; + } + + /** + * getPCWord() + * + * @this {CPUState8080} + * @return {number} word at the current PC; PC advanced by 2 + */ + getPCWord() + { + var w = this.getWord(this.regPC); + this.setPC(this.regPC + 2); + return w; + } + + /** + * popWord() + * + * @this {CPUState8080} + * @return {number} word popped from the current SP; SP increased by 2 + */ + popWord() + { + var w = this.getWord(this.regSP); + this.setSP(this.regSP + 2); + return w; + } + + /** + * pushWord(w) + * + * @this {CPUState8080} + * @param {number} w is the word (16-bit) value to push at current SP; SP decreased by 2 + */ + pushWord(w) + { + this.setSP(this.regSP - 2); + this.setWord(this.regSP, w); + } + + /** + * checkINTR() + * + * @this {CPUState8080} + * @return {boolean} true if execution may proceed, false if not + */ + checkINTR() + { + /* + * If the Debugger is single-stepping, this.nStepCycles will always be zero, which we take + * advantage of here to avoid processing interrupts. The Debugger will have to issue a "g" + * command (or "p" command on a call instruction) if you want interrupts to be processed. + */ + if (this.nStepCycles) { + if ((this.intFlags & CPUDef8080.INTFLAG.INTR) && this.getIF()) { + for (var nLevel = 0; nLevel < 8; nLevel++) { + if (this.intFlags & (1 << nLevel)) break; + } + this.clearINTR(nLevel); + this.clearIF(); + this.intFlags &= ~CPUDef8080.INTFLAG.HALT; + this.aOps[CPUDef8080.OPCODE.RST0 | (nLevel << 3)].call(this); + } + } + if (this.intFlags & CPUDef8080.INTFLAG.HALT) { + /* + * As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead, opHLT() + * calls requestHALT(), which sets INTFLAG.HALT and signals to stepCPU() that it's free to end the + * current burst AND that it should not execute any more instructions until checkINTR() indicates + * that a hardware interrupt has been requested. + */ + this.endBurst(); + return false; + } + return true; + } + + /** + * clearINTR(nLevel) + * + * Clear the corresponding interrupt level. + * + * nLevel can either be a valid interrupt level (0-7), or -1 to clear all pending interrupts + * (eg, in the event of a system-wide reset). + * + * @this {CPUState8080} + * @param {number} nLevel (0-7, or -1 for all) + */ + clearINTR(nLevel) + { + var bitsClear = nLevel < 0? 0xff : (1 << nLevel); + this.intFlags &= ~bitsClear; + } + + /** + * requestHALT() + * + * @this {CPUState8080} + */ + requestHALT() + { + this.intFlags |= CPUDef8080.INTFLAG.HALT; this.endBurst(); } -}; -/** - * updateReg(sReg, nValue, cch) - * - * This function helps updateStatus() by massaging the register names and values according to - * CPU type before passing the call to displayValue(); in the "old days", updateStatus() called - * displayValue() directly (although then it was called displayReg()). - * - * @this {CPUState8080} - * @param {string} sReg - * @param {number} nValue - * @param {number} [cch] (default is 2 hex digits) - */ -CPUState8080.prototype.updateReg = function(sReg, nValue, cch) -{ - this.displayValue(sReg, nValue, cch || 2); -}; - -/** - * updateStatus(fForce) - * - * This provides periodic Control Panel updates (eg, a few times per second; see YIELDS_PER_STATUS). - * this is where we take care of any DOM updates (eg, register values) while the CPU is running. - * - * Any high-frequency updates should be performed in updateVideo(), which should avoid DOM updates, - * since updateVideo() can be called up to 60 times per second. - * - * @this {CPUState8080} - * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) - */ -CPUState8080.prototype.updateStatus = function(fForce) -{ - if (this.cLiveRegs) { - if (fForce || !this.flags.running || this.flags.displayLiveRegs) { - this.updateReg("A", this.regA); - this.updateReg("B", this.regB); - this.updateReg("C", this.regC); - this.updateReg("BC", this.getBC(), 4); - this.updateReg("D", this.regD); - this.updateReg("E", this.regE); - this.updateReg("DE", this.getDE(), 4); - this.updateReg("H", this.regH); - this.updateReg("L", this.regL); - this.updateReg("HL", this.getHL(), 4); - this.updateReg("SP", this.getSP(), 4); - this.updateReg("PC", this.getPC(), 4); - var regPS = this.getPS(); - this.updateReg("PS", regPS, 4); - this.updateReg("IF", (regPS & CPUDef8080.PS.IF)? 1 : 0, 1); - this.updateReg("SF", (regPS & CPUDef8080.PS.SF)? 1 : 0, 1); - this.updateReg("ZF", (regPS & CPUDef8080.PS.ZF)? 1 : 0, 1); - this.updateReg("AF", (regPS & CPUDef8080.PS.AF)? 1 : 0, 1); - this.updateReg("PF", (regPS & CPUDef8080.PS.PF)? 1 : 0, 1); - this.updateReg("CF", (regPS & CPUDef8080.PS.CF)? 1 : 0, 1); + /** + * requestINTR(nLevel) + * + * Request the corresponding interrupt level. + * + * Each interrupt level (0-7) has its own intFlags bit (0-7). If the Interrupt Flag (IF) is also + * set, then we know that checkINTR() will want to issue the interrupt, so we end the current burst + * by setting nStepCycles to zero. But before we do, we subtract nStepCycles from nBurstCycles, + * so that the calculation of how many cycles were actually executed on this burst is correct. + * + * @this {CPUState8080} + * @param {number} nLevel (0-7) + */ + requestINTR(nLevel) + { + this.intFlags |= (1 << nLevel); + if (this.getIF()) { + this.endBurst(); } } - var controlSpeed = this.bindings["speed"]; - if (controlSpeed) controlSpeed.textContent = this.getSpeedCurrent(); -}; -/** - * stepCPU(nMinCycles) - * - * NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger - * operation that's completely independent of Trap status. The CPU can go in and out of Trap mode, - * in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective, - * they're all one continuous stream of instructions that can be stepped or run at will. Moreover, - * stepping vs. running should never change the behavior of the simulation. - * - * @this {CPUState8080} - * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) - * @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint - * was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive - * number indicates successful completion of that many cycles (which should always be >= nMinCycles). - */ -CPUState8080.prototype.stepCPU = function(nMinCycles) -{ - /* - * The Debugger uses fComplete to determine if the instruction completed (true) or was interrupted - * by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript - * exceptions, which stepCPU() expects the caller to catch using its own exception handler. + /** + * updateReg(sReg, nValue, cch) * - * The CPU relies on the use of stopCPU() rather than fComplete, because the CPU never single-steps - * (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the - * Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set, - * so stopCPU() would have no effect as far as the Debugger is concerned. - */ - this.flags.complete = true; - - /* - * fDebugCheck is true if we need to "check" every instruction with the Debugger. - */ - var fDebugCheck = this.flags.debugCheck = (DEBUGGER && this.dbg && this.dbg.checksEnabled()); - - /* - * nDebugState is checked only when fDebugCheck is true, and its sole purpose is to tell the first call - * to checkInstruction() that it can skip breakpoint checks, and that will be true ONLY when fStarting is - * true OR nMinCycles is zero (the latter means the Debugger is single-stepping). + * This function helps updateStatus() by massaging the register names and values according to + * CPU type before passing the call to displayValue(); in the "old days", updateStatus() called + * displayValue() directly (although then it was called displayReg()). * - * Once we snap fStarting, we clear it, because technically, we've moved beyond "starting" and have - * officially "started" now. + * @this {CPUState8080} + * @param {string} sReg + * @param {number} nValue + * @param {number} [cch] (default is 2 hex digits) */ - var nDebugState = (!nMinCycles)? -1 : (this.flags.starting? 0 : 1); - this.flags.starting = false; + updateReg(sReg, nValue, cch) + { + this.displayValue(sReg, nValue, cch || 2); + } - /* - * We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other - * functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check - * any other criteria to determine whether we should continue stepping or not. + /** + * updateStatus(fForce) + * + * This provides periodic Control Panel updates (eg, a few times per second; see YIELDS_PER_STATUS). + * this is where we take care of any DOM updates (eg, register values) while the CPU is running. + * + * Any high-frequency updates should be performed in updateVideo(), which should avoid DOM updates, + * since updateVideo() can be called up to 60 times per second. + * + * @this {CPUState8080} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) */ - this.nBurstCycles = this.nStepCycles = nMinCycles; - - /* - * NOTE: If checkINTR() returns false, INTFLAG.HALT must be set, so no instructions should be executed. - */ - if (this.checkINTR()) { - do { - if (DEBUGGER && fDebugCheck) { - if (this.dbg.checkInstruction(this.regPC, nDebugState)) { - this.stopCPU(); - break; - } - nDebugState = 1; + updateStatus(fForce) + { + if (this.cLiveRegs) { + if (fForce || !this.flags.running || this.flags.displayLiveRegs) { + this.updateReg("A", this.regA); + this.updateReg("B", this.regB); + this.updateReg("C", this.regC); + this.updateReg("BC", this.getBC(), 4); + this.updateReg("D", this.regD); + this.updateReg("E", this.regE); + this.updateReg("DE", this.getDE(), 4); + this.updateReg("H", this.regH); + this.updateReg("L", this.regL); + this.updateReg("HL", this.getHL(), 4); + this.updateReg("SP", this.getSP(), 4); + this.updateReg("PC", this.getPC(), 4); + var regPS = this.getPS(); + this.updateReg("PS", regPS, 4); + this.updateReg("IF", (regPS & CPUDef8080.PS.IF)? 1 : 0, 1); + this.updateReg("SF", (regPS & CPUDef8080.PS.SF)? 1 : 0, 1); + this.updateReg("ZF", (regPS & CPUDef8080.PS.ZF)? 1 : 0, 1); + this.updateReg("AF", (regPS & CPUDef8080.PS.AF)? 1 : 0, 1); + this.updateReg("PF", (regPS & CPUDef8080.PS.PF)? 1 : 0, 1); + this.updateReg("CF", (regPS & CPUDef8080.PS.CF)? 1 : 0, 1); } - this.aOps[this.getPCByte()].call(this); - - } while (this.nStepCycles > 0); + } + var controlSpeed = this.bindings["speed"]; + if (controlSpeed) controlSpeed.textContent = this.getSpeedCurrent(); } - return (this.flags.complete? this.nBurstCycles - this.nStepCycles : (this.flags.complete === undefined? 0 : -1)); -}; + /** + * stepCPU(nMinCycles) + * + * NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger + * operation that's completely independent of Trap status. The CPU can go in and out of Trap mode, + * in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective, + * they're all one continuous stream of instructions that can be stepped or run at will. Moreover, + * stepping vs. running should never change the behavior of the simulation. + * + * @this {CPUState8080} + * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) + * @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint + * was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive + * number indicates successful completion of that many cycles (which should always be >= nMinCycles). + */ + stepCPU(nMinCycles) + { + /* + * The Debugger uses fComplete to determine if the instruction completed (true) or was interrupted + * by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript + * exceptions, which stepCPU() expects the caller to catch using its own exception handler. + * + * The CPU relies on the use of stopCPU() rather than fComplete, because the CPU never single-steps + * (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the + * Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set, + * so stopCPU() would have no effect as far as the Debugger is concerned. + */ + this.flags.complete = true; -/** - * CPUState8080.init() - * - * This function operates on every HTML element of class "cpu", extracting the - * JSON-encoded parameters for the CPUState8080 constructor from the element's "data-value" - * attribute, invoking the constructor (which in turn invokes the CPU constructor) - * to create a CPUState8080 component, and then binding any associated HTML controls to the - * new component. - */ -CPUState8080.init = function() -{ - var aeCPUs = Component.getElementsByClass(document, PC8080.APPCLASS, "cpu"); - for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) { - var eCPU = aeCPUs[iCPU]; - var parmsCPU = Component.getComponentParms(eCPU); - var cpu = new CPUState8080(parmsCPU); - Component.bindComponentControls(cpu, eCPU, PC8080.APPCLASS); + /* + * fDebugCheck is true if we need to "check" every instruction with the Debugger. + */ + var fDebugCheck = this.flags.debugCheck = (DEBUGGER && this.dbg && this.dbg.checksEnabled()); + + /* + * nDebugState is checked only when fDebugCheck is true, and its sole purpose is to tell the first call + * to checkInstruction() that it can skip breakpoint checks, and that will be true ONLY when fStarting is + * true OR nMinCycles is zero (the latter means the Debugger is single-stepping). + * + * Once we snap fStarting, we clear it, because technically, we've moved beyond "starting" and have + * officially "started" now. + */ + var nDebugState = (!nMinCycles)? -1 : (this.flags.starting? 0 : 1); + this.flags.starting = false; + + /* + * We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other + * functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check + * any other criteria to determine whether we should continue stepping or not. + */ + this.nBurstCycles = this.nStepCycles = nMinCycles; + + /* + * NOTE: If checkINTR() returns false, INTFLAG.HALT must be set, so no instructions should be executed. + */ + if (this.checkINTR()) { + do { + if (DEBUGGER && fDebugCheck) { + if (this.dbg.checkInstruction(this.regPC, nDebugState)) { + this.stopCPU(); + break; + } + nDebugState = 1; + } + this.aOps[this.getPCByte()].call(this); + + } while (this.nStepCycles > 0); + } + + return (this.flags.complete? this.nBurstCycles - this.nStepCycles : (this.flags.complete === undefined? 0 : -1)); } -}; + + /** + * CPUState8080.init() + * + * This function operates on every HTML element of class "cpu", extracting the + * JSON-encoded parameters for the CPUState8080 constructor from the element's "data-value" + * attribute, invoking the constructor (which in turn invokes the CPU constructor) + * to create a CPUState8080 component, and then binding any associated HTML controls to the + * new component. + */ + static init() + { + var aeCPUs = Component.getElementsByClass(document, PC8080.APPCLASS, "cpu"); + for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) { + var eCPU = aeCPUs[iCPU]; + var parmsCPU = Component.getComponentParms(eCPU); + var cpu = new CPUState8080(parmsCPU); + Component.bindComponentControls(cpu, eCPU, PC8080.APPCLASS); + } + } +} /* * Initialize every CPU module on the page */ -web.onInit(CPUState8080.init); +Web.onInit(CPUState8080.init); -if (NODE) module.exports = CPUState8080; +module.exports = CPUState8080; diff --git a/modules/pc8080/lib/debugger.js b/modules/pc8080/lib/debugger.js index dfb051b7e..4bd156e01 100644 --- a/modules/pc8080/lib/debugger.js +++ b/modules/pc8080/lib/debugger.js @@ -28,22 +28,18 @@ "use strict"; -if (DEBUGGER) { - if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var Debugger = require("../../shared/lib/debugger"); - var Keys = require("../../shared/lib/keys"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var CPUDef8080 = require("./cpudef"); - var CPU8080 = require("./cpu"); - var Messages8080= require("./messages"); - var Memory8080 = require("./memory"); - } -} +var Str = require("../../shared/es6/strlib"); +var Usr = require("../../shared/es6/usrlib"); +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var Debugger = require("../../shared/es6/debugger"); +var Keys = require("../../shared/es6/keys"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var CPUDef8080 = require("./cpudef"); +var CPU8080 = require("./cpu"); +var Memory8080 = require("./memory"); +var Messages8080 = require("./messages"); /** * Debugger8080 Address Object @@ -58,124 +54,3860 @@ if (DEBUGGER) { * fTemporary:(boolean|undefined), * sCmd:(string|undefined), * aCmds:(Array.|undefined) - * }} DbgAddr8080 + * }} */ var DbgAddr8080; /** - * Debugger8080(parmsDbg) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * @constructor - * @extends Debugger - * @param {Object} parmsDbg - * - * The Debugger8080 component supports the following optional (parmsDbg) properties: - * - * commands: string containing zero or more commands, separated by ';' - * - * messages: string containing zero or more message categories to enable; - * multiple categories must be separated by '|' or ';'. Parsed by messageInit(). - * - * The Debugger8080 component is an optional component that implements a variety of user - * commands for controlling the CPU, dumping and editing memory, etc. + * @unrestricted */ -function Debugger8080(parmsDbg) -{ - if (DEBUGGER) { +class Debugger8080 extends Debugger { + /** + * Debugger8080(parmsDbg) + * + * The Debugger8080 component supports the following optional (parmsDbg) properties: + * + * commands: string containing zero or more commands, separated by ';' + * + * messages: string containing zero or more message categories to enable; + * multiple categories must be separated by '|' or ';'. Parsed by messageInit(). + * + * The Debugger8080 component is an optional component that implements a variety of user + * commands for controlling the CPU, dumping and editing memory, etc. + * + * @this {Debugger8080} + * @param {Object} parmsDbg + */ + constructor(parmsDbg) + { + if (DEBUGGER) { - Debugger.call(this, parmsDbg); + super(parmsDbg); - this.style = Debugger8080.STYLE_8080; + this.style = Debugger8080.STYLE_8080; - /* - * Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode - * or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn, - * update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done. - * - * For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically - * cleared when they're hit. - */ - this.dbgAddrNextCode = this.newAddr(); - this.dbgAddrNextData = this.newAddr(); - this.dbgAddrAssemble = this.newAddr(); + /* + * Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode + * or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn, + * update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done. + * + * For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically + * cleared when they're hit. + */ + this.dbgAddrNextCode = this.newAddr(); + this.dbgAddrNextData = this.newAddr(); + this.dbgAddrAssemble = this.newAddr(); - /* - * aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space, - * where each object contains the following properties: - * - * sModule - * addr (physical address, if any; eg, symbols for a ROM) - * len - * aSymbols - * aOffsets - * - * See addSymbols() for more details, since that's how callers add sets of symbols to the table. - */ - this.aSymbolTable = []; + /* + * aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space, + * where each object contains the following properties: + * + * sModule + * addr (physical address, if any; eg, symbols for a ROM) + * len + * aSymbols + * aOffsets + * + * See addSymbols() for more details, since that's how callers add sets of symbols to the table. + */ + this.aSymbolTable = []; - /* - * clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses - * to halt on whenever attempting to execute an instruction at the corresponding address, - * and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write, - * respectively, occurs at the corresponding address. - * - * NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609, - * the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead - * property was already defined. To eliminate whatever was confusing the Closure Compiler, I've - * explicitly initialized all the properties that clearBreakpoints() (re)initializes. - */ - this.aBreakExec = this.aBreakRead = this.aBreakWrite = []; - this.clearBreakpoints(); + /* + * clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses + * to halt on whenever attempting to execute an instruction at the corresponding address, + * and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write, + * respectively, occurs at the corresponding address. + * + * NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609, + * the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead + * property was already defined. To eliminate whatever was confusing the Closure Compiler, I've + * explicitly initialized all the properties that clearBreakpoints() (re)initializes. + */ + this.aBreakExec = this.aBreakRead = this.aBreakWrite = []; + this.clearBreakpoints(); - /* - * The new "bn" command allows you to specify a number of instructions to execute and then stop; - * "bn 0" disables any outstanding count. - */ - this.nBreakIns = 0; + /* + * The new "bn" command allows you to specify a number of instructions to execute and then stop; + * "bn 0" disables any outstanding count. + */ + this.nBreakIns = 0; - /* - * Execution history is allocated by historyInit() whenever checksEnabled() conditions change. - * Execution history is updated whenever the CPU calls checkInstruction(), which will happen - * only when checksEnabled() returns true (eg, whenever one or more breakpoints have been set). - * This ensures that, by default, the CPU runs as fast as possible. - */ - this.historyInit(); + /* + * Execution history is allocated by historyInit() whenever checksEnabled() conditions change. + * Execution history is updated whenever the CPU calls checkInstruction(), which will happen + * only when checksEnabled() returns true (eg, whenever one or more breakpoints have been set). + * This ensures that, by default, the CPU runs as fast as possible. + */ + this.historyInit(); - /* - * Initialize Debugger8080 message support - */ - this.afnDumpers = {}; - this.messageInit(parmsDbg['messages']); + /* + * Initialize Debugger8080 message support + */ + this.afnDumpers = {}; + this.messageInit(parmsDbg['messages']); - this.sInitCommands = parmsDbg['commands']; + this.sInitCommands = parmsDbg['commands']; - /* - * Make it easier to access Debugger8080 commands from an external REPL (eg, the WebStorm - * "live" console window); eg: - * - * pc8080('r') - * pc8080('dw 0:0') - * pc8080('h') - * ... - */ - var dbg = this; - if (window) { - if (window[PC8080.APPCLASS] === undefined) { - window[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); }; + /* + * Make it easier to access Debugger8080 commands from an external REPL (eg, the WebStorm + * "live" console window); eg: + * + * pc8080('r') + * pc8080('dw 0:0') + * pc8080('h') + * ... + */ + var dbg = this; + if (window) { + if (window[PC8080.APPCLASS] === undefined) { + window[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); }; + } + } else { + if (global[PC8080.APPCLASS] === undefined) { + global[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); }; + } } + + } // endif DEBUGGER + } + + /** + * initBus(bus, cpu, dbg) + * + * @this {Debugger8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.cmp = cmp; + + /* + * Re-initialize Debugger message support if necessary + */ + var sMessages = cmp.getMachineParm('messages'); + if (sMessages) this.messageInit(sMessages); + + this.aaOpDescs = Debugger8080.aaOpDescs; + + this.messageDump(Messages8080.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); }); + + this.setReady(); + } + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {Debugger8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + var dbg = this; + switch (sBinding) { + + case "debugInput": + this.bindings[sBinding] = control; + this.controlDebug = control; + /* + * For halted machines, this is fine, but for auto-start machines, it can be annoying. + * + * control.focus(); + */ + control.onkeydown = function onKeyDownDebugInput(event) { + var sCmd; + if (event.keyCode == Keys.KEYCODE.CR) { + sCmd = control.value; + control.value = ""; + dbg.doCommands(sCmd, true); + } + else if (event.keyCode == Keys.KEYCODE.ESC) { + control.value = sCmd = ""; + } + else { + if (event.keyCode == Keys.KEYCODE.UP) { + sCmd = dbg.getPrevCommand(); + } + else if (event.keyCode == Keys.KEYCODE.DOWN) { + sCmd = dbg.getNextCommand(); + } + if (sCmd != null) { + var cch = sCmd.length; + control.value = sCmd; + control.setSelectionRange(cch, cch); + } + } + if (sCmd != null && event.preventDefault) event.preventDefault(); + }; + return true; + + case "debugEnter": + this.bindings[sBinding] = control; + Web.onClickRepeat( + control, + 500, 100, + function onClickDebugEnter(fRepeat) { + if (dbg.controlDebug) { + var sCmds = dbg.controlDebug.value; + dbg.controlDebug.value = ""; + dbg.doCommands(sCmds, true); + return true; + } + if (DEBUG) dbg.log("no debugger input buffer"); + return false; + } + ); + return true; + + case "step": + this.bindings[sBinding] = control; + Web.onClickRepeat( + control, + 500, 100, + function onClickStep(fRepeat) { + var fCompleted = false; + if (!dbg.isBusy(true)) { + dbg.setBusy(true); + fCompleted = dbg.stepCPU(fRepeat? 1 : 0); + dbg.setBusy(false); + } + return fCompleted; + } + ); + return true; + + default: + break; + } + return false; + } + + /** + * updateFocus() + * + * @this {Debugger8080} + */ + updateFocus() + { + if (this.controlDebug) this.controlDebug.focus(); + } + + /** + * getAddr(dbgAddr, fWrite, nb) + * + * @this {Debugger8080} + * @param {DbgAddr8080|null|undefined} dbgAddr + * @param {boolean} [fWrite] + * @param {number} [nb] number of bytes to check (1 or 2); default is 1 + * @return {number} is the corresponding linear address, or CPUDef8080.ADDR_INVALID + */ + getAddr(dbgAddr, fWrite, nb) + { + var addr = dbgAddr && dbgAddr.addr; + if (addr == null) { + addr = CPUDef8080.ADDR_INVALID; + } + return addr; + } + + /** + * getByte(dbgAddr, inc) + * + * We must route all our memory requests through the CPU now, in case paging is enabled. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} [inc] + * @return {number} + */ + getByte(dbgAddr, inc) + { + var b = 0xff; + var addr = this.getAddr(dbgAddr, false, 1); + if (addr !== CPUDef8080.ADDR_INVALID) { + b = this.bus.getByteDirect(addr); + if (inc) this.incAddr(dbgAddr, inc); + } + return b; + } + + /** + * getWord(dbgAddr, fAdvance) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {boolean} [fAdvance] + * @return {number} + */ + getWord(dbgAddr, fAdvance) + { + return this.getShort(dbgAddr, fAdvance? 2 : 0); + } + + /** + * getShort(dbgAddr, inc) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} [inc] + * @return {number} + */ + getShort(dbgAddr, inc) + { + var w = 0xffff; + var addr = this.getAddr(dbgAddr, false, 2); + if (addr !== CPUDef8080.ADDR_INVALID) { + w = this.bus.getShortDirect(addr); + if (inc) this.incAddr(dbgAddr, inc); + } + return w; + } + + /** + * setByte(dbgAddr, b, inc) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} b + * @param {number} [inc] + */ + setByte(dbgAddr, b, inc) + { + var addr = this.getAddr(dbgAddr, true, 1); + if (addr !== CPUDef8080.ADDR_INVALID) { + this.bus.setByteDirect(addr, b); + if (inc) this.incAddr(dbgAddr, inc); + this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target + } + } + + /** + * setShort(dbgAddr, w, inc) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} w + * @param {number} [inc] + */ + setShort(dbgAddr, w, inc) + { + var addr = this.getAddr(dbgAddr, true, 2); + if (addr !== CPUDef8080.ADDR_INVALID) { + this.bus.setShortDirect(addr, w); + if (inc) this.incAddr(dbgAddr, inc); + this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target + } + } + + /** + * newAddr(addr) + * + * Returns a NEW DbgAddr8080 object, initialized with specified values and/or defaults. + * + * @this {Debugger8080} + * @param {number} [addr] + * @return {DbgAddr8080} + */ + newAddr(addr) + { + return {addr: addr, fTemporary: false}; + } + + /** + * setAddr(dbgAddr, addr) + * + * Updates an EXISTING DbgAddr8080 object, initialized with specified values and/or defaults. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} addr + * @return {DbgAddr8080} + */ + setAddr(dbgAddr, addr) + { + dbgAddr.addr = addr; + dbgAddr.fTemporary = false; + return dbgAddr; + } + + /** + * packAddr(dbgAddr) + * + * Packs a DbgAddr8080 object into an Array suitable for saving in a machine state object. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @return {Array} + */ + packAddr(dbgAddr) + { + return [dbgAddr.addr, dbgAddr.fTemporary]; + } + + /** + * unpackAddr(aAddr) + * + * Unpacks a DbgAddr8080 object from an Array created by packAddr() and restored from a saved machine state. + * + * @this {Debugger8080} + * @param {Array} aAddr + * @return {DbgAddr8080} + */ + unpackAddr(aAddr) + { + return {addr: aAddr[0], fTemporary: aAddr[1]}; + } + + /** + * parseAddr(sAddr, fCode, fNoChecks, fPrint) + * + * Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's + * done later, by getAddr(), which returns CPUDef8080.ADDR_INVALID for invalid segments, out-of-range offsets, + * etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an + * invalid address is passed through to the Bus memory interfaces, the address will simply be masked with + * Bus8080.nBusLimit; in the case of CPUDef8080.ADDR_INVALID, that will generally refer to the top of the physical + * address space. + * + * @this {Debugger8080} + * @param {string|undefined} sAddr + * @param {boolean} [fCode] (true if target is code, false if target is data) + * @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later) + * @param {boolean} [fPrint] + * @return {DbgAddr8080|null|undefined} + */ + parseAddr(sAddr, fCode, fNoChecks, fPrint) + { + var dbgAddr; + var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData); + var addr = dbgAddrNext.addr; + if (sAddr !== undefined) { + sAddr = this.parseReference(sAddr); + dbgAddr = this.findSymbolAddr(sAddr); + if (dbgAddr) return dbgAddr; + addr = this.parseExpression(sAddr, fPrint); + } + if (addr != null) { + dbgAddr = this.newAddr(addr); + } + return dbgAddr; + } + + /** + * parseAddrOptions(dbdAddr, sOptions) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {string} [sOptions] + */ + parseAddrOptions(dbgAddr, sOptions) + { + if (sOptions) { + var a = sOptions.match(/(['"])(.*?)\1/); + if (a) { + dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]); + } + } + } + + /** + * incAddr(dbgAddr, inc) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} [inc] contains value to increment dbgAddr by (default is 1) + */ + incAddr(dbgAddr, inc) + { + if (dbgAddr.addr != null) { + dbgAddr.addr += (inc || 1); + } + } + + /** + * toHexOffset(off) + * + * @this {Debugger8080} + * @param {number|null|undefined} [off] + * @return {string} the hex representation of off + */ + toHexOffset(off) + { + return Str.toHex(off, 4); + } + + /** + * toHexAddr(dbgAddr) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @return {string} the hex representation of the address + */ + toHexAddr(dbgAddr) + { + return this.toHexOffset(dbgAddr.addr); + } + + /** + * getSZ(dbgAddr, cchMax) + * + * Gets zero-terminated (aka "ASCIIZ") string from dbgAddr. It also stops at the first '$', in case this is + * a '$'-terminated string -- mainly because I'm lazy and didn't feel like writing a separate get() function. + * Yes, a zero-terminated string containing a '$' will be prematurely terminated, and no, I don't care. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {number} [cchMax] (default is 256) + * @return {string} (and dbgAddr advanced past the terminating zero) + */ + getSZ(dbgAddr, cchMax) + { + var s = ""; + cchMax = cchMax || 256; + while (s.length < cchMax) { + var b = this.getByte(dbgAddr, 1); + if (!b || b == 0x24 || b >= 127) break; + s += (b >= 32? String.fromCharCode(b) : '.'); + } + return s; + } + + /** + * dumpBlocks(aBlocks, sAddr) + * + * @this {Debugger8080} + * @param {Array} aBlocks + * @param {string} [sAddr] (optional block address) + */ + dumpBlocks(aBlocks, sAddr) + { + var addr = 0, i = 0, n = aBlocks.length; + + if (sAddr) { + addr = this.getAddr(this.parseAddr(sAddr)); + if (addr === CPUDef8080.ADDR_INVALID) { + this.println("invalid address: " + sAddr); + return; + } + i = addr >>> this.bus.nBlockShift; + n = 1; + } + + this.println("blockid physical blockaddr used size type"); + this.println("-------- --------- ---------- ------ ------ ----"); + + var typePrev = -1, cPrev = 0; + while (n--) { + var block = aBlocks[i]; + if (block.type == typePrev) { + if (!cPrev++) this.println("..."); + } else { + typePrev = block.type; + var sType = Memory8080.TYPE.NAMES[typePrev]; + if (block) { + this.println(Str.toHex(block.id) + " %" + Str.toHex(i << this.bus.nBlockShift) + " %%" + Str.toHex(block.addr) + " " + Str.toHexWord(block.used) + " " + Str.toHexWord(block.size) + " " + sType); + } + if (typePrev != Memory8080.TYPE.NONE) typePrev = -1; + cPrev = 0; + } + addr += this.bus.nBlockSize; + i++; + } + } + + /** + * dumpBus(asArgs) + * + * Dumps Bus allocations. + * + * @this {Debugger8080} + * @param {Array.} asArgs (asArgs[0] is an optional block address) + */ + dumpBus(asArgs) + { + this.dumpBlocks(this.bus.aMemBlocks, asArgs[0]); + } + + /** + * dumpHistory(sPrev, sLines) + * + * If sLines is not a number, it can be a instruction filter. However, for the moment, the only + * supported filter is "call", which filters the history buffer for all CALL and RET instructions + * from the specified previous point forward. + * + * @this {Debugger8080} + * @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10) + * @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10) + */ + dumpHistory(sPrev, sLines) + { + var sMore = ""; + var cHistory = 0; + var iHistory = this.iOpcodeHistory; + var aHistory = this.aOpcodeHistory; + + if (aHistory.length) { + var nPrev = +sPrev || this.nextHistory; + var nLines = +sLines || 10; + + if (isNaN(nPrev)) { + nPrev = nLines; + } else { + sMore = "more "; + } + + if (nPrev > aHistory.length) { + this.println("note: only " + aHistory.length + " available"); + nPrev = aHistory.length; + } + + iHistory -= nPrev; + if (iHistory < 0) { + /* + * If the dbgAddr of the last aHistory element contains a valid selector, wrap around. + */ + if (aHistory[aHistory.length - 1].addr == null) { + nPrev = iHistory + nPrev; + iHistory = 0; + } else { + iHistory += aHistory.length; + } + } + + var aFilters = []; + if (sLines == "call") { + nLines = 100000; + aFilters = ["CALL"]; + } + + if (sPrev !== undefined) { + this.println(nPrev + " instructions earlier:"); + } + + /* + * TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive) + * faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically + * increase the amount of memory used to store instruction history (eg, storing copies of all the instruction + * bytes alongside the execution addresses). + * + * For now, we're living dangerously, so that our history dumps actually work. + * + * this.nSuppressBreaks++; + * + * If you re-enable this protection, be sure to re-enable the decrement below, too. + */ + while (nLines > 0 && iHistory != this.iOpcodeHistory) { + + var dbgAddr = aHistory[iHistory++]; + if (dbgAddr.addr == null) break; + + /* + * We must create a new dbgAddr from the address in aHistory, because dbgAddr was + * a reference, not a copy, and we don't want getInstruction() modifying the original. + */ + var dbgAddrNew = this.newAddr(dbgAddr.addr); + + var sComment = "history"; + var nSequence = nPrev--; + if (DEBUG && dbgAddr.cycleCount != null) { + sComment = "cycles"; + nSequence = dbgAddr.cycleCount; + } + + var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence); + + if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) { + this.println(sInstruction); + } + + /* + * If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction() + * will have automatically disassembled additional bytes, so skip additional history entries. + */ + if (dbgAddrNew.cOverrides) { + iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides; + } + + if (iHistory >= aHistory.length) iHistory = 0; + this.nextHistory = nPrev; + cHistory++; + nLines--; + } + /* + * See comments above. + * + * this.nSuppressBreaks--; + */ + } + + if (!cHistory) { + this.println("no " + sMore + "history available"); + this.nextHistory = undefined; + } + } + + /** + * messageInit(sEnable) + * + * @this {Debugger8080} + * @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|' + */ + messageInit(sEnable) + { + this.dbg = this; + this.bitsMessage = this.bitsWarning = Messages8080.WARN; + this.sMessagePrev = null; + this.aMessageBuffer = []; + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". + */ + var aEnable = this.parseCommand(sEnable.replace("keys","key").replace("kbd","keyboard"), false, '|'); + if (aEnable.length) { + for (var m in Messages8080.CATEGORIES) { + if (Usr.indexOf(aEnable, m) >= 0) { + this.bitsMessage |= Messages8080.CATEGORIES[m]; + this.println(m + " messages enabled"); + } + } + } + } + + /** + * messageDump(bitMessage, fnDumper) + * + * @this {Debugger8080} + * @param {number} bitMessage is one Messages category flag + * @param {function(Array.)} fnDumper is a function the Debugger can use to dump data for that category + * @return {boolean} true if successfully registered, false if not + */ + messageDump(bitMessage, fnDumper) + { + for (var m in Messages8080.CATEGORIES) { + if (bitMessage == Messages8080.CATEGORIES[m]) { + this.afnDumpers[m] = fnDumper; + return true; + } + } + return false; + } + + /** + * getRegIndex(sReg, off) + * + * @this {Debugger8080} + * @param {string} sReg + * @param {number} [off] optional offset into sReg + * @return {number} register index, or -1 if not found + */ + getRegIndex(sReg, off) + { + var i; + sReg = sReg.toUpperCase(); + if (off == null) { + i = Usr.indexOf(Debugger8080.REGS, sReg); } else { - if (global[PC8080.APPCLASS] === undefined) { - global[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); }; + i = Usr.indexOf(Debugger8080.REGS, sReg.substr(off, 2)); + if (i < 0) i = Usr.indexOf(Debugger8080.REGS, sReg.substr(off, 1)); + } + return i; + } + + /** + * getRegString(iReg) + * + * @this {Debugger8080} + * @param {number} iReg + * @return {string} + */ + getRegString(iReg) + { + var cch = 0; + var n = this.getRegValue(iReg); + if (n !== undefined) { + switch(iReg) { + case Debugger8080.REG_A: + case Debugger8080.REG_B: + case Debugger8080.REG_C: + case Debugger8080.REG_D: + case Debugger8080.REG_E: + case Debugger8080.REG_H: + case Debugger8080.REG_L: + case Debugger8080.REG_M: + cch = 2; + break; + case Debugger8080.REG_BC: + case Debugger8080.REG_DE: + case Debugger8080.REG_HL: + case Debugger8080.REG_SP: + case Debugger8080.REG_PC: + case Debugger8080.REG_PS: + case Debugger8080.REG_PSW: + cch = 4; + break; + } + } + return cch? Str.toHex(n, cch) : "??"; + } + + /** + * getRegValue(iReg) + * + * @this {Debugger8080} + * @param {number} iReg + * @return {number|undefined} + */ + getRegValue(iReg) + { + var n; + if (iReg >= 0) { + var cpu = this.cpu; + switch(iReg) { + case Debugger8080.REG_A: + n = cpu.regA; + break; + case Debugger8080.REG_B: + n = cpu.regB; + break; + case Debugger8080.REG_C: + n = cpu.regC; + break; + case Debugger8080.REG_BC: + n = cpu.getBC(); + break; + case Debugger8080.REG_D: + n = cpu.regD; + break; + case Debugger8080.REG_E: + n = cpu.regE; + break; + case Debugger8080.REG_DE: + n = cpu.getDE(); + break; + case Debugger8080.REG_H: + n = cpu.regH; + break; + case Debugger8080.REG_L: + n = cpu.regL; + break; + case Debugger8080.REG_HL: + n = cpu.getHL(); + break; + case Debugger8080.REG_M: + n = cpu.getByte(cpu.getHL()); + break; + case Debugger8080.REG_SP: + n = cpu.getSP(); + break; + case Debugger8080.REG_PC: + n = cpu.getPC(); + break; + case Debugger8080.REG_PS: + n = cpu.getPS(); + break; + case Debugger8080.REG_PSW: + n = cpu.getPSW(); + break; + default: + break; + } + } + return n; + } + + /** + * replaceRegs(s) + * + * @this {Debugger8080} + * @param {string} s + * @return {string} + */ + replaceRegs(s) + { + /* + * Replace any references first; this means that register references inside the reference + * do NOT need to be prefixed with '@'. + */ + s = this.parseReference(s); + + /* + * Replace every @XX (or @XXX), where XX (or XXX) is a register, with the register's value. + */ + var i = 0; + var b, sChar, sAddr, dbgAddr, sReplace; + while ((i = s.indexOf('@', i)) >= 0) { + var iReg = this.getRegIndex(s, i + 1); + if (iReg >= 0) { + s = s.substr(0, i) + this.getRegString(iReg) + s.substr(i + 1 + Debugger8080.REGS[iReg].length); + } + i++; + } + /* + * Replace every #XX, where XX is a hex byte value, with the corresponding ASCII character (if printable). + */ + i = 0; + while ((i = s.indexOf('#', i)) >= 0) { + sChar = s.substr(i+1, 2); + b = Str.parseInt(sChar, 16); + if (b != null && b >= 32 && b < 128) { + sReplace = sChar + " '" + String.fromCharCode(b) + "'"; + s = s.replace('#' + sChar, sReplace); + i += sReplace.length; + continue; + } + i++; + } + /* + * Replace every $XXXX:XXXX, where XXXX:XXXX is a segmented address, with the zero-terminated string at that address. + */ + i = 0; + while ((i = s.indexOf('$', i)) >= 0) { + sAddr = s.substr(i+1, 9); + dbgAddr = this.parseAddr(sAddr); + if (dbgAddr) { + sReplace = sAddr + ' "' + this.getSZ(dbgAddr) + '"'; + s = s.replace('$' + sAddr, sReplace); + i += sReplace.length; + continue; + } + i++; + } + /* + * Replace every ^XXXX:XXXX, where XXXX:XXXX is a segmented address, with the FCB filename stored at that address. + */ + i = 0; + while ((i = s.indexOf('^', i)) >= 0) { + sAddr = s.substr(i+1, 9); + dbgAddr = this.parseAddr(sAddr); + if (dbgAddr) { + this.incAddr(dbgAddr); + sReplace = sAddr + ' "' + this.getSZ(dbgAddr, 11) + '"'; + s = s.replace('^' + sAddr, sReplace); + i += sReplace.length; + continue; + } + i++; + } + return s; + } + + /** + * message(sMessage, fAddress) + * + * @this {Debugger8080} + * @param {string} sMessage is any caller-defined message string + * @param {boolean} [fAddress] is true to display the current CS:IP + */ + message(sMessage, fAddress) + { + if (fAddress) { + sMessage += " at " + this.toHexAddr(this.newAddr(this.cpu.getPC())); + } + + if (this.bitsMessage & Messages8080.BUFFER) { + this.aMessageBuffer.push(sMessage); + return; + } + + if (this.sMessagePrev && sMessage == this.sMessagePrev) return; + this.sMessagePrev = sMessage; + + if (this.bitsMessage & Messages8080.HALT) { + this.stopCPU(); + sMessage += " (cpu halted)"; + } + + this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)" + + /* + * We have no idea what the frequency of println() calls might be; all we know is that they easily + * screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every + * message, to effectively end the current burst and start fresh. + * + * TODO: See CPU8080.calcStartTime() for a discussion of why we might want to call yieldCPU() *before* + * we display the message. + */ + if (this.cpu) this.cpu.yieldCPU(); + } + + /** + * messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage) + * + * Most (if not all) port handlers should provide a name for their respective ports, so if no name is provided, + * we assume this is an unknown port, and display a message by default. + * + * @this {Debugger8080} + * @param {Component} component + * @param {number} port + * @param {number|null} bOut if an output operation + * @param {number|null} [addrFrom] + * @param {string|null} [name] of the port, if any + * @param {number|null} [bIn] is the input value, if known, on an input operation + * @param {number} [bitsMessage] is one or more Messages category flag(s) + */ + messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage) + { + bitsMessage |= Messages8080.PORT; + if (name == null || (this.bitsMessage & bitsMessage) == bitsMessage) { + this.message(component.idComponent + '.' + (bOut != null? "outPort" : "inPort") + '(' + Str.toHexWord(port) + ',' + (name? name : "unknown") + (bOut != null? ',' + Str.toHexByte(bOut) : "") + ')' + (bIn != null? (": " + Str.toHexByte(bIn)) : "") + (addrFrom != null? (" at " + this.toHexOffset(addrFrom)) : "")); + } + } + + /** + * init() + * + * @this {Debugger8080} + */ + init() + { + this.println("Type ? for help with PC8080 Debugger commands"); + this.updateStatus(); + if (this.sInitCommands) { + var sCmds = this.sInitCommands; + this.sInitCommands = null; + this.doCommands(sCmds); + } + } + + /** + * historyInit(fQuiet) + * + * This function is intended to be called by the constructor, reset(), addBreakpoint(), findBreakpoint() + * and any other function that changes the checksEnabled() criteria used to decide whether checkInstruction() + * should be called. + * + * That is, if the history arrays need to be allocated and haven't already been allocated, then allocate them, + * and if the arrays are no longer needed, then deallocate them. + * + * @this {Debugger8080} + * @param {boolean} [fQuiet] + */ + historyInit(fQuiet) + { + var i; + if (!this.checksEnabled()) { + if (this.aOpcodeHistory && this.aOpcodeHistory.length && !fQuiet) { + this.println("instruction history buffer freed"); + } + this.iOpcodeHistory = 0; + this.aOpcodeHistory = []; + this.aaOpcodeCounts = []; + return; + } + if (!this.aOpcodeHistory || !this.aOpcodeHistory.length) { + this.aOpcodeHistory = new Array(Debugger8080.HISTORY_LIMIT); + for (i = 0; i < this.aOpcodeHistory.length; i++) { + /* + * Preallocate dummy Addr (Array) objects in every history slot, so that + * checkInstruction() doesn't need to call newAddr() on every slot update. + */ + this.aOpcodeHistory[i] = this.newAddr(); + } + this.iOpcodeHistory = 0; + if (!fQuiet) { + this.println("instruction history buffer allocated"); + } + } + if (!this.aaOpcodeCounts || !this.aaOpcodeCounts.length) { + this.aaOpcodeCounts = new Array(256); + for (i = 0; i < this.aaOpcodeCounts.length; i++) { + this.aaOpcodeCounts[i] = [i, 0]; + } + } + } + + /** + * runCPU(fUpdateFocus) + * + * @this {Debugger8080} + * @param {boolean} [fUpdateFocus] is true to update focus + * @return {boolean} true if run request successful, false if not + */ + runCPU(fUpdateFocus) + { + if (!this.isCPUAvail()) return false; + this.cpu.runCPU(fUpdateFocus); + return true; + } + + /** + * stepCPU(nCycles, fRegs, fUpdateCPU) + * + * @this {Debugger8080} + * @param {number} nCycles (0 for one instruction without checking breakpoints) + * @param {boolean} [fRegs] is true to display registers after step (default is false) + * @param {boolean} [fUpdateCPU] is false to disable calls to updateCPU() (default is true) + * @return {boolean} + */ + stepCPU(nCycles, fRegs, fUpdateCPU) + { + if (!this.isCPUAvail()) return false; + + this.nCycles = 0; + + if (!nCycles) { + /* + * When single-stepping, the CPU won't call checkInstruction(), which is good for + * avoiding breakpoints, but bad for instruction data collection if checks are enabled. + * So we call checkInstruction() ourselves. + */ + if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0); + } + try { + var nCyclesStep = this.cpu.stepCPU(nCycles); + if (nCyclesStep > 0) { + this.nCycles += nCyclesStep; + this.cpu.addCycles(nCyclesStep, true); + this.cpu.updateChecksum(nCyclesStep); + this.cOpcodes++; + } + } + catch(exception) { + if (typeof exception != "number") { + var e = exception; + this.nCycles = 0; + this.cpu.setError(e.stack || e.message); } } - } // endif DEBUGGER + /* + * Because we called cpu.stepCPU() and not cpu.runCPU(), we must nudge the cpu's update code, + * and then update our own state. Normally, the only time fUpdateCPU will be false is when doTrace() + * is calling us in a loop, in which case it will perform its own updateCPU() when it's done. + */ + if (fUpdateCPU !== false) this.cpu.updateCPU(); + + this.updateStatus(fRegs || false); + return (this.nCycles > 0); + } + + /** + * stopCPU() + * + * @this {Debugger8080} + * @param {boolean} [fComplete] + */ + stopCPU(fComplete) + { + if (this.cpu) this.cpu.stopCPU(fComplete); + } + + /** + * updateStatus(fRegs) + * + * @this {Debugger8080} + * @param {boolean} [fRegs] (default is true) + */ + updateStatus(fRegs) + { + if (fRegs === undefined) fRegs = true; + + this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); + /* + * this.nStep used to be a simple boolean, but now it's 0 (or undefined) + * if inactive, 1 if stepping over an instruction without a register dump, or 2 + * if stepping over an instruction with a register dump. + */ + if (!fRegs || this.nStep == 1) + this.doUnassemble(); + else { + this.doRegisters(); + } + } + + /** + * isCPUAvail() + * + * Make sure the CPU is ready (finished initializing), not busy (already running), and not in an error state. + * + * @this {Debugger8080} + * @return {boolean} + */ + isCPUAvail() + { + if (!this.cpu) + return false; + if (!this.cpu.isReady()) + return false; + if (!this.cpu.isPowered()) + return false; + if (this.cpu.isBusy()) + return false; + return !this.cpu.isError(); + } + + /** + * powerUp(data, fRepower) + * + * @this {Debugger8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + /* + * Because Debugger save/restore support is somewhat limited (and didn't always exist), + * we deviate from the typical save/restore design pattern: instead of reset OR restore, + * we always reset and then perform a (potentially limited) restore. + */ + this.reset(true); + + // this.println(data? "resuming" : "powering up"); + + if (data && this.restore) { + if (!this.restore(data)) return false; + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {Debugger8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} + */ + powerDown(fSave, fShutdown) + { + if (fShutdown) this.println(fSave? "suspending" : "shutting down"); + return fSave? this.save() : true; + } + + /** + * reset(fQuiet) + * + * This is a notification handler, called by the Computer, to inform us of a reset. + * + * @this {Debugger8080} + * @param {boolean} fQuiet (true only when called from our own powerUp handler) + */ + reset(fQuiet) + { + this.historyInit(); + this.cOpcodes = this.cOpcodesStart = 0; + this.sMessagePrev = null; + this.nCycles = 0; + this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); + /* + * fRunning is set by start() and cleared by stop(). In addition, we clear + * it here, so that if the CPU is reset while running, we can prevent stop() + * from unnecessarily dumping the CPU state. + */ + this.flags.running = false; + this.clearTempBreakpoint(); + if (!fQuiet) this.updateStatus(); + } + + /** + * save() + * + * This implements (very rudimentary) save support for the Debugger component. + * + * @this {Debugger8080} + * @return {Object} + */ + save() + { + var state = new State(this); + state.set(0, this.packAddr(this.dbgAddrNextCode)); + state.set(1, this.packAddr(this.dbgAddrAssemble)); + state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]); + state.set(3, this.aSymbolTable); + return state.data(); + } + + /** + * restore(data) + * + * This implements (very rudimentary) restore support for the Debugger component. + * + * @this {Debugger8080} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + var i = 0; + if (data[2] !== undefined) { + this.dbgAddrNextCode = this.unpackAddr(data[i++]); + this.dbgAddrAssemble = this.unpackAddr(data[i++]); + this.aPrevCmds = data[i][0]; + if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds]; + this.fAssemble = data[i][1]; + this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state + } + if (data[3]) this.aSymbolTable = data[3]; + return true; + } + + /** + * start(ms, nCycles) + * + * This is a notification handler, called by the Computer, to inform us the CPU has started. + * + * @this {Debugger8080} + * @param {number} ms + * @param {number} nCycles + */ + start(ms, nCycles) + { + if (!this.nStep) this.println("running"); + this.flags.running = true; + this.msStart = ms; + this.nCyclesStart = nCycles; + } + + /** + * stop(ms, nCycles) + * + * This is a notification handler, called by the Computer, to inform us the CPU has now stopped. + * + * @this {Debugger8080} + * @param {number} ms + * @param {number} nCycles + */ + stop(ms, nCycles) + { + if (this.flags.running) { + this.flags.running = false; + this.nCycles = nCycles - this.nCyclesStart; + if (!this.nStep) { + var sStopped = "stopped"; + if (this.nCycles) { + var msTotal = ms - this.msStart; + var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0); + sStopped += " ("; + if (this.checksEnabled()) { + sStopped += this.cOpcodes + " opcodes, "; + /* + * $ops displays progress by calculating cOpcodes - cOpcodesStart, so before + * zeroing cOpcodes, we should subtract cOpcodes from cOpcodesStart (since we're + * effectively subtracting cOpcodes from cOpcodes as well). + */ + this.cOpcodesStart -= this.cOpcodes; + this.cOpcodes = 0; + } + sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)"; + } else { + if (this.messageEnabled(Messages8080.HALT)) { + /* + * It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault() + * for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful + * message (another helpful tip would be to simply turn off the "halt" message category). + */ + sStopped += " (use the 't' command to execute blocked faults)"; + } + } + this.println(sStopped); + } + this.updateStatus(true); + this.updateFocus(); + this.clearTempBreakpoint(this.cpu.getPC()); + } + } + + /** + * checksEnabled(fRelease) + * + * This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked. + * + * Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints + * no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate + * "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've + * simplified the test below. + * + * @this {Debugger8080} + * @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria) + * @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not + */ + checksEnabled(fRelease) + { + return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakIns)); + } + + /** + * checkInstruction(addr, nState) + * + * This "check" function is called by the CPU to inform us about the next instruction to be executed, + * giving us an opportunity to look for "exec" breakpoints and update opcode frequencies and instruction history. + * + * @this {Debugger8080} + * @param {number} addr + * @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running + * @return {boolean} true if breakpoint hit, false if not + */ + checkInstruction(addr, nState) + { + var cpu = this.cpu; + + if (nState > 0) { + if (this.nBreakIns && !--this.nBreakIns) { + return true; + } + if (this.checkBreakpoint(addr, 1, this.aBreakExec)) { + return true; + } + } + + /* + * The rest of the instruction tracking logic can only be performed if historyInit() has allocated the + * necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than + * adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction(); + * well, OK, and a few other things now, like enabling Messages8080.INT messages. + */ + if (nState >= 0 && this.aaOpcodeCounts.length) { + this.cOpcodes++; + var bOpcode = this.bus.getByteDirect(addr); + if (bOpcode != null) { + this.aaOpcodeCounts[bOpcode][1]++; + var dbgAddr = this.aOpcodeHistory[this.iOpcodeHistory]; + this.setAddr(dbgAddr, cpu.getPC()); + if (DEBUG) dbgAddr.cycleCount = cpu.getCycles(); + if (++this.iOpcodeHistory == this.aOpcodeHistory.length) this.iOpcodeHistory = 0; + } + } + return false; + } + + /** + * checkMemoryRead(addr, nb) + * + * This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an + * opportunity to track the read if we want, and look for a matching "read" breakpoint, if any. + * + * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now + * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read. + * + * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. + * + * @this {Debugger8080} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + checkMemoryRead(addr, nb) + { + if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) { + this.stopCPU(true); + return true; + } + return false; + } + + /** + * checkMemoryWrite(addr, nb) + * + * This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an + * opportunity to track the write if we want, and look for a matching "write" breakpoint, if any. + * + * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now + * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write. + * + * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. + * + * @this {Debugger8080} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + checkMemoryWrite(addr, nb) + { + if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) { + this.stopCPU(true); + return true; + } + return false; + } + + /** + * checkPortInput(port, size, data) + * + * This "check" function is called by the Bus component to inform us that port input occurred. + * + * @this {Debugger8080} + * @param {number} port + * @param {number} size + * @param {number} data + * @return {boolean} true if breakpoint hit, false if not + */ + checkPortInput(port, size, data) + { + /* + * We trust that the Bus component won't call us unless we told it to, so we halt unconditionally + */ + this.println("break on input from port " + Str.toHexWord(port) + ": " + Str.toHex(data)); + this.stopCPU(true); + return true; + } + + /** + * checkPortOutput(port, size, data) + * + * This "check" function is called by the Bus component to inform us that port output occurred. + * + * @this {Debugger8080} + * @param {number} port + * @param {number} size + * @param {number} data + * @return {boolean} true if breakpoint hit, false if not + */ + checkPortOutput(port, size, data) + { + /* + * We trust that the Bus component won't call us unless we told it to, so we halt unconditionally + */ + this.println("break on output to port " + Str.toHexWord(port) + ": " + Str.toHex(data)); + this.stopCPU(true); + return true; + } + + /** + * clearBreakpoints() + * + * @this {Debugger8080} + */ + clearBreakpoints() + { + var i, dbgAddr; + this.aBreakExec = ["bp"]; + if (this.aBreakRead !== undefined) { + for (i = 1; i < this.aBreakRead.length; i++) { + dbgAddr = this.aBreakRead[i]; + this.bus.removeMemBreak(this.getAddr(dbgAddr), false); + } + } + this.aBreakRead = ["br"]; + if (this.aBreakWrite !== undefined) { + for (i = 1; i < this.aBreakWrite.length; i++) { + dbgAddr = this.aBreakWrite[i]; + this.bus.removeMemBreak(this.getAddr(dbgAddr), true); + } + } + this.aBreakWrite = ["bw"]; + /* + * nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup requires + * reading a segment descriptor via getSegment(), and that triggers more memory reads, which triggers + * more breakpoint checks. + */ + this.nSuppressBreaks = 0; + } + + /** + * addBreakpoint(aBreak, dbgAddr, fTemporary) + * + * In case you haven't already figured this out, all our breakpoint commands use the address + * to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers; + * see doBreak() for details. + * + * This has a few implications, one being that you CANNOT set more than one kind of breakpoint + * on a single address. In practice, that's rarely a problem, because you can almost always set + * a different breakpoint on a neighboring address. + * + * Also, there is one exception to the "one address, one breakpoint" rule, and that involves + * temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command + * may create to step over a chunk of code). Those breakpoints automatically clear themselves, + * so there usually isn't any need to refer to them using breakpoint commands. + * + * TODO: Consider supporting the more "traditional" breakpoint index syntax; the current + * address-based syntax was implemented solely for expediency and consistency. At the same time, + * also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific + * breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc. + * + * @this {Debugger8080} + * @param {Array} aBreak + * @param {DbgAddr8080} dbgAddr + * @param {boolean} [fTemporary] + * @return {boolean} true if breakpoint added, false if already exists + */ + addBreakpoint(aBreak, dbgAddr, fTemporary) + { + var fSuccess = true; + + // this.nSuppressBreaks++; + + /* + * Instead of complaining that a breakpoint already exists (as we used to do), we now + * allow breakpoints to be re-set; this makes it easier to update any commands that may + * be associated with the breakpoint. + * + * The only exception: we DO allow a temporary breakpoint at an address where there may + * already be a breakpoint, so that you can easily step ("p" or "g") over such addresses. + */ + if (!fTemporary) { + this.findBreakpoint(aBreak, dbgAddr, true, false, true); + } + + if (aBreak != this.aBreakExec) { + var addr = this.getAddr(dbgAddr); + if (addr === CPUDef8080.ADDR_INVALID) { + this.println("invalid address: " + this.toHexAddr(dbgAddr)); + fSuccess = false; + } else { + this.bus.addMemBreak(addr, aBreak == this.aBreakWrite); + } + } + + if (fSuccess) { + aBreak.push(dbgAddr); + if (fTemporary) { + dbgAddr.fTemporary = true; + } + else { + this.printBreakpoint(aBreak, aBreak.length-1, "set"); + this.historyInit(); + } + } + + // this.nSuppressBreaks--; + + return fSuccess; + } + + /** + * findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) + * + * @this {Debugger8080} + * @param {Array} aBreak + * @param {DbgAddr8080} dbgAddr + * @param {boolean} [fRemove] + * @param {boolean} [fTemporary] + * @param {boolean} [fQuiet] + * @return {boolean} true if found, false if not + */ + findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) + { + var fFound = false; + var addr = this.getAddr(dbgAddr); + for (var i = 1; i < aBreak.length; i++) { + var dbgAddrBreak = aBreak[i]; + if (addr == this.getAddr(dbgAddrBreak)) { + if (!fTemporary || dbgAddrBreak.fTemporary) { + fFound = true; + if (fRemove) { + if (!dbgAddrBreak.fTemporary && !fQuiet) { + this.printBreakpoint(aBreak, i, "cleared"); + } + aBreak.splice(i, 1); + if (aBreak != this.aBreakExec) { + this.bus.removeMemBreak(addr, aBreak == this.aBreakWrite); + } + /* + * We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this + * was a temporary breakpoint. + */ + if (!dbgAddrBreak.fTemporary) { + this.historyInit(); + } + break; + } + if (!fQuiet) this.printBreakpoint(aBreak, i, "exists"); + break; + } + } + } + return fFound; + } + + /** + * listBreakpoints(aBreak) + * + * @this {Debugger8080} + * @param {Array} aBreak + * @return {number} of breakpoints listed, 0 if none + */ + listBreakpoints(aBreak) + { + for (var i = 1; i < aBreak.length; i++) { + this.printBreakpoint(aBreak, i); + } + return aBreak.length - 1; + } + + /** + * printBreakpoint(aBreak, i, sAction) + * + * @this {Debugger8080} + * @param {Array} aBreak + * @param {number} i + * @param {string} [sAction] + */ + printBreakpoint(aBreak, i, sAction) + { + var dbgAddr = aBreak[i]; + this.println(aBreak[0] + ' ' + this.toHexAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : ''))); + } + + /** + * setTempBreakpoint(dbgAddr) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr of new temp breakpoint + */ + setTempBreakpoint(dbgAddr) + { + this.addBreakpoint(this.aBreakExec, dbgAddr, true); + } + + /** + * clearTempBreakpoint(addr) + * + * @this {Debugger8080} + * @param {number|undefined} [addr] clear all temp breakpoints if no address specified + */ + clearTempBreakpoint(addr) + { + if (addr !== undefined) { + this.checkBreakpoint(addr, 1, this.aBreakExec, true); + this.nStep = 0; + } else { + for (var i = 1; i < this.aBreakExec.length; i++) { + var dbgAddrBreak = this.aBreakExec[i]; + if (dbgAddrBreak.fTemporary) { + if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break; + i = 0; + } + } + } + } + + /** + * checkBreakpoint(addr, nb, aBreak, fTemporary) + * + * @this {Debugger8080} + * @param {number} addr + * @param {number} nb (# of bytes) + * @param {Array} aBreak + * @param {boolean} [fTemporary] + * @return {boolean} true if breakpoint has been hit, false if not + */ + checkBreakpoint(addr, nb, aBreak, fTemporary) + { + /* + * Time to check for execution breakpoints; note that this should be done BEFORE updating frequency + * or history data (see checkInstruction), since we might not actually execute the current instruction. + */ + var fBreak = false; + + if (!this.nSuppressBreaks++) { + + for (var i = 1; !fBreak && i < aBreak.length; i++) { + + var dbgAddrBreak = aBreak[i]; + + if (fTemporary && !dbgAddrBreak.fTemporary) continue; + + /* + * We used to calculate the linear address of the breakpoint at the time the + * breakpoint was added, so that a breakpoint set in one mode (eg, in real-mode) + * would still work as intended if the mode changed later (eg, to protected-mode). + * + * However, that created difficulties setting protected-mode breakpoints in segments + * that might not be defined yet, or that could move in physical memory. + * + * If you want to create a real-mode breakpoint that will break regardless of mode, + * use the physical address of the real-mode memory location instead. + */ + var addrBreak = this.getAddr(dbgAddrBreak); + for (var n = 0; n < nb; n++) { + if (addr + n == addrBreak) { + var a; + fBreak = true; + if (dbgAddrBreak.fTemporary) { + this.findBreakpoint(aBreak, dbgAddrBreak, true, true); + fTemporary = true; + } + if (a = dbgAddrBreak.aCmds) { + /* + * When one or more commands are attached to a breakpoint, we don't halt by default. + * Instead, we set fBreak to true only if, at the completion of all the commands, the + * CPU is halted; in other words, you should include "h" as one of the breakpoint commands + * if you want the breakpoint to stop execution. + * + * Another useful command is "if", which will return false if the expression is false, + * at which point we'll jump ahead to the next "else" command, and if there isn't an "else", + * we abort. + */ + fBreak = false; + for (var j = 0; j < a.length; j++) { + if (!this.doCommand(a[j], true)) { + if (a[j].indexOf("if")) { + fBreak = true; // the failed command wasn't "if", so abort + break; + } + var k = j + 1; + for (; k < a.length; k++) { + if (!a[k].indexOf("else")) break; + j++; + } + if (k == a.length) { // couldn't find an "else" after the "if", so abort + fBreak = true; + break; + } + /* + * If we're still here, we'll execute the "else" command (which is just a no-op), + * followed by any remaining commands. + */ + } + } + if (!this.cpu.isRunning()) fBreak = true; + } + if (fBreak) { + if (!fTemporary) this.printBreakpoint(aBreak, i, "hit"); + break; + } + } + } + } + } + + this.nSuppressBreaks--; + + return fBreak; + } + + /** + * getInstruction(dbgAddr, sComment, nSequence) + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {string} [sComment] is an associated comment + * @param {number|null} [nSequence] is an associated sequence number, undefined if none + * @return {string} (and dbgAddr is updated to the next instruction) + */ + getInstruction(dbgAddr, sComment, nSequence) + { + var dbgAddrIns = this.newAddr(dbgAddr.addr); + + var bOpcode = this.getByte(dbgAddr, 1); + + var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086; + var aOpDesc = this.aaOpDescs[bOpcode]; + var iIns = aOpDesc[0]; + + var sOperands = ""; + var sOpcode = asOpcodes[iIns]; + var cOperands = aOpDesc.length - 1; + var typeSizeDefault = Debugger8080.TYPE_NONE, type; + + for (var iOperand = 1; iOperand <= cOperands; iOperand++) { + + var disp, off, cch; + var sOperand = ""; + + type = aOpDesc[iOperand]; + if (type === undefined) continue; + if ((type & Debugger8080.TYPE_OPT) && this.style == Debugger8080.STYLE_8080) continue; + + var typeMode = type & Debugger8080.TYPE_MODE; + if (!typeMode) continue; + + var typeSize = type & Debugger8080.TYPE_SIZE; + if (!typeSize) { + type |= typeSizeDefault; + } else { + typeSizeDefault = typeSize; + } + + var typeOther = type & Debugger8080.TYPE_OTHER; + if (!typeOther) { + type |= (iOperand == 1? Debugger8080.TYPE_OUT : Debugger8080.TYPE_IN); + } + + if (typeMode & Debugger8080.TYPE_IMM) { + sOperand = this.getImmOperand(type, dbgAddr); + } + else if (typeMode & Debugger8080.TYPE_REG) { + sOperand = this.getRegOperand((type & Debugger8080.TYPE_IREG) >> 8, type, dbgAddr); + } + else if (typeMode & Debugger8080.TYPE_INT) { + sOperand = ((bOpcode >> 3) & 0x7).toString(); + } + + if (!sOperand || !sOperand.length) { + sOperands = "INVALID"; + break; + } + if (sOperands.length > 0) sOperands += ','; + sOperands += (sOperand || "???"); + } + + var sBytes = ""; + var sLine = this.toHexAddr(dbgAddrIns) + ' '; + if (dbgAddrIns.addr !== CPUDef8080.ADDR_INVALID && dbgAddr.addr !== CPUDef8080.ADDR_INVALID) { + do { + sBytes += Str.toHex(this.getByte(dbgAddrIns, 1), 2); + if (dbgAddrIns.addr == null) break; + } while (dbgAddrIns.addr != dbgAddr.addr); + } + + sLine += Str.pad(sBytes, 10); + sLine += (type & Debugger8080.TYPE_UNDOC)? '*' : ' '; + sLine += Str.pad(sOpcode, 7); + if (sOperands) sLine += ' ' + sOperands; + + if (sComment) { + sLine = Str.pad(sLine, 40) + ';' + sComment; + if (!this.cpu.flags.checksum) { + sLine += (nSequence != null? '=' + nSequence.toString() : ""); + } else { + var nCycles = this.cpu.getCycles(); + sLine += "cycles=" + nCycles.toString() + " cs=" + Str.toHex(this.cpu.nChecksum); + } + } + return sLine; + } + + /** + * getImmOperand(type, dbgAddr) + * + * @this {Debugger8080} + * @param {number} type + * @param {DbgAddr8080} dbgAddr + * @return {string} operand + */ + getImmOperand(type, dbgAddr) + { + var sOperand = ' '; + var typeSize = type & Debugger8080.TYPE_SIZE; + + switch (typeSize) { + case Debugger8080.TYPE_BYTE: + sOperand = Str.toHex(this.getByte(dbgAddr, 1), 2); + break; + case Debugger8080.TYPE_SBYTE: + sOperand = Str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4); + break; + case Debugger8080.TYPE_WORD: + sOperand = Str.toHex(this.getShort(dbgAddr, 2), 4); + break; + default: + return "imm(" + Str.toHexWord(type) + ')'; + } + if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) { + sOperand = '[' + sOperand + ']'; + } else if (!(type & Debugger8080.TYPE_REG)) { + sOperand = (this.style == Debugger8080.STYLE_8080? '$' : "0x") + sOperand; + } + return sOperand; + } + + /** + * getRegOperand(iReg, type, dbgAddr) + * + * @this {Debugger8080} + * @param {number} iReg + * @param {number} type + * @param {DbgAddr8080} dbgAddr + * @return {string} operand + */ + getRegOperand(iReg, type, dbgAddr) + { + /* + * Although this breaks with 8080 assembler conventions, I'm going to experiment with some different + * mnemonics; specifically, "[HL]" instead of "M". This is also more in keeping with how getImmOperand() + * displays memory references (ie, by enclosing them in brackets). + */ + var sOperand = Debugger8080.REGS[iReg]; + if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) { + if (iReg == Debugger8080.REG_M) { + sOperand = "HL"; + } + sOperand = '[' + sOperand + ']'; + } + return sOperand; + } + + /** + * parseInstruction(sOp, sOperand, addr) + * + * TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a working implementation. + * + * @this {Debugger8080} + * @param {string} sOp + * @param {string|undefined} sOperand + * @param {DbgAddr8080} dbgAddr of memory where this instruction is being assembled + * @return {Array.} of opcode bytes; if the instruction can't be parsed, the array will be empty + */ + parseInstruction(sOp, sOperand, dbgAddr) + { + var aOpBytes = []; + this.println("not supported yet"); + return aOpBytes; + } + + /** + * getFlagOutput(sFlag) + * + * @this {Debugger8080} + * @param {string} sFlag + * @return {string} value of flag + */ + getFlagOutput(sFlag) + { + var b; + switch (sFlag) { + case "IF": + b = this.cpu.getIF(); + break; + case "SF": + b = this.cpu.getSF(); + break; + case "ZF": + b = this.cpu.getZF(); + break; + case "AF": + b = this.cpu.getAF(); + break; + case "PF": + b = this.cpu.getPF(); + break; + case "CF": + b = this.cpu.getCF(); + break; + default: + b = 0; + break; + } + return sFlag.charAt(0) + (b? '1' : '0') + ' '; + } + + /** + * getRegOutput(iReg) + * + * @this {Debugger8080} + * @param {number} iReg + * @return {string} + */ + getRegOutput(iReg) + { + var sReg = Debugger8080.REGS[iReg]; + return sReg + '=' + this.getRegString(iReg) + ' '; + } + + /** + * getRegDump() + * + * Sample 8080 register dump: + * + * A=00 BC=0000 DE=0000 HL=0000 SP=0000 I0 S0 Z0 A0 P0 C0 + * 0000 00 NOP + * + * @this {Debugger8080} + * @return {string} + */ + getRegDump() + { + var s; + s = this.getRegOutput(Debugger8080.REG_A) + + this.getRegOutput(Debugger8080.REG_BC) + + this.getRegOutput(Debugger8080.REG_DE) + + this.getRegOutput(Debugger8080.REG_HL) + + this.getRegOutput(Debugger8080.REG_SP) + + this.getFlagOutput("IF") + this.getFlagOutput("SF") + this.getFlagOutput("ZF") + + this.getFlagOutput("AF") + this.getFlagOutput("PF") + this.getFlagOutput("CF"); + return s; + } + + /** + * comparePairs(p1, p2) + * + * @this {Debugger8080} + * @param {number|string|Array|Object} p1 + * @param {number|string|Array|Object} p2 + * @return {number} + */ + comparePairs(p1, p2) + { + return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0; + } + + /** + * addSymbols(sModule, addr, len, aSymbols) + * + * As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist + * of all the symbols (in upper-case), and the values of those properties are objects containing any or all of + * the following properties: + * + * 'v': the value of an absolute (unsized) value + * 'b': either 1, 2, 4 or undefined if an unsized value + * 's': either a hard-coded segment or undefined + * 'o': the offset of the symbol within the associated address space + * 'l': the original-case version of the symbol, present only if it wasn't originally upper-case + * 'a': annotation for the specified offset; eg, the original assembly language, with optional comment + * + * To that list of properties, we also add: + * + * 'p': the physical address (calculated whenever both 's' and 'o' properties are defined) + * + * Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values + * for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original + * annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for + * converting them back to individual double-quotes. + * + * For example: + * { + * 'HF_PORT': { + * 'v':800 + * }, + * 'HDISK_INT': { + * 'b':4, 's':0, 'o':52 + * }, + * 'ORG_VECTOR': { + * 'b':4, 's':0, 'o':76 + * }, + * 'CMD_BLOCK': { + * 'b':1, 's':64, 'o':66 + * }, + * 'DISK_SETUP': { + * 'o':3 + * }, + * '.40': { + * 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR" + * } + * } + * + * If a symbol only has an offset, then that offset value can be assigned to the symbol property directly: + * + * 'DISK_SETUP': 3 + * + * The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol. + * Such entries are identified by a period followed by a unique number (usually the offset of the entry), and + * they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading + * period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols. + * + * We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which + * look like: + * + * [addr, len, aSymbols, aOffsets] + * + * There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any, + * at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous + * symbol with a matching offset ('o') property. + * + * To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs + * (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space. + * + * We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert(); + * it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're + * hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates + * properly. + * + * @this {Debugger8080} + * @param {string|null} sModule + * @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM) + * @param {number} len (the size of the region, in bytes) + * @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below) + */ + addSymbols(sModule, addr, len, aSymbols) + { + var dbgAddr = {}; + var aOffsets = []; + for (var sSymbol in aSymbols) { + var symbol = aSymbols[sSymbol]; + if (typeof symbol == "number") { + aSymbols[sSymbol] = symbol = {'o': symbol}; + } + var offSymbol = symbol['o']; + var sAnnotation = symbol['a']; + if (offSymbol !== undefined) { + Usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs); + } + if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\""); + } + var symbolTable = { + sModule: sModule, + addr: addr, + len: len, + aSymbols: aSymbols, + aOffsets: aOffsets + }; + this.aSymbolTable.push(symbolTable); + } + + /** + * dumpSymbols() + * + * TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever + * order they appeared in the original MAP file. + * + * @this {Debugger8080} + */ + dumpSymbols() + { + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + for (var sSymbol in symbolTable.aSymbols) { + if (sSymbol.charAt(0) == '.') continue; + var symbol = symbolTable.aSymbols[sSymbol]; + var offSymbol = symbol['o']; + if (offSymbol === undefined) continue; + var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l']; + if (sSymbolOrig) sSymbol = sSymbolOrig; + this.println(this.toHexOffset(offSymbol) + ' ' + sSymbol); + } + } + } + + /** + * findSymbol(dbgAddr, fNearest) + * + * Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found). + * + * If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of + * entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @param {boolean} [fNearest] + * @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment + */ + findSymbol(dbgAddr, fNearest) + { + var aSymbol = []; + var addrSymbol = this.getAddr(dbgAddr) >>> 0; + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + var addr = symbolTable.addr >>> 0; + var len = symbolTable.len; + if (addrSymbol >= addr && addrSymbol < addr + len) { + var offSymbol = addrSymbol - addr; + var result = Usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs); + if (result >= 0) { + this.returnSymbol(iTable, result, aSymbol); + } + else if (fNearest) { + result = ~result; + this.returnSymbol(iTable, result-1, aSymbol); + this.returnSymbol(iTable, result, aSymbol); + } + break; + } + } + return aSymbol; + } + + /** + * findSymbolAddr(sSymbol) + * + * Search aSymbolTable for sSymbol, and if found, return a dbgAddr (same as parseAddr()) + * + * @this {Debugger8080} + * @param {string} sSymbol + * @return {DbgAddr8080|undefined} + */ + findSymbolAddr(sSymbol) + { + var dbgAddr; + if (sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) { + var sUpperCase = sSymbol.toUpperCase(); + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + var symbol = symbolTable.aSymbols[sUpperCase]; + if (symbol !== undefined) { + var offSymbol = symbol['o']; + if (offSymbol !== undefined) { + /* + * We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an + * explicitly-defined segment, we return the segment associated with the entire group; for + * a ROM, that segment is normally "addrROM >>> 4". Down the road, we may want/need to + * support a special symbol entry (eg, ".ORG") that defines an alternate origin. + */ + dbgAddr = this.newAddr(offSymbol); + } + /* + * The symbol matched, but it wasn't for an address (no 'o' offset), and there's no point + * looking any farther, since each symbol appears only once, so we indicate it's an unknown symbol. + */ + break; + } + } + } + return dbgAddr; + } + + /** + * returnSymbol(iTable, iOffset, aSymbol) + * + * Helper function for findSymbol(). + * + * @param {number} iTable + * @param {number} iOffset + * @param {Array} aSymbol is updated with the specified symbol, if it exists + */ + returnSymbol(iTable, iOffset, aSymbol) + { + var symbol = {}; + var aOffsets = this.aSymbolTable[iTable].aOffsets; + var offset = 0, sSymbol = null; + if (iOffset >= 0 && iOffset < aOffsets.length) { + offset = aOffsets[iOffset][0]; + sSymbol = aOffsets[iOffset][1]; + } + if (sSymbol) { + symbol = this.aSymbolTable[iTable].aSymbols[sSymbol]; + sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol)); + } + aSymbol.push(sSymbol); + aSymbol.push(offset); + aSymbol.push(symbol['a']); + aSymbol.push(symbol['c']); + } + + /** + * doHelp() + * + * @this {Debugger8080} + */ + doHelp() + { + var s = "commands:"; + for (var sCommand in Debugger8080.COMMANDS) { + s += '\n' + Str.pad(sCommand, 9) + Debugger8080.COMMANDS[sCommand]; + } + if (!this.checksEnabled()) s += "\nnote: frequency/history disabled if no exec breakpoints"; + this.println(s); + } + + /** + * doAssemble(asArgs) + * + * This always receives the complete argument array, where the order of the arguments is: + * + * [0]: the assemble command (assumed to be "a") + * [1]: the target address (eg, "200") + * [2]: the operation code, aka instruction name (eg, "adc") + * [3]: the operation mode operand, if any (eg, "14", "[1234]", etc) + * + * The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present. + * As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands + * until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a" + * and the next available target address to the argument array. + * + * Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg: + * + * a ff00 cld + * a ff01 ldx 28 + * ... + * + * without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage + * of automatic target address advancement (see dbgAddrAssemble). + * + * NOTE: As the previous example implies, you can even assemble new instructions into ROM address space; + * as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU. + * + * @this {Debugger8080} + * @param {Array.} asArgs is the complete argument array, beginning with the "a" command in asArgs[0] + */ + doAssemble(asArgs) + { + var dbgAddr = this.parseAddr(asArgs[1], true); + if (!dbgAddr) return; + + this.dbgAddrAssemble = dbgAddr; + if (asArgs[2] === undefined) { + this.println("begin assemble at " + this.toHexAddr(dbgAddr)); + this.fAssemble = true; + this.cpu.updateCPU(); + return; + } + + var aOpBytes = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr); + if (aOpBytes.length) { + for (var i = 0; i < aOpBytes.length; i++) { + this.setByte(dbgAddr, aOpBytes[i], 1); + } + /* + * Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced. + */ + this.println(this.getInstruction(this.dbgAddrAssemble)); + } + } + + /** + * doBreak(sCmd, sAddr, sOptions) + * + * As the "help" output below indicates, the following breakpoint commands are supported: + * + * bp [a] set exec breakpoint on linear addr [a] + * br [a] set read breakpoint on linear addr [a] + * bw [a] set write breakpoint on linear addr [a] + * bc [a] clear breakpoint on linear addr [a] (use "*" for all breakpoints) + * bl list breakpoints + * + * to which we have recently added the following I/O breakpoint commands: + * + * bi [p] toggle input breakpoint on port [p] (use "*" for all input ports) + * bo [p] toggle output breakpoint on port [p] (use "*" for all output ports) + * + * These two new commands operate as toggles so that if "*" is used to trap all input (or output), + * you can also use these commands to NOT trap specific ports. + * + * bn [n] break after [n] instructions + * + * TODO: Update the "bl" command to include any/all I/O breakpoints, and the "bc" command to + * clear them. Because "bi" and "bo" commands are piggy-backing on Bus functions, those breakpoints + * are currently outside the realm of what the "bl" and "bc" commands are aware of. + * + * @this {Debugger8080} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + */ + doBreak(sCmd, sAddr, sOptions) + { + if (sAddr == '?') { + this.println("breakpoint commands:"); + this.println("\tbi [p]\ttoggle break on input port [p]"); + this.println("\tbo [p]\ttoggle break on output port [p]"); + this.println("\tbp [a]\tset exec breakpoint at addr [a]"); + this.println("\tbr [a]\tset read breakpoint at addr [a]"); + this.println("\tbw [a]\tset write breakpoint at addr [a]"); + this.println("\tbc [a]\tclear breakpoint at addr [a]"); + this.println("\tbl\tlist all breakpoints"); + this.println("\tbn [n]\tbreak after [n] instruction(s)"); + return; + } + + var sParm = sCmd.charAt(1); + if (sParm == 'l') { + var cBreaks = 0; + cBreaks += this.listBreakpoints(this.aBreakExec); + cBreaks += this.listBreakpoints(this.aBreakRead); + cBreaks += this.listBreakpoints(this.aBreakWrite); + if (!cBreaks) this.println("no breakpoints"); + return; + } + + if (sParm == 'n') { + this.nBreakIns = this.parseValue(sAddr); + this.println("break after " + this.nBreakIns + " instruction(s)"); + return; + } + + if (sAddr === undefined) { + this.println("missing breakpoint address"); + return; + } + + var dbgAddr = this.newAddr(); + if (sAddr != '*') { + dbgAddr = this.parseAddr(sAddr, true, true); + if (!dbgAddr) return; + } + + sAddr = Str.toHexWord(dbgAddr.addr); + + if (sParm == 'c') { + if (dbgAddr.addr == null) { + this.clearBreakpoints(); + this.println("all breakpoints cleared"); + return; + } + if (this.findBreakpoint(this.aBreakExec, dbgAddr, true)) + return; + if (this.findBreakpoint(this.aBreakRead, dbgAddr, true)) + return; + if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true)) + return; + this.println("breakpoint missing: " + this.toHexAddr(dbgAddr)); + return; + } + + if (sParm == 'i') { + this.println("breakpoint " + (this.bus.addPortInputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (input)"); + return; + } + + if (sParm == 'o') { + this.println("breakpoint " + (this.bus.addPortOutputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (output)"); + return; + } + + if (dbgAddr.addr == null) return; + + this.parseAddrOptions(dbgAddr, sOptions); + + if (sParm == 'p') { + this.addBreakpoint(this.aBreakExec, dbgAddr); + return; + } + if (sParm == 'r') { + this.addBreakpoint(this.aBreakRead, dbgAddr); + return; + } + if (sParm == 'w') { + this.addBreakpoint(this.aBreakWrite, dbgAddr); + return; + } + this.println("unknown breakpoint command: " + sParm); + } + + /** + * doClear(sCmd) + * + * @this {Debugger8080} + * @param {string} [sCmd] (eg, "cls" or "clear") + */ + doClear(sCmd) + { + /* + * TODO: There should be a clear() component method that the Control Panel overrides to perform this function. + */ + if (this.controlPrint) this.controlPrint.value = ""; + } + + /** + * doDump(asArgs) + * + * The length parameter is interpreted as a number of bytes, in hex, which we convert to the appropriate number + * of lines, because we always display whole lines. If the length is omitted/undefined, it defaults to 0x80 (128.) + * bytes, which normally translates to 8 lines. + * + * @this {Debugger8080} + * @param {Array.} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes]) + */ + doDump(asArgs) + { + var m; + var sCmd = asArgs[0]; + var sAddr = asArgs[1]; + var sLen = asArgs[2]; + var sBytes = asArgs[3]; + + if (sAddr == '?') { + var sDumpers = ""; + for (m in Messages8080.CATEGORIES) { + if (this.afnDumpers[m]) { + if (sDumpers) sDumpers += ','; + sDumpers = sDumpers + m; + } + } + sDumpers += ",state,symbols"; + this.println("dump memory commands:"); + this.println("\tdb [a] [#] dump # bytes at address a"); + this.println("\tdw [a] [#] dump # words at address a"); + this.println("\tdd [a] [#] dump # dwords at address a"); + this.println("\tdh [#] [#] dump # instructions from history"); + if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers); + return; + } + + if (sAddr == "state") { + var sState = this.cmp.powerOff(true); + if (sLen == "console") { + /* + * Console buffers are notoriously small, and even the following code, which breaks the + * data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful. + * + * var nPart = +sBytes; + * if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000); + * + * So, the best way to capture a large machine state is to use the new "Save Machine" link + * that downloads a machine's entire state. Alternatively, run your own local server and use + * server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML + * control to the computer.powerOff() and computer.saveServerState() functions. + */ + console.log(sState); + } else { + this.doClear(); + if (sState) this.println(sState); + } + return; + } + + if (sAddr == "symbols") { + this.dumpSymbols(); + return; + } + + if (sCmd == "d") { + for (m in Messages8080.CATEGORIES) { + if (asArgs[1] == m) { + var fnDumper = this.afnDumpers[m]; + if (fnDumper) { + asArgs.shift(); + asArgs.shift(); + fnDumper(asArgs); + } else { + this.println("no dump registered for " + sAddr); + } + return; + } + } + if (!sAddr) sCmd = this.sCmdDumpPrev || "db"; + } else { + this.sCmdDumpPrev = sCmd; + } + + if (sCmd == "dh") { + this.dumpHistory(sAddr, sLen); + return; + } + + var dbgAddr = this.parseAddr(sAddr); + if (!dbgAddr) return; + + var len = 0; // 0 is not a default; it triggers the appropriate default below + if (sLen) { + if (sLen.charAt(0) == 'l') { + sLen = sLen.substr(1) || sBytes; + } + len = this.parseValue(sLen) >>> 0; // negative lengths not allowed + if (len > 0x10000) len = 0x10000; // prevent bad user (or variable) input from producing excessive output + } + + var sDump = ""; + var size = (sCmd == "dd"? 4 : (sCmd == "dw"? 2 : 1)); + var cb = (size * len) || 128; + var cLines = ((cb + 15) >> 4) || 1; + + while (cLines-- && cb > 0) { + var data = 0, iByte = 0, i; + var sData = "", sChars = ""; + sAddr = this.toHexAddr(dbgAddr); + for (i = 16; i > 0 && cb > 0; i--) { + var b = this.getByte(dbgAddr, 1); + data |= (b << (iByte++ << 3)); + if (iByte == size) { + sData += Str.toHex(data, size * 2); + sData += (size == 1? (i == 9? '-' : ' ') : " "); + data = iByte = 0; + } + sChars += (b >= 32 && b < 128? String.fromCharCode(b) : '.'); + cb--; + } + if (sDump) sDump += '\n'; + sDump += sAddr + " " + sData + ((i == 0)? (' ' + sChars) : ""); + } + + if (sDump) this.println(sDump); + this.dbgAddrNextData = dbgAddr; + } + + /** + * doEdit(asArgs) + * + * @this {Debugger8080} + * @param {Array.} asArgs + */ + doEdit(asArgs) + { + var size = 1; + var mask = 0xff; + var fnGet = this.getByte; + var fnSet = this.setByte; + if (asArgs[0] == "ew") { + size = 2; + mask = 0xffff; + fnGet = this.getShort; + fnSet = this.setShort; + } + var cch = size << 1; + + var sAddr = asArgs[1]; + if (sAddr == null) { + this.println("edit memory commands:"); + this.println("\teb [a] [...] edit bytes at address a"); + this.println("\tew [a] [...] edit words at address a"); + return; + } + + var dbgAddr = this.parseAddr(sAddr); + if (!dbgAddr) return; + + for (var i = 2; i < asArgs.length; i++) { + var vNew = this.parseExpression(asArgs[i]); + if (vNew === undefined) { + this.println("unrecognized value: " + asArgs[i]); + break; + } + if (vNew & ~mask) { + this.println("warning: " + Str.toHex(vNew) + " exceeds " + size + "-byte value"); + } + var vOld = fnGet.call(this, dbgAddr); + this.println("changing " + this.toHexAddr(dbgAddr) + " from " + Str.toHex(vOld, cch, true) + " to " + Str.toHex(vNew, cch, true)); + fnSet.call(this, dbgAddr, vNew, size); + } + } + + /** + * doFreqs(sParm) + * + * @this {Debugger8080} + * @param {string|undefined} sParm + */ + doFreqs(sParm) + { + if (sParm == '?') { + this.println("frequency commands:"); + this.println("\tclear\tclear all frequency counts"); + return; + } + var i; + var cData = 0; + if (this.aaOpcodeCounts) { + if (sParm == "clear") { + for (i = 0; i < this.aaOpcodeCounts.length; i++) + this.aaOpcodeCounts[i] = [i, 0]; + this.println("frequency data cleared"); + cData++; + } + else if (sParm !== undefined) { + this.println("unknown frequency command: " + sParm); + cData++; + } + else { + var aaSortedOpcodeCounts = this.aaOpcodeCounts.slice(); + aaSortedOpcodeCounts.sort(function(p, q) { + return q[1] - p[1]; + }); + var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086; + for (i = 0; i < aaSortedOpcodeCounts.length; i++) { + var bOpcode = aaSortedOpcodeCounts[i][0]; + var cFreq = aaSortedOpcodeCounts[i][1]; + if (cFreq) { + this.println((asOpcodes[this.aaOpDescs[bOpcode][0]] + " ").substr(0, 5) + " (" + Str.toHexByte(bOpcode) + "): " + cFreq + " times"); + cData++; + } + } + } + } + if (!cData) { + this.println("no frequency data available"); + } + } + + /** + * doHalt(fQuiet) + * + * @this {Debugger8080} + * @param {boolean} [fQuiet] + */ + doHalt(fQuiet) + { + var sMsg; + if (this.flags.running) { + sMsg = "halting"; + this.stopCPU(); + } else { + if (this.isBusy(true)) return; + sMsg = "already halted"; + } + if (!fQuiet) this.println(sMsg); + } + + /** + * doIf(sCmd, fQuiet) + * + * NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate + * an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period + * (eg, "10.") if you really intend decimal. + * + * Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes + * "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true). + * + * @this {Debugger8080} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error) + */ + doIf(sCmd, fQuiet) + { + sCmd = Str.trim(sCmd); + if (!this.parseExpression(sCmd)) { + if (!fQuiet) this.println("false: " + sCmd); + return false; + } + if (!fQuiet) this.println("true: " + sCmd); + return true; + } + + /** + * doInfo(asArgs) + * + * @this {Debugger8080} + * @param {Array.} asArgs + * @return {boolean} true only if the instruction info command ("n") is supported + */ + doInfo(asArgs) + { + if (DEBUG) { + this.println("msPerYield: " + this.cpu.msPerYield); + this.println("nCyclesPerYield: " + this.cpu.nCyclesPerYield); + return true; + } + return false; + } + + /** + * doInput(sPort) + * + * Simulate a 1-byte port input operation. + * + * @this {Debugger8080} + * @param {string|undefined} sPort + */ + doInput(sPort) + { + if (!sPort || sPort == '?') { + this.println("input commands:"); + this.println("\ti [p]\tread port [p]"); + /* + * TODO: Regarding this warning, consider adding an "unchecked" version of + * bus.checkPortInputNotify(), since all Debugger memory accesses are unchecked, too. + * + * All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined), + * but changing them all to be non-destructive would take time, and situations where you + * actually want to affect the hardware state are just as likely as not.... + */ + this.println("warning: port accesses can affect hardware state"); + return; + } + var port = this.parseValue(sPort); + if (port !== undefined) { + var bIn = this.bus.checkPortInputNotify(port, 1); + this.println(Str.toHexWord(port) + ": " + Str.toHexByte(bIn)); + } + } + + /** + * doInt(sLevel) + * + * @this {Debugger8080} + * @param {string} sLevel + * @return {boolean} true if success, false if error + */ + doInt(sLevel) + { + if (!this.cpu.getIF()) { + this.println("interrupts disabled (use rif=1 to enable)"); + return false; + } + var nLevel = this.parseExpression(sLevel); + if (nLevel == null) return false; + this.println("requesting interrupt level " + nLevel); + this.cpu.requestINTR(nLevel); + return true; + } + + /** + * doVar(sCmd) + * + * The command must be of the form "{variable} = [{expression}]", where expression may contain constants, + * operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if + * any, is deleted. + * + * Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}" + * prints the value of the specified variable. + * + * @this {Debugger8080} + * @param {string} sCmd + * @return {boolean} true if valid "var" assignment, false if not + */ + doVar(sCmd) + { + var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i); + if (a) { + if (!a[1]) { + if (!this.printVariable()) this.println("no variables"); + return true; // it's not considered an error to print an empty list of variables + } + if (!a[2]) { + return this.printVariable(a[1]); + } + if (!a[3]) { + this.delVariable(a[1]); + return true; // it's not considered an error to delete a variable that didn't exist + } + var v = this.parseExpression(a[3]); + if (v !== undefined) { + this.setVariable(a[1], v); + return true; + } + return false; + } + this.println("invalid assignment:" + sCmd); + return false; + } + + /** + * doList(sAddr, fPrint) + * + * @this {Debugger8080} + * @param {string} sAddr + * @param {boolean} [fPrint] + * @return {string|null} + */ + doList(sAddr, fPrint) + { + var sSymbol = null; + + var dbgAddr = this.parseAddr(sAddr, true); + if (dbgAddr) { + var addr = this.getAddr(dbgAddr); + var aSymbol = this.findSymbol(dbgAddr, true); + if (aSymbol.length) { + var nDelta, sDelta, s; + if (aSymbol[0]) { + sDelta = ""; + nDelta = dbgAddr.addr - aSymbol[1]; + if (nDelta) sDelta = " + " + Str.toHexWord(nDelta); + s = aSymbol[0] + " (" + this.toHexOffset(aSymbol[1]) + ')' + sDelta; + if (fPrint) this.println(s); + sSymbol = s; + } + if (aSymbol.length > 4 && aSymbol[4]) { + sDelta = ""; + nDelta = aSymbol[5] - dbgAddr.addr; + if (nDelta) sDelta = " - " + Str.toHexWord(nDelta); + s = aSymbol[4] + " (" + this.toHexOffset(aSymbol[5]) + ')' + sDelta; + if (fPrint) this.println(s); + if (!sSymbol) sSymbol = s; + } + } else { + if (fPrint) this.println("no symbols"); + } + } + return sSymbol; + } + + /** + * doMessages(asArgs) + * + * @this {Debugger8080} + * @param {Array.} asArgs + */ + doMessages(asArgs) + { + var m; + var fCriteria = null; + var sCategory = asArgs[1]; + if (sCategory == '?') sCategory = undefined; + + if (sCategory !== undefined) { + var bitsMessage = 0; + if (sCategory == "all") { + bitsMessage = (0xffffffff|0) & ~(Messages8080.HALT | Messages8080.KEYS | Messages8080.LOG); + sCategory = null; + } else if (sCategory == "on") { + fCriteria = true; + sCategory = null; + } else if (sCategory == "off") { + fCriteria = false; + sCategory = null; + } else { + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". + */ + if (sCategory == "keys") sCategory = "key"; + if (sCategory == "kbd") sCategory = "keyboard"; + for (m in Messages8080.CATEGORIES) { + if (sCategory == m) { + bitsMessage = Messages8080.CATEGORIES[m]; + fCriteria = !!(this.bitsMessage & bitsMessage); + break; + } + } + if (!bitsMessage) { + this.println("unknown message category: " + sCategory); + return; + } + } + if (bitsMessage) { + if (asArgs[2] == "on") { + this.bitsMessage |= bitsMessage; + fCriteria = true; + } + else if (asArgs[2] == "off") { + this.bitsMessage &= ~bitsMessage; + fCriteria = false; + if (bitsMessage == Messages8080.BUFFER) { + for (var i = 0; i < this.aMessageBuffer.length; i++) { + this.println(this.aMessageBuffer[i]); + } + this.aMessageBuffer = []; + } + } + } + } + + /* + * Display those message categories that match the current criteria (on or off) + */ + var n = 0; + var sCategories = ""; + for (m in Messages8080.CATEGORIES) { + if (!sCategory || sCategory == m) { + var bitMessage = Messages8080.CATEGORIES[m]; + var fEnabled = !!(this.bitsMessage & bitMessage); + if (fCriteria !== null && fCriteria != fEnabled) continue; + if (sCategories) sCategories += ','; + if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys". + */ + if (m == "key") m = "keys"; + sCategories += m; + } + } + + if (sCategory === undefined) { + this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off"); + } + + this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none")); + + this.historyInit(); // call this just in case Messages8080.INT was turned on + } + + /** + * doOptions(asArgs) + * + * @this {Debugger8080} + * @param {Array.} asArgs + */ + doOptions(asArgs) + { + switch (asArgs[1]) { + case "8080": + this.style = Debugger8080.STYLE_8080; + break; + + case "8086": + this.style = Debugger8080.STYLE_8086; + break; + + case "cs": + var nCycles; + if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion + switch (asArgs[2]) { + case "int": + this.cpu.nCyclesChecksumInterval = nCycles; + break; + case "start": + this.cpu.nCyclesChecksumStart = nCycles; + break; + case "stop": + this.cpu.nCyclesChecksumStop = nCycles; + break; + default: + this.println("unknown cs option"); + return; + } + if (nCycles !== undefined) { + this.cpu.resetChecksum(); + } + this.println("checksums " + (this.cpu.flags.checksum? "enabled" : "disabled")); + return; + + case "sp": + if (asArgs[2] !== undefined) { + if (!this.cpu.setSpeed(+asArgs[2])) { + this.println("warning: using 1x multiplier, previous target not reached"); + } + } + this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)"); + return; + + case "?": + this.println("debugger options:"); + this.println("\t8080\t\tselect 8080-style mnemonics"); + this.println("\t8086\t\tselect 8086-style mnemonics"); + this.println("\tcs int #\tset checksum cycle interval to #"); + this.println("\tcs start #\tset checksum cycle start count to #"); + this.println("\tcs stop #\tset checksum cycle stop count to #"); + this.println("\tsp #\t\tset speed multiplier to #"); + break; + + default: + if (asArgs[1]) { + this.println("unknown option: " + asArgs[1]); + return; + } + break; + } + this.println(this.style + "-style mnemonics enabled"); + } + + /** + * doOutput(sPort, sByte) + * + * Simulate a 1-byte port output operation. + * + * @this {Debugger8080} + * @param {string|undefined} sPort + * @param {string|undefined} sByte (string representation of 1 byte) + */ + doOutput(sPort, sByte) + { + if (!sPort || sPort == '?') { + this.println("output commands:"); + this.println("\to [p] [b]\twrite byte [b] to port [p]"); + /* + * TODO: Regarding this warning, consider adding an "unchecked" version of + * bus.checkPortOutputNotify(), since all Debugger memory accesses are unchecked, too. + * + * All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined), + * but changing them all to be non-destructive would take time, and situations where you + * actually want to affect the hardware state are just as likely as not.... + */ + this.println("warning: port accesses can affect hardware state"); + return; + } + var port = this.parseValue(sPort, "port #"); + var bOut = this.parseValue(sByte); + if (port !== undefined && bOut !== undefined) { + this.bus.checkPortOutputNotify(port, 1, bOut); + this.println(Str.toHexWord(port) + ": " + Str.toHexByte(bOut)); + } + } + + /** + * doRegisters(asArgs, fInstruction) + * + * @this {Debugger8080} + * @param {Array.} [asArgs] + * @param {boolean} [fInstruction] (true to include the current instruction; default is true) + */ + doRegisters(asArgs, fInstruction) + { + if (asArgs && asArgs[1] == '?') { + this.println("register commands:"); + this.println("\tr\tdump registers"); + this.println("\trx [#]\tset flag or register x to [#]"); + return; + } + + var cpu = this.cpu; + if (fInstruction == null) fInstruction = true; + + if (asArgs != null && asArgs.length > 1) { + var sReg = asArgs[1]; + var sValue = null; + var i = sReg.indexOf('='); + if (i > 0) { + sValue = sReg.substr(i + 1); + sReg = sReg.substr(0, i); + } + else if (asArgs.length > 2) { + sValue = asArgs[2]; + } + else { + this.println("missing value for " + asArgs[1]); + return; + } + + var w = this.parseExpression(sValue); + if (w === undefined) return; + + var sRegMatch = sReg.toUpperCase(); + switch (sRegMatch) { + case "A": + cpu.regA = w & 0xff; + break; + case "B": + cpu.regB = w & 0xff; + break; + case "BC": + cpu.regB = ((w >> 8) & 0xff); + /* falls through */ + case "C": + cpu.regC = w & 0xff; + break; + case "D": + cpu.regD = w & 0xff; + break; + case "DE": + cpu.regD = ((w >> 8) & 0xff); + /* falls through */ + case "E": + cpu.regE = w & 0xff; + break; + case "H": + cpu.regH = w & 0xff; + break; + case "HL": + cpu.regH = ((w >> 8) & 0xff); + /* falls through */ + case "L": + cpu.regL = w & 0xff; + break; + case "SP": + cpu.setSP(w); + break; + case "PC": + cpu.setPC(w); + this.dbgAddrNextCode = this.newAddr(cpu.getPC()); + break; + case "PS": + cpu.setPS(w); + break; + case "PSW": + cpu.setPSW(w); + break; + case "CF": + if (w) cpu.setCF(); else cpu.clearCF(); + break; + case "PF": + if (w) cpu.setPF(); else cpu.clearPF(); + break; + case "AF": + if (w) cpu.setAF(); else cpu.clearAF(); + break; + case "ZF": + if (w) cpu.setZF(); else cpu.clearZF(); + break; + case "SF": + if (w) cpu.setSF(); else cpu.clearSF(); + break; + case "IF": + if (w) cpu.setIF(); else cpu.clearIF(); + break; + default: + this.println("unknown register: " + sReg); + return; + } + cpu.updateCPU(); + this.println("updated registers:"); + } + + this.println(this.getRegDump()); + + if (fInstruction) { + this.dbgAddrNextCode = this.newAddr(cpu.getPC()); + this.doUnassemble(this.toHexAddr(this.dbgAddrNextCode)); + } + } + + /** + * doRun(sCmd, sAddr, sOptions, fQuiet) + * + * @this {Debugger8080} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + * @param {boolean} [fQuiet] + */ + doRun(sCmd, sAddr, sOptions, fQuiet) + { + if (sCmd == "gt") { + this.fIgnoreNextCheckFault = true; + } + if (sAddr !== undefined) { + var dbgAddr = this.parseAddr(sAddr, true); + if (!dbgAddr) return; + this.parseAddrOptions(dbgAddr, sOptions); + this.setTempBreakpoint(dbgAddr); + } + if (!this.runCPU(true)) { + if (!fQuiet) this.println("cpu busy or unavailable, run command ignored"); + } + } + + /** + * doPrint(sCmd) + * + * NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that + * you can take advantage of all the special replacement options used for software interrupt logging. + * + * @this {Debugger8080} + * @param {string} sCmd + */ + doPrint(sCmd) + { + sCmd = Str.trim(sCmd); + var a = sCmd.match(/^(['"])(.*?)\1$/); + if (!a) { + this.parseExpression(sCmd, true); + } else { + this.println(this.replaceRegs(a[2])); + } + } + + /** + * doStep(sCmd) + * + * @this {Debugger8080} + * @param {string} [sCmd] "p" or "pr" + */ + doStep(sCmd) + { + var fCallStep = true; + var fRegs = (sCmd == "pr"? 1 : 0); + /* + * Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants + * a subsequent register dump ("pr") or not ("p"). + */ + var nStep = 1 + fRegs; + if (!this.nStep) { + var dbgAddr = this.newAddr(this.cpu.getPC()); + var bOpcode = this.getByte(dbgAddr); + + switch (bOpcode) { + case CPUDef8080.OPCODE.CALL: + if (fCallStep) { + this.nStep = nStep; + this.incAddr(dbgAddr, 3); + } + break; + default: + break; + } + + if (this.nStep) { + this.setTempBreakpoint(dbgAddr); + if (!this.runCPU()) { + if (this.cmp) this.cmp.updateFocus(); + this.nStep = 0; + } + /* + * A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(), + * which will clear nStep, so there's your assurance that nStep will be reset. Now we may have + * stopped for reasons unrelated to the temporary breakpoint, but that's OK. + */ + } else { + this.doTrace(fRegs? "tr" : "t"); + } + } else { + this.println("step in progress"); + } + } + + /** + * getCall(dbgAddr) + * + * Given a possible return address (typically from the stack), look for a matching CALL (or INT) that + * immediately precedes that address. + * + * @this {Debugger8080} + * @param {DbgAddr8080} dbgAddr + * @return {string|null} CALL instruction at or near dbgAddr, or null if none + */ + getCall(dbgAddr) + { + var sCall = null; + var addr = dbgAddr.addr; + var addrOrig = addr; + for (var n = 1; n <= 6 && !!addr; n++) { + if (n > 2) { + dbgAddr.addr = addr; + var s = this.getInstruction(dbgAddr); + if (s.indexOf("CALL") >= 0) { + /* + * Verify that the length of this CALL (or INT), when added to the address of the CALL (or INT), + * matches the original return address. We do this by getting the string index of the opcode bytes, + * subtracting that from the string index of the next space, and dividing that difference by two, + * to yield the length of the CALL (or INT) instruction, in bytes. + */ + var i = s.indexOf(' '); + var j = s.indexOf(' ', i+1); + if (addr + (j - i - 1)/2 == addrOrig) { + sCall = s; + break; + } + } + } + addr--; + } + dbgAddr.addr = addrOrig; + return sCall; + } + + /** + * doStackTrace(sCmd, sAddr) + * + * Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info. + * + * @this {Debugger8080} + * @param {string} [sCmd] + * @param {string} [sAddr] (not used yet) + */ + doStackTrace(sCmd, sAddr) + { + if (sAddr == '?') { + this.println("stack trace commands:"); + this.println("\tk\tshow frame addresses"); + this.println("\tks\tshow symbol information"); + return; + } + + var nFrames = 10, cFrames = 0; + var dbgAddrCall = this.newAddr(); + var dbgAddrStack = this.newAddr(this.cpu.getSP()); + this.println("stack trace for " + this.toHexAddr(dbgAddrStack)); + + while (cFrames < nFrames) { + var sCall = null, sCallPrev = null, cTests = 256; + while ((dbgAddrStack.addr >>> 0) < 0x10000) { + dbgAddrCall.addr = this.getWord(dbgAddrStack, true); + /* + * Because we're using the auto-increment feature of getWord(), and because that will automatically + * wrap the offset around the end of the segment, we must also check the addr property to detect the wrap. + */ + if (dbgAddrStack.addr == null || !cTests--) break; + sCall = this.getCall(dbgAddrCall); + if (sCall) break; + } + /* + * The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always) + * indicative of a false positive, in which case the previous call is probably bogus as well, but + * at least we won't duplicate that mistake. Of course, there are always exceptions, recursion + * being one of them, but it's rare that we're debugging recursive code. + */ + if (!sCall || sCall == sCallPrev) break; + var sSymbol = null; + if (sCmd == "ks") { + var a = sCall.match(/[0-9A-F]+$/); + if (a) sSymbol = this.doList(a[0]); + } + sCall = Str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toHexAddr(dbgAddrStack)); // + " return=" + this.toHexAddr(dbgAddrCall)); + this.println(sCall); + sCallPrev = sCall; + cFrames++; + } + if (!cFrames) this.println("no return addresses found"); + } + + /** + * doTrace(sCmd, sCount) + * + * The "t" and "tr" commands interpret the count as a number of instructions, and since + * we call the Debugger's stepCPU() for each iteration, a single instruction includes + * any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only + * difference between "t" and "tr": the former displays only the next instruction, while + * the latter also displays the (updated) registers. + * + * The "tc" command interprets the count as a number of cycles rather than instructions, + * allowing you to quickly execute large chunks of instructions with a single command; it + * doesn't display anything until the the chunk has finished. + * + * However, generally a more useful command is "bn", which allows you to break after some + * number of instructions have been executed (as opposed to some number of cycles). + * + * @this {Debugger8080} + * @param {string} [sCmd] ("t", "tc", or "tr") + * @param {string} [sCount] # of instructions to step + */ + doTrace(sCmd, sCount) + { + var dbg = this; + var fRegs = (sCmd != "t"); + var nCount = this.parseValue(sCount, null, true) || 1; + var nCycles = (nCount == 1? 0 : 1); + if (sCmd == "tc") { + nCycles = nCount; + nCount = 1; + } + Web.onCountRepeat( + nCount, + function onCountStep() { + return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false); + }, + function onCountStepComplete() { + /* + * We explicitly called stepCPU() with fUpdateCPU === false, because repeatedly + * calling updateCPU() can be very slow, especially when fDisplayLiveRegs is true, + * so once the repeat count has been exhausted, we must perform a final updateCPU(). + */ + dbg.cpu.updateCPU(); + dbg.setBusy(false); + } + ); + } + + /** + * doUnassemble(sAddr, sAddrEnd, n) + * + * @this {Debugger8080} + * @param {string} [sAddr] + * @param {string} [sAddrEnd] + * @param {number} [n] + */ + doUnassemble(sAddr, sAddrEnd, n) + { + var dbgAddr = this.parseAddr(sAddr, true); + if (!dbgAddr) return; + + if (n === undefined) n = 1; + + var cb = 0x100; + if (sAddrEnd !== undefined) { + + var dbgAddrEnd = this.parseAddr(sAddrEnd, true); + if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return; + + cb = dbgAddrEnd.addr - dbgAddr.addr; + if (!DEBUG && cb > 0x100) { + /* + * Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to + * prevent the user from wedging the browser by dumping too many lines, but also a recognition + * that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway. + */ + this.println("range too large"); + return; + } + n = -1; + } + + var cLines = 0; + var sInstruction; + + while (cb > 0 && n--) { + + var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null; + var sComment = (nSequence != null? "cycles" : null); + var aSymbol = this.findSymbol(dbgAddr); + + var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it + + if (aSymbol[0] && n) { + if (!cLines && n || aSymbol[0].indexOf('+') < 0) { + var sLabel = aSymbol[0] + ':'; + if (aSymbol[2]) sLabel += ' ' + aSymbol[2]; + this.println(sLabel); + } + } + + if (aSymbol[3]) { + sComment = aSymbol[3]; + nSequence = null; + } + + sInstruction = this.getInstruction(dbgAddr, sComment, nSequence); + + this.println(sInstruction); + this.dbgAddrNextCode = dbgAddr; + cb -= dbgAddr.addr - addr; + cLines++; + } + } + + /** + * parseCommand(sCmd, fSave, chSep) + * + * @this {Debugger8080} + * @param {string|undefined} sCmd + * @param {boolean} [fSave] is true to save the command, false if not + * @param {string} [chSep] is the command separator character (default is ';') + * @return {Array.} + */ + parseCommand(sCmd, fSave, chSep) + { + if (fSave) { + if (!sCmd) { + if (this.fAssemble) { + sCmd = "end"; + } else { + sCmd = this.aPrevCmds[this.iPrevCmd+1]; + } + } else { + if (this.iPrevCmd < 0 && this.aPrevCmds.length) { + this.iPrevCmd = 0; + } + if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) { + this.aPrevCmds.splice(0, 0, sCmd); + this.iPrevCmd = 0; + } + this.iPrevCmd--; + } + } + var a = []; + if (sCmd) { + /* + * With the introduction of breakpoint commands (ie, quoted command sequences + * associated with a breakpoint), we can no longer perform simplistic splitting. + * + * a = sCmd.split(chSep || ';'); + * for (var i = 0; i < a.length; i++) a[i] = str.trim(a[i]); + * + * We may now split on semi-colons ONLY if they are outside a quoted sequence. + * + * Also, to allow quoted strings *inside* breakpoint commands, we first replace all + * DOUBLE double-quotes with single quotes. + */ + sCmd = sCmd.toLowerCase().replace(/""/g, "'"); + + var iPrev = 0; + var chQuote = null; + chSep = chSep || ';'; + /* + * NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking + * advantage of the fact that charAt() with an invalid index returns an empty string, + * allowing us to use the same substring() call to capture the final portion of sCmd. + * + * In a sense, it allows us to pretend that the string ends with a zero terminator. + */ + for (var i = 0; i <= sCmd.length; i++) { + var ch = sCmd.charAt(i); + if (ch == '"' || ch == "'") { + if (!chQuote) { + chQuote = ch; + } else if (ch == chQuote) { + chQuote = null; + } + } + else if (ch == chSep && !chQuote || !ch) { + /* + * Recall that substring() accepts starting (inclusive) and ending (exclusive) + * indexes, whereas substr() accepts a starting index and a length. We need the former. + */ + a.push(Str.trim(sCmd.substring(iPrev, i))); + iPrev = i + 1; + } + } + } + return a; + } + + /** + * shiftArgs(asArgs) + * + * Used with any command (eg, "r") that allows but doesn't require whitespace between command and first argument. + * + * @this {Debugger8080} + * @param {Array.} asArgs + * @return {Array.} + */ + shiftArgs(asArgs) + { + if (asArgs && asArgs.length) { + var s0 = asArgs[0]; + var ch0 = s0.charAt(0); + for (var i = 1; i < s0.length; i++) { + var ch = s0.charAt(i); + if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') { + asArgs[0] = s0.substr(i); + asArgs.unshift(s0.substr(0, i)); + break; + } + } + } + return asArgs; + } + + /** + * doCommand(sCmd, fQuiet) + * + * @this {Debugger8080} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if command processed, false if unrecognized + */ + doCommand(sCmd, fQuiet) + { + var result = true; + + try { + if (!sCmd.length || sCmd == "end") { + if (this.fAssemble) { + this.println("ended assemble at " + this.toHexAddr(this.dbgAddrAssemble)); + this.dbgAddrNextCode = this.dbgAddrAssemble; + this.fAssemble = false; + } + sCmd = ""; + } + else if (!fQuiet) { + var sPrompt = ">> "; + this.println(sPrompt + sCmd); + } + + var ch = sCmd.charAt(0); + if (ch == '"' || ch == "'") return true; + + /* + * Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat. + */ + this.sMessagePrev = null; + + /* + * I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally. + */ + if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) { + + if (this.fAssemble) { + sCmd = "a " + this.toHexAddr(this.dbgAddrAssemble) + ' ' + sCmd; + } + + var asArgs = this.shiftArgs(sCmd.replace(/ +/g, ' ').split(' ')); + + switch (asArgs[0].charAt(0)) { + case 'a': + this.doAssemble(asArgs); + break; + case 'b': + this.doBreak(asArgs[0], asArgs[1], sCmd); + break; + case 'c': + this.doClear(asArgs[0]); + break; + case 'd': + if (!COMPILED && sCmd == "debug") { + window.DEBUG = true; + this.println("DEBUG checks on"); + break; + } + this.doDump(asArgs); + break; + case 'e': + if (asArgs[0] == "else") break; + this.doEdit(asArgs); + break; + case 'f': + this.doFreqs(asArgs[1]); + break; + case 'g': + this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet); + break; + case 'h': + this.doHalt(fQuiet); + break; + case 'i': + if (asArgs[0] == "if") { + if (!this.doIf(sCmd.substr(2), fQuiet)) { + result = false; + } + break; + } + if (asArgs[0] == "int") { + if (!this.doInt(asArgs[1])) { + result = false; + } + break; + } + this.doInput(asArgs[1]); + break; + case 'k': + this.doStackTrace(asArgs[0], asArgs[1]); + break; + case 'l': + if (asArgs[0] == "ln") { + this.doList(asArgs[1], true); + break; + } + break; + case 'm': + this.doMessages(asArgs); + break; + case 'o': + this.doOutput(asArgs[1], asArgs[2]); + break; + case 'p': + if (asArgs[0] == "print") { + this.doPrint(sCmd.substr(5)); + break; + } + this.doStep(asArgs[0]); + break; + case 'r': + if (sCmd == "reset") { + if (this.cmp) this.cmp.reset(); + break; + } + this.doRegisters(asArgs); + break; + case 's': + this.doOptions(asArgs); + break; + case 't': + this.doTrace(asArgs[0], asArgs[1]); + break; + case 'u': + this.doUnassemble(asArgs[1], asArgs[2], 8); + break; + case 'v': + if (asArgs[0] == "var") { + if (!this.doVar(sCmd.substr(3))) { + result = false; + } + break; + } + this.println((PC8080.APPNAME || "PC8080") + " version " + (XMLVERSION || PC8080.APPVERSION) + " (" + this.cpu.model + (PC8080.COMPILED? ",RELEASE" : (PC8080.DEBUG? ",DEBUG" : ",NODEBUG")) + (PC8080.TYPEDARRAYS? ",TYPEDARRAYS" : (PC8080.BYTEARRAYS? ",BYTEARRAYS" : ",LONGARRAYS")) + ')'); + this.println(Web.getUserAgent()); + break; + case '?': + if (asArgs[1]) { + this.doPrint(sCmd.substr(1)); + break; + } + this.doHelp(); + break; + case 'n': + if (!COMPILED && sCmd == "nodebug") { + window.DEBUG = false; + this.println("DEBUG checks off"); + break; + } + if (this.doInfo(asArgs)) break; + /* falls through */ + default: + this.println("unknown command: " + sCmd); + result = false; + break; + } + } + } catch(e) { + this.println("debugger error: " + (e.stack || e.message)); + result = false; + } + return result; + } + + /** + * doCommands(sCmds, fSave) + * + * @this {Debugger8080} + * @param {string} sCmds + * @param {boolean} [fSave] + * @return {boolean} true if all commands processed, false if not + */ + doCommands(sCmds, fSave) + { + var a = this.parseCommand(sCmds, fSave); + for (var s in a) { + if (!this.doCommand(a[+s])) return false; + } + return true; + } + + /** + * Debugger8080.init() + * + * This function operates on every HTML element of class "debugger", extracting the + * JSON-encoded parameters for the Debugger constructor from the element's "data-value" + * attribute, invoking the constructor to create a Debugger component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeDbg = Component.getElementsByClass(document, PC8080.APPCLASS, "debugger"); + for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) { + var eDbg = aeDbg[iDbg]; + var parmsDbg = Component.getComponentParms(eDbg); + var dbg = new Debugger8080(parmsDbg); + Component.bindComponentControls(dbg, eDbg, PC8080.APPCLASS); + } + } } if (DEBUGGER) { - Component.subclass(Debugger8080, Debugger); - /* * NOTE: Every Debugger8080 property from here to the first prototype function definition (initBus()) is * considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but @@ -627,3741 +4359,11 @@ if (DEBUGGER) { Debugger8080.HISTORY_LIMIT = DEBUG? 100000 : 1000; - /** - * initBus(bus, cpu, dbg) - * - * @this {Debugger8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ - Debugger8080.prototype.initBus = function(cmp, bus, cpu, dbg) - { - this.bus = bus; - this.cpu = cpu; - this.cmp = cmp; - - /* - * Re-initialize Debugger message support if necessary - */ - var sMessages = cmp.getMachineParm('messages'); - if (sMessages) this.messageInit(sMessages); - - this.aaOpDescs = Debugger8080.aaOpDescs; - - this.messageDump(Messages8080.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); }); - - this.setReady(); - }; - - /** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {Debugger8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ - Debugger8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) - { - var dbg = this; - switch (sBinding) { - - case "debugInput": - this.bindings[sBinding] = control; - this.controlDebug = control; - /* - * For halted machines, this is fine, but for auto-start machines, it can be annoying. - * - * control.focus(); - */ - control.onkeydown = function onKeyDownDebugInput(event) { - var sCmd; - if (event.keyCode == Keys.KEYCODE.CR) { - sCmd = control.value; - control.value = ""; - dbg.doCommands(sCmd, true); - } - else if (event.keyCode == Keys.KEYCODE.ESC) { - control.value = sCmd = ""; - } - else { - if (event.keyCode == Keys.KEYCODE.UP) { - sCmd = dbg.getPrevCommand(); - } - else if (event.keyCode == Keys.KEYCODE.DOWN) { - sCmd = dbg.getNextCommand(); - } - if (sCmd != null) { - var cch = sCmd.length; - control.value = sCmd; - control.setSelectionRange(cch, cch); - } - } - if (sCmd != null && event.preventDefault) event.preventDefault(); - }; - return true; - - case "debugEnter": - this.bindings[sBinding] = control; - web.onClickRepeat( - control, - 500, 100, - function onClickDebugEnter(fRepeat) { - if (dbg.controlDebug) { - var sCmds = dbg.controlDebug.value; - dbg.controlDebug.value = ""; - dbg.doCommands(sCmds, true); - return true; - } - if (DEBUG) dbg.log("no debugger input buffer"); - return false; - } - ); - return true; - - case "step": - this.bindings[sBinding] = control; - web.onClickRepeat( - control, - 500, 100, - function onClickStep(fRepeat) { - var fCompleted = false; - if (!dbg.isBusy(true)) { - dbg.setBusy(true); - fCompleted = dbg.stepCPU(fRepeat? 1 : 0); - dbg.setBusy(false); - } - return fCompleted; - } - ); - return true; - - default: - break; - } - return false; - }; - - /** - * updateFocus() - * - * @this {Debugger8080} - */ - Debugger8080.prototype.updateFocus = function() - { - if (this.controlDebug) this.controlDebug.focus(); - }; - - /** - * getAddr(dbgAddr, fWrite, nb) - * - * @this {Debugger8080} - * @param {DbgAddr8080|null|undefined} dbgAddr - * @param {boolean} [fWrite] - * @param {number} [nb] number of bytes to check (1 or 2); default is 1 - * @return {number} is the corresponding linear address, or CPUDef8080.ADDR_INVALID - */ - Debugger8080.prototype.getAddr = function(dbgAddr, fWrite, nb) - { - var addr = dbgAddr && dbgAddr.addr; - if (addr == null) { - addr = CPUDef8080.ADDR_INVALID; - } - return addr; - }; - - /** - * getByte(dbgAddr, inc) - * - * We must route all our memory requests through the CPU now, in case paging is enabled. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} [inc] - * @return {number} - */ - Debugger8080.prototype.getByte = function(dbgAddr, inc) - { - var b = 0xff; - var addr = this.getAddr(dbgAddr, false, 1); - if (addr !== CPUDef8080.ADDR_INVALID) { - b = this.bus.getByteDirect(addr); - if (inc) this.incAddr(dbgAddr, inc); - } - return b; - }; - - /** - * getWord(dbgAddr, fAdvance) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {boolean} [fAdvance] - * @return {number} - */ - Debugger8080.prototype.getWord = function(dbgAddr, fAdvance) - { - return this.getShort(dbgAddr, fAdvance? 2 : 0); - }; - - /** - * getShort(dbgAddr, inc) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} [inc] - * @return {number} - */ - Debugger8080.prototype.getShort = function(dbgAddr, inc) - { - var w = 0xffff; - var addr = this.getAddr(dbgAddr, false, 2); - if (addr !== CPUDef8080.ADDR_INVALID) { - w = this.bus.getShortDirect(addr); - if (inc) this.incAddr(dbgAddr, inc); - } - return w; - }; - - /** - * setByte(dbgAddr, b, inc) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} b - * @param {number} [inc] - */ - Debugger8080.prototype.setByte = function(dbgAddr, b, inc) - { - var addr = this.getAddr(dbgAddr, true, 1); - if (addr !== CPUDef8080.ADDR_INVALID) { - this.bus.setByteDirect(addr, b); - if (inc) this.incAddr(dbgAddr, inc); - this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target - } - }; - - /** - * setShort(dbgAddr, w, inc) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} w - * @param {number} [inc] - */ - Debugger8080.prototype.setShort = function(dbgAddr, w, inc) - { - var addr = this.getAddr(dbgAddr, true, 2); - if (addr !== CPUDef8080.ADDR_INVALID) { - this.bus.setShortDirect(addr, w); - if (inc) this.incAddr(dbgAddr, inc); - this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target - } - }; - - /** - * newAddr(addr) - * - * Returns a NEW DbgAddr8080 object, initialized with specified values and/or defaults. - * - * @this {Debugger8080} - * @param {number} [addr] - * @return {DbgAddr8080} - */ - Debugger8080.prototype.newAddr = function(addr) - { - return {addr: addr, fTemporary: false}; - }; - - /** - * setAddr(dbgAddr, addr) - * - * Updates an EXISTING DbgAddr8080 object, initialized with specified values and/or defaults. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} addr - * @return {DbgAddr8080} - */ - Debugger8080.prototype.setAddr = function(dbgAddr, addr) - { - dbgAddr.addr = addr; - dbgAddr.fTemporary = false; - return dbgAddr; - }; - - /** - * packAddr(dbgAddr) - * - * Packs a DbgAddr8080 object into an Array suitable for saving in a machine state object. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @return {Array} - */ - Debugger8080.prototype.packAddr = function(dbgAddr) - { - return [dbgAddr.addr, dbgAddr.fTemporary]; - }; - - /** - * unpackAddr(aAddr) - * - * Unpacks a DbgAddr8080 object from an Array created by packAddr() and restored from a saved machine state. - * - * @this {Debugger8080} - * @param {Array} aAddr - * @return {DbgAddr8080} - */ - Debugger8080.prototype.unpackAddr = function(aAddr) - { - return {addr: aAddr[0], fTemporary: aAddr[1]}; - }; - - /** - * parseAddr(sAddr, fCode, fNoChecks, fPrint) - * - * Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's - * done later, by getAddr(), which returns CPUDef8080.ADDR_INVALID for invalid segments, out-of-range offsets, - * etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an - * invalid address is passed through to the Bus memory interfaces, the address will simply be masked with - * Bus8080.nBusLimit; in the case of CPUDef8080.ADDR_INVALID, that will generally refer to the top of the physical - * address space. - * - * @this {Debugger8080} - * @param {string|undefined} sAddr - * @param {boolean} [fCode] (true if target is code, false if target is data) - * @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later) - * @param {boolean} [fPrint] - * @return {DbgAddr8080|null|undefined} - */ - Debugger8080.prototype.parseAddr = function(sAddr, fCode, fNoChecks, fPrint) - { - var dbgAddr; - var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData); - var addr = dbgAddrNext.addr; - if (sAddr !== undefined) { - sAddr = this.parseReference(sAddr); - dbgAddr = this.findSymbolAddr(sAddr); - if (dbgAddr) return dbgAddr; - addr = this.parseExpression(sAddr, fPrint); - } - if (addr != null) { - dbgAddr = this.newAddr(addr); - } - return dbgAddr; - }; - - /** - * parseAddrOptions(dbdAddr, sOptions) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {string} [sOptions] - */ - Debugger8080.prototype.parseAddrOptions = function(dbgAddr, sOptions) - { - if (sOptions) { - var a = sOptions.match(/(['"])(.*?)\1/); - if (a) { - dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]); - } - } - }; - - /** - * incAddr(dbgAddr, inc) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} [inc] contains value to increment dbgAddr by (default is 1) - */ - Debugger8080.prototype.incAddr = function(dbgAddr, inc) - { - if (dbgAddr.addr != null) { - dbgAddr.addr += (inc || 1); - } - }; - - /** - * toHexOffset(off) - * - * @this {Debugger8080} - * @param {number|null|undefined} [off] - * @return {string} the hex representation of off - */ - Debugger8080.prototype.toHexOffset = function(off) - { - return str.toHex(off, 4); - }; - - /** - * toHexAddr(dbgAddr) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @return {string} the hex representation of the address - */ - Debugger8080.prototype.toHexAddr = function(dbgAddr) - { - return this.toHexOffset(dbgAddr.addr); - }; - - /** - * getSZ(dbgAddr, cchMax) - * - * Gets zero-terminated (aka "ASCIIZ") string from dbgAddr. It also stops at the first '$', in case this is - * a '$'-terminated string -- mainly because I'm lazy and didn't feel like writing a separate get() function. - * Yes, a zero-terminated string containing a '$' will be prematurely terminated, and no, I don't care. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {number} [cchMax] (default is 256) - * @return {string} (and dbgAddr advanced past the terminating zero) - */ - Debugger8080.prototype.getSZ = function(dbgAddr, cchMax) - { - var s = ""; - cchMax = cchMax || 256; - while (s.length < cchMax) { - var b = this.getByte(dbgAddr, 1); - if (!b || b == 0x24 || b >= 127) break; - s += (b >= 32? String.fromCharCode(b) : '.'); - } - return s; - }; - - /** - * dumpBlocks(aBlocks, sAddr) - * - * @this {Debugger8080} - * @param {Array} aBlocks - * @param {string} [sAddr] (optional block address) - */ - Debugger8080.prototype.dumpBlocks = function(aBlocks, sAddr) - { - var addr = 0, i = 0, n = aBlocks.length; - - if (sAddr) { - addr = this.getAddr(this.parseAddr(sAddr)); - if (addr === CPUDef8080.ADDR_INVALID) { - this.println("invalid address: " + sAddr); - return; - } - i = addr >>> this.bus.nBlockShift; - n = 1; - } - - this.println("blockid physical blockaddr used size type"); - this.println("-------- --------- ---------- ------ ------ ----"); - - var typePrev = -1, cPrev = 0; - while (n--) { - var block = aBlocks[i]; - if (block.type == typePrev) { - if (!cPrev++) this.println("..."); - } else { - typePrev = block.type; - var sType = Memory8080.TYPE.NAMES[typePrev]; - if (block) { - this.println(str.toHex(block.id) + " %" + str.toHex(i << this.bus.nBlockShift) + " %%" + str.toHex(block.addr) + " " + str.toHexWord(block.used) + " " + str.toHexWord(block.size) + " " + sType); - } - if (typePrev != Memory8080.TYPE.NONE) typePrev = -1; - cPrev = 0; - } - addr += this.bus.nBlockSize; - i++; - } - }; - - /** - * dumpBus(asArgs) - * - * Dumps Bus allocations. - * - * @this {Debugger8080} - * @param {Array.} asArgs (asArgs[0] is an optional block address) - */ - Debugger8080.prototype.dumpBus = function(asArgs) - { - this.dumpBlocks(this.bus.aMemBlocks, asArgs[0]); - }; - - /** - * dumpHistory(sPrev, sLines) - * - * If sLines is not a number, it can be a instruction filter. However, for the moment, the only - * supported filter is "call", which filters the history buffer for all CALL and RET instructions - * from the specified previous point forward. - * - * @this {Debugger8080} - * @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10) - * @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10) - */ - Debugger8080.prototype.dumpHistory = function(sPrev, sLines) - { - var sMore = ""; - var cHistory = 0; - var iHistory = this.iOpcodeHistory; - var aHistory = this.aOpcodeHistory; - - if (aHistory.length) { - var nPrev = +sPrev || this.nextHistory; - var nLines = +sLines || 10; - - if (isNaN(nPrev)) { - nPrev = nLines; - } else { - sMore = "more "; - } - - if (nPrev > aHistory.length) { - this.println("note: only " + aHistory.length + " available"); - nPrev = aHistory.length; - } - - iHistory -= nPrev; - if (iHistory < 0) { - /* - * If the dbgAddr of the last aHistory element contains a valid selector, wrap around. - */ - if (aHistory[aHistory.length - 1].addr == null) { - nPrev = iHistory + nPrev; - iHistory = 0; - } else { - iHistory += aHistory.length; - } - } - - var aFilters = []; - if (sLines == "call") { - nLines = 100000; - aFilters = ["CALL"]; - } - - if (sPrev !== undefined) { - this.println(nPrev + " instructions earlier:"); - } - - /* - * TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive) - * faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically - * increase the amount of memory used to store instruction history (eg, storing copies of all the instruction - * bytes alongside the execution addresses). - * - * For now, we're living dangerously, so that our history dumps actually work. - * - * this.nSuppressBreaks++; - * - * If you re-enable this protection, be sure to re-enable the decrement below, too. - */ - while (nLines > 0 && iHistory != this.iOpcodeHistory) { - - var dbgAddr = aHistory[iHistory++]; - if (dbgAddr.addr == null) break; - - /* - * We must create a new dbgAddr from the address in aHistory, because dbgAddr was - * a reference, not a copy, and we don't want getInstruction() modifying the original. - */ - var dbgAddrNew = this.newAddr(dbgAddr.addr); - - var sComment = "history"; - var nSequence = nPrev--; - if (DEBUG && dbgAddr.cycleCount != null) { - sComment = "cycles"; - nSequence = dbgAddr.cycleCount; - } - - var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence); - - if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) { - this.println(sInstruction); - } - - /* - * If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction() - * will have automatically disassembled additional bytes, so skip additional history entries. - */ - if (dbgAddrNew.cOverrides) { - iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides; - } - - if (iHistory >= aHistory.length) iHistory = 0; - this.nextHistory = nPrev; - cHistory++; - nLines--; - } - /* - * See comments above. - * - * this.nSuppressBreaks--; - */ - } - - if (!cHistory) { - this.println("no " + sMore + "history available"); - this.nextHistory = undefined; - } - }; - - /** - * messageInit(sEnable) - * - * @this {Debugger8080} - * @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|' - */ - Debugger8080.prototype.messageInit = function(sEnable) - { - this.dbg = this; - this.bitsMessage = this.bitsWarning = Messages8080.WARN; - this.sMessagePrev = null; - this.aMessageBuffer = []; - /* - * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, - * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". - */ - var aEnable = this.parseCommand(sEnable.replace("keys","key").replace("kbd","keyboard"), false, '|'); - if (aEnable.length) { - for (var m in Messages8080.CATEGORIES) { - if (usr.indexOf(aEnable, m) >= 0) { - this.bitsMessage |= Messages8080.CATEGORIES[m]; - this.println(m + " messages enabled"); - } - } - } - }; - - /** - * messageDump(bitMessage, fnDumper) - * - * @this {Debugger8080} - * @param {number} bitMessage is one Messages category flag - * @param {function(Array.)} fnDumper is a function the Debugger can use to dump data for that category - * @return {boolean} true if successfully registered, false if not - */ - Debugger8080.prototype.messageDump = function(bitMessage, fnDumper) - { - for (var m in Messages8080.CATEGORIES) { - if (bitMessage == Messages8080.CATEGORIES[m]) { - this.afnDumpers[m] = fnDumper; - return true; - } - } - return false; - }; - - /** - * getRegIndex(sReg, off) - * - * @this {Debugger8080} - * @param {string} sReg - * @param {number} [off] optional offset into sReg - * @return {number} register index, or -1 if not found - */ - Debugger8080.prototype.getRegIndex = function(sReg, off) - { - var i; - sReg = sReg.toUpperCase(); - if (off == null) { - i = usr.indexOf(Debugger8080.REGS, sReg); - } else { - i = usr.indexOf(Debugger8080.REGS, sReg.substr(off, 2)); - if (i < 0) i = usr.indexOf(Debugger8080.REGS, sReg.substr(off, 1)); - } - return i; - }; - - /** - * getRegString(iReg) - * - * @this {Debugger8080} - * @param {number} iReg - * @return {string} - */ - Debugger8080.prototype.getRegString = function(iReg) - { - var cch = 0; - var n = this.getRegValue(iReg); - if (n !== undefined) { - switch(iReg) { - case Debugger8080.REG_A: - case Debugger8080.REG_B: - case Debugger8080.REG_C: - case Debugger8080.REG_D: - case Debugger8080.REG_E: - case Debugger8080.REG_H: - case Debugger8080.REG_L: - case Debugger8080.REG_M: - cch = 2; - break; - case Debugger8080.REG_BC: - case Debugger8080.REG_DE: - case Debugger8080.REG_HL: - case Debugger8080.REG_SP: - case Debugger8080.REG_PC: - case Debugger8080.REG_PS: - case Debugger8080.REG_PSW: - cch = 4; - break; - } - } - return cch? str.toHex(n, cch) : "??"; - }; - - /** - * getRegValue(iReg) - * - * @this {Debugger8080} - * @param {number} iReg - * @return {number|undefined} - */ - Debugger8080.prototype.getRegValue = function(iReg) - { - var n; - if (iReg >= 0) { - var cpu = this.cpu; - switch(iReg) { - case Debugger8080.REG_A: - n = cpu.regA; - break; - case Debugger8080.REG_B: - n = cpu.regB; - break; - case Debugger8080.REG_C: - n = cpu.regC; - break; - case Debugger8080.REG_BC: - n = cpu.getBC(); - break; - case Debugger8080.REG_D: - n = cpu.regD; - break; - case Debugger8080.REG_E: - n = cpu.regE; - break; - case Debugger8080.REG_DE: - n = cpu.getDE(); - break; - case Debugger8080.REG_H: - n = cpu.regH; - break; - case Debugger8080.REG_L: - n = cpu.regL; - break; - case Debugger8080.REG_HL: - n = cpu.getHL(); - break; - case Debugger8080.REG_M: - n = cpu.getByte(cpu.getHL()); - break; - case Debugger8080.REG_SP: - n = cpu.getSP(); - break; - case Debugger8080.REG_PC: - n = cpu.getPC(); - break; - case Debugger8080.REG_PS: - n = cpu.getPS(); - break; - case Debugger8080.REG_PSW: - n = cpu.getPSW(); - break; - default: - break; - } - } - return n; - }; - - /** - * replaceRegs(s) - * - * @this {Debugger8080} - * @param {string} s - * @return {string} - */ - Debugger8080.prototype.replaceRegs = function(s) - { - /* - * Replace any references first; this means that register references inside the reference - * do NOT need to be prefixed with '@'. - */ - s = this.parseReference(s); - - /* - * Replace every @XX (or @XXX), where XX (or XXX) is a register, with the register's value. - */ - var i = 0; - var b, sChar, sAddr, dbgAddr, sReplace; - while ((i = s.indexOf('@', i)) >= 0) { - var iReg = this.getRegIndex(s, i + 1); - if (iReg >= 0) { - s = s.substr(0, i) + this.getRegString(iReg) + s.substr(i + 1 + Debugger8080.REGS[iReg].length); - } - i++; - } - /* - * Replace every #XX, where XX is a hex byte value, with the corresponding ASCII character (if printable). - */ - i = 0; - while ((i = s.indexOf('#', i)) >= 0) { - sChar = s.substr(i+1, 2); - b = str.parseInt(sChar, 16); - if (b != null && b >= 32 && b < 128) { - sReplace = sChar + " '" + String.fromCharCode(b) + "'"; - s = s.replace('#' + sChar, sReplace); - i += sReplace.length; - continue; - } - i++; - } - /* - * Replace every $XXXX:XXXX, where XXXX:XXXX is a segmented address, with the zero-terminated string at that address. - */ - i = 0; - while ((i = s.indexOf('$', i)) >= 0) { - sAddr = s.substr(i+1, 9); - dbgAddr = this.parseAddr(sAddr); - if (dbgAddr) { - sReplace = sAddr + ' "' + this.getSZ(dbgAddr) + '"'; - s = s.replace('$' + sAddr, sReplace); - i += sReplace.length; - continue; - } - i++; - } - /* - * Replace every ^XXXX:XXXX, where XXXX:XXXX is a segmented address, with the FCB filename stored at that address. - */ - i = 0; - while ((i = s.indexOf('^', i)) >= 0) { - sAddr = s.substr(i+1, 9); - dbgAddr = this.parseAddr(sAddr); - if (dbgAddr) { - this.incAddr(dbgAddr); - sReplace = sAddr + ' "' + this.getSZ(dbgAddr, 11) + '"'; - s = s.replace('^' + sAddr, sReplace); - i += sReplace.length; - continue; - } - i++; - } - return s; - }; - - /** - * message(sMessage, fAddress) - * - * @this {Debugger8080} - * @param {string} sMessage is any caller-defined message string - * @param {boolean} [fAddress] is true to display the current CS:IP - */ - Debugger8080.prototype.message = function(sMessage, fAddress) - { - if (fAddress) { - sMessage += " at " + this.toHexAddr(this.newAddr(this.cpu.getPC())); - } - - if (this.bitsMessage & Messages8080.BUFFER) { - this.aMessageBuffer.push(sMessage); - return; - } - - if (this.sMessagePrev && sMessage == this.sMessagePrev) return; - this.sMessagePrev = sMessage; - - if (this.bitsMessage & Messages8080.HALT) { - this.stopCPU(); - sMessage += " (cpu halted)"; - } - - this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)" - - /* - * We have no idea what the frequency of println() calls might be; all we know is that they easily - * screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every - * message, to effectively end the current burst and start fresh. - * - * TODO: See CPU8080.calcStartTime() for a discussion of why we might want to call yieldCPU() *before* - * we display the message. - */ - if (this.cpu) this.cpu.yieldCPU(); - }; - - /** - * messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage) - * - * Most (if not all) port handlers should provide a name for their respective ports, so if no name is provided, - * we assume this is an unknown port, and display a message by default. - * - * @this {Debugger8080} - * @param {Component} component - * @param {number} port - * @param {number|null} bOut if an output operation - * @param {number|null} [addrFrom] - * @param {string|null} [name] of the port, if any - * @param {number|null} [bIn] is the input value, if known, on an input operation - * @param {number} [bitsMessage] is one or more Messages category flag(s) - */ - Debugger8080.prototype.messageIO = function(component, port, bOut, addrFrom, name, bIn, bitsMessage) - { - bitsMessage |= Messages8080.PORT; - if (name == null || (this.bitsMessage & bitsMessage) == bitsMessage) { - this.message(component.idComponent + '.' + (bOut != null? "outPort" : "inPort") + '(' + str.toHexWord(port) + ',' + (name? name : "unknown") + (bOut != null? ',' + str.toHexByte(bOut) : "") + ')' + (bIn != null? (": " + str.toHexByte(bIn)) : "") + (addrFrom != null? (" at " + this.toHexOffset(addrFrom)) : "")); - } - }; - - /** - * init() - * - * @this {Debugger8080} - */ - Debugger8080.prototype.init = function() - { - this.println("Type ? for help with PC8080 Debugger commands"); - this.updateStatus(); - if (this.sInitCommands) { - var sCmds = this.sInitCommands; - this.sInitCommands = null; - this.doCommands(sCmds); - } - }; - - /** - * historyInit(fQuiet) - * - * This function is intended to be called by the constructor, reset(), addBreakpoint(), findBreakpoint() - * and any other function that changes the checksEnabled() criteria used to decide whether checkInstruction() - * should be called. - * - * That is, if the history arrays need to be allocated and haven't already been allocated, then allocate them, - * and if the arrays are no longer needed, then deallocate them. - * - * @this {Debugger8080} - * @param {boolean} [fQuiet] - */ - Debugger8080.prototype.historyInit = function(fQuiet) - { - var i; - if (!this.checksEnabled()) { - if (this.aOpcodeHistory && this.aOpcodeHistory.length && !fQuiet) { - this.println("instruction history buffer freed"); - } - this.iOpcodeHistory = 0; - this.aOpcodeHistory = []; - this.aaOpcodeCounts = []; - return; - } - if (!this.aOpcodeHistory || !this.aOpcodeHistory.length) { - this.aOpcodeHistory = new Array(Debugger8080.HISTORY_LIMIT); - for (i = 0; i < this.aOpcodeHistory.length; i++) { - /* - * Preallocate dummy Addr (Array) objects in every history slot, so that - * checkInstruction() doesn't need to call newAddr() on every slot update. - */ - this.aOpcodeHistory[i] = this.newAddr(); - } - this.iOpcodeHistory = 0; - if (!fQuiet) { - this.println("instruction history buffer allocated"); - } - } - if (!this.aaOpcodeCounts || !this.aaOpcodeCounts.length) { - this.aaOpcodeCounts = new Array(256); - for (i = 0; i < this.aaOpcodeCounts.length; i++) { - this.aaOpcodeCounts[i] = [i, 0]; - } - } - }; - - /** - * runCPU(fUpdateFocus) - * - * @this {Debugger8080} - * @param {boolean} [fUpdateFocus] is true to update focus - * @return {boolean} true if run request successful, false if not - */ - Debugger8080.prototype.runCPU = function(fUpdateFocus) - { - if (!this.isCPUAvail()) return false; - this.cpu.runCPU(fUpdateFocus); - return true; - }; - - /** - * stepCPU(nCycles, fRegs, fUpdateCPU) - * - * @this {Debugger8080} - * @param {number} nCycles (0 for one instruction without checking breakpoints) - * @param {boolean} [fRegs] is true to display registers after step (default is false) - * @param {boolean} [fUpdateCPU] is false to disable calls to updateCPU() (default is true) - * @return {boolean} - */ - Debugger8080.prototype.stepCPU = function(nCycles, fRegs, fUpdateCPU) - { - if (!this.isCPUAvail()) return false; - - this.nCycles = 0; - - if (!nCycles) { - /* - * When single-stepping, the CPU won't call checkInstruction(), which is good for - * avoiding breakpoints, but bad for instruction data collection if checks are enabled. - * So we call checkInstruction() ourselves. - */ - if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0); - } - try { - var nCyclesStep = this.cpu.stepCPU(nCycles); - if (nCyclesStep > 0) { - this.nCycles += nCyclesStep; - this.cpu.addCycles(nCyclesStep, true); - this.cpu.updateChecksum(nCyclesStep); - this.cOpcodes++; - } - } - catch(exception) { - if (typeof exception != "number") { - var e = exception; - this.nCycles = 0; - this.cpu.setError(e.stack || e.message); - } - } - - /* - * Because we called cpu.stepCPU() and not cpu.runCPU(), we must nudge the cpu's update code, - * and then update our own state. Normally, the only time fUpdateCPU will be false is when doTrace() - * is calling us in a loop, in which case it will perform its own updateCPU() when it's done. - */ - if (fUpdateCPU !== false) this.cpu.updateCPU(); - - this.updateStatus(fRegs || false); - return (this.nCycles > 0); - }; - - /** - * stopCPU() - * - * @this {Debugger8080} - * @param {boolean} [fComplete] - */ - Debugger8080.prototype.stopCPU = function(fComplete) - { - if (this.cpu) this.cpu.stopCPU(fComplete); - }; - - /** - * updateStatus(fRegs) - * - * @this {Debugger8080} - * @param {boolean} [fRegs] (default is true) - */ - Debugger8080.prototype.updateStatus = function(fRegs) - { - if (fRegs === undefined) fRegs = true; - - this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); - /* - * this.nStep used to be a simple boolean, but now it's 0 (or undefined) - * if inactive, 1 if stepping over an instruction without a register dump, or 2 - * if stepping over an instruction with a register dump. - */ - if (!fRegs || this.nStep == 1) - this.doUnassemble(); - else { - this.doRegisters(); - } - }; - - /** - * isCPUAvail() - * - * Make sure the CPU is ready (finished initializing), not busy (already running), and not in an error state. - * - * @this {Debugger8080} - * @return {boolean} - */ - Debugger8080.prototype.isCPUAvail = function() - { - if (!this.cpu) - return false; - if (!this.cpu.isReady()) - return false; - if (!this.cpu.isPowered()) - return false; - if (this.cpu.isBusy()) - return false; - return !this.cpu.isError(); - }; - - /** - * powerUp(data, fRepower) - * - * @this {Debugger8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ - Debugger8080.prototype.powerUp = function(data, fRepower) - { - if (!fRepower) { - /* - * Because Debugger save/restore support is somewhat limited (and didn't always exist), - * we deviate from the typical save/restore design pattern: instead of reset OR restore, - * we always reset and then perform a (potentially limited) restore. - */ - this.reset(true); - - // this.println(data? "resuming" : "powering up"); - - if (data && this.restore) { - if (!this.restore(data)) return false; - } - } - return true; - }; - - /** - * powerDown(fSave, fShutdown) - * - * @this {Debugger8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} - */ - Debugger8080.prototype.powerDown = function(fSave, fShutdown) - { - if (fShutdown) this.println(fSave? "suspending" : "shutting down"); - return fSave? this.save() : true; - }; - - /** - * reset(fQuiet) - * - * This is a notification handler, called by the Computer, to inform us of a reset. - * - * @this {Debugger8080} - * @param {boolean} fQuiet (true only when called from our own powerUp handler) - */ - Debugger8080.prototype.reset = function(fQuiet) - { - this.historyInit(); - this.cOpcodes = this.cOpcodesStart = 0; - this.sMessagePrev = null; - this.nCycles = 0; - this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); - /* - * fRunning is set by start() and cleared by stop(). In addition, we clear - * it here, so that if the CPU is reset while running, we can prevent stop() - * from unnecessarily dumping the CPU state. - */ - this.flags.running = false; - this.clearTempBreakpoint(); - if (!fQuiet) this.updateStatus(); - }; - - /** - * save() - * - * This implements (very rudimentary) save support for the Debugger component. - * - * @this {Debugger8080} - * @return {Object} - */ - Debugger8080.prototype.save = function() - { - var state = new State(this); - state.set(0, this.packAddr(this.dbgAddrNextCode)); - state.set(1, this.packAddr(this.dbgAddrAssemble)); - state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]); - state.set(3, this.aSymbolTable); - return state.data(); - }; - - /** - * restore(data) - * - * This implements (very rudimentary) restore support for the Debugger component. - * - * @this {Debugger8080} - * @param {Object} data - * @return {boolean} true if successful, false if failure - */ - Debugger8080.prototype.restore = function(data) - { - var i = 0; - if (data[2] !== undefined) { - this.dbgAddrNextCode = this.unpackAddr(data[i++]); - this.dbgAddrAssemble = this.unpackAddr(data[i++]); - this.aPrevCmds = data[i][0]; - if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds]; - this.fAssemble = data[i][1]; - this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state - } - if (data[3]) this.aSymbolTable = data[3]; - return true; - }; - - /** - * start(ms, nCycles) - * - * This is a notification handler, called by the Computer, to inform us the CPU has started. - * - * @this {Debugger8080} - * @param {number} ms - * @param {number} nCycles - */ - Debugger8080.prototype.start = function(ms, nCycles) - { - if (!this.nStep) this.println("running"); - this.flags.running = true; - this.msStart = ms; - this.nCyclesStart = nCycles; - }; - - /** - * stop(ms, nCycles) - * - * This is a notification handler, called by the Computer, to inform us the CPU has now stopped. - * - * @this {Debugger8080} - * @param {number} ms - * @param {number} nCycles - */ - Debugger8080.prototype.stop = function(ms, nCycles) - { - if (this.flags.running) { - this.flags.running = false; - this.nCycles = nCycles - this.nCyclesStart; - if (!this.nStep) { - var sStopped = "stopped"; - if (this.nCycles) { - var msTotal = ms - this.msStart; - var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0); - sStopped += " ("; - if (this.checksEnabled()) { - sStopped += this.cOpcodes + " opcodes, "; - /* - * $ops displays progress by calculating cOpcodes - cOpcodesStart, so before - * zeroing cOpcodes, we should subtract cOpcodes from cOpcodesStart (since we're - * effectively subtracting cOpcodes from cOpcodes as well). - */ - this.cOpcodesStart -= this.cOpcodes; - this.cOpcodes = 0; - } - sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)"; - } else { - if (this.messageEnabled(Messages8080.HALT)) { - /* - * It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault() - * for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful - * message (another helpful tip would be to simply turn off the "halt" message category). - */ - sStopped += " (use the 't' command to execute blocked faults)"; - } - } - this.println(sStopped); - } - this.updateStatus(true); - this.updateFocus(); - this.clearTempBreakpoint(this.cpu.getPC()); - } - }; - - /** - * checksEnabled(fRelease) - * - * This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked. - * - * Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints - * no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate - * "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've - * simplified the test below. - * - * @this {Debugger8080} - * @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria) - * @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not - */ - Debugger8080.prototype.checksEnabled = function(fRelease) - { - return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakIns)); - }; - - /** - * checkInstruction(addr, nState) - * - * This "check" function is called by the CPU to inform us about the next instruction to be executed, - * giving us an opportunity to look for "exec" breakpoints and update opcode frequencies and instruction history. - * - * @this {Debugger8080} - * @param {number} addr - * @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running - * @return {boolean} true if breakpoint hit, false if not - */ - Debugger8080.prototype.checkInstruction = function(addr, nState) - { - var cpu = this.cpu; - - if (nState > 0) { - if (this.nBreakIns && !--this.nBreakIns) { - return true; - } - if (this.checkBreakpoint(addr, 1, this.aBreakExec)) { - return true; - } - } - - /* - * The rest of the instruction tracking logic can only be performed if historyInit() has allocated the - * necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than - * adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction(); - * well, OK, and a few other things now, like enabling Messages8080.INT messages. - */ - if (nState >= 0 && this.aaOpcodeCounts.length) { - this.cOpcodes++; - var bOpcode = this.bus.getByteDirect(addr); - if (bOpcode != null) { - this.aaOpcodeCounts[bOpcode][1]++; - var dbgAddr = this.aOpcodeHistory[this.iOpcodeHistory]; - this.setAddr(dbgAddr, cpu.getPC()); - if (DEBUG) dbgAddr.cycleCount = cpu.getCycles(); - if (++this.iOpcodeHistory == this.aOpcodeHistory.length) this.iOpcodeHistory = 0; - } - } - return false; - }; - - /** - * checkMemoryRead(addr, nb) - * - * This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an - * opportunity to track the read if we want, and look for a matching "read" breakpoint, if any. - * - * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now - * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read. - * - * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. - * - * @this {Debugger8080} - * @param {number} addr - * @param {number} [nb] (# of bytes; default is 1) - * @return {boolean} true if breakpoint hit, false if not - */ - Debugger8080.prototype.checkMemoryRead = function(addr, nb) - { - if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) { - this.stopCPU(true); - return true; - } - return false; - }; - - /** - * checkMemoryWrite(addr, nb) - * - * This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an - * opportunity to track the write if we want, and look for a matching "write" breakpoint, if any. - * - * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now - * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write. - * - * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. - * - * @this {Debugger8080} - * @param {number} addr - * @param {number} [nb] (# of bytes; default is 1) - * @return {boolean} true if breakpoint hit, false if not - */ - Debugger8080.prototype.checkMemoryWrite = function(addr, nb) - { - if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) { - this.stopCPU(true); - return true; - } - return false; - }; - - /** - * checkPortInput(port, size, data) - * - * This "check" function is called by the Bus component to inform us that port input occurred. - * - * @this {Debugger8080} - * @param {number} port - * @param {number} size - * @param {number} data - * @return {boolean} true if breakpoint hit, false if not - */ - Debugger8080.prototype.checkPortInput = function(port, size, data) - { - /* - * We trust that the Bus component won't call us unless we told it to, so we halt unconditionally - */ - this.println("break on input from port " + str.toHexWord(port) + ": " + str.toHex(data)); - this.stopCPU(true); - return true; - }; - - /** - * checkPortOutput(port, size, data) - * - * This "check" function is called by the Bus component to inform us that port output occurred. - * - * @this {Debugger8080} - * @param {number} port - * @param {number} size - * @param {number} data - * @return {boolean} true if breakpoint hit, false if not - */ - Debugger8080.prototype.checkPortOutput = function(port, size, data) - { - /* - * We trust that the Bus component won't call us unless we told it to, so we halt unconditionally - */ - this.println("break on output to port " + str.toHexWord(port) + ": " + str.toHex(data)); - this.stopCPU(true); - return true; - }; - - /** - * clearBreakpoints() - * - * @this {Debugger8080} - */ - Debugger8080.prototype.clearBreakpoints = function() - { - var i, dbgAddr; - this.aBreakExec = ["bp"]; - if (this.aBreakRead !== undefined) { - for (i = 1; i < this.aBreakRead.length; i++) { - dbgAddr = this.aBreakRead[i]; - this.bus.removeMemBreak(this.getAddr(dbgAddr), false); - } - } - this.aBreakRead = ["br"]; - if (this.aBreakWrite !== undefined) { - for (i = 1; i < this.aBreakWrite.length; i++) { - dbgAddr = this.aBreakWrite[i]; - this.bus.removeMemBreak(this.getAddr(dbgAddr), true); - } - } - this.aBreakWrite = ["bw"]; - /* - * nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup requires - * reading a segment descriptor via getSegment(), and that triggers more memory reads, which triggers - * more breakpoint checks. - */ - this.nSuppressBreaks = 0; - }; - - /** - * addBreakpoint(aBreak, dbgAddr, fTemporary) - * - * In case you haven't already figured this out, all our breakpoint commands use the address - * to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers; - * see doBreak() for details. - * - * This has a few implications, one being that you CANNOT set more than one kind of breakpoint - * on a single address. In practice, that's rarely a problem, because you can almost always set - * a different breakpoint on a neighboring address. - * - * Also, there is one exception to the "one address, one breakpoint" rule, and that involves - * temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command - * may create to step over a chunk of code). Those breakpoints automatically clear themselves, - * so there usually isn't any need to refer to them using breakpoint commands. - * - * TODO: Consider supporting the more "traditional" breakpoint index syntax; the current - * address-based syntax was implemented solely for expediency and consistency. At the same time, - * also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific - * breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc. - * - * @this {Debugger8080} - * @param {Array} aBreak - * @param {DbgAddr8080} dbgAddr - * @param {boolean} [fTemporary] - * @return {boolean} true if breakpoint added, false if already exists - */ - Debugger8080.prototype.addBreakpoint = function(aBreak, dbgAddr, fTemporary) - { - var fSuccess = true; - - // this.nSuppressBreaks++; - - /* - * Instead of complaining that a breakpoint already exists (as we used to do), we now - * allow breakpoints to be re-set; this makes it easier to update any commands that may - * be associated with the breakpoint. - * - * The only exception: we DO allow a temporary breakpoint at an address where there may - * already be a breakpoint, so that you can easily step ("p" or "g") over such addresses. - */ - if (!fTemporary) { - this.findBreakpoint(aBreak, dbgAddr, true, false, true); - } - - if (aBreak != this.aBreakExec) { - var addr = this.getAddr(dbgAddr); - if (addr === CPUDef8080.ADDR_INVALID) { - this.println("invalid address: " + this.toHexAddr(dbgAddr)); - fSuccess = false; - } else { - this.bus.addMemBreak(addr, aBreak == this.aBreakWrite); - } - } - - if (fSuccess) { - aBreak.push(dbgAddr); - if (fTemporary) { - dbgAddr.fTemporary = true; - } - else { - this.printBreakpoint(aBreak, aBreak.length-1, "set"); - this.historyInit(); - } - } - - // this.nSuppressBreaks--; - - return fSuccess; - }; - - /** - * findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) - * - * @this {Debugger8080} - * @param {Array} aBreak - * @param {DbgAddr8080} dbgAddr - * @param {boolean} [fRemove] - * @param {boolean} [fTemporary] - * @param {boolean} [fQuiet] - * @return {boolean} true if found, false if not - */ - Debugger8080.prototype.findBreakpoint = function(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) - { - var fFound = false; - var addr = this.getAddr(dbgAddr); - for (var i = 1; i < aBreak.length; i++) { - var dbgAddrBreak = aBreak[i]; - if (addr == this.getAddr(dbgAddrBreak)) { - if (!fTemporary || dbgAddrBreak.fTemporary) { - fFound = true; - if (fRemove) { - if (!dbgAddrBreak.fTemporary && !fQuiet) { - this.printBreakpoint(aBreak, i, "cleared"); - } - aBreak.splice(i, 1); - if (aBreak != this.aBreakExec) { - this.bus.removeMemBreak(addr, aBreak == this.aBreakWrite); - } - /* - * We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this - * was a temporary breakpoint. - */ - if (!dbgAddrBreak.fTemporary) { - this.historyInit(); - } - break; - } - if (!fQuiet) this.printBreakpoint(aBreak, i, "exists"); - break; - } - } - } - return fFound; - }; - - /** - * listBreakpoints(aBreak) - * - * @this {Debugger8080} - * @param {Array} aBreak - * @return {number} of breakpoints listed, 0 if none - */ - Debugger8080.prototype.listBreakpoints = function(aBreak) - { - for (var i = 1; i < aBreak.length; i++) { - this.printBreakpoint(aBreak, i); - } - return aBreak.length - 1; - }; - - /** - * printBreakpoint(aBreak, i, sAction) - * - * @this {Debugger8080} - * @param {Array} aBreak - * @param {number} i - * @param {string} [sAction] - */ - Debugger8080.prototype.printBreakpoint = function(aBreak, i, sAction) - { - var dbgAddr = aBreak[i]; - this.println(aBreak[0] + ' ' + this.toHexAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : ''))); - }; - - /** - * setTempBreakpoint(dbgAddr) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr of new temp breakpoint - */ - Debugger8080.prototype.setTempBreakpoint = function(dbgAddr) - { - this.addBreakpoint(this.aBreakExec, dbgAddr, true); - }; - - /** - * clearTempBreakpoint(addr) - * - * @this {Debugger8080} - * @param {number|undefined} [addr] clear all temp breakpoints if no address specified - */ - Debugger8080.prototype.clearTempBreakpoint = function(addr) - { - if (addr !== undefined) { - this.checkBreakpoint(addr, 1, this.aBreakExec, true); - this.nStep = 0; - } else { - for (var i = 1; i < this.aBreakExec.length; i++) { - var dbgAddrBreak = this.aBreakExec[i]; - if (dbgAddrBreak.fTemporary) { - if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break; - i = 0; - } - } - } - }; - - /** - * checkBreakpoint(addr, nb, aBreak, fTemporary) - * - * @this {Debugger8080} - * @param {number} addr - * @param {number} nb (# of bytes) - * @param {Array} aBreak - * @param {boolean} [fTemporary] - * @return {boolean} true if breakpoint has been hit, false if not - */ - Debugger8080.prototype.checkBreakpoint = function(addr, nb, aBreak, fTemporary) - { - /* - * Time to check for execution breakpoints; note that this should be done BEFORE updating frequency - * or history data (see checkInstruction), since we might not actually execute the current instruction. - */ - var fBreak = false; - - if (!this.nSuppressBreaks++) { - - for (var i = 1; !fBreak && i < aBreak.length; i++) { - - var dbgAddrBreak = aBreak[i]; - - if (fTemporary && !dbgAddrBreak.fTemporary) continue; - - /* - * We used to calculate the linear address of the breakpoint at the time the - * breakpoint was added, so that a breakpoint set in one mode (eg, in real-mode) - * would still work as intended if the mode changed later (eg, to protected-mode). - * - * However, that created difficulties setting protected-mode breakpoints in segments - * that might not be defined yet, or that could move in physical memory. - * - * If you want to create a real-mode breakpoint that will break regardless of mode, - * use the physical address of the real-mode memory location instead. - */ - var addrBreak = this.getAddr(dbgAddrBreak); - for (var n = 0; n < nb; n++) { - if (addr + n == addrBreak) { - var a; - fBreak = true; - if (dbgAddrBreak.fTemporary) { - this.findBreakpoint(aBreak, dbgAddrBreak, true, true); - fTemporary = true; - } - if (a = dbgAddrBreak.aCmds) { - /* - * When one or more commands are attached to a breakpoint, we don't halt by default. - * Instead, we set fBreak to true only if, at the completion of all the commands, the - * CPU is halted; in other words, you should include "h" as one of the breakpoint commands - * if you want the breakpoint to stop execution. - * - * Another useful command is "if", which will return false if the expression is false, - * at which point we'll jump ahead to the next "else" command, and if there isn't an "else", - * we abort. - */ - fBreak = false; - for (var j = 0; j < a.length; j++) { - if (!this.doCommand(a[j], true)) { - if (a[j].indexOf("if")) { - fBreak = true; // the failed command wasn't "if", so abort - break; - } - var k = j + 1; - for (; k < a.length; k++) { - if (!a[k].indexOf("else")) break; - j++; - } - if (k == a.length) { // couldn't find an "else" after the "if", so abort - fBreak = true; - break; - } - /* - * If we're still here, we'll execute the "else" command (which is just a no-op), - * followed by any remaining commands. - */ - } - } - if (!this.cpu.isRunning()) fBreak = true; - } - if (fBreak) { - if (!fTemporary) this.printBreakpoint(aBreak, i, "hit"); - break; - } - } - } - } - } - - this.nSuppressBreaks--; - - return fBreak; - }; - - /** - * getInstruction(dbgAddr, sComment, nSequence) - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {string} [sComment] is an associated comment - * @param {number} [nSequence] is an associated sequence number, undefined if none - * @return {string} (and dbgAddr is updated to the next instruction) - */ - Debugger8080.prototype.getInstruction = function(dbgAddr, sComment, nSequence) - { - var dbgAddrIns = this.newAddr(dbgAddr.addr); - - var bOpcode = this.getByte(dbgAddr, 1); - - var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086; - var aOpDesc = this.aaOpDescs[bOpcode]; - var iIns = aOpDesc[0]; - - var sOperands = ""; - var sOpcode = asOpcodes[iIns]; - var cOperands = aOpDesc.length - 1; - var typeSizeDefault = Debugger8080.TYPE_NONE, type; - - for (var iOperand = 1; iOperand <= cOperands; iOperand++) { - - var disp, off, cch; - var sOperand = ""; - - type = aOpDesc[iOperand]; - if (type === undefined) continue; - if ((type & Debugger8080.TYPE_OPT) && this.style == Debugger8080.STYLE_8080) continue; - - var typeMode = type & Debugger8080.TYPE_MODE; - if (!typeMode) continue; - - var typeSize = type & Debugger8080.TYPE_SIZE; - if (!typeSize) { - type |= typeSizeDefault; - } else { - typeSizeDefault = typeSize; - } - - var typeOther = type & Debugger8080.TYPE_OTHER; - if (!typeOther) { - type |= (iOperand == 1? Debugger8080.TYPE_OUT : Debugger8080.TYPE_IN); - } - - if (typeMode & Debugger8080.TYPE_IMM) { - sOperand = this.getImmOperand(type, dbgAddr); - } - else if (typeMode & Debugger8080.TYPE_REG) { - sOperand = this.getRegOperand((type & Debugger8080.TYPE_IREG) >> 8, type, dbgAddr); - } - else if (typeMode & Debugger8080.TYPE_INT) { - sOperand = ((bOpcode >> 3) & 0x7).toString(); - } - - if (!sOperand || !sOperand.length) { - sOperands = "INVALID"; - break; - } - if (sOperands.length > 0) sOperands += ','; - sOperands += (sOperand || "???"); - } - - var sBytes = ""; - var sLine = this.toHexAddr(dbgAddrIns) + ' '; - if (dbgAddrIns.addr !== CPUDef8080.ADDR_INVALID && dbgAddr.addr !== CPUDef8080.ADDR_INVALID) { - do { - sBytes += str.toHex(this.getByte(dbgAddrIns, 1), 2); - if (dbgAddrIns.addr == null) break; - } while (dbgAddrIns.addr != dbgAddr.addr); - } - - sLine += str.pad(sBytes, 10); - sLine += (type & Debugger8080.TYPE_UNDOC)? '*' : ' '; - sLine += str.pad(sOpcode, 7); - if (sOperands) sLine += ' ' + sOperands; - - if (sComment) { - sLine = str.pad(sLine, 40) + ';' + sComment; - if (!this.cpu.flags.checksum) { - sLine += (nSequence != null? '=' + nSequence.toString() : ""); - } else { - var nCycles = this.cpu.getCycles(); - sLine += "cycles=" + nCycles.toString() + " cs=" + str.toHex(this.cpu.nChecksum); - } - } - return sLine; - }; - - /** - * getImmOperand(type, dbgAddr) - * - * @this {Debugger8080} - * @param {number} type - * @param {DbgAddr8080} dbgAddr - * @return {string} operand - */ - Debugger8080.prototype.getImmOperand = function(type, dbgAddr) - { - var sOperand = ' '; - var typeSize = type & Debugger8080.TYPE_SIZE; - - switch (typeSize) { - case Debugger8080.TYPE_BYTE: - sOperand = str.toHex(this.getByte(dbgAddr, 1), 2); - break; - case Debugger8080.TYPE_SBYTE: - sOperand = str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4); - break; - case Debugger8080.TYPE_WORD: - sOperand = str.toHex(this.getShort(dbgAddr, 2), 4); - break; - default: - return "imm(" + str.toHexWord(type) + ')'; - } - if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) { - sOperand = '[' + sOperand + ']'; - } else if (!(type & Debugger8080.TYPE_REG)) { - sOperand = (this.style == Debugger8080.STYLE_8080? '$' : "0x") + sOperand; - } - return sOperand; - }; - - /** - * getRegOperand(iReg, type, dbgAddr) - * - * @this {Debugger8080} - * @param {number} iReg - * @param {number} type - * @param {DbgAddr8080} dbgAddr - * @return {string} operand - */ - Debugger8080.prototype.getRegOperand = function(iReg, type, dbgAddr) - { - /* - * Although this breaks with 8080 assembler conventions, I'm going to experiment with some different - * mnemonics; specifically, "[HL]" instead of "M". This is also more in keeping with how getImmOperand() - * displays memory references (ie, by enclosing them in brackets). - */ - var sOperand = Debugger8080.REGS[iReg]; - if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) { - if (iReg == Debugger8080.REG_M) { - sOperand = "HL"; - } - sOperand = '[' + sOperand + ']'; - } - return sOperand; - }; - - /** - * parseInstruction(sOp, sOperand, addr) - * - * TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a working implementation. - * - * @this {Debugger8080} - * @param {string} sOp - * @param {string|undefined} sOperand - * @param {DbgAddr8080} dbgAddr of memory where this instruction is being assembled - * @return {Array.} of opcode bytes; if the instruction can't be parsed, the array will be empty - */ - Debugger8080.prototype.parseInstruction = function(sOp, sOperand, dbgAddr) - { - var aOpBytes = []; - this.println("not supported yet"); - return aOpBytes; - }; - - /** - * getFlagOutput(sFlag) - * - * @this {Debugger8080} - * @param {string} sFlag - * @return {string} value of flag - */ - Debugger8080.prototype.getFlagOutput = function(sFlag) - { - var b; - switch (sFlag) { - case "IF": - b = this.cpu.getIF(); - break; - case "SF": - b = this.cpu.getSF(); - break; - case "ZF": - b = this.cpu.getZF(); - break; - case "AF": - b = this.cpu.getAF(); - break; - case "PF": - b = this.cpu.getPF(); - break; - case "CF": - b = this.cpu.getCF(); - break; - default: - b = 0; - break; - } - return sFlag.charAt(0) + (b? '1' : '0') + ' '; - }; - - /** - * getRegOutput(iReg) - * - * @this {Debugger8080} - * @param {number} iReg - * @return {string} - */ - Debugger8080.prototype.getRegOutput = function(iReg) - { - var sReg = Debugger8080.REGS[iReg]; - return sReg + '=' + this.getRegString(iReg) + ' '; - }; - - /** - * getRegDump() - * - * Sample 8080 register dump: - * - * A=00 BC=0000 DE=0000 HL=0000 SP=0000 I0 S0 Z0 A0 P0 C0 - * 0000 00 NOP - * - * @this {Debugger8080} - * @return {string} - */ - Debugger8080.prototype.getRegDump = function() - { - var s; - s = this.getRegOutput(Debugger8080.REG_A) + - this.getRegOutput(Debugger8080.REG_BC) + - this.getRegOutput(Debugger8080.REG_DE) + - this.getRegOutput(Debugger8080.REG_HL) + - this.getRegOutput(Debugger8080.REG_SP) + - this.getFlagOutput("IF") + this.getFlagOutput("SF") + this.getFlagOutput("ZF") + - this.getFlagOutput("AF") + this.getFlagOutput("PF") + this.getFlagOutput("CF"); - return s; - }; - - /** - * comparePairs(p1, p2) - * - * @this {Debugger8080} - * @param {number|string|Array|Object} p1 - * @param {number|string|Array|Object} p2 - * @return {number} - */ - Debugger8080.prototype.comparePairs = function(p1, p2) - { - return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0; - }; - - /** - * addSymbols(sModule, addr, len, aSymbols) - * - * As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist - * of all the symbols (in upper-case), and the values of those properties are objects containing any or all of - * the following properties: - * - * 'v': the value of an absolute (unsized) value - * 'b': either 1, 2, 4 or undefined if an unsized value - * 's': either a hard-coded segment or undefined - * 'o': the offset of the symbol within the associated address space - * 'l': the original-case version of the symbol, present only if it wasn't originally upper-case - * 'a': annotation for the specified offset; eg, the original assembly language, with optional comment - * - * To that list of properties, we also add: - * - * 'p': the physical address (calculated whenever both 's' and 'o' properties are defined) - * - * Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values - * for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original - * annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for - * converting them back to individual double-quotes. - * - * For example: - * { - * 'HF_PORT': { - * 'v':800 - * }, - * 'HDISK_INT': { - * 'b':4, 's':0, 'o':52 - * }, - * 'ORG_VECTOR': { - * 'b':4, 's':0, 'o':76 - * }, - * 'CMD_BLOCK': { - * 'b':1, 's':64, 'o':66 - * }, - * 'DISK_SETUP': { - * 'o':3 - * }, - * '.40': { - * 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR" - * } - * } - * - * If a symbol only has an offset, then that offset value can be assigned to the symbol property directly: - * - * 'DISK_SETUP': 3 - * - * The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol. - * Such entries are identified by a period followed by a unique number (usually the offset of the entry), and - * they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading - * period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols. - * - * We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which - * look like: - * - * [addr, len, aSymbols, aOffsets] - * - * There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any, - * at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous - * symbol with a matching offset ('o') property. - * - * To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs - * (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space. - * - * We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert(); - * it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're - * hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates - * properly. - * - * @this {Debugger8080} - * @param {string|null} sModule - * @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM) - * @param {number} len (the size of the region, in bytes) - * @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below) - */ - Debugger8080.prototype.addSymbols = function(sModule, addr, len, aSymbols) - { - var dbgAddr = {}; - var aOffsets = []; - for (var sSymbol in aSymbols) { - var symbol = aSymbols[sSymbol]; - if (typeof symbol == "number") { - aSymbols[sSymbol] = symbol = {'o': symbol}; - } - var offSymbol = symbol['o']; - var sAnnotation = symbol['a']; - if (offSymbol !== undefined) { - usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs); - } - if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\""); - } - var symbolTable = { - sModule: sModule, - addr: addr, - len: len, - aSymbols: aSymbols, - aOffsets: aOffsets - }; - this.aSymbolTable.push(symbolTable); - }; - - /** - * dumpSymbols() - * - * TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever - * order they appeared in the original MAP file. - * - * @this {Debugger8080} - */ - Debugger8080.prototype.dumpSymbols = function() - { - for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { - var symbolTable = this.aSymbolTable[iTable]; - for (var sSymbol in symbolTable.aSymbols) { - if (sSymbol.charAt(0) == '.') continue; - var symbol = symbolTable.aSymbols[sSymbol]; - var offSymbol = symbol['o']; - if (offSymbol === undefined) continue; - var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l']; - if (sSymbolOrig) sSymbol = sSymbolOrig; - this.println(this.toHexOffset(offSymbol) + ' ' + sSymbol); - } - } - }; - - /** - * findSymbol(dbgAddr, fNearest) - * - * Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found). - * - * If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of - * entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @param {boolean} [fNearest] - * @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment - */ - Debugger8080.prototype.findSymbol = function(dbgAddr, fNearest) - { - var aSymbol = []; - var addrSymbol = this.getAddr(dbgAddr) >>> 0; - for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { - var symbolTable = this.aSymbolTable[iTable]; - var addr = symbolTable.addr >>> 0; - var len = symbolTable.len; - if (addrSymbol >= addr && addrSymbol < addr + len) { - var offSymbol = addrSymbol - addr; - var result = usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs); - if (result >= 0) { - this.returnSymbol(iTable, result, aSymbol); - } - else if (fNearest) { - result = ~result; - this.returnSymbol(iTable, result-1, aSymbol); - this.returnSymbol(iTable, result, aSymbol); - } - break; - } - } - return aSymbol; - }; - - /** - * findSymbolAddr(sSymbol) - * - * Search aSymbolTable for sSymbol, and if found, return a dbgAddr (same as parseAddr()) - * - * @this {Debugger8080} - * @param {string} sSymbol - * @return {DbgAddr8080|undefined} - */ - Debugger8080.prototype.findSymbolAddr = function(sSymbol) - { - var dbgAddr; - if (sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) { - var sUpperCase = sSymbol.toUpperCase(); - for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { - var symbolTable = this.aSymbolTable[iTable]; - var symbol = symbolTable.aSymbols[sUpperCase]; - if (symbol !== undefined) { - var offSymbol = symbol['o']; - if (offSymbol !== undefined) { - /* - * We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an - * explicitly-defined segment, we return the segment associated with the entire group; for - * a ROM, that segment is normally "addrROM >>> 4". Down the road, we may want/need to - * support a special symbol entry (eg, ".ORG") that defines an alternate origin. - */ - dbgAddr = this.newAddr(offSymbol); - } - /* - * The symbol matched, but it wasn't for an address (no 'o' offset), and there's no point - * looking any farther, since each symbol appears only once, so we indicate it's an unknown symbol. - */ - break; - } - } - } - return dbgAddr; - }; - - /** - * returnSymbol(iTable, iOffset, aSymbol) - * - * Helper function for findSymbol(). - * - * @param {number} iTable - * @param {number} iOffset - * @param {Array} aSymbol is updated with the specified symbol, if it exists - */ - Debugger8080.prototype.returnSymbol = function(iTable, iOffset, aSymbol) - { - var symbol = {}; - var aOffsets = this.aSymbolTable[iTable].aOffsets; - var offset = 0, sSymbol = null; - if (iOffset >= 0 && iOffset < aOffsets.length) { - offset = aOffsets[iOffset][0]; - sSymbol = aOffsets[iOffset][1]; - } - if (sSymbol) { - symbol = this.aSymbolTable[iTable].aSymbols[sSymbol]; - sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol)); - } - aSymbol.push(sSymbol); - aSymbol.push(offset); - aSymbol.push(symbol['a']); - aSymbol.push(symbol['c']); - }; - - /** - * doHelp() - * - * @this {Debugger8080} - */ - Debugger8080.prototype.doHelp = function() - { - var s = "commands:"; - for (var sCommand in Debugger8080.COMMANDS) { - s += '\n' + str.pad(sCommand, 9) + Debugger8080.COMMANDS[sCommand]; - } - if (!this.checksEnabled()) s += "\nnote: frequency/history disabled if no exec breakpoints"; - this.println(s); - }; - - /** - * doAssemble(asArgs) - * - * This always receives the complete argument array, where the order of the arguments is: - * - * [0]: the assemble command (assumed to be "a") - * [1]: the target address (eg, "200") - * [2]: the operation code, aka instruction name (eg, "adc") - * [3]: the operation mode operand, if any (eg, "14", "[1234]", etc) - * - * The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present. - * As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands - * until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a" - * and the next available target address to the argument array. - * - * Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg: - * - * a ff00 cld - * a ff01 ldx 28 - * ... - * - * without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage - * of automatic target address advancement (see dbgAddrAssemble). - * - * NOTE: As the previous example implies, you can even assemble new instructions into ROM address space; - * as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU. - * - * @this {Debugger8080} - * @param {Array.} asArgs is the complete argument array, beginning with the "a" command in asArgs[0] - */ - Debugger8080.prototype.doAssemble = function(asArgs) - { - var dbgAddr = this.parseAddr(asArgs[1], true); - if (!dbgAddr) return; - - this.dbgAddrAssemble = dbgAddr; - if (asArgs[2] === undefined) { - this.println("begin assemble at " + this.toHexAddr(dbgAddr)); - this.fAssemble = true; - this.cpu.updateCPU(); - return; - } - - var aOpBytes = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr); - if (aOpBytes.length) { - for (var i = 0; i < aOpBytes.length; i++) { - this.setByte(dbgAddr, aOpBytes[i], 1); - } - /* - * Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced. - */ - this.println(this.getInstruction(this.dbgAddrAssemble)); - } - }; - - /** - * doBreak(sCmd, sAddr, sOptions) - * - * As the "help" output below indicates, the following breakpoint commands are supported: - * - * bp [a] set exec breakpoint on linear addr [a] - * br [a] set read breakpoint on linear addr [a] - * bw [a] set write breakpoint on linear addr [a] - * bc [a] clear breakpoint on linear addr [a] (use "*" for all breakpoints) - * bl list breakpoints - * - * to which we have recently added the following I/O breakpoint commands: - * - * bi [p] toggle input breakpoint on port [p] (use "*" for all input ports) - * bo [p] toggle output breakpoint on port [p] (use "*" for all output ports) - * - * These two new commands operate as toggles so that if "*" is used to trap all input (or output), - * you can also use these commands to NOT trap specific ports. - * - * bn [n] break after [n] instructions - * - * TODO: Update the "bl" command to include any/all I/O breakpoints, and the "bc" command to - * clear them. Because "bi" and "bo" commands are piggy-backing on Bus functions, those breakpoints - * are currently outside the realm of what the "bl" and "bc" commands are aware of. - * - * @this {Debugger8080} - * @param {string} sCmd - * @param {string|undefined} [sAddr] - * @param {string} [sOptions] (the rest of the breakpoint command-line) - */ - Debugger8080.prototype.doBreak = function(sCmd, sAddr, sOptions) - { - if (sAddr == '?') { - this.println("breakpoint commands:"); - this.println("\tbi [p]\ttoggle break on input port [p]"); - this.println("\tbo [p]\ttoggle break on output port [p]"); - this.println("\tbp [a]\tset exec breakpoint at addr [a]"); - this.println("\tbr [a]\tset read breakpoint at addr [a]"); - this.println("\tbw [a]\tset write breakpoint at addr [a]"); - this.println("\tbc [a]\tclear breakpoint at addr [a]"); - this.println("\tbl\tlist all breakpoints"); - this.println("\tbn [n]\tbreak after [n] instruction(s)"); - return; - } - - var sParm = sCmd.charAt(1); - if (sParm == 'l') { - var cBreaks = 0; - cBreaks += this.listBreakpoints(this.aBreakExec); - cBreaks += this.listBreakpoints(this.aBreakRead); - cBreaks += this.listBreakpoints(this.aBreakWrite); - if (!cBreaks) this.println("no breakpoints"); - return; - } - - if (sParm == 'n') { - this.nBreakIns = this.parseValue(sAddr); - this.println("break after " + this.nBreakIns + " instruction(s)"); - return; - } - - if (sAddr === undefined) { - this.println("missing breakpoint address"); - return; - } - - var dbgAddr = this.newAddr(); - if (sAddr != '*') { - dbgAddr = this.parseAddr(sAddr, true, true); - if (!dbgAddr) return; - } - - sAddr = str.toHexWord(dbgAddr.addr); - - if (sParm == 'c') { - if (dbgAddr.addr == null) { - this.clearBreakpoints(); - this.println("all breakpoints cleared"); - return; - } - if (this.findBreakpoint(this.aBreakExec, dbgAddr, true)) - return; - if (this.findBreakpoint(this.aBreakRead, dbgAddr, true)) - return; - if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true)) - return; - this.println("breakpoint missing: " + this.toHexAddr(dbgAddr)); - return; - } - - if (sParm == 'i') { - this.println("breakpoint " + (this.bus.addPortInputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (input)"); - return; - } - - if (sParm == 'o') { - this.println("breakpoint " + (this.bus.addPortOutputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (output)"); - return; - } - - if (dbgAddr.addr == null) return; - - this.parseAddrOptions(dbgAddr, sOptions); - - if (sParm == 'p') { - this.addBreakpoint(this.aBreakExec, dbgAddr); - return; - } - if (sParm == 'r') { - this.addBreakpoint(this.aBreakRead, dbgAddr); - return; - } - if (sParm == 'w') { - this.addBreakpoint(this.aBreakWrite, dbgAddr); - return; - } - this.println("unknown breakpoint command: " + sParm); - }; - - /** - * doClear(sCmd) - * - * @this {Debugger8080} - * @param {string} [sCmd] (eg, "cls" or "clear") - */ - Debugger8080.prototype.doClear = function(sCmd) - { - /* - * TODO: There should be a clear() component method that the Control Panel overrides to perform this function. - */ - if (this.controlPrint) this.controlPrint.value = ""; - }; - - /** - * doDump(asArgs) - * - * The length parameter is interpreted as a number of bytes, in hex, which we convert to the appropriate number - * of lines, because we always display whole lines. If the length is omitted/undefined, it defaults to 0x80 (128.) - * bytes, which normally translates to 8 lines. - * - * @this {Debugger8080} - * @param {Array.} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes]) - */ - Debugger8080.prototype.doDump = function(asArgs) - { - var m; - var sCmd = asArgs[0]; - var sAddr = asArgs[1]; - var sLen = asArgs[2]; - var sBytes = asArgs[3]; - - if (sAddr == '?') { - var sDumpers = ""; - for (m in Messages8080.CATEGORIES) { - if (this.afnDumpers[m]) { - if (sDumpers) sDumpers += ','; - sDumpers = sDumpers + m; - } - } - sDumpers += ",state,symbols"; - this.println("dump memory commands:"); - this.println("\tdb [a] [#] dump # bytes at address a"); - this.println("\tdw [a] [#] dump # words at address a"); - this.println("\tdd [a] [#] dump # dwords at address a"); - this.println("\tdh [#] [#] dump # instructions from history"); - if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers); - return; - } - - if (sAddr == "state") { - var sState = this.cmp.powerOff(true); - if (sLen == "console") { - /* - * Console buffers are notoriously small, and even the following code, which breaks the - * data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful. - * - * var nPart = +sBytes; - * if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000); - * - * So, the best way to capture a large machine state is to use the new "Save Machine" link - * that downloads a machine's entire state. Alternatively, run your own local server and use - * server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML - * control to the computer.powerOff() and computer.saveServerState() functions. - */ - console.log(sState); - } else { - this.doClear(); - this.println(sState); - } - return; - } - - if (sAddr == "symbols") { - this.dumpSymbols(); - return; - } - - if (sCmd == "d") { - for (m in Messages8080.CATEGORIES) { - if (asArgs[1] == m) { - var fnDumper = this.afnDumpers[m]; - if (fnDumper) { - asArgs.shift(); - asArgs.shift(); - fnDumper(asArgs); - } else { - this.println("no dump registered for " + sAddr); - } - return; - } - } - if (!sAddr) sCmd = this.sCmdDumpPrev || "db"; - } else { - this.sCmdDumpPrev = sCmd; - } - - if (sCmd == "dh") { - this.dumpHistory(sAddr, sLen); - return; - } - - var dbgAddr = this.parseAddr(sAddr); - if (!dbgAddr) return; - - var len = 0; // 0 is not a default; it triggers the appropriate default below - if (sLen) { - if (sLen.charAt(0) == 'l') { - sLen = sLen.substr(1) || sBytes; - } - len = this.parseValue(sLen) >>> 0; // negative lengths not allowed - if (len > 0x10000) len = 0x10000; // prevent bad user (or variable) input from producing excessive output - } - - var sDump = ""; - var size = (sCmd == "dd"? 4 : (sCmd == "dw"? 2 : 1)); - var cb = (size * len) || 128; - var cLines = ((cb + 15) >> 4) || 1; - - while (cLines-- && cb > 0) { - var data = 0, iByte = 0, i; - var sData = "", sChars = ""; - sAddr = this.toHexAddr(dbgAddr); - for (i = 16; i > 0 && cb > 0; i--) { - var b = this.getByte(dbgAddr, 1); - data |= (b << (iByte++ << 3)); - if (iByte == size) { - sData += str.toHex(data, size * 2); - sData += (size == 1? (i == 9? '-' : ' ') : " "); - data = iByte = 0; - } - sChars += (b >= 32 && b < 128? String.fromCharCode(b) : '.'); - cb--; - } - if (sDump) sDump += '\n'; - sDump += sAddr + " " + sData + ((i == 0)? (' ' + sChars) : ""); - } - - if (sDump) this.println(sDump); - this.dbgAddrNextData = dbgAddr; - }; - - /** - * doEdit(asArgs) - * - * @this {Debugger8080} - * @param {Array.} asArgs - */ - Debugger8080.prototype.doEdit = function(asArgs) - { - var size = 1; - var mask = 0xff; - var fnGet = this.getByte; - var fnSet = this.setByte; - if (asArgs[0] == "ew") { - size = 2; - mask = 0xffff; - fnGet = this.getShort; - fnSet = this.setShort; - } - var cch = size << 1; - - var sAddr = asArgs[1]; - if (sAddr == null) { - this.println("edit memory commands:"); - this.println("\teb [a] [...] edit bytes at address a"); - this.println("\tew [a] [...] edit words at address a"); - return; - } - - var dbgAddr = this.parseAddr(sAddr); - if (!dbgAddr) return; - - for (var i = 2; i < asArgs.length; i++) { - var vNew = this.parseExpression(asArgs[i]); - if (vNew === undefined) { - this.println("unrecognized value: " + asArgs[i]); - break; - } - if (vNew & ~mask) { - this.println("warning: " + str.toHex(vNew) + " exceeds " + size + "-byte value"); - } - var vOld = fnGet.call(this, dbgAddr); - this.println("changing " + this.toHexAddr(dbgAddr) + " from " + str.toHex(vOld, cch, true) + " to " + str.toHex(vNew, cch, true)); - fnSet.call(this, dbgAddr, vNew, size); - } - }; - - /** - * doFreqs(sParm) - * - * @this {Debugger8080} - * @param {string|undefined} sParm - */ - Debugger8080.prototype.doFreqs = function(sParm) - { - if (sParm == '?') { - this.println("frequency commands:"); - this.println("\tclear\tclear all frequency counts"); - return; - } - var i; - var cData = 0; - if (this.aaOpcodeCounts) { - if (sParm == "clear") { - for (i = 0; i < this.aaOpcodeCounts.length; i++) - this.aaOpcodeCounts[i] = [i, 0]; - this.println("frequency data cleared"); - cData++; - } - else if (sParm !== undefined) { - this.println("unknown frequency command: " + sParm); - cData++; - } - else { - var aaSortedOpcodeCounts = this.aaOpcodeCounts.slice(); - aaSortedOpcodeCounts.sort(function(p, q) { - return q[1] - p[1]; - }); - var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086; - for (i = 0; i < aaSortedOpcodeCounts.length; i++) { - var bOpcode = aaSortedOpcodeCounts[i][0]; - var cFreq = aaSortedOpcodeCounts[i][1]; - if (cFreq) { - this.println((asOpcodes[this.aaOpDescs[bOpcode][0]] + " ").substr(0, 5) + " (" + str.toHexByte(bOpcode) + "): " + cFreq + " times"); - cData++; - } - } - } - } - if (!cData) { - this.println("no frequency data available"); - } - }; - - /** - * doHalt(fQuiet) - * - * @this {Debugger8080} - * @param {boolean} [fQuiet] - */ - Debugger8080.prototype.doHalt = function(fQuiet) - { - var sMsg; - if (this.flags.running) { - sMsg = "halting"; - this.stopCPU(); - } else { - if (this.isBusy(true)) return; - sMsg = "already halted"; - } - if (!fQuiet) this.println(sMsg); - }; - - /** - * doIf(sCmd, fQuiet) - * - * NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate - * an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period - * (eg, "10.") if you really intend decimal. - * - * Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes - * "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true). - * - * @this {Debugger8080} - * @param {string} sCmd - * @param {boolean} [fQuiet] - * @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error) - */ - Debugger8080.prototype.doIf = function(sCmd, fQuiet) - { - sCmd = str.trim(sCmd); - if (!this.parseExpression(sCmd)) { - if (!fQuiet) this.println("false: " + sCmd); - return false; - } - if (!fQuiet) this.println("true: " + sCmd); - return true; - }; - - /** - * doInfo(asArgs) - * - * @this {Debugger8080} - * @param {Array.} asArgs - * @return {boolean} true only if the instruction info command ("n") is supported - */ - Debugger8080.prototype.doInfo = function(asArgs) - { - if (DEBUG) { - this.println("msPerYield: " + this.cpu.msPerYield); - this.println("nCyclesPerYield: " + this.cpu.nCyclesPerYield); - return true; - } - return false; - }; - - /** - * doInput(sPort) - * - * Simulate a 1-byte port input operation. - * - * @this {Debugger8080} - * @param {string|undefined} sPort - */ - Debugger8080.prototype.doInput = function(sPort) - { - if (!sPort || sPort == '?') { - this.println("input commands:"); - this.println("\ti [p]\tread port [p]"); - /* - * TODO: Regarding this warning, consider adding an "unchecked" version of - * bus.checkPortInputNotify(), since all Debugger memory accesses are unchecked, too. - * - * All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined), - * but changing them all to be non-destructive would take time, and situations where you - * actually want to affect the hardware state are just as likely as not.... - */ - this.println("warning: port accesses can affect hardware state"); - return; - } - var port = this.parseValue(sPort); - if (port !== undefined) { - var bIn = this.bus.checkPortInputNotify(port, 1); - this.println(str.toHexWord(port) + ": " + str.toHexByte(bIn)); - } - }; - - /** - * doInt(sLevel) - * - * @this {Debugger8080} - * @param {string} sLevel - * @return {boolean} true if success, false if error - */ - Debugger8080.prototype.doInt = function(sLevel) - { - if (!this.cpu.getIF()) { - this.println("interrupts disabled (use rif=1 to enable)"); - return false; - } - var nLevel = this.parseExpression(sLevel); - if (nLevel == null) return false; - this.println("requesting interrupt level " + nLevel); - this.cpu.requestINTR(nLevel); - return true; - }; - - /** - * doVar(sCmd) - * - * The command must be of the form "{variable} = [{expression}]", where expression may contain constants, - * operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if - * any, is deleted. - * - * Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}" - * prints the value of the specified variable. - * - * @this {Debugger8080} - * @param {string} sCmd - * @return {boolean} true if valid "var" assignment, false if not - */ - Debugger8080.prototype.doVar = function(sCmd) - { - var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i); - if (a) { - if (!a[1]) { - if (!this.printVariable()) this.println("no variables"); - return true; // it's not considered an error to print an empty list of variables - } - if (!a[2]) { - return this.printVariable(a[1]); - } - if (!a[3]) { - this.delVariable(a[1]); - return true; // it's not considered an error to delete a variable that didn't exist - } - var v = this.parseExpression(a[3]); - if (v !== undefined) { - this.setVariable(a[1], v); - return true; - } - return false; - } - this.println("invalid assignment:" + sCmd); - return false; - }; - - /** - * doList(sAddr, fPrint) - * - * @this {Debugger8080} - * @param {string} sAddr - * @param {boolean} [fPrint] - * @return {string|null} - */ - Debugger8080.prototype.doList = function(sAddr, fPrint) - { - var sSymbol = null; - - var dbgAddr = this.parseAddr(sAddr, true); - if (dbgAddr) { - var addr = this.getAddr(dbgAddr); - var aSymbol = this.findSymbol(dbgAddr, true); - if (aSymbol.length) { - var nDelta, sDelta, s; - if (aSymbol[0]) { - sDelta = ""; - nDelta = dbgAddr.addr - aSymbol[1]; - if (nDelta) sDelta = " + " + str.toHexWord(nDelta); - s = aSymbol[0] + " (" + this.toHexOffset(aSymbol[1]) + ')' + sDelta; - if (fPrint) this.println(s); - sSymbol = s; - } - if (aSymbol.length > 4 && aSymbol[4]) { - sDelta = ""; - nDelta = aSymbol[5] - dbgAddr.addr; - if (nDelta) sDelta = " - " + str.toHexWord(nDelta); - s = aSymbol[4] + " (" + this.toHexOffset(aSymbol[5]) + ')' + sDelta; - if (fPrint) this.println(s); - if (!sSymbol) sSymbol = s; - } - } else { - if (fPrint) this.println("no symbols"); - } - } - return sSymbol; - }; - - /** - * doMessages(asArgs) - * - * @this {Debugger8080} - * @param {Array.} asArgs - */ - Debugger8080.prototype.doMessages = function(asArgs) - { - var m; - var fCriteria = null; - var sCategory = asArgs[1]; - if (sCategory == '?') sCategory = undefined; - - if (sCategory !== undefined) { - var bitsMessage = 0; - if (sCategory == "all") { - bitsMessage = (0xffffffff|0) & ~(Messages8080.HALT | Messages8080.KEYS | Messages8080.LOG); - sCategory = null; - } else if (sCategory == "on") { - fCriteria = true; - sCategory = null; - } else if (sCategory == "off") { - fCriteria = false; - sCategory = null; - } else { - /* - * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, - * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". - */ - if (sCategory == "keys") sCategory = "key"; - if (sCategory == "kbd") sCategory = "keyboard"; - for (m in Messages8080.CATEGORIES) { - if (sCategory == m) { - bitsMessage = Messages8080.CATEGORIES[m]; - fCriteria = !!(this.bitsMessage & bitsMessage); - break; - } - } - if (!bitsMessage) { - this.println("unknown message category: " + sCategory); - return; - } - } - if (bitsMessage) { - if (asArgs[2] == "on") { - this.bitsMessage |= bitsMessage; - fCriteria = true; - } - else if (asArgs[2] == "off") { - this.bitsMessage &= ~bitsMessage; - fCriteria = false; - if (bitsMessage == Messages8080.BUFFER) { - for (var i = 0; i < this.aMessageBuffer.length; i++) { - this.println(this.aMessageBuffer[i]); - } - this.aMessageBuffer = []; - } - } - } - } - - /* - * Display those message categories that match the current criteria (on or off) - */ - var n = 0; - var sCategories = ""; - for (m in Messages8080.CATEGORIES) { - if (!sCategory || sCategory == m) { - var bitMessage = Messages8080.CATEGORIES[m]; - var fEnabled = !!(this.bitsMessage & bitMessage); - if (fCriteria !== null && fCriteria != fEnabled) continue; - if (sCategories) sCategories += ','; - if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line - /* - * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, - * but externally, we allow the user to specify "keys". - */ - if (m == "key") m = "keys"; - sCategories += m; - } - } - - if (sCategory === undefined) { - this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off"); - } - - this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none")); - - this.historyInit(); // call this just in case Messages8080.INT was turned on - }; - - /** - * doOptions(asArgs) - * - * @this {Debugger8080} - * @param {Array.} asArgs - */ - Debugger8080.prototype.doOptions = function(asArgs) - { - switch (asArgs[1]) { - case "8080": - this.style = Debugger8080.STYLE_8080; - break; - - case "8086": - this.style = Debugger8080.STYLE_8086; - break; - - case "cs": - var nCycles; - if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion - switch (asArgs[2]) { - case "int": - this.cpu.nCyclesChecksumInterval = nCycles; - break; - case "start": - this.cpu.nCyclesChecksumStart = nCycles; - break; - case "stop": - this.cpu.nCyclesChecksumStop = nCycles; - break; - default: - this.println("unknown cs option"); - return; - } - if (nCycles !== undefined) { - this.cpu.resetChecksum(); - } - this.println("checksums " + (this.cpu.flags.checksum? "enabled" : "disabled")); - return; - - case "sp": - if (asArgs[2] !== undefined) { - if (!this.cpu.setSpeed(+asArgs[2])) { - this.println("warning: using 1x multiplier, previous target not reached"); - } - } - this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)"); - return; - - case "?": - this.println("debugger options:"); - this.println("\t8080\t\tselect 8080-style mnemonics"); - this.println("\t8086\t\tselect 8086-style mnemonics"); - this.println("\tcs int #\tset checksum cycle interval to #"); - this.println("\tcs start #\tset checksum cycle start count to #"); - this.println("\tcs stop #\tset checksum cycle stop count to #"); - this.println("\tsp #\t\tset speed multiplier to #"); - break; - - default: - if (asArgs[1]) { - this.println("unknown option: " + asArgs[1]); - return; - } - break; - } - this.println(this.style + "-style mnemonics enabled"); - }; - - /** - * doOutput(sPort, sByte) - * - * Simulate a 1-byte port output operation. - * - * @this {Debugger8080} - * @param {string|undefined} sPort - * @param {string|undefined} sByte (string representation of 1 byte) - */ - Debugger8080.prototype.doOutput = function(sPort, sByte) - { - if (!sPort || sPort == '?') { - this.println("output commands:"); - this.println("\to [p] [b]\twrite byte [b] to port [p]"); - /* - * TODO: Regarding this warning, consider adding an "unchecked" version of - * bus.checkPortOutputNotify(), since all Debugger memory accesses are unchecked, too. - * - * All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined), - * but changing them all to be non-destructive would take time, and situations where you - * actually want to affect the hardware state are just as likely as not.... - */ - this.println("warning: port accesses can affect hardware state"); - return; - } - var port = this.parseValue(sPort, "port #"); - var bOut = this.parseValue(sByte); - if (port !== undefined && bOut !== undefined) { - this.bus.checkPortOutputNotify(port, 1, bOut); - this.println(str.toHexWord(port) + ": " + str.toHexByte(bOut)); - } - }; - - /** - * doRegisters(asArgs, fInstruction) - * - * @this {Debugger8080} - * @param {Array.} [asArgs] - * @param {boolean} [fInstruction] (true to include the current instruction; default is true) - */ - Debugger8080.prototype.doRegisters = function(asArgs, fInstruction) - { - if (asArgs && asArgs[1] == '?') { - this.println("register commands:"); - this.println("\tr\tdump registers"); - this.println("\trx [#]\tset flag or register x to [#]"); - return; - } - - var cpu = this.cpu; - if (fInstruction == null) fInstruction = true; - - if (asArgs != null && asArgs.length > 1) { - var sReg = asArgs[1]; - var sValue = null; - var i = sReg.indexOf('='); - if (i > 0) { - sValue = sReg.substr(i + 1); - sReg = sReg.substr(0, i); - } - else if (asArgs.length > 2) { - sValue = asArgs[2]; - } - else { - this.println("missing value for " + asArgs[1]); - return; - } - - var w = this.parseExpression(sValue); - if (w === undefined) return; - - var sRegMatch = sReg.toUpperCase(); - switch (sRegMatch) { - case "A": - cpu.regA = w & 0xff; - break; - case "B": - cpu.regB = w & 0xff; - break; - case "BC": - cpu.regB = ((w >> 8) & 0xff); - /* falls through */ - case "C": - cpu.regC = w & 0xff; - break; - case "D": - cpu.regD = w & 0xff; - break; - case "DE": - cpu.regD = ((w >> 8) & 0xff); - /* falls through */ - case "E": - cpu.regE = w & 0xff; - break; - case "H": - cpu.regH = w & 0xff; - break; - case "HL": - cpu.regH = ((w >> 8) & 0xff); - /* falls through */ - case "L": - cpu.regL = w & 0xff; - break; - case "SP": - cpu.setSP(w); - break; - case "PC": - cpu.setPC(w); - this.dbgAddrNextCode = this.newAddr(cpu.getPC()); - break; - case "PS": - cpu.setPS(w); - break; - case "PSW": - cpu.setPSW(w); - break; - case "CF": - if (w) cpu.setCF(); else cpu.clearCF(); - break; - case "PF": - if (w) cpu.setPF(); else cpu.clearPF(); - break; - case "AF": - if (w) cpu.setAF(); else cpu.clearAF(); - break; - case "ZF": - if (w) cpu.setZF(); else cpu.clearZF(); - break; - case "SF": - if (w) cpu.setSF(); else cpu.clearSF(); - break; - case "IF": - if (w) cpu.setIF(); else cpu.clearIF(); - break; - default: - this.println("unknown register: " + sReg); - return; - } - cpu.updateCPU(); - this.println("updated registers:"); - } - - this.println(this.getRegDump()); - - if (fInstruction) { - this.dbgAddrNextCode = this.newAddr(cpu.getPC()); - this.doUnassemble(this.toHexAddr(this.dbgAddrNextCode)); - } - }; - - /** - * doRun(sCmd, sAddr, sOptions, fQuiet) - * - * @this {Debugger8080} - * @param {string} sCmd - * @param {string|undefined} [sAddr] - * @param {string} [sOptions] (the rest of the breakpoint command-line) - * @param {boolean} [fQuiet] - */ - Debugger8080.prototype.doRun = function(sCmd, sAddr, sOptions, fQuiet) - { - if (sCmd == "gt") { - this.fIgnoreNextCheckFault = true; - } - if (sAddr !== undefined) { - var dbgAddr = this.parseAddr(sAddr, true); - if (!dbgAddr) return; - this.parseAddrOptions(dbgAddr, sOptions); - this.setTempBreakpoint(dbgAddr); - } - if (!this.runCPU(true)) { - if (!fQuiet) this.println("cpu busy or unavailable, run command ignored"); - } - }; - - /** - * doPrint(sCmd) - * - * NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that - * you can take advantage of all the special replacement options used for software interrupt logging. - * - * @this {Debugger8080} - * @param {string} sCmd - */ - Debugger8080.prototype.doPrint = function(sCmd) - { - sCmd = str.trim(sCmd); - var a = sCmd.match(/^(['"])(.*?)\1$/); - if (!a) { - this.parseExpression(sCmd, true); - } else { - this.println(this.replaceRegs(a[2])); - } - }; - - /** - * doStep(sCmd) - * - * @this {Debugger8080} - * @param {string} [sCmd] "p" or "pr" - */ - Debugger8080.prototype.doStep = function(sCmd) - { - var fCallStep = true; - var fRegs = (sCmd == "pr"? 1 : 0); - /* - * Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants - * a subsequent register dump ("pr") or not ("p"). - */ - var nStep = 1 + fRegs; - if (!this.nStep) { - var dbgAddr = this.newAddr(this.cpu.getPC()); - var bOpcode = this.getByte(dbgAddr); - - switch (bOpcode) { - case CPUDef8080.OPCODE.CALL: - if (fCallStep) { - this.nStep = nStep; - this.incAddr(dbgAddr, 3); - } - break; - default: - break; - } - - if (this.nStep) { - this.setTempBreakpoint(dbgAddr); - if (!this.runCPU()) { - if (this.cmp) this.cmp.updateFocus(); - this.nStep = 0; - } - /* - * A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(), - * which will clear nStep, so there's your assurance that nStep will be reset. Now we may have - * stopped for reasons unrelated to the temporary breakpoint, but that's OK. - */ - } else { - this.doTrace(fRegs? "tr" : "t"); - } - } else { - this.println("step in progress"); - } - }; - - /** - * getCall(dbgAddr) - * - * Given a possible return address (typically from the stack), look for a matching CALL (or INT) that - * immediately precedes that address. - * - * @this {Debugger8080} - * @param {DbgAddr8080} dbgAddr - * @return {string|null} CALL instruction at or near dbgAddr, or null if none - */ - Debugger8080.prototype.getCall = function(dbgAddr) - { - var sCall = null; - var addr = dbgAddr.addr; - var addrOrig = addr; - for (var n = 1; n <= 6 && !!addr; n++) { - if (n > 2) { - dbgAddr.addr = addr; - var s = this.getInstruction(dbgAddr); - if (s.indexOf("CALL") >= 0) { - /* - * Verify that the length of this CALL (or INT), when added to the address of the CALL (or INT), - * matches the original return address. We do this by getting the string index of the opcode bytes, - * subtracting that from the string index of the next space, and dividing that difference by two, - * to yield the length of the CALL (or INT) instruction, in bytes. - */ - var i = s.indexOf(' '); - var j = s.indexOf(' ', i+1); - if (addr + (j - i - 1)/2 == addrOrig) { - sCall = s; - break; - } - } - } - addr--; - } - dbgAddr.addr = addrOrig; - return sCall; - }; - - /** - * doStackTrace(sCmd, sAddr) - * - * Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info. - * - * @this {Debugger8080} - * @param {string} [sCmd] - * @param {string} [sAddr] (not used yet) - */ - Debugger8080.prototype.doStackTrace = function(sCmd, sAddr) - { - if (sAddr == '?') { - this.println("stack trace commands:"); - this.println("\tk\tshow frame addresses"); - this.println("\tks\tshow symbol information"); - return; - } - - var nFrames = 10, cFrames = 0; - var dbgAddrCall = this.newAddr(); - var dbgAddrStack = this.newAddr(this.cpu.getSP()); - this.println("stack trace for " + this.toHexAddr(dbgAddrStack)); - - while (cFrames < nFrames) { - var sCall = null, sCallPrev = null, cTests = 256; - while ((dbgAddrStack.addr >>> 0) < 0x10000) { - dbgAddrCall.addr = this.getWord(dbgAddrStack, true); - /* - * Because we're using the auto-increment feature of getWord(), and because that will automatically - * wrap the offset around the end of the segment, we must also check the addr property to detect the wrap. - */ - if (dbgAddrStack.addr == null || !cTests--) break; - sCall = this.getCall(dbgAddrCall); - if (sCall) break; - } - /* - * The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always) - * indicative of a false positive, in which case the previous call is probably bogus as well, but - * at least we won't duplicate that mistake. Of course, there are always exceptions, recursion - * being one of them, but it's rare that we're debugging recursive code. - */ - if (!sCall || sCall == sCallPrev) break; - var sSymbol = null; - if (sCmd == "ks") { - var a = sCall.match(/[0-9A-F]+$/); - if (a) sSymbol = this.doList(a[0]); - } - sCall = str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toHexAddr(dbgAddrStack)); // + " return=" + this.toHexAddr(dbgAddrCall)); - this.println(sCall); - sCallPrev = sCall; - cFrames++; - } - if (!cFrames) this.println("no return addresses found"); - }; - - /** - * doTrace(sCmd, sCount) - * - * The "t" and "tr" commands interpret the count as a number of instructions, and since - * we call the Debugger's stepCPU() for each iteration, a single instruction includes - * any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only - * difference between "t" and "tr": the former displays only the next instruction, while - * the latter also displays the (updated) registers. - * - * The "tc" command interprets the count as a number of cycles rather than instructions, - * allowing you to quickly execute large chunks of instructions with a single command; it - * doesn't display anything until the the chunk has finished. - * - * However, generally a more useful command is "bn", which allows you to break after some - * number of instructions have been executed (as opposed to some number of cycles). - * - * @this {Debugger8080} - * @param {string} [sCmd] ("t", "tc", or "tr") - * @param {string} [sCount] # of instructions to step - */ - Debugger8080.prototype.doTrace = function(sCmd, sCount) - { - var dbg = this; - var fRegs = (sCmd != "t"); - var nCount = this.parseValue(sCount, null, true) || 1; - var nCycles = (nCount == 1? 0 : 1); - if (sCmd == "tc") { - nCycles = nCount; - nCount = 1; - } - web.onCountRepeat( - nCount, - function onCountStep() { - return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false); - }, - function onCountStepComplete() { - /* - * We explicitly called stepCPU() with fUpdateCPU === false, because repeatedly - * calling updateCPU() can be very slow, especially when fDisplayLiveRegs is true, - * so once the repeat count has been exhausted, we must perform a final updateCPU(). - */ - dbg.cpu.updateCPU(); - dbg.setBusy(false); - } - ); - }; - - /** - * doUnassemble(sAddr, sAddrEnd, n) - * - * @this {Debugger8080} - * @param {string} [sAddr] - * @param {string} [sAddrEnd] - * @param {number} [n] - */ - Debugger8080.prototype.doUnassemble = function(sAddr, sAddrEnd, n) - { - var dbgAddr = this.parseAddr(sAddr, true); - if (!dbgAddr) return; - - if (n === undefined) n = 1; - - var cb = 0x100; - if (sAddrEnd !== undefined) { - - var dbgAddrEnd = this.parseAddr(sAddrEnd, true); - if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return; - - cb = dbgAddrEnd.addr - dbgAddr.addr; - if (!DEBUG && cb > 0x100) { - /* - * Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to - * prevent the user from wedging the browser by dumping too many lines, but also a recognition - * that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway. - */ - this.println("range too large"); - return; - } - n = -1; - } - - var cLines = 0; - var sInstruction; - - while (cb > 0 && n--) { - - var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null; - var sComment = (nSequence != null? "cycles" : null); - var aSymbol = this.findSymbol(dbgAddr); - - var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it - - if (aSymbol[0] && n) { - if (!cLines && n || aSymbol[0].indexOf('+') < 0) { - var sLabel = aSymbol[0] + ':'; - if (aSymbol[2]) sLabel += ' ' + aSymbol[2]; - this.println(sLabel); - } - } - - if (aSymbol[3]) { - sComment = aSymbol[3]; - nSequence = null; - } - - sInstruction = this.getInstruction(dbgAddr, sComment, nSequence); - - this.println(sInstruction); - this.dbgAddrNextCode = dbgAddr; - cb -= dbgAddr.addr - addr; - cLines++; - } - }; - - /** - * parseCommand(sCmd, fSave, chSep) - * - * @this {Debugger8080} - * @param {string|undefined} sCmd - * @param {boolean} [fSave] is true to save the command, false if not - * @param {string} [chSep] is the command separator character (default is ';') - * @return {Array.} - */ - Debugger8080.prototype.parseCommand = function(sCmd, fSave, chSep) - { - if (fSave) { - if (!sCmd) { - if (this.fAssemble) { - sCmd = "end"; - } else { - sCmd = this.aPrevCmds[this.iPrevCmd+1]; - } - } else { - if (this.iPrevCmd < 0 && this.aPrevCmds.length) { - this.iPrevCmd = 0; - } - if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) { - this.aPrevCmds.splice(0, 0, sCmd); - this.iPrevCmd = 0; - } - this.iPrevCmd--; - } - } - var a = []; - if (sCmd) { - /* - * With the introduction of breakpoint commands (ie, quoted command sequences - * associated with a breakpoint), we can no longer perform simplistic splitting. - * - * a = sCmd.split(chSep || ';'); - * for (var i = 0; i < a.length; i++) a[i] = str.trim(a[i]); - * - * We may now split on semi-colons ONLY if they are outside a quoted sequence. - * - * Also, to allow quoted strings *inside* breakpoint commands, we first replace all - * DOUBLE double-quotes with single quotes. - */ - sCmd = sCmd.toLowerCase().replace(/""/g, "'"); - - var iPrev = 0; - var chQuote = null; - chSep = chSep || ';'; - /* - * NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking - * advantage of the fact that charAt() with an invalid index returns an empty string, - * allowing us to use the same substring() call to capture the final portion of sCmd. - * - * In a sense, it allows us to pretend that the string ends with a zero terminator. - */ - for (var i = 0; i <= sCmd.length; i++) { - var ch = sCmd.charAt(i); - if (ch == '"' || ch == "'") { - if (!chQuote) { - chQuote = ch; - } else if (ch == chQuote) { - chQuote = null; - } - } - else if (ch == chSep && !chQuote || !ch) { - /* - * Recall that substring() accepts starting (inclusive) and ending (exclusive) - * indexes, whereas substr() accepts a starting index and a length. We need the former. - */ - a.push(str.trim(sCmd.substring(iPrev, i))); - iPrev = i + 1; - } - } - } - return a; - }; - - /** - * shiftArgs(asArgs) - * - * Used with any command (eg, "r") that allows but doesn't require whitespace between command and first argument. - * - * @this {Debugger8080} - * @param {Array.} asArgs - * @return {Array.} - */ - Debugger8080.prototype.shiftArgs = function(asArgs) - { - if (asArgs && asArgs.length) { - var s0 = asArgs[0]; - var ch0 = s0.charAt(0); - for (var i = 1; i < s0.length; i++) { - var ch = s0.charAt(i); - if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') { - asArgs[0] = s0.substr(i); - asArgs.unshift(s0.substr(0, i)); - break; - } - } - } - return asArgs; - }; - - /** - * doCommand(sCmd, fQuiet) - * - * @this {Debugger8080} - * @param {string} sCmd - * @param {boolean} [fQuiet] - * @return {boolean} true if command processed, false if unrecognized - */ - Debugger8080.prototype.doCommand = function(sCmd, fQuiet) - { - var result = true; - - try { - if (!sCmd.length || sCmd == "end") { - if (this.fAssemble) { - this.println("ended assemble at " + this.toHexAddr(this.dbgAddrAssemble)); - this.dbgAddrNextCode = this.dbgAddrAssemble; - this.fAssemble = false; - } - sCmd = ""; - } - else if (!fQuiet) { - var sPrompt = ">> "; - this.println(sPrompt + sCmd); - } - - var ch = sCmd.charAt(0); - if (ch == '"' || ch == "'") return true; - - /* - * Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat. - */ - this.sMessagePrev = null; - - /* - * I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally. - */ - if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) { - - if (this.fAssemble) { - sCmd = "a " + this.toHexAddr(this.dbgAddrAssemble) + ' ' + sCmd; - } - - var asArgs = this.shiftArgs(sCmd.replace(/ +/g, ' ').split(' ')); - - switch (asArgs[0].charAt(0)) { - case 'a': - this.doAssemble(asArgs); - break; - case 'b': - this.doBreak(asArgs[0], asArgs[1], sCmd); - break; - case 'c': - this.doClear(asArgs[0]); - break; - case 'd': - if (!COMPILED && sCmd == "debug") { - window.DEBUG = true; - this.println("DEBUG checks on"); - break; - } - this.doDump(asArgs); - break; - case 'e': - if (asArgs[0] == "else") break; - this.doEdit(asArgs); - break; - case 'f': - this.doFreqs(asArgs[1]); - break; - case 'g': - this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet); - break; - case 'h': - this.doHalt(fQuiet); - break; - case 'i': - if (asArgs[0] == "if") { - if (!this.doIf(sCmd.substr(2), fQuiet)) { - result = false; - } - break; - } - if (asArgs[0] == "int") { - if (!this.doInt(asArgs[1])) { - result = false; - } - break; - } - this.doInput(asArgs[1]); - break; - case 'k': - this.doStackTrace(asArgs[0], asArgs[1]); - break; - case 'l': - if (asArgs[0] == "ln") { - this.doList(asArgs[1], true); - break; - } - break; - case 'm': - this.doMessages(asArgs); - break; - case 'o': - this.doOutput(asArgs[1], asArgs[2]); - break; - case 'p': - if (asArgs[0] == "print") { - this.doPrint(sCmd.substr(5)); - break; - } - this.doStep(asArgs[0]); - break; - case 'r': - if (sCmd == "reset") { - if (this.cmp) this.cmp.reset(); - break; - } - this.doRegisters(asArgs); - break; - case 's': - this.doOptions(asArgs); - break; - case 't': - this.doTrace(asArgs[0], asArgs[1]); - break; - case 'u': - this.doUnassemble(asArgs[1], asArgs[2], 8); - break; - case 'v': - if (asArgs[0] == "var") { - if (!this.doVar(sCmd.substr(3))) { - result = false; - } - break; - } - this.println((PC8080.APPNAME || "PC8080") + " version " + (XMLVERSION || PC8080.APPVERSION) + " (" + this.cpu.model + (PC8080.COMPILED? ",RELEASE" : (PC8080.DEBUG? ",DEBUG" : ",NODEBUG")) + (PC8080.TYPEDARRAYS? ",TYPEDARRAYS" : (PC8080.BYTEARRAYS? ",BYTEARRAYS" : ",LONGARRAYS")) + ')'); - this.println(web.getUserAgent()); - break; - case '?': - if (asArgs[1]) { - this.doPrint(sCmd.substr(1)); - break; - } - this.doHelp(); - break; - case 'n': - if (!COMPILED && sCmd == "nodebug") { - window.DEBUG = false; - this.println("DEBUG checks off"); - break; - } - if (this.doInfo(asArgs)) break; - /* falls through */ - default: - this.println("unknown command: " + sCmd); - result = false; - break; - } - } - } catch(e) { - this.println("debugger error: " + (e.stack || e.message)); - result = false; - } - return result; - }; - - /** - * doCommands(sCmds, fSave) - * - * @this {Debugger8080} - * @param {string} sCmds - * @param {boolean} [fSave] - * @return {boolean} true if all commands processed, false if not - */ - Debugger8080.prototype.doCommands = function(sCmds, fSave) - { - var a = this.parseCommand(sCmds, fSave); - for (var s in a) { - if (!this.doCommand(a[+s])) return false; - } - return true; - }; - - /** - * Debugger8080.init() - * - * This function operates on every HTML element of class "debugger", extracting the - * JSON-encoded parameters for the Debugger constructor from the element's "data-value" - * attribute, invoking the constructor to create a Debugger component, and then binding - * any associated HTML controls to the new component. - */ - Debugger8080.init = function() - { - var aeDbg = Component.getElementsByClass(document, PC8080.APPCLASS, "debugger"); - for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) { - var eDbg = aeDbg[iDbg]; - var parmsDbg = Component.getComponentParms(eDbg); - var dbg = new Debugger8080(parmsDbg); - Component.bindComponentControls(dbg, eDbg, PC8080.APPCLASS); - } - }; - /* * Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-)) */ - web.onInit(Debugger8080.init); + Web.onInit(Debugger8080.init); } // endif DEBUGGER -if (NODE) module.exports = Debugger8080; +module.exports = Debugger8080; diff --git a/modules/pc8080/lib/defines.js b/modules/pc8080/lib/defines.js index 70ac46fc5..6c6f603ff 100644 --- a/modules/pc8080/lib/defines.js +++ b/modules/pc8080/lib/defines.js @@ -98,6 +98,5 @@ if (NODE) { global.BYTEARRAYS = BYTEARRAYS; global.TYPEDARRAYS = TYPEDARRAYS; global.PC8080 = PC8080; - - module.exports = PC8080; + module.exports = PC8080; } diff --git a/modules/pc8080/lib/keyboard.js b/modules/pc8080/lib/keyboard.js index b8928e40d..58e8292ed 100644 --- a/modules/pc8080/lib/keyboard.js +++ b/modules/pc8080/lib/keyboard.js @@ -28,47 +28,844 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var Keys = require("../../shared/lib/keys"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var ChipSet8080 = require("./chipset"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var Keys = require("../../shared/es6/keys"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var ChipSet8080 = require("./chipset"); +var Messages8080 = require("./messages"); /** - * Keyboard8080(parmsKbd) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The Keyboard8080 component has the following component-specific (parmsKbd) properties: - * - * model: eg, "VT100" (should be a member of Keyboard8080.MODELS) - * - * @constructor - * @extends Component - * @param {Object} parmsKbd + * @unrestricted */ -function Keyboard8080(parmsKbd) -{ - Component.call(this, "Keyboard", parmsKbd, Keyboard8080, Messages8080.KEYBOARD); +class Keyboard8080 extends Component { + /** + * Keyboard8080(parmsKbd) + * + * The Keyboard8080 component has the following component-specific (parmsKbd) properties: + * + * model: eg, "VT100" (should be a member of Keyboard8080.MODELS) + * + * @this {Keyboard8080} + * @param {Object} parmsKbd + */ + constructor(parmsKbd) + { + super("Keyboard", parmsKbd, Keyboard8080, Messages8080.KEYBOARD); - var model = parmsKbd['model']; + var model = parmsKbd['model']; - if (model && !Keyboard8080.MODELS[model]) { - Component.notice("Unrecognized Keyboard8080 model: " + model); + if (model && !Keyboard8080.MODELS[model]) { + Component.notice("Unrecognized Keyboard8080 model: " + model); + } + + this.config = Keyboard8080.MODELS[model] || {}; + + this.reset(); + + this.setReady(); } - this.config = Keyboard8080.MODELS[model] || {}; + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {Keyboard8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + /* + * There's a special binding that the Video component uses ("screen") to effectively bind its + * screen to the entire keyboard, in Video.powerUp(); ie: + * + * video.kbd.setBinding("canvas", "screen", video.canvasScreen); + * or: + * video.kbd.setBinding("textarea", "screen", video.textareaScreen); + * + * However, it's also possible for the keyboard XML definition to define a control that serves + * a similar purpose; eg: + * + * Keyboard + * + * The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on + * certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will + * still be enabled (there's currently no way to configure the Video component to not bind its screen, + * but we could certainly add one if the need ever arose). + */ + var kbd = this; + var id = sHTMLType + '-' + sBinding; - this.reset(); + if (this.bindings[id] === undefined) { - this.setReady(); + if (sHTMLType == "led" && this.config.LEDCODES[sBinding]) { + this.bindings[id] = control; + return true; + } + + switch (sBinding) { + case "kbd": + case "screen": + /* + * Recording the binding ID prevents multiple controls (or components) from attempting to erroneously + * bind a control to the same ID, but in the case of a "dual display" configuration, we actually want + * to allow BOTH video components to call setBinding() for "screen", so that it doesn't matter which + * display the user gives focus to. + * + * this.bindings[id] = control; + */ + control.onkeydown = function onKeyDown(event) { + return kbd.onKeyDown(event, true); + }; + control.onkeyup = function onKeyUp(event) { + return kbd.onKeyDown(event, false); + }; + control.onkeypress = function onKeyPress(event) { + return kbd.onKeyPress(event); + }; + control.onpaste = function onKeyPaste(event) { + return kbd.onPaste(event); + }; + return true; + + default: + if (this.config.SOFTCODES && this.config.SOFTCODES[sBinding] !== undefined) { + this.bindings[id] = control; + control.onclick = function(kbd, keyCode) { + return function onKeyboardBindingDown(event) { + /* + * iOS Usability Improvement: Calling preventDefault() prevents rapid clicks from + * also being (mis)interpreted as a desire to "zoom" in on the machine. + */ + if (event.preventDefault) event.preventDefault(); + kbd.onSoftKeyDown(keyCode, true, true); + /* + * I'm assuming we only need to give focus back on the "up" event... + * + * if (kbd.cmp) kbd.cmp.updateFocus(); + */ + }; + }(this, this.config.SOFTCODES[sBinding]); + // + // var fnUp = function (kbd, keyCode) { + // return function onKeyboardBindingUp(event) { + // kbd.onSoftKeyDown(keyCode, false); + // /* + // * Give focus back to the machine (since clicking the button takes focus away). + // * + // * if (kbd.cmp) kbd.cmp.updateFocus(); + // * + // * iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down + // * (assuming it was already down). + // */ + // }; + // }(this, this.config.SOFTCODES[sBinding]); + // + // if ('ontouchstart' in window) { + // control.ontouchstart = fnDown; + // control.ontouchend = fnUp; + // } else { + // control.onmousedown = fnDown; + // control.onmouseup = control.onmouseout = fnUp; + // } + // + // UPDATE: Since the only controls that we explicitly bind to SOFTCODES are buttons, I'm simplifying + // the above code with a conventional "onclick" handler. The only corresponding change I had to make + // to the onclick (formerly fnDown) function was to set fAutoRelease on its call to onSoftKeyDown(), + // since we're no longer attempting to detect when the control (ie, the button) is actually released. + // + // This change also resolves a problem I ran into with the Epiphany (WebKit-based) web browser running + // on the "elementary" (Ubuntu-based) OS, where clicks on the SET-UP button were ignored; perhaps its + // buttons don't generate mouse and/or touch events. Anyway, an argument for keeping things simple. + // + return true; + } + break; + } + } + return false; + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {Keyboard8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.cmp = cmp; + this.cpu = cpu; + this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work + + var kbd = this; + this.timerReleaseKeys = this.cpu.addTimer(function() { + kbd.checkSoftKeysToRelease(); + }); + + this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet")); + + this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort")); + + bus.addPortInputTable(this, this.config.portsInput); + bus.addPortOutputTable(this, this.config.portsOutput); + } + + /** + * powerUp(data, fRepower) + * + * @this {Keyboard8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + if (!data) { + this.reset(); + } else { + if (!this.restore(data)) return false; + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {Keyboard8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * reset() + * + * @this {Keyboard8080} + */ + reset() + { + /* + * As keyDown events are encountered, a corresponding "softCode" is looked up. If one is found, + * then an entry for the key is added to the aKeysActive array. Each "key" entry in aKeysActive contains: + * + * softCode: number or string representing the key pressed + * msDown: timestamp of the most recent "down" event + * fAutoRelease: true to auto-release the key after MINPRESSTIME (set when "up" occurs too quickly) + * + * When the key is finally released (or auto-released), its entry is removed from the array. + */ + this.aKeysActive = []; + + /* + * The current (assumed) physical (and simulated) states of the various shift/lock keys. + * + * TODO: Determine how (or whether) we can query the browser's initial shift/lock key states. + */ + this.bitsState = 0; + + if (this.config.INIT && !this.restore(this.config.INIT)) { + this.notice("reset error"); + } + } + + /** + * save() + * + * This implements save support for the Keyboard component. + * + * @this {Keyboard8080} + * @return {Object} + */ + save() + { + var state = new State(this); + switch(this.config.MODEL) { + case Keyboard8080.SI1978.MODEL: + break; + case Keyboard8080.VT100.MODEL: + state.set(0, [this.bVT100Status, this.bVT100Address, this.fVT100UARTBusy, this.nVT100UARTCycleSnap, -1]); + break; + } + return state.data(); + } + + /** + * restore(data) + * + * This implements restore support for the Keyboard component. + * + * @this {Keyboard8080} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + var a; + if (data && (a = data[0]) && a.length) { + switch(this.config.MODEL) { + case Keyboard8080.SI1978.MODEL: + return true; + + case Keyboard8080.VT100.MODEL: + this.bVT100Status = a[0]; + this.updateLEDs(this.bVT100Status & Keyboard8080.VT100.STATUS.LEDS); + this.bVT100Address = a[1]; + this.fVT100UARTBusy = a[2]; + this.nVT100UARTCycleSnap = a[3]; + this.iKeyNext = a[4]; + return true; + } + } + return false; + } + + /** + * setLED(control, f, color) + * + * TODO: Add support for user-definable LED colors + * + * @this {Keyboard8080} + * @param {Object} control is an HTML control DOM object + * @param {boolean|number} f is true if the LED represented by control should be "on", false if "off" + * @param {number} color (ie, 0xff0000 for RED, or 0x00ff00 for GREEN) + */ + setLED(control, f, color) + { + control.style.backgroundColor = (f? ('#' + Str.toHex(color, 6)) : "#000000"); + } + + /** + * updateLEDs(bLEDs) + * + * @this {Keyboard8080} + * @param {number} [bLEDs] + */ + updateLEDs(bLEDs) + { + var id, control; + if (bLEDs != null) { + this.bLEDs = bLEDs; + } else { + bLEDs = this.bLEDs; + } + for (var sBinding in this.config.LEDCODES) { + id = "led-" + sBinding; + control = this.bindings[id]; + if (control) { + var bitLED = this.config.LEDCODES[sBinding]; + var fOn = !!(bLEDs & bitLED); + if (bitLED & (bitLED-1)) { + fOn = !(bLEDs & ~bitLED); + } + this.setLED(control, fOn, 0xff0000); + } + } + id = "led-caps-lock"; + control = this.bindings[id]; + if (control) { + this.setLED(control, (this.bitsState & Keyboard8080.STATE.CAPS_LOCK), 0x00ff00); + } + } + + /** + * checkModifierKeys(keyCode, fDown, fRight) + * + * @this {Keyboard8080} + * @param {number} keyCode (ie, either a keycode or string ID) + * @param {boolean} fDown (true if key going down, false if key going up) + * @param {boolean} fRight (true if key is on the right, false if not or unknown or n/a) + */ + checkModifierKeys(keyCode, fDown, fRight) + { + var bit = 0; + switch(keyCode) { + case Keys.KEYCODE.SHIFT: + bit = fRight? Keyboard8080.STATE.RSHIFT : Keyboard8080.STATE.SHIFT; + break; + case Keys.KEYCODE.CTRL: + bit = fRight? Keyboard8080.STATE.RCTRL : Keyboard8080.STATE.CTRL; + break; + case Keys.KEYCODE.ALT: + bit = fRight? Keyboard8080.STATE.RALT : Keyboard8080.STATE.ALT; + break; + case Keys.KEYCODE.CMD: + bit = fRight? Keyboard8080.STATE.RCMD : Keyboard8080.STATE.CMD; + break; + case Keys.KEYCODE.CAPS_LOCK: + bit = Keyboard8080.STATE.CAPS_LOCK; + break; + } + if (bit) { + if (fDown) { + this.bitsState |= bit; + } else { + this.bitsState &= ~bit; + } + } + } + + /** + * getSoftCode(keyCode) + * + * Returns a number if the keyCode exists in the KEYMAP, or a string if the keyCode has a string ID. + * + * @this {Keyboard8080} + * @return {string|number|null} + */ + getSoftCode(keyCode) + { + keyCode = this.config.ALTCODES[keyCode] || keyCode; + if (this.config.KEYMAP[keyCode]) { + return keyCode; + } + for (var sSoftCode in this.config.SOFTCODES) { + if (this.config.SOFTCODES[sSoftCode] === keyCode) { + return sSoftCode; + } + } + return null; + } + + /** + * onKeyDown(event, fDown) + * + * @this {Keyboard8080} + * @param {Object} event + * @param {boolean} fDown is true for a keyDown event, false for up + * @return {boolean} true to pass the event along, false to consume it + */ + onKeyDown(event, fDown) + { + var fPass = true; + var keyCode = event.keyCode; + + /* + * We now keep track of physical keyboard modifier keys. This makes it possible for new services + * to eventually be implemented (simulateKeysDown() and simulateKeysUp()), to map special ALT-key + * combinations to VT100 keys, etc. + */ + this.checkModifierKeys(keyCode, fDown, event.location == Keys.LOCATION.RIGHT); + + var softCode = this.getSoftCode(keyCode); + if (softCode) { + /* + * Key combinations involving the "meta" key (ie, the Windows or Command key) are meaningless to + * the VT100, so we ignore them. The "meta" key itself is already effectively ignored, because it's + * not acknowledged by getSoftCode(), but we also don't want any of the keys combined with "meta" + * slipping through either. + */ + if (!event.metaKey) { + /* + * The LINE-FEED key is an important key on the VT100, and while we DO map a host function key + * to it (F7), I like the idea of making ALT-ENTER an alias for LINE-FEED as well. Ditto for + * making ALT-DELETE an alias for BACKSPACE (and no, I don't mean ALT-BACKSPACE as an alias for + * DELETE; see my earlier discussion involving BACKSPACE and DELETE). + * + * Of course, as experienced VT100 users know, it's always possible to type CTRL-J for LINE-FEED + * and CTRL-H for BACKSPACE, too. But not all our users are that experienced. + * + * I was also tempted to use CTRL-ENTER or SHIFT-ENTER, but those are composable VT100 key + * sequences, so it's best not to muck with those. + * + * Finally, this hack is complicated by the fact that if the ALT key is released first, we run + * the risk of the remapped key being stuck "down". Hence the new REMAPPED bit, which should + * remain set (as a "proxy" for the ALT bit) as long as a remapped key is down. + */ + var fRemapped = false; + if (this.bitsState & (Keyboard8080.STATE.ALTS | Keyboard8080.STATE.REMAPPED)) { + if (softCode == Keys.KEYCODE.CR) { + softCode = Keys.KEYCODE.F7; + fRemapped = true; + } + else if (softCode == Keys.KEYCODE.BS) { + softCode = Keys.KEYCODE.DEL; + fRemapped = true; + } + if (fRemapped) { + if (fDown) { + this.bitsState |= Keyboard8080.STATE.REMAPPED; + } else { + this.bitsState &= ~Keyboard8080.STATE.REMAPPED; + } + } + } + fPass = this.onSoftKeyDown(softCode, fDown); + /* + * As onKeyPress() explains, the only key presses we're interested in are letters, which provide + * an important clue regarding the CAPS-LOCK state. For all other keys, we call preventDefault(), + * which normally "suppresses" the keyPress event, as well as other unwanted browser behaviors + * (eg, the SPACE key, which browsers interpret as a desire to scroll the entire web page down). + * + * And, even if the key IS a letter, we STILL want to call preventDefault() if a CTRL key is down, + * so that Windows-based browsers (eg, Edge) don't interfere with their stupid CTRL-based shortcuts. ;-) + * + * NOTE: We COULD check event.ctrlKey too, but it's six of one, half a dozen of another. + */ + if (!(softCode >= Keys.ASCII.A && softCode <= Keys.ASCII.Z) || (this.bitsState | Keyboard8080.STATE.CTRLS)) { + if (event.preventDefault) event.preventDefault(); + } + } + } + + if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) { + this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): softCode=" + softCode + ", pass=" + fPass, true); + } + + return fPass; + } + + /** + * onKeyPress(event) + * + * For now, our only interest in keyPress events is letters, as a means of detecting the CAPS-LOCK state. + * + * @this {Keyboard8080} + * @param {Object} event + * @return {boolean} true to pass the event along, false to consume it + */ + onKeyPress(event) + { + var keyCode = event.keyCode; + if (keyCode >= Keys.ASCII.A && keyCode <= Keys.ASCII.Z) { + if (!(this.bitsState & (Keyboard8080.STATE.SHIFTS | Keyboard8080.STATE.CAPS_LOCK))) { + this.bitsState |= Keyboard8080.STATE.CAPS_LOCK; + this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, true); + this.updateLEDs(); + } + } + else if (keyCode >= Keys.ASCII.a && keyCode <= Keys.ASCII.z) { + if (this.bitsState & Keyboard8080.STATE.CAPS_LOCK) { + this.bitsState &= ~Keyboard8080.STATE.CAPS_LOCK; + this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, false); + this.updateLEDs(); + } + } + return true; + } + + /** + * onPaste(event) + * + * @this {Keyboard8080} + * @param {Object} event + * @return {boolean} true to pass the event along, false to consume it + */ + onPaste(event) + { + /* + * TODO: In a perfect world, we would have implemented simulateKeysDown() and simulateKeysUp(), + * which would transform any given text into the appropriate keystrokes. But for now, we're going + * to leapfrog all that and try invoking the SerialPort's sendData() function, which if available, + * is nothing more than a call into a connected machine's receiveData() function. + * + * Besides, paste functionality doesn't seem to be consistently implemented across all browsers + * (partly out of security concerns, apparently) so it may not make sense to expend much more + * effort on this right now. If you want to paste a lot of text into a machine, you're better off + * pasting into a machine that's been configured to use a textarea as part of its Control Panel. + * A visible textarea seems to have less issues than the hidden textarea overlaid on top of our + * Video display. + */ + if (this.serial && this.serial.sendData) { + if (event.stopPropagation) event.stopPropagation(); + if (event.preventDefault) event.preventDefault(); + var clipboardData = event.clipboardData || window.clipboardData; + if (clipboardData) { + this.serial.transmitData(clipboardData.getData('Text')); + return false; + } + } + return true; + } + + /** + * indexOfSoftKey(softCode) + * + * @this {Keyboard8080} + * @param {number|string} softCode + * @return {number} index of softCode in aKeysActive, or -1 if not found + */ + indexOfSoftKey(softCode) + { + for (var i = 0; i < this.aKeysActive.length; i++) { + if (this.aKeysActive[i].softCode == softCode) return i; + } + return -1; + } + + /** + * onSoftKeyDown(softCode, fDown, fAutoRelease) + * + * @this {Keyboard8080} + * @param {number|string} softCode + * @param {boolean} fDown is true for a down event, false for up + * @param {boolean} [fAutoRelease] is true only if we know we want the key to auto-release + * @return {boolean} true to pass the event along, false to consume it + */ + onSoftKeyDown(softCode, fDown, fAutoRelease) + { + var i = this.indexOfSoftKey(softCode); + if (fDown) { + // this.println(softCode + " down"); + if (i < 0) { + this.aKeysActive.push({ + softCode: softCode, + msDown: Date.now(), + fAutoRelease: fAutoRelease || false + }); + } else { + this.aKeysActive[i].msDown = Date.now(); + this.aKeysActive[i].fAutoRelease = fAutoRelease || false; + } + if (fAutoRelease) this.checkSoftKeysToRelease(); // prime the pump + } else if (i >= 0) { + // this.println(softCode + " up"); + if (!this.aKeysActive[i].fAutoRelease) { + var msDown = this.aKeysActive[i].msDown; + if (msDown) { + var msElapsed = Date.now() - msDown; + if (msElapsed < Keyboard8080.MINPRESSTIME) { + // this.println(softCode + " released after only " + msElapsed + "ms"); + this.aKeysActive[i].fAutoRelease = true; + this.checkSoftKeysToRelease(); + return true; + } + } + } + this.aKeysActive.splice(i, 1); + } else { + // this.println(softCode + " up with no down?"); + } + + if (this.chipset) { + var bit = 0; + switch(softCode) { + case '1p': + bit = ChipSet8080.SI1978.STATUS1.P1; + break; + case '2p': + bit = ChipSet8080.SI1978.STATUS1.P2; + break; + case 'coin': + bit = ChipSet8080.SI1978.STATUS1.CREDIT; + break; + case 'left': + bit = ChipSet8080.SI1978.STATUS1.P1_LEFT; + break; + case 'right': + bit = ChipSet8080.SI1978.STATUS1.P1_RIGHT; + break; + case 'fire': + bit = ChipSet8080.SI1978.STATUS1.P1_FIRE; + break; + } + if (bit) { + this.chipset.updateStatus1(bit, fDown); + } + } + return true; + } + + /** + * checkSoftKeysToRelease() + * + * @this {Keyboard8080} + */ + checkSoftKeysToRelease() + { + var i = 0; + var msDelayMin = -1; + while (i < this.aKeysActive.length) { + if (this.aKeysActive[i].fAutoRelease) { + var softCode = this.aKeysActive[i].softCode; + var msDown = this.aKeysActive[i].msDown; + var msElapsed = Date.now() - msDown; + var msDelay = Keyboard8080.MINPRESSTIME - msElapsed; + if (msDelay > 0) { + if (msDelayMin < 0 || msDelayMin > msDelay) { + msDelayMin = msDelay; + } + } else { + /* + * Because the key is already in the auto-release state, this next call guarantees that the + * key will be removed from the array; a consequence of that removal, however, is that we must + * reset our array index to zero. + */ + this.onSoftKeyDown(softCode, false); + i = 0; + continue; + } + } + i++; + } + if (msDelayMin >= 0) { + /* + * Replaced the klunky browser setTimeout() call with our own timer service. + * + * var kbd = this; + * setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin); + */ + this.cpu.setTimer(this.timerReleaseKeys, msDelayMin); + } + } + + /** + * isVT100TransmitterReady() + * + * Called whenever the VT100 ChipSet circuit needs the Keyboard UART's transmitter status. + * + * From p. 4-32 of the VT100 Technical Manual (July 1982): + * + * The operating clock for the keyboard interface comes from an address line in the video processor (LBA4). + * This signal has an average period of 7.945 microseconds. Each data byte is transmitted with one start bit + * and one stop bit, and each bit lasts 16 clock periods. The total time for each data byte is 160 times 7.945 + * or 1.27 milliseconds. Each time the Transmit Buffer Empty flag on the terminal's UART gets set (when the + * current byte is being transmitted), the microprocessor loads another byte into the transmit buffer. In this + * way, the stream of status bytes to the keyboard is continuous. + * + * We used to always return true (after all, what's wrong with an infinitely fast UART?), but unfortunately, + * the VT100 firmware relies on the UART's slow transmission speed to drive cursor blink rate. We have several + * options: + * + * 1) Snapshot the CPU cycle count each time a byte is transmitted (see outVT100UARTStatus()) and then every + * time this is polled, see if the cycle count has exceeded the snapshot value by the necessary threshold; + * if we assume 361.69ns per CPU cycle, there are 22 CPU cycles for every 1 LBA4 cycle, and since transmission + * time is supposed to last for 160 LBA4 cycles, the threshold is 22*160 CPU cycles, or 3520 cycles. + * + * 2) Set a CPU timer using the new setTimer() interface, which can be passed the number of milliseconds to + * wait before firing (in this case, roughly 1.27ms). + * + * 3) Call the ChipSet's getVT100LBA(4) function for the state of the simulated LBA4, and count 160 LBA4 + * transitions; however, that would be the worst solution, because there's no guarantee that the firmware's + * UART polling will occur regularly and/or frequently enough for us to catch every LBA4 transition. + * + * I'm going with solution #1 because it's less overhead. + * + * @this {Keyboard8080} + * @return {boolean} (true if ready, false if not) + */ + isVT100TransmitterReady() + { + if (this.fVT100UARTBusy) { + /* + * NOTE: getMSCycles(1.2731488) should work out to 3520 cycles for a CPU clocked at 361.69ns per cycle, + * which is roughly 2.76Mhz. We could just hard-code 3520 instead of calling getMSCycles(), but this helps + * maintain a reasonable blink rate for the cursor even when the user cranks up the CPU speed. + */ + if (this.cpu.getCycles() >= this.nVT100UARTCycleSnap + this.cpu.getMSCycles(1.2731488)) { + this.fVT100UARTBusy = false; + } + } + return !this.fVT100UARTBusy; + } + + /** + * inVT100UARTAddress(port, addrFrom) + * + * We take our cue from iKeyNext. If it's -1 (default), we simply return the last value latched + * in bVT100Address. Otherwise, if iKeyNext is a valid index into aKeysActive, we look up the key + * in the VT100.KEYMAP, latch it, and increment iKeyNext. Failing that, we latch Keyboard8080.VT100.KEYLAST + * and reset iKeyNext to -1. + * + * @this {Keyboard8080} + * @param {number} port (0x82) + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + * @return {number} simulated port value + */ + inVT100UARTAddress(port, addrFrom) + { + var b = this.bVT100Address; + if (this.iKeyNext >= 0) { + if (this.iKeyNext < this.aKeysActive.length) { + var key = this.aKeysActive[this.iKeyNext]; + if (!MAXDEBUG) { + this.iKeyNext++; + } else { + /* + * In MAXDEBUG builds, this code removes the key as soon as it's been reported, because + * when debugging, it's easy for the window to lose focus and never receive the keyUp event, + * thereby leaving us with a stuck key. However, this may cause more problems than it solves, + * because the VT100's ROM seems to require that key presses persist for more than a single poll. + */ + this.aKeysActive.splice(this.iKeyNext, 1); + } + b = Keyboard8080.VT100.KEYMAP[key.softCode]; + if (b & 0x80) { + /* + * TODO: This code is supposed to be accompanied by a SHIFT key; make sure that it is. + */ + b &= 0x7F; + } + } else { + this.iKeyNext = -1; + b = Keyboard8080.VT100.KEYLAST; + } + this.bVT100Address = b; + this.cpu.requestINTR(1); + } + this.printMessageIO(port, null, addrFrom, "KBDUART.ADDRESS", b); + return b; + } + + /** + * outVT100UARTStatus(port, b, addrFrom) + * + * @this {Keyboard8080} + * @param {number} port (0x82) + * @param {number} b + * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) + */ + outVT100UARTStatus(port, b, addrFrom) + { + this.printMessageIO(port, b, addrFrom, "KBDUART.STATUS"); + this.bVT100Status = b; + this.fVT100UARTBusy = true; + this.nVT100UARTCycleSnap = this.cpu.getCycles(); + this.updateLEDs(b & Keyboard8080.VT100.STATUS.LEDS); + if (b & Keyboard8080.VT100.STATUS.START) { + this.iKeyNext = 0; + this.cpu.requestINTR(1); + } + } + + /** + * Keyboard8080.init() + * + * This function operates on every HTML element of class "keyboard", extracting the + * JSON-encoded parameters for the Keyboard constructor from the element's "data-value" + * attribute, invoking the constructor to create a Keyboard component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeKbd = Component.getElementsByClass(document, PC8080.APPCLASS, "keyboard"); + for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) { + var eKbd = aeKbd[iKbd]; + var parmsKbd = Component.getComponentParms(eKbd); + var kbd = new Keyboard8080(parmsKbd); + Component.bindComponentControls(kbd, eKbd, PC8080.APPCLASS); + } + } } -Component.subclass(Keyboard8080); - /* * Now that we want to keep track of the physical (and simulated) state of modifier keys, I've * grabbed a copy of the same bit definitions used by /modules/pcx86/lib/keyboard.js, since it's @@ -311,190 +1108,6 @@ Keyboard8080.MODELS = { "VT100": Keyboard8080.VT100 }; -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * @this {Keyboard8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -Keyboard8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - /* - * There's a special binding that the Video component uses ("screen") to effectively bind its - * screen to the entire keyboard, in Video.powerUp(); ie: - * - * video.kbd.setBinding("canvas", "screen", video.canvasScreen); - * or: - * video.kbd.setBinding("textarea", "screen", video.textareaScreen); - * - * However, it's also possible for the keyboard XML definition to define a control that serves - * a similar purpose; eg: - * - * Keyboard - * - * The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on - * certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will - * still be enabled (there's currently no way to configure the Video component to not bind its screen, - * but we could certainly add one if the need ever arose). - */ - var kbd = this; - var id = sHTMLType + '-' + sBinding; - - if (this.bindings[id] === undefined) { - - if (sHTMLType == "led" && this.config.LEDCODES[sBinding]) { - this.bindings[id] = control; - return true; - } - - switch (sBinding) { - case "kbd": - case "screen": - /* - * Recording the binding ID prevents multiple controls (or components) from attempting to erroneously - * bind a control to the same ID, but in the case of a "dual display" configuration, we actually want - * to allow BOTH video components to call setBinding() for "screen", so that it doesn't matter which - * display the user gives focus to. - * - * this.bindings[id] = control; - */ - control.onkeydown = function onKeyDown(event) { - return kbd.onKeyDown(event, true); - }; - control.onkeyup = function onKeyUp(event) { - return kbd.onKeyDown(event, false); - }; - control.onkeypress = function onKeyPress(event) { - return kbd.onKeyPress(event); - }; - control.onpaste = function onKeyPaste(event) { - return kbd.onPaste(event); - }; - return true; - - default: - if (this.config.SOFTCODES && this.config.SOFTCODES[sBinding] !== undefined) { - this.bindings[id] = control; - control.onclick = function(kbd, keyCode) { - return function onKeyboardBindingDown(event) { - /* - * iOS Usability Improvement: Calling preventDefault() prevents rapid clicks from - * also being (mis)interpreted as a desire to "zoom" in on the machine. - */ - if (event.preventDefault) event.preventDefault(); - kbd.onSoftKeyDown(keyCode, true, true); - /* - * I'm assuming we only need to give focus back on the "up" event... - * - * if (kbd.cmp) kbd.cmp.updateFocus(); - */ - }; - }(this, this.config.SOFTCODES[sBinding]); - // - // var fnUp = function (kbd, keyCode) { - // return function onKeyboardBindingUp(event) { - // kbd.onSoftKeyDown(keyCode, false); - // /* - // * Give focus back to the machine (since clicking the button takes focus away). - // * - // * if (kbd.cmp) kbd.cmp.updateFocus(); - // * - // * iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down - // * (assuming it was already down). - // */ - // }; - // }(this, this.config.SOFTCODES[sBinding]); - // - // if ('ontouchstart' in window) { - // control.ontouchstart = fnDown; - // control.ontouchend = fnUp; - // } else { - // control.onmousedown = fnDown; - // control.onmouseup = control.onmouseout = fnUp; - // } - // - // UPDATE: Since the only controls that we explicitly bind to SOFTCODES are buttons, I'm simplifying - // the above code with a conventional "onclick" handler. The only corresponding change I had to make - // to the onclick (formerly fnDown) function was to set fAutoRelease on its call to onSoftKeyDown(), - // since we're no longer attempting to detect when the control (ie, the button) is actually released. - // - // This change also resolves a problem I ran into with the Epiphany (WebKit-based) web browser running - // on the "elementary" (Ubuntu-based) OS, where clicks on the SET-UP button were ignored; perhaps its - // buttons don't generate mouse and/or touch events. Anyway, an argument for keeping things simple. - // - return true; - } - break; - } - } - return false; -}; - -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {Keyboard8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ -Keyboard8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.cmp = cmp; - this.cpu = cpu; - this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work - - var kbd = this; - this.timerReleaseKeys = this.cpu.addTimer(function() { - kbd.checkSoftKeysToRelease(); - }); - - this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet")); - - this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort")); - - bus.addPortInputTable(this, this.config.portsInput); - bus.addPortOutputTable(this, this.config.portsOutput); -}; - -/** - * powerUp(data, fRepower) - * - * @this {Keyboard8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -Keyboard8080.prototype.powerUp = function(data, fRepower) -{ - if (!fRepower) { - if (!data) { - this.reset(); - } else { - if (!this.restore(data)) return false; - } - } - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * @this {Keyboard8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -Keyboard8080.prototype.powerDown = function(fSave, fShutdown) -{ - return fSave? this.save() : true; -}; - Keyboard8080.VT100.INIT = [ [ Keyboard8080.VT100.STATUS.INIT, // bVT100Status @@ -505,595 +1118,6 @@ Keyboard8080.VT100.INIT = [ ] ]; -/** - * reset() - * - * @this {Keyboard8080} - */ -Keyboard8080.prototype.reset = function() -{ - /* - * As keyDown events are encountered, a corresponding "softCode" is looked up. If one is found, - * then an entry for the key is added to the aKeysActive array. Each "key" entry in aKeysActive contains: - * - * softCode: number or string representing the key pressed - * msDown: timestamp of the most recent "down" event - * fAutoRelease: true to auto-release the key after MINPRESSTIME (set when "up" occurs too quickly) - * - * When the key is finally released (or auto-released), its entry is removed from the array. - */ - this.aKeysActive = []; - - /* - * The current (assumed) physical (and simulated) states of the various shift/lock keys. - * - * TODO: Determine how (or whether) we can query the browser's initial shift/lock key states. - */ - this.bitsState = 0; - - if (this.config.INIT && !this.restore(this.config.INIT)) { - this.notice("reset error"); - } -}; - -/** - * save() - * - * This implements save support for the Keyboard component. - * - * @this {Keyboard8080} - * @return {Object} - */ -Keyboard8080.prototype.save = function() -{ - var state = new State(this); - switch(this.config.MODEL) { - case Keyboard8080.SI1978.MODEL: - break; - case Keyboard8080.VT100.MODEL: - state.set(0, [this.bVT100Status, this.bVT100Address, this.fVT100UARTBusy, this.nVT100UARTCycleSnap, -1]); - break; - } - return state.data(); -}; - -/** - * restore(data) - * - * This implements restore support for the Keyboard component. - * - * @this {Keyboard8080} - * @param {Object} data - * @return {boolean} true if successful, false if failure - */ -Keyboard8080.prototype.restore = function(data) -{ - var a; - if (data && (a = data[0]) && a.length) { - switch(this.config.MODEL) { - case Keyboard8080.SI1978.MODEL: - return true; - - case Keyboard8080.VT100.MODEL: - this.bVT100Status = a[0]; - this.updateLEDs(this.bVT100Status & Keyboard8080.VT100.STATUS.LEDS); - this.bVT100Address = a[1]; - this.fVT100UARTBusy = a[2]; - this.nVT100UARTCycleSnap = a[3]; - this.iKeyNext = a[4]; - return true; - } - } - return false; -}; - -/** - * setLED(control, f, color) - * - * TODO: Add support for user-definable LED colors - * - * @this {Keyboard8080} - * @param {Object} control is an HTML control DOM object - * @param {boolean|number} f is true if the LED represented by control should be "on", false if "off" - * @param {number} color (ie, 0xff0000 for RED, or 0x00ff00 for GREEN) - */ -Keyboard8080.prototype.setLED = function(control, f, color) -{ - control.style.backgroundColor = (f? ('#' + str.toHex(color, 6)) : "#000000"); -}; - -/** - * updateLEDs(bLEDs) - * - * @this {Keyboard8080} - * @param {number} [bLEDs] - */ -Keyboard8080.prototype.updateLEDs = function(bLEDs) -{ - var id, control; - if (bLEDs != null) { - this.bLEDs = bLEDs; - } else { - bLEDs = this.bLEDs; - } - for (var sBinding in this.config.LEDCODES) { - id = "led-" + sBinding; - control = this.bindings[id]; - if (control) { - var bitLED = this.config.LEDCODES[sBinding]; - var fOn = !!(bLEDs & bitLED); - if (bitLED & (bitLED-1)) { - fOn = !(bLEDs & ~bitLED); - } - this.setLED(control, fOn, 0xff0000); - } - } - id = "led-caps-lock"; - control = this.bindings[id]; - if (control) { - this.setLED(control, (this.bitsState & Keyboard8080.STATE.CAPS_LOCK), 0x00ff00); - } -}; - -/** - * checkModifierKeys(keyCode, fDown, fRight) - * - * @this {Keyboard8080} - * @param {number} keyCode (ie, either a keycode or string ID) - * @param {boolean} fDown (true if key going down, false if key going up) - * @param {boolean} fRight (true if key is on the right, false if not or unknown or n/a) - */ -Keyboard8080.prototype.checkModifierKeys = function(keyCode, fDown, fRight) -{ - var bit = 0; - switch(keyCode) { - case Keys.KEYCODE.SHIFT: - bit = fRight? Keyboard8080.STATE.RSHIFT : Keyboard8080.STATE.SHIFT; - break; - case Keys.KEYCODE.CTRL: - bit = fRight? Keyboard8080.STATE.RCTRL : Keyboard8080.STATE.CTRL; - break; - case Keys.KEYCODE.ALT: - bit = fRight? Keyboard8080.STATE.RALT : Keyboard8080.STATE.ALT; - break; - case Keys.KEYCODE.CMD: - bit = fRight? Keyboard8080.STATE.RCMD : Keyboard8080.STATE.CMD; - break; - case Keys.KEYCODE.CAPS_LOCK: - bit = Keyboard8080.STATE.CAPS_LOCK; - break; - } - if (bit) { - if (fDown) { - this.bitsState |= bit; - } else { - this.bitsState &= ~bit; - } - } -}; - -/** - * getSoftCode(keyCode) - * - * Returns a number if the keyCode exists in the KEYMAP, or a string if the keyCode has a string ID. - * - * @this {Keyboard8080} - * @return {string|number|null} - */ -Keyboard8080.prototype.getSoftCode = function(keyCode) -{ - keyCode = this.config.ALTCODES[keyCode] || keyCode; - if (this.config.KEYMAP[keyCode]) { - return keyCode; - } - for (var sSoftCode in this.config.SOFTCODES) { - if (this.config.SOFTCODES[sSoftCode] === keyCode) { - return sSoftCode; - } - } - return null; -}; - -/** - * onKeyDown(event, fDown) - * - * @this {Keyboard8080} - * @param {Object} event - * @param {boolean} fDown is true for a keyDown event, false for up - * @return {boolean} true to pass the event along, false to consume it - */ -Keyboard8080.prototype.onKeyDown = function(event, fDown) -{ - var fPass = true; - var keyCode = event.keyCode; - - /* - * We now keep track of physical keyboard modifier keys. This makes it possible for new services - * to eventually be implemented (simulateKeysDown() and simulateKeysUp()), to map special ALT-key - * combinations to VT100 keys, etc. - */ - this.checkModifierKeys(keyCode, fDown, event.location == Keys.LOCATION.RIGHT); - - var softCode = this.getSoftCode(keyCode); - if (softCode) { - /* - * Key combinations involving the "meta" key (ie, the Windows or Command key) are meaningless to - * the VT100, so we ignore them. The "meta" key itself is already effectively ignored, because it's - * not acknowledged by getSoftCode(), but we also don't want any of the keys combined with "meta" - * slipping through either. - */ - if (!event.metaKey) { - /* - * The LINE-FEED key is an important key on the VT100, and while we DO map a host function key - * to it (F7), I like the idea of making ALT-ENTER an alias for LINE-FEED as well. Ditto for - * making ALT-DELETE an alias for BACKSPACE (and no, I don't mean ALT-BACKSPACE as an alias for - * DELETE; see my earlier discussion involving BACKSPACE and DELETE). - * - * Of course, as experienced VT100 users know, it's always possible to type CTRL-J for LINE-FEED - * and CTRL-H for BACKSPACE, too. But not all our users are that experienced. - * - * I was also tempted to use CTRL-ENTER or SHIFT-ENTER, but those are composable VT100 key - * sequences, so it's best not to muck with those. - * - * Finally, this hack is complicated by the fact that if the ALT key is released first, we run - * the risk of the remapped key being stuck "down". Hence the new REMAPPED bit, which should - * remain set (as a "proxy" for the ALT bit) as long as a remapped key is down. - */ - var fRemapped = false; - if (this.bitsState & (Keyboard8080.STATE.ALTS | Keyboard8080.STATE.REMAPPED)) { - if (softCode == Keys.KEYCODE.CR) { - softCode = Keys.KEYCODE.F7; - fRemapped = true; - } - else if (softCode == Keys.KEYCODE.BS) { - softCode = Keys.KEYCODE.DEL; - fRemapped = true; - } - if (fRemapped) { - if (fDown) { - this.bitsState |= Keyboard8080.STATE.REMAPPED; - } else { - this.bitsState &= ~Keyboard8080.STATE.REMAPPED; - } - } - } - fPass = this.onSoftKeyDown(softCode, fDown); - /* - * As onKeyPress() explains, the only key presses we're interested in are letters, which provide - * an important clue regarding the CAPS-LOCK state. For all other keys, we call preventDefault(), - * which normally "suppresses" the keyPress event, as well as other unwanted browser behaviors - * (eg, the SPACE key, which browsers interpret as a desire to scroll the entire web page down). - * - * And, even if the key IS a letter, we STILL want to call preventDefault() if a CTRL key is down, - * so that Windows-based browsers (eg, Edge) don't interfere with their stupid CTRL-based shortcuts. ;-) - * - * NOTE: We COULD check event.ctrlKey too, but it's six of one, half a dozen of another. - */ - if (!(softCode >= Keys.ASCII.A && softCode <= Keys.ASCII.Z) || (this.bitsState | Keyboard8080.STATE.CTRLS)) { - if (event.preventDefault) event.preventDefault(); - } - } - } - - if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) { - this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): softCode=" + softCode + ", pass=" + fPass, true); - } - - return fPass; -}; - -/** - * onKeyPress(event) - * - * For now, our only interest in keyPress events is letters, as a means of detecting the CAPS-LOCK state. - * - * @this {Keyboard8080} - * @param {Object} event - * @return {boolean} true to pass the event along, false to consume it - */ -Keyboard8080.prototype.onKeyPress = function(event) -{ - var keyCode = event.keyCode; - if (keyCode >= Keys.ASCII.A && keyCode <= Keys.ASCII.Z) { - if (!(this.bitsState & (Keyboard8080.STATE.SHIFTS | Keyboard8080.STATE.CAPS_LOCK))) { - this.bitsState |= Keyboard8080.STATE.CAPS_LOCK; - this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, true); - this.updateLEDs(); - } - } - else if (keyCode >= Keys.ASCII.a && keyCode <= Keys.ASCII.z) { - if (this.bitsState & Keyboard8080.STATE.CAPS_LOCK) { - this.bitsState &= ~Keyboard8080.STATE.CAPS_LOCK; - this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, false); - this.updateLEDs(); - } - } - return true; -}; - -/** - * onPaste(event) - * - * @this {Keyboard8080} - * @param {Object} event - * @return {boolean} true to pass the event along, false to consume it - */ -Keyboard8080.prototype.onPaste = function(event) { - /* - * TODO: In a perfect world, we would have implemented simulateKeysDown() and simulateKeysUp(), - * which would transform any given text into the appropriate keystrokes. But for now, we're going - * to leapfrog all that and try invoking the SerialPort's sendData() function, which if available, - * is nothing more than a call into a connected machine's receiveData() function. - * - * Besides, paste functionality doesn't seem to be consistently implemented across all browsers - * (partly out of security concerns, apparently) so it may not make sense to expend much more - * effort on this right now. If you want to paste a lot of text into a machine, you're better off - * pasting into a machine that's been configured to use a textarea as part of its Control Panel. - * A visible textarea seems to have less issues than the hidden textarea overlaid on top of our - * Video display. - */ - if (this.serial && this.serial.sendData) { - if (event.stopPropagation) event.stopPropagation(); - if (event.preventDefault) event.preventDefault(); - var clipboardData = event.clipboardData || window.clipboardData; - if (clipboardData) { - this.serial.transmitData(clipboardData.getData('Text')); - return false; - } - } - return true; -}; - -/** - * indexOfSoftKey(softCode) - * - * @this {Keyboard8080} - * @param {number|string} softCode - * @return {number} index of softCode in aKeysActive, or -1 if not found - */ -Keyboard8080.prototype.indexOfSoftKey = function(softCode) -{ - for (var i = 0; i < this.aKeysActive.length; i++) { - if (this.aKeysActive[i].softCode == softCode) return i; - } - return -1; -}; - -/** - * onSoftKeyDown(softCode, fDown, fAutoRelease) - * - * @this {Keyboard8080} - * @param {number|string} softCode - * @param {boolean} fDown is true for a down event, false for up - * @param {boolean} [fAutoRelease] is true only if we know we want the key to auto-release - * @return {boolean} true to pass the event along, false to consume it - */ -Keyboard8080.prototype.onSoftKeyDown = function(softCode, fDown, fAutoRelease) -{ - var i = this.indexOfSoftKey(softCode); - if (fDown) { - // this.println(softCode + " down"); - if (i < 0) { - this.aKeysActive.push({ - softCode: softCode, - msDown: Date.now(), - fAutoRelease: fAutoRelease || false - }); - } else { - this.aKeysActive[i].msDown = Date.now(); - this.aKeysActive[i].fAutoRelease = fAutoRelease || false; - } - if (fAutoRelease) this.checkSoftKeysToRelease(); // prime the pump - } else if (i >= 0) { - // this.println(softCode + " up"); - if (!this.aKeysActive[i].fAutoRelease) { - var msDown = this.aKeysActive[i].msDown; - if (msDown) { - var msElapsed = Date.now() - msDown; - if (msElapsed < Keyboard8080.MINPRESSTIME) { - // this.println(softCode + " released after only " + msElapsed + "ms"); - this.aKeysActive[i].fAutoRelease = true; - this.checkSoftKeysToRelease(); - return true; - } - } - } - this.aKeysActive.splice(i, 1); - } else { - // this.println(softCode + " up with no down?"); - } - - if (this.chipset) { - var bit = 0; - switch(softCode) { - case '1p': - bit = ChipSet8080.SI1978.STATUS1.P1; - break; - case '2p': - bit = ChipSet8080.SI1978.STATUS1.P2; - break; - case 'coin': - bit = ChipSet8080.SI1978.STATUS1.CREDIT; - break; - case 'left': - bit = ChipSet8080.SI1978.STATUS1.P1_LEFT; - break; - case 'right': - bit = ChipSet8080.SI1978.STATUS1.P1_RIGHT; - break; - case 'fire': - bit = ChipSet8080.SI1978.STATUS1.P1_FIRE; - break; - } - if (bit) { - this.chipset.updateStatus1(bit, fDown); - } - } - return true; -}; - -/** - * checkSoftKeysToRelease() - * - * @this {Keyboard8080} - */ -Keyboard8080.prototype.checkSoftKeysToRelease = function() -{ - var i = 0; - var msDelayMin = -1; - while (i < this.aKeysActive.length) { - if (this.aKeysActive[i].fAutoRelease) { - var softCode = this.aKeysActive[i].softCode; - var msDown = this.aKeysActive[i].msDown; - var msElapsed = Date.now() - msDown; - var msDelay = Keyboard8080.MINPRESSTIME - msElapsed; - if (msDelay > 0) { - if (msDelayMin < 0 || msDelayMin > msDelay) { - msDelayMin = msDelay; - } - } else { - /* - * Because the key is already in the auto-release state, this next call guarantees that the - * key will be removed from the array; a consequence of that removal, however, is that we must - * reset our array index to zero. - */ - this.onSoftKeyDown(softCode, false); - i = 0; - continue; - } - } - i++; - } - if (msDelayMin >= 0) { - /* - * Replaced the klunky browser setTimeout() call with our own timer service. - * - * var kbd = this; - * setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin); - */ - this.cpu.setTimer(this.timerReleaseKeys, msDelayMin); - } -}; - -/** - * isVT100TransmitterReady() - * - * Called whenever the VT100 ChipSet circuit needs the Keyboard UART's transmitter status. - * - * From p. 4-32 of the VT100 Technical Manual (July 1982): - * - * The operating clock for the keyboard interface comes from an address line in the video processor (LBA4). - * This signal has an average period of 7.945 microseconds. Each data byte is transmitted with one start bit - * and one stop bit, and each bit lasts 16 clock periods. The total time for each data byte is 160 times 7.945 - * or 1.27 milliseconds. Each time the Transmit Buffer Empty flag on the terminal's UART gets set (when the - * current byte is being transmitted), the microprocessor loads another byte into the transmit buffer. In this - * way, the stream of status bytes to the keyboard is continuous. - * - * We used to always return true (after all, what's wrong with an infinitely fast UART?), but unfortunately, - * the VT100 firmware relies on the UART's slow transmission speed to drive cursor blink rate. We have several - * options: - * - * 1) Snapshot the CPU cycle count each time a byte is transmitted (see outVT100UARTStatus()) and then every - * time this is polled, see if the cycle count has exceeded the snapshot value by the necessary threshold; - * if we assume 361.69ns per CPU cycle, there are 22 CPU cycles for every 1 LBA4 cycle, and since transmission - * time is supposed to last for 160 LBA4 cycles, the threshold is 22*160 CPU cycles, or 3520 cycles. - * - * 2) Set a CPU timer using the new setTimer() interface, which can be passed the number of milliseconds to - * wait before firing (in this case, roughly 1.27ms). - * - * 3) Call the ChipSet's getVT100LBA(4) function for the state of the simulated LBA4, and count 160 LBA4 - * transitions; however, that would be the worst solution, because there's no guarantee that the firmware's - * UART polling will occur regularly and/or frequently enough for us to catch every LBA4 transition. - * - * I'm going with solution #1 because it's less overhead. - * - * @this {Keyboard8080} - * @return {boolean} (true if ready, false if not) - */ -Keyboard8080.prototype.isVT100TransmitterReady = function() -{ - if (this.fVT100UARTBusy) { - /* - * NOTE: getMSCycles(1.2731488) should work out to 3520 cycles for a CPU clocked at 361.69ns per cycle, - * which is roughly 2.76Mhz. We could just hard-code 3520 instead of calling getMSCycles(), but this helps - * maintain a reasonable blink rate for the cursor even when the user cranks up the CPU speed. - */ - if (this.cpu.getCycles() >= this.nVT100UARTCycleSnap + this.cpu.getMSCycles(1.2731488)) { - this.fVT100UARTBusy = false; - } - } - return !this.fVT100UARTBusy; -}; - -/** - * inVT100UARTAddress(port, addrFrom) - * - * We take our cue from iKeyNext. If it's -1 (default), we simply return the last value latched - * in bVT100Address. Otherwise, if iKeyNext is a valid index into aKeysActive, we look up the key - * in the VT100.KEYMAP, latch it, and increment iKeyNext. Failing that, we latch Keyboard8080.VT100.KEYLAST - * and reset iKeyNext to -1. - * - * @this {Keyboard8080} - * @param {number} port (0x82) - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - * @return {number} simulated port value - */ -Keyboard8080.prototype.inVT100UARTAddress = function(port, addrFrom) -{ - var b = this.bVT100Address; - if (this.iKeyNext >= 0) { - if (this.iKeyNext < this.aKeysActive.length) { - var key = this.aKeysActive[this.iKeyNext]; - if (!MAXDEBUG) { - this.iKeyNext++; - } else { - /* - * In MAXDEBUG builds, this code removes the key as soon as it's been reported, because - * when debugging, it's easy for the window to lose focus and never receive the keyUp event, - * thereby leaving us with a stuck key. However, this may cause more problems than it solves, - * because the VT100's ROM seems to require that key presses persist for more than a single poll. - */ - this.aKeysActive.splice(this.iKeyNext, 1); - } - b = Keyboard8080.VT100.KEYMAP[key.softCode]; - if (b & 0x80) { - /* - * TODO: This code is supposed to be accompanied by a SHIFT key; make sure that it is. - */ - b &= 0x7F; - } - } else { - this.iKeyNext = -1; - b = Keyboard8080.VT100.KEYLAST; - } - this.bVT100Address = b; - this.cpu.requestINTR(1); - } - this.printMessageIO(port, null, addrFrom, "KBDUART.ADDRESS", b); - return b; -}; - -/** - * outVT100UARTStatus(port, b, addrFrom) - * - * @this {Keyboard8080} - * @param {number} port (0x82) - * @param {number} b - * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) - */ -Keyboard8080.prototype.outVT100UARTStatus = function(port, b, addrFrom) -{ - this.printMessageIO(port, b, addrFrom, "KBDUART.STATUS"); - this.bVT100Status = b; - this.fVT100UARTBusy = true; - this.nVT100UARTCycleSnap = this.cpu.getCycles(); - this.updateLEDs(b & Keyboard8080.VT100.STATUS.LEDS); - if (b & Keyboard8080.VT100.STATUS.START) { - this.iKeyNext = 0; - this.cpu.requestINTR(1); - } -}; - /* * Port notification tables */ @@ -1105,28 +1129,9 @@ Keyboard8080.VT100.portsOutput = { 0x82: Keyboard8080.prototype.outVT100UARTStatus }; -/** - * Keyboard8080.init() - * - * This function operates on every HTML element of class "keyboard", extracting the - * JSON-encoded parameters for the Keyboard constructor from the element's "data-value" - * attribute, invoking the constructor to create a Keyboard component, and then binding - * any associated HTML controls to the new component. - */ -Keyboard8080.init = function() -{ - var aeKbd = Component.getElementsByClass(document, PC8080.APPCLASS, "keyboard"); - for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) { - var eKbd = aeKbd[iKbd]; - var parmsKbd = Component.getComponentParms(eKbd); - var kbd = new Keyboard8080(parmsKbd); - Component.bindComponentControls(kbd, eKbd, PC8080.APPCLASS); - } -}; - /* * Initialize every Keyboard module on the page. */ -web.onInit(Keyboard8080.init); +Web.onInit(Keyboard8080.init); -if (NODE) module.exports = Keyboard8080; +module.exports = Keyboard8080; diff --git a/modules/pc8080/lib/memory.js b/modules/pc8080/lib/memory.js index 95006c8b1..44bc2e98b 100644 --- a/modules/pc8080/lib/memory.js +++ b/modules/pc8080/lib/memory.js @@ -28,12 +28,10 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var Component = require("../../shared/lib/component"); - var CPUDef8080 = require("./cpudef"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Component = require("../../shared/es6/component"); +var CPUDef8080 = require("./cpudef"); +var Messages8080 = require("./messages"); /** * @class DataView @@ -52,112 +50,787 @@ var littleEndian = (TYPEDARRAYS? (function() { })() : false); /** - * Memory8080(addr, used, size, type) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The Bus component allocates Memory8080 objects so that each has a memory buffer with a - * block-granular starting address and an address range equal to bus.nBlockSize; however, - * the size of any given Memory8080 object's underlying buffer can be either zero or bus.nBlockSize; - * memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone. - * - * The Bus allocates empty blocks for the entire address space during initialization, so that - * any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM - * components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate - * as many new blockSize Memory8080 objects as the ranges require. Partial Memory8080 blocks could - * also be supported in theory, but in practice, they're not. - * - * Because Memory8080 blocks now allow us to have a "sparse" address space, we could choose to - * take the memory hit of allocating 4K arrays per block, where each element stores only one byte, - * instead of the more frugal but slightly slower approach of allocating arrays of 32-bit dwords - * (LONGARRAYS) and shifting/masking bytes/words to/from dwords; in theory, byte accesses would - * be faster and word accesses somewhat less faster. - * - * However, preliminary testing of that feature (BYTEARRAYS) did not yield significantly faster - * performance, so it is OFF by default to minimize our memory consumption. Using TYPEDARRAYS - * would seem best, but as discussed in defines.js, it's off by default, because it doesn't perform - * as well as LONGARRAYS; the other advantage of TYPEDARRAYS is that it should theoretically use - * about 1/2 the memory of LONGARRAYS (32-bit elements vs 64-bit numbers), but I value speed over - * size at this point. Also, not all JavaScript implementations support TYPEDARRAYS (IE9 is probably - * the only real outlier: it lacks typed arrays but otherwise has all the necessary HTML5 support). - * - * WARNING: Since Memory8080 blocks are low-level objects that have no UI requirements, they - * do not inherit from the Component class, so if you want to use any Component class methods, - * such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger - * is available). - * - * @constructor - * @param {number|null} [addr] of lowest used address in block - * @param {number} [used] portion of block in bytes (0 for none); must be a multiple of 4 - * @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4 - * @param {number} [type] is one of the Memory8080.TYPE constants (default is Memory8080.TYPE.NONE) + * @unrestricted */ -function Memory8080(addr, used, size, type) -{ - var i; - this.id = (Memory8080.idBlock += 2); - this.adw = null; - this.offset = 0; - this.addr = addr; - this.used = used; - this.size = size || 0; - this.type = type || Memory8080.TYPE.NONE; - this.fReadOnly = (type == Memory8080.TYPE.ROM); - this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize - - /* - * TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory() - * to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer. - * But the benefit to saveMemory() is minimal, and the Video component has other options; for example, it now - * uses a custom memory controller for all EGA/VGA video modes, which performs its own dirty block tracking, - * and that could easily be extended to the older MDA/CGA video modes, which still use conventional memory blocks. - * Alternatively, we could restrict the use of dirty block tracking to certain memory types (eg, VIDEO memory). +class Memory8080 { + /** + * Memory8080(addr, used, size, type) * - * However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance, - * so I think the overhead of our block-based architecture is swamping the impact of these micro-updates. + * The Bus component allocates Memory8080 objects so that each has a memory buffer with a + * block-granular starting address and an address range equal to bus.nBlockSize; however, + * the size of any given Memory8080 object's underlying buffer can be either zero or bus.nBlockSize; + * memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone. + * + * The Bus allocates empty blocks for the entire address space during initialization, so that + * any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM + * components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate + * as many new blockSize Memory8080 objects as the ranges require. Partial Memory8080 blocks could + * also be supported in theory, but in practice, they're not. + * + * Because Memory8080 blocks now allow us to have a "sparse" address space, we could choose to + * take the memory hit of allocating 4K arrays per block, where each element stores only one byte, + * instead of the more frugal but slightly slower approach of allocating arrays of 32-bit dwords + * (LONGARRAYS) and shifting/masking bytes/words to/from dwords; in theory, byte accesses would + * be faster and word accesses somewhat less faster. + * + * However, preliminary testing of that feature (BYTEARRAYS) did not yield significantly faster + * performance, so it is OFF by default to minimize our memory consumption. Using TYPEDARRAYS + * would seem best, but as discussed in defines.js, it's off by default, because it doesn't perform + * as well as LONGARRAYS; the other advantage of TYPEDARRAYS is that it should theoretically use + * about 1/2 the memory of LONGARRAYS (32-bit elements vs 64-bit numbers), but I value speed over + * size at this point. Also, not all JavaScript implementations support TYPEDARRAYS (IE9 is probably + * the only real outlier: it lacks typed arrays but otherwise has all the necessary HTML5 support). + * + * WARNING: Since Memory8080 blocks are low-level objects that have no UI requirements, they + * do not inherit from the Component class, so if you want to use any Component class methods, + * such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger + * is available). + * + * @this {Memory8080} + * @param {number|null} [addr] of lowest used address in block + * @param {number} [used] portion of block in bytes (0 for none); must be a multiple of 4 + * @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4 + * @param {number} [type] is one of the Memory8080.TYPE constants (default is Memory8080.TYPE.NONE) */ - this.fDirty = this.fDirtyEver = false; + constructor(addr, used, size, type) + { + var i; + this.id = (Memory8080.idBlock += 2); + this.adw = null; + this.offset = 0; + this.addr = addr; + this.used = used; + this.size = size || 0; + this.type = type || Memory8080.TYPE.NONE; + this.fReadOnly = (type == Memory8080.TYPE.ROM); + this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize - /* - * For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers. - */ - if (!size) { - this.setAccess(); - return; + /* + * TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory() + * to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer. + * But the benefit to saveMemory() is minimal, and the Video component has other options; for example, it now + * uses a custom memory controller for all EGA/VGA video modes, which performs its own dirty block tracking, + * and that could easily be extended to the older MDA/CGA video modes, which still use conventional memory blocks. + * Alternatively, we could restrict the use of dirty block tracking to certain memory types (eg, VIDEO memory). + * + * However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance, + * so I think the overhead of our block-based architecture is swamping the impact of these micro-updates. + */ + this.fDirty = this.fDirtyEver = false; + + /* + * For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers. + */ + if (!size) { + this.setAccess(); + return; + } + + /* + * This is the normal case: allocate a buffer that provides 8 bits of data per address; + * no controller is required because our default memory access functions (see afnMemory) + * know how to deal with this simple 1-1 mapping of addresses to bytes and words. + * + * TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG + * mode; pseudo-random might be best, to help make any bugs reproducible. + */ + if (TYPEDARRAYS) { + this.buffer = new ArrayBuffer(size); + this.dv = new DataView(this.buffer, 0, size); + /* + * If littleEndian is true, we can use ab[], aw[] and adw[] directly; well, we can use them + * whenever the offset is a multiple of 1, 2 or 4, respectively. Otherwise, we must fallback to + * dv.getUint8()/dv.setUint8(), dv.getUint16()/dv.setUint16() and dv.getInt32()/dv.setInt32(). + */ + this.ab = new Uint8Array(this.buffer, 0, size); + this.aw = new Uint16Array(this.buffer, 0, size >> 1); + this.adw = new Int32Array(this.buffer, 0, size >> 2); + this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE); + } else { + if (BYTEARRAYS) { + this.ab = new Array(size); + } else { + /* + * NOTE: This is the default mode of operation (!TYPEDARRAYS && !BYTEARRAYS), because it + * seems to provide the best performance; and although in theory, that performance might + * come at twice the overhead of TYPEDARRAYS, it's increasingly likely that the JavaScript + * runtime will notice that all we ever store are 32-bit values, and optimize accordingly. + */ + this.adw = new Array(size >> 2); + for (i = 0; i < this.adw.length; i++) this.adw[i] = 0; + } + this.setAccess(Memory8080.afnMemory); + } } - /* - * This is the normal case: allocate a buffer that provides 8 bits of data per address; - * no controller is required because our default memory access functions (see afnMemory) - * know how to deal with this simple 1-1 mapping of addresses to bytes and words. + /** + * init(addr) * - * TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG - * mode; pseudo-random might be best, to help make any bugs reproducible. + * Quick reinitializer when reusing a Memory8080 block. + * + * @this {Memory8080} + * @param {number} addr */ - if (TYPEDARRAYS) { - this.buffer = new ArrayBuffer(size); - this.dv = new DataView(this.buffer, 0, size); + init(addr) + { + this.addr = addr; + } + + /** + * clone(mem, type) + * + * Converts the current Memory8080 block (this) into a clone of the given Memory8080 block (mem), + * and optionally overrides the current block's type with the specified type. + * + * @this {Memory8080} + * @param {Memory8080} mem + * @param {number} [type] + * @param {Debugger8080} [dbg] + */ + clone(mem, type, dbg) + { /* - * If littleEndian is true, we can use ab[], aw[] and adw[] directly; well, we can use them - * whenever the offset is a multiple of 1, 2 or 4, respectively. Otherwise, we must fallback to - * dv.getUint8()/dv.setUint8(), dv.getUint16()/dv.setUint16() and dv.getInt32()/dv.setInt32(). + * Original memory block IDs are even; cloned memory block IDs are odd; + * the original ID of the current block is lost, but that's OK, since it was presumably + * produced merely to become a clone. */ - this.ab = new Uint8Array(this.buffer, 0, size); - this.aw = new Uint16Array(this.buffer, 0, size >> 1); - this.adw = new Int32Array(this.buffer, 0, size >> 2); - this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE); - } else { - if (BYTEARRAYS) { - this.ab = new Array(size); - } else { - /* - * NOTE: This is the default mode of operation (!TYPEDARRAYS && !BYTEARRAYS), because it - * seems to provide the best performance; and although in theory, that performance might - * come at twice the overhead of TYPEDARRAYS, it's increasingly likely that the JavaScript - * runtime will notice that all we ever store are 32-bit values, and optimize accordingly. - */ - this.adw = new Array(size >> 2); - for (i = 0; i < this.adw.length; i++) this.adw[i] = 0; + this.id = mem.id | 0x1; + this.used = mem.used; + this.size = mem.size; + if (type) { + this.type = type; + this.fReadOnly = (type == Memory8080.TYPE.ROM); } - this.setAccess(Memory8080.afnMemory); + if (TYPEDARRAYS) { + this.buffer = mem.buffer; + this.dv = mem.dv; + this.ab = mem.ab; + this.aw = mem.aw; + this.adw = mem.adw; + this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE); + } else { + if (BYTEARRAYS) { + this.ab = mem.ab; + } else { + this.adw = mem.adw; + } + this.setAccess(Memory8080.afnMemory); + } + this.copyBreakpoints(dbg, mem); + } + + /** + * save() + * + * This gets the contents of a Memory8080 block as an array of 32-bit values; used by Bus8080.saveMemory(), + * which in turn is called by CPUState.save(). + * + * Memory8080 blocks with custom memory controllers do NOT save their contents; that's the responsibility + * of the controller component. + * + * @this {Memory8080} + * @return {Array|Int32Array|null} + */ + save() + { + var adw, i; + if (BYTEARRAYS) { + adw = new Array(this.size >> 2); + var off = 0; + for (i = 0; i < adw.length; i++) { + adw[i] = this.ab[off] | (this.ab[off + 1] << 8) | (this.ab[off + 2] << 16) | (this.ab[off + 3] << 24); + off += 4; + } + } + else if (TYPEDARRAYS) { + /* + * It might be tempting to just return a copy of Int32Array(this.buffer, 0, this.size >> 2), + * but we can't be sure of the "endianness" of an Int32Array -- which would be OK if the array + * was always saved/restored on the same machine, but there's no guarantee of that, either. + * So we use getInt32() and require little-endian values. + * + * Moreover, an Int32Array isn't treated by JSON.stringify() and JSON.parse() exactly like + * a normal array; it's serialized as an Object rather than an Array, so it lacks a "length" + * property and causes problems for State.store() and State.parse(). + */ + adw = new Array(this.size >> 2); + for (i = 0; i < adw.length; i++) { + adw[i] = this.dv.getInt32(i << 2, true); + } + } + else { + adw = this.adw; + } + return adw; + } + + /** + * restore(adw) + * + * This restores the contents of a Memory8080 block from an array of 32-bit values; + * used by Bus8080.restoreMemory(), which is called by CPUState.restore(), after all other + * components have been restored and thus all Memory8080 blocks have been allocated + * by their respective components. + * + * @this {Memory8080} + * @param {Array|null} adw + * @return {boolean} true if successful, false if block size mismatch + */ + restore(adw) + { + /* + * At this point, it's a consistency error for adw to be null; it's happened once already, + * when there was a restore bug in the Video component that added the frame buffer at the video + * card's "spec'ed" address instead of the programmed address, so there were no controller-owned + * memory blocks installed at the programmed address, and so we arrived here at a block with + * no controller AND no data. + */ + Component.assert(adw != null); + + if (adw && this.size == adw.length << 2) { + var i; + if (BYTEARRAYS) { + var off = 0; + for (i = 0; i < adw.length; i++) { + this.ab[off] = adw[i] & 0xff; + this.ab[off + 1] = (adw[i] >> 8) & 0xff; + this.ab[off + 2] = (adw[i] >> 16) & 0xff; + this.ab[off + 3] = (adw[i] >> 24) & 0xff; + off += 4; + } + } else if (TYPEDARRAYS) { + for (i = 0; i < adw.length; i++) { + this.dv.setInt32(i << 2, adw[i], true); + } + } else { + this.adw = adw; + } + this.fDirty = true; + return true; + } + return false; + } + + /** + * setAccess(afn, fDirect) + * + * If no function table is specified, a default is selected based on the Memory8080 type. + * + * @this {Memory8080} + * @param {Array.} [afn] function table + * @param {boolean} [fDirect] (true to update direct access functions as well; default is true) + */ + setAccess(afn, fDirect) + { + if (!afn) { + Component.assert(this.type == Memory8080.TYPE.NONE); + afn = Memory8080.afnNone; + } + this.setReadAccess(afn, fDirect); + this.setWriteAccess(afn, fDirect); + } + + /** + * setReadAccess(afn, fDirect) + * + * @this {Memory8080} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setReadAccess(afn, fDirect) + { + if (!fDirect || !this.cReadBreakpoints) { + this.readByte = afn[0] || this.readNone; + this.readShort = afn[2] || this.readShortDefault; + } + if (fDirect || fDirect === undefined) { + this.readByteDirect = afn[0] || this.readNone; + this.readShortDirect = afn[2] || this.readShortDefault; + } + } + + /** + * setWriteAccess(afn, fDirect) + * + * @this {Memory8080} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setWriteAccess(afn, fDirect) + { + if (!fDirect || !this.cWriteBreakpoints) { + this.writeByte = !this.fReadOnly && afn[1] || this.writeNone; + this.writeShort = !this.fReadOnly && afn[3] || this.writeShortDefault; + } + if (fDirect || fDirect === undefined) { + this.writeByteDirect = afn[1] || this.writeNone; + this.writeShortDirect = afn[3] || this.writeShortDefault; + } + } + + /** + * resetReadAccess() + * + * @this {Memory8080} + */ + resetReadAccess() + { + this.readByte = this.readByteDirect; + this.readShort = this.readShortDirect; + } + + /** + * resetWriteAccess() + * + * @this {Memory8080} + */ + resetWriteAccess() + { + this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect; + this.writeShort = this.fReadOnly? this.writeShortDefault : this.writeShortDirect; + } + + /** + * printAddr(sMessage) + * + * @this {Memory8080} + * @param {string} sMessage + */ + printAddr(sMessage) + { + if (DEBUG && this.dbg && this.dbg.messageEnabled(Messages8080.MEM)) { + this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('%' + Str.toHex(this.addr)) : '#' + this.id), true); + } + } + + /** + * addBreakpoint(off, fWrite) + * + * @this {Memory8080} + * @param {number} off + * @param {boolean} fWrite + */ + addBreakpoint(off, fWrite) + { + if (!fWrite) { + if (this.cReadBreakpoints++ === 0) { + this.setReadAccess(Memory8080.afnChecked, false); + } + if (DEBUG) this.printAddr("read breakpoint added to memory block"); + } + else { + if (this.cWriteBreakpoints++ === 0) { + this.setWriteAccess(Memory8080.afnChecked, false); + } + if (DEBUG) this.printAddr("write breakpoint added to memory block"); + } + } + + /** + * removeBreakpoint(off, fWrite) + * + * @this {Memory8080} + * @param {number} off + * @param {boolean} fWrite + */ + removeBreakpoint(off, fWrite) + { + if (!fWrite) { + if (--this.cReadBreakpoints === 0) { + this.resetReadAccess(); + if (DEBUG) this.printAddr("all read breakpoints removed from memory block"); + } + Component.assert(this.cReadBreakpoints >= 0); + } + else { + if (--this.cWriteBreakpoints === 0) { + this.resetWriteAccess(); + if (DEBUG) this.printAddr("all write breakpoints removed from memory block"); + } + Component.assert(this.cWriteBreakpoints >= 0); + } + } + + /** + * copyBreakpoints(dbg, mem) + * + * @this {Memory8080} + * @param {Debugger8080} [dbg] + * @param {Memory8080} [mem] (outgoing Memory8080 block to copy breakpoints from, if any) + */ + copyBreakpoints(dbg, mem) + { + this.dbg = dbg; + this.cReadBreakpoints = this.cWriteBreakpoints = 0; + if (mem) { + if ((this.cReadBreakpoints = mem.cReadBreakpoints)) { + this.setReadAccess(Memory8080.afnChecked, false); + } + if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) { + this.setWriteAccess(Memory8080.afnChecked, false); + } + } + } + + /** + * readNone(off) + * + * Previously, this always returned 0x00, but the initial memory probe by the COMPAQ DeskPro 386 ROM BIOS + * writes 0x0000 to the first word of every 64Kb block in the nearly 16Mb address space it supports, and + * if it reads back 0x0000, it will initially think that LOTS of RAM exists, only to be disappointed later + * when it performs a more exhaustive memory test, generating unwanted error messages in the process. + * + * TODO: Determine if we should have separate readByteNone(), readShortNone() and readLongNone() functions + * to return 0xff, 0xffff and 0xffffffff|0, respectively. This seems sufficient for now, as it seems unlikely + * that a system would require nonexistent memory locations to return ALL bits set. + * + * Also, I'm reluctant to address that potential issue by simply returning -1, because to date, the above + * Memory8080 interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively. + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readNone(off, addr) + { + if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) { + this.dbg.message("attempt to read invalid block %" + Str.toHex(this.addr), true); + } + return 0xff; + } + + /** + * writeNone(off, v, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} v (could be either a byte or word value, since we use the same handler for both kinds of accesses) + * @param {number} addr + */ + writeNone(off, v, addr) + { + if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) { + this.dbg.message("attempt to write " + Str.toHexWord(v) + " to invalid block %" + Str.toHex(this.addr), true); + } + } + + /** + * readShortDefault(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortDefault(off, addr) + { + return this.readByte(off++, addr++) | (this.readByte(off, addr) << 8); + } + + /** + * writeShortDefault(off, w, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} w + * @param {number} addr + */ + writeShortDefault(off, w, addr) + { + this.writeByte(off++, w & 0xff, addr++); + this.writeByte(off, w >> 8, addr); + } + + /** + * readByteMemory(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteMemory(off, addr) + { + if (BYTEARRAYS) { + return this.ab[off]; + } + return ((this.adw[off >> 2] >>> ((off & 0x3) << 3)) & 0xff); + } + + /** + * readShortMemory(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortMemory(off, addr) + { + if (BYTEARRAYS) { + return this.ab[off] | (this.ab[off + 1] << 8); + } + var w; + var idw = off >> 2; + var nShift = (off & 0x3) << 3; + var dw = (this.adw[idw] >> nShift); + if (nShift < 24) { + w = dw & 0xffff; + } else { + w = (dw & 0xff) | ((this.adw[idw + 1] & 0xff) << 8); + } + return w; + } + + /** + * writeByteMemory(off, b, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} b + * @param {number} addr + */ + writeByteMemory(off, b, addr) + { + if (BYTEARRAYS) { + this.ab[off] = b; + } else { + var idw = off >> 2; + var nShift = (off & 0x3) << 3; + this.adw[idw] = (this.adw[idw] & ~(0xff << nShift)) | (b << nShift); + } + this.fDirty = true; + } + + /** + * writeShortMemory(off, w, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} w + * @param {number} addr + */ + writeShortMemory(off, w, addr) + { + if (BYTEARRAYS) { + this.ab[off] = (w & 0xff); + this.ab[off + 1] = (w >> 8); + } else { + var idw = off >> 2; + var nShift = (off & 0x3) << 3; + if (nShift < 24) { + this.adw[idw] = (this.adw[idw] & ~(0xffff << nShift)) | (w << nShift); + } else { + this.adw[idw] = (this.adw[idw] & 0x00ffffff) | (w << 24); + idw++; + this.adw[idw] = (this.adw[idw] & (0xffffff00|0)) | (w >> 8); + } + } + this.fDirty = true; + } + + /** + * readByteChecked(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteChecked(off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryRead(this.addr + off); + } + return this.readByteDirect(off, addr); + } + + /** + * readShortChecked(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortChecked(off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryRead(this.addr + off, 2); + } + return this.readShortDirect(off, addr); + } + + /** + * writeByteChecked(off, b, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @param {number} b + */ + writeByteChecked(off, b, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryWrite(this.addr + off); + } + if (this.fReadOnly) this.writeNone(off, b, addr); else this.writeByteDirect(off, b, addr); + } + + /** + * writeShortChecked(off, w, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @param {number} w + */ + writeShortChecked(off, w, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryWrite(this.addr + off, 2) + } + if (this.fReadOnly) this.writeNone(off, w, addr); else this.writeShortDirect(off, w, addr); + } + + /** + * readByteBE(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteBE(off, addr) + { + return this.ab[off]; + } + + /** + * readByteLE(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteLE(off, addr) + { + return this.ab[off]; + } + + /** + * readShortBE(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortBE(off, addr) + { + return this.dv.getUint16(off, true); + } + + /** + * readShortLE(off, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortLE(off, addr) + { + /* + * TODO: It remains to be seen if there's any advantage to checking the offset for an aligned read + * vs. always reading the bytes separately; it seems a safe bet for longs, but it's less clear for shorts. + */ + return (off & 0x1)? (this.ab[off] | (this.ab[off+1] << 8)) : this.aw[off >> 1]; + } + + /** + * writeByteBE(off, b, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} b + * @param {number} addr + */ + writeByteBE(off, b, addr) + { + this.ab[off] = b; + this.fDirty = true; + } + + /** + * writeByteLE(off, b, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @param {number} b + */ + writeByteLE(off, b, addr) + { + this.ab[off] = b; + this.fDirty = true; + } + + /** + * writeShortBE(off, w, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @param {number} w + */ + writeShortBE(off, w, addr) + { + this.dv.setUint16(off, w, true); + this.fDirty = true; + } + + /** + * writeShortLE(off, w, addr) + * + * @this {Memory8080} + * @param {number} off + * @param {number} addr + * @param {number} w + */ + writeShortLE(off, w, addr) + { + /* + * TODO: It remains to be seen if there's any advantage to checking the offset for an aligned write + * vs. always writing the bytes separately; it seems a safe bet for longs, but it's less clear for shorts. + */ + if (off & 0x1) { + this.ab[off] = w; + this.ab[off+1] = w >> 8; + } else { + this.aw[off >> 1] = w; + } + this.fDirty = true; + } + + /** + * adjustEndian(dw) + * + * @param {number} dw + * @return {number} + */ + static adjustEndian(dw) + { + if (TYPEDARRAYS && !littleEndian) { + dw = (dw << 24) | ((dw << 8) & 0x00ff0000) | ((dw >> 8) & 0x0000ff00) | (dw >>> 24); + } + return dw; } } @@ -198,609 +871,6 @@ Memory8080.TYPE = { */ Memory8080.idBlock = 0; -/** - * adjustEndian(dw) - * - * @param {number} dw - * @return {number} - */ -Memory8080.adjustEndian = function(dw) { - if (TYPEDARRAYS && !littleEndian) { - dw = (dw << 24) | ((dw << 8) & 0x00ff0000) | ((dw >> 8) & 0x0000ff00) | (dw >>> 24); - } - return dw; -}; - -Memory8080.prototype = { - constructor: Memory8080, - parent: null, - /** - * init(addr) - * - * Quick reinitializer when reusing a Memory8080 block. - * - * @this {Memory8080} - * @param {number} addr - */ - init: function(addr) { - this.addr = addr; - }, - /** - * clone(mem, type) - * - * Converts the current Memory8080 block (this) into a clone of the given Memory8080 block (mem), - * and optionally overrides the current block's type with the specified type. - * - * @this {Memory8080} - * @param {Memory8080} mem - * @param {number} [type] - * @param {Debugger8080} [dbg] - */ - clone: function(mem, type, dbg) { - /* - * Original memory block IDs are even; cloned memory block IDs are odd; - * the original ID of the current block is lost, but that's OK, since it was presumably - * produced merely to become a clone. - */ - this.id = mem.id | 0x1; - this.used = mem.used; - this.size = mem.size; - if (type) { - this.type = type; - this.fReadOnly = (type == Memory8080.TYPE.ROM); - } - if (TYPEDARRAYS) { - this.buffer = mem.buffer; - this.dv = mem.dv; - this.ab = mem.ab; - this.aw = mem.aw; - this.adw = mem.adw; - this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE); - } else { - if (BYTEARRAYS) { - this.ab = mem.ab; - } else { - this.adw = mem.adw; - } - this.setAccess(Memory8080.afnMemory); - } - this.copyBreakpoints(dbg, mem); - }, - /** - * save() - * - * This gets the contents of a Memory8080 block as an array of 32-bit values; used by Bus8080.saveMemory(), - * which in turn is called by CPUState.save(). - * - * Memory8080 blocks with custom memory controllers do NOT save their contents; that's the responsibility - * of the controller component. - * - * @this {Memory8080} - * @return {Array|Int32Array|null} - */ - save: function() { - var adw, i; - if (BYTEARRAYS) { - adw = new Array(this.size >> 2); - var off = 0; - for (i = 0; i < adw.length; i++) { - adw[i] = this.ab[off] | (this.ab[off + 1] << 8) | (this.ab[off + 2] << 16) | (this.ab[off + 3] << 24); - off += 4; - } - } - else if (TYPEDARRAYS) { - /* - * It might be tempting to just return a copy of Int32Array(this.buffer, 0, this.size >> 2), - * but we can't be sure of the "endianness" of an Int32Array -- which would be OK if the array - * was always saved/restored on the same machine, but there's no guarantee of that, either. - * So we use getInt32() and require little-endian values. - * - * Moreover, an Int32Array isn't treated by JSON.stringify() and JSON.parse() exactly like - * a normal array; it's serialized as an Object rather than an Array, so it lacks a "length" - * property and causes problems for State.store() and State.parse(). - */ - adw = new Array(this.size >> 2); - for (i = 0; i < adw.length; i++) { - adw[i] = this.dv.getInt32(i << 2, true); - } - } - else { - adw = this.adw; - } - return adw; - }, - /** - * restore(adw) - * - * This restores the contents of a Memory8080 block from an array of 32-bit values; - * used by Bus8080.restoreMemory(), which is called by CPUState.restore(), after all other - * components have been restored and thus all Memory8080 blocks have been allocated - * by their respective components. - * - * @this {Memory8080} - * @param {Array|null} adw - * @return {boolean} true if successful, false if block size mismatch - */ - restore: function(adw) { - /* - * At this point, it's a consistency error for adw to be null; it's happened once already, - * when there was a restore bug in the Video component that added the frame buffer at the video - * card's "spec'ed" address instead of the programmed address, so there were no controller-owned - * memory blocks installed at the programmed address, and so we arrived here at a block with - * no controller AND no data. - */ - Component.assert(adw != null); - - if (adw && this.size == adw.length << 2) { - var i; - if (BYTEARRAYS) { - var off = 0; - for (i = 0; i < adw.length; i++) { - this.ab[off] = adw[i] & 0xff; - this.ab[off + 1] = (adw[i] >> 8) & 0xff; - this.ab[off + 2] = (adw[i] >> 16) & 0xff; - this.ab[off + 3] = (adw[i] >> 24) & 0xff; - off += 4; - } - } else if (TYPEDARRAYS) { - for (i = 0; i < adw.length; i++) { - this.dv.setInt32(i << 2, adw[i], true); - } - } else { - this.adw = adw; - } - this.fDirty = true; - return true; - } - return false; - }, - /** - * setAccess(afn, fDirect) - * - * If no function table is specified, a default is selected based on the Memory8080 type. - * - * @this {Memory8080} - * @param {Array.} [afn] function table - * @param {boolean} [fDirect] (true to update direct access functions as well; default is true) - */ - setAccess: function(afn, fDirect) { - if (!afn) { - Component.assert(this.type == Memory8080.TYPE.NONE); - afn = Memory8080.afnNone; - } - this.setReadAccess(afn, fDirect); - this.setWriteAccess(afn, fDirect); - }, - /** - * setReadAccess(afn, fDirect) - * - * @this {Memory8080} - * @param {Array.} afn - * @param {boolean} [fDirect] - */ - setReadAccess: function(afn, fDirect) { - if (!fDirect || !this.cReadBreakpoints) { - this.readByte = afn[0] || this.readNone; - this.readShort = afn[2] || this.readShortDefault; - } - if (fDirect || fDirect === undefined) { - this.readByteDirect = afn[0] || this.readNone; - this.readShortDirect = afn[2] || this.readShortDefault; - } - }, - /** - * setWriteAccess(afn, fDirect) - * - * @this {Memory8080} - * @param {Array.} afn - * @param {boolean} [fDirect] - */ - setWriteAccess: function(afn, fDirect) { - if (!fDirect || !this.cWriteBreakpoints) { - this.writeByte = !this.fReadOnly && afn[1] || this.writeNone; - this.writeShort = !this.fReadOnly && afn[3] || this.writeShortDefault; - } - if (fDirect || fDirect === undefined) { - this.writeByteDirect = afn[1] || this.writeNone; - this.writeShortDirect = afn[3] || this.writeShortDefault; - } - }, - /** - * resetReadAccess() - * - * @this {Memory8080} - */ - resetReadAccess: function() { - this.readByte = this.readByteDirect; - this.readShort = this.readShortDirect; - }, - /** - * resetWriteAccess() - * - * @this {Memory8080} - */ - resetWriteAccess: function() { - this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect; - this.writeShort = this.fReadOnly? this.writeShortDefault : this.writeShortDirect; - }, - /** - * printAddr(sMessage) - * - * @this {Memory8080} - * @param {string} sMessage - */ - printAddr: function(sMessage) { - if (DEBUG && this.dbg && this.dbg.messageEnabled(Messages8080.MEM)) { - this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('%' + str.toHex(this.addr)) : '#' + this.id), true); - } - }, - /** - * addBreakpoint(off, fWrite) - * - * @this {Memory8080} - * @param {number} off - * @param {boolean} fWrite - */ - addBreakpoint: function(off, fWrite) { - if (!fWrite) { - if (this.cReadBreakpoints++ === 0) { - this.setReadAccess(Memory8080.afnChecked, false); - } - if (DEBUG) this.printAddr("read breakpoint added to memory block"); - } - else { - if (this.cWriteBreakpoints++ === 0) { - this.setWriteAccess(Memory8080.afnChecked, false); - } - if (DEBUG) this.printAddr("write breakpoint added to memory block"); - } - }, - /** - * removeBreakpoint(off, fWrite) - * - * @this {Memory8080} - * @param {number} off - * @param {boolean} fWrite - */ - removeBreakpoint: function(off, fWrite) { - if (!fWrite) { - if (--this.cReadBreakpoints === 0) { - this.resetReadAccess(); - if (DEBUG) this.printAddr("all read breakpoints removed from memory block"); - } - Component.assert(this.cReadBreakpoints >= 0); - } - else { - if (--this.cWriteBreakpoints === 0) { - this.resetWriteAccess(); - if (DEBUG) this.printAddr("all write breakpoints removed from memory block"); - } - Component.assert(this.cWriteBreakpoints >= 0); - } - }, - /** - * copyBreakpoints(dbg, mem) - * - * @this {Memory8080} - * @param {Debugger8080} [dbg] - * @param {Memory8080} [mem] (outgoing Memory8080 block to copy breakpoints from, if any) - */ - copyBreakpoints: function(dbg, mem) { - this.dbg = dbg; - this.cReadBreakpoints = this.cWriteBreakpoints = 0; - if (mem) { - if ((this.cReadBreakpoints = mem.cReadBreakpoints)) { - this.setReadAccess(Memory8080.afnChecked, false); - } - if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) { - this.setWriteAccess(Memory8080.afnChecked, false); - } - } - }, - /** - * readNone(off) - * - * Previously, this always returned 0x00, but the initial memory probe by the COMPAQ DeskPro 386 ROM BIOS - * writes 0x0000 to the first word of every 64Kb block in the nearly 16Mb address space it supports, and - * if it reads back 0x0000, it will initially think that LOTS of RAM exists, only to be disappointed later - * when it performs a more exhaustive memory test, generating unwanted error messages in the process. - * - * TODO: Determine if we should have separate readByteNone(), readShortNone() and readLongNone() functions - * to return 0xff, 0xffff and 0xffffffff|0, respectively. This seems sufficient for now, as it seems unlikely - * that a system would require nonexistent memory locations to return ALL bits set. - * - * Also, I'm reluctant to address that potential issue by simply returning -1, because to date, the above - * Memory8080 interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively. - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readNone: function readNone(off, addr) { - if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) { - this.dbg.message("attempt to read invalid block %" + str.toHex(this.addr), true); - } - return 0xff; - }, - /** - * writeNone(off, v, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} v (could be either a byte or word value, since we use the same handler for both kinds of accesses) - * @param {number} addr - */ - writeNone: function writeNone(off, v, addr) { - if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) { - this.dbg.message("attempt to write " + str.toHexWord(v) + " to invalid block %" + str.toHex(this.addr), true); - } - }, - /** - * readShortDefault(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readShortDefault: function readShortDefault(off, addr) { - return this.readByte(off++, addr++) | (this.readByte(off, addr) << 8); - }, - /** - * writeShortDefault(off, w, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} w - * @param {number} addr - */ - writeShortDefault: function writeShortDefault(off, w, addr) { - this.writeByte(off++, w & 0xff, addr++); - this.writeByte(off, w >> 8, addr); - }, - /** - * readByteMemory(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readByteMemory: function readByteMemory(off, addr) { - if (BYTEARRAYS) { - return this.ab[off]; - } - return ((this.adw[off >> 2] >>> ((off & 0x3) << 3)) & 0xff); - }, - /** - * readShortMemory(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readShortMemory: function readShortMemory(off, addr) { - if (BYTEARRAYS) { - return this.ab[off] | (this.ab[off + 1] << 8); - } - var w; - var idw = off >> 2; - var nShift = (off & 0x3) << 3; - var dw = (this.adw[idw] >> nShift); - if (nShift < 24) { - w = dw & 0xffff; - } else { - w = (dw & 0xff) | ((this.adw[idw + 1] & 0xff) << 8); - } - return w; - }, - /** - * writeByteMemory(off, b, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} b - * @param {number} addr - */ - writeByteMemory: function writeByteMemory(off, b, addr) { - if (BYTEARRAYS) { - this.ab[off] = b; - } else { - var idw = off >> 2; - var nShift = (off & 0x3) << 3; - this.adw[idw] = (this.adw[idw] & ~(0xff << nShift)) | (b << nShift); - } - this.fDirty = true; - }, - /** - * writeShortMemory(off, w, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} w - * @param {number} addr - */ - writeShortMemory: function writeShortMemory(off, w, addr) { - if (BYTEARRAYS) { - this.ab[off] = (w & 0xff); - this.ab[off + 1] = (w >> 8); - } else { - var idw = off >> 2; - var nShift = (off & 0x3) << 3; - if (nShift < 24) { - this.adw[idw] = (this.adw[idw] & ~(0xffff << nShift)) | (w << nShift); - } else { - this.adw[idw] = (this.adw[idw] & 0x00ffffff) | (w << 24); - idw++; - this.adw[idw] = (this.adw[idw] & (0xffffff00|0)) | (w >> 8); - } - } - this.fDirty = true; - }, - /** - * readByteChecked(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readByteChecked: function readByteChecked(off, addr) { - if (DEBUGGER && this.dbg && this.addr != null) { - this.dbg.checkMemoryRead(this.addr + off); - } - return this.readByteDirect(off, addr); - }, - /** - * readShortChecked(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readShortChecked: function readShortChecked(off, addr) { - if (DEBUGGER && this.dbg && this.addr != null) { - this.dbg.checkMemoryRead(this.addr + off, 2); - } - return this.readShortDirect(off, addr); - }, - /** - * writeByteChecked(off, b, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @param {number} b - */ - writeByteChecked: function writeByteChecked(off, b, addr) { - if (DEBUGGER && this.dbg && this.addr != null) { - this.dbg.checkMemoryWrite(this.addr + off); - } - if (this.fReadOnly) this.writeNone(off, b, addr); else this.writeByteDirect(off, b, addr); - }, - /** - * writeShortChecked(off, w, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @param {number} w - */ - writeShortChecked: function writeShortChecked(off, w, addr) { - if (DEBUGGER && this.dbg && this.addr != null) { - this.dbg.checkMemoryWrite(this.addr + off, 2) - } - if (this.fReadOnly) this.writeNone(off, w, addr); else this.writeShortDirect(off, w, addr); - }, - /** - * readByteBE(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readByteBE: function readByteBE(off, addr) { - return this.ab[off]; - }, - /** - * readByteLE(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readByteLE: function readByteLE(off, addr) { - return this.ab[off]; - }, - /** - * readShortBE(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readShortBE: function readShortBE(off, addr) { - return this.dv.getUint16(off, true); - }, - /** - * readShortLE(off, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @return {number} - */ - readShortLE: function readShortLE(off, addr) { - /* - * TODO: It remains to be seen if there's any advantage to checking the offset for an aligned read - * vs. always reading the bytes separately; it seems a safe bet for longs, but it's less clear for shorts. - */ - return (off & 0x1)? (this.ab[off] | (this.ab[off+1] << 8)) : this.aw[off >> 1]; - }, - /** - * writeByteBE(off, b, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} b - * @param {number} addr - */ - writeByteBE: function writeByteBE(off, b, addr) { - this.ab[off] = b; - this.fDirty = true; - }, - /** - * writeByteLE(off, b, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @param {number} b - */ - writeByteLE: function writeByteLE(off, b, addr) { - this.ab[off] = b; - this.fDirty = true; - }, - /** - * writeShortBE(off, w, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @param {number} w - */ - writeShortBE: function writeShortBE(off, w, addr) { - this.dv.setUint16(off, w, true); - this.fDirty = true; - }, - /** - * writeShortLE(off, w, addr) - * - * @this {Memory8080} - * @param {number} off - * @param {number} addr - * @param {number} w - */ - writeShortLE: function writeShortLE(off, w, addr) { - /* - * TODO: It remains to be seen if there's any advantage to checking the offset for an aligned write - * vs. always writing the bytes separately; it seems a safe bet for longs, but it's less clear for shorts. - */ - if (off & 0x1) { - this.ab[off] = w; - this.ab[off+1] = w >> 8; - } else { - this.aw[off >> 1] = w; - } - this.fDirty = true; - } -}; /* * This is the effective definition of afnNone, but we need not fully define it, because setAccess() @@ -845,4 +915,4 @@ if (TYPEDARRAYS) { ]; } -if (NODE) module.exports = Memory8080; +module.exports = Memory8080; diff --git a/modules/pc8080/lib/messages.js b/modules/pc8080/lib/messages.js index de792335a..0123e99c6 100644 --- a/modules/pc8080/lib/messages.js +++ b/modules/pc8080/lib/messages.js @@ -91,4 +91,4 @@ Messages8080.CATEGORIES = { "halt": Messages8080.HALT }; -if (NODE) module.exports = Messages8080; +module.exports = Messages8080; diff --git a/modules/pc8080/lib/panel.js b/modules/pc8080/lib/panel.js index cdf83b241..8dae5ebd5 100644 --- a/modules/pc8080/lib/panel.js +++ b/modules/pc8080/lib/panel.js @@ -28,155 +28,157 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var usr = require("../../shared/lib/usrlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var PC8080 = require("./defines"); - var Bus8080 = require("./bus"); - var CPUDef8080 = require("./cpudef"); - var Memory8080 = require("./memory"); -} +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var PC8080 = require("./defines"); +var Bus8080 = require("./bus"); +var CPUDef8080 = require("./cpudef"); +var Memory8080 = require("./memory"); /** - * Panel8080(parmsPanel) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The Panel8080 component has no required (parmsPanel) properties. - * - * @constructor - * @extends Component - * @param {Object} parmsPanel + * @unrestricted */ -function Panel8080(parmsPanel) -{ - Component.call(this, "Panel", parmsPanel, Panel8080); -} - -Component.subclass(Panel8080); - -/** - * setBinding(sHTMLType, sBinding, control, sValue) - * - * Most panel layouts don't have bindings of their own, so we pass along all binding requests to the - * Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any component - * that doesn't recognize the specified binding should simply ignore it. - * - * @this {Panel8080} - * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") - * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") - * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) - * @param {string} [sValue] optional data value - * @return {boolean} true if binding was successful, false if unrecognized binding request - */ -Panel8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) -{ - if (this.cmp && this.cmp.setBinding(sHTMLType, sBinding, control, sValue)) return true; - if (this.cpu && this.cpu.setBinding(sHTMLType, sBinding, control, sValue)) return true; - if (this.kbd && this.kbd.setBinding(sHTMLType, sBinding, control, sValue)) return true; - if (DEBUGGER && this.dbg && this.dbg.setBinding(sHTMLType, sBinding, control, sValue)) return true; - return this.parent.setBinding.call(this, sHTMLType, sBinding, control, sValue); -}; - -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {Panel8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ -Panel8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.cmp = cmp; - this.bus = bus; - this.cpu = cpu; - this.dbg = dbg; - this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard")); -}; - -/** - * powerUp(data, fRepower) - * - * @this {Panel8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -Panel8080.prototype.powerUp = function(data, fRepower) -{ - if (!fRepower) Panel8080.init(); - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * @this {Panel8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -Panel8080.prototype.powerDown = function(fSave, fShutdown) -{ - return true; -}; - -/** - * updateStatus(fForce) - * - * Update function for Panels containing elements with high-frequency display requirements. - * - * For older (and slower) DOM-based display elements, those are sill being managed by the CPUState component, - * so it has its own updateStatus() handler. - * - * The Computer's updateStatus() handler is currently responsible for calling both our handler and the CPU's handler. - * - * @this {Panel8080} - * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) - */ -Panel8080.prototype.updateStatus = function(fForce) -{ -}; - -/** - * Panel8080.init() - * - * This function operates on every HTML element of class "panel", extracting the - * JSON-encoded parameters for the Panel8080 constructor from the element's "data-value" - * attribute, invoking the constructor to create a Panel8080 component, and then binding - * any associated HTML controls to the new component. - * - * NOTE: Unlike most other component init() functions, this one is designed to be - * called multiple times: once at load time, so that we can bind our print() - * function to the panel's output control ASAP, and again when the Computer component - * is verifying that all components are ready and invoking their powerUp() functions. - * - * Our powerUp() method gives us a second opportunity to notify any components that - * that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they - * might want to use. - */ -Panel8080.init = function() -{ - var fReady = false; - var aePanels = Component.getElementsByClass(document, PC8080.APPCLASS, "panel"); - for (var iPanel=0; iPanel < aePanels.length; iPanel++) { - var ePanel = aePanels[iPanel]; - var parmsPanel = Component.getComponentParms(ePanel); - var panel = Component.getComponentByID(parmsPanel['id']); - if (!panel) { - fReady = true; - panel = new Panel8080(parmsPanel); - } - Component.bindComponentControls(panel, ePanel, PC8080.APPCLASS); - if (fReady) panel.setReady(); +class Panel8080 extends Component { + /** + * Panel8080(parmsPanel) + * + * The Panel8080 component has no required (parmsPanel) properties. + * + * @this {Panel8080} + * @param {Object} parmsPanel + */ + constructor(parmsPanel) + { + super("Panel", parmsPanel, Panel8080); } -}; + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * Most panel layouts don't have bindings of their own, so we pass along all binding requests to the + * Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any component + * that doesn't recognize the specified binding should simply ignore it. + * + * @this {Panel8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) + { + if (this.cmp && this.cmp.setBinding(sHTMLType, sBinding, control, sValue)) return true; + if (this.cpu && this.cpu.setBinding(sHTMLType, sBinding, control, sValue)) return true; + if (this.kbd && this.kbd.setBinding(sHTMLType, sBinding, control, sValue)) return true; + if (DEBUGGER && this.dbg && this.dbg.setBinding(sHTMLType, sBinding, control, sValue)) return true; + return super.setBinding(sHTMLType, sBinding, control, sValue); + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {Panel8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.cmp = cmp; + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard")); + } + + /** + * powerUp(data, fRepower) + * + * @this {Panel8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) Panel8080.init(); + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {Panel8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return true; + } + + /** + * updateStatus(fForce) + * + * Update function for Panels containing elements with high-frequency display requirements. + * + * For older (and slower) DOM-based display elements, those are sill being managed by the CPUState component, + * so it has its own updateStatus() handler. + * + * The Computer's updateStatus() handler is currently responsible for calling both our handler and the CPU's handler. + * + * @this {Panel8080} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) + */ + updateStatus(fForce) + { + } + + /** + * Panel8080.init() + * + * This function operates on every HTML element of class "panel", extracting the + * JSON-encoded parameters for the Panel8080 constructor from the element's "data-value" + * attribute, invoking the constructor to create a Panel8080 component, and then binding + * any associated HTML controls to the new component. + * + * NOTE: Unlike most other component init() functions, this one is designed to be + * called multiple times: once at load time, so that we can bind our print() + * function to the panel's output control ASAP, and again when the Computer component + * is verifying that all components are ready and invoking their powerUp() functions. + * + * Our powerUp() method gives us a second opportunity to notify any components that + * that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they + * might want to use. + */ + static init() + { + var fReady = false; + var aePanels = Component.getElementsByClass(document, PC8080.APPCLASS, "panel"); + for (var iPanel=0; iPanel < aePanels.length; iPanel++) { + var ePanel = aePanels[iPanel]; + var parmsPanel = Component.getComponentParms(ePanel); + var panel = Component.getComponentByID(parmsPanel['id']); + if (!panel) { + fReady = true; + panel = new Panel8080(parmsPanel); + } + Component.bindComponentControls(panel, ePanel, PC8080.APPCLASS); + if (fReady) panel.setReady(); + } + } +} /* * Initialize every Panel module on the page. */ -web.onInit(Panel8080.init); +Web.onInit(Panel8080.init); -if (NODE) module.exports = Panel8080; +module.exports = Panel8080; diff --git a/modules/pc8080/lib/ram.js b/modules/pc8080/lib/ram.js index f13aae1b4..dc7742cb9 100644 --- a/modules/pc8080/lib/ram.js +++ b/modules/pc8080/lib/ram.js @@ -28,74 +28,344 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var web = require("../../shared/lib/weblib"); - var DumpAPI = require("../../shared/lib/dumpapi"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var CPUDef8080 = require("./cpudef"); - var Memory8080 = require("./memory"); -} +var Str = require("../../shared/es6/strlib"); +var Web = require("../../shared/es6/weblib"); +var DumpAPI = require("../../shared/es6/dumpapi"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var CPUDef8080 = require("./cpudef"); +var Memory8080 = require("./memory"); /** - * RAM8080(parmsRAM) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The RAM8080 component expects the following (parmsRAM) properties: - * - * addr: starting physical address of RAM (default is 0) - * size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings) - * file: name of optional data file to load into RAM (default is "") - * load: optional file load address (overrides any load address specified in the data file; default is null) - * exec: optional file exec address (overrides any exec address specified in the data file; default is null) - * - * NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the - * Computer component calls powerUp(). - * - * @constructor - * @extends Component - * @param {Object} parmsRAM + * @unrestricted */ -function RAM8080(parmsRAM) -{ - Component.call(this, "RAM", parmsRAM, RAM8080); +class RAM8080 extends Component { + /** + * RAM8080(parmsRAM) + * + * The RAM8080 component expects the following (parmsRAM) properties: + * + * addr: starting physical address of RAM (default is 0) + * size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings) + * file: name of optional data file to load into RAM (default is "") + * load: optional file load address (overrides any load address specified in the data file; default is null) + * exec: optional file exec address (overrides any exec address specified in the data file; default is null) + * + * NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the + * Computer component calls powerUp(). + * + * @this {RAM8080} + * @param {Object} parmsRAM + */ + constructor(parmsRAM) + { + super("RAM", parmsRAM, RAM8080); - this.abInit = null; - this.aSymbols = null; + this.abInit = null; + this.aSymbols = null; - this.addrRAM = parmsRAM['addr']; - this.sizeRAM = parmsRAM['size']; - this.addrLoad = parmsRAM['load']; - this.addrExec = parmsRAM['exec']; + this.addrRAM = parmsRAM['addr']; + this.sizeRAM = parmsRAM['size']; + this.addrLoad = parmsRAM['load']; + this.addrExec = parmsRAM['exec']; - this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified - this.fAllocated = false; + this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified + this.fAllocated = false; - this.sFilePath = parmsRAM['file']; - this.sFileName = str.getBaseName(this.sFilePath); + this.sFilePath = parmsRAM['file']; + this.sFileName = Str.getBaseName(this.sFilePath); - if (this.sFilePath) { - var sFileURL = this.sFilePath; - if (DEBUG) this.log('load("' + sFileURL + '")'); - /* - * If the selected data file has a ".json" extension, then we assume it's pre-converted - * JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension. - * Otherwise, we ask our server-side converter to return the file in a JSON-compatible format. - */ - var sFileExt = str.getExtension(this.sFileName); - if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { - sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + if (this.sFilePath) { + var sFileURL = this.sFilePath; + if (DEBUG) this.log('load("' + sFileURL + '")'); + /* + * If the selected data file has a ".json" extension, then we assume it's pre-converted + * JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension. + * Otherwise, we ask our server-side converter to return the file in a JSON-compatible format. + */ + var sFileExt = Str.getExtension(this.sFileName); + if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { + sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + } + var ram = this; + Web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { + ram.doneLoad(sURL, sResponse, nErrorCode); + }); + } + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {RAM8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.initRAM(); + } + + /** + * powerUp(data, fRepower) + * + * @this {RAM8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + /* + * The Computer powers up the CPU last, at which point CPUState state is restored, + * which includes the Bus state, and since we use the Bus to allocate all our memory, + * memory contents are already restored for us, so we don't need the usual restore + * logic. + */ + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {RAM8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + /* + * The Computer powers down the CPU first, at which point CPUState state is saved, + * which includes the Bus state, and since we use the Bus component to allocate all + * our memory, memory contents are already saved for us, so we don't need the usual + * save logic. + */ + return true; + } + + /** + * doneLoad(sURL, sData, nErrorCode) + * + * @this {RAM8080} + * @param {string} sURL + * @param {string} sData + * @param {number} nErrorCode (response from server if anything other than 200) + */ + doneLoad(sURL, sData, nErrorCode) + { + if (nErrorCode) { + this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")"); + return; + } + + Component.addMachineResource(this.idMachine, sURL, sData); + + var resource = Web.parseMemoryResource(sURL, sData); + if (resource) { + this.abInit = resource.aBytes; + this.aSymbols = resource.aSymbols; + if (this.addrLoad == null) this.addrLoad = resource.addrLoad; + if (this.addrExec == null) this.addrExec = resource.addrExec; + } else { + this.sFilePath = null; + } + this.initRAM(); + } + + /** + * initRAM() + * + * This function is called by both initBus() and doneLoad(), but it cannot copy the initial data into place + * until after initBus() has received the Bus component AND doneLoad() has received the data. When both those + * criteria are satisfied, the component becomes "ready". + * + * @this {RAM8080} + */ + initRAM() + { + if (!this.fAllocated && this.sizeRAM) { + if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory8080.TYPE.RAM)) { + this.fAllocated = true; + } + } + if (!this.isReady()) { + if (!this.fAllocated) { + Component.error("No RAM allocated"); + } + else if (this.sFilePath) { + /* + * Too early... + */ + if (!this.abInit || !this.bus) return; + + var addr = this.addrRAM; + if (this.addrLoad !== null) addr = this.addrLoad; + for (var i = 0; i < this.abInit.length; i++) { + this.bus.setByteDirect(addr + i, this.abInit[i]); + } + + if (this.addrExec !== null) { + /* + * Here's where we enable our "Fake CP/M" support, triggered by the user loading a "writable" ROM image + * at offset 0x100. Fake CP/M support works by installing HLT opcodes at well-known CP/M addresses + * (namely, 0x0000, which is the CP/M reset vector, and 0x0005, which is the CP/M system call vector) and + * then telling the CPU to call us whenever a HLT occurs, so we can check PC for one of these addresses. + */ + if (this.addrExec == RAM8080.CPM.INIT) { + for (i = 0; i < RAM8080.CPM.VECTORS.length; i++) { + this.bus.setByteDirect(RAM8080.CPM.VECTORS[i], CPUDef8080.OPCODE.HLT); + } + + this.cpu.addHaltCheck(function(rom) { + return function(addr) { + return rom.checkCPMVector(addr) + }; + }(this)); + } + this.cpu.setReset(this.addrExec); + } + + /* + * TODO: Consider an option to retain this data and give the user a way of restoring the initial contents. + */ + delete this.abInit; + } + this.setReady(); + } + } + + /** + * reset() + * + * @this {RAM8080} + */ + reset() + { + /* + * If you want to zero RAM on reset, then this would be a good place to do it. + */ + } + + /** + * checkCPMVector(addr) + * + * @this {RAM8080} + * @param {number} addr (of the HLT opcode) + * @return {boolean} true if special processing performed, false if not + */ + checkCPMVector(addr) + { + var i = RAM8080.CPM.VECTORS.indexOf(addr); + if (i >= 0) { + var fCPM = false; + var cpu = this.cpu; + var dbg = this.dbg; + if (addr == RAM8080.CPM.BDOS.VECTOR) { + fCPM = true; + switch(cpu.regC) { + case RAM8080.CPM.BDOS.FUNC.CON_WRITE: + this.writeCPMString(this.getCPMChar(cpu.regE)); + break; + case RAM8080.CPM.BDOS.FUNC.STR_WRITE: + this.writeCPMString(this.getCPMString(cpu.getDE(), '$')); + break; + default: + fCPM = false; + break; + } + } + if (fCPM) { + CPUDef8080.opRET.call(cpu); // for recognized calls, automatically return + } + else if (dbg) { + this.println("\nCP/M vector " + Str.toHexWord(addr)); + cpu.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT + dbg.stopCPU(); + } + return true; + } + return false; + } + + /** + * getCPMChar(ch) + * + * @this {RAM8080} + * @param {number} ch + * @return {string} + */ + getCPMChar(ch) + { + return String.fromCharCode(ch); + } + + /** + * getCPMString(addr, chEnd) + * + * @this {RAM8080} + * @param {number} addr (of a string) + * @param {string|number} [chEnd] (terminating character, default is 0) + * @return {string} + */ + getCPMString(addr, chEnd) + { + var s = ""; + var cchMax = 255; + var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0; + while (cchMax--) { + var b = this.cpu.getByte(addr++); + if (b == bEnd) break; + s += String.fromCharCode(b); + } + return s; + } + + /** + * writeCPMString(s) + * + * @this {RAM8080} + * @param {string} s + */ + writeCPMString(s) + { + s = s.replace(/\r/g, ''); + if (this.controlPrint) { + this.controlPrint.value += s; + this.controlPrint.scrollTop = this.controlPrint.scrollHeight; + } + } + + /** + * RAM8080.init() + * + * This function operates on every HTML element of class "ram", extracting the + * JSON-encoded parameters for the RAM8080 constructor from the element's "data-value" + * attribute, invoking the constructor to create a RAM8080 component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeRAM = Component.getElementsByClass(document, PC8080.APPCLASS, "ram"); + for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) { + var eRAM = aeRAM[iRAM]; + var parmsRAM = Component.getComponentParms(eRAM); + var ram = new RAM8080(parmsRAM); + Component.bindComponentControls(ram, eRAM, PC8080.APPCLASS); } - var ram = this; - web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { - ram.doneLoad(sURL, sResponse, nErrorCode); - }); } } -Component.subclass(RAM8080); - RAM8080.CPM = { BIOS: { VECTOR: 0x0000 @@ -121,276 +391,9 @@ RAM8080.CPM = { RAM8080.CPM.VECTORS = [RAM8080.CPM.BIOS.VECTOR, RAM8080.CPM.BDOS.VECTOR]; -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {RAM8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ -RAM8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.bus = bus; - this.cpu = cpu; - this.dbg = dbg; - this.initRAM(); -}; - -/** - * powerUp(data, fRepower) - * - * @this {RAM8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -RAM8080.prototype.powerUp = function(data, fRepower) -{ - /* - * The Computer powers up the CPU last, at which point CPUState state is restored, - * which includes the Bus state, and since we use the Bus to allocate all our memory, - * memory contents are already restored for us, so we don't need the usual restore - * logic. - */ - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * @this {RAM8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -RAM8080.prototype.powerDown = function(fSave, fShutdown) -{ - /* - * The Computer powers down the CPU first, at which point CPUState state is saved, - * which includes the Bus state, and since we use the Bus component to allocate all - * our memory, memory contents are already saved for us, so we don't need the usual - * save logic. - */ - return true; -}; - -/** - * doneLoad(sURL, sData, nErrorCode) - * - * @this {RAM8080} - * @param {string} sURL - * @param {string} sData - * @param {number} nErrorCode (response from server if anything other than 200) - */ -RAM8080.prototype.doneLoad = function(sURL, sData, nErrorCode) -{ - if (nErrorCode) { - this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")"); - return; - } - - Component.addMachineResource(this.idMachine, sURL, sData); - - var resource = web.parseMemoryResource(sURL, sData); - if (resource) { - this.abInit = resource.aBytes; - this.aSymbols = resource.aSymbols; - if (this.addrLoad == null) this.addrLoad = resource.addrLoad; - if (this.addrExec == null) this.addrExec = resource.addrExec; - } else { - this.sFilePath = null; - } - this.initRAM(); -}; - -/** - * initRAM() - * - * This function is called by both initBus() and doneLoad(), but it cannot copy the initial data into place - * until after initBus() has received the Bus component AND doneLoad() has received the data. When both those - * criteria are satisfied, the component becomes "ready". - * - * @this {RAM8080} - */ -RAM8080.prototype.initRAM = function() -{ - if (!this.fAllocated && this.sizeRAM) { - if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory8080.TYPE.RAM)) { - this.fAllocated = true; - } - } - if (!this.isReady()) { - if (!this.fAllocated) { - Component.error("No RAM allocated"); - } - else if (this.sFilePath) { - /* - * Too early... - */ - if (!this.abInit || !this.bus) return; - - var addr = this.addrRAM; - if (this.addrLoad !== null) addr = this.addrLoad; - for (var i = 0; i < this.abInit.length; i++) { - this.bus.setByteDirect(addr + i, this.abInit[i]); - } - - if (this.addrExec !== null) { - /* - * Here's where we enable our "Fake CP/M" support, triggered by the user loading a "writable" ROM image - * at offset 0x100. Fake CP/M support works by installing HLT opcodes at well-known CP/M addresses - * (namely, 0x0000, which is the CP/M reset vector, and 0x0005, which is the CP/M system call vector) and - * then telling the CPU to call us whenever a HLT occurs, so we can check PC for one of these addresses. - */ - if (this.addrExec == RAM8080.CPM.INIT) { - for (i = 0; i < RAM8080.CPM.VECTORS.length; i++) { - this.bus.setByteDirect(RAM8080.CPM.VECTORS[i], CPUDef8080.OPCODE.HLT); - } - - this.cpu.addHaltCheck(function(rom) { - return function(addr) { - return rom.checkCPMVector(addr) - }; - }(this)); - } - this.cpu.setReset(this.addrExec); - } - - /* - * TODO: Consider an option to retain this data and give the user a way of restoring the initial contents. - */ - delete this.abInit; - } - this.setReady(); - } -}; - -/** - * reset() - * - * @this {RAM8080} - */ -RAM8080.prototype.reset = function() -{ - /* - * If you want to zero RAM on reset, then this would be a good place to do it. - */ -}; - -/** - * checkCPMVector(addr) - * - * @this {RAM8080} - * @param {number} addr (of the HLT opcode) - * @return {boolean} true if special processing performed, false if not - */ -RAM8080.prototype.checkCPMVector = function(addr) -{ - var i = RAM8080.CPM.VECTORS.indexOf(addr); - if (i >= 0) { - var fCPM = false; - var cpu = this.cpu; - var dbg = this.dbg; - if (addr == RAM8080.CPM.BDOS.VECTOR) { - fCPM = true; - switch(cpu.regC) { - case RAM8080.CPM.BDOS.FUNC.CON_WRITE: - this.writeCPMString(this.getCPMChar(cpu.regE)); - break; - case RAM8080.CPM.BDOS.FUNC.STR_WRITE: - this.writeCPMString(this.getCPMString(cpu.getDE(), '$')); - break; - default: - fCPM = false; - break; - } - } - if (fCPM) { - CPUDef8080.opRET.call(cpu); // for recognized calls, automatically return - } - else if (dbg) { - this.println("\nCP/M vector " + str.toHexWord(addr)); - cpu.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT - dbg.stopCPU(); - } - return true; - } - return false; -}; - - -/** - * getCPMChar(ch) - * - * @this {RAM8080} - * @param {number} ch - * @return {string} - */ -RAM8080.prototype.getCPMChar = function(ch) -{ - return String.fromCharCode(ch); -}; - -/** - * getCPMString(addr, chEnd) - * - * @this {RAM8080} - * @param {number} addr (of a string) - * @param {string|number} [chEnd] (terminating character, default is 0) - * @return {string} - */ -RAM8080.prototype.getCPMString = function(addr, chEnd) -{ - var s = ""; - var cchMax = 255; - var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0; - while (cchMax--) { - var b = this.cpu.getByte(addr++); - if (b == bEnd) break; - s += String.fromCharCode(b); - } - return s; -}; - -/** - * writeCPMString(s) - * - * @this {RAM8080} - * @param {string} s - */ -RAM8080.prototype.writeCPMString = function(s) -{ - s = s.replace(/\r/g, ''); - if (this.controlPrint) { - this.controlPrint.value += s; - this.controlPrint.scrollTop = this.controlPrint.scrollHeight; - } -}; - -/** - * RAM8080.init() - * - * This function operates on every HTML element of class "ram", extracting the - * JSON-encoded parameters for the RAM8080 constructor from the element's "data-value" - * attribute, invoking the constructor to create a RAM8080 component, and then binding - * any associated HTML controls to the new component. - */ -RAM8080.init = function() -{ - var aeRAM = Component.getElementsByClass(document, PC8080.APPCLASS, "ram"); - for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) { - var eRAM = aeRAM[iRAM]; - var parmsRAM = Component.getComponentParms(eRAM); - var ram = new RAM8080(parmsRAM); - Component.bindComponentControls(ram, eRAM, PC8080.APPCLASS); - } -}; - /* * Initialize all the RAM8080 modules on the page. */ -web.onInit(RAM8080.init); +Web.onInit(RAM8080.init); -if (NODE) module.exports = RAM8080; +module.exports = RAM8080; diff --git a/modules/pc8080/lib/rom.js b/modules/pc8080/lib/rom.js index c4e04132a..cd90e0800 100644 --- a/modules/pc8080/lib/rom.js +++ b/modules/pc8080/lib/rom.js @@ -28,80 +28,337 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var web = require("../../shared/lib/weblib"); - var DumpAPI = require("../../shared/lib/dumpapi"); - var Component = require("../../shared/lib/component"); - var PC8080 = require("./defines"); - var Memory8080 = require("./memory"); -} +var Str = require("../../shared/es6/strlib"); +var Web = require("../../shared/es6/weblib"); +var DumpAPI = require("../../shared/es6/dumpapi"); +var Component = require("../../shared/es6/component"); +var PC8080 = require("./defines"); +var Memory8080 = require("./memory"); /** - * ROM8080(parmsROM) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The ROM8080 component expects the following (parmsROM) properties: - * - * addr: physical address of ROM - * size: amount of ROM, in bytes - * alias: physical alias address (null if none) - * file: name of ROM data file - * - * NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND the - * ROM data file has finished loading (see doneLoad()). - * - * Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM you received - * is the ROM you expected. - * - * @constructor - * @extends Component - * @param {Object} parmsROM + * @unrestricted */ -function ROM8080(parmsROM) -{ - Component.call(this, "ROM", parmsROM, ROM8080); - - this.abROM = null; - this.addrROM = parmsROM['addr']; - this.sizeROM = parmsROM['size']; - - /* - * The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of - * physical addresses; eg: +class ROM8080 extends Component { + /** + * ROM8080(parmsROM) * - * [0xf0000,0xffff0000,0xffff8000] + * The ROM8080 component expects the following (parmsROM) properties: * - * We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any - * aliased locations listed under a separate property. + * addr: physical address of ROM + * size: amount of ROM, in bytes + * alias: physical alias address (null if none) + * file: name of ROM data file * - * Most ROMs are not aliased, in which case the 'alias' property should have the default value of null. + * NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND the + * ROM data file has finished loading (see doneLoad()). + * + * Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM you received + * is the ROM you expected. + * + * @this {ROM8080} + * @param {Object} parmsROM */ - this.addrAlias = parmsROM['alias']; + constructor(parmsROM) + { + super("ROM", parmsROM, ROM8080); - this.sFilePath = parmsROM['file']; - this.sFileName = str.getBaseName(this.sFilePath); + this.abROM = null; + this.addrROM = parmsROM['addr']; + this.sizeROM = parmsROM['size']; - if (this.sFilePath) { - var sFileURL = this.sFilePath; - if (DEBUG) this.log('load("' + sFileURL + '")'); /* - * If the selected ROM file has a ".json" extension, then we assume it's pre-converted - * JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension. - * Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format. + * The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of + * physical addresses; eg: + * + * [0xf0000,0xffff0000,0xffff8000] + * + * We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any + * aliased locations listed under a separate property. + * + * Most ROMs are not aliased, in which case the 'alias' property should have the default value of null. */ - var sFileExt = str.getExtension(this.sFileName); - if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { - sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + this.addrAlias = parmsROM['alias']; + + this.sFilePath = parmsROM['file']; + this.sFileName = Str.getBaseName(this.sFilePath); + + if (this.sFilePath) { + var sFileURL = this.sFilePath; + if (DEBUG) this.log('load("' + sFileURL + '")'); + /* + * If the selected ROM file has a ".json" extension, then we assume it's pre-converted + * JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension. + * Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format. + */ + var sFileExt = Str.getExtension(this.sFileName); + if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { + sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + } + var rom = this; + Web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { + rom.doneLoad(sURL, sResponse, nErrorCode); + }); + } + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {ROM8080} + * @param {Computer8080} cmp + * @param {Bus8080} bus + * @param {CPUState8080} cpu + * @param {Debugger8080} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.copyROM(); + } + + /** + * powerUp(data, fRepower) + * + * @this {ROM8080} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (this.aSymbols) { + if (this.dbg) { + this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols); + } + /* + * Our only role in the handling of symbols is to hand them off to the Debugger at our + * first opportunity. Now that we've done that, our copy of the symbols, if any, are toast. + */ + delete this.aSymbols; + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * Since we have nothing to do on powerDown(), and no state to return, we could simply omit + * this function. But it doesn't hurt anything, and maybe we'll use our state to save something + * useful down the road, like user-defined symbols (ie, symbols that the Debugger may have + * created, above and beyond those symbols we automatically loaded, if any, along with the ROM). + * + * @this {ROM8080} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return true; + } + + /** + * doneLoad(sURL, sROMData, nErrorCode) + * + * @this {ROM8080} + * @param {string} sURL + * @param {string} sROMData + * @param {number} nErrorCode (response from server if anything other than 200) + */ + doneLoad(sURL, sROMData, nErrorCode) + { + if (nErrorCode) { + this.notice("Unable to load system ROM (error " + nErrorCode + ": " + sURL + ")"); + return; + } + + Component.addMachineResource(this.idMachine, sURL, sROMData); + + if (sROMData.charAt(0) == "[" || sROMData.charAt(0) == "{") { + try { + /* + * The most likely source of any exception will be here: parsing the JSON-encoded ROM data. + */ + var rom = eval("(" + sROMData + ")"); + var ab = rom['bytes']; + var adw = rom['data']; + + if (ab) { + this.abROM = ab; + } + else if (adw) { + /* + * Convert all the DWORDs into BYTEs, so that subsequent code only has to deal with abROM. + */ + this.abROM = new Array(adw.length * 4); + for (var idw = 0, ib = 0; idw < adw.length; idw++) { + this.abROM[ib++] = adw[idw] & 0xff; + this.abROM[ib++] = (adw[idw] >> 8) & 0xff; + this.abROM[ib++] = (adw[idw] >> 16) & 0xff; + this.abROM[ib++] = (adw[idw] >> 24) & 0xff; + } + } + else { + this.abROM = rom; + } + + this.aSymbols = rom['symbols']; + + if (!this.abROM.length) { + Component.error("Empty ROM: " + sURL); + return; + } + else if (this.abROM.length == 1) { + Component.error(this.abROM[0]); + return; + } + } catch (e) { + this.notice("ROM data error: " + e.message); + return; + } + } + else { + /* + * Parse the ROM data manually; we assume it's in "simplified" hex form (a series of hex byte-values + * separated by whitespace). + */ + var sHexData = sROMData.replace(/\n/gm, " ").replace(/ +$/, ""); + var asHexData = sHexData.split(" "); + this.abROM = new Array(asHexData.length); + for (var i = 0; i < asHexData.length; i++) { + this.abROM[i] = Str.parseInt(asHexData[i], 16); + } + } + this.copyROM(); + } + + /** + * copyROM() + * + * This function is called by both initBus() and doneLoad(), but it cannot copy the the ROM data into place + * until after initBus() has received the Bus component AND doneLoad() has received the abROM data. When both + * those criteria are satisfied, the component becomes "ready". + * + * @this {ROM8080} + */ + copyROM() + { + if (!this.isReady()) { + if (!this.sFilePath) { + this.setReady(); + } + else if (this.abROM && this.bus) { + /* + * If no explicit size was specified, then use whatever the actual size is. + */ + if (!this.sizeROM) { + this.sizeROM = this.abROM.length; + } + if (this.abROM.length != this.sizeROM) { + /* + * Note that setError() sets the component's fError flag, which in turn prevents setReady() from + * marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a + * good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be + * times when we'd like to forge ahead anyway. + */ + this.setError("ROM size (" + Str.toHexLong(this.abROM.length) + ") does not match specified size (" + Str.toHexLong(this.sizeROM) + ")"); + } + else if (this.addROM(this.addrROM)) { + + var aliases = []; + if (typeof this.addrAlias == "number") { + aliases.push(this.addrAlias); + } else if (this.addrAlias != null && this.addrAlias.length) { + aliases = this.addrAlias; + } + for (var i = 0; i < aliases.length; i++) { + this.cloneROM(aliases[i]); + } + /* + * We used to hang onto the original ROM data so that we could restore any bytes the CPU overwrote, + * using memory write-notification handlers, but with the introduction of read-only memory blocks, that's + * no longer necessary. + * + * TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs. + * That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless + * whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger, + * and Debugger users should know better. + */ + delete this.abROM; + } + this.setReady(); + } + } + } + + /** + * addROM(addr) + * + * @this {ROM8080} + * @param {number} addr + * @return {boolean} + */ + addROM(addr) + { + if (this.bus.addMemory(addr, this.sizeROM, Memory8080.TYPE.ROM)) { + if (DEBUG) this.log("addROM(): copying ROM to " + Str.toHexLong(addr) + " (" + Str.toHexLong(this.abROM.length) + " bytes)"); + var i; + for (i = 0; i < this.abROM.length; i++) { + this.bus.setByteDirect(addr + i, this.abROM[i]); + } + return true; + } + /* + * We don't need to report an error here, because addMemory() already takes care of that. + */ + return false; + } + + /** + * cloneROM(addr) + * + * For ROMs with one or more alias addresses, we used to call addROM() for each address. However, + * that obviously wasted memory, since each alias was an independent copy, and if you used the + * Debugger to edit the ROM in one location, the changes would not appear in the other location(s). + * + * Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods + * to manually get and set the blocks of any memory range, it is now possible to create true aliases. + * + * @this {ROM8080} + * @param {number} addr + */ + cloneROM(addr) + { + var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM); + this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks); + } + + /** + * ROM8080.init() + * + * This function operates on every HTML element of class "rom", extracting the + * JSON-encoded parameters for the ROM8080 constructor from the element's "data-value" + * attribute, invoking the constructor to create a ROM8080 component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeROM = Component.getElementsByClass(document, PC8080.APPCLASS, "rom"); + for (var iROM = 0; iROM < aeROM.length; iROM++) { + var eROM = aeROM[iROM]; + var parmsROM = Component.getComponentParms(eROM); + var rom = new ROM8080(parmsROM); + Component.bindComponentControls(rom, eROM, PC8080.APPCLASS); } - var rom = this; - web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { - rom.doneLoad(sURL, sResponse, nErrorCode); - }); } } -Component.subclass(ROM8080); - /* * NOTE: There's currently no need for this component to have a reset() function, since * once the ROM data is loaded, it can't be changed, so there's nothing to reinitialize. @@ -115,262 +372,9 @@ Component.subclass(ROM8080); * via bus.addMemory(). */ -/** - * initBus(cmp, bus, cpu, dbg) - * - * @this {ROM8080} - * @param {Computer8080} cmp - * @param {Bus8080} bus - * @param {CPUState8080} cpu - * @param {Debugger8080} dbg - */ -ROM8080.prototype.initBus = function(cmp, bus, cpu, dbg) -{ - this.bus = bus; - this.cpu = cpu; - this.dbg = dbg; - this.copyROM(); -}; - -/** - * powerUp(data, fRepower) - * - * @this {ROM8080} - * @param {Object|null} data - * @param {boolean} [fRepower] - * @return {boolean} true if successful, false if failure - */ -ROM8080.prototype.powerUp = function(data, fRepower) -{ - if (this.aSymbols) { - if (this.dbg) { - this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols); - } - /* - * Our only role in the handling of symbols is to hand them off to the Debugger at our - * first opportunity. Now that we've done that, our copy of the symbols, if any, are toast. - */ - delete this.aSymbols; - } - return true; -}; - -/** - * powerDown(fSave, fShutdown) - * - * Since we have nothing to do on powerDown(), and no state to return, we could simply omit - * this function. But it doesn't hurt anything, and maybe we'll use our state to save something - * useful down the road, like user-defined symbols (ie, symbols that the Debugger may have - * created, above and beyond those symbols we automatically loaded, if any, along with the ROM). - * - * @this {ROM8080} - * @param {boolean} [fSave] - * @param {boolean} [fShutdown] - * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure - */ -ROM8080.prototype.powerDown = function(fSave, fShutdown) -{ - return true; -}; - -/** - * doneLoad(sURL, sROMData, nErrorCode) - * - * @this {ROM8080} - * @param {string} sURL - * @param {string} sROMData - * @param {number} nErrorCode (response from server if anything other than 200) - */ -ROM8080.prototype.doneLoad = function(sURL, sROMData, nErrorCode) -{ - if (nErrorCode) { - this.notice("Unable to load system ROM (error " + nErrorCode + ": " + sURL + ")"); - return; - } - - Component.addMachineResource(this.idMachine, sURL, sROMData); - - if (sROMData.charAt(0) == "[" || sROMData.charAt(0) == "{") { - try { - /* - * The most likely source of any exception will be here: parsing the JSON-encoded ROM data. - */ - var rom = eval("(" + sROMData + ")"); - var ab = rom['bytes']; - var adw = rom['data']; - - if (ab) { - this.abROM = ab; - } - else if (adw) { - /* - * Convert all the DWORDs into BYTEs, so that subsequent code only has to deal with abROM. - */ - this.abROM = new Array(adw.length * 4); - for (var idw = 0, ib = 0; idw < adw.length; idw++) { - this.abROM[ib++] = adw[idw] & 0xff; - this.abROM[ib++] = (adw[idw] >> 8) & 0xff; - this.abROM[ib++] = (adw[idw] >> 16) & 0xff; - this.abROM[ib++] = (adw[idw] >> 24) & 0xff; - } - } - else { - this.abROM = rom; - } - - this.aSymbols = rom['symbols']; - - if (!this.abROM.length) { - Component.error("Empty ROM: " + sURL); - return; - } - else if (this.abROM.length == 1) { - Component.error(this.abROM[0]); - return; - } - } catch (e) { - this.notice("ROM data error: " + e.message); - return; - } - } - else { - /* - * Parse the ROM data manually; we assume it's in "simplified" hex form (a series of hex byte-values - * separated by whitespace). - */ - var sHexData = sROMData.replace(/\n/gm, " ").replace(/ +$/, ""); - var asHexData = sHexData.split(" "); - this.abROM = new Array(asHexData.length); - for (var i = 0; i < asHexData.length; i++) { - this.abROM[i] = str.parseInt(asHexData[i], 16); - } - } - this.copyROM(); -}; - -/** - * copyROM() - * - * This function is called by both initBus() and doneLoad(), but it cannot copy the the ROM data into place - * until after initBus() has received the Bus component AND doneLoad() has received the abROM data. When both - * those criteria are satisfied, the component becomes "ready". - * - * @this {ROM8080} - */ -ROM8080.prototype.copyROM = function() -{ - if (!this.isReady()) { - if (!this.sFilePath) { - this.setReady(); - } - else if (this.abROM && this.bus) { - /* - * If no explicit size was specified, then use whatever the actual size is. - */ - if (!this.sizeROM) { - this.sizeROM = this.abROM.length; - } - if (this.abROM.length != this.sizeROM) { - /* - * Note that setError() sets the component's fError flag, which in turn prevents setReady() from - * marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a - * good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be - * times when we'd like to forge ahead anyway. - */ - this.setError("ROM size (" + str.toHexLong(this.abROM.length) + ") does not match specified size (" + str.toHexLong(this.sizeROM) + ")"); - } - else if (this.addROM(this.addrROM)) { - - var aliases = []; - if (typeof this.addrAlias == "number") { - aliases.push(this.addrAlias); - } else if (this.addrAlias != null && this.addrAlias.length) { - aliases = this.addrAlias; - } - for (var i = 0; i < aliases.length; i++) { - this.cloneROM(aliases[i]); - } - /* - * We used to hang onto the original ROM data so that we could restore any bytes the CPU overwrote, - * using memory write-notification handlers, but with the introduction of read-only memory blocks, that's - * no longer necessary. - * - * TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs. - * That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless - * whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger, - * and Debugger users should know better. - */ - delete this.abROM; - } - this.setReady(); - } - } -}; - -/** - * addROM(addr) - * - * @this {ROM8080} - * @param {number} addr - * @return {boolean} - */ -ROM8080.prototype.addROM = function(addr) -{ - if (this.bus.addMemory(addr, this.sizeROM, Memory8080.TYPE.ROM)) { - if (DEBUG) this.log("addROM(): copying ROM to " + str.toHexLong(addr) + " (" + str.toHexLong(this.abROM.length) + " bytes)"); - var i; - for (i = 0; i < this.abROM.length; i++) { - this.bus.setByteDirect(addr + i, this.abROM[i]); - } - return true; - } - /* - * We don't need to report an error here, because addMemory() already takes care of that. - */ - return false; -}; - -/** - * cloneROM(addr) - * - * For ROMs with one or more alias addresses, we used to call addROM() for each address. However, - * that obviously wasted memory, since each alias was an independent copy, and if you used the - * Debugger to edit the ROM in one location, the changes would not appear in the other location(s). - * - * Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods - * to manually get and set the blocks of any memory range, it is now possible to create true aliases. - * - * @this {ROM8080} - * @param {number} addr - */ -ROM8080.prototype.cloneROM = function(addr) -{ - var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM); - this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks); -}; - -/** - * ROM8080.init() - * - * This function operates on every HTML element of class "rom", extracting the - * JSON-encoded parameters for the ROM8080 constructor from the element's "data-value" - * attribute, invoking the constructor to create a ROM8080 component, and then binding - * any associated HTML controls to the new component. - */ -ROM8080.init = function() -{ - var aeROM = Component.getElementsByClass(document, PC8080.APPCLASS, "rom"); - for (var iROM = 0; iROM < aeROM.length; iROM++) { - var eROM = aeROM[iROM]; - var parmsROM = Component.getComponentParms(eROM); - var rom = new ROM8080(parmsROM); - Component.bindComponentControls(rom, eROM, PC8080.APPCLASS); - } -}; - /* * Initialize all the ROM8080 modules on the page. */ -web.onInit(ROM8080.init); +Web.onInit(ROM8080.init); -if (NODE) module.exports = ROM8080; +module.exports = ROM8080; diff --git a/modules/pc8080/lib/serialport.js b/modules/pc8080/lib/serialport.js index 99566af17..a488fe076 100644 --- a/modules/pc8080/lib/serialport.js +++ b/modules/pc8080/lib/serialport.js @@ -28,117 +28,807 @@ "use strict"; -if (NODE) { - var str = require("../../shared/lib/strlib"); - var web = require("../../shared/lib/weblib"); - var Component = require("../../shared/lib/component"); - var State = require("../../shared/lib/state"); - var PC8080 = require("./defines"); - var Messages8080= require("./messages"); -} +var Str = require("../../shared/es6/strlib"); +var Web = require("../../shared/es6/weblib"); +var Component = require("../../shared/es6/component"); +var State = require("../../shared/es6/state"); +var PC8080 = require("./defines"); +var Messages8080 = require("./messages"); /** - * SerialPort8080(parmsSerial) + * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, + * which would force us to declare all class properties in the constructor, as well as prevent + * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * - * The SerialPort8080 component has the following component-specific (parmsSerial) properties: - * - * adapter: 0 if not defined - * - * binding: name of a control (based on its "binding" attribute) to bind to this port's I/O - * - * tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to - * the above binding); default is 0 (no conversion) - * - * In the future, we may support 'port' and 'irq' properties that allow the machine to define a - * non-standard serial port configuration, instead of only our pre-defined 'adapter' configurations. - * - * NOTE: Since the XSL file defines 'adapter' as a number, not a string, there's no need to use - * parseInt(), and as an added benefit, we don't need to worry about whether a hex or decimal format - * was used. - * - * @constructor - * @extends Component - * @param {Object} parmsSerial + * @unrestricted */ -function SerialPort8080(parmsSerial) { - - this.iAdapter = +parmsSerial['adapter']; - - switch (this.iAdapter) { - case 0: - this.portBase = 0; - this.nIRQ = 2; - break; - default: - Component.warning("Unrecognized serial adapter #" + this.iAdapter); - return; - } +class SerialPort8080 extends Component { /** - * consoleOutput becomes a string that records serial port output if the 'binding' property is set to the - * reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output - * or the string length reaches a threshold (currently, 1024 characters). + * SerialPort8080(parmsSerial) * - * @type {string|null} - */ - this.consoleOutput = null; - - /** - * controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()). + * The SerialPort8080 component has the following component-specific (parmsSerial) properties: * - * @type {Object} - */ - this.controlIOBuffer = null; - - /* - * If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters - * being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate - * number of spaces. + * adapter: 0 if not defined * - * charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably - * isn't generally useful; I use it internally to preformat serial output. - */ - this.tabSize = parmsSerial['tabSize']; - this.charBOL = parmsSerial['charBOL']; - this.iLogicalCol = 0; - - /* - * fAutoXOFF enables some experimental auto-XOFF/XON processing. It assumes if the VT100 firmware - * issues an XOFF, receiveByte() should stop accepting more data until the firmware issues an XOFF. + * binding: name of a control (based on its "binding" attribute) to bind to this port's I/O * - * The downside is that this doesn't really do anything to stem the flow of incoming data; it just - * prevents the VT100's internal buffer from overflowing. TODO: Eliminate the need for this hack - * and add some *real* flow-control interfaces between connected SerialPort components. + * tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to + * the above binding); default is 0 (no conversion) + * + * In the future, we may support 'port' and 'irq' properties that allow the machine to define a + * non-standard serial port configuration, instead of only our pre-defined 'adapter' configurations. + * + * NOTE: Since the XSL file defines 'adapter' as a number, not a string, there's no need to use + * parseInt(), and as an added benefit, we don't need to worry about whether a hex or decimal format + * was used. + * + * @this {SerialPort8080} + * @param {Object} parmsSerial */ - this.fAutoXOFF = true; - this.fAutoStop = false; - this.fNullModem = true; + constructor(parmsSerial) + { + super("SerialPort", parmsSerial, SerialPort8080, Messages8080.SERIAL); - Component.call(this, "SerialPort", parmsSerial, SerialPort8080, Messages8080.SERIAL); + this.iAdapter = +parmsSerial['adapter']; - var sBinding = parmsSerial['binding']; - if (sBinding == "console") { - this.consoleOutput = ""; - } else { - /* - * NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing. + switch (this.iAdapter) { + case 0: + this.portBase = 0; + this.nIRQ = 2; + break; + default: + Component.warning("Unrecognized serial adapter #" + this.iAdapter); + return; + } + /** + * consoleOutput becomes a string that records serial port output if the 'binding' property is set to the + * reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output + * or the string length reaches a threshold (currently, 1024 characters). + * + * @type {string|null} */ - Component.bindExternalControl(this, sBinding, SerialPort8080.sIOBuffer); + this.consoleOutput = null; + + /** + * controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()). + * + * @type {Object} + */ + this.controlIOBuffer = null; + + /* + * If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters + * being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate + * number of spaces. + * + * charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably + * isn't generally useful; I use it internally to preformat serial output. + */ + this.tabSize = parmsSerial['tabSize']; + this.charBOL = parmsSerial['charBOL']; + this.iLogicalCol = 0; + + /* + * fAutoXOFF enables some experimental auto-XOFF/XON processing. It assumes if the VT100 firmware + * issues an XOFF, receiveByte() should stop accepting more data until the firmware issues an XOFF. + * + * The downside is that this doesn't really do anything to stem the flow of incoming data; it just + * prevents the VT100's internal buffer from overflowing. TODO: Eliminate the need for this hack + * and add some *real* flow-control interfaces between connected SerialPort components. + */ + this.fAutoXOFF = true; + this.fAutoStop = false; + this.fNullModem = true; + + var sBinding = parmsSerial['binding']; + if (sBinding == "console") { + this.consoleOutput = ""; + } else { + /* + * NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing. + */ + Component.bindExternalControl(this, sBinding, SerialPort8080.sIOBuffer); + } + + /* + * No connection until initConnection() is called. + */ + this.sDataReceived = ""; + this.connection = this.sendData = this.updateStatus = null; + + /* + * Export all functions required by initConnection(). + */ + this['exports'] = { + 'connect': this.initConnection, + 'receiveData': this.receiveData, + 'receiveStatus': this.receiveStatus + }; } - /* - * No connection until initConnection() is called. + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {SerialPort8080} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "buffer") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request */ - this.sDataReceived = ""; - this.connection = this.sendData = this.updateStatus = null; + setBinding(sHTMLType, sBinding, control, sValue) + { + var serial = this; - /* - * Export all functions required by initConnection(). + switch (sBinding) { + case SerialPort8080.sIOBuffer: + this.bindings[sBinding] = this.controlIOBuffer = control; + + /* + * By establishing an onkeypress handler here, we make it possible for DOS commands like + * "CTTY COM1" to more or less work (use "CTTY CON" to restore control to the DOS console). + */ + control.onkeydown = function onKeyDown(event) { + /* + * This is required in addition to onkeypress, because it's the only way to prevent + * BACKSPACE (keyCode 8) from being interpreted by the browser as a "Back" operation; + * moreover, not all browsers generate an onkeypress notification for BACKSPACE. + * + * A related problem exists for Ctrl-key combinations in most Windows-based browsers + * (eg, IE, Edge, Chrome for Windows, etc), because keys like Ctrl-C and Ctrl-S have + * special meanings (eg, Copy, Save). To the extent the browser will allow it, we + * attempt to disable that default behavior when this control receives an onkeydown + * event for one of those keys (probably the only event the browser generates for them). + */ + event = event || window.event; + var keyCode = event.keyCode; + if (keyCode === 0x08 || event.ctrlKey && keyCode >= 0x41 && keyCode <= 0x5A) { + if (event.preventDefault) event.preventDefault(); + if (keyCode > 0x40) keyCode -= 0x40; + serial.receiveByte(keyCode); + } + return true; + }; + + control.onkeypress = function onKeyPress(event) { + /* + * Browser-independent keyCode extraction; refer to onKeyPress() and the other key event + * handlers in keyboard.js. + */ + event = event || window.event; + var keyCode = event.which || event.keyCode; + serial.receiveByte(keyCode); + /* + * Since we're going to remove the "readonly" attribute from the