diff --git a/modules/pc6502/lib/bus.js b/modules/pc6502/lib/bus.js new file mode 100644 index 000000000..65457f629 --- /dev/null +++ b/modules/pc6502/lib/bus.js @@ -0,0 +1,1073 @@ +/** + * @fileoverview Implements the PC6502 Bus component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var usr = require("../../shared/lib/usrlib"); + var Component = require("../../shared/lib/component"); + var Memory = require("./memory"); + var Messages = require("./messages"); + var State = require("./state"); +} + +/** + * Bus(cpu, dbg) + * + * The Bus component manages physical memory and I/O address spaces. + * + * The Bus 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. + * + * When initMemory() initializes the entire address space, it also passes aMemBlocks + * to the CPU object, so that the CPU can perform its own address-to-block calculations + * (essential, for example, when the CPU enables paging). + * + * 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 Bus 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 {CPUState} cpu + * @param {Debugger} dbg + */ +function Bus(parmsBus, cpu, dbg) +{ + Component.call(this, "Bus", parmsBus, Bus); + + this.cpu = cpu; + this.dbg = dbg; + + this.nBusWidth = parmsBus['busWidth'] || 16; + + /* + * Compute all Bus memory block parameters, based on the width of the bus. + * + * Regarding blockTotal, we want to avoid using block overflow expressions like: + * + * iBlock < this.nBlockTotal? iBlock : 0 + * + * As long as we know that blockTotal is a power of two (eg, 256 or 0x100, in the case of + * nBusWidth == 20 and blockSize == 4096), we can define blockMask as (blockTotal - 1) and + * rewrite the previous expression as: + * + * iBlock & this.nBlockMask + * + * Bus Property Old hard-coded values (when nBusWidth was always 20) + * ------------ ---------------------------------------------------- + * this.nBusLimit 0xfffff + * this.nBusMask [same as busLimit] + * this.nBlockSize 4096 + * this.nBlockLen (this.nBlockSize >> 2) + * this.nBlockShift 12 + * this.nBlockLimit 0xfff + * this.nBlockTotal ((this.nBusLimit + this.nBlockSize) / this.nBlockSize) | 0 + * this.nBlockMask (this.nBlockTotal - 1) [ie, 0xff] + * + * Note that we choose a nBlockShift value (and thus a physical memory block size) based on "buswidth": + * + * Bus Width Block Shift Block Size + * --------- ----------- ---------- + * 16 bits (64Kb address space): 10 1Kb (64 maximum blocks) + * 20 bits (1Mb address space): 12 4Kb (256 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 <= 16)? 10 : ((this.nBusWidth <= 20)? 12 : (this.nBusWidth <= 24? 14 : 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 <= Bus.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(); +} + +Component.subclass(Bus); + +Bus.ERROR = { + ADD_MEM_INUSE: 1, + ADD_MEM_BADRANGE: 2, + SET_MEM_BADRANGE: 4, + REM_MEM_BADRANGE: 5 +}; + +/** + * @typedef {number} + */ +var BlockInfo; + +/** + * This defines the BlockInfo bit fields used by scanMemory() when it creates the aBlocks array. + * + * @typedef {{ + * num: BitField, + * count: BitField, + * btmod: BitField, + * type: BitField + * }} + */ +Bus.BlockInfo = usr.defineBitFields({num:20, count:8, btmod:1, type:3}); + +/** + * BusInfo object definition (returned by scanMemory()) + * + * cbTotal: total bytes allocated + * cBlocks: total Memory blocks allocated + * aBlocks: array of allocated Memory block numbers + * + * @typedef {{ + * cbTotal: number, + * cBlocks: number, + * aBlocks: Array. + * }} + */ +var BusInfo; + +/** + * initMemory() + * + * Allocate enough (empty) Memory blocks to span the entire physical address space. + * + * @this {Bus} + */ +Bus.prototype.initMemory = function() +{ + var block = new Memory(); + block.copyBreakpoints(this.dbg); + this.aMemBlocks = new Array(this.nBlockTotal); + for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { + this.aMemBlocks[iBlock] = block; + } +}; + +/** + * reset() + * + * @this {Bus} + */ +Bus.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 {Bus} + * @param {Object|null} data (always null because we supply no powerDown() handler) + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ +Bus.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, Bus 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 {Bus} + * @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 Memory.TYPE constants + * @return {boolean} true if successful, false if not + */ +Bus.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(Bus.ERROR.ADD_MEM_INUSE, addrNext, sizeLeft); + } + + var blockNew = new Memory(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 " + Memory.TYPE.NAMES[type] + " at " + str.toHexWord(addr)); + return true; + } + + return this.reportError(Bus.ERROR.ADD_MEM_BADRANGE, addr, size); +}; + +/** + * cleanMemory(addr, size) + * + * @this {Bus} + * @param {number} addr + * @param {number} size + * @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process + */ +Bus.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; +}; + +/** + * scanMemory(info, addr, size) + * + * Returns a BusInfo object for the specified address range. + * + * @this {Bus} + * @param {Object} [info] previous BusInfo, 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) + */ +Bus.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(Bus.BlockInfo, iBlock, 0, 0, block.type)); + info.cBlocks++ + } + iBlock++; + } + return info; +}; + +/** + * getWidth() + * + * @this {Bus} + * @return {number} + */ +Bus.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 {Bus} + * @param {number} addr + * @param {number} size + * @return {boolean} true if successful, false if not + */ +Bus.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 Memory(addr); + blockNew.copyBreakpoints(this.dbg, blockOld); + this.aMemBlocks[iBlock++] = blockNew; + addr = iBlock * this.nBlockSize; + size -= this.nBlockSize; + } + return true; + } + return this.reportError(Bus.ERROR.REM_MEM_BADRANGE, addr, size); +}; + +/** + * getMemoryBlocks(addr, size) + * + * @this {Bus} + * @param {number} addr is the starting physical address + * @param {number} size of the request, in bytes + * @return {Array} of Memory blocks + */ +Bus.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 {Bus} + * @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 Memory.TYPE constants + */ +Bus.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 Memory(addr); + blockNew.clone(block, type, this.dbg); + block = blockNew; + } + this.aMemBlocks[iBlock++] = block; + size -= this.nBlockSize; + } +}; + +/** + * getByte(addr) + * + * @this {Bus} + * @param {number} addr is a physical address + * @return {number} byte (8-bit) value at that address + */ +Bus.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 {Bus} + * @param {number} addr is a physical address + * @return {number} byte (8-bit) value at that address + */ +Bus.prototype.getByteDirect = function(addr) +{ + return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByteDirect(addr & this.nBlockLimit, addr); +}; + +/** + * getShort(addr) + * + * @this {Bus} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ +Bus.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 {Bus} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ +Bus.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 {Bus} + * @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) + */ +Bus.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 {Bus} + * @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) + */ +Bus.prototype.setByteDirect = function(addr, b) +{ + this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr); +}; + +/** + * setShort(addr, w) + * + * @this {Bus} + * @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) + */ +Bus.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 {Bus} + * @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) + */ +Bus.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 {Bus} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ +Bus.prototype.addMemBreak = function(addr, fWrite) +{ + if (DEBUGGER) { + var iBlock = addr >>> this.nBlockShift; + this.aMemBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite); + } +}; + +/** + * removeMemBreak(addr, fWrite) + * + * @this {Bus} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ +Bus.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 {Bus} + * @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags) + * @return {Array} a + */ +Bus.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 != Memory.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 {Bus} + * @param {Array} a + * @return {boolean} true if successful, false if not + */ +Bus.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 {Bus} + * @param {number} [port] + * @return {boolean} true if break on port input enabled, false if disabled + */ +Bus.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 {Bus} + * @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 + */ +Bus.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 {Bus} + * @param {Component} component + * @param {Object} table + * @param {number} [offset] is an optional port offset + */ +Bus.prototype.addPortInputTable = function(component, table, offset) +{ + if (offset === undefined) offset = 0; + 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 {Bus} + * @param {number} port + * @param {number} size (1, 2 or 4) + */ +Bus.prototype.addPortInputWidth = function(port, size) +{ + this.aPortInputWidth[port] = size; +}; + +/** + * checkPortInputNotify(port, size, addrIP) + * + * @this {Bus} + * @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. + */ +Bus.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 {Bus} + * @param {number} start address + * @param {number} end address + * +Bus.prototype.removePortInputNotify = function(start, end) + { + for (var port = start; port < end; port++) { + if (this.aPortInputNotify[port]) { + delete this.aPortInputNotify[port]; + } + } +}; + */ + +/** + * addPortOutputBreak(port) + * + * @this {Bus} + * @param {number} [port] + * @return {boolean} true if break on port output enabled, false if disabled + */ +Bus.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 {Bus} + * @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 + */ +Bus.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 {Bus} + * @param {Component} component + * @param {Object} table + * @param {number} [offset] is an optional port offset + */ +Bus.prototype.addPortOutputTable = function(component, table, offset) +{ + if (offset === undefined) offset = 0; + 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 {Bus} + * @param {number} port + * @param {number} size (1, 2 or 4) + */ +Bus.prototype.addPortOutputWidth = function(port, size) +{ + this.aPortOutputWidth[port] = size; +}; + +/** + * checkPortOutputNotify(port, size, data, addrIP) + * + * @this {Bus} + * @param {number} port + * @param {number} size + * @param {number} data + * @param {number} [addrIP] is the IP value at the time of the output + */ +Bus.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 {Bus} + * @param {number} start address + * @param {number} end address + * +Bus.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 {Bus} + * @param {number} op + * @param {number} addr + * @param {number} size + * @param {boolean} [fQuiet] (true if any error should be quietly logged) + * @return {boolean} false + */ +Bus.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 = Bus; diff --git a/modules/pc6502/lib/computer.js b/modules/pc6502/lib/computer.js new file mode 100644 index 000000000..2b1bb7094 --- /dev/null +++ b/modules/pc6502/lib/computer.js @@ -0,0 +1,1638 @@ +/** + * @fileoverview Implements the PC6502 Computer component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"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 Messages = require("./messages"); + var Bus = require("./bus"); + var State = require("./state"); +} + +/** + * Computer(parmsComputer, parmsMachine, fSuspended) + * + * @constructor + * @extends Component + * @param {Object} parmsComputer + * @param {Object} [parmsMachine] + * @param {boolean} [fSuspended] + * + * The Computer 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 Computer.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 Computer'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 Computer'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". + */ +function Computer(parmsComputer, parmsMachine, fSuspended) { + + Component.call(this, "Computer", parmsComputer, Computer, Messages.COMPUTER); + + this.flags.fPowered = 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 = Computer.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 {CPUState} */ (Component.getComponentByType("CPU", this.id)); + if (!this.cpu) { + Component.error("Unable to find CPU component"); + return; + } + this.dbg = /** @type {Debugger} */ (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 Bus({'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 {Panel} */ (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(Computer.APPNAME + " v" + Computer.APPVERSION + "\n" + Computer.COPYRIGHT + "\n" + Computer.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 = Computer.RESUME_NONE; + } + if (this.resume) { + this.stateComputer = new State(this, Computer.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); +} + +Component.subclass(Computer); + +Computer.APPNAME = APPNAME || "PC6502"; +Computer.APPVERSION = APPVERSION; +Computer.COPYRIGHT = "Copyright © 2012-2016 Jeff Parsons "; + +/* + * I think it's a good idea to also display a GPL notice, putting people on notice that even + * the "compiled" source code has all the same GPL requirements as the uncompiled source code. + */ +Computer.LICENSE = "License: GPL version 3 or later "; + +Computer.STATE_FAILSAFE = "failsafe"; +Computer.STATE_VALIDATE = "validate"; +Computer.STATE_TIMESTAMP = "timestamp"; +Computer.STATE_VERSION = "version"; +Computer.STATE_HOSTURL = "url"; +Computer.STATE_BROWSER = "browser"; +Computer.STATE_USERID = "user"; + +/* + * The following constants define all the resume options. Negative values (eg, RESUME_REPOWER) are for + * internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states + * whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2. + */ +Computer.RESUME_REPOWER = -1; // resume without changing any state (for internal use only) +Computer.RESUME_NONE = 0; // default (no resume) +Computer.RESUME_AUTO = 1; // automatically save/restore state +Computer.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) +Computer.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) + +/** + * getMachineID() + * + * @return {string} + */ +Computer.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] + */ +Computer.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} + */ +Computer.prototype.getMachineParm = function(sParm, parmsComponent) +{ + /* + * When checking parmsURL, 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 = Component.parmsURL[sParm] || Component.parmsURL[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} + */ +Computer.prototype.saveMachineParms = function() +{ + return this.parmsMachine? JSON.stringify(this.parmsMachine) : null; +}; + +/** + * getUserID() + * + * @return {string} + */ +Computer.prototype.getUserID = function() +{ + return this.sUserID || ""; +}; + +/** + * doneLoad(sURL, sStateData, nErrorCode) + * + * @this {Computer} + * @param {string} sURL + * @param {string} sStateData + * @param {number} nErrorCode + */ +Computer.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 {Computer} + * @param {function(...)} fn + * @param {number|Array} [parms] optional parameters + */ +Computer.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("Computer.wait(ready)"); + fn.call(this, parms); +}; + +/** + * validateState(stateComputer) + * + * NOTE: We clear() stateValidate only when there's no stateComputer. + * + * @this {Computer} + * @param {State|null} [stateComputer] + * @return {boolean} true if state passes validation, false if not + */ +Computer.prototype.validateState = function(stateComputer) +{ + var fValid = true; + var stateValidate = new State(this, Computer.APPVERSION, Computer.STATE_VALIDATE); + if (stateValidate.load() && stateValidate.parse()) { + var sTimestampValidate = stateValidate.get(Computer.STATE_TIMESTAMP); + var sTimestampComputer = stateComputer ? stateComputer.get(Computer.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 {Computer} + * @param {number} [resume] is a valid RESUME value; default is this.resume + */ +Computer.prototype.powerOn = function(resume) +{ + if (resume === undefined) { + resume = this.resume || (this.sStateData? Computer.RESUME_AUTO : Computer.RESUME_NONE); + } + + if (DEBUG && this.messageEnabled()) { + this.printMessage("Computer.powerOn(" + (resume == Computer.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, Computer.APPVERSION); + + if (resume == Computer.RESUME_REPOWER) { + fRepower = true; + } + else if (resume > Computer.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, Computer.APPVERSION, Computer.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 = Computer.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(Computer.STATE_TIMESTAMP, usr.getTimestamp()); + this.stateFailSafe.store(); + + var fValidate = this.resume && !this.fServerState; + if (resume == Computer.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + Computer.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 == Computer.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 != Computer.RESUME_REPOWER) { + this.wait(this.donePowerOn, aParms); + return; + } + this.donePowerOn(aParms); +}; + +/** + * powerRestore(component, stateComputer, fRepower, fRestore) + * + * @this {Computer} + * @param {Component} component + * @param {State} stateComputer + * @param {boolean} fRepower + * @param {boolean} fRestore + * @return {boolean} true if restore should continue, false if not + */ +Computer.prototype.powerRestore = function(component, stateComputer, fRepower, fRestore) +{ + if (!component.flags.fPowered) { + + component.flags.fPowered = 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 = Computer.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 {Computer} + * @param {Array} aParms containing [stateComputer, resume, fRestore] + */ +Computer.prototype.donePowerOn = function(aParms) +{ + var stateComputer = aParms[0]; + var fRepower = (aParms[1] < 0); + var fRestore = aParms[2]; + + if (DEBUG && this.flags.fPowered && this.messageEnabled()) { + this.printMessage("Computer.donePowerOn(): redundant"); + } + + this.fInitialized = true; + this.flags.fPowered = 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 {Computer} + * @return {boolean} true if the computer is fully powered, false otherwise + */ +Computer.prototype.checkPower = function() +{ + if (this.flags.fPowered) 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.fReady) break; + } + if (iComponent == aComponents.length) { + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component !== this && !component.flags.fPowered) break; + } + } + if (iComponent == aComponents.length) component = this; + var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.fReady? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + "."; + web.alertUser(s); + return false; +}; + +/** + * powerReport(stateComputer) + * + * @this {Computer} + * @param {State} stateComputer + */ +Computer.prototype.powerReport = function(stateComputer) +{ + if (web.confirmUser("There may be a problem with your " + Computer.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + Computer.APPNAME + " machine state to http://" + SITEHOST + ".")) { + web.sendReport(Computer.APPNAME, Computer.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 {Computer} + * @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) + */ +Computer.prototype.powerOff = function(fSave, fShutdown) +{ + var data; + var sState = "none"; + + if (DEBUG && this.messageEnabled()) { + this.printMessage("Computer.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); + } + + if (this.nPowerChange) { + return null; + } + this.nPowerChange--; + + var stateComputer = new State(this, Computer.APPVERSION); + var stateValidate = new State(this, Computer.APPVERSION, Computer.STATE_VALIDATE); + + var sTimestamp = usr.getTimestamp(); + stateValidate.set(Computer.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(Computer.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(Computer.STATE_VERSION, APPVERSION); + stateComputer.set(Computer.STATE_HOSTURL, web.getHostURL()); + stateComputer.set(Computer.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.fPowered = 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.fPowered) { + if (component.powerDown) { + data = component.powerDown(fSave, fShutdown); + if (typeof data === "object") stateComputer.set(component.id, data); + } + if (fShutdown) { + component.flags.fPowered = 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 == Computer.RESUME_DELETE); + } + } + if (fClear) { + stateComputer.clear(fClearAll); + } + } else { + sState = stateComputer.toString(); + } + } + + if (fShutdown) { + this.flags.fPowered = 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 {Computer} + */ +Computer.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 {Computer} + * @param {number} ms + * @param {number} nCycles + */ +Computer.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 {Computer} + * @param {number} ms + * @param {number} nCycles + */ +Computer.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 {Computer} + * @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 + */ +Computer.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() + */ +Computer.prototype.resetUserID = function() +{ + web.setLocalStorageItem(Computer.STATE_USERID, ""); + this.sUserID = null; +}; + +/** + * queryUserID(fPrompt) + * + * @param {boolean} [fPrompt] + * @returns {string|null|undefined} + */ +Computer.prototype.queryUserID = function(fPrompt) +{ + var sUserID = this.sUserID; + if (!sUserID) { + sUserID = web.getLocalStorageItem(Computer.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 {Computer} + * @param {string} sUserID + * @return {string} validated user ID, or null if error + */ +Computer.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(Computer.STATE_USERID, response.data); + if (fMessages) this.printMessage(Computer.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 {Computer} + * @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage) + */ +Computer.prototype.getServerStatePath = function() +{ + var sStatePath = null; + if (this.sUserID) { + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer.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, Computer.APPVERSION); + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer.STATE_USERID + " unavailable"); + } + } + return sStatePath; +}; + +/** + * saveServerState(sUserID, sState) + * + * @param {string} sUserID + * @param {string|null} sState + */ +Computer.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 {Computer} + * @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 + */ +Computer.prototype.storeServerState = function(sUserID, sState, fSync) +{ + if (DEBUG && this.messageEnabled()) { + this.printMessage(Computer.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, Computer.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 {Computer} + */ +Computer.prototype.onPower = function() +{ + if (!this.nPowerChange) { + if (!this.flags.fPowered) { + 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 {Computer} + */ +Computer.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.fPowered || 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 == Computer.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 == Computer.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + Computer.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(Computer.RESUME_NONE); + this.fReload = false; + } else { + this.reset(); + if (this.cpu) this.cpu.autoStart(); + } +}; + +/** + * getMachineComponent(sType, componentPrev) + * + * @this {Computer} + * @param {string} sType + * @param {Component|null} [componentPrev] of previously returned component, if any + * @return {Component|null} + */ +Computer.prototype.getMachineComponent = function(sType, 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; + } + return null; +}; + +/** + * updateFocus(fScroll) + * + * NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button), + * the scroll feature has annoying effect on iOS, so we no longer do it by default (fScroll must be true). + * + * @this {Computer} + * @param {boolean} [fScroll] + */ +Computer.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 {Computer} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) + */ +Computer.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(n) + * + * Any high-frequency updates should be performed here (avoid updating DOM elements). + * + * @this {Computer} + * @param {number} n (where 0 <= n < VIDEO_UPDATES_PER_SECOND for a normal update, or -1 for a forced update) + */ +Computer.prototype.updateVideo = function(n) +{ + for (var i = 0; i < this.aVideo.length; i++) { + this.aVideo[i].updateScreen(n); + } +}; + +/** + * Computer.init() + * + * For every machine represented by an HTML element of class "pcjs-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. + */ +Computer.init = function() +{ + /* + * In non-COMPILED builds, embedMachine() may have set XMLVERSION. + */ + if (!COMPILED && XMLVERSION) Computer.APPVERSION = XMLVERSION; + + var aeMachines = Component.getElementsByClass(document, PCJSCLASS + "-machine"); + + for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) { + + var eMachine = aeMachines[iMachine]; + var parmsMachine = Component.getComponentParms(eMachine); + + var aeComputers = Component.getElementsByClass(eMachine, PCJSCLASS, "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 Computer(parmsComputer, parmsMachine, true); + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onInit(" + computer.flags.fPowered + ")"); + } + + /* + * 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, PCJSCLASS); + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (computer.fAutoPower) computer.wait(computer.powerOn); + } + } +}; + +/** + * Computer.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. + */ +Computer.show = function() +{ + var aeComputers = Component.getElementsByClass(document, PCJSCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {Computer} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.fPowered + ")"); + } + + if (computer.fInitialized && !computer.flags.fPowered) { + /** + * Repower the computer, notifying every component to continue running as-is. + */ + computer.powerOn(Computer.RESUME_REPOWER); + } + } + } +}; + +/** + * Computer.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 Computer.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. + */ +Computer.exit = function() +{ + var aeComputers = Component.getElementsByClass(document, PCJSCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {Computer} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onExit(" + computer.flags.fPowered + ")"); + } + + if (computer.flags.fPowered) { + /** + * 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(Computer.init); +web.onShow(Computer.show); +web.onExit(Computer.exit); + +if (NODE) module.exports = Computer; diff --git a/modules/pc6502/lib/cpu.js b/modules/pc6502/lib/cpu.js new file mode 100644 index 000000000..f1502d48e --- /dev/null +++ b/modules/pc6502/lib/cpu.js @@ -0,0 +1,1128 @@ +/** + * @fileoverview Controls the PC6502 CPU component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var usr = require("../../shared/lib/usrlib"); + var Component = require("../../shared/lib/component"); + var Messages = require("./messages"); +} + +/** + * CPU(parmsCPU, nCyclesDefault) + * + * The CPU class supports the following (parmsCPU) properties: + * + * cycles: the machine's base cycles per second; the CPUState 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 CPUState component, where the simulation control logic resides. + * + * @constructor + * @extends Component + * @param {Object} parmsCPU + * @param {number} nCyclesDefault + */ +function CPU(parmsCPU, nCyclesDefault) +{ + Component.call(this, "CPU", parmsCPU, CPU, Messages.CPU); + + var nCycles = parmsCPU['cycles'] || nCyclesDefault; + + var nMultiplier = parmsCPU['multiplier'] || 1; + + this.aCounts = {}; + this.aCounts.nCyclesPerSecond = nCycles; + this.aCounts.nVideoUpdates = 0; + + /* + * 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.aCounts.nCyclesMultiplier = nMultiplier; + this.aCounts.mhzDefault = Math.round(this.aCounts.nCyclesPerSecond / 10000) / 100; + /* + * TODO: Take care of this with an initial setSpeed() call instead? + */ + this.aCounts.mhzTarget = this.aCounts.mhzDefault * this.aCounts.nCyclesMultiplier; + + /* + * We add a number of flags to the set initialized by Component + */ + this.flags.fRunning = false; + this.flags.fStarting = false; + this.flags.fAutoStart = parmsCPU['autoStart']; + + /* + * TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both) + */ + this.flags.fDisplayLiveRegs = 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.fChecksum = false; + this.aCounts.nChecksum = this.aCounts.nCyclesChecksumNext = 0; + this.aCounts.nCyclesChecksumStart = parmsCPU["csStart"]; + this.aCounts.nCyclesChecksumInterval = parmsCPU["csInterval"]; + this.aCounts.nCyclesChecksumStop = parmsCPU["csStop"]; + + this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); }; + + this.setReady(); +} + +Component.subclass(CPU); + +/* + * Constants that control the frequency at which various updates should occur. + * + * These values do NOT control the simulation directly. Instead, they are used by + * calcCycles(), which uses the nCyclesPerSecond passed to the constructor as a starting + * point and computes the following variables: + * + * this.aCounts.nCyclesPerYield (this.aCounts.nCyclesPerSecond / CPU.YIELDS_PER_SECOND) + * this.aCounts.nCyclesPerVideoUpdate (this.aCounts.nCyclesPerSecond / CPU.VIDEO_UPDATES_PER_SECOND) + * this.aCounts.nCyclesPerStatusUpdate (this.aCounts.nCyclesPerSecond / CPU.STATUS_UPDATES_PER_SECOND) + * + * The above variables are also multiplied by any cycle multiplier in effect, via setSpeed(), + * and then they're used to initialize another set of variables for each runCPU() iteration: + * + * this.aCounts.nCyclesNextYield <= this.aCounts.nCyclesPerYield + * this.aCounts.nCyclesNextVideoUpdate <= this.aCounts.nCyclesPerVideoUpdate + * this.aCounts.nCyclesNextStatusUpdate <= this.aCounts.nCyclesPerStatusUpdate + */ +CPU.YIELDS_PER_SECOND = 30; +CPU.VIDEO_UPDATES_PER_SECOND = 60; +CPU.STATUS_UPDATES_PER_SECOND = 2; + +CPU.BUTTONS = ["power", "reset"]; + +/** + * initBus(cmp, bus, cpu, dbg) + * + * @this {CPU} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPU} cpu + * @param {Debugger} dbg + */ +CPU.prototype.initBus = function(cmp, bus, cpu, dbg) +{ + this.cmp = cmp; + this.bus = bus; + this.dbg = dbg; + + for (var i = 0; i < CPU.BUTTONS.length; i++) { + var control = this.bindings[CPU.BUTTONS[i]]; + if (control) this.cmp.setBinding(null, CPU.BUTTONS[i], control); + } + + /* + * We need to know the refresh rate (and corresponding interrupt rate, if any) of the Video component. + */ + var video = cmp.getMachineComponent("Video"); + this.refreshRate = video && video.getRefreshRate() || CPU.VIDEO_UPDATES_PER_SECOND; + + /* + * Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts. + */ + this.chipset = 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.fAutoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart)); + } + + this.setReady(); +}; + +/** + * reset() + * + * @this {CPU} + */ +CPU.prototype.reset = function() +{ + this.aCounts.nVideoUpdates = 0; +}; + +/** + * save() + * + * This is a placeholder for save support (overridden by the CPUState component). + * + * @this {CPU} + * @return {Object|null} + */ +CPU.prototype.save = function() +{ + return null; +}; + +/** + * restore(data) + * + * This is a placeholder for restore support (overridden by the CPUState component). + * + * @this {CPU} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ +CPU.prototype.restore = function(data) +{ + return false; +}; + +/** + * powerUp(data, fRepower) + * + * @this {CPU} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ +CPU.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.fPowered = true; + */ + this.updateCPU(); + return true; +}; + +/** + * powerDown(fSave, fShutdown) + * + * @this {CPU} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ +CPU.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.fPowered = false; + */ + return fSave? this.save() : true; +}; + +/** + * autoStart() + * + * @this {CPU} + * @return {boolean} true if started, false if not + */ +CPU.prototype.autoStart = function() +{ + /* + * Start running automatically on power-up, assuming there's no Debugger and no "Run" button + */ + if (this.flags.fAutoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) { + /* + * Now we 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. + */ + this.runCPU(true); + return true; + } + return false; +}; + +/** + * isPowered() + * + * @this {CPU} + * @return {boolean} + */ +CPU.prototype.isPowered = function() +{ + if (!this.flags.fPowered) { + this.println(this.toString() + " not powered"); + return false; + } + return true; +}; + +/** + * isRunning() + * + * @this {CPU} + * @return {boolean} + */ +CPU.prototype.isRunning = function() +{ + return this.flags.fRunning; +}; + +/** + * getChecksum() + * + * This will be implemented by the CPUState component. + * + * @this {CPU} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ +CPU.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 {CPU} + * @return {boolean} true if checksum generation enabled, false if not + */ +CPU.prototype.resetChecksum = function() +{ + if (this.aCounts.nCyclesChecksumStart === undefined) this.aCounts.nCyclesChecksumStart = 0; + if (this.aCounts.nCyclesChecksumInterval === undefined) this.aCounts.nCyclesChecksumInterval = -1; + if (this.aCounts.nCyclesChecksumStop === undefined) this.aCounts.nCyclesChecksumStop = -1; + this.flags.fChecksum = (this.aCounts.nCyclesChecksumStart >= 0 && this.aCounts.nCyclesChecksumInterval > 0); + if (this.flags.fChecksum) { + this.aCounts.nChecksum = 0; + this.aCounts.nCyclesChecksumNext = this.aCounts.nCyclesChecksumStart - this.nTotalCycles; + /* + * this.aCounts.nCyclesChecksumNext = this.aCounts.nCyclesChecksumStart + this.aCounts.nCyclesChecksumInterval - + * (this.nTotalCycles % this.aCounts.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 {CPU} + * @param {number} nCycles + */ +CPU.prototype.updateChecksum = function(nCycles) +{ + if (this.flags.fChecksum) { + /* + * Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum + */ + var fDisplay = false; + this.aCounts.nChecksum = (this.aCounts.nChecksum + this.getChecksum())|0; + this.aCounts.nCyclesChecksumNext -= nCycles; + if (this.aCounts.nCyclesChecksumNext <= 0) { + this.aCounts.nCyclesChecksumNext += this.aCounts.nCyclesChecksumInterval; + fDisplay = true; + } + if (this.aCounts.nCyclesChecksumStop >= 0) { + if (this.aCounts.nCyclesChecksumStop <= this.getCycles()) { + this.aCounts.nCyclesChecksumInterval = this.aCounts.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 {CPU} + */ +CPU.prototype.displayChecksum = function() +{ + this.println(this.getCycles() + " cycles: " + "checksum=" + str.toHex(this.aCounts.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 {CPU} + * @param {string} sLabel + * @param {number} nValue + * @param {number} cch + */ +CPU.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.fRunning || this.flags.fDisplayLiveRegs) { + 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 {CPU} + * @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 + */ +CPU.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; + if (!cpu.flags.fRunning) + cpu.runCPU(true); + else + cpu.stopCPU(true); + }; + 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.aCounts.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. + * + * @this {CPU} + * @param {number} nCycles is the target number of cycles to drop the current burst to + * @return {boolean} + */ +CPU.prototype.setBurstCycles = function(nCycles) +{ + if (this.flags.fRunning) { + 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 {CPU} + * @param {number} nCycles + * @param {boolean} [fEndStep] + */ +CPU.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 the following values: + * + * CPU.YIELDS_PER_SECOND (eg, 30) + * CPU.VIDEO_UPDATES_PER_SECOND (eg, 60) + * CPU.STATUS_UPDATES_PER_SECOND (eg, 5) + * + * The largest of the above values forces the size of the burst to its smallest value. Let's say that + * largest value is 30. Assuming nCyclesPerSecond is 1,000,000, that results in bursts of 33,333 cycles. + * + * At the end of each burst, we subtract burst cycles from yield, video, and status cycle "threshold" + * counters. 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(). + * + * Similarly, whenever the "next video update" cycle counter goes to (or below) zero, we call updateVideo(), + * and whenever the "next status update" cycle counter goes to (or below) zero, we call updateStatus(). + * + * @this {CPU} + * @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent + * speed calculation (see calcSpeed). + */ +CPU.prototype.calcCycles = function(fRecalc) +{ + /* + * Calculate the most cycles we're allowed to execute in a single "burst" + */ + var nMostUpdatesPerSecond = CPU.YIELDS_PER_SECOND; + if (nMostUpdatesPerSecond < this.refreshRate) nMostUpdatesPerSecond = this.refreshRate; + if (nMostUpdatesPerSecond < CPU.STATUS_UPDATES_PER_SECOND) nMostUpdatesPerSecond = CPU.STATUS_UPDATES_PER_SECOND; + + /* + * Calculate cycle "per" values for the yield, video update, and status update cycle counters + */ + var vMultiplier = 1; + if (fRecalc) { + if (this.aCounts.nCyclesMultiplier > 1 && this.aCounts.mhz) { + vMultiplier = (this.aCounts.mhz / this.aCounts.mhzDefault); + } + } + + this.aCounts.msPerYield = Math.round(1000 / CPU.YIELDS_PER_SECOND); + this.aCounts.nCyclesPerBurst = Math.floor(this.aCounts.nCyclesPerSecond / nMostUpdatesPerSecond * vMultiplier); + this.aCounts.nCyclesPerYield = Math.floor(this.aCounts.nCyclesPerSecond / CPU.YIELDS_PER_SECOND * vMultiplier); + this.aCounts.nCyclesPerVideoUpdate = Math.floor(this.aCounts.nCyclesPerSecond / this.refreshRate * vMultiplier); + this.aCounts.nCyclesPerStatusUpdate = Math.floor(this.aCounts.nCyclesPerSecond / CPU.STATUS_UPDATES_PER_SECOND * vMultiplier); + + /* + * And initialize "next" yield, video update, and status update cycle "threshold" counters to those "per" values + */ + if (!fRecalc) { + this.aCounts.nCyclesNextYield = this.aCounts.nCyclesPerYield; + this.aCounts.nCyclesNextVideoUpdate = this.aCounts.nCyclesPerVideoUpdate; + this.aCounts.nCyclesNextStatusUpdate = this.aCounts.nCyclesPerStatusUpdate; + } + this.aCounts.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 {CPU} + * @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1 + * @return {number} + */ +CPU.prototype.getCycles = function(fScaled) +{ + var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles; + if (fScaled && this.aCounts.nCyclesMultiplier > 1 && this.aCounts.mhz > this.aCounts.mhzDefault) { + /* + * We could scale the current cycle count by the current effective speed (this.aCounts.mhz); eg: + * + * nCycles = Math.round(nCycles / (this.aCounts.mhz / this.aCounts.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.aCounts.nCyclesMultiplier); + } + return nCycles; +}; + +/** + * getCyclesPerSecond() + * + * This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine) + * + * @this {CPU} + * @return {number} + */ +CPU.prototype.getCyclesPerSecond = function() +{ + return this.aCounts.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 {CPU} + */ +CPU.prototype.resetCycles = function() +{ + this.aCounts.mhz = 0; + this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = 0; + this.resetChecksum(); + this.setSpeed(1); +}; + +/** + * getSpeed() + * + * @this {CPU} + * @return {number} the current speed multiplier + */ +CPU.prototype.getSpeed = function() +{ + return this.aCounts.nCyclesMultiplier; +}; + +/** + * getSpeedCurrent() + * + * @this {CPU} + * @return {string} the current speed, in mhz, as a string formatted to two decimal places + */ +CPU.prototype.getSpeedCurrent = function() +{ + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return ((this.flags.fRunning && this.aCounts.mhz)? (this.aCounts.mhz.toFixed(2) + "Mhz") : "Stopped"); +}; + +/** + * getSpeedTarget() + * + * @this {CPU} + * @return {string} the target speed, in mhz, as a string formatted to two decimal places + */ +CPU.prototype.getSpeedTarget = function() +{ + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return this.aCounts.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. + * + * @this {CPU} + * @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 + * + * @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). + */ +CPU.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.aCounts.mhz / this.aCounts.mhzTarget < 0.8) { + nMultiplier = 1; + } else { + fSuccess = true; + } + this.aCounts.nCyclesMultiplier = nMultiplier; + var mhz = this.aCounts.mhzDefault * this.aCounts.nCyclesMultiplier; + if (this.aCounts.mhzTarget != mhz) { + this.aCounts.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.aCounts.msStartRun = usr.getTime(); + this.aCounts.msEndThisRun = 0; + this.calcCycles(); + return fSuccess; +}; + +/** + * calcSpeed(nCycles, msElapsed) + * + * @this {CPU} + * @param {number} nCycles + * @param {number} msElapsed + */ +CPU.prototype.calcSpeed = function(nCycles, msElapsed) +{ + if (msElapsed) { + this.aCounts.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 {CPU} + */ +CPU.prototype.calcStartTime = function() +{ + if (this.aCounts.nCyclesRecalc >= this.aCounts.nCyclesPerSecond) { + this.calcCycles(true); + } + this.aCounts.nCyclesThisRun = 0; + this.aCounts.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.aCounts.msEndThisRun) { + var msDelta = this.aCounts.msStartThisRun - this.aCounts.msEndThisRun; + if (msDelta > this.aCounts.msPerYield) { + if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms"); + this.aCounts.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.aCounts.msStartRun <= this.aCounts.msStartThisRun); + if (this.aCounts.msStartRun > this.aCounts.msStartThisRun) { + this.aCounts.msStartRun = this.aCounts.msStartThisRun; + } + } + } +}; + +/** + * calcRemainingTime() + * + * @this {CPU} + * @return {number} + */ +CPU.prototype.calcRemainingTime = function() +{ + this.aCounts.msEndThisRun = usr.getTime(); + + var msYield = this.aCounts.msPerYield; + if (this.aCounts.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.aCounts.nCyclesThisRun / this.aCounts.nCyclesPerYield); + } + + var msElapsedThisRun = this.aCounts.msEndThisRun - this.aCounts.msStartThisRun; + var msRemainsThisRun = msYield - msElapsedThisRun; + + /* + * We could pass only "this run" results to calcSpeed(): + * + * nCycles = this.aCounts.nCyclesThisRun; + * msElapsed = msElapsedThisRun; + * + * but it seems preferable to use longer time periods and hopefully get a more accurate speed. + * + * Also, if msRemainsThisRun >= 0 && this.aCounts.nCyclesMultiplier == 1, we could pass these results instead: + * + * nCycles = this.aCounts.nCyclesThisRun; + * msElapsed = this.aCounts.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.aCounts.msEndThisRun - this.aCounts.msStartRun; + + if (MAXDEBUG && msRemainsThisRun < 0 && this.aCounts.nCyclesMultiplier > 1) { + this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)"); + } + + this.calcSpeed(nCycles, msElapsed); + + if (msRemainsThisRun < 0 || this.aCounts.mhz < this.aCounts.mhzTarget) { + /* + * 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.aCounts.nCyclesRecalc += this.aCounts.nCyclesThisRun; + + if (DEBUG && this.messageEnabled(Messages.LOG) && msRemainsThisRun) { + this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.aCounts.msEndThisRun + "ms"); + } + + this.aCounts.msEndThisRun += msRemainsThisRun; + return msRemainsThisRun; +}; + +/** + * runCPU(fUpdateFocus) + * + * @this {CPU} + * @param {boolean} [fUpdateFocus] is true to update Computer focus + */ +CPU.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 { + var nCyclesPerBurst = (this.flags.fChecksum? 1 : this.aCounts.nCyclesPerBurst); + + /* + * nCyclesPerBurst is how many cycles we WANT to run on each iteration of stepCPU(), but it may run + * significantly less (or slightly more, since we can't execute partial instructions). + */ + this.stepCPU(nCyclesPerBurst); + + /* + * nBurstCycles, less any remaining nStepCycles, is how many cycles stepCPU() ACTUALLY ran (nCycles). + * We add that to nCyclesThisRun, as well as nRunCycles, which is the cycle count since the CPU first + * started running. + */ + var nCycles = this.nBurstCycles - this.nStepCycles; + this.nRunCycles += nCycles; + this.aCounts.nCyclesThisRun += nCycles; + this.addCycles(0, true); + this.updateChecksum(nCycles); + + this.aCounts.nCyclesNextVideoUpdate -= nCycles; + if (this.aCounts.nCyclesNextVideoUpdate <= 0) { + this.aCounts.nCyclesNextVideoUpdate += this.aCounts.nCyclesPerVideoUpdate; + if (this.cmp) this.cmp.updateVideo(this.aCounts.nVideoUpdates++); + if (this.aCounts.nVideoUpdates > this.refreshRate) this.aCounts.nVideoUpdates = 0; + } + + this.aCounts.nCyclesNextStatusUpdate -= nCycles; + if (this.aCounts.nCyclesNextStatusUpdate <= 0) { + this.aCounts.nCyclesNextStatusUpdate += this.aCounts.nCyclesPerStatusUpdate; + if (this.cmp) this.cmp.updateStatus(); + } + + this.aCounts.nCyclesNextYield -= nCycles; + if (this.aCounts.nCyclesNextYield <= 0) { + this.aCounts.nCyclesNextYield += this.aCounts.nCyclesPerYield; + break; + } + } while (this.flags.fRunning); + } + 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] + */ +CPU.prototype.startCPU = function(fUpdateFocus) +{ + if (!this.flags.fRunning) { + /* + * 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.aCounts.msStartRun, this.getCycles()); + this.flags.fRunning = true; + this.flags.fStarting = 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 CPUState component. + * + * @this {CPU} + * @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 + */ +CPU.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 {CPU} + * @param {boolean} [fComplete] + */ +CPU.prototype.stopCPU = function(fComplete) +{ + this.isBusy(true); + this.nBurstCycles -= this.nStepCycles; + this.nStepCycles = 0; + this.addCycles(this.nRunCycles); + this.nRunCycles = 0; + if (this.flags.fRunning) { + this.flags.fRunning = false; + if (this.chipset) this.chipset.stop(); + var controlRun = this.bindings["run"]; + if (controlRun) controlRun.textContent = "Run"; + } + this.flags.fComplete = 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 {CPU} + * @param {boolean} [fForce] (true to force a video update; used by the Debugger) + */ +CPU.prototype.updateCPU = function(fForce) +{ + if (this.cmp) { + this.cmp.updateVideo(-1); + 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 {CPU} + */ +CPU.prototype.yieldCPU = function() +{ + this.aCounts.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU() + this.nBurstCycles -= this.nStepCycles; + this.nStepCycles = 0; // this will break us 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 = CPU; diff --git a/modules/pc6502/lib/cpudef.js b/modules/pc6502/lib/cpudef.js new file mode 100644 index 000000000..49ad464b7 --- /dev/null +++ b/modules/pc6502/lib/cpudef.js @@ -0,0 +1,92 @@ +/** + * @fileoverview Defines PC6502 CPU constants. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +var CPUDef = { + /* + * CPU model numbers (supported) + */ + MODEL_6502: 6502, + + /* + * This constant is used to mark points in the code where the physical address being returned + * is invalid and should not be used. + * + * In a 32-bit CPU, -1 (ie, 0xffffffff) could actually be a valid address, so consider changing + * ADDR_INVALID to NaN or null (which is also why all ADDR_INVALID tests should use strict equality + * operators). + * + * The main reason I'm NOT using NaN or null now is my concern that, by mixing non-numbers + * (specifically, values outside the range of signed 32-bit integers), performance may suffer. + * + * WARNING: Like many of the properties defined here, ADDR_INVALID is a common constant, which the + * Closure Compiler will happily inline (with or without @const annotations; in fact, I've yet to + * see a @const annotation EVER improve automatic inlining). However, if you don't make ABSOLUTELY + * certain that this file is included BEFORE the first reference to any of these properties, that + * automatic inlining will no longer occur. + */ + ADDR_INVALID: -1, + + /* + * Processor Status flag definitions (stored in regPS) + */ + PS: { + SF: 0x80, // this.BIT_PN = 0x80; // N = sign + OF: 0x40, // this.BIT_PV = 0x40; // V = overflow + BF: 0x10, // this.BIT_PB = 0x10; // B = break + DF: 0x08, // this.BIT_PD = 0x08; // D = decimal + IF: 0x04, // this.BIT_PI = 0x04; // I = interrupt + ZF: 0x02, // this.BIT_PZ = 0x02; // Z = zero + CF: 0x01 // this.BIT_PC = 0x01; // C = carry + }, + VECTOR: { + NMI: 0xFFFA, // this.VECTOR_NMI = 0xfffa; + RESET: 0xFFFC, // this.VECTOR_RESET = 0xfffc; + IRQ: 0xFFFE // this.VECTOR_IRQ = 0xfffe; + }, + /* + * Opcode definitions + */ + OPCODE: { + JSR: 0x20, + /* + * opSim operation codes + */ + SIM: 0x02, + SIMOP_HLT: 0x00, + SIMOP_MSG: 0x01 + // to be continued.... + } +}; + +if (NODE) module.exports = CPUDef; diff --git a/modules/pc6502/lib/cpuops.js b/modules/pc6502/lib/cpuops.js new file mode 100644 index 000000000..de4f832d5 --- /dev/null +++ b/modules/pc6502/lib/cpuops.js @@ -0,0 +1,2208 @@ +/** + * @fileoverview Implements PC6502 opcode handlers. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var Messages = require("./messages"); + var CPUDef = require("./CPUDef"); +} + +/** + * @this {CPUState} + */ +CPUDef.opBRK = function() +{ // opcode 0x00 + // PC++; + this.regPC++; + // STACK(S--) = PCH; + this.abMem[this.regS--] = (this.regPC >> 8); + this.regS |= 0x100; + // STACK(S--) = PCL; + this.abMem[this.regS--] = (this.regPC & 0xff); + this.regS |= 0x100; + // B = 1; + this.regP |= 0x10; + // C = LAZY_C; Z = LAZY_Z; V = LAZY_V; N = LAZY_N; + this.regP = this.getRegP(); + // STACK(S--) = P; + this.abMem[this.regS--] = this.regP; + this.regS |= 0x100; + // B = 0; + this.regP &= 0xef; + // EA = 0xFFFE; + this.regEA = 0xFFFE; + // PC = M; + this.regPC = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAindx = function() +{ // opcode 0x01 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAzp = function() +{ // opcode 0x05 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opASLzp = function() +{ // opcode 0x06 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // RC = ML << 1; + this.regRC = this.abMem[this.regEAWrite] << 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opPHP = function() +{ // opcode 0x08 + this.regP = this.getRegP(); + // STACK(S--) = P; + this.abMem[this.regS--] = this.regP; + this.regS |= 0x100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAimm = function() +{ // opcode 0x09 + // EA = PC++; + this.regEA = this.regPC++; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opASLacc = function() +{ // opcode 0x0a + // RC = A << 1; + this.regRC = this.regA << 1; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAabs = function() +{ // opcode 0x0d + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opASLabs = function() +{ // opcode 0x0e + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = ML << 1; + this.regRC = this.abMem[this.regEAWrite] << 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBPL = function() +{ // opcode 0x10 + // PC = PC + (LAZY_N == 0? SBYTE(PC) : 0) + 1; + this.regPC += (!(this.regRN & 0x80)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAindy = function() +{ // opcode 0x11 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAzpx = function() +{ // opcode 0x15 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA |= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opASLzpx = function() +{ // opcode 0x16 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RC = ML << 1; + this.regRC = this.abMem[this.regEAWrite] << 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opCLC = function() +{ // opcode 0x18 + // SET_LAZY_C(0); + this.regRC = 0x00; +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAabsy = function() +{ // opcode 0x19 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA |= this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opORAabsx = function() +{ // opcode 0x1d + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = A | ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA |= this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opASLabsx = function() +{ // opcode 0x1e + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RC = ML << 1; + this.regRC = this.abMem[this.regEAWrite] << 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opJSRabs = function() +{ // opcode 0x20 + // EA = PC; PC += 1; + this.regEA = this.regPC++; + // STACK(S--) = PCH; + this.abMem[this.regS--] = (this.regPC >> 8); + this.regS |= 0x100; + // STACK(S--) = PCL; + this.abMem[this.regS--] = (this.regPC & 0xff); + this.regS |= 0x100; + // PC = M; + this.regPC = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDindx = function() +{ // opcode 0x21 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opBITzp = function() +{ // opcode 0x24 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // SET_LAZY_Z((A & ML) == 0); + this.regRZ = (this.regA & this.abMem[this.regEA]); + // SET_LAZY_N(ML7); + this.regRN = ((this.regRN & 0x7f) | (this.abMem[this.regEA] & 0x80)); + // SET_LAZY_V(ML6); + this.regRV = 0; this.regRU = ((this.abMem[this.regEA] & 0x40)? 0x80 : 0x00); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDzp = function() +{ // opcode 0x25 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opROLzp = function() +{ // opcode 0x26 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RC = RC << 1; + this.regRC <<= 1; + // RCL0 = RCH1; + this.regRC = ((this.regRC & 0xfffe) | (((this.regRC & 0x0200))? 0x0001 : 0)); + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opPLP = function() +{ // opcode 0x28 + // P = STACK(++S); + this.regS = ((this.regS+1) & 0xff) | 0x100; + this.regP = this.abMem[this.regS]; + // SET_LAZY_C(C); + this.regRC = ((this.regP & 0x01)? 0x0100 : 0); + // SET_LAZY_Z(Z); + this.regRZ = (!(this.regP & 0x02)? 0x01 : 0); + // SET_LAZY_N(N); + this.regRN = (this.regP & 0x80); + // SET_LAZY_V(V); + this.regRV = 0; this.regRU = ((this.regP & 0x40)? 0x80 : 0x00); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDimm = function() +{ // opcode 0x29 + // EA = PC++; + this.regEA = this.regPC++; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opROLacc = function() +{ // opcode 0x2a + // RCL = A; + this.regRC = ((this.regRC & 0xff00) | this.regA); + // RC = RC << 1; + this.regRC <<= 1; + // RCL0 = RCH1; + this.regRC = ((this.regRC & 0xfffe) | ((this.regRC & 0x0200)? 0x0001 : 0)); + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opBITabs = function() +{ // opcode 0x2c + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // SET_LAZY_Z((A & ML) == 0); + this.regRZ = (this.regA & this.abMem[this.regEA]); + // SET_LAZY_N(ML7); + this.regRN = ((this.regRN & 0x7f) | (this.abMem[this.regEA] & 0x80)); + // SET_LAZY_V(ML6); + this.regRV = 0; this.regRU = ((this.abMem[this.regEA] & 0x40)? 0x80 : 0x00); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDabs = function() +{ // opcode 0x2d + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opROLabs = function() +{ // opcode 0x2e + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RC = RC << 1; + this.regRC <<= 1; + // RCL0 = RCH1; + this.regRC = ((this.regRC & 0xfffe) | (((this.regRC & 0x0200))? 0x0001 : 0)); + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBMI = function() +{ // opcode 0x30 + // PC = PC + (LAZY_N != 0? SBYTE(PC) : 0) + 1; + this.regPC += ((this.regRN & 0x80)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDindy = function() +{ // opcode 0x31 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDzpx = function() +{ // opcode 0x35 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opROLzpx = function() +{ // opcode 0x36 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RC = RC << 1; + this.regRC <<= 1; + // RCL0 = RCH1; + this.regRC = ((this.regRC & 0xfffe) | (((this.regRC & 0x0200))? 0x0001 : 0)); + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSEC = function() +{ // opcode 0x38 + // SET_LAZY_C(1); + this.regRC = 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDabsy = function() +{ // opcode 0x39 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opANDabsx = function() +{ // opcode 0x3d + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = A & ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA &= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opROLabsx = function() +{ // opcode 0x3e + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RC = RC << 1; + this.regRC <<= 1; + // RCL0 = RCH1; + this.regRC = ((this.regRC & 0xfffe) | (((this.regRC & 0x0200))? 0x0001 : 0)); + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opRTI = function() +{ // opcode 0x40 + // P = STACK(++S); + this.regS = ((this.regS+1) & 0xff) | 0x100; + this.regP = this.abMem[this.regS]; + // SET_LAZY_C(C); + this.regRC = ((this.regP & 0x01)? 0x0100 : 0); + // SET_LAZY_Z(Z); + this.regRZ = (!(this.regP & 0x02)? 0x01 : 0); + // SET_LAZY_N(N); + this.regRN = (this.regP & 0x80); + // SET_LAZY_V(V); + this.regRV = 0; this.regRU = ((this.regP & 0x40)? 0x80 : 0x00); + // PCL = STACK(++S); + // PCH = STACK(++S); + this.regS = ((this.regS+2) & 0xff) | 0x100; + this.regPC = (this.abMem[(this.regS-1) | 0x100]) | (this.abMem[this.regS] << 8); +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORindx = function() +{ // opcode 0x41 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORzp = function() +{ // opcode 0x45 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opLSRzp = function() +{ // opcode 0x46 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // SET_LAZY_C(ML0); + this.regRC = ((this.regRC & 0xfeff) | ((this.abMem[this.regEAWrite] & 0x01)? 0x0100 : 0)); + // ML = RCL = ML >> 1; + this.abMem[this.regEAWrite] = ((this.regRC = ((this.regRC & 0xff00) | (this.abMem[this.regEAWrite] >> 1))) & 0xff); + // SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opPHA = function() +{ // opcode 0x48 + // STACK(S--) = A; + this.abMem[this.regS--] = this.regA; + this.regS |= 0x100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORimm = function() +{ // opcode 0x49 + // EA = PC++; + this.regEA = this.regPC++; + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opLSRacc = function() +{ // opcode 0x4a + // SET_LAZY_C( A0); + this.regRC = ((this.regRC & 0xfeff) | ((this.regA & 0x01)? 0x0100 : 0)); + // A = RCL = A >> 1; + this.regA = ((this.regRC = ((this.regRC & 0xff00) | (this.regA >> 1))) & 0xff); + // SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opJMPimm16 = function() +{ // opcode 0x4c + // EA = PC; + this.regEA = this.regPC; + // PC += 2; + // this.regPC += 2; + // PC = M; + this.regPC = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORabs = function() +{ // opcode 0x4d + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opLSRabs = function() +{ // opcode 0x4e + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // SET_LAZY_C(ML0); + this.regRC = ((this.regRC & 0xfeff) | ((this.abMem[this.regEAWrite] & 0x01)? 0x0100 : 0)); + // ML = RCL = ML >> 1; + this.abMem[this.regEAWrite] = ((this.regRC = ((this.regRC & 0xff00) | (this.abMem[this.regEAWrite] >> 1))) & 0xff); + // SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBVC = function() +{ // opcode 0x50 + // PC = PC + (LAZY_V == 0? SBYTE(PC) : 0) + 1; + this.regPC += (!((((this.regRV & 0xff) ^ this.regRU) ^ (this.regRV >> 1)) & 0x80)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORindy = function() +{ // opcode 0x51 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = this.abMem[this.regPC++]; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORzpx = function() +{ // opcode 0x55 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opLSRzpx = function() +{ // opcode 0x56 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // SET_LAZY_C(ML0); + this.regRC = ((this.regRC & 0xfeff) | ((this.abMem[this.regEAWrite] & 0x01)? 0x0100 : 0)); + // ML = RCL = ML >> 1; + this.abMem[this.regEAWrite] = ((this.regRC = ((this.regRC & 0xff00) | (this.abMem[this.regEAWrite] >> 1))) & 0xff); + // SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opCLI = function() +{ // opcode 0x58 + // I = 0; + this.regP &= 0xfb; +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORabsy = function() +{ // opcode 0x59 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = A ^ ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opEORabsx = function() +{ // opcode 0x5d + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = A ^ ML; SET_LAZY_NZ(A) + this.regRN = this.regRZ = (this.regA ^= this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opLSRabsx = function() +{ // opcode 0x5e + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // SET_LAZY_C(ML0); + this.regRC = ((this.regRC & 0xfeff) | ((this.abMem[this.regEAWrite] & 0x01)? 0x0100 : 0)); + // ML = RCL = ML >> 1; + this.abMem[this.regEAWrite] = ((this.regRC = ((this.regRC & 0xff00) | (this.abMem[this.regEAWrite] >> 1))) & 0xff); + // SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opRTS = function() +{ // opcode 0x60 + // PCL = STACK(++S); + // PCH = STACK(++S); + // PC++; + this.regS = ((this.regS+2) & 0xff) | 0x100; + this.regPC = (((this.abMem[(this.regS-1) | 0x100])) | ((this.abMem[this.regS]) << 8)) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCindx = function() +{ // opcode 0x61 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCindxBCD = function() +{ // opcode 0x61 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCzp = function() +{ // opcode 0x65 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCzpBCD = function() +{ // opcode 0x65 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opRORzp = function() +{ // opcode 0x66 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RCH1 = RCL0; + this.regRC = ((this.regRC & 0xfdff) | ((this.regRC & 0x0001)? 0x0200 : 0)); + // RC = RC >> 1; + this.regRC >>= 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opPLA = function() +{ // opcode 0x68 + // A = STACK(++S); SET_LAZY_NZ(A); + this.regS = ((this.regS+1) & 0xff) | 0x100; + this.regRN = this.regRZ = this.regA = this.abMem[this.regS]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCimm = function() +{ // opcode 0x69 + // EA = PC++; + this.regEA = this.regPC++; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCimmBCD = function() +{ // opcode 0x69 + // EA = PC++; + this.regEA = this.regPC++; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opRORacc = function() +{ // opcode 0x6a + // RCL = A; + this.regRC = ((this.regRC & 0xff00) | this.regA); + // RCH1 = RCL0; + this.regRC = ((this.regRC & 0xfdff) | ((this.regRC & 0x0001)? 0x0200 : 0)); + // RC = RC >> 1; + this.regRC >>= 1; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + * + * NOTE from Wikipedia: "The 6502's memory indirect jump instruction, JMP (
), is partially broken. + * If
is hex xxFF (i.e., any word ending in FF), the processor will not jump to the address stored in xxFF and xxFF+1 as expected, + * but rather the one defined by xxFF and xx00. This defect continued through the entire NMOS line, but was corrected in the CMOS derivatives." + */ +CPUDef.opJMPabs16 = function() +{ // opcode 0x6c + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // PC = M; + this.regPC = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabs = function() +{ // opcode 0x6d + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = (A + ML + LAZY_C); + this.regRC =(this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabsBCD = function() +{ // opcode 0x6d + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opRORabs = function() +{ // opcode 0x6e + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RCH1 = RCL0; + this.regRC = ((this.regRC & 0xfdff) | ((this.regRC & 0x0001)? 0x0200 : 0)); + // RC = RC >> 1; + this.regRC >>= 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBVS = function() +{ // opcode 0x70 + // PC = PC + (LAZY_V != 0? SBYTE(PC) : 0) + 1; + this.regPC += (((((this.regRV & 0xff) ^ this.regRU) ^ (this.regRV >> 1)) & 0x80)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCindy = function() +{ // opcode 0x71 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCindyBCD = function() +{ // opcode 0x71 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCzpx = function() +{ // opcode 0x75 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCzpxBCD = function() +{ // opcode 0x75 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opRORzpx = function() +{ // opcode 0x76 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RCH1 = RCL0; + this.regRC = ((this.regRC & 0xfdff) | ((this.regRC & 0x0001)? 0x0200 : 0)); + // RC = RC >> 1; + this.regRC >>= 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSEI = function() +{ // opcode 0x78 + // I = 1; + this.regP |= 0x04; +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabsy = function() +{ // opcode 0x79 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabsyBCD = function() +{ // opcode 0x79 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabsx = function() +{ // opcode 0x7d + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RC = (A + ML + LAZY_C); + this.regRC = (this.regA + this.abMem[this.regEA] + ((this.regRC & 0x0100)? 1 : 0)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opADCabsxBCD = function() +{ // opcode 0x7d + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = this.addBCD(A,ML); + this.regA = this.addBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opRORabsx = function() +{ // opcode 0x7e + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RCL = ML; + this.regRC = ((this.regRC & 0xff00) | this.abMem[this.regEAWrite]); + // RCH1 = RCL0; + this.regRC = ((this.regRC & 0xfdff) | ((this.regRC & 0x0001)? 0x0200 : 0)); + // RC = RC >> 1; + this.regRC >>= 1; + // ML = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = (this.regRC & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAindx = function() +{ // opcode 0x81 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEAWrite = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEAWrite = (this.abMem[this.regEAWrite] | (this.abMem[this.regEAWrite+1] << 8)); + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTYzp = function() +{ // opcode 0x84 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // ML = Y; + this.abMem[this.regEAWrite] = this.regY; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAzp = function() +{ // opcode 0x85 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTXzp = function() +{ // opcode 0x86 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // ML = X; + this.abMem[this.regEAWrite] = this.regX; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opDEY = function() +{ // opcode 0x88 + // Y = ((Y - 1) & 0xff); + this.regY = ((this.regY - 1) & 0xff); + // SET_LAZY_NZ(Y); + this.regRN = this.regRZ = (this.regY); +}; + +/** + * @this {CPUState} + */ +CPUDef.opTXA = function() +{ // opcode 0x8a + // A = X; SET_LAZY_NZ(X); + this.regRN = this.regRZ = this.regA = this.regX; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTYabs = function() +{ // opcode 0x8c + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // ML = Y; + this.abMem[this.regEAWrite] = this.regY; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAabs = function() +{ // opcode 0x8d + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTXabs = function() +{ // opcode 0x8e + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // ML = X; + this.abMem[this.regEAWrite] = this.regX; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBCC = function() +{ // opcode 0x90 + // PC = PC + (LAZY_C == 0? SBYTE(PC) : 0) + 1; + this.regPC += (!(this.regRC & 0x0100)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAindy = function() +{ // opcode 0x91 + // EA = WORD(BYTE(PC++))+Y; + this.regEAWrite = (this.abMem[this.regPC++]); + this.regEAWrite = (this.abMem[this.regEAWrite] | (this.abMem[this.regEAWrite+1] << 8)) + this.regY; + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTYzpx = function() +{ // opcode 0x94 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // ML = Y; + this.abMem[this.regEAWrite] = this.regY; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAzpx = function() +{ // opcode 0x95 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTXzpy = function() +{ // opcode 0x96 + // EA = (BYTE(PC++)+Y) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regY) & 0xff; + // ML = X; + this.abMem[this.regEAWrite] = this.regX; + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opTYA = function() +{ // opcode 0x98 + // A = Y; SET_LAZY_NZ(Y); + this.regRN = this.regRZ = this.regA = this.regY; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAabsy = function() +{ // opcode 0x99 + // EA = WORD(PC)+Y; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opTXS = function() +{ // opcode 0x9a + // S = X; + this.regS = this.regX | 0x100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSTAabsx = function() +{ // opcode 0x9d + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // ML = A; + this.abMem[this.regEAWrite] = this.regA; + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDYimm = function() +{ // opcode 0xa0 + // EA = PC++; + this.regEA = this.regPC++; + // Y = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regY = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAindx = function() +{ // opcode 0xa1 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDXimm = function() +{ // opcode 0xa2 + // EA = PC++; + this.regEA = this.regPC++; + // X = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regX = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDYzp = function() +{ // opcode 0xa4 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // Y = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regY = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAzp = function() +{ // opcode 0xa5 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDXzp = function() +{ // opcode 0xa6 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // X = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regX = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opTAY = function() +{ // opcode 0xa8 + // Y = A; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regY = this.regA; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAimm = function() +{ // opcode 0xa9 + // EA = PC++; + this.regEA = this.regPC++; + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opTAX = function() +{ // opcode 0xaa + // X = A; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regX = this.regA; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDYabs = function() +{ // opcode 0xac + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // Y = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regY = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAabs = function() +{ // opcode 0xad + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDXabs = function() +{ // opcode 0xae + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // X = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regX = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBCS = function() +{ // opcode 0xb0 + // PC = PC + (LAZY_C != 0? SBYTE(PC) : 0) + 1; + this.regPC += ((this.regRC & 0x0100)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAindy = function() +{ // opcode 0xb1 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDYzpx = function() +{ // opcode 0xb4 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // Y = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regY = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAzpx = function() +{ // opcode 0xb5 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDXzpy = function() +{ // opcode 0xb6 + // EA = (BYTE(PC++)+Y) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regY) & 0xff; + // X = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regX = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCLV = function() +{ // opcode 0xb8 + // SET_LAZY_V(0); + this.regRV = 0; this.regRU = 0; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAabsy = function() +{ // opcode 0xb9 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opTSX = function() +{ // opcode 0xba + // X = S; SET_LAZY_NZ(S); + this.regRN = this.regRZ = this.regX = this.regS & 0xff; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDYabsx = function() +{ // opcode 0xbc + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // Y = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regY = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDAabsx = function() +{ // opcode 0xbd + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = ML; SET_LAZY_NZ(A); + this.regRN = this.regRZ = this.regA = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opLDXabsy = function() +{ // opcode 0xbe + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // X = ML; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.regX = this.abMem[this.regEA]; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPYimm = function() +{ // opcode 0xc0 + // EA = PC++; + this.regEA = this.regPC++; + // RC = Y - ML; + this.regRC = this.regY - this.abMem[this.regEA]; + // SET_LAZY_NZ(RC); + this.regRN = this.regRZ = (this.regRC); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPindx = function() +{ // opcode 0xc1 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPYzp = function() +{ // opcode 0xc4 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // RC = Y - ML; + this.regRC = this.regY - this.abMem[this.regEA]; + // SET_LAZY_NZ(RC); + this.regRN = this.regRZ = (this.regRC); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPzp = function() +{ // opcode 0xc5 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opDECzp = function() +{ // opcode 0xc6 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // ML = ML - 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] - 1) & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opINY = function() +{ // opcode 0xc8 + // Y = ((Y + 1) & 0xff); + this.regY = ((this.regY + 1) & 0xff); + // SET_LAZY_NZ(Y); + this.regRN = this.regRZ = (this.regY); +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPimm = function() +{ // opcode 0xc9 + // EA = PC++; + this.regEA = this.regPC++; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opDEX = function() +{ // opcode 0xca + // X = ((X - 1) & 0xff); SET_LAZY_NZ(X); + this.regRN = this.regRZ = this.regX = ((this.regX - 1) & 0xff); +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPYabs = function() +{ // opcode 0xcc + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = Y - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = this.regY - this.abMem[this.regEA]; + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPabs = function() +{ // opcode 0xcd + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opDECabs = function() +{ // opcode 0xce + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // ML = ML - 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] - 1) & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBNE = function() +{ // opcode 0xd0 + // PC = PC + (LAZY_Z == 0? SBYTE(PC) : 0) + 1; + this.regPC += ((this.regRZ & 0xff)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPindy = function() +{ // opcode 0xd1 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPzpx = function() +{ // opcode 0xd5 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opDECzpx = function() +{ // opcode 0xd6 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // ML = ML - 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] - 1) & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opCLD = function() +{ // opcode 0xd8 + // D = 0; + this.clearBCD(); +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPabsy = function() +{ // opcode 0xd9 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCMPabsx = function() +{ // opcode 0xdd + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RC = A - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = (this.regA - this.abMem[this.regEA]); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opDECabsx = function() +{ // opcode 0xde + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // ML = ML - 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] - 1) & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPXimm = function() +{ // opcode 0xe0 + // EA = PC++; + this.regEA = this.regPC++; + // RC = X - ML; + this.regRC = this.regX - this.abMem[this.regEA]; + // SET_LAZY_NZ(RC); + this.regRN = this.regRZ = (this.regRC); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCindx = function() +{ // opcode 0xe1 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCindxBCD = function() +{ // opcode 0xe1 + // EA = WORD((BYTE(PC++)+X) & 0xff); + this.regEA = ((this.abMem[this.regPC++]) + this.regX) & 0xff; + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)); + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPXzp = function() +{ // opcode 0xe4 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // RC = X - ML; + this.regRC = this.regX - this.abMem[this.regEA]; + // SET_LAZY_NZ(RC); + this.regRN = this.regRZ = (this.regRC); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCzp = function() +{ // opcode 0xe5 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCzpBCD = function() +{ // opcode 0xe5 + // EA = BYTE(PC++); + this.regEA = this.abMem[this.regPC++]; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opINCzp = function() +{ // opcode 0xe6 + // EA = BYTE(PC++); + this.regEAWrite = this.abMem[this.regPC++]; + // ML = ML + 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] + 1) & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opINX = function() +{ // opcode 0xe8 + // X = ((X + 1) & 0xff); + this.regX = ((this.regX + 1) & 0xff); + // SET_LAZY_NZ(X); + this.regRN = this.regRZ = (this.regX); +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCimm = function() +{ // opcode 0xe9 + // EA = PC++; + this.regEA = this.regPC++; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCimmBCD = function() +{ // opcode 0xe9 + // EA = PC++; + this.regEA = this.regPC++; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opNOP = function() +{ // opcode 0xea + // +}; + +/** + * @this {CPUState} + */ +CPUDef.opCPXabs = function() +{ // opcode 0xec + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = X - ML; SET_LAZY_NZ(RC); + this.regRN = this.regRZ = this.regRC = this.regX - this.abMem[this.regEA]; + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabs = function() +{ // opcode 0xed + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabsBCD = function() +{ // opcode 0xed + // EA = WORD(PC); PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opINCabs = function() +{ // opcode 0xee + // EA = WORD(PC); PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + // ML = ML + 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] + 1) & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opBEQ = function() +{ // opcode 0xf0 + // PC = PC + (LAZY_Z == 1? SBYTE(PC) : 0) + 1; + this.regPC += (!(this.regRZ & 0xff)? (this.nStepCycles--,((this.abMem[this.regPC] << 24) >> 24)) : 0) + 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCindy = function() +{ // opcode 0xf1 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCindyBCD = function() +{ // opcode 0xf1 + // EA = WORD(BYTE(PC++))+Y; + this.regEA = (this.abMem[this.regPC++]); + this.regEA = (this.abMem[this.regEA] | (this.abMem[this.regEA+1] << 8)) + this.regY; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCzpx = function() +{ // opcode 0xf5 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCzpxBCD = function() +{ // opcode 0xf5 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEA = (this.abMem[this.regPC++]+this.regX) & 0xff; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opINCzpx = function() +{ // opcode 0xf6 + // EA = (BYTE(PC++)+X) & 0xff; + this.regEAWrite = (this.abMem[this.regPC++]+this.regX) & 0xff; + // ML = ML + 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] + 1) & 0xff); + // W = 1; + // NOTE: Consider alternatives for tracking zero-page writes (eg, regEAWriteZP) +}; + +/** + * @this {CPUState} + */ +CPUDef.opSED = function() +{ // opcode 0xf8 + // D = 1; + this.setBCD(); +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabsy = function() +{ // opcode 0xf9 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabsyBCD = function() +{ // opcode 0xf9 + // EA = WORD(PC)+Y; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regY; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabsx = function() +{ // opcode 0xfd + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // RC = (A - ML - !LAZY_C); + this.regRC = (this.regA - this.abMem[this.regEA] - ((this.regRC & 0x0100)? 0 : 1)); + // SET_LAZY_OV(A,ML,RC); + this.regRU = this.regA ^ this.abMem[this.regEA]; this.regRV = this.regRC; + // A = RCL; SET_LAZY_NZ(RCL); + this.regRN = this.regRZ = this.regA = (this.regRC & 0xff); + // SET_LAZY_C(!LAZY_C); + this.regRC ^= 0x0100; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSBCabsxBCD = function() +{ // opcode 0xfd + // EA = WORD(PC)+X; PC += 2; + this.regEA = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // A = this.subBCD(A,ML); + this.regA = this.subBCD(this.regA, this.abMem[this.regEA]); +}; + +/** + * @this {CPUState} + */ +CPUDef.opINCabsx = function() +{ // opcode 0xfe + // EA = WORD(PC)+X; PC += 2; + this.regEAWrite = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)) + this.regX; + // ML = ML + 1; SET_LAZY_NZ(ML); + this.regRN = this.regRZ = this.abMem[this.regEAWrite] = ((this.abMem[this.regEAWrite] + 1) & 0xff); + // W = 1; +}; + +/** + * @this {CPUState} + */ +CPUDef.opSim = function() +{ + var addr; + var bSimOp = this.abMem[this.regPC++]; + switch(bSimOp) { + + case this.SIMOP_HLT: + this.println("HALT"); + this.halt(); + break; + + case this.SIMOP_MSG: + addr = this.regPC; // currently we're using "inline" strings + // addr = (this.abMem[this.regPC++] | (this.abMem[this.regPC++] << 8)); + var s = ""; + while (addr < this.abMem.length) { + var b = this.abMem[addr++]; + if (!b) break; + s += String.fromCharCode(b); + } + this.regPC = addr; // update regPC as long as we're doing "inline" strings + /* + * Before simply printing the string, what kinds of handy substitutions should we provide? + * + * eg: %A for this.regA, %X for this.regX, etc + */ + s = s.replace(/%A/g, str.toHex(this.regA, 2)).replace(/%X/g, str.toHex(this.regX, 2)).replace(/%Y/g, str.toHex(this.regY, 2)); + this.println(s); + /* + * To make printing "smoother", let's force a yield + */ + this.yieldCPU(); + break; + + default: + this.regPC -= 2; + this.println("undefined opSim: " + str.toHexByte(bSimOp) + " at " + str.toHexWord(this.regPC)); + this.halt(); + } +}; + +/** + * @this {CPUState} + */ +CPUDef.opUndefined = function() +{ + var b = this.abMem[--this.regPC]; + this.println("undefined opcode: " + str.toHexByte(b) + " at " + str.toHexWord(this.regPC)); + this.halt(); +}; diff --git a/modules/pc6502/lib/cpustate.js b/modules/pc6502/lib/cpustate.js new file mode 100644 index 000000000..8cc495f3b --- /dev/null +++ b/modules/pc6502/lib/cpustate.js @@ -0,0 +1,1434 @@ +/** + * @fileoverview Implements the PC6502 CPU component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var Messages = require("./messages"); + var Memory = require("./memory"); + var State = require("./state"); + var CPU = require("./cpu"); + var CPUDef = require("./cpudef"); +} + +/** + * CPUState(parmsCPU) + * + * The CPUState class uses the following (parmsCPU) properties: + * + * model: a string (eg, "8080") that should match one of the CPUDef.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 CPUState 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 CPU + * @param {Object} parmsCPU + */ +function CPUState(parmsCPU) +{ + this.model = +parmsCPU['model'] || CPUDef.MODEL_8080; + + var nCyclesDefault = 0; + switch(this.model) { + case CPUDef.MODEL_8080: + default: + nCyclesDefault = 1000000; + break; + } + + CPU.call(this, parmsCPU, nCyclesDefault); + + /* + * Initialize processor operation to match the requested model + */ + 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.fComplete = this.flags.fDebugCheck = 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(CPUState, CPU); + +/** + * addHaltCheck(fn) + * + * Records a function that will be called during HLT opcode processing. + * + * @this {CPUState} + * @param {function(number)} fn + */ +CPUState.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 {CPUState} + */ +CPUState.prototype.initProcessor = function() +{ + /* + * This 256-entry array of opcode functions is at the heart of the CPU engine: step(n). + * + * It might be worth trying a switch() statement instead, to see how the performance compares, + * but I suspect that will vary quite a bit across JavaScript engines; for now, I'm putting my + * money on array lookup. + */ + this.aOpcodeFuncs = [ + CPUDef.opBRK, // 0x00 + CPUDef.opORAindx, // 0x01 + CPUDef.opSim, // 0x02 + CPUDef.opUndefined, // 0x03 + CPUDef.opUndefined, // 0x04 + CPUDef.opORAzp, // 0x05 + CPUDef.opASLzp, // 0x06 + CPUDef.opUndefined, // 0x07 + CPUDef.opPHP, // 0x08 + CPUDef.opORAimm, // 0x09 + CPUDef.opASLacc, // 0x0a + CPUDef.opUndefined, // 0x0b + CPUDef.opUndefined, // 0x0c + CPUDef.opORAabs, // 0x0d + CPUDef.opASLabs, // 0x0e + CPUDef.opUndefined, // 0x0f + CPUDef.opBPL, // 0x10 + CPUDef.opORAindy, // 0x11 + CPUDef.opUndefined, // 0x12 + CPUDef.opUndefined, // 0x13 + CPUDef.opUndefined, // 0x14 + CPUDef.opORAzpx, // 0x15 + CPUDef.opASLzpx, // 0x16 + CPUDef.opUndefined, // 0x17 + CPUDef.opCLC, // 0x18 + CPUDef.opORAabsy, // 0x19 + CPUDef.opUndefined, // 0x1a + CPUDef.opUndefined, // 0x1b + CPUDef.opUndefined, // 0x1c + CPUDef.opORAabsx, // 0x1d + CPUDef.opASLabsx, // 0x1e + CPUDef.opUndefined, // 0x1f + CPUDef.opJSRabs, // 0x20 + CPUDef.opANDindx, // 0x21 + CPUDef.opUndefined, // 0x22 + CPUDef.opUndefined, // 0x23 + CPUDef.opBITzp, // 0x24 + CPUDef.opANDzp, // 0x25 + CPUDef.opROLzp, // 0x26 + CPUDef.opUndefined, // 0x27 + CPUDef.opPLP, // 0x28 + CPUDef.opANDimm, // 0x29 + CPUDef.opROLacc, // 0x2a + CPUDef.opUndefined, // 0x2b + CPUDef.opBITabs, // 0x2c + CPUDef.opANDabs, // 0x2d + CPUDef.opROLabs, // 0x2e + CPUDef.opUndefined, // 0x2f + CPUDef.opBMI, // 0x30 + CPUDef.opANDindy, // 0x31 + CPUDef.opUndefined, // 0x32 + CPUDef.opUndefined, // 0x33 + CPUDef.opUndefined, // 0x34 + CPUDef.opANDzpx, // 0x35 + CPUDef.opROLzpx, // 0x36 + CPUDef.opUndefined, // 0x37 + CPUDef.opSEC, // 0x38 + CPUDef.opANDabsy, // 0x39 + CPUDef.opUndefined, // 0x3a + CPUDef.opUndefined, // 0x3b + CPUDef.opUndefined, // 0x3c + CPUDef.opANDabsx, // 0x3d + CPUDef.opROLabsx, // 0x3e + CPUDef.opUndefined, // 0x3f + CPUDef.opRTI, // 0x40 + CPUDef.opEORindx, // 0x41 + CPUDef.opUndefined, // 0x42 + CPUDef.opUndefined, // 0x43 + CPUDef.opUndefined, // 0x44 + CPUDef.opEORzp, // 0x45 + CPUDef.opLSRzp, // 0x46 + CPUDef.opUndefined, // 0x47 + CPUDef.opPHA, // 0x48 + CPUDef.opEORimm, // 0x49 + CPUDef.opLSRacc, // 0x4a + CPUDef.opUndefined, // 0x4b + CPUDef.opJMPimm16, // 0x4c + CPUDef.opEORabs, // 0x4d + CPUDef.opLSRabs, // 0x4e + CPUDef.opUndefined, // 0x4f + CPUDef.opBVC, // 0x50 + CPUDef.opEORindy, // 0x51 + CPUDef.opUndefined, // 0x52 + CPUDef.opUndefined, // 0x53 + CPUDef.opUndefined, // 0x54 + CPUDef.opEORzpx, // 0x55 + CPUDef.opLSRzpx, // 0x56 + CPUDef.opUndefined, // 0x57 + CPUDef.opCLI, // 0x58 + CPUDef.opEORabsy, // 0x59 + CPUDef.opUndefined, // 0x5a + CPUDef.opUndefined, // 0x5b + CPUDef.opUndefined, // 0x5c + CPUDef.opEORabsx, // 0x5d + CPUDef.opLSRabsx, // 0x5e + CPUDef.opUndefined, // 0x5f + CPUDef.opRTS, // 0x60 + CPUDef.opADCindx, // 0x61 + CPUDef.opUndefined, // 0x62 + CPUDef.opUndefined, // 0x63 + CPUDef.opUndefined, // 0x64 + CPUDef.opADCzp, // 0x65 + CPUDef.opRORzp, // 0x66 + CPUDef.opUndefined, // 0x67 + CPUDef.opPLA, // 0x68 + CPUDef.opADCimm, // 0x69 + CPUDef.opRORacc, // 0x6a + CPUDef.opUndefined, // 0x6b + CPUDef.opJMPabs16, // 0x6c + CPUDef.opADCabs, // 0x6d + CPUDef.opRORabs, // 0x6e + CPUDef.opUndefined, // 0x6f + CPUDef.opBVS, // 0x70 + CPUDef.opADCindy, // 0x71 + CPUDef.opUndefined, // 0x72 + CPUDef.opUndefined, // 0x73 + CPUDef.opUndefined, // 0x74 + CPUDef.opADCzpx, // 0x75 + CPUDef.opRORzpx, // 0x76 + CPUDef.opUndefined, // 0x77 + CPUDef.opSEI, // 0x78 + CPUDef.opADCabsy, // 0x79 + CPUDef.opUndefined, // 0x7a + CPUDef.opUndefined, // 0x7b + CPUDef.opUndefined, // 0x7c + CPUDef.opADCabsx, // 0x7d + CPUDef.opRORabsx, // 0x7e + CPUDef.opUndefined, // 0x7f + CPUDef.opUndefined, // 0x80 + CPUDef.opSTAindx, // 0x81 + CPUDef.opUndefined, // 0x82 + CPUDef.opUndefined, // 0x83 + CPUDef.opSTYzp, // 0x84 + CPUDef.opSTAzp, // 0x85 + CPUDef.opSTXzp, // 0x86 + CPUDef.opUndefined, // 0x87 + CPUDef.opDEY, // 0x88 + CPUDef.opUndefined, // 0x89 + CPUDef.opTXA, // 0x8a + CPUDef.opUndefined, // 0x8b + CPUDef.opSTYabs, // 0x8c + CPUDef.opSTAabs, // 0x8d + CPUDef.opSTXabs, // 0x8e + CPUDef.opUndefined, // 0x8f + CPUDef.opBCC, // 0x90 + CPUDef.opSTAindy, // 0x91 + CPUDef.opUndefined, // 0x92 + CPUDef.opUndefined, // 0x93 + CPUDef.opSTYzpx, // 0x94 + CPUDef.opSTAzpx, // 0x95 + CPUDef.opSTXzpy, // 0x96 + CPUDef.opUndefined, // 0x97 + CPUDef.opTYA, // 0x98 + CPUDef.opSTAabsy, // 0x99 + CPUDef.opTXS, // 0x9a + CPUDef.opUndefined, // 0x9b + CPUDef.opUndefined, // 0x9c + CPUDef.opSTAabsx, // 0x9d + CPUDef.opUndefined, // 0x9e + CPUDef.opUndefined, // 0x9f + CPUDef.opLDYimm, // 0xa0 + CPUDef.opLDAindx, // 0xa1 + CPUDef.opLDXimm, // 0xa2 + CPUDef.opUndefined, // 0xa3 + CPUDef.opLDYzp, // 0xa4 + CPUDef.opLDAzp, // 0xa5 + CPUDef.opLDXzp, // 0xa6 + CPUDef.opUndefined, // 0xa7 + CPUDef.opTAY, // 0xa8 + CPUDef.opLDAimm, // 0xa9 + CPUDef.opTAX, // 0xaa + CPUDef.opUndefined, // 0xab + CPUDef.opLDYabs, // 0xac + CPUDef.opLDAabs, // 0xad + CPUDef.opLDXabs, // 0xae + CPUDef.opUndefined, // 0xaf + CPUDef.opBCS, // 0xb0 + CPUDef.opLDAindy, // 0xb1 + CPUDef.opUndefined, // 0xb2 + CPUDef.opUndefined, // 0xb3 + CPUDef.opLDYzpx, // 0xb4 + CPUDef.opLDAzpx, // 0xb5 + CPUDef.opLDXzpy, // 0xb6 + CPUDef.opUndefined, // 0xb7 + CPUDef.opCLV, // 0xb8 + CPUDef.opLDAabsy, // 0xb9 + CPUDef.opTSX, // 0xba + CPUDef.opUndefined, // 0xbb + CPUDef.opLDYabsx, // 0xbc + CPUDef.opLDAabsx, // 0xbd + CPUDef.opLDXabsy, // 0xbe + CPUDef.opUndefined, // 0xbf + CPUDef.opCPYimm, // 0xc0 + CPUDef.opCMPindx, // 0xc1 + CPUDef.opUndefined, // 0xc2 + CPUDef.opUndefined, // 0xc3 + CPUDef.opCPYzp, // 0xc4 + CPUDef.opCMPzp, // 0xc5 + CPUDef.opDECzp, // 0xc6 + CPUDef.opUndefined, // 0xc7 + CPUDef.opINY, // 0xc8 + CPUDef.opCMPimm, // 0xc9 + CPUDef.opDEX, // 0xca + CPUDef.opUndefined, // 0xcb + CPUDef.opCPYabs, // 0xcc + CPUDef.opCMPabs, // 0xcd + CPUDef.opDECabs, // 0xce + CPUDef.opUndefined, // 0xcf + CPUDef.opBNE, // 0xd0 + CPUDef.opCMPindy, // 0xd1 + CPUDef.opUndefined, // 0xd2 + CPUDef.opUndefined, // 0xd3 + CPUDef.opUndefined, // 0xd4 + CPUDef.opCMPzpx, // 0xd5 + CPUDef.opDECzpx, // 0xd6 + CPUDef.opUndefined, // 0xd7 + CPUDef.opCLD, // 0xd8 + CPUDef.opCMPabsy, // 0xd9 + CPUDef.opUndefined, // 0xda + CPUDef.opUndefined, // 0xdb + CPUDef.opUndefined, // 0xdc + CPUDef.opCMPabsx, // 0xdd + CPUDef.opDECabsx, // 0xde + CPUDef.opUndefined, // 0xdf + CPUDef.opCPXimm, // 0xe0 + CPUDef.opSBCindx, // 0xe1 + CPUDef.opUndefined, // 0xe2 + CPUDef.opUndefined, // 0xe3 + CPUDef.opCPXzp, // 0xe4 + CPUDef.opSBCzp, // 0xe5 + CPUDef.opINCzp, // 0xe6 + CPUDef.opUndefined, // 0xe7 + CPUDef.opINX, // 0xe8 + CPUDef.opSBCimm, // 0xe9 + CPUDef.opNOP, // 0xea + CPUDef.opUndefined, // 0xeb + CPUDef.opCPXabs, // 0xec + CPUDef.opSBCabs, // 0xed + CPUDef.opINCabs, // 0xee + CPUDef.opUndefined, // 0xef + CPUDef.opBEQ, // 0xf0 + CPUDef.opSBCindy, // 0xf1 + CPUDef.opUndefined, // 0xf2 + CPUDef.opUndefined, // 0xf3 + CPUDef.opUndefined, // 0xf4 + CPUDef.opSBCzpx, // 0xf5 + CPUDef.opINCzpx, // 0xf6 + CPUDef.opUndefined, // 0xf7 + CPUDef.opSED, // 0xf8 + CPUDef.opSBCabsy, // 0xf9 + CPUDef.opUndefined, // 0xfa + CPUDef.opUndefined, // 0xfb + CPUDef.opUndefined, // 0xfc + CPUDef.opSBCabsx, // 0xfd + CPUDef.opINCabsx, // 0xfe + CPUDef.opUndefined // 0xff + ]; +}; + +/** + * reset() + * + * @this {CPUState} + */ +CPUState.prototype.reset = function() +{ + if (this.flags.fRunning) 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 {CPUState} + */ +CPUState.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 = CPUDef.INTFLAG.NONE; +}; + +/** + * setReset(addr) + * + * @this {CPUState} + * @param {number} addr + */ +CPUState.prototype.setReset = function(addr) +{ + this.addrReset = addr; + this.setPC(addr); +}; + +/** + * getChecksum() + * + * @this {CPUState} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ +CPUState.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 CPUState component. + * + * @this {CPUState} + * @return {Object|null} + */ +CPUState.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 CPUState component. + * + * @this {CPUState} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ +CPUState.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 {CPUState} + * @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 + */ +CPUState.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 {CPUState} + * @return {number} + */ +CPUState.prototype.getBC = function() +{ + return (this.regB << 8) | this.regC; +}; + +/** + * setBC(w) + * + * @this {CPUState} + * @param {number} w + */ +CPUState.prototype.setBC = function(w) +{ + this.regB = (w >> 8) & 0xff; + this.regC = w & 0xff; +}; + +/** + * getDE() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getDE = function() +{ + return (this.regD << 8) | this.regE; +}; + +/** + * setDE(w) + * + * @this {CPUState} + * @param {number} w + */ +CPUState.prototype.setDE = function(w) +{ + this.regD = (w >> 8) & 0xff; + this.regE = w & 0xff; +}; + +/** + * getHL() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getHL = function() +{ + return (this.regH << 8) | this.regL; +}; + +/** + * setHL(w) + * + * @this {CPUState} + * @param {number} w + */ +CPUState.prototype.setHL = function(w) +{ + this.regH = (w >> 8) & 0xff; + this.regL = w & 0xff; +}; + +/** + * getSP() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getSP = function() +{ + return this.regSP; +}; + +/** + * setSP(off) + * + * @this {CPUState} + * @param {number} off + */ +CPUState.prototype.setSP = function(off) +{ + this.regSP = off & 0xffff; +}; + +/** + * getPC() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getPC = function() +{ + return this.regPC; +}; + +/** + * offPC() + * + * @this {CPUState} + * @param {number} off + * @return {number} + */ +CPUState.prototype.offPC = function(off) +{ + return (this.regPC + off) & 0xffff; +}; + +/** + * setPC(off) + * + * @this {CPUState} + * @param {number} off + */ +CPUState.prototype.setPC = function(off) +{ + this.regPC = off & 0xffff; +}; + +/** + * clearCF() + * + * @this {CPUState} + */ +CPUState.prototype.clearCF = function() +{ + this.resultZeroCarry &= 0xff; +}; + +/** + * getCF() + * + * @this {CPUState} + * @return {number} 0 or 1 (CPUDef.PS.CF) + */ +CPUState.prototype.getCF = function() +{ + return (this.resultZeroCarry & 0x100)? CPUDef.PS.CF : 0; +}; + +/** + * setCF() + * + * @this {CPUState} + */ +CPUState.prototype.setCF = function() +{ + this.resultZeroCarry |= 0x100; +}; + +/** + * updateCF(CF) + * + * @this {CPUState} + * @param {number} CF (0x000 or 0x100) + */ +CPUState.prototype.updateCF = function(CF) +{ + this.resultZeroCarry = (this.resultZeroCarry & 0xff) | CF; +}; + +/** + * clearPF() + * + * @this {CPUState} + */ +CPUState.prototype.clearPF = function() +{ + if (this.getPF()) this.resultParitySign ^= 0x1; +}; + +/** + * getPF() + * + * @this {CPUState} + * @return {number} 0 or CPUDef.PS.PF + */ +CPUState.prototype.getPF = function() +{ + return (CPUDef.PARITY[this.resultParitySign & 0xff])? CPUDef.PS.PF : 0; +}; + +/** + * setPF() + * + * @this {CPUState} + */ +CPUState.prototype.setPF = function() +{ + if (!this.getPF()) this.resultParitySign ^= 0x1; +}; + +/** + * clearAF() + * + * @this {CPUState} + */ +CPUState.prototype.clearAF = function() +{ + this.resultAuxOverflow = (this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); +}; + +/** + * getAF() + * + * @this {CPUState} + * @return {number} 0 or CPUDef.PS.AF + */ +CPUState.prototype.getAF = function() +{ + return ((this.resultParitySign ^ this.resultAuxOverflow) & 0x10)? CPUDef.PS.AF : 0; +}; + +/** + * setAF() + * + * @this {CPUState} + */ +CPUState.prototype.setAF = function() +{ + this.resultAuxOverflow = (~this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10); +}; + +/** + * clearZF() + * + * @this {CPUState} + */ +CPUState.prototype.clearZF = function() +{ + this.resultZeroCarry |= 0xff; +}; + +/** + * getZF() + * + * @this {CPUState} + * @return {number} 0 or CPUDef.PS.ZF + */ +CPUState.prototype.getZF = function() +{ + return (this.resultZeroCarry & 0xff)? 0 : CPUDef.PS.ZF; +}; + +/** + * setZF() + * + * @this {CPUState} + */ +CPUState.prototype.setZF = function() +{ + this.resultZeroCarry &= ~0xff; +}; + +/** + * clearSF() + * + * @this {CPUState} + */ +CPUState.prototype.clearSF = function() +{ + if (this.getSF()) this.resultParitySign ^= 0xc0; +}; + +/** + * getSF() + * + * @this {CPUState} + * @return {number} 0 or CPUDef.PS.SF + */ +CPUState.prototype.getSF = function() +{ + return (this.resultParitySign & 0x80)? CPUDef.PS.SF : 0; +}; + +/** + * setSF() + * + * @this {CPUState} + */ +CPUState.prototype.setSF = function() +{ + if (!this.getSF()) this.resultParitySign ^= 0xc0; +}; + +/** + * clearIF() + * + * @this {CPUState} + */ +CPUState.prototype.clearIF = function() +{ + this.regPS &= ~CPUDef.PS.IF; +}; + +/** + * getIF() + * + * @this {CPUState} + * @return {number} 0 or CPUDef.PS.IF + */ +CPUState.prototype.getIF = function() +{ + return (this.regPS & CPUDef.PS.IF); +}; + +/** + * setIF() + * + * @this {CPUState} + */ +CPUState.prototype.setIF = function() +{ + this.regPS |= CPUDef.PS.IF; +}; + +/** + * getPS() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getPS = function() +{ + return (this.regPS & ~CPUDef.PS.RESULT) | (this.getSF() | this.getZF() | this.getAF() | this.getPF() | this.getCF()); +}; + +/** + * setPS(regPS) + * + * @this {CPUState} + * @param {number} regPS + */ +CPUState.prototype.setPS = function(regPS) +{ + this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = 0; + if (regPS & CPUDef.PS.CF) this.resultZeroCarry |= 0x100; + if (!(regPS & CPUDef.PS.PF)) this.resultParitySign |= 0x01; + if (regPS & CPUDef.PS.AF) this.resultAuxOverflow |= 0x10; + if (!(regPS & CPUDef.PS.ZF)) this.resultZeroCarry |= 0xff; + if (regPS & CPUDef.PS.SF) this.resultParitySign ^= 0xc0; + this.regPS = (this.regPS & ~(CPUDef.PS.RESULT | CPUDef.PS.INTERNAL)) | (regPS & CPUDef.PS.INTERNAL) | CPUDef.PS.SET; + Component.assert((regPS & CPUDef.PS.RESULT) == (this.getPS() & CPUDef.PS.RESULT)); +}; + +/** + * getPSW() + * + * @this {CPUState} + * @return {number} + */ +CPUState.prototype.getPSW = function() +{ + return (this.getPS() & CPUDef.PS.MASK) | (this.regA << 8); +}; + +/** + * setPSW(w) + * + * @this {CPUState} + * @param {number} w + */ +CPUState.prototype.setPSW = function(w) +{ + this.setPS((w & CPUDef.PS.MASK) | (this.regPS & ~CPUDef.PS.MASK)); + this.regA = w >> 8; +}; + +/** + * addByte(src) + * + * @this {CPUState} + * @param {number} src + * @return {number} regA + src + */ +CPUState.prototype.addByte = function(src) +{ + this.resultAuxOverflow = this.regA ^ src; + return this.resultParitySign = (this.resultZeroCarry = this.regA + src) & 0xff; +}; + +/** + * addByteCarry(src) + * + * @this {CPUState} + * @param {number} src + * @return {number} regA + src + carry + */ +CPUState.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 {CPUState} + * @param {number} src + * @return {number} regA & src + */ +CPUState.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 {CPUState} + * @param {number} b + * @return {number} + */ +CPUState.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 {CPUState} + * @param {number} b + * @return {number} + */ +CPUState.prototype.incByte = function(b) +{ + this.resultAuxOverflow = b; + b = this.resultParitySign = (b + 1) & 0xff; + this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b; + return b; +}; + +/** + * orByte(src) + * + * @this {CPUState} + * @param {number} src + * @return {number} regA | src + */ +CPUState.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 {CPUState} + * @param {number} src + * @return {number} regA - src + */ +CPUState.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 {CPUState} + * @param {number} src + * @return {number} regA - src - carry + */ +CPUState.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 {CPUState} + * @param {number} src + * @return {number} regA ^ src + */ +CPUState.prototype.xorByte = function(src) +{ + return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA ^ src; +}; + +/** + * getByte(addr) + * + * @this {CPUState} + * @param {number} addr is a linear address + * @return {number} byte (8-bit) value at that address + */ +CPUState.prototype.getByte = function(addr) +{ + return this.bus.getByte(addr); +}; + +/** + * getWord(addr) + * + * @this {CPUState} + * @param {number} addr is a linear address + * @return {number} word (16-bit) value at that address + */ +CPUState.prototype.getWord = function(addr) +{ + return this.bus.getShort(addr); +}; + +/** + * setByte(addr, b) + * + * @this {CPUState} + * @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) + */ +CPUState.prototype.setByte = function(addr, b) +{ + this.bus.setByte(addr, b); +}; + +/** + * setWord(addr, w) + * + * @this {CPUState} + * @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) + */ +CPUState.prototype.setWord = function(addr, w) +{ + this.bus.setShort(addr, w); +}; + +/** + * getPCByte() + * + * @this {CPUState} + * @return {number} byte at the current PC; PC advanced by 1 + */ +CPUState.prototype.getPCByte = function() +{ + var b = this.getByte(this.regPC); + this.setPC(this.regPC + 1); + return b; +}; + +/** + * getPCWord() + * + * @this {CPUState} + * @return {number} word at the current PC; PC advanced by 2 + */ +CPUState.prototype.getPCWord = function() +{ + var w = this.getWord(this.regPC); + this.setPC(this.regPC + 2); + return w; +}; + +/** + * popWord() + * + * @this {CPUState} + * @return {number} word popped from the current SP; SP increased by 2 + */ +CPUState.prototype.popWord = function() +{ + var w = this.getWord(this.regSP); + this.setSP(this.regSP + 2); + return w; +}; + +/** + * pushWord(w) + * + * @this {CPUState} + * @param {number} w is the word (16-bit) value to push at current SP; SP decreased by 2 + */ +CPUState.prototype.pushWord = function(w) +{ + this.setSP(this.regSP - 2); + this.setWord(this.regSP, w); +}; + +/** + * checkINTR() + * + * @this {CPUState} + * @return {boolean} true if h/w interrupt has just been acknowledged, false if not + */ +CPUState.prototype.checkINTR = function() +{ + if ((this.intFlags & CPUDef.INTFLAG.INTR) && this.getIF()) { + var bRST = CPUDef.OPCODE.RST0 | ((this.intFlags & CPUDef.INTFLAG.INTL) << 3); + this.clearINTR(); + this.clearIF(); + this.aOps[bRST].call(this); + return true; + } + return false; +}; + +/** + * clearINTR() + * + * @this {CPUState} + */ +CPUState.prototype.clearINTR = function() +{ + this.intFlags &= ~(CPUDef.INTFLAG.INTL | CPUDef.INTFLAG.INTR); +}; + +/** + * requestINTR(nLevel) + * + * This is called by any component that wants to request a h/w interrupt. + * + * NOTE: We allow INTR to be set regardless of the current state of interrupt flag (IF), on the theory + * that if/when the CPU briefly turns interrupts off, it shouldn't lose the last h/w interrupt requested. + * So instead of ignoring INTR here, checkINTR() ignores INTR as long as the interrupt flag (IF) is clear. + * + * The downside is that, as long as the CPU has interrupts disabled, an active INTR state will slow stepCPU() + * down slightly. We could avoid that by introducing a two-stage interrupt tracking system, where a separate + * variable keeps track of the last interrupt requested whenever the interrupt flag (IF) is clear, and when + * setIF() finally occurs, that interrupt is propagated to intFlags. But for now, we're going to assume that + * scenario is rare. + * + * @this {CPUState} + * @param {number} nLevel (0-7) + */ +CPUState.prototype.requestINTR = function(nLevel) +{ + this.intFlags = (this.intFlags & ~CPUDef.INTFLAG.INTL) | nLevel | CPUDef.INTFLAG.INTR; +}; + +/** + * 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 {CPUState} + * @param {string} sReg + * @param {number} nValue + * @param {number} [cch] (default is 2 hex digits) + */ +CPUState.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 STATUS_UPDATES_PER_SECOND). + * 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 (see VIDEO_UPDATES_PER_SECOND). + * + * @this {CPUState} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) + */ +CPUState.prototype.updateStatus = function(fForce) +{ + if (this.cLiveRegs) { + if (fForce || !this.flags.fRunning || this.flags.fDisplayLiveRegs) { + 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 & CPUDef.PS.IF)? 1 : 0, 1); + this.updateReg("SF", (regPS & CPUDef.PS.SF)? 1 : 0, 1); + this.updateReg("ZF", (regPS & CPUDef.PS.ZF)? 1 : 0, 1); + this.updateReg("AF", (regPS & CPUDef.PS.AF)? 1 : 0, 1); + this.updateReg("PF", (regPS & CPUDef.PS.PF)? 1 : 0, 1); + this.updateReg("CF", (regPS & CPUDef.PS.CF)? 1 : 0, 1); + } + } + 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. + * + * As a result, the Debugger's complete independence means you can run other 8086/8088 debuggers + * (eg, DEBUG) inside the simulation without interference; you can even "debug" them with the Debugger. + * + * @this {CPUState} + * @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). + */ +CPUState.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. + * + * 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.fComplete = true; + + /* + * fDebugCheck is true if we need to "check" every instruction with the Debugger. + */ + var fDebugCheck = this.flags.fDebugCheck = (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.fStarting? 0 : 1); + this.flags.fStarting = 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; + + do { + if (this.intFlags) { + if (this.checkINTR()) { + if (!nMinCycles) { + this.assert(DEBUGGER); // nMinCycles of zero should be generated ONLY by the Debugger + if (DEBUGGER) { + this.println("interrupt dispatched"); + break; + } + } + } + else if (this.intFlags & CPUDef.INTFLAG.HALT) { + /* + * As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead, + * opHLT() sets CPUDef.INTFLAG.HALT, signalling to us that we're free to end the current burst + * AND that we should not execute any more instructions until checkINTR() indicates a hardware + * interrupt has been requested. + * + * One downside to this approach is that it *might* appear to the careful observer that we + * executed a full complement of instructions during bursts where CPUDef.INTFLAG.HALT was set, + * when in fact we did not. However, the steady advance of the overall cycle count, and thus + * the steady series calls to stepCPU(), is needed to ensure that timer updates, video updates, + * etc, all continue to occur at the expected rates. + * + * If necessary, we can add another bookkeeping cycle counter (eg, one that keeps tracks of the + * number of cycles during which we did not actually execute any instructions). + */ + this.nStepCycles = 0; + break; + } + } + + 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.fComplete? this.nBurstCycles - this.nStepCycles : (this.flags.fComplete === undefined? 0 : -1)); +}; + +/** + * CPUState.init() + * + * This function operates on every HTML element of class "cpu", extracting the + * JSON-encoded parameters for the CPUState constructor from the element's "data-value" + * attribute, invoking the constructor (which in turn invokes the CPU constructor) + * to create a CPUState component, and then binding any associated HTML controls to the + * new component. + */ +CPUState.init = function() +{ + var aeCPUs = Component.getElementsByClass(document, PCJSCLASS, "cpu"); + for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) { + var eCPU = aeCPUs[iCPU]; + var parmsCPU = Component.getComponentParms(eCPU); + var cpu = new CPUState(parmsCPU); + Component.bindComponentControls(cpu, eCPU, PCJSCLASS); + } +}; + +/* + * Initialize every CPU module on the page + */ +web.onInit(CPUState.init); + +if (NODE) module.exports = CPUState; diff --git a/modules/pc6502/lib/debugger.js b/modules/pc6502/lib/debugger.js new file mode 100644 index 000000000..8c946b1cb --- /dev/null +++ b/modules/pc6502/lib/debugger.js @@ -0,0 +1,4795 @@ +/** + * @fileoverview Implements the PC6502 Debugger component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"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 Messages = require("./messages"); + var Memory = require("./memory"); + var Keyboard = require("./keyboard"); + var State = require("./state"); + var CPU = require("./cpu"); + var CPUDef = require("./cpudef"); + } +} + +/** + * Debugger Address Object + * + * addr address + * fTemporary true if this is a temporary breakpoint address + * sCmd set for breakpoint addresses if there's an associated command string + * aCmds preprocessed commands (from sCmd) + * + * @typedef {{ + * addr:(number|undefined), + * fTemporary:(boolean|undefined), + * sCmd:(string|undefined), + * aCmds:(Array.|undefined) + * }} + */ +var DbgAddr; + +/** + * Debugger(parmsDbg) + * + * @constructor + * @extends Component + * @param {Object} parmsDbg + * + * The Debugger 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 Debugger component is an optional component that implements a variety of user + * commands for controlling the CPU, dumping and editing memory, etc. + */ +function Debugger(parmsDbg) +{ + if (DEBUGGER) { + + Component.call(this, "Debugger", parmsDbg, Debugger); + + this.style = Debugger.STYLE_8080; + + /* + * These keep track of instruction activity, but only when tracing or when Debugger checks + * have been enabled (eg, one or more breakpoints have been set). + * + * They are zeroed by the reset() notification handler. cInstructions is advanced by + * stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop() + * call and simply represents the number of cycles performed by the last run of instructions. + */ + this.nCycles = 0; + this.cOpcodes = this.cOpcodesStart = 0; + + /* + * 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 maintains command history. New commands are inserted at index 0 of the array. + * When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0]. + */ + this.iPrevCmd = -1; + this.aPrevCmds = []; + + /* + * fAssemble is true when "assemble mode" is active, false when not. + */ + this.fAssemble = false; + 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 = []; + + /* + * aVariables is an object with properties that grows as setVariable() assigns more variables; + * each property corresponds to one variable, where the property name is the variable name (ie, + * a string beginning with a letter or underscore, followed by zero or more additional letters, + * digits, or underscores) and the property value is the variable's numeric value. See doVar() + * and setVariable() for details. + * + * Note that parseValue(), through its reliance on str.parseInt(), assumes a default base of 16 + * if no base is explicitly indicated (eg, a trailing decimal period), and if you define variable + * names containing exclusively hex alpha characters (a-f), those variables will take precedence + * over the corresponding hex values. In other words, if you define variables "a" and "b", you + * will no longer be able to simply type "a" or "b" to specify the decimal values 10 or 11. + */ + this.aVariables = {}; + + /* + * 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; + + /* + * 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 Debugger message support + */ + this.afnDumpers = []; + this.messageInit(parmsDbg['messages']); + + this.sInitCommands = parmsDbg['commands']; + + /* + * Make it easier to access Debugger commands from an external REPL (eg, the WebStorm + * "live" console window); eg: + * + * $('r') + * $('dw 0:0') + * $('h') + * ... + */ + var dbg = this; + if (window) { + if (window['$'] === undefined) { + window['$'] = function(s) { return dbg.doCommands(s); }; + } + } else { + if (global['$'] === undefined) { + global['$'] = function(s) { return dbg.doCommands(s); }; + } + } + + } // endif DEBUGGER +} + +if (DEBUGGER) { + + Component.subclass(Debugger); + + /* + * NOTE: Every Debugger property from here to the first prototype function definition (initBus()) is a + * considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but + * that wouldn't help us catch any bugs). + * + * Technically, all of them should ALSO be preceded by a "@const" annotation, but that's a lot of work and it + * really clutters the code. I wish the Closure Compiler had a way to annotate every definition with a given + * section with a single annotation.... + * + * Bugs can slip through the cracks without those annotations; for example, I unthinkingly redefined TYPE_SI + * at one point, and if all the definitions had been preceded by an "@const", that mistake would have been + * caught at compile-time. + */ + + Debugger.COMMANDS = { + '?': "help/print", + 'a [#]': "assemble", // TODO: Implement this command someday + 'b [#]': "breakpoint", // multiple variations (use b? to list them) + 'c': "clear output", + 'd [#]': "dump memory", // additional syntax: d [#] [l#], where l# is a number of bytes to dump + 'e [#]': "edit memory", + 'f': "frequencies", + 'g [#]': "go [to #]", + 'h': "halt", + 'i [#]': "input port #", + 'if': "eval expression", + 'k': "stack trace", + "ln": "list nearest symbol(s)", + 'm': "messages", + 'o [#]': "output port #", + 'p': "step over", // other variations: pr (step and dump registers) + 'print': "print expression", + 'r': "dump/set registers", + 'reset': "reset machine", + 's': "set options", + 't [#]': "trace", // other variations: tr (trace and dump registers) + 'u [#]': "unassemble", + 'v': "print version", + 'var': "assign variable" + }; + + Debugger.STYLE_8080 = 8080; + Debugger.STYLE_8086 = 8086; + + /* + * CPU instruction ordinals + */ + Debugger.INS = { + NONE: 0, ACI: 1, ADC: 2, ADD: 3, ADI: 4, ANA: 5, ANI: 6, CALL: 7, + CC: 8, CM: 9, CNC: 10, CNZ: 11, CP: 12, CPE: 13, CPO: 14, CZ: 15, + CMA: 16, CMC: 17, CMP: 18, CPI: 19, DAA: 20, DAD: 21, DCR: 22, DCX: 23, + DI: 24, EI: 25, HLT: 26, IN: 27, INR: 28, INX: 29, JMP: 30, JC: 31, + JM: 32, JNC: 33, JNZ: 34, JP: 35, JPE: 36, JPO: 37, JZ: 38, LDA: 39, + LDAX: 40, LHLD: 41, LXI: 42, MOV: 43, MVI: 44, NOP: 45, ORA: 46, ORI: 47, + OUT: 48, PCHL: 49, POP: 50, PUSH: 51, RAL: 52, RAR: 53, RET: 54, RC: 55, + RM: 56, RNC: 57, RNZ: 58, RP: 59, RPE: 60, RPO: 61, RZ: 62, RLC: 63, + RRC: 64, RST: 65, SBB: 66, SBI: 67, SHLD: 68, SPHL: 69, STA: 70, STAX: 71, + STC: 72, SUB: 73, SUI: 74, XCHG: 75, XRA: 76, XRI: 77, XTHL: 78 + }; + + /* + * CPU instruction names (mnemonics), indexed by CPU instruction ordinal (above) + * + * If you change the default style, using the "s" command (eg, "s 8086"), then the 8086 table + * will be used instead. TODO: Add a "s z80" command for Z80-style mnemonics. + */ + Debugger.INS_NAMES = [ + "NONE", "ACI", "ADC", "ADD", "ADI", "ANA", "ANI", "CALL", + "CC", "CM", "CNC", "CNZ", "CP", "CPE", "CPO", "CZ", + "CMA", "CMC", "CMP", "CPI", "DAA", "DAD", "DCR", "DCX", + "DI", "EI", "HLT", "IN", "INR", "INX", "JMP", "JC", + "JM", "JNC", "JNZ", "JP", "JPE", "JPO", "JZ", "LDA", + "LDAX", "LHLD", "LXI", "MOV", "MVI", "NOP", "ORA", "ORI", + "OUT", "PCHL", "POP", "PUSH", "RAL", "RAR", "RET", "RC", + "RM", "RNC", "RNZ", "RP", "RPE", "RPO", "RZ", "RLC", + "RRC", "RST", "SBB", "SBI", "SHLD", "SPHL", "STA", "STAX", + "STC", "SUB", "SUI", "XCHG", "XRA", "XRI", "XTHL" + ]; + + Debugger.INS_NAMES_8086 = [ + "NONE", "ADC", "ADC", "ADD", "ADD", "AND", "AND", "CALL", + "CALLC", "CALLS", "CALLNC", "CALLNZ", "CALLNS", "CALLP", "CALLNP", "CALLZ", + "NOT", "CMC", "CMP", "CMP", "DAA", "ADD", "DEC", "DEC", + "CLI", "STI", "HLT", "IN", "INC", "INC", "JMP", "JC", + "JS", "JNC", "JNZ", "JNS", "JP", "JNP", "JZ", "MOV", + "MOV", "MOV", "MOV", "MOV", "MOV", "NOP", "OR", "OR", + "OUT", "JMP", "POP", "PUSH", "RCL", "RCR", "RET", "RETC", + "RETS", "RETNC", "RETNZ", "RETNS", "RETP", "RETNP", "RETZ", "ROL", + "ROR", "RST", "SBB", "SBB", "MOV", "MOV", "MOV", "MOV", + "STC", "SUB", "SUB", "XCHG", "XOR", "XOR", "XCHG" + ]; + + Debugger.REG_B = 0x00; + Debugger.REG_C = 0x01; + Debugger.REG_D = 0x02; + Debugger.REG_E = 0x03; + Debugger.REG_H = 0x04; + Debugger.REG_L = 0x05; + Debugger.REG_M = 0x06; + Debugger.REG_A = 0x07; + Debugger.REG_BC = 0x08; + Debugger.REG_DE = 0x09; + Debugger.REG_HL = 0x0A; + Debugger.REG_SP = 0x0B; + Debugger.REG_PC = 0x0C; + Debugger.REG_PS = 0x0D; + Debugger.REG_PSW = 0x0E; // aka AF if Z80-style mnemonics + + /* + * NOTE: "PS" is the complete processor status, which includes bits like the Interrupt flag (IF), + * which is NOT the same as "PSW", which is the low 8 bits of "PS" combined with "A" in the high byte. + */ + Debugger.REGS = [ + "B", "C", "D", "E", "H", "L", "M", "A", "BC", "DE", "HL", "SP", "PC", "PS", "PSW" + ]; + + /* + * Operand type descriptor masks and definitions + */ + Debugger.TYPE_SIZE = 0x000F; // size field + Debugger.TYPE_MODE = 0x00F0; // mode field + Debugger.TYPE_IREG = 0x0F00; // implied register field + Debugger.TYPE_OTHER = 0xF000; // "other" field + + /* + * TYPE_SIZE values + */ + Debugger.TYPE_NONE = 0x0000; // (all other TYPE fields ignored) + Debugger.TYPE_BYTE = 0x0001; // byte, regardless of operand size + Debugger.TYPE_SBYTE = 0x0002; // byte sign-extended to word + Debugger.TYPE_WORD = 0x0003; // word (16-bit value) + + /* + * TYPE_MODE values + */ + Debugger.TYPE_REG = 0x0010; // register + Debugger.TYPE_IMM = 0x0020; // immediate data + Debugger.TYPE_ADDR = 0x0033; // immediate (word) address + Debugger.TYPE_MEM = 0x0040; // memory reference + Debugger.TYPE_INT = 0x0080; // interrupt level encoded in instruction (bits 3-5) + + /* + * TYPE_IREG values, based on the REG_* constants. + * + * NOte that TYPE_M isn't really a register, just an alternative form of TYPE_HL | TYPE_MEM. + */ + Debugger.TYPE_A = (Debugger.REG_A << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_B = (Debugger.REG_B << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_C = (Debugger.REG_C << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_D = (Debugger.REG_D << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_E = (Debugger.REG_E << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_H = (Debugger.REG_H << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_L = (Debugger.REG_L << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE); + Debugger.TYPE_M = (Debugger.REG_M << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE | Debugger.TYPE_MEM); + Debugger.TYPE_BC = (Debugger.REG_BC << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + Debugger.TYPE_DE = (Debugger.REG_DE << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + Debugger.TYPE_HL = (Debugger.REG_HL << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + Debugger.TYPE_SP = (Debugger.REG_SP << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + Debugger.TYPE_PC = (Debugger.REG_PC << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + Debugger.TYPE_PSW = (Debugger.REG_PSW<< 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD); + + /* + * TYPE_OTHER bit definitions + */ + Debugger.TYPE_IN = 0x1000; // operand is input + Debugger.TYPE_OUT = 0x2000; // operand is output + Debugger.TYPE_BOTH = (Debugger.TYPE_IN | Debugger.TYPE_OUT); + Debugger.TYPE_OPT = 0x4000; // optional operand (ie, normally omitted in 8080 assembly language) + Debugger.TYPE_UNDOC = 0x8000; // opcode is an undocumented alternative encoding + + /* + * The aaOpDescs array is indexed by opcode, and each element is a sub-array (aOpDesc) that describes + * the corresponding opcode. The sub-elements are as follows: + * + * [0]: {number} of the opcode name (see INS.*) + * [1]: {number} containing the destination operand descriptor bit(s), if any + * [2]: {number} containing the source operand descriptor bit(s), if any + * [3]: {number} containing the occasional third operand descriptor bit(s), if any + * + * These sub-elements are all optional. If [0] is not present, the opcode is undefined; if [1] is not + * present (or contains zero), the opcode has no (or only implied) operands; if [2] is not present, the + * opcode has only a single operand. And so on. + * + * Additional default rules: + * + * 1) If no TYPE_OTHER bits are specified for the first (destination) operand, TYPE_OUT is assumed; + * 2) If no TYPE_OTHER bits are specified for the second (source) operand, TYPE_IN is assumed; + * 3) If no size is specified for the second operand, the size is assumed to match the first operand. + */ + Debugger.aaOpDescs = [ + /* 0x00 */ [Debugger.INS.NOP], + /* 0x01 */ [Debugger.INS.LXI, Debugger.TYPE_BC, Debugger.TYPE_IMM], + /* 0x02 */ [Debugger.INS.STAX, Debugger.TYPE_BC | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT], + /* 0x03 */ [Debugger.INS.INX, Debugger.TYPE_BC], + /* 0x04 */ [Debugger.INS.INR, Debugger.TYPE_B], + /* 0x05 */ [Debugger.INS.DCR, Debugger.TYPE_B], + /* 0x06 */ [Debugger.INS.MVI, Debugger.TYPE_B, Debugger.TYPE_IMM], + /* 0x07 */ [Debugger.INS.RLC], + /* 0x08 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x09 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_BC], + /* 0x0A */ [Debugger.INS.LDAX, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_BC | Debugger.TYPE_MEM], + /* 0x0B */ [Debugger.INS.DCX, Debugger.TYPE_BC], + /* 0x0C */ [Debugger.INS.INR, Debugger.TYPE_C], + /* 0x0D */ [Debugger.INS.DCR, Debugger.TYPE_C], + /* 0x0E */ [Debugger.INS.MVI, Debugger.TYPE_C, Debugger.TYPE_IMM], + /* 0x0F */ [Debugger.INS.RRC], + /* 0x10 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x11 */ [Debugger.INS.LXI, Debugger.TYPE_DE, Debugger.TYPE_IMM], + /* 0x12 */ [Debugger.INS.STAX, Debugger.TYPE_DE | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT], + /* 0x13 */ [Debugger.INS.INX, Debugger.TYPE_DE], + /* 0x14 */ [Debugger.INS.INR, Debugger.TYPE_D], + /* 0x15 */ [Debugger.INS.DCR, Debugger.TYPE_D], + /* 0x16 */ [Debugger.INS.MVI, Debugger.TYPE_D, Debugger.TYPE_IMM], + /* 0x17 */ [Debugger.INS.RAL], + /* 0x18 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x19 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_DE], + /* 0x1A */ [Debugger.INS.LDAX, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_DE | Debugger.TYPE_MEM], + /* 0x1B */ [Debugger.INS.DCX, Debugger.TYPE_DE], + /* 0x1C */ [Debugger.INS.INR, Debugger.TYPE_E], + /* 0x1D */ [Debugger.INS.DCR, Debugger.TYPE_E], + /* 0x1E */ [Debugger.INS.MVI, Debugger.TYPE_E, Debugger.TYPE_IMM], + /* 0x1F */ [Debugger.INS.RAR], + /* 0x20 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x21 */ [Debugger.INS.LXI, Debugger.TYPE_HL, Debugger.TYPE_IMM], + /* 0x22 */ [Debugger.INS.SHLD, Debugger.TYPE_ADDR | Debugger.TYPE_MEM, Debugger.TYPE_HL | Debugger.TYPE_OPT], + /* 0x23 */ [Debugger.INS.INX, Debugger.TYPE_HL], + /* 0x24 */ [Debugger.INS.INR, Debugger.TYPE_H], + /* 0x25 */ [Debugger.INS.DCR, Debugger.TYPE_H], + /* 0x26 */ [Debugger.INS.MVI, Debugger.TYPE_H, Debugger.TYPE_IMM], + /* 0x27 */ [Debugger.INS.DAA], + /* 0x28 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x29 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_HL], + /* 0x2A */ [Debugger.INS.LHLD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_ADDR | Debugger.TYPE_MEM], + /* 0x2B */ [Debugger.INS.DCX, Debugger.TYPE_HL], + /* 0x2C */ [Debugger.INS.INR, Debugger.TYPE_L], + /* 0x2D */ [Debugger.INS.DCR, Debugger.TYPE_L], + /* 0x2E */ [Debugger.INS.MVI, Debugger.TYPE_L, Debugger.TYPE_IMM], + /* 0x2F */ [Debugger.INS.CMA, Debugger.TYPE_A | Debugger.TYPE_OPT], + /* 0x30 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x31 */ [Debugger.INS.LXI, Debugger.TYPE_SP, Debugger.TYPE_IMM], + /* 0x32 */ [Debugger.INS.STA, Debugger.TYPE_ADDR | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT], + /* 0x33 */ [Debugger.INS.INX, Debugger.TYPE_SP], + /* 0x34 */ [Debugger.INS.INR, Debugger.TYPE_M], + /* 0x35 */ [Debugger.INS.DCR, Debugger.TYPE_M], + /* 0x36 */ [Debugger.INS.MVI, Debugger.TYPE_M, Debugger.TYPE_IMM], + /* 0x37 */ [Debugger.INS.STC], + /* 0x38 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC], + /* 0x39 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_SP], + /* 0x3A */ [Debugger.INS.LDA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_ADDR | Debugger.TYPE_MEM], + /* 0x3B */ [Debugger.INS.DCX, Debugger.TYPE_SP], + /* 0x3C */ [Debugger.INS.INR, Debugger.TYPE_A], + /* 0x3D */ [Debugger.INS.DCR, Debugger.TYPE_A], + /* 0x3E */ [Debugger.INS.MVI, Debugger.TYPE_A, Debugger.TYPE_IMM], + /* 0x3F */ [Debugger.INS.CMC], + /* 0x40 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_B], + /* 0x41 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_C], + /* 0x42 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_D], + /* 0x43 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_E], + /* 0x44 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_H], + /* 0x45 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_L], + /* 0x46 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_M], + /* 0x47 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_A], + /* 0x48 */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_B], + /* 0x49 */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_C], + /* 0x4A */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_D], + /* 0x4B */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_E], + /* 0x4C */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_H], + /* 0x4D */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_L], + /* 0x4E */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_M], + /* 0x4F */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_A], + /* 0x50 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_B], + /* 0x51 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_C], + /* 0x52 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_D], + /* 0x53 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_E], + /* 0x54 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_H], + /* 0x55 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_L], + /* 0x56 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_M], + /* 0x57 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_A], + /* 0x58 */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_B], + /* 0x59 */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_C], + /* 0x5A */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_D], + /* 0x5B */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_E], + /* 0x5C */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_H], + /* 0x5D */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_L], + /* 0x5E */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_M], + /* 0x5F */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_A], + /* 0x60 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_B], + /* 0x61 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_C], + /* 0x62 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_D], + /* 0x63 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_E], + /* 0x64 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_H], + /* 0x65 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_L], + /* 0x66 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_M], + /* 0x67 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_A], + /* 0x68 */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_B], + /* 0x69 */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_C], + /* 0x6A */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_D], + /* 0x6B */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_E], + /* 0x6C */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_H], + /* 0x6D */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_L], + /* 0x6E */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_M], + /* 0x6F */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_A], + /* 0x70 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_B], + /* 0x71 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_C], + /* 0x72 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_D], + /* 0x73 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_E], + /* 0x74 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_H], + /* 0x75 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_L], + /* 0x76 */ [Debugger.INS.HLT], + /* 0x77 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_A], + /* 0x78 */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_B], + /* 0x79 */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_C], + /* 0x7A */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_D], + /* 0x7B */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_E], + /* 0x7C */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_H], + /* 0x7D */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_L], + /* 0x7E */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_M], + /* 0x7F */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_A], + /* 0x80 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0x81 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0x82 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0x83 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0x84 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0x85 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0x86 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0x87 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0x88 */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0x89 */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0x8A */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0x8B */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0x8C */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0x8D */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0x8E */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0x8F */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0x90 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0x91 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0x92 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0x93 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0x94 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0x95 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0x96 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0x97 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0x98 */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0x99 */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0x9A */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0x9B */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0x9C */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0x9D */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0x9E */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0x9F */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0xA0 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0xA1 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0xA2 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0xA3 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0xA4 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0xA5 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0xA6 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0xA7 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0xA8 */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0xA9 */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0xAA */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0xAB */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0xAC */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0xAD */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0xAE */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0xAF */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0xB0 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0xB1 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0xB2 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0xB3 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0xB4 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0xB5 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0xB6 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0xB7 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0xB8 */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B], + /* 0xB9 */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C], + /* 0xBA */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D], + /* 0xBB */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E], + /* 0xBC */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H], + /* 0xBD */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L], + /* 0xBE */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M], + /* 0xBF */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A], + /* 0xC0 */ [Debugger.INS.RNZ], + /* 0xC1 */ [Debugger.INS.POP, Debugger.TYPE_BC], + /* 0xC2 */ [Debugger.INS.JNZ, Debugger.TYPE_ADDR], + /* 0xC3 */ [Debugger.INS.JMP, Debugger.TYPE_ADDR], + /* 0xC4 */ [Debugger.INS.CNZ, Debugger.TYPE_ADDR], + /* 0xC5 */ [Debugger.INS.PUSH, Debugger.TYPE_BC], + /* 0xC6 */ [Debugger.INS.ADI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xC7 */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xC8 */ [Debugger.INS.RZ], + /* 0xC9 */ [Debugger.INS.RET], + /* 0xCA */ [Debugger.INS.JZ, Debugger.TYPE_ADDR], + /* 0xCB */ [Debugger.INS.JMP, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC], + /* 0xCC */ [Debugger.INS.CZ, Debugger.TYPE_ADDR], + /* 0xCD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR], + /* 0xCE */ [Debugger.INS.ACI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xCF */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xD0 */ [Debugger.INS.RNC], + /* 0xD1 */ [Debugger.INS.POP, Debugger.TYPE_DE], + /* 0xD2 */ [Debugger.INS.JNC, Debugger.TYPE_ADDR], + /* 0xD3 */ [Debugger.INS.OUT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE,Debugger.TYPE_A | Debugger.TYPE_OPT], + /* 0xD4 */ [Debugger.INS.CNC, Debugger.TYPE_ADDR], + /* 0xD5 */ [Debugger.INS.PUSH, Debugger.TYPE_DE], + /* 0xD6 */ [Debugger.INS.SUI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xD7 */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xD8 */ [Debugger.INS.RC], + /* 0xD9 */ [Debugger.INS.RET, Debugger.TYPE_UNDOC], + /* 0xDA */ [Debugger.INS.JC, Debugger.TYPE_ADDR], + /* 0xDB */ [Debugger.INS.IN, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xDC */ [Debugger.INS.CC, Debugger.TYPE_ADDR], + /* 0xDD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC], + /* 0xDE */ [Debugger.INS.SBI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xDF */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xE0 */ [Debugger.INS.RPO], + /* 0xE1 */ [Debugger.INS.POP, Debugger.TYPE_HL], + /* 0xE2 */ [Debugger.INS.JPO, Debugger.TYPE_ADDR], + /* 0xE3 */ [Debugger.INS.XTHL, Debugger.TYPE_SP | Debugger.TYPE_MEM| Debugger.TYPE_OPT, Debugger.TYPE_HL | Debugger.TYPE_OPT], + /* 0xE4 */ [Debugger.INS.CPO, Debugger.TYPE_ADDR], + /* 0xE5 */ [Debugger.INS.PUSH, Debugger.TYPE_HL], + /* 0xE6 */ [Debugger.INS.ANI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xE7 */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xE8 */ [Debugger.INS.RPE], + /* 0xE9 */ [Debugger.INS.PCHL, Debugger.TYPE_HL], + /* 0xEA */ [Debugger.INS.JPE, Debugger.TYPE_ADDR], + /* 0xEB */ [Debugger.INS.XCHG, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_DE | Debugger.TYPE_OPT], + /* 0xEC */ [Debugger.INS.CPE, Debugger.TYPE_ADDR], + /* 0xED */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC], + /* 0xEE */ [Debugger.INS.XRI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xEF */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xF0 */ [Debugger.INS.RP], + /* 0xF1 */ [Debugger.INS.POP, Debugger.TYPE_PSW], + /* 0xF2 */ [Debugger.INS.JP, Debugger.TYPE_ADDR], + /* 0xF3 */ [Debugger.INS.DI], + /* 0xF4 */ [Debugger.INS.CP, Debugger.TYPE_ADDR], + /* 0xF5 */ [Debugger.INS.PUSH, Debugger.TYPE_PSW], + /* 0xF6 */ [Debugger.INS.ORI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xF7 */ [Debugger.INS.RST, Debugger.TYPE_INT], + /* 0xF8 */ [Debugger.INS.RM], + /* 0xF9 */ [Debugger.INS.SPHL, Debugger.TYPE_SP | Debugger.TYPE_OPT, Debugger.TYPE_HL | Debugger.TYPE_OPT], + /* 0xFA */ [Debugger.INS.JM, Debugger.TYPE_ADDR], + /* 0xFB */ [Debugger.INS.EI], + /* 0xFC */ [Debugger.INS.CM, Debugger.TYPE_ADDR], + /* 0xFD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC], + /* 0xFE */ [Debugger.INS.CPI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE], + /* 0xFF */ [Debugger.INS.RST, Debugger.TYPE_INT] + ]; + + /* + * Message categories supported by the messageEnabled() function and other assorted message + * functions. Each category has a corresponding bit value that can be combined (ie, OR'ed) as + * needed. The Debugger's message command ("m") is used to turn message categories on and off, + * like so: + * + * m port on + * m port off + * ... + * + * NOTE: The order of these categories can be rearranged, alphabetized, etc, as desired; just be + * aware that changing the bit values could break saved Debugger states (not a huge concern, just + * something to be aware of). + */ + Debugger.MESSAGES = { + "cpu": Messages.CPU, + "bus": Messages.BUS, + "mem": Messages.MEM, + "port": Messages.PORT, + "chipset": Messages.CHIPSET, + "keyboard": Messages.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages() + "key": Messages.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects + "video": Messages.VIDEO, + "fdc": Messages.FDC, + "disk": Messages.DISK, + "serial": Messages.SERIAL, + "speaker": Messages.SPEAKER, + "computer": Messages.COMPUTER, + "log": Messages.LOG, + "warn": Messages.WARN, + /* + * Now we turn to message actions rather than message types; for example, setting "halt" + * on or off doesn't enable "halt" messages, but rather halts the CPU on any message above. + */ + "halt": Messages.HALT + }; + + Debugger.HISTORY_LIMIT = DEBUG? 100000 : 1000; + + /** + * initBus(bus, cpu, dbg) + * + * @this {Debugger} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPUState} cpu + * @param {Debugger} dbg + */ + Debugger.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 = Debugger.aaOpDescs; + + this.messageDump(Messages.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); }); + + this.setReady(); + }; + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {Debugger} + * @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 + */ + Debugger.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 sCmds; + if (event.keyCode == Keyboard.KEYCODE.CR) { + sCmds = control.value; + control.value = ""; + dbg.doCommands(sCmds, true); + } + else if (event.keyCode == Keyboard.KEYCODE.ESC) { + control.value = sCmds = ""; + } + else { + if (event.keyCode == Keyboard.KEYCODE.UP) { + if (dbg.iPrevCmd < dbg.aPrevCmds.length - 1) { + sCmds = dbg.aPrevCmds[++dbg.iPrevCmd]; + } + } + else if (event.keyCode == Keyboard.KEYCODE.DOWN) { + if (dbg.iPrevCmd > 0) { + sCmds = dbg.aPrevCmds[--dbg.iPrevCmd]; + } else { + sCmds = ""; + dbg.iPrevCmd = -1; + } + } + if (sCmds != null) { + var cch = sCmds.length; + control.value = sCmds; + control.setSelectionRange(cch, cch); + } + } + if (sCmds != 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 {Debugger} + */ + Debugger.prototype.updateFocus = function() + { + if (this.controlDebug) this.controlDebug.focus(); + }; + + /** + * getAddr(dbgAddr, fWrite, nb) + * + * @this {Debugger} + * @param {DbgAddr|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 CPUDef.ADDR_INVALID + */ + Debugger.prototype.getAddr = function(dbgAddr, fWrite, nb) + { + var addr = dbgAddr && dbgAddr.addr; + if (addr == null) { + addr = CPUDef.ADDR_INVALID; + } + return addr; + }; + + /** + * getByte(dbgAddr, inc) + * + * We must route all our memory requests through the CPU now, in case paging is enabled. + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} [inc] + * @return {number} + */ + Debugger.prototype.getByte = function(dbgAddr, inc) + { + var b = 0xff; + var addr = this.getAddr(dbgAddr, false, 1); + if (addr !== CPUDef.ADDR_INVALID) { + b = this.bus.getByteDirect(addr); + if (inc) this.incAddr(dbgAddr, inc); + } + return b; + }; + + /** + * getWord(dbgAddr, fAdvance) + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @param {boolean} [fAdvance] + * @return {number} + */ + Debugger.prototype.getWord = function(dbgAddr, fAdvance) + { + return this.getShort(dbgAddr, fAdvance? 2 : 0); + }; + + /** + * getShort(dbgAddr, inc) + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} [inc] + * @return {number} + */ + Debugger.prototype.getShort = function(dbgAddr, inc) + { + var w = 0xffff; + var addr = this.getAddr(dbgAddr, false, 2); + if (addr !== CPUDef.ADDR_INVALID) { + w = this.bus.getShortDirect(addr); + if (inc) this.incAddr(dbgAddr, inc); + } + return w; + }; + + /** + * setByte(dbgAddr, b, inc) + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} b + * @param {number} [inc] + */ + Debugger.prototype.setByte = function(dbgAddr, b, inc) + { + var addr = this.getAddr(dbgAddr, true, 1); + if (addr !== CPUDef.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} w + * @param {number} [inc] + */ + Debugger.prototype.setShort = function(dbgAddr, w, inc) + { + var addr = this.getAddr(dbgAddr, true, 2); + if (addr !== CPUDef.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 DbgAddr object, initialized with specified values and/or defaults. + * + * @this {Debugger} + * @param {number} [addr] + * @return {DbgAddr} + */ + Debugger.prototype.newAddr = function(addr) + { + return {addr: addr, fTemporary: false}; + }; + + /** + * setAddr(dbgAddr, addr) + * + * Updates an EXISTING DbgAddr object, initialized with specified values and/or defaults. + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} addr + * @return {DbgAddr} + */ + Debugger.prototype.setAddr = function(dbgAddr, addr) + { + dbgAddr.addr = addr; + dbgAddr.fTemporary = false; + return dbgAddr; + }; + + /** + * packAddr(dbgAddr) + * + * Packs a DbgAddr object into an Array suitable for saving in a machine state object. + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @return {Array} + */ + Debugger.prototype.packAddr = function(dbgAddr) + { + return [dbgAddr.addr, dbgAddr.fTemporary]; + }; + + /** + * unpackAddr(aAddr) + * + * Unpacks a DbgAddr object from an Array created by packAddr() and restored from a saved machine state. + * + * @this {Debugger} + * @param {Array} aAddr + * @return {DbgAddr} + */ + Debugger.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 CPUDef.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 + * Bus.nBusLimit; in the case of CPUDef.ADDR_INVALID, that will generally refer to the top of the physical + * address space. + * + * @this {Debugger} + * @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 {DbgAddr|null|undefined} + */ + Debugger.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @param {string} [sOptions] + */ + Debugger.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} [inc] contains value to increment dbgAddr by (default is 1) + */ + Debugger.prototype.incAddr = function(dbgAddr, inc) + { + if (dbgAddr.addr != null) { + dbgAddr.addr += (inc || 1); + } + }; + + /** + * toHexOffset(off) + * + * @this {Debugger} + * @param {number|null|undefined} [off] + * @return {string} the hex representation of off + */ + Debugger.prototype.toHexOffset = function(off) + { + return str.toHex(off, 4); + }; + + /** + * toHexAddr(dbgAddr) + * + * @this {Debugger} + * @param {DbgAddr} dbgAddr + * @return {string} the hex representation of the address + */ + Debugger.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @param {number} [cchMax] (default is 256) + * @return {string} (and dbgAddr advanced past the terminating zero) + */ + Debugger.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 {Debugger} + * @param {Array} aBlocks + * @param {string} [sAddr] (optional block address) + */ + Debugger.prototype.dumpBlocks = function(aBlocks, sAddr) + { + var addr = 0, i = 0, n = aBlocks.length; + + if (sAddr) { + addr = this.getAddr(this.parseAddr(sAddr)); + if (addr === CPUDef.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 = Memory.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 != Memory.TYPE.NONE) typePrev = -1; + cPrev = 0; + } + addr += this.bus.nBlockSize; + i++; + } + }; + + /** + * dumpBus(asArgs) + * + * Dumps Bus allocations. + * + * @this {Debugger} + * @param {Array.} asArgs (asArgs[0] is an optional block address) + */ + Debugger.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 {Debugger} + * @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) + */ + Debugger.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 {Debugger} + * @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|' + */ + Debugger.prototype.messageInit = function(sEnable) + { + this.dbg = this; + this.bitsMessage = this.bitsWarning = Messages.WARN; + this.sMessagePrev = null; + /* + * 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 Debugger.MESSAGES) { + if (usr.indexOf(aEnable, m) >= 0) { + this.bitsMessage |= Debugger.MESSAGES[m]; + this.println(m + " messages enabled"); + } + } + } + }; + + /** + * messageDump(bitMessage, fnDumper) + * + * @this {Debugger} + * @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 + */ + Debugger.prototype.messageDump = function(bitMessage, fnDumper) + { + for (var m in Debugger.MESSAGES) { + if (bitMessage == Debugger.MESSAGES[m]) { + this.afnDumpers[m] = fnDumper; + return true; + } + } + return false; + }; + + /** + * getRegIndex(sReg, off) + * + * @this {Debugger} + * @param {string} sReg + * @param {number} [off] optional offset into sReg + * @return {number} register index, or -1 if not found + */ + Debugger.prototype.getRegIndex = function(sReg, off) + { + var i; + sReg = sReg.toUpperCase(); + if (off == null) { + i = usr.indexOf(Debugger.REGS, sReg); + } else { + i = usr.indexOf(Debugger.REGS, sReg.substr(off, 2)); + if (i < 0) i = usr.indexOf(Debugger.REGS, sReg.substr(off, 1)); + } + return i; + }; + + /** + * getRegString(iReg) + * + * @this {Debugger} + * @param {number} iReg + * @return {string} + */ + Debugger.prototype.getRegString = function(iReg) + { + var cch = 0; + var n = this.getRegValue(iReg); + if (n !== undefined) { + switch(iReg) { + case Debugger.REG_A: + case Debugger.REG_B: + case Debugger.REG_C: + case Debugger.REG_D: + case Debugger.REG_E: + case Debugger.REG_H: + case Debugger.REG_L: + case Debugger.REG_M: + cch = 2; + break; + case Debugger.REG_BC: + case Debugger.REG_DE: + case Debugger.REG_HL: + case Debugger.REG_SP: + case Debugger.REG_PC: + case Debugger.REG_PS: + case Debugger.REG_PSW: + cch = 4; + break; + } + } + return cch? str.toHex(n, cch) : "??"; + }; + + /** + * getRegValue(iReg) + * + * @this {Debugger} + * @param {number} iReg + * @return {number|undefined} + */ + Debugger.prototype.getRegValue = function(iReg) + { + var n; + if (iReg >= 0) { + var cpu = this.cpu; + switch(iReg) { + case Debugger.REG_A: + n = cpu.regA; + break; + case Debugger.REG_B: + n = cpu.regB; + break; + case Debugger.REG_C: + n = cpu.regC; + break; + case Debugger.REG_BC: + n = cpu.getBC(); + break; + case Debugger.REG_D: + n = cpu.regD; + break; + case Debugger.REG_E: + n = cpu.regE; + break; + case Debugger.REG_DE: + n = cpu.getDE(); + break; + case Debugger.REG_H: + n = cpu.regH; + break; + case Debugger.REG_L: + n = cpu.regL; + break; + case Debugger.REG_HL: + n = cpu.getHL(); + break; + case Debugger.REG_M: + n = cpu.getByte(cpu.getHL()); + break; + case Debugger.REG_SP: + n = cpu.getSP(); + break; + case Debugger.REG_PC: + n = cpu.getPC(); + break; + case Debugger.REG_PS: + n = cpu.getPS(); + break; + case Debugger.REG_PSW: + n = cpu.getPSW(); + break; + default: + break; + } + } + return n; + }; + + /** + * replaceRegs(s) + * + * @this {Debugger} + * @param {string} s + * @return {string} + */ + Debugger.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 + Debugger.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 {Debugger} + * @param {string} sMessage is any caller-defined message string + * @param {boolean} [fAddress] is true to display the current CS:IP + */ + Debugger.prototype.message = function(sMessage, fAddress) + { + if (fAddress) { + sMessage += " at " + this.toHexAddr(this.newAddr(this.cpu.getPC())); + } + + if (this.sMessagePrev && sMessage == this.sMessagePrev) return; + this.sMessagePrev = sMessage; + + if (this.bitsMessage & Messages.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 CPU.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 {Debugger} + * @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) + */ + Debugger.prototype.messageIO = function(component, port, bOut, addrFrom, name, bIn, bitsMessage) + { + bitsMessage |= Messages.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 {Debugger} + */ + Debugger.prototype.init = function() + { + this.println("Type ? for help with PC6502 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 {Debugger} + * @param {boolean} [fQuiet] + */ + Debugger.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(Debugger.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 {Debugger} + * @param {boolean} [fUpdateFocus] is true to update focus + * @return {boolean} true if run request successful, false if not + */ + Debugger.prototype.runCPU = function(fUpdateFocus) + { + if (!this.isCPUAvail()) return false; + this.cpu.runCPU(fUpdateFocus); + return true; + }; + + /** + * stepCPU(nCycles, fRegs, fUpdateCPU) + * + * @this {Debugger} + * @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} + */ + Debugger.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 {Debugger} + * @param {boolean} [fComplete] + */ + Debugger.prototype.stopCPU = function(fComplete) + { + if (this.cpu) this.cpu.stopCPU(fComplete); + }; + + /** + * updateStatus(fRegs) + * + * @this {Debugger} + * @param {boolean} [fRegs] (default is true) + */ + Debugger.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 {Debugger} + * @return {boolean} + */ + Debugger.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 {Debugger} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + Debugger.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 {Debugger} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} + */ + Debugger.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 {Debugger} + * @param {boolean} fQuiet (true only when called from our own powerUp handler) + */ + Debugger.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.fRunning = false; + this.clearTempBreakpoint(); + if (!fQuiet) this.updateStatus(); + }; + + /** + * save() + * + * This implements (very rudimentary) save support for the Debugger component. + * + * @this {Debugger} + * @return {Object} + */ + Debugger.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 {Debugger} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + Debugger.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 {Debugger} + * @param {number} ms + * @param {number} nCycles + */ + Debugger.prototype.start = function(ms, nCycles) + { + if (!this.nStep) this.println("running"); + this.flags.fRunning = 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 {Debugger} + * @param {number} ms + * @param {number} nCycles + */ + Debugger.prototype.stop = function(ms, nCycles) + { + if (this.flags.fRunning) { + this.flags.fRunning = 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(Messages.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 {Debugger} + * @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 + */ + Debugger.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 {Debugger} + * @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 + */ + Debugger.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 Messages.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 {Debugger} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + Debugger.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 {Debugger} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + Debugger.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 {Debugger} + * @param {number} port + * @param {number} size + * @param {number} data + * @return {boolean} true if breakpoint hit, false if not + */ + Debugger.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 {Debugger} + * @param {number} port + * @param {number} size + * @param {number} data + * @return {boolean} true if breakpoint hit, false if not + */ + Debugger.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 {Debugger} + */ + Debugger.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 {Debugger} + * @param {Array} aBreak + * @param {DbgAddr} dbgAddr + * @param {boolean} [fTemporary] + * @return {boolean} true if breakpoint added, false if already exists + */ + Debugger.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 === CPUDef.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 {Debugger} + * @param {Array} aBreak + * @param {DbgAddr} dbgAddr + * @param {boolean} [fRemove] + * @param {boolean} [fTemporary] + * @param {boolean} [fQuiet] + * @return {boolean} true if found, false if not + */ + Debugger.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 {Debugger} + * @param {Array} aBreak + * @return {number} of breakpoints listed, 0 if none + */ + Debugger.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 {Debugger} + * @param {Array} aBreak + * @param {number} i + * @param {string} [sAction] + */ + Debugger.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 {Debugger} + * @param {DbgAddr} dbgAddr of new temp breakpoint + */ + Debugger.prototype.setTempBreakpoint = function(dbgAddr) + { + this.addBreakpoint(this.aBreakExec, dbgAddr, true); + }; + + /** + * clearTempBreakpoint(addr) + * + * @this {Debugger} + * @param {number|undefined} [addr] clear all temp breakpoints if no address specified + */ + Debugger.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 {Debugger} + * @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 + */ + Debugger.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 {Debugger} + * @param {DbgAddr} 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) + */ + Debugger.prototype.getInstruction = function(dbgAddr, sComment, nSequence) + { + var dbgAddrIns = this.newAddr(dbgAddr.addr); + + var bOpcode = this.getByte(dbgAddr, 1); + + var asOpcodes = this.style != Debugger.STYLE_8086? Debugger.INS_NAMES : Debugger.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 = Debugger.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 & Debugger.TYPE_OPT) && this.style == Debugger.STYLE_8080) continue; + + var typeMode = type & Debugger.TYPE_MODE; + if (!typeMode) continue; + + var typeSize = type & Debugger.TYPE_SIZE; + if (!typeSize) { + type |= typeSizeDefault; + } else { + typeSizeDefault = typeSize; + } + + var typeOther = type & Debugger.TYPE_OTHER; + if (!typeOther) { + type |= (iOperand == 1? Debugger.TYPE_OUT : Debugger.TYPE_IN); + } + + if (typeMode & Debugger.TYPE_IMM) { + sOperand = this.getImmOperand(type, dbgAddr); + } + else if (typeMode & Debugger.TYPE_REG) { + sOperand = this.getRegOperand((type & Debugger.TYPE_IREG) >> 8, type, dbgAddr); + } + else if (typeMode & Debugger.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 !== CPUDef.ADDR_INVALID && dbgAddr.addr !== CPUDef.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 & Debugger.TYPE_UNDOC)? '*' : ' '; + sLine += str.pad(sOpcode, 7); + if (sOperands) sLine += ' ' + sOperands; + + if (sComment) { + sLine = str.pad(sLine, 40) + ';' + sComment; + if (!this.cpu.flags.fChecksum) { + sLine += (nSequence != null? '=' + nSequence.toString() : ""); + } else { + var nCycles = this.cpu.getCycles(); + sLine += "cycles=" + nCycles.toString() + " cs=" + str.toHex(this.cpu.aCounts.nChecksum); + } + } + return sLine; + }; + + /** + * getImmOperand(type, dbgAddr) + * + * @this {Debugger} + * @param {number} type + * @param {DbgAddr} dbgAddr + * @return {string} operand + */ + Debugger.prototype.getImmOperand = function(type, dbgAddr) + { + var sOperand = ' '; + var typeSize = type & Debugger.TYPE_SIZE; + + switch (typeSize) { + case Debugger.TYPE_BYTE: + sOperand = str.toHex(this.getByte(dbgAddr, 1), 2); + break; + case Debugger.TYPE_SBYTE: + sOperand = str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4); + break; + case Debugger.TYPE_WORD: + sOperand = str.toHex(this.getShort(dbgAddr, 2), 4); + break; + default: + return "imm(" + str.toHexWord(type) + ')'; + } + if (this.style == Debugger.STYLE_8086 && (type & Debugger.TYPE_MEM)) { + sOperand = '[' + sOperand + ']'; + } else if (!(type & Debugger.TYPE_REG)) { + sOperand = (this.style == Debugger.STYLE_8080? '$' : "0x") + sOperand; + } + return sOperand; + }; + + /** + * getRegOperand(iReg, type, dbgAddr) + * + * @this {Debugger} + * @param {number} iReg + * @param {number} type + * @param {DbgAddr} dbgAddr + * @return {string} operand + */ + Debugger.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 = Debugger.REGS[iReg]; + if (this.style == Debugger.STYLE_8086 && (type & Debugger.TYPE_MEM)) { + if (iReg == Debugger.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 {Debugger} + * @param {string} sOp + * @param {string|undefined} sOperand + * @param {DbgAddr} 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 + */ + Debugger.prototype.parseInstruction = function(sOp, sOperand, dbgAddr) + { + var aOpBytes = []; + this.println("not supported yet"); + return aOpBytes; + }; + + /** + * getFlagOutput(sFlag) + * + * @this {Debugger} + * @param {string} sFlag + * @return {string} value of flag + */ + Debugger.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 {Debugger} + * @param {number} iReg + * @return {string} + */ + Debugger.prototype.getRegOutput = function(iReg) + { + var sReg = Debugger.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 {Debugger} + * @return {string} + */ + Debugger.prototype.getRegDump = function() + { + var s; + s = this.getRegOutput(Debugger.REG_A) + + this.getRegOutput(Debugger.REG_BC) + + this.getRegOutput(Debugger.REG_DE) + + this.getRegOutput(Debugger.REG_HL) + + this.getRegOutput(Debugger.REG_SP) + + this.getFlagOutput("IF") + this.getFlagOutput("SF") + this.getFlagOutput("ZF") + + this.getFlagOutput("AF") + this.getFlagOutput("PF") + this.getFlagOutput("CF"); + return s; + }; + + Debugger.aBinOpPrecedence = { + '||': 0, // logical OR + '&&': 1, // logical AND + '|': 2, // bitwise OR + '^': 3, // bitwise XOR + '&': 4, // bitwise AND + '!=': 5, // inequality + '==': 5, // equality + '>=': 6, // greater than or equal to + '>': 6, // greater than + '<=': 6, // less than or equal to + '<': 6, // less than + '>>>': 7, // unsigned bitwise right shift + '>>': 7, // bitwise right shift + '<<': 7, // bitwise left shift + '-': 8, // subtraction + '+': 8, // addition + '%': 9, // remainder + '/': 9, // division + '*': 9 // multiplication + }; + + /** + * evalExpression(aVals, aOps, cOps) + * + * In Node, if you set a variable to 0x80000001; ie: + * + * foo=0x80000001|0 + * + * and then calculate foo*foo using "(foo*foo).toString(2)", the result is: + * + * '11111111111111111111111111111100000000000000000000000000000000' + * + * which is slightly incorrect because it has overflowed JavaScript's floating-point precision. + * + * 0x80000001 in decimal is -2147483647, so the product is 4611686014132420609, which is 0x3FFFFFFF00000001. + * + * @this {Debugger} + * @param {Array.} aVals + * @param {Array.} aOps + * @param {number} [cOps] (default is all) + * @return {boolean} true if successful, false if error + */ + Debugger.prototype.evalExpression = function(aVals, aOps, cOps) + { + cOps = cOps || -1; + while (cOps-- && aOps.length) { + var chOp = aOps.pop(); + if (aVals.length < 2) return false; + var valNew; + var val2 = aVals.pop(); + var val1 = aVals.pop(); + switch(chOp) { + case '*': + valNew = val1 * val2; + break; + case '/': + if (!val2) return false; + valNew = val1 / val2; + break; + case '%': + if (!val2) return false; + valNew = val1 % val2; + break; + case '+': + valNew = val1 + val2; + break; + case '-': + valNew = val1 - val2; + break; + case '<<': + valNew = val1 << val2; + break; + case '>>': + valNew = val1 >> val2; + break; + case '>>>': + valNew = val1 >>> val2; + break; + case '<': + valNew = (val1 < val2? 1 : 0); + break; + case '<=': + valNew = (val1 <= val2? 1 : 0); + break; + case '>': + valNew = (val1 > val2? 1 : 0); + break; + case '>=': + valNew = (val1 >= val2? 1 : 0); + break; + case '==': + valNew = (val1 == val2? 1 : 0); + break; + case '!=': + valNew = (val1 != val2? 1 : 0); + break; + case '&': + valNew = val1 & val2; + break; + case '^': + valNew = val1 ^ val2; + break; + case '|': + valNew = val1 | val2; + break; + case '&&': + valNew = (val1 && val2? 1 : 0); + break; + case '||': + valNew = (val1 || val2? 1 : 0); + break; + default: + return false; + } + aVals.push(valNew|0); + } + return true; + }; + + /** + * parseExpression(sExp, fPrint) + * + * A quick-and-dirty expression parser. It takes an expression like: + * + * EDX+EDX*4+12345678 + * + * and builds a value stack in aVals and a "binop" (binary operator) stack in aOps: + * + * aVals aOps + * ----- ---- + * EDX + + * EDX * + * 4 + + * ... + * + * We pop 1 "binop" from aOps and 2 values from aVals whenever a "binop" of lower priority than its + * predecessor is encountered, evaluate, and push the result back onto aVals. + * + * Unary operators like '~' and ternary operators like '?:' are not supported; neither are parentheses. + * + * However, parseReference() now makes it possible to write parenthetical-style sub-expressions by using + * {...} (braces), as well as address references by using [...] (brackets). + * + * Why am I using braces instead of parentheses for sub-expressions? Because parseReference() serves + * multiple purposes, the other being reference replacement in message strings passing through replaceRegs(), + * and I didn't want parentheses taking on a new meaning in message strings. + * + * @this {Debugger} + * @param {string|undefined} sExp + * @param {boolean} [fPrint] is true to print all resolved values, false for quiet parsing + * @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values + */ + Debugger.prototype.parseExpression = function(sExp, fPrint) + { + var value; + + if (sExp) { + /* + * First process (and eliminate) any references, aka sub-expressions. + */ + sExp = this.parseReference(sExp); + + var i = 0; + var fError = false; + var sExpOrig = sExp; + var aVals = [], aOps = []; + /* + * All browsers (including, I believe, IE9 and up) support the following idiosyncrasy of a regexp split(): + * when the regexp uses a capturing pattern, the resulting array will include entries for all the pattern + * matches along with the non-matches. This effectively means that, in the set of expressions that we + * support, all even entries in asValues will contain "values" and all odd entries will contain "operators". + * + * And although I tried to list the supported operators in "precedential" order, bitwise operators must + * be out-of-order so that we don't mistakenly match either '>' or '<' when they're part of '>>' or '<<'. + */ + var regExp = /(\|\||&&|\||^|&|!=|==|>=|>>>|>>|>|<=|<<|<|-|\+|%|\/|\*)/; + var asValues = sExp.split(regExp); + while (i < asValues.length) { + var sValue = asValues[i++]; + var cchValue = sValue.length; + var s = str.trim(sValue); + if (!s) { + fError = true; + break; + } + var v = this.parseValue(s, null, fPrint === false); + if (v === undefined) { + fError = true; + fPrint = false; + break; + } + aVals.push(v); + if (i == asValues.length) break; + var sOp = asValues[i++], cchOp = sOp.length; + this.assert(Debugger.aBinOpPrecedence[sOp] != null); + if (aOps.length && Debugger.aBinOpPrecedence[sOp] < Debugger.aBinOpPrecedence[aOps[aOps.length-1]]) { + this.evalExpression(aVals, aOps, 1); + } + aOps.push(sOp); + sExp = sExp.substr(cchValue + cchOp); + } + if (!this.evalExpression(aVals, aOps) || aVals.length != 1) { + fError = true; + } + if (!fError) { + value = aVals.pop(); + if (fPrint) this.printValue(null, value); + } else { + if (fPrint) this.println("error parsing '" + sExpOrig + "' at character " + (sExpOrig.length - sExp.length)); + } + } + return value; + }; + + /** + * parseReference(s) + * + * Returns the given string with any "{expression}" sequences replaced with the value of the expression, + * and any "[address]" references replaced with the contents of the address. Expressions are parsed BEFORE + * addresses. Owing to this function's simplistic parsing, nested braces/brackets are not supported + * (define intermediate variables if needed). + * + * @this {Debugger} + * @param {string} s + * @return {string} + */ + Debugger.prototype.parseReference = function(s) + { + var a; + while (a = s.match(/\{(.*?)}/)) { + if (a[1].indexOf('{') >= 0) break; // unsupported nested brace(s) + var value = this.parseExpression(a[1]); + s = s.replace('{' + a[1] + '}', value != null? str.toHex(value) : "undefined"); + } + while (a = s.match(/\[(.*?)]/)) { + if (a[1].indexOf('[') >= 0) break; // unsupported nested bracket(s) + var dbgAddr = this.parseAddr(a[1]); + s = s.replace('[' + a[1] + ']', dbgAddr? str.toHex(this.getWord(dbgAddr), 4) : "undefined"); + } + return this.parseSysVars(s); + }; + + /** + * parseSysVars(s) + * + * Returns the given string with any recognized "$var" replaced with its value; eg: + * + * $ops: the number of opcodes executed since the last time it was displayed (or reset) + * + * @this {Debugger} + * @param {string} s + * @return {string} + */ + Debugger.prototype.parseSysVars = function(s) + { + var a; + while (a = s.match(/\$([a-z]+)/i)) { + var v = null; + switch(a[1].toLowerCase()) { + case "ops": + v = this.cOpcodes - this.cOpcodesStart; + break; + } + if (v == null) break; + s = s.replace(a[0], v.toString()); + } + return s; + }; + + /** + * parseValue(sValue, sName, fQuiet) + * + * @this {Debugger} + * @param {string|undefined} sValue + * @param {string|null} [sName] is the name of the value, if any + * @param {boolean} [fQuiet] + * @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid + */ + Debugger.prototype.parseValue = function(sValue, sName, fQuiet) + { + var value; + if (sValue !== undefined) { + var iReg = this.getRegIndex(sValue); + if (iReg >= 0) { + value = this.getRegValue(iReg); + } else { + value = this.getVariable(sValue); + if (value === undefined) value = str.parseInt(sValue); + } + if (value === undefined && !fQuiet) this.println("invalid " + (sName? sName : "value") + ": " + sValue); + } else { + if (!fQuiet) this.println("missing " + (sName || "value")); + } + return value; + }; + + /** + * printValue(sVar, value) + * + * @this {Debugger} + * @param {string|null} sVar + * @param {number|undefined} value + * @return {boolean} true if value defined, false if not + */ + Debugger.prototype.printValue = function(sVar, value) + { + var sValue; + var fDefined = false; + if (value !== undefined) { + fDefined = true; + sValue = str.toHexLong(value) + " " + value + ". (" + str.toBinBytes(value) + ")"; + } + sVar = (sVar != null? (sVar + ": ") : ""); + this.println(sVar + sValue); + return fDefined; + }; + + /** + * printVariable(sVar) + * + * @this {Debugger} + * @param {string} [sVar] + * @return {boolean} true if all value(s) defined, false if not + */ + Debugger.prototype.printVariable = function(sVar) + { + if (sVar) { + return this.printValue(sVar, this.aVariables[sVar]); + } + var cVariables = 0; + for (sVar in this.aVariables) { + this.printValue(sVar, this.aVariables[sVar]); + cVariables++; + } + return cVariables > 0; + }; + + /** + * delVariable(sVar) + * + * @this {Debugger} + * @param {string} sVar + */ + Debugger.prototype.delVariable = function(sVar) + { + delete this.aVariables[sVar]; + }; + + /** + * getVariable(sVar) + * + * @this {Debugger} + * @param {string} sVar + * @return {number|undefined} + */ + Debugger.prototype.getVariable = function(sVar) + { + return this.aVariables[sVar]; + }; + + /** + * setVariable(sVar, value) + * + * @this {Debugger} + * @param {string} sVar + * @param {number} value + */ + Debugger.prototype.setVariable = function(sVar, value) + { + this.aVariables[sVar] = value; + }; + + /** + * comparePairs(p1, p2) + * + * @this {Debugger} + * @param {number|string|Array|Object} p1 + * @param {number|string|Array|Object} p2 + * @return {number} + */ + Debugger.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 {Debugger} + * @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) + */ + Debugger.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 {Debugger} + */ + Debugger.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @param {boolean} [fNearest] + * @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment + */ + Debugger.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 {Debugger} + * @param {string} sSymbol + * @return {DbgAddr|undefined} + */ + Debugger.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 + */ + Debugger.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 {Debugger} + */ + Debugger.prototype.doHelp = function() + { + var s = "commands:"; + for (var sCommand in Debugger.COMMANDS) { + s += '\n' + str.pad(sCommand, 7) + Debugger.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 {Debugger} + * @param {Array.} asArgs is the complete argument array, beginning with the "a" command in asArgs[0] + */ + Debugger.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 {Debugger} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + */ + Debugger.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 {Debugger} + * @param {string} [sCmd] (eg, "cls" or "clear") + */ + Debugger.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 {Debugger} + * @param {Array.} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes]) + */ + Debugger.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 Debugger.MESSAGES) { + 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 Debugger.MESSAGES) { + 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 {Debugger} + * @param {Array.} asArgs + */ + Debugger.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 0x" + str.toHex(vOld, cch) + " to 0x" + str.toHex(vNew, cch)); + fnSet.call(this, dbgAddr, vNew, size); + } + }; + + /** + * doFreqs(sParm) + * + * @this {Debugger} + * @param {string|undefined} sParm + */ + Debugger.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 != Debugger.STYLE_8086? Debugger.INS_NAMES : Debugger.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 {Debugger} + * @param {boolean} [fQuiet] + */ + Debugger.prototype.doHalt = function(fQuiet) + { + var sMsg; + if (this.flags.fRunning) { + 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 {Debugger} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error) + */ + Debugger.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 {Debugger} + * @param {Array.} asArgs + * @return {boolean} true only if the instruction info command ("n") is supported + */ + Debugger.prototype.doInfo = function(asArgs) + { + if (DEBUG) { + this.println("msPerYield: " + this.cpu.aCounts.msPerYield); + this.println("nCyclesPerBurst: " + this.cpu.aCounts.nCyclesPerBurst); + this.println("nCyclesPerYield: " + this.cpu.aCounts.nCyclesPerYield); + this.println("nCyclesPerVideoUpdate: " + this.cpu.aCounts.nCyclesPerVideoUpdate); + this.println("nCyclesPerStatusUpdate: " + this.cpu.aCounts.nCyclesPerStatusUpdate); + return true; + } + return false; + }; + + /** + * doInput(sPort) + * + * Simulate a 1-byte port input operation. + * + * @this {Debugger} + * @param {string|undefined} sPort + */ + Debugger.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)); + } + }; + + /** + * 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 {Debugger} + * @param {string} sCmd + * @return {boolean} true if valid "var" assignment, false if not + */ + Debugger.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 {Debugger} + * @param {string} sAddr + * @param {boolean} [fPrint] + * @return {string|null} + */ + Debugger.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 {Debugger} + * @param {Array.} asArgs + */ + Debugger.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) & ~(Messages.HALT | Messages.KEYS | Messages.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 Debugger.MESSAGES) { + if (sCategory == m) { + bitsMessage = Debugger.MESSAGES[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; + } + } + } + + /* + * Display those message categories that match the current criteria (on or off) + */ + var n = 0; + var sCategories = ""; + for (m in Debugger.MESSAGES) { + if (!sCategory || sCategory == m) { + var bitMessage = Debugger.MESSAGES[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 Messages.INT was turned on + }; + + /** + * doOptions(asArgs) + * + * @this {Debugger} + * @param {Array.} asArgs + */ + Debugger.prototype.doOptions = function(asArgs) + { + switch (asArgs[1]) { + case "8080": + this.style = Debugger.STYLE_8080; + break; + + case "8086": + this.style = Debugger.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.aCounts.nCyclesChecksumInterval = nCycles; + break; + case "start": + this.cpu.aCounts.nCyclesChecksumStart = nCycles; + break; + case "stop": + this.cpu.aCounts.nCyclesChecksumStop = nCycles; + break; + default: + this.println("unknown cs option"); + return; + } + if (nCycles !== undefined) { + this.cpu.resetChecksum(); + } + this.println("checksums " + (this.cpu.flags.fChecksum? "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 {Debugger} + * @param {string|undefined} sPort + * @param {string|undefined} sByte (string representation of 1 byte) + */ + Debugger.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 {Debugger} + * @param {Array.} [asArgs] + * @param {boolean} [fInstruction] (true to include the current instruction; default is true) + */ + Debugger.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 fValid = false; + var w = this.parseExpression(sValue); + + if (w !== undefined) { + fValid = true; + 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; + } + } + if (!fValid) { + this.println("invalid value: " + sValue); + 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 {Debugger} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + * @param {boolean} [fQuiet] + */ + Debugger.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 {Debugger} + * @param {string} sCmd + */ + Debugger.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 {Debugger} + * @param {string} [sCmd] "p" or "pr" + */ + Debugger.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 CPUDef.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 {Debugger} + * @param {DbgAddr} dbgAddr + * @return {string|null} CALL instruction at or near dbgAddr, or null if none + */ + Debugger.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 {Debugger} + * @param {string} [sCmd] + * @param {string} [sAddr] (not used yet) + */ + Debugger.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 {Debugger} + * @param {string} [sCmd] ("t", "tc", or "tr") + * @param {string} [sCount] # of instructions to step + */ + Debugger.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 {Debugger} + * @param {string} [sAddr] + * @param {string} [sAddrEnd] + * @param {number} [n] + */ + Debugger.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 {Debugger} + * @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.} + */ + Debugger.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 {Debugger} + * @param {Array.} asArgs + * @return {Array.} + */ + Debugger.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 {Debugger} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if command processed, false if unrecognized + */ + Debugger.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; + } + 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((APPNAME + " version " + (XMLVERSION || APPVERSION) + " (" + this.cpu.model + (COMPILED? ",RELEASE" : (DEBUG? ",DEBUG" : ",NODEBUG")) + (TYPEDARRAYS? ",TYPEDARRAYS" : (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 {Debugger} + * @param {string} sCmds + * @param {boolean} [fSave] + * @return {boolean} true if all commands processed, false if not + */ + Debugger.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; + }; + + /** + * Debugger.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. + */ + Debugger.init = function() + { + var aeDbg = Component.getElementsByClass(document, PCJSCLASS, "debugger"); + for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) { + var eDbg = aeDbg[iDbg]; + var parmsDbg = Component.getComponentParms(eDbg); + var dbg = new Debugger(parmsDbg); + Component.bindComponentControls(dbg, eDbg, PCJSCLASS); + } + }; + + /* + * Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-)) + */ + web.onInit(Debugger.init); + +} // endif DEBUGGER + +if (NODE) module.exports = Debugger; diff --git a/modules/pc6502/lib/defines.js b/modules/pc6502/lib/defines.js new file mode 100644 index 000000000..a0be1c910 --- /dev/null +++ b/modules/pc6502/lib/defines.js @@ -0,0 +1,81 @@ +/** + * @fileoverview PC6502-specific compile-time definitions. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +/** + * @define {string} + */ +var PCJSCLASS = "pc8080"; // this @define is the default application class (formerly APPCLASS) to use + +/** + * @define {boolean} + * + * WARNING: DEBUGGER needs to accurately reflect whether or not the Debugger component is (or will be) loaded. + * In the compiled case, we rely on the Closure Compiler to override DEBUGGER as appropriate. When it's *false*, + * nearly all of debugger.js will be conditionally removed by the compiler, reducing it to little more than a + * "type skeleton", which also solves some type-related warnings we would otherwise have if we tried to remove + * debugger.js from the compilation process altogether. + * + * However, when we're in "development mode" and running uncompiled code in debugger-less configurations, + * I would like to skip loading debugger.js altogether. When doing that, we must ALSO arrange for an additional file + * (nodebugger.js) to be loaded immediately after this file, which *explicitly* overrides DEBUGGER with *false*. + */ +var DEBUGGER = true; // this @define is overridden by the Closure Compiler to remove Debugger-related support + +/** + * @define {boolean} + * + * BYTEARRAYS is a Closure Compiler compile-time option that allocates an Array of numbers for every Memory block, + * where each a number represents ONE byte; very wasteful, but potentially slightly faster. + * + * See the Memory component for details. + */ +var BYTEARRAYS = false; + +/** + * TYPEDARRAYS enables use of typed arrays for Memory blocks. This used to be a compile-time-only option, but I've + * added Memory access functions for typed arrays (see Memory.afnTypedArray), so support can be enabled dynamically now. + * + * See the Memory component for details. + */ +var TYPEDARRAYS = (typeof ArrayBuffer !== 'undefined'); + +if (NODE) { + global.PCJSCLASS = PCJSCLASS; + global.DEBUGGER = DEBUGGER; + global.BYTEARRAYS = BYTEARRAYS; + global.TYPEDARRAYS = TYPEDARRAYS; + /* + * TODO: When we're "required" by Node, should we return anything via module.exports? + */ +} diff --git a/modules/pc6502/lib/keyboard.js b/modules/pc6502/lib/keyboard.js new file mode 100644 index 000000000..586b3e735 --- /dev/null +++ b/modules/pc6502/lib/keyboard.js @@ -0,0 +1,237 @@ +/** + * @fileoverview Implements the PC6502 Keyboard component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var Messages = require("./messages"); + var State = require("./state"); + var CPU = require("./cpu"); +} + +/** + * Keyboard(parmsKbd) + * + * @constructor + * @extends Component + * @param {Object} parmsKbd + */ +function Keyboard(parmsKbd) +{ + Component.call(this, "Keyboard", parmsKbd, Keyboard, Messages.KEYBOARD); + + this.setReady(); +} + +Component.subclass(Keyboard); + +/** + * Alphanumeric and other common (printable) ASCII codes. + * + * TODO: Determine what we can do to get ALL constants like these inlined (enum doesn't seem to + * get the job done); the problem seems to be limited to property references that use quotes, which + * is why I've 'unquoted' as many of them as possible. + * + * @enum {number} + */ +Keyboard.ASCII = { + CTRL_A: 1, CTRL_C: 3, CTRL_Z: 26, + ' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39, + '(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47, + '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, + '8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63, + '@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, + H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, + P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, + X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95, + '`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103, + h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111, + p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119, + x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126 +}; + +/** + * Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric + * or function keys, some which may require special treatment (eg, preventDefault() if returning false on + * the initial keyDown event is insufficient). + * + * keyCodes for most common ASCII keys can simply use the appropriate ASCII code above. + * + * Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined + * them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37). This conflict is + * discussed further in the definition of CLICKCODE below. + * + * @enum {number} + */ +Keyboard.KEYCODE = { + /* 0x08 */ BS: 8, + /* 0x09 */ TAB: 9, + /* 0x0A */ LF: 10, + /* 0x0D */ CR: 13, + /* 0x10 */ SHIFT: 16, + /* 0x11 */ CTRL: 17, + /* 0x12 */ ALT: 18, + /* 0x13 */ PAUSE: 19, // PAUSE/BREAK + /* 0x14 */ CAPS_LOCK: 20, + /* 0x1B */ ESC: 27, + /* 0x20 */ SPACE: 32, + /* 0x21 */ PGUP: 33, + /* 0x22 */ PGDN: 34, + /* 0x23 */ END: 35, + /* 0x24 */ HOME: 36, + /* 0x25 */ LEFT: 37, + /* 0x26 */ UP: 38, + /* 0x27 */ RIGHT: 39, + /* 0x27 */ FF_QUOTE: 39, + /* 0x28 */ DOWN: 40, + /* 0x2C */ FF_COMMA: 44, + /* 0x2C */ PRTSC: 44, + /* 0x2D */ INS: 45, + /* 0x2E */ DEL: 46, + /* 0x2E */ FF_PERIOD: 46, + /* 0x2F */ FF_SLASH: 47, + /* 0x3B */ FF_SEMI: 59, + /* 0x3D */ FF_EQUALS: 61, + /* 0x5B */ CMD: 91, // aka WIN + /* 0x5B */ FF_LBRACK: 91, + /* 0x5C */ FF_BSLASH: 92, + /* 0x5D */ RCMD: 93, // aka MENU + /* 0x5D */ FF_RBRACK: 93, + /* 0x60 */ NUM_INS: 96, // 0 + /* 0x60 */ FF_BQUOTE: 96, + /* 0x61 */ NUM_END: 97, // 1 + /* 0x62 */ NUM_DOWN: 98, // 2 + /* 0x63 */ NUM_PGDN: 99, // 3 + /* 0x64 */ NUM_LEFT: 100, // 4 + /* 0x65 */ NUM_CENTER: 101, // 5 + /* 0x66 */ NUM_RIGHT: 102, // 6 + /* 0x67 */ NUM_HOME: 103, // 7 + /* 0x68 */ NUM_UP: 104, // 8 + /* 0x69 */ NUM_PGUP: 105, // 9 + /* 0x6A */ NUM_MUL: 106, + /* 0x6B */ NUM_ADD: 107, + /* 0x6D */ NUM_SUB: 109, + /* 0x6E */ NUM_DEL: 110, // . + /* 0x6F */ NUM_DIV: 111, + /* 0x70 */ F1: 112, + /* 0x71 */ F2: 113, + /* 0x72 */ F3: 114, + /* 0x73 */ F4: 115, + /* 0x74 */ F5: 116, + /* 0x75 */ F6: 117, + /* 0x76 */ F7: 118, + /* 0x77 */ F8: 119, + /* 0x78 */ F9: 120, + /* 0x79 */ F10: 121, + /* 0x7A */ F11: 122, + /* 0x7B */ F12: 123, + /* 0x90 */ NUM_LOCK: 144, + /* 0x91 */ SCROLL_LOCK: 145, + /* 0xAD */ FF_DASH: 173, + /* 0xBA */ SEMI: 186, // Firefox: 59 + /* 0xBB */ EQUALS: 187, // Firefox: 61 + /* 0xBC */ COMMA: 188, // Firefox: 44 + /* 0xBD */ DASH: 189, // Firefox: 173 + /* 0xBE */ PERIOD: 190, // Firefox: 46 + /* 0xBF */ SLASH: 191, // Firefox: 47 + /* 0xC0 */ BQUOTE: 192, // Firefox: 96 + /* 0xDB */ LBRACK: 219, // Firefox: 91 + /* 0xDC */ BSLASH: 220, // Firefox: 92 + /* 0xDD */ RBRACK: 221, // Firefox: 93 + /* 0xDE */ QUOTE: 222, // Firefox: 39 + /* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD) + // + // The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD), + // where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4. + // + // Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really + // been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I. + // + // ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press". + // + ONDOWN: 1000, + // + // ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left") + // + ONRIGHT: 2000, + // + // FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations. + // The actual values are for internal use only and merely need to be unique and used consistently. + // + FAKE: 4000 +}; + +/* + * Maps "stupid" keyCodes to their "non-stupid" counterparts + */ +Keyboard.STUPID_KEYCODES = {}; +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.SEMI] = Keyboard.ASCII[';']; // 186 -> 59 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.EQUALS] = Keyboard.ASCII['=']; // 187 -> 61 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.COMMA] = Keyboard.ASCII[',']; // 188 -> 44 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.DASH] = Keyboard.ASCII['-']; // 189 -> 45 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.PERIOD] = Keyboard.ASCII['.']; // 190 -> 46 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.SLASH] = Keyboard.ASCII['/']; // 191 -> 47 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.BQUOTE] = Keyboard.ASCII['`']; // 192 -> 96 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.LBRACK] = Keyboard.ASCII['[']; // 219 -> 91 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.BSLASH] = Keyboard.ASCII['\\']; // 220 -> 92 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.RBRACK] = Keyboard.ASCII[']']; // 221 -> 93 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.QUOTE] = Keyboard.ASCII["'"]; // 222 -> 39 +Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.FF_DASH] = Keyboard.ASCII['-']; + +/** + * Keyboard.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. + */ +Keyboard.init = function() +{ + var aeKbd = Component.getElementsByClass(document, PCJSCLASS, "keyboard"); + for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) { + var eKbd = aeKbd[iKbd]; + var parmsKbd = Component.getComponentParms(eKbd); + var kbd = new Keyboard(parmsKbd); + Component.bindComponentControls(kbd, eKbd, PCJSCLASS); + } +}; + +/* + * Initialize every Keyboard module on the page. + */ +web.onInit(Keyboard.init); + +if (NODE) module.exports = Keyboard; diff --git a/modules/pc6502/lib/memory.js b/modules/pc6502/lib/memory.js new file mode 100644 index 000000000..5dba300ca --- /dev/null +++ b/modules/pc6502/lib/memory.js @@ -0,0 +1,825 @@ +/** + * @fileoverview Implements the PC6502 Memory component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var Component = require("../../shared/lib/component"); + var Messages = require("./messages"); + var CPUDef = require("./cpudef"); +} + +/** + * @class DataView + * @property {function(number,boolean):number} getUint8 + * @property {function(number,number,boolean)} setUint8 + * @property {function(number,boolean):number} getUint16 + * @property {function(number,number,boolean)} setUint16 + * @property {function(number,boolean):number} getInt32 + * @property {function(number,number,boolean)} setInt32 + */ + +var littleEndian = (TYPEDARRAYS? (function() { + var buffer = new ArrayBuffer(2); + new DataView(buffer).setUint16(0, 256, true); + return new Uint16Array(buffer)[0] === 256; +})() : false); + +/** + * Memory(addr, used, size, type) + * + * The Bus component allocates Memory 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 Memory 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 Memory objects as the ranges require. Partial Memory blocks could + * also be supported in theory, but in practice, they're not. + * + * Because Memory 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 Memory 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 Memory.TYPE constants (default is Memory.TYPE.NONE) + */ +function Memory(addr, used, size, type) +{ + var i; + this.id = (Memory.idBlock += 2); + this.adw = null; + this.offset = 0; + this.addr = addr; + this.used = used; + this.size = size || 0; + this.type = type || Memory.TYPE.NONE; + this.fReadOnly = (type == Memory.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). + * + * 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? Memory.afnArrayLE : Memory.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(Memory.afnMemory); + } +} + +/* + * Basic memory types + * + * RAM is the most conventional memory type, providing full read/write capability to x86-compatible (ie, + * 'little endian") storage. ROM is equally conventional, except that the fReadOnly property is set, + * disabling writes. VIDEO is treated exactly like RAM, unless a controller is provided. Both RAM and + * VIDEO memory are always considered writable, and even ROM can be written using the Bus setByteDirect() + * interface (which in turn uses the Memory writeByteDirect() interface), allowing the ROM component to + * initialize its own memory. The CTRL type is used to identify memory-mapped devices that do not need + * any default storage and always provide their own controller. + * + * Unallocated regions of the address space contain a special memory block of type NONE that contains + * no storage. Mapping every addressible location to a memory block allows all accesses to be routed in + * exactly the same manner, without resorting to any range or processor checks. + * + * These types are not mutually exclusive. For example, VIDEO memory could be allocated as RAM, with or + * without a custom controller (the original Monochrome and CGA video cards used read/write storage that + * was indistinguishable from RAM), and CTRL memory could be allocated as an empty block of any type, with + * a custom controller. A few types are required for certain features (eg, ROM is required if you want + * read-only memory), but the larger purpose of these types is to help document the caller's intent and to + * provide the Control Panel with the ability to highlight memory regions accordingly. + */ +Memory.TYPE = { + NONE: 0, + RAM: 1, + ROM: 2, + VIDEO: 3, + CTRL: 4, + COLORS: ["black", "blue", "green", "cyan"], + NAMES: ["NONE", "RAM", "ROM", "VID", "H/W"] +}; + +/* + * Last used block ID (used for debugging only) + */ +Memory.idBlock = 0; + +/** + * adjustEndian(dw) + * + * @param {number} dw + * @return {number} + */ +Memory.adjustEndian = function(dw) { + if (TYPEDARRAYS && !littleEndian) { + dw = (dw << 24) | ((dw << 8) & 0x00ff0000) | ((dw >> 8) & 0x0000ff00) | (dw >>> 24); + } + return dw; +}; + +Memory.prototype = { + constructor: Memory, + parent: null, + /** + * init(addr) + * + * Quick reinitializer when reusing a Memory block. + * + * @this {Memory} + * @param {number} addr + */ + init: function(addr) { + this.addr = addr; + }, + /** + * clone(mem, type) + * + * Converts the current Memory block (this) into a clone of the given Memory block (mem), + * and optionally overrides the current block's type with the specified type. + * + * @this {Memory} + * @param {Memory} mem + * @param {number} [type] + * @param {Debugger} [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 == Memory.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? Memory.afnArrayLE : Memory.afnArrayBE); + } else { + if (BYTEARRAYS) { + this.ab = mem.ab; + } else { + this.adw = mem.adw; + } + this.setAccess(Memory.afnMemory); + } + this.copyBreakpoints(dbg, mem); + }, + /** + * save() + * + * This gets the contents of a Memory block as an array of 32-bit values; used by Bus.saveMemory(), + * which in turn is called by CPUState.save(). + * + * Memory blocks with custom memory controllers do NOT save their contents; that's the responsibility + * of the controller component. + * + * @this {Memory} + * @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 Memory block from an array of 32-bit values; + * used by Bus.restoreMemory(), which is called by CPUState.restore(), after all other + * components have been restored and thus all Memory blocks have been allocated + * by their respective components. + * + * @this {Memory} + * @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 Memory type. + * + * @this {Memory} + * @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 == Memory.TYPE.NONE); + afn = Memory.afnNone; + } + this.setReadAccess(afn, fDirect); + this.setWriteAccess(afn, fDirect); + }, + /** + * setReadAccess(afn, fDirect) + * + * @this {Memory} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setReadAccess: function(afn, fDirect) { + if (!fDirect || !this.cReadBreakpoints) { + this.readByte = afn[0] || this.readNone; + this.readShort = afn[1] || this.readShortDefault; + } + if (fDirect || fDirect === undefined) { + this.readByteDirect = afn[0] || this.readNone; + this.readShortDirect = afn[1] || this.readShortDefault; + } + }, + /** + * setWriteAccess(afn, fDirect) + * + * @this {Memory} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setWriteAccess: function(afn, fDirect) { + if (!fDirect || !this.cWriteBreakpoints) { + this.writeByte = !this.fReadOnly && afn[2] || this.writeNone; + this.writeShort = !this.fReadOnly && afn[3] || this.writeShortDefault; + } + if (fDirect || fDirect === undefined) { + this.writeByteDirect = afn[2] || this.writeNone; + this.writeShortDirect = afn[3] || this.writeShortDefault; + } + }, + /** + * resetReadAccess() + * + * @this {Memory} + */ + resetReadAccess: function() { + this.readByte = this.readByteDirect; + this.readShort = this.readShortDirect; + }, + /** + * resetWriteAccess() + * + * @this {Memory} + */ + resetWriteAccess: function() { + this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect; + this.writeShort = this.fReadOnly? this.writeShortDefault : this.writeShortDirect; + }, + /** + * printAddr(sMessage) + * + * @this {Memory} + * @param {string} sMessage + */ + printAddr: function(sMessage) { + if (DEBUG && this.dbg && this.dbg.messageEnabled(Messages.MEM)) { + this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('%' + str.toHex(this.addr)) : '#' + this.id), true); + } + }, + /** + * addBreakpoint(off, fWrite) + * + * @this {Memory} + * @param {number} off + * @param {boolean} fWrite + */ + addBreakpoint: function(off, fWrite) { + if (!fWrite) { + if (this.cReadBreakpoints++ === 0) { + this.setReadAccess(Memory.afnChecked, false); + } + if (DEBUG) this.printAddr("read breakpoint added to memory block"); + } + else { + if (this.cWriteBreakpoints++ === 0) { + this.setWriteAccess(Memory.afnChecked, false); + } + if (DEBUG) this.printAddr("write breakpoint added to memory block"); + } + }, + /** + * removeBreakpoint(off, fWrite) + * + * @this {Memory} + * @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 {Memory} + * @param {Debugger} [dbg] + * @param {Memory} [mem] (outgoing Memory 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(Memory.afnChecked, false); + } + if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) { + this.setWriteAccess(Memory.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 + * Memory interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively. + * + * @this {Memory} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readNone: function readNone(off, addr) { + if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages.CPU | Messages.MEM) /* && !off */) { + this.dbg.message("attempt to read invalid block %" + str.toHex(this.addr), true); + } + return 0xff; + }, + /** + * writeNone(off, v, addr) + * + * @this {Memory} + * @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(Messages.CPU | Messages.MEM) /* && !off */) { + this.dbg.message("attempt to write " + str.toHexWord(v) + " to invalid block %" + str.toHex(this.addr), true); + } + }, + /** + * readShortDefault(off, addr) + * + * @this {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteBE: function readByteBE(off, addr) { + return this.ab[off]; + }, + /** + * readByteLE(off, addr) + * + * @this {Memory} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readByteLE: function readByteLE(off, addr) { + return this.ab[off]; + }, + /** + * readShortBE(off, addr) + * + * @this {Memory} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readShortBE: function readShortBE(off, addr) { + return this.dv.getUint16(off, true); + }, + /** + * readShortLE(off, addr) + * + * @this {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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 {Memory} + * @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() + * uses these defaults when any of the 6 handlers (ie, 3 read handlers and 3 write handlers) are undefined. + * +Memory.afnNone = [Memory.prototype.readNone, Memory.prototype.readShortDefault, Memory.prototype.writeNone, Memory.prototype.writeShortDefault]; + */ + +Memory.afnNone = []; +Memory.afnMemory = [Memory.prototype.readByteMemory, Memory.prototype.readShortMemory, Memory.prototype.writeByteMemory, Memory.prototype.writeShortMemory]; +Memory.afnChecked = [Memory.prototype.readByteChecked, Memory.prototype.readShortChecked, Memory.prototype.writeByteChecked, Memory.prototype.writeShortChecked]; + +if (TYPEDARRAYS) { + Memory.afnArrayBE = [Memory.prototype.readByteBE, Memory.prototype.readShortBE, Memory.prototype.writeByteBE, Memory.prototype.writeShortBE]; + Memory.afnArrayLE = [Memory.prototype.readByteLE, Memory.prototype.readShortLE, Memory.prototype.writeByteLE, Memory.prototype.writeShortLE]; +} + +if (NODE) module.exports = Memory; diff --git a/modules/pc6502/lib/messages.js b/modules/pc6502/lib/messages.js new file mode 100644 index 000000000..567de03a0 --- /dev/null +++ b/modules/pc6502/lib/messages.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Defines message categories. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +var Messages = { + CPU: 0x00000001, + BUS: 0x00000040, + MEM: 0x00000080, + PORT: 0x00000100, + CHIPSET: 0x00008000, + KEYBOARD: 0x00010000, + KEYS: 0x00020000, + VIDEO: 0x00040000, + FDC: 0x00080000, + DISK: 0x00200000, + SERIAL: 0x00800000, + SPEAKER: 0x02000000, + COMPUTER: 0x04000000, + LOG: 0x20000000, + WARN: 0x40000000, + HALT: 0x80000000|0 +}; + +if (NODE) module.exports = Messages; diff --git a/modules/pc6502/lib/nodebugger.js b/modules/pc6502/lib/nodebugger.js new file mode 100644 index 000000000..1fd18747d --- /dev/null +++ b/modules/pc6502/lib/nodebugger.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Compile-time definitions for Debugger-less configurations. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +/* + * WARNING: DEBUGGER needs to accurately reflect whether or not the Debugger component is (or will be) loaded. + * In the compiled case, we rely on the Closure Compiler to override DEBUGGER as appropriate. When it's *false*, + * nearly all of debugger.js will be conditionally removed by the compiler, reducing it to little more than a + * "type skeleton", which also solves some type-related warnings we would otherwise have if we tried to remove + * debugger.js from the compilation process altogether. + * + * However, when we're in "development mode" and running uncompiled code in debugger-less configurations, + * I would still like to skip loading debugger.js altogether. To do that, we must arrange for this additional file, + * nodebugger.js, to be loaded immediately after defines.js, *explicitly* overriding the previously defined value + * of DEBUGGER with *false*. + */ +DEBUGGER = false; diff --git a/modules/pc6502/lib/panel.js b/modules/pc6502/lib/panel.js new file mode 100644 index 000000000..ec9b248b1 --- /dev/null +++ b/modules/pc6502/lib/panel.js @@ -0,0 +1,185 @@ +/** + * @fileoverview Implements the PC6502 Panel component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"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 Bus = require("./bus"); + var Memory = require("./memory"); + var CPUDef = require("./cpudef"); +} + +/** + * Panel(parmsPanel) + * + * The Panel component has no required (parmsPanel) properties. + * + * @constructor + * @extends Component + * @param {Object} parmsPanel + */ +function Panel(parmsPanel) +{ + Component.call(this, "Panel", parmsPanel, Panel); +} + +Component.subclass(Panel); + +/** + * 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 {Panel} + * @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 + */ +Panel.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 {Panel} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPUState} cpu + * @param {Debugger} dbg + */ +Panel.prototype.initBus = function(cmp, bus, cpu, dbg) +{ + this.cmp = cmp; + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.kbd = cmp.getMachineComponent("Keyboard"); +}; + +/** + * powerUp(data, fRepower) + * + * @this {Panel} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ +Panel.prototype.powerUp = function(data, fRepower) +{ + if (!fRepower) Panel.init(); + return true; +}; + +/** + * powerDown(fSave, fShutdown) + * + * @this {Panel} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ +Panel.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 {Panel} + * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) + */ +Panel.prototype.updateStatus = function(fForce) +{ +}; + +/** + * Panel.init() + * + * This function operates on every HTML element of class "panel", extracting the + * JSON-encoded parameters for the Panel constructor from the element's "data-value" + * attribute, invoking the constructor to create a Panel 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. + */ +Panel.init = function() +{ + var fReady = false; + var aePanels = Component.getElementsByClass(document, PCJSCLASS, "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 Panel(parmsPanel); + } + Component.bindComponentControls(panel, ePanel, PCJSCLASS); + if (fReady) panel.setReady(); + } +}; + +/* + * Initialize every Panel module on the page. + */ +web.onInit(Panel.init); + +if (NODE) module.exports = Panel; diff --git a/modules/pc6502/lib/ram.js b/modules/pc6502/lib/ram.js new file mode 100644 index 000000000..9eb3e874d --- /dev/null +++ b/modules/pc6502/lib/ram.js @@ -0,0 +1,197 @@ +/** + * @fileoverview Implements the PC6502 RAM component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var str = require("../../shared/lib/strlib"); + var web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var Memory = require("./memory"); + var ROM = require("./rom"); + var State = require("./state"); +} + +/** + * RAM(parmsRAM) + * + * The RAM 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) + * + * 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 + */ +function RAM(parmsRAM) +{ + Component.call(this, "RAM", parmsRAM, RAM); + + this.addrRAM = parmsRAM['addr']; + this.sizeRAM = parmsRAM['size']; + this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified + this.fAllocated = false; +} + +Component.subclass(RAM); + +/** + * initBus(cmp, bus, cpu, dbg) + * + * @this {RAM} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPUState} cpu + * @param {Debugger} dbg + */ +RAM.prototype.initBus = function(cmp, bus, cpu, dbg) +{ + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.setReady(); +}; + +/** + * powerUp(data, fRepower) + * + * @this {RAM} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ +RAM.prototype.powerUp = function(data, fRepower) +{ + if (!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. We just need to call reset(), to allocate memory for the RAM. + */ + this.reset(); + } + return true; +}; + +/** + * powerDown(fSave, fShutdown) + * + * @this {RAM} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ +RAM.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 (fSave)? this.save() : true; +}; + +/** + * reset() + * + * @this {RAM} + */ +RAM.prototype.reset = function() +{ + if (!this.fAllocated && this.sizeRAM) { + if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory.TYPE.RAM)) { + this.fAllocated = true; + } + } + if (!this.fAllocated) { + Component.error("No RAM allocated"); + } +}; + +/** + * save() + * + * This implements save support for the RAM component. + * + * @this {RAM} + * @return {Object} + */ +RAM.prototype.save = function() +{ + return null; +}; + +/** + * restore(data) + * + * This implements restore support for the RAM component. + * + * @this {RAM} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ +RAM.prototype.restore = function(data) +{ + return true; +}; + +/** + * RAM.init() + * + * This function operates on every HTML element of class "ram", extracting the + * JSON-encoded parameters for the RAM constructor from the element's "data-value" + * attribute, invoking the constructor to create a RAM component, and then binding + * any associated HTML controls to the new component. + */ +RAM.init = function() +{ + var aeRAM = Component.getElementsByClass(document, PCJSCLASS, "ram"); + for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) { + var eRAM = aeRAM[iRAM]; + var parmsRAM = Component.getComponentParms(eRAM); + var ram = new RAM(parmsRAM); + Component.bindComponentControls(ram, eRAM, PCJSCLASS); + } +}; + +/* + * Initialize all the RAM modules on the page. + */ +web.onInit(RAM.init); + +if (NODE) module.exports = RAM; diff --git a/modules/pc6502/lib/rom.js b/modules/pc6502/lib/rom.js new file mode 100644 index 000000000..996c3d3ab --- /dev/null +++ b/modules/pc6502/lib/rom.js @@ -0,0 +1,388 @@ +/** + * @fileoverview Implements the PC6502 ROM component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"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 Memory = require("./memory"); + var CPUDef = require("./cpudef"); +} + +/** + * ROM(parmsROM) + * + * The ROM 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 + * writable: true to make ROM writable (default is false) + * + * 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. + * + * Finally, while making ROM "writable" may seem a contradiction in terms, I want to be able to load selected + * CP/M binary files into memory purely for testing purposes, and the RAM component has no "file" option, so the + * simplest solution was to add the option to load binary files into memory as "writable" ROMs. + * + * Moreover, if a "writable" ROM is installed at addr 0x100, that triggers our "Fake CP/M" support, providing + * a quick-and-dirty means of loading simple CP/M test binaries. See addROM() for details. + * + * @constructor + * @extends Component + * @param {Object} parmsROM + */ +function ROM(parmsROM) +{ + Component.call(this, "ROM", parmsROM, ROM); + + 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: + * + * [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. + */ + 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); + }); + } +} + +Component.subclass(ROM); + +/* + * 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. + * + * OK, well, I take that back, because the Debugger, if installed, has the ability to modify + * ROM contents, so in that case, having a reset() function that restores the original ROM data + * might be useful; then again, it might not, depending on what you're trying to debug. + * + * If we do add reset(), then we'll want to change copyROM() to hang onto the original + * ROM data; currently, we release it after copying it into the read-only memory allocated + * via bus.addMemory(). + */ + +/** + * initBus(cmp, bus, cpu, dbg) + * + * @this {ROM} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPUState} cpu + * @param {Debugger} dbg + */ +ROM.prototype.initBus = function(cmp, bus, cpu, dbg) +{ + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.copyROM(); +}; + +/** + * powerUp(data, fRepower) + * + * @this {ROM} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ +ROM.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 {ROM} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ +ROM.prototype.powerDown = function(fSave, fShutdown) +{ + return true; +}; + +/** + * doneLoad(sURL, sROMData, nErrorCode) + * + * @this {ROM} + * @param {string} sURL + * @param {string} sROMData + * @param {number} nErrorCode (response from server if anything other than 200) + */ +ROM.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 {ROM} + */ +ROM.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 {ROM} + * @param {number} addr + * @return {boolean} + */ +ROM.prototype.addROM = function(addr) +{ + if (this.bus.addMemory(addr, this.sizeROM, Memory.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 {ROM} + * @param {number} addr + */ +ROM.prototype.cloneROM = function(addr) +{ + var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM); + this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks); +}; + +/** + * ROM.init() + * + * This function operates on every HTML element of class "rom", extracting the + * JSON-encoded parameters for the ROM constructor from the element's "data-value" + * attribute, invoking the constructor to create a ROM component, and then binding + * any associated HTML controls to the new component. + */ +ROM.init = function() +{ + var aeROM = Component.getElementsByClass(document, PCJSCLASS, "rom"); + for (var iROM = 0; iROM < aeROM.length; iROM++) { + var eROM = aeROM[iROM]; + var parmsROM = Component.getComponentParms(eROM); + var rom = new ROM(parmsROM); + Component.bindComponentControls(rom, eROM, PCJSCLASS); + } +}; + +/* + * Initialize all the ROM modules on the page. + */ +web.onInit(ROM.init); + +if (NODE) module.exports = ROM; diff --git a/modules/pc6502/lib/state.js b/modules/pc6502/lib/state.js new file mode 100644 index 000000000..e258b6045 --- /dev/null +++ b/modules/pc6502/lib/state.js @@ -0,0 +1,397 @@ +/** + * @fileoverview The State class used by PCjs machines. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"use strict"; + +if (NODE) { + var web = require("./../../shared/lib/weblib"); + var Component = require("./../../shared/lib/component"); + var Messages = require("./messages"); +} + +/** + * State(component, sVersion, sSuffix) + * + * State objects are used by components to save/restore their state. + * + * During a save operation, components add data to a State object via set(), + * and then return the resulting data using data(). + * + * During a restore operation, the Computer component passes the results of each + * data() call back to the originating component. + * + * WARNING: Since State objects are low-level objects that have no UI requirements, + * they do not inherit from the Component class, so you should only use class methods + * of Component, such as Component.assert(), or Debugger methods if the Debugger + * is available. + * + * @constructor + * @param {Component} component + * @param {string} [sVersion] is used to append a major version number to the key + * @param {string} [sSuffix] is used to append any additional suffixes to the key + */ +function State(component, sVersion, sSuffix) { + this.id = component.id; + this.key = State.key(component, sVersion, sSuffix); + this.dbg = component.dbg; + this.unload(component.parms); +} + +/** + * State.key(component, sVersion, sSuffix) + * + * This encapsulates the key generation code. + * + * @param {Component} component + * @param {string} [sVersion] is used to append a major version number to the key + * @param {string} [sSuffix] is used to append any additional suffixes to the key + * @return {string} key + */ +State.key = function(component, sVersion, sSuffix) { + var key = component.id; + if (sVersion) { + var i = sVersion.indexOf('.'); + if (i > 0) key += ".v" + sVersion.substr(0, i); + } + if (sSuffix) { + key += "." + sSuffix; + } + return key; +}; + +/** + * State.compress(aSrc) + * + * @param {Array.|null} aSrc + * @return {Array.|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp) + */ +State.compress = function(aSrc) { + if (aSrc) { + var iSrc = 0; + var iComp = 0; + var aComp = []; + while (iSrc < aSrc.length) { + var n = aSrc[iSrc]; + Component.assert(n !== undefined); + var iCompare = iSrc + 1; + while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare++; + aComp[iComp++] = iCompare - iSrc; + aComp[iComp++] = n; + iSrc = iCompare; + } + if (aComp.length < aSrc.length) return aComp; + } + return aSrc; +}; + +/** + * State.decompress(aComp) + * + * @param {Array.} aComp + * @param {number} nLength is expected length of decompressed data + * @return {Array.} + */ +State.decompress = function(aComp, nLength) { + var iDst = 0; + var aDst = new Array(nLength); + var iComp = 0; + while (iComp < aComp.length - 1) { + var c = aComp[iComp++]; + var n = aComp[iComp++]; + while (c--) { + aDst[iDst++] = n; + } + } + Component.assert(aDst.length == nLength); + return aDst; +}; + +/** + * State.compressEvenOdd(aSrc) + * + * This is a very simple variation on compress() that compresses all the EVEN elements of aSrc first, + * followed by all the ODD elements. This tends to work better on EGA video memory, because when odd/even + * addressing is enabled (eg, for text modes), the DWORD values tend to alternate, which is the worst case + * for compress(), but the best case for compressEvenOdd(). + * + * One wrinkle we support: if the first element is uninitialized, then we assume the entire array is undefined, + * and return an empty compressed array. Conversely, decompressEvenOdd() will take an empty compressed array + * and return an uninitialized array. + * + * @param {Array.|null} aSrc + * @return {Array.|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp) + */ +State.compressEvenOdd = function(aSrc) { + if (aSrc) { + var iComp = 0, aComp = []; + if (aSrc[0] !== undefined) { + for (var off = 0; off < 2; off++) { + var iSrc = off; + while (iSrc < aSrc.length) { + var n = aSrc[iSrc]; + var iCompare = iSrc + 2; + while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2; + aComp[iComp++] = (iCompare - iSrc) >> 1; + aComp[iComp++] = n; + iSrc = iCompare; + } + } + } + if (aComp.length < aSrc.length) return aComp; + } + return aSrc; +}; + +/** + * State.decompressEvenOdd(aComp, nLength) + * + * This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence + * that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent: + * if you used even/odd compression, then you must use even/odd decompression. + * + * @param {Array.} aComp + * @param {number} nLength is expected length of decompressed data + * @return {Array.} + */ +State.decompressEvenOdd = function(aComp, nLength) { + var iDst = 0; + var aDst = new Array(nLength); + var iComp = 0; + while (iComp < aComp.length - 1) { + var c = aComp[iComp++]; + var n = aComp[iComp++]; + while (c--) { + aDst[iDst] = n; + iDst += 2; + } + /* + * The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it + * the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be + * done. + */ + Component.assert(iDst <= nLength || iComp == aComp.length); + if (iDst == nLength) iDst = 1; + } + Component.assert(aDst.length == nLength); + return aDst; +}; + +State.prototype = { + constructor: State, + /** + * set(id, data) + * + * @this {State} + * @param {number|string} id + * @param {Object|string} data + */ + set: function(id, data) { + try { + this[this.id][id] = data; + } catch(e) { + Component.log(e.message); + } + }, + /** + * get(id) + * + * @this {State} + * @param {number|string} id + * @return {Object|string|null} + */ + get: function(id) { + return this[this.id][id] || null; + }, + /** + * value() + * + * Use this instead of data() if you haven't called parse() yet. + * + * @this {State} + * @return {string} + */ + value: function() { + return this[this.id]; + }, + /** + * data() + * + * @this {State} + * @return {Object} + */ + data: function() { + return this[this.id]; + }, + /** + * load(s) + * + * WARNING: Make sure you follow this call with either a call to parse() or unload(), + * because any stringified data that we've loaded isn't usable until it's been parsed. + * + * @this {State} + * @param {Object|string|null} [s] + * @return {boolean} true if state exists in localStorage, false if not + */ + load: function(s) { + if (s) { + this[this.id] = s; + this.fLoaded = true; + return true; + } + if (this.fLoaded) { + /* + * This is assumed to be a redundant load(). + */ + return true; + } + if (web.hasLocalStorage()) { + s = web.getLocalStorageItem(this.key); + if (s) { + this[this.id] = s; + this.fLoaded = true; + if (DEBUG) this.printString("localStorage(" + this.key + "): " + s.length + " bytes loaded"); + return true; + } + } + return false; + }, + /** + * parse() + * + * This completes the load() operation, by parsing what was loaded, on the assumption there + * might be some benefit to deferring parsing until we've given the user a chance to confirm. + * Otherwise, load() could have just as easily done this, too. + * + * @this {State} + * @return {boolean} true if successful, false if error + */ + parse: function() { + var fSuccess = true; + try { + this[this.id] = JSON.parse(this[this.id]); + } catch (e) { + Component.error(e.message || e); + fSuccess = false; + } + return fSuccess; + }, + /** + * store() + * + * @this {State} + * @return {boolean} true if successful, false if error + */ + store: function() { + var fSuccess = true; + if (web.hasLocalStorage()) { + var s = JSON.stringify(this[this.id]); + if (web.setLocalStorageItem(this.key, s)) { + if (DEBUG) this.printString("localStorage(" + this.key + "): " + s.length + " bytes stored"); + } else { + /* + * WARNING: Because browsers tend to disable all alerts() during an "unload" operation, + * it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to + * think of some way to notify the user that there's a problem, and offer a way of cleaning + * up old states. + */ + Component.error("Unable to store " + s.length + " bytes in browser local storage"); + fSuccess = false; + } + } + return fSuccess; + }, + /** + * toString() + * + * We can't know whether this might be called before parse() or after parse(), so we check. + * If before, then this[this.id] will still be in string form; if after, it will be an Object. + * + * @this {State} + * @return {string} JSON-encoded state + */ + toString: function() { + var value = this[this.id]; + return (typeof value == "string"? value : JSON.stringify(value)); + }, + /** + * unload(parms) + * + * This discards any data saved via set() or loaded via load(), creating an empty State object. + * Note that you have to follow this call with an explicit call to store() if you want to remove + * the state from localStorage as well. + * + * @this {State} + * @param {Object} [parms] + */ + unload: function(parms) { + this[this.id] = {}; + if (parms) this.set("parms", parms); + this.fLoaded = false; + }, + /** + * clear(fAll) + * + * This unloads the current state, and then clears ALL localStorage for the current machine, + * independent of version, to reduce the chance of orphaned states wasting part of our limited allocation. + * + * @this {State} + * @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain + */ + clear: function(fAll) { + this.unload(); + var aKeys = web.getLocalStorageKeys(); + for (var i = 0; i < aKeys.length; i++) { + var sKey = aKeys[i]; + if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) { + web.removeLocalStorageItem(sKey); + if (DEBUG) this.printString("localStorage(" + sKey + ") removed"); + aKeys.splice(i, 1); + i = 0; + } + } + }, + /** + * printString(s) + * + * @this {State} + * @param {string} s is any caller-defined string + */ + printString: function(s) { + if (DEBUG && DEBUGGER && this.dbg) { + if (this.dbg.messageEnabled(Messages.LOG)) { + this.dbg.message(s); + } + } + } +}; + +if (NODE) module.exports = State; diff --git a/modules/pc6502/lib/video.js b/modules/pc6502/lib/video.js new file mode 100644 index 000000000..02c41cb85 --- /dev/null +++ b/modules/pc6502/lib/video.js @@ -0,0 +1,719 @@ +/** + * @fileoverview Implements the PC6502 Video component. + * @author Jeff Parsons + * @version 1.0 + * Created 2016-May-12 + * + * Copyright © 2012-2016 Jeff Parsons + * + * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) + * at and . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every source code file of every + * copy or modified version of this work, and to display that copyright notice on every screen + * that loads or runs any version of this software (see Computer.COPYRIGHT). + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of the + * PCjs program for purposes of the GNU General Public License, and the author does not claim + * any copyright as to their contents. + */ + +"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 Memory = require("./memory"); + var Messages = require("./messages"); + var State = require("./state"); +} + +/** + * Video(parmsVideo, canvas, context, textarea, container) + * + * The Video component can be configured with the following (parmsVideo) properties: + * + * screenWidth: width of the screen canvas, in pixels + * screenHeight: height of the screen canvas, in pixels + * screenColor: background color of the screen canvas (default is black) + * screenRotate: the amount of counter-clockwise screen rotation required (eg, -90 or 270) + * aspectRatio (eg, 1.33) + * bufferAddr: the starting address of the frame buffer (eg, 0x2400) + * bufferCols: the width of a single frame buffer row, in pixels (eg, 256) + * bufferRows: the number of frame buffer rows (eg, 224) + * bufferBits: the number of bits per column (default is 1) + * bufferLeft: the bit position of the left-most pixel in a byte (default is 0; CGA uses 7) + * bufferRotate: the amount of counter-clockwise buffer rotation required (eg, -90 or 270) + * interruptRate: normally the same as (or some multiple of) refreshRate (eg, 120) + * refreshRate: how many times updateScreen() should be performed per second (eg, 60) + * + * We record all the above values now, but we defer creation of the frame buffer until our initBus() + * handler is called. At that point, we will also compute the extent of the frame buffer, determine the + * appropriate "cell" size (ie, the number of pixels that updateScreen() will fetch and process at once), + * and then allocate our cell cache. + * + * Why interruptRate in addition to refreshRate? A higher interrupt rate is required for Space Invaders, + * because even though the CRT refreshes at 60Hz, the CRT controller interrupts the CPU *twice* per + * refresh (once after the top half of the screen has been redrawn, and again after the bottom half has + * been redrawn), so we need an interrupt rate of 120Hz. We pass the higher rate on to the CPU, so that + * it will call updateScreen() more frequently, but we still limit our screen updates to every *other* call. + * + * bufferRotate is an alternative to screenRotate; you may set one or the other (but not both) to -90 to + * enable different approaches to counter-clockwise 90-degree image rotation. screenRotate uses canvas + * transformation methods (translate(), rotate(), and scale()), while bufferRotate inverts the dimensions + * of the off-screen buffer and then relies on setPixel() to "rotate" the data into it. + * + * @constructor + * @extends Component + * @param {Object} parmsVideo + * @param {Object} [canvas] + * @param {Object} [context] + * @param {Object} [textarea] + * @param {Object} [container] + */ +function Video(parmsVideo, canvas, context, textarea, container) +{ + var video = this; + this.fGecko = web.isUserAgent("Gecko/"); + var i, sEvent, asWebPrefixes = ['', 'moz', 'ms', 'webkit']; + + Component.call(this, "Video", parmsVideo, Video, Messages.VIDEO); + + this.cxScreen = parmsVideo['screenWidth']; + this.cyScreen = parmsVideo['screenHeight']; + + this.addrBuffer = parmsVideo['bufferAddr']; + this.cxBuffer = parmsVideo['bufferCols']; + this.cyBuffer = parmsVideo['bufferRows']; + this.nBitsPerPixel = parmsVideo['bufferBits'] || 1; + this.iBitFirstPixel = parmsVideo['bufferLeft'] || 0; + this.rotateBuffer = parmsVideo['bufferRotate']; + if (this.rotateBuffer) { + this.rotateBuffer = this.rotateBuffer % 360; + if (this.rotateBuffer > 0) this.rotateBuffer -= 360; + if (this.rotateBuffer != -90) { + this.notice("unsupported buffer rotation: " + this.rotateBuffer); + this.rotateBuffer = 0; + } + } + + this.interruptRate = parmsVideo['interruptRate']; + this.refreshRate = parmsVideo['refreshRate'] || 60; + + this.canvasScreen = canvas; + this.contextScreen = context; + this.textareaScreen = textarea; + this.inputScreen = textarea || canvas || null; + + /* + * Support for disabling (or, less commonly, enabling) image smoothing, which all browsers + * seem to support now (well, OK, I still have to test the latest MS Edge browser), despite + * it still being labelled "experimental technology". Let's hope the browsers standardize + * on this. I see other options emerging, like the CSS property "image-rendering: pixelated" + * that's apparently been added to Chrome. Sigh. + */ + var fSmoothing = parmsVideo['smoothing']; + var sSmoothing = Component.parmsURL['smoothing']; + if (sSmoothing) fSmoothing = (sSmoothing == "true"); + if (fSmoothing != null) { + for (i = 0; i < asWebPrefixes.length; i++) { + sEvent = asWebPrefixes[i]; + if (!sEvent) { + sEvent = 'imageSmoothingEnabled'; + } else { + sEvent += 'ImageSmoothingEnabled'; + } + if (this.contextScreen[sEvent] !== undefined) { + this.contextScreen[sEvent] = fSmoothing; + break; + } + } + } + + this.rotateScreen = parmsVideo['screenRotate']; + if (this.rotateScreen) { + this.rotateScreen = this.rotateScreen % 360; + if (this.rotateScreen > 0) this.rotateScreen -= 360; + if (this.rotateScreen != -90) { + this.notice("unsupported screen rotation: " + this.rotateScreen); + this.rotateScreen = 0; + } else { + this.contextScreen.translate(0, this.cyScreen); + this.contextScreen.rotate((this.rotateScreen * Math.PI)/180); + this.contextScreen.scale(this.cyScreen/this.cxScreen, this.cxScreen/this.cyScreen); + } + } + + this.initColors(); + + /* + * Allocate off-screen buffers. + */ + var cxBuffer = this.cxBuffer; + var cyBuffer = this.cyBuffer; + if (this.rotateBuffer) { + cxBuffer = this.cyBuffer; + cyBuffer = this.cxBuffer; + } + this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer); + this.canvasBuffer = document.createElement("canvas"); + this.canvasBuffer.width = cxBuffer; + this.canvasBuffer.height = cyBuffer; + this.contextBuffer = this.canvasBuffer.getContext("2d"); + + /* + * Here's the gross code to handle full-screen support across all supported browsers. The lack of standards + * is exasperating; browsers can't agree on 'full' or 'Full, 'request' or 'Request', 'screen' or 'Screen', and + * while some browsers honor other browser prefixes, most browsers don't. + */ + this.container = container; + if (this.container) { + this.container.doFullScreen = container['requestFullscreen'] || container['msRequestFullscreen'] || container['mozRequestFullScreen'] || container['webkitRequestFullscreen']; + if (this.container.doFullScreen) { + for (i = 0; i < asWebPrefixes.length; i++) { + sEvent = asWebPrefixes[i] + 'fullscreenchange'; + if ('on' + sEvent in document) { + var onFullScreenChange = function() { + var fFullScreen = (document['fullscreenElement'] || document['msFullscreenElement'] || document['mozFullScreenElement'] || document['webkitFullscreenElement']); + video.notifyFullScreen(fFullScreen? true : false); + }; + document.addEventListener(sEvent, onFullScreenChange, false); + break; + } + } + for (i = 0; i < asWebPrefixes.length; i++) { + sEvent = asWebPrefixes[i] + 'fullscreenerror'; + if ('on' + sEvent in document) { + var onFullScreenError = function() { + video.notifyFullScreen(null); + }; + document.addEventListener(sEvent, onFullScreenError, false); + break; + } + } + } + } +} + +Component.subclass(Video); + +Video.COLORS = { + OVERLAY_TOP: 0, + OVERLAY_BOTTOM: 1, + OVERLAY_TOTAL: 2 +}; + +/** + * initBus(cmp, bus, cpu, dbg) + * + * @this {Video} + * @param {Computer} cmp + * @param {Bus} bus + * @param {CPUState} cpu + * @param {Debugger} dbg + */ +Video.prototype.initBus = function(cmp, bus, cpu, dbg) +{ + this.cmp = cmp; + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + + /* + * Compute the size of the frame buffer and allocate. + */ + this.sizeBuffer = ((this.cxBuffer * this.nBitsPerPixel) >> 3) * this.cyBuffer; + if (this.bus.addMemory(this.addrBuffer, this.sizeBuffer, Memory.TYPE.VIDEO)) { + /* + * Compute the number of cells and initialize the cell cache. + */ + this.nCellCache = this.sizeBuffer >> 1; + this.nPixelsPerCell = (16 / this.nBitsPerPixel)|0; + this.initCache(); + } + this.setReady(); +}; + +/** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * @this {Video} + * @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, "refresh") + * @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 + */ +Video.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) +{ + var video = this; + + switch (sBinding) { + case "fullScreen": + this.bindings[sBinding] = control; + if (this.container && this.container.doFullScreen) { + control.onclick = function onClickFullScreen() { + if (DEBUG) video.printMessage("fullScreen()"); + video.doFullScreen(); + }; + } else { + if (DEBUG) this.log("FullScreen API not available"); + control.parentNode.removeChild(/** @type {Node} */ (control)); + } + return true; + + default: + break; + } + return false; +}; + +/** + * doFullScreen() + * + * @this {Video} + * @return {boolean} true if request successful, false if not (eg, failed OR not supported) + */ +Video.prototype.doFullScreen = function() +{ + var fSuccess = false; + if (this.container) { + if (this.container.doFullScreen) { + /* + * Styling the container with a width of "100%" and a height of "auto" works great when the aspect ratio + * of our virtual screen is at least roughly equivalent to the physical screen's aspect ratio, but now that + * we support virtual VGA screens with an aspect ratio of 1.33, that's very much out of step with modern + * wide-screen monitors, which usually have an aspect ratio of 1.6 or greater. + * + * And unfortunately, none of the browsers I've tested appear to make any attempt to scale our container to + * the physical screen's dimensions, so the bottom of our screen gets clipped. To prevent that, I reduce + * the width from 100% to whatever percentage will accommodate the entire height of the virtual screen. + * + * NOTE: Mozilla recommends both a width and a height of "100%", but all my tests suggest that using "auto" + * for height works equally well, so I'm sticking with it, because "auto" is also consistent with how I've + * implemented a responsive canvas when the browser window is being resized. + */ + var sWidth = "100%"; + var sHeight = "auto"; + if (screen && screen.width && screen.height) { + var aspectPhys = screen.width / screen.height; + var aspectVirt = this.cxScreen / this.cyScreen; + if (aspectPhys > aspectVirt) { + sWidth = Math.round(aspectVirt / aspectPhys * 100) + '%'; + } + // TODO: We may need to someday consider the case of a physical screen with an aspect ratio < 1.0.... + } + if (!this.fGecko) { + this.container.style.width = sWidth; + this.container.style.height = sHeight; + } else { + /* + * Sadly, the above code doesn't work for Firefox, because as http://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode + * explains: + * + * 'It's worth noting a key difference here between the Gecko and WebKit implementations at this time: + * Gecko automatically adds CSS rules to the element to stretch it to fill the screen: "width: 100%; height: 100%". + * + * Which would be OK if Gecko did that BEFORE we're called, but apparently it does that AFTER, effectively + * overwriting our careful calculations. So we style the inner element (canvasScreen) instead, which + * requires even more work to ensure that the canvas is properly centered. FYI, this solution is consistent + * with Mozilla's recommendation for working around their automatic CSS rules: + * + * '[I]f you're trying to emulate WebKit's behavior on Gecko, you need to place the element you want + * to present inside another element, which you'll make fullscreen instead, and use CSS rules to adjust + * the inner element to match the appearance you want.' + */ + this.canvasScreen.style.width = sWidth; + this.canvasScreen.style.width = sWidth; + this.canvasScreen.style.display = "block"; + this.canvasScreen.style.margin = "auto"; + } + this.container.style.backgroundColor = "black"; + this.container.doFullScreen(); + fSuccess = true; + } + this.setFocus(); + } + return fSuccess; +}; + +/** + * notifyFullScreen(fFullScreen) + * + * @this {Video} + * @param {boolean|null} fFullScreen (null if there was a full-screen error) + */ +Video.prototype.notifyFullScreen = function(fFullScreen) +{ + if (!fFullScreen && this.container) { + if (!this.fGecko) { + this.container.style.width = this.container.style.height = ""; + } else { + this.canvasScreen.style.width = this.canvasScreen.style.height = ""; + } + } + this.printMessage("notifyFullScreen(" + fFullScreen + ")", true); +}; + +/** + * setFocus() + * + * @this {Video} + */ +Video.prototype.setFocus = function() +{ + if (this.inputScreen) this.inputScreen.focus(); +}; + +/** + * getRefreshRate() + * + * @this {Video} + * @return {number} + */ +Video.prototype.getRefreshRate = function() +{ + return Math.max(this.refreshRate, this.interruptRate); +}; + +/** + * initCache() + * + * Initializes the contents of our internal cell cache. + * + * @this {Video} + */ +Video.prototype.initCache = function() +{ + this.fCellCacheValid = false; + if (this.aCellCache === undefined || this.aCellCache.length != this.nCellCache) { + this.aCellCache = new Array(this.nCellCache); + } +}; + +/** + * initColors() + * + * This creates an array of nColors, with additional OVERLAY_TOTAL colors tacked on to the end of the array. + * + * @this {Video} + */ +Video.prototype.initColors = function() +{ + var rgbBlack = [0x00, 0x00, 0x00, 0xff]; + var rgbWhite = [0xff, 0xff, 0xff, 0xff]; + var rgbGreen = [0x00, 0xff, 0x00, 0xff]; + var rgbYellow = [0xff, 0xff, 0x00, 0xff]; + this.nColors = (1 << this.nBitsPerPixel); + this.aRGB = new Array(this.nColors + Video.COLORS.OVERLAY_TOTAL); + this.aRGB[0] = rgbBlack; + this.aRGB[1] = rgbWhite; + this.aRGB[this.nColors + Video.COLORS.OVERLAY_TOP] = rgbYellow; + this.aRGB[this.nColors + Video.COLORS.OVERLAY_BOTTOM] = rgbGreen; +}; + +/** + * setPixel(imageBuffer, x, y, bPixel) + * + * @this {Video} + * @param {Object} imageBuffer + * @param {number} x + * @param {number} y + * @param {number} bPixel (ie, an index into aRGB) + */ +Video.prototype.setPixel = function(imageBuffer, x, y, bPixel) +{ + var index; + if (!this.rotateBuffer) { + index = (x + y * imageBuffer.width); + } else { + index = (imageBuffer.height - x - 1) * imageBuffer.width + y; + } + if (bPixel) { + if (x >= 208 && x < 236) { + bPixel = this.nColors + Video.COLORS.OVERLAY_TOP; + } + else if (x >= 28 && x < 72) { + bPixel = this.nColors + Video.COLORS.OVERLAY_BOTTOM; + } + } + var rgb = this.aRGB[bPixel]; + index *= rgb.length; + imageBuffer.data[index] = rgb[0]; + imageBuffer.data[index+1] = rgb[1]; + imageBuffer.data[index+2] = rgb[2]; + imageBuffer.data[index+3] = rgb[3]; +}; + +/** + * updateScreen(n) + * + * Propagates the video buffer to the cell cache and updates the screen with any changes. Forced updates + * are generally internal updates triggered by an I/O operation or other state change, while non-forced updates + * are the periodic updates coming from the CPU. + * + * For every cell in the video buffer, compare it to the cell stored in the cell cache, render if it differs, + * and then update the cell cache to match. Since initCache() sets every cell in the cell cache to an + * invalid value, we're assured that the next call to updateScreen() will redraw the entire (visible) video buffer. + * + * @this {Video} + * @param {number} n (where 0 <= n < getRefreshRate() for a normal update, or -1 for a forced update) + */ +Video.prototype.updateScreen = function(n) +{ + if (n >= 0) { + if (!(n & 1)) { + /* + * On even updates, call cpu.requestINTR(1), and also update our copy of the screen. + */ + this.cpu.requestINTR(1); + } else { + /* + * On odd updates, call cpu.requestINTR(2), but do NOT update our copy of the screen, because + * the machine has presumably only updated the top half of the frame buffer at this point; it will + * update the bottom half of the frame buffer after acknowledging this interrupt. + */ + this.cpu.requestINTR(2); + return; + } + + /* + * Since this is not a forced update, if our cell cache is valid AND the buffer is clean, then do nothing. + */ + if (this.fCellCacheValid && this.bus.cleanMemory(this.addrBuffer, this.sizeBuffer)) { + return; + } + } + + var addr = this.addrBuffer; + var addrLimit = addr + this.sizeBuffer; + + var iCell = 0; + var nPixelShift = 1; + + var xBuffer = 0, yBuffer = 0; + var xDirty = this.cxBuffer, xMaxDirty = 0, yDirty = this.cyBuffer, yMaxDirty = 0; + + var nShiftInit = 0; + var nShiftPixel = this.nBitsPerPixel; + var nMask = (1 << nShiftPixel) - 1; + if (this.iBitFirstPixel) { + nShiftPixel = -nShiftPixel; + nShiftInit = 16 + nShiftPixel; + } + + while (addr < addrLimit) { + var data = this.bus.getShortDirect(addr); + this.assert(iCell < this.aCellCache.length); + if (this.fCellCacheValid && data === this.aCellCache[iCell]) { + xBuffer += this.nPixelsPerCell; + } else { + this.aCellCache[iCell] = data; + var nShift = nShiftInit; + if (nShift) data = ((data >> 8) | ((data & 0xff) << 8)); + if (xBuffer < xDirty) xDirty = xBuffer; + var cPixels = this.nPixelsPerCell; + while (cPixels--) { + var bPixel = (data >> nShift) & nMask; + this.setPixel(this.imageBuffer, xBuffer++, yBuffer, bPixel); + nShift += nShiftPixel; + } + if (xBuffer > xMaxDirty) xMaxDirty = xBuffer; + if (yBuffer < yDirty) yDirty = yBuffer; + if (yBuffer >= yMaxDirty) yMaxDirty = yBuffer + 1; + } + addr += 2; iCell++; + if (xBuffer >= this.cxBuffer) { + xBuffer = 0; yBuffer++; + if (yBuffer > this.cyBuffer) break; + } + } + + this.fCellCacheValid = true; + + /* + * Instead of blasting the ENTIRE imageBuffer into contextBuffer, and then blasting the ENTIRE + * canvasBuffer onto contextScreen, even for the smallest change, let's try to be a bit smarter about + * the update (well, to the extent that the canvas APIs permit). + */ + if (xDirty < this.cxBuffer) { + var cxDirty = xMaxDirty - xDirty; + var cyDirty = yMaxDirty - yDirty; + if (this.rotateBuffer) { + /* + * If rotateBuffer is set, then it must be -90, so we must "rotate" the dirty coordinates as well, + * because they are relative to the frame buffer, not the rotated image buffer. Alternatively, you + * can use the following call to blast the ENTIRE imageBuffer into contextBuffer instead: + * + * this.contextBuffer.putImageData(this.imageBuffer, 0, 0); + */ + var xDirtyOrig = xDirty, cxDirtyOrig = cxDirty; + //noinspection JSSuspiciousNameCombination + xDirty = yDirty; + cxDirty = cyDirty; + yDirty = this.cxBuffer - (xDirtyOrig + cxDirtyOrig); + cyDirty = cxDirtyOrig; + } + this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty); + this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.canvasBuffer.width, this.canvasBuffer.height, 0, 0, this.cxScreen, this.cyScreen); + } +}; + +/** + * Video.init() + * + * This function operates on every HTML element of class "video", extracting the + * JSON-encoded parameters for the Video constructor from the element's "data-value" + * attribute, invoking the constructor to create a Video component, and then binding + * any associated HTML controls to the new component. + */ +Video.init = function() +{ + var aeVideo = Component.getElementsByClass(document, PCJSCLASS, "video"); + for (var iVideo = 0; iVideo < aeVideo.length; iVideo++) { + var eVideo = aeVideo[iVideo]; + var parmsVideo = Component.getComponentParms(eVideo); + + var eCanvas = document.createElement("canvas"); + if (eCanvas === undefined || !eCanvas.getContext) { + eVideo.innerHTML = "
Missing <canvas> support. Please try a newer web browser."; + return; + } + + eCanvas.setAttribute("class", "pcjs-canvas"); + eCanvas.setAttribute("width", parmsVideo['screenWidth']); + eCanvas.setAttribute("height", parmsVideo['screenHeight']); + eCanvas.style.backgroundColor = parmsVideo['screenColor']; + + /* + * The "contenteditable" attribute on a canvas element NOTICEABLY slows down canvas drawing on + * Safari as soon as you give the canvas focus (ie, click away from the canvas, and drawing speeds + * up; click on the canvas, and drawing slows down). So the "transparent textarea hack" that we + * once employed as only a work-around for Android devices is now our default. + * + * eCanvas.setAttribute("contenteditable", "true"); + * + * HACK: A canvas style of "auto" provides for excellent responsive canvas scaling in EVERY browser + * except IE9/IE10, so I recalculate the appropriate CSS height every time the parent DIV is resized; + * IE11 works without this hack, so we take advantage of the fact that IE11 doesn't identify as "MSIE". + * + * The other reason it's good to keep this particular hack limited to IE9/IE10 is that most other + * browsers don't actually support an 'onresize' handler on anything but the window object. + */ + eCanvas.style.height = "auto"; + if (web.getUserAgent().indexOf("MSIE") >= 0) { + eVideo.onresize = function(eParent, eChild, cx, cy) { + return function onResizeVideo() { + eChild.style.height = (((eParent.clientWidth * cy) / cx) | 0) + "px"; + }; + }(eVideo, eCanvas, parmsVideo['screenWidth'], parmsVideo['screenHeight']); + eVideo.onresize(); + } + /* + * The following is a related hack that allows the user to force the screen to use a particular aspect + * ratio if an 'aspect' attribute or URL parameter is set. Initially, it's just for testing purposes + * until we figure out a better UI. And note that we use our web.onPageEvent() helper function to make + * sure we don't trample any other 'onresize' handler(s) attached to the window object. + */ + var aspect = +(parmsVideo['aspect'] || Component.parmsURL['aspect']); + /* + * No 'aspect' parameter yields NaN, which is falsey, and anything else must satisfy my arbitrary + * constraints of 0.3 <= aspect <= 3.33, to prevent any useless (or worse, browser-blowing) results. + */ + if (aspect && aspect >= 0.3 && aspect <= 3.33) { + web.onPageEvent('onresize', function(eParent, eChild, aspectRatio) { + return function onResizeWindow() { + /* + * Since aspectRatio is the target width/height, we have: + * + * eParent.clientWidth / eChild.style.height = aspectRatio + * + * which means that: + * + * eChild.style.height = eParent.clientWidth / aspectRatio + * + * so for example, if aspectRatio is 16:9, or 1.78, and clientWidth = 640, + * then the calculated height should approximately 360. + */ + eChild.style.height = ((eParent.clientWidth / aspectRatio)|0) + "px"; + }; + }(eVideo, eCanvas, aspect)); + window['onresize'](); + } + eVideo.appendChild(eCanvas); + + /* + * HACK: Android-based browsers, like the Silk (Amazon) browser and Chrome for Android, don't honor the + * "contenteditable" attribute; that is, when the canvas receives focus, they don't activate the on-screen + * keyboard. So my fallback is to create a transparent textarea on top of the canvas. + * + * The parent DIV must have a style of "position:relative" (alternatively, a class of "pcjs-container"), + * so that we can position the textarea using absolute coordinates. Also, we don't want the textarea to be + * visible, but we must use "opacity:0" instead of "visibility:hidden", because the latter seems to prevent + * the element from receiving events. These styling requirements are taken care of in components.css + * (see references to the "pcjs-video-object" class). + * + * UPDATE: Unfortunately, Android keyboards like to compose whole words before transmitting any of the + * intervening characters; our textarea's keyDown/keyUp event handlers DO receive intervening key events, + * but their keyCode property is ZERO. Virtually the only usable key event we receive is the Enter key. + * Android users will have to use machines that include their own on-screen "soft keyboard", or use an + * external keyboard. + * + * The following attempt to use a password-enabled input field didn't work any better on Android. You could + * clearly see the overlaid semi-transparent input field, but none of the input characters were passed along, + * with the exception of the "Go" (Enter) key. + * + * var eInput = document.createElement("input"); + * eInput.setAttribute("type", "password"); + * eInput.setAttribute("style", "position:absolute; left:0; top:0; width:100%; height:100%; opacity:0.5"); + * eVideo.appendChild(eInput); + * + * See this Chromium issue for more information: https://code.google.com/p/chromium/issues/detail?id=118639 + */ + var eTextArea = document.createElement("textarea"); + + /* + * As noted in keyboard.js, the keyboard on an iOS device tends to pop up with the SHIFT key depressed, + * which is not the initial keyboard state that the Keyboard component expects, so hopefully turning off + * these "auto" attributes will help. + */ + if (web.isUserAgent("iOS")) { + eTextArea.setAttribute("autocapitalize", "off"); + eTextArea.setAttribute("autocorrect", "off"); + } + eVideo.appendChild(eTextArea); + + /* + * Now we can create the Video object, record it, and wire it up to the associated document elements. + */ + var eContext = eCanvas.getContext("2d"); + var video = new Video(parmsVideo, eCanvas, eContext, eTextArea /* || eInput */, eVideo); + + /* + * Bind any video-specific controls (eg, the Refresh button). There are no essential controls, however; + * even the "Refresh" button is just a diagnostic tool, to ensure that the screen contents are up-to-date. + */ + Component.bindComponentControls(video, eVideo, PCJSCLASS); + } +}; + +/* + * Initialize every Video module on the page. + */ +web.onInit(Video.init); + +if (NODE) module.exports = Video;