pcjs/versions/pc8080/1.35.1/pc8080-uncompiled.js

25398 lines
890 KiB
JavaScript

"use strict";
/**
* @copyright http://pcjs.org/modules/shared/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @define {string}
*/
var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json
var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML
var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons <Jeff@pcjs.org>";
var LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
var CSSCLASS = "pcjs";
/**
* @define {string}
*/
var SITEHOST = "localhost:8088";// this @define is overridden by the Closure Compiler with "www.pcjs.org"
/**
* @define {boolean}
*/
var COMPILED = false; // this @define is overridden by the Closure Compiler (to true)
/**
* @define {boolean}
*/
var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code
/**
* @define {boolean}
*/
var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code
/**
* @define {boolean}
*/
var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code
/*
* RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value.
*
* SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be "virtually"
* connected to each other via a null-modem cable, which assumes the following cross-wiring:
*
* G 1 <-> 1 G (Ground)
* TD 2 <-> 3 RD (Received Data)
* RD 3 <-> 2 TD (Transmitted Data)
* RTS 4 <-> 5 CTS (Clear To Send)
* CTS 5 <-> 4 RTS (Request To Send)
* DSR 6+8 <-> 20 DTR (Data Terminal Ready)
* SG 7 <-> 7 SG (Signal Ground)
* DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect)
* RI 22 <-> 22 RI (Ring Indicator)
*
* TODO: Move these definitions to a more appropriate shared file at some point.
*/
var RS232 = {
RTS: {
PIN: 4,
MASK: 0x00000010
},
CTS: {
PIN: 5,
MASK: 0x00000020
},
DSR: {
PIN: 6,
MASK: 0x00000040
},
CD: {
PIN: 8,
MASK: 0x00000100
},
DTR: {
PIN: 20,
MASK: 0x00100000
},
RI: {
PIN: 22,
MASK: 0x00400000
}
};
/*
* NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser)
*/
var NODE = false;
/**
* @copyright http://pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Our "DiskDump API", such as it was, used to look like:
*
* http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* To make it (a bit) more "REST-like", the above request now looks like:
*
* http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* Similarly, our "FileDump API" used to look like:
*
* http://jsmachines.net/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* and that request now looks like:
*
* http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the
* the rest of the URL would be (a) confusing, and (b) more work to parse.
*/
var DumpAPI = {
ENDPOINT: "/api/v1/dump",
QUERY: {
DIR: "dir", // value is path of a directory (DiskDump only)
DISK: "disk", // value is path of a disk image (DiskDump only)
FILE: "file", // value is path of a ROM image file (FileDump only)
IMG: "img", // alias for DISK
PATH: "path", // value is path of a one or more files (DiskDump only)
FORMAT: "format", // value is one of FORMAT values below
COMMENTS: "comments", // value is either "true" or "false"
DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise
MBHD: "mbhd", // value is hard drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED)
SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only)
},
FORMAT: {
JSON: "json", // default
JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON
DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only)
HEX: "hex", // deprecated
OCTAL: "octal", // displays data as octal words
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
WORDS: "words", // displays data as hex words; normally used only when comments are enabled
LONGS: "longs", // displays data as dwords
IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only)
ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only)
}
};
/*
* Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must
* also provide a list of commands which, when combined with the endpoint, define a unique request.
*/
DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH];
DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE];
/**
* @copyright http://pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2017
*/
var ReportAPI = {
ENDPOINT: "/api/v1/report",
QUERY: {
APP: "app",
VER: "ver",
URL: "url",
USER: "user",
TYPE: "type",
DATA: "data"
},
TYPE: {
BUG: "bug"
},
RES: {
OK: "Thank you"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/userapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Examples of User API requests:
*
* web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;
*/
var UserAPI = {
ENDPOINT: "/api/v1/user",
QUERY: {
REQ: "req", // specifies a request
USER: "user", // specifies a user ID
STATE: "state", // specifies a state ID
DATA: "data" // specifies state data
},
REQ: {
CREATE: "create", // creates a user ID
VERIFY: "verify", // requests verification of a user ID
STORE: "store", // stores a machine state on the server
LOAD: "load" // loads a machine state from the server
},
RES: {
CODE: "code",
DATA: "data"
},
CODE: {
OK: "ok",
FAIL: "error"
},
FAIL: {
DUPLICATE: "user already exists",
VERIFY: "unable to verify user",
BADSTATE: "invalid state parameter",
NOSTATE: "no machine state",
BADLOAD: "unable to load machine state",
BADSTORE: "unable to save machine state"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/keys.js (C) Jeff Parsons 2012-2017
*/
var Keys = {
/*
* Keys and/or key combinations that generate common ASCII codes.
*
* NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js;
* if something's missing, that's probably the more appropriate table to add it to.
*
* TODO: The Closure Compiler doesn't inline all references to these values, at least those with
* quoted property names, which is why I've 'unquoted' as many of them as possible. One solution
* would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead
* of ' ', AMP instead of '&', etc.)
*/
ASCII: {
BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7,
CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15,
CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23,
CTRL_X: 24, CTRL_Y: 25, 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, DEL: 127
},
/*
* 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).
*/
KEYCODE: {
/* 0x08 */ BS: 8, // BACKSPACE (ASCII.CTRL_H)
/* 0x09 */ TAB: 9, // TAB (ASCII.CTRL_I)
/* 0x0A */ LF: 10, // LINE FEED (ASCII.CTRL_J) (TODO: Determine if any key actually generates this)
/* 0x0D */ CR: 13, // CARRIAGE RETURN (ASCII.CTRL_M)
/* 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,
/* 0x30 */ ZERO: 48,
/* 0x31 */ ONE: 49,
/* 0x32 */ TWO: 50,
/* 0x33 */ THREE: 51,
/* 0x34 */ FOUR: 52,
/* 0x35 */ FIVE: 53,
/* 0x36 */ SIX: 54,
/* 0x37 */ SEVEN: 55,
/* 0x38 */ EIGHT: 56,
/* 0x39 */ NINE: 57,
/* 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_0: 96,
/* 0x60 */ NUM_INS: 96,
/* 0x60 */ FF_BQUOTE: 96,
/* 0x61 */ NUM_1: 97,
/* 0x61 */ NUM_END: 97,
/* 0x62 */ NUM_2: 98,
/* 0x62 */ NUM_DOWN: 98,
/* 0x63 */ NUM_3: 99,
/* 0x63 */ NUM_PGDN: 99,
/* 0x64 */ NUM_4: 100,
/* 0x64 */ NUM_LEFT: 100,
/* 0x65 */ NUM_5: 101,
/* 0x65 */ NUM_CENTER: 101,
/* 0x66 */ NUM_6: 102,
/* 0x66 */ NUM_RIGHT: 102,
/* 0x67 */ NUM_7: 103,
/* 0x67 */ NUM_HOME: 103,
/* 0x68 */ NUM_8: 104,
/* 0x68 */ NUM_UP: 104,
/* 0x69 */ NUM_9: 105,
/* 0x69 */ NUM_PGUP: 105,
/* 0x6A */ NUM_MUL: 106,
/* 0x6B */ NUM_ADD: 107,
/* 0x6D */ NUM_SUB: 109,
/* 0x6E */ NUM_DEL: 110, // aka PERIOD
/* 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 (FF_SEMI)
/* 0xBB */ EQUALS: 187, // Firefox: 61 (FF_EQUALS)
/* 0xBC */ COMMA: 188,
/* 0xBD */ DASH: 189, // Firefox: 173 (FF_DASH)
/* 0xBE */ PERIOD: 190,
/* 0xBF */ SLASH: 191,
/* 0xC0 */ BQUOTE: 192,
/* 0xDB */ LBRACK: 219,
/* 0xDC */ BSLASH: 220,
/* 0xDD */ RBRACK: 221,
/* 0xDE */ QUOTE: 222,
/* 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
},
/*
* The set of values that a browser may store in the 'location' property of a keyboard event object
* which we also support.
*/
LOCATION: {
LEFT: 1,
RIGHT: 2,
NUMPAD: 3
}
};
/*
* Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys.
*/
Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;
/*
* Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table
* are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I
* simply extended this table to resolve additional differences in other browsers (ie, Opera), but without
* browser-specific checks, it's not safe to perform all the mappings shown below.
*/
Keys.FF_KEYCODES = {};
Keys.FF_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.KEYCODE.SEMI; // 59 -> 186
Keys.FF_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.KEYCODE.EQUALS; // 61 -> 187
Keys.FF_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.KEYCODE.DASH; // 173 -> 189
Keys.FF_KEYCODES[Keys.KEYCODE.FF_CMD] = Keys.KEYCODE.CMD; // 224 -> 91
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_COMMA] = Keys.KEYCODE.COMMA; // 44 -> 188
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_PERIOD] = Keys.KEYCODE.PERIOD; // 46 -> 190
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_SLASH] = Keys.KEYCODE.SLASH; // 47 -> 191
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BQUOTE] = Keys.KEYCODE.BQUOTE; // 96 -> 192
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_LBRACK = Keys.KEYCODE.LBRACK; // 91 -> 219
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BSLASH] = Keys.KEYCODE.BSLASH; // 92 -> 220
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_RBRACK] = Keys.KEYCODE.RBRACK; // 93 -> 221
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_QUOTE] = Keys.KEYCODE.QUOTE; // 39 -> 222
/*
* Maps non-ASCII keyCodes to their ASCII counterparts
*/
Keys.NONASCII_KEYCODES = {};
Keys.NONASCII_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-']; // 173 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59
Keys.NONASCII_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61
Keys.NONASCII_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44
Keys.NONASCII_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96
Keys.NONASCII_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\']; // 220 -> 92
Keys.NONASCII_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93
Keys.NONASCII_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII["'"]; // 222 -> 39
/*
* Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down.
* Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration.
*/
Keys.SHIFTED_KEYCODES = {};
Keys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['('];
Keys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['"'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':'];
/**
* @copyright http://pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2017
*/
class Str {
/**
* isValidInt(s, base)
*
* The built-in parseInt() function has the annoying feature of returning a partial value (ie,
* up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf.
*
* So it's best to use our own Str.parseInt() function, which will in turn use this function to
* validate the entire string.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported
* @return {boolean} true if valid, false if invalid (or the specified base isn't supported)
*/
static isValidInt(s, base)
{
if (!base || base == 10) return s.match(/^-?[0-9]+$/) !== null;
if (base == 16) return s.match(/^-?[0-9a-f]+$/i) !== null;
if (base == 8) return s.match(/^-?[0-7]+$/) !== null;
if (base == 2) return s.match(/^-?[01]+$/) !== null;
return false;
}
/**
* parseInt(s, base)
*
* This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes
* ('$' or "0x" for hex, '#' or "0o" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for
* binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values;
* see isValidInt() for details.
*
* The use of multiple prefix/suffix combinations is undefined (although for the record, we process
* prefixes first). We do NOT support the "0b" prefix to indicate binary UNLESS one or more commas are
* also present (because "0b" is also a valid hex sequence), and we do NOT support a single leading zero
* to indicate octal (because such a number could also be decimal or hex). Any number of commas are
* allowed; we remove them all before calling the built-in parseInt().
*
* More recently, we've added support for "^D", "^O", and "^B" prefixes to accommodate the base overrides
* that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively).
* If this support turns out to adversely affect other debuggers, then it will have to be "conditionalized".
* Similarly, we've added support for "K", "M", and "G" MACRO-10-style suffixes that add 3, 6, or 9 zeros
* to the value to be parsed, respectively.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes
* @return {number|undefined} corresponding value, or undefined if invalid
*/
static parseInt(s, base)
{
var value;
if (s) {
if (!base) base = 10;
var ch, chPrefix, chSuffix;
var fCommas = (s.indexOf(',') > 0);
if (fCommas) s = s.replace(/,/g, '');
ch = chPrefix = s.charAt(0);
if (chPrefix == '#') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '$') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) {
s = s.substr(1);
}
else {
ch = chPrefix = s.substr(0, 2);
if (chPrefix == '0b' && fCommas || chPrefix == '^B') {
base = 2;
chPrefix = '';
}
else if (chPrefix == '0o' || chPrefix == '^O') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '^D') {
base = 10;
chPrefix = '';
}
else if (chPrefix == '0x') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) s = s.substr(2);
}
ch = chSuffix = s.slice(-1);
if (chSuffix == 'Y' || chSuffix == 'y') {
base = 2;
chSuffix = '';
}
else if (chSuffix == '.') {
base = 10;
chSuffix = '';
}
else if (chSuffix == 'H' || chSuffix == 'h') {
base = 16;
chSuffix = '';
}
else if (chSuffix == 'K') {
chSuffix = '000';
}
else if (chSuffix == 'M') {
chSuffix = '000000';
}
else if (chSuffix == 'G') {
chSuffix = '000000000';
}
if (ch != chSuffix) s = s.slice(0, -1) + chSuffix;
/*
* This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the
* number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed,
* which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then
* a right shift of -(35 - n) is required.
*/
var v, shift = 0;
if (base <= 10) {
var match = s.match(/(-?[0-9]+)B([0-9]*)/);
if (match) {
s = match[1];
shift = 35 - ((match[2] || 35) & 0xff);
}
}
if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {
/*
* With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful.
*
* value = v|0;
*/
if (shift) {
/*
* Since binary shifting is a logical operation, and since shifting by division only works properly
* with positive numbers, we must convert a negative value to a positive value, by computing the two's
* complement.
*/
if (v < 0) v += Math.pow(2, 36);
if (shift > 0) {
v *= Math.pow(2, shift);
} else {
v = Math.trunc(v / Math.pow(2, -shift));
}
}
value = v;
}
}
return value;
}
/**
* toBase(n, radix, cch, sPrefix, nGrouping)
*
* Displays the given number as an unsigned integer using the specified radix and number of digits.
*
* @param {number|null|undefined} n
* @param {number} radix (ie, the base)
* @param {number} cch (the desired number of digits)
* @param {string} [sPrefix] (default is none)
* @param {number} [nGrouping]
* @return {string}
*/
static toBase(n, radix, cch, sPrefix = "", nGrouping = 0)
{
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
var s = "";
if (isNaN(n)) {
n = null;
} else if (n != null) {
/*
* Callers that produced an input by dividing by a power of two rather than shifting (in order
* to access more than 32 bits) may produce a fractional result, which ordinarily we would simply
* ignore, but if the integer portion is zero and the sign is negative, we should probably treat
* this value as a sign-extension.
*/
if (n < 0 && n > -1) n = -1;
/*
* Negative values should be two's complemented according to the number of digits; for example,
* 12 octal digits implies an upper limit 8^12.
*/
if (n < 0) {
n += Math.pow(radix, cch);
}
if (n >= Math.pow(radix, cch)) {
cch = Math.ceil(Math.log(n) / Math.log(radix));
}
}
var g = nGrouping || -1;
while (cch-- > 0) {
if (!g) {
s = ',' + s;
g = nGrouping;
}
if (n == null) {
s = '?' + s;
} else {
var d = n % radix;
d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);
s = String.fromCharCode(d) + s;
n = Math.trunc(n / radix);
}
g--;
}
return sPrefix + s;
}
/**
* toBin(n, cch, nGrouping)
*
* Converts an integer to binary, with the specified number of digits (up to a maximum of 36).
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36)
* @param {number} [nGrouping]
* @return {string} the binary representation of n
*/
static toBin(n, cch, nGrouping)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1;
var v = Math.abs(n);
if (v <= 0b11111111) {
cch = 8;
} else if (v <= 0b111111111111111111) {
cch = 18;
} else {
cch = 36;
}
} else if (cch > 36) cch = 36;
return Str.toBase(n, 2, cch, "", nGrouping);
}
/**
* toBinBytes(n, cb, fPrefix)
*
* Converts an integer to binary, with the specified number of bytes (up to the default of 4).
*
* @param {number|null|undefined} n (interpreted as a 32-bit value)
* @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)
* @param {boolean} [fPrefix]
* @return {string} the binary representation of n
*/
static toBinBytes(n, cb, fPrefix)
{
var s = "";
if (!cb || cb > 4) cb = 4;
for (var i = 0; i < cb; i++) {
if (s) s = ',' + s;
s = Str.toBin(n & 0xff, 8) + s;
n >>= 8;
}
return (fPrefix? "0b" : "") + s;
}
/**
* toOct(n, cch, fPrefix)
*
* Converts an integer to octal, with the specified number of digits (default of 6; max of 12)
*
* You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12)
* @param {boolean} [fPrefix]
* @return {string} the octal representation of n
*/
static toOct(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1;
var v = Math.abs(n);
if (v <= 0o777777) {
cch = 6;
} else if (v <= 0o77777777) {
cch = 8;
} else {
cch = 12;
}
} else if (cch > 12) cch = 12;
return Str.toBase(n, 8, cch, fPrefix? "0o" : "");
}
/**
* toDec(n, cch)
*
* Converts an integer to decimal, with the specified number of digits (default of 5; max of 11)
*
* You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11)
* @return {string} the decimal representation of n
*/
static toDec(n, cch)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1;
var v = Math.abs(n);
if (v <= 99999) {
cch = 5;
} else {
cch = 11;
}
} else if (cch > 11) cch = 11;
return Str.toBase(n, 10, cch);
}
/**
* toHex(n, cch, fPrefix)
*
* Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9).
*
* You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16)
* will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will
* throw an exception, whereas this function will return '?' characters.
*
* NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution,
* taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values:
*
* s = (n < 0? n + 0x100000000 : n).toString(16);
* s = "00000000".substr(0, 8 - s.length) + s;
* s = s.substr(0, cch).toUpperCase();
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9)
* @param {boolean} [fPrefix]
* @return {string} the hex representation of n
*/
static toHex(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1;
var v = Math.abs(n);
if (v <= 0xffff) {
cch = 4;
} else if (v <= 0xffffffff) {
cch = 8;
} else {
cch = 9;
}
} else if (cch > 9) cch = 9;
return Str.toBase(n, 16, cch, fPrefix? "0x" : "");
}
/**
* toHexByte(b)
*
* Alias for Str.toHex(b, 2, true)
*
* @param {number|null|undefined} b is a byte value
* @return {string} the hex representation of b
*/
static toHexByte(b)
{
return Str.toHex(b, 2, true);
}
/**
* toHexWord(w)
*
* Alias for Str.toHex(w, 4, true)
*
* @param {number|null|undefined} w is a word (16-bit) value
* @return {string} the hex representation of w
*/
static toHexWord(w)
{
return Str.toHex(w, 4, true);
}
/**
* toHexLong(l)
*
* Alias for Str.toHex(l, 8, true)
*
* @param {number|null|undefined} l is a dword (32-bit) value
* @return {string} the hex representation of w
*/
static toHexLong(l)
{
return Str.toHex(l, 8, true);
}
/**
* getBaseName(sFileName, fStripExt)
*
* This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.
*
* Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only
* if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux").
*
* @param {string} sFileName
* @param {boolean} [fStripExt]
* @return {string}
*/
static getBaseName(sFileName, fStripExt)
{
var sBaseName = sFileName;
var i = sFileName.lastIndexOf('/');
if (i >= 0) sBaseName = sFileName.substr(i + 1);
/*
* This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters.
*/
i = sBaseName.indexOf('&');
if (i > 0) sBaseName = sBaseName.substr(0, i);
if (fStripExt) {
i = sBaseName.lastIndexOf(".");
if (i > 0) {
sBaseName = sBaseName.substring(0, i);
}
}
return sBaseName;
}
/**
* getExtension(sFileName)
*
* This is a poor-man's version of Node's path.extname(), which Node-only components should use instead.
*
* Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it.
*
* @param {string} sFileName
* @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string
*/
static getExtension(sFileName)
{
var sExtension = "";
var i = sFileName.lastIndexOf(".");
if (i >= 0) {
sExtension = sFileName.substr(i + 1).toLowerCase();
}
return sExtension;
}
/**
* endsWith(s, sSuffix)
*
* @param {string} s
* @param {string} sSuffix
* @return {boolean} true if s ends with sSuffix, false if not
*/
static endsWith(s, sSuffix)
{
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
}
/**
* escapeHTML(sHTML)
*
* @param {string} sHTML
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
*/
static escapeHTML(sHTML)
{
return sHTML.replace(/[&<>"']/g, function(m)
{
return Str.aHTMLEscapeMap[m];
});
}
/**
* replace(sFind, sReplace, s)
*
* The JavaScript replace() function ALWAYS interprets "$" specially in replacement strings, even when
* the search string is NOT a RegExp; specifically:
*
* $$ Inserts a "$"
* $& Inserts the matched substring
* $` Inserts the portion of the string that precedes the matched substring
* $' Inserts the portion of the string that follows the matched substring
* $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string,
* provided the first argument was a RegExp object
*
* So, if a replacement string containing dollar signs passes through a series of replace() calls, untold
* problems could result. Hence, this function, which simply uses the replacement string as-is.
*
* Similar to the JavaScript replace() method, this replaces only one occurrence (ie, the FIRST occurrence);
* it might be nice to add options to replace the LAST occurrence and/or ALL occurrences, but we'll revisit
* that later.
*
* @param {string} sFind
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replace(sFind, sReplace, s)
{
var i = s.indexOf(sFind);
if (i >= 0) {
s = s.substr(0, i) + sReplace + s.substr(i + sFind.length);
}
return s;
}
/**
* replaceAll(sFind, sReplace, s)
*
* @param {string} sFind
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replaceAll(sFind, sReplace, s)
{
var a = {};
a[sFind] = sReplace;
return Str.replaceArray(a, s);
}
/**
* replaceArray(a, s)
*
* @param {Object} a
* @param {string} s
* @return {string}
*/
static replaceArray(a, s)
{
var sMatch = "";
for (var k in a) {
/*
* As noted in:
*
* http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/
*
* inside character classes, only backslash, caret, hyphen and the closing bracket need to be
* escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first,
* and the hyphen is last, you can avoid escaping those as well.
*/
k = k.replace(/([\\[\]*{}().+?])/g, "\\$1");
sMatch += (sMatch? '|' : '') + k;
}
return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m)
{
return a[m];
});
}
/**
* pad(s, cch, fPadLeft)
*
* NOTE: the maximum amount of padding currently supported is 40 spaces.
*
* @param {string} s is a string
* @param {number} cch is desired length
* @param {boolean} [fPadLeft] (default is padding on the right)
* @return {string} the original string (s) with spaces padding it to the specified length
*/
static pad(s, cch, fPadLeft)
{
var sPadding = " ";
return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);
}
/**
* stripLeadingZeros(s, fPad)
*
* @param {string} s
* @param {boolean} [fPad]
* @return {string}
*/
static stripLeadingZeros(s, fPad)
{
var cch = s.length;
s = s.replace(/^0+([0-9A-F]+)$/i, "$1");
if (fPad) s = Str.pad(s, cch, true);
return s;
}
/**
* trim(s)
*
* @param {string} s
* @return {string}
*/
static trim(s)
{
if (String.prototype.trim) {
return s.trim();
}
return s.replace(/^\s+|\s+$/g, "");
}
/**
* toASCIICode(b)
*
* @param {number} b
* @return {string}
*/
static toASCIICode(b)
{
var s;
if (b != Str.ASCII.CR && b != Str.ASCII.LF) {
s = Str.aASCIICodes[b];
}
if (s) {
s = '<' + s + '>';
} else {
s = String.fromCharCode(b);
}
return s;
}
}
Str.aHTMLEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
/*
* Future home of a general-purpose ASCII table. TODO: Flesh it out.
*/
Str.ASCII = {
LF: 0x0A,
CR: 0x0D
};
/*
* Table for converting "unprintable" ASCII codes into mnemonics, to more clearly see what's being printed.
*/
Str.aASCIICodes = {
0x00: "NUL",
0x01: "SOH", // (CTRL_A) Start of Heading
0x02: "STX", // (CTRL_B) Start of Text
0x03: "ETX", // (CTRL_C) End of Text
0x04: "EOT", // (CTRL_D) End of Transmission
0x05: "ENQ", // (CTRL_E) Enquiry
0x06: "ACK", // (CTRL_F) Acknowledge
0x07: "BEL", // (CTRL_G) Bell
0x08: "BS", // (CTRL_H) Backspace
0x09: "TAB", // (CTRL_I) Horizontal Tab
0x0A: "LF", // (CTRL_J) Line Feed (New Line)
0x0B: "VT", // (CTRL_K) Vertical Tab
0x0C: "FF", // (CTRL_L) Form Feed (New Page)
0x0D: "CR", // (CTRL_M) Carriage Return
0x0E: "SO", // (CTRL_N) Shift Out
0x0F: "SI", // (CTRL_O) Shift In
0x10: "DLE", // (CTRL_P) Data Link Escape
0x11: "XON", // (CTRL_Q) Device Control 1 (aka DC1)
0x12: "DC2", // (CTRL_R) Device Control 2
0x13: "XOFF", // (CTRL_S) Device Control 3 (aka DC3)
0x14: "DC4", // (CTRL_T) Device Control 4
0x15: "NAK", // (CTRL_U) Negative Acknowledge
0x16: "SYN", // (CTRL_V) Synchronous Idle
0x17: "ETB", // (CTRL_W) End of Transmission Block
0x18: "CAN", // (CTRL_X) Cancel
0x19: "EM", // (CTRL_Y) End of Medium
0x1A: "SUB", // (CTRL_Z) Substitute
0x1B: "ESC", // Escape
0x1C: "FS", // File Separator
0x1D: "GS", // Group Separator
0x1E: "RS", // Record Separator
0x1F: "US" // Unit Separator
};
Str.TYPES = {
NULL: 0,
BYTE: 1,
WORD: 2,
DWORD: 3,
NUMBER: 4,
STRING: 5,
BOOLEAN: 6,
OBJECT: 7,
ARRAY: 8
};
/**
* @copyright http://pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2017
*/
/**
* @typedef {{
* mask: number,
* shift: number
* }}
*/
var BitField;
/**
* @typedef {Object.<BitField>}
*/
var BitFields;
class Usr {
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
* @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point
*/
static binarySearch(a, v, fnCompare)
{
var left = 0;
var right = a.length;
var found = 0;
if (fnCompare === undefined) {
fnCompare = function(a, b)
{
return a > b ? 1 : a < b ? -1 : 0;
};
}
while (left < right) {
var middle = (left + right) >> 1;
var compareResult;
compareResult = fnCompare(v, a[middle]);
if (compareResult > 0) {
left = middle + 1;
} else {
right = middle;
found = !compareResult;
}
}
return found ? left : ~left;
}
/**
* binaryInsert(a, v, fnCompare)
*
* If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the
* element is inserted into the array at the appropriate index.
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v is the value to insert
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
*/
static binaryInsert(a, v, fnCompare)
{
var index = Usr.binarySearch(a, v, fnCompare);
if (index < 0) {
a.splice(-(index + 1), 0, v);
}
}
/**
* getTimestamp()
*
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
*/
static getTimestamp()
{
var date = new Date();
var padNum = function(n)
{
return (n < 10 ? "0" : "") + n;
};
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate()) + " " + padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
}
/**
* getMonthDays(nMonth, nYear)
*
* Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100),
* so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails
* the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap
* year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year.
*
* TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD,
* so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and
* we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what
* a real RTC actually does.
*
* @param {number} nMonth (1-12)
* @param {number} nYear (normally a 4-digit year, but it may also be mod 100)
* @return {number} the maximum (1-based) day allowed for the specified month and year
*/
static getMonthDays(nMonth, nYear)
{
var nDays = Usr.aMonthDays[nMonth - 1];
if (nDays == 28) {
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
nDays++;
}
}
return nDays;
}
/**
* formatDate(sFormat, date)
*
* @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s")
* @param {Date} [date] (default is the current time)
* @return {string}
*
* Supported identifiers in sFormat include:
*
* a: lowercase ante meridiem and post meridiem (am or pm)
* d: day of the month, 2 digits with leading zeros (01,02,...,31)
* D: 3-letter day of the week ("Sun","Mon",...,"Sat")
* F: month ("January","February",...,"December")
* g: hour in 12-hour format, without leading zeros (1,2,...,12)
* h: hour in 24-hour format, without leading zeros (0,1,...,23)
* H: hour in 24-hour format, with leading zeros (00,01,...,23)
* i: minutes, with leading zeros (00,01,...,59)
* j: day of the month, without leading zeros (1,2,...,31)
* l: day of the week ("Sunday","Monday",...,"Saturday")
* m: month, with leading zeros (01,02,...,12)
* M: 3-letter month ("Jan","Feb",...,"Dec")
* n: month, without leading zeros (1,2,...,12)
* s: seconds, with leading zeros (00,01,...,59)
* y: 2-digit year (eg, 14)
* Y: 4-digit year (eg, 2014)
*
* For more inspiration, see: http://php.net/manual/en/function.date.php (of which we support ONLY a subset).
*/
static formatDate(sFormat, date)
{
var sDate = "";
if (!date) date = new Date();
var iHour = date.getHours();
var iDay = date.getDate();
var iMonth = date.getMonth() + 1;
for (var i = 0; i < sFormat.length; i++) {
var ch;
switch ((ch = sFormat.charAt(i))) {
case 'a':
sDate += (iHour < 12 ? "am" : "pm");
break;
case 'd':
sDate += ('0' + iDay).slice(-2);
break;
case 'D':
sDate += Usr.asDays[date.getDay()].substr(0, 3);
break;
case 'F':
sDate += Usr.asMonths[iMonth - 1];
break;
case 'g':
sDate += (!iHour ? 12 : (iHour > 12 ? iHour - 12 : iHour));
break;
case 'h':
sDate += iHour;
break;
case 'H':
sDate += ('0' + iHour).slice(-2);
break;
case 'i':
sDate += ('0' + date.getMinutes()).slice(-2);
break;
case 'j':
sDate += iDay;
break;
case 'l':
sDate += Usr.asDays[date.getDay()];
break;
case 'm':
sDate += ('0' + iMonth).slice(-2);
break;
case 'M':
sDate += Usr.asMonths[iMonth - 1].substr(0, 3);
break;
case 'n':
sDate += iMonth;
break;
case 's':
sDate += ('0' + date.getSeconds()).slice(-2);
break;
case 'y':
sDate += ("" + date.getFullYear()).slice(-2);
break;
case 'Y':
sDate += date.getFullYear();
break;
default:
sDate += ch;
break;
}
}
return sDate;
}
/**
* defineBitFields(bfs)
*
* Prepares a bit field definition for use with getBitField() and setBitField(); eg:
*
* var bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
*
* The above defines a set of bit fields containing four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31).
*
* Usr.setBitField(bfs.num, n, 1);
*
* The above set bit field "bfs.num" in numeric variable "n" to the value 1.
*
* @param {Object} bfs
* @return {BitFields}
*/
static defineBitFields(bfs)
{
var bit = 0;
for (var f in bfs) {
var width = bfs[f];
var mask = ((1 << width) - 1) << bit;
bfs[f] = {mask: mask, shift: bit};
bit += width;
}
return bfs;
}
/**
* initBitFields(bfs, ...)
*
* @param {BitFields} bfs
* @param {...number} var_args
* @return {number} a value containing all supplied bit fields
*/
static initBitFields(bfs, var_args)
{
var v = 0, i = 1;
for (var f in bfs) {
if (i >= arguments.length) break;
v = Usr.setBitField(bfs[f], v, arguments[i++]);
}
return v;
}
/**
* getBitField(bf, v)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @return {number} the value of the bit field in v defined by bf
*/
static getBitField(bf, v)
{
return (v & bf.mask) >> bf.shift;
}
/**
* setBitField(bf, v, n)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @param {number} n is a value to store in v in the bit field defined by bf
* @return {number} updated v
*/
static setBitField(bf, v, n)
{
return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);
}
/**
* indexOf(a, t, i)
*
* Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.
*
* @param {Array} a
* @param {*} t
* @param {number} [i]
* @returns {number}
*/
static indexOf(a, t, i)
{
if (Array.prototype.indexOf) {
return a.indexOf(t, i);
}
i = i || 0;
if (i < 0) i += a.length;
if (i < 0) i = 0;
for (var n = a.length; i < n; i++) {
if (i in a && a[i] === t) return i;
}
return -1;
}
}
Usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
Usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
Usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
Usr.getTime = Date.now || function() { return +new Date(); };
/**
* @copyright http://pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2017
*/
/*
* According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties
* and functions of JavaScript-in-the-Browser:
*
* Property Description
* ---
* Infinity A numeric value that represents positive/negative infinity
* NaN "Not-a-Number" value
* undefined Indicates that a variable has not been assigned a value
*
* Function Description
* ---
* decodeURI() Decodes a URI
* decodeURIComponent() Decodes a URI component
* encodeURI() Encodes a URI
* encodeURIComponent() Encodes a URI component
* escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead
* eval() Evaluates a string and executes it as if it was script code
* isFinite() Determines whether a value is a finite, legal number
* isNaN() Determines whether a value is an illegal number
* Number() Converts an object's value to a number
* parseFloat() Parses a string and returns a floating point number
* parseInt() Parses a string and returns an integer
* String() Converts an object's value to a string
* unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead
*
* And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions
* of the *window* object.
*
* Property Description
* ---
* closed Returns a Boolean value indicating whether a window has been closed or not
* defaultStatus Sets or returns the default text in the statusbar of a window
* document Returns the Document object for the window (See Document object)
* frames Returns an array of all the frames (including iframes) in the current window
* history Returns the History object for the window (See History object)
* innerHeight Returns the inner height of a window's content area
* innerWidth Returns the inner width of a window's content area
* length Returns the number of frames (including iframes) in a window
* location Returns the Location object for the window (See Location object)
* name Sets or returns the name of a window
* navigator Returns the Navigator object for the window (See Navigator object)
* opener Returns a reference to the window that created the window
* outerHeight Returns the outer height of a window, including toolbars/scrollbars
* outerWidth Returns the outer width of a window, including toolbars/scrollbars
* pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window
* pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window
* parent Returns the parent window of the current window
* screen Returns the Screen object for the window (See Screen object)
* screenLeft Returns the x coordinate of the window relative to the screen
* screenTop Returns the y coordinate of the window relative to the screen
* screenX Returns the x coordinate of the window relative to the screen
* screenY Returns the y coordinate of the window relative to the screen
* self Returns the current window
* status Sets or returns the text in the statusbar of a window
* top Returns the topmost browser window
*
* Method Description
* ---
* alert() Displays an alert box with a message and an OK button
* atob() Decodes a base-64 encoded string
* blur() Removes focus from the current window
* btoa() Encodes a string in base-64
* clearInterval() Clears a timer set with setInterval()
* clearTimeout() Clears a timer set with setTimeout()
* close() Closes the current window
* confirm() Displays a dialog box with a message and an OK and a Cancel button
* createPopup() Creates a pop-up window
* focus() Sets focus to the current window
* moveBy() Moves a window relative to its current position
* moveTo() Moves a window to the specified position
* open() Opens a new browser window
* print() Prints the content of the current window
* prompt() Displays a dialog box that prompts the visitor for input
* resizeBy() Resizes the window by the specified pixels
* resizeTo() Resizes the window to the specified width and height
* scroll() This method has been replaced by the scrollTo() method.
* scrollBy() Scrolls the content by the specified number of pixels
* scrollTo() Scrolls the content to the specified coordinates
* setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds)
* setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds
* stop() Stops the window from loading
*/
class Web {
/**
* log(s, type)
*
* For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line)
* for Component.log() to display anything.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
Component.log(s, type);
}
/**
* notice(s, fPrintOnly, id)
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
static notice(s, fPrintOnly, id)
{
Component.notice(s, fPrintOnly, id);
}
/**
* getResource(sURL, dataPost, fAsync, done)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no
* idea when the request is complete or what the response was. done() is passed three parameters:
*
* done(sURL, sResource, nErrorCode)
*
* If nErrorCode is zero, sResource should contain the requested data; otherwise, an error occurred.
*
* If dataPost is set to a string, that string can be used to control the response format;
* by default, the response format is plain text, but you can specify "bytes" to request arbitrary
* binary data, which should come back as a string of bytes.
*
* TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice.
* Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum)
* by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send().
*
* @param {string} sURL
* @param {string|Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string,number)} [done]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
static getResource(sURL, dataPost, fAsync = false, done)
{
var nErrorCode = 0, sResource = null, response = null;
if (typeof resources == 'object' && (sResource = resources[sURL])) {
if (done) done(sURL, sResource, nErrorCode);
return [sResource, nErrorCode];
}
else if (fAsync && typeof resources == 'function') {
resources(sURL, function(sResource, nErrorCode)
{
if (done) done(sURL, sResource, nErrorCode);
});
return response;
}
if (DEBUG) {
/*
* The larger resources we put on archive.pcjs.org should also be available locally...
*/
sURL = sURL.replace(/^http:\/\/archive.pcjs.org(\/.*)\/([^\/]*)$/, "$1/archive/$2");
}
var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP"));
if (fAsync) {
xmlHTTP.onreadystatechange = function()
{
if (xmlHTTP.readyState === 4) {
/*
* The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple
* times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when
* debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests
* pending, any debugging activity means most of them simply get dropped on floor, so what may actually be
* happening are mis-notifications rather than redundant notifications.
*
* xmlHTTP.onreadystatechange = undefined;
*/
sResource = xmlHTTP.responseText;
/*
* The normal "success" case is an HTTP status code of 200, but when testing with files loaded
* from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible".
*/
if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && Web.getHostProtocol() == "file:") {
if (MAXDEBUG) Web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes");
}
else {
nErrorCode = xmlHTTP.status || -1;
Web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
}
};
}
if (dataPost && typeof dataPost == "object") {
var sDataPost = "";
for (var p in dataPost) {
if (!dataPost.hasOwnProperty(p)) continue;
if (sDataPost) sDataPost += "&";
sDataPost += p + '=' + encodeURIComponent(dataPost[p]);
}
sDataPost = sDataPost.replace(/%20/g, '+');
if (MAXDEBUG) Web.log("Web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes");
xmlHTTP.open("POST", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlHTTP.send(sDataPost);
} else {
if (MAXDEBUG) Web.log("Web.getResource(GET " + sURL + ")");
xmlHTTP.open("GET", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
if (dataPost == "bytes") {
xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined");
}
xmlHTTP.send();
}
if (!fAsync) {
sResource = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
if (MAXDEBUG) Web.log("Web.getResource(" + sURL + "): returned " + sResource.length + " bytes");
} else {
nErrorCode = xmlHTTP.status || -1;
Web.log("Web.getResource(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
return response;
}
/**
* parseMemoryResource(sURL, sData)
*
* This converts a variety of JSON-style data streams into an Object with the following properties:
*
* aBytes
* aSymbols
* addrLoad
* addrExec
*
* If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if
* it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes',
* and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and
* stored in 'aBytes'.
*
* Alternatively, if the source data contains a 'data' array, we simply pass that through to the output
* object as:
*
* aData
*
* @param {string} sURL
* @param {string} sData
* @return {Object|null} (resource)
*/
static parseMemoryResource(sURL, sData)
{
var i;
var resource = {
aBytes: null,
aSymbols: null,
addrLoad: null,
addrExec: null
};
if (sData.charAt(0) == "[" || sData.charAt(0) == "{") {
try {
var a, ib, data;
if (sData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/*
* Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* tape data. So if the data we've received appears to be "HTML-like", we treat it as an error message.
*/
throw new Error(sData);
}
/*
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
if (sData.indexOf("0x") < 0 && sData.indexOf("0o") < 0 && sData.substr(0, 2) != '["') {
data = JSON.parse(sData.replace(/([a-z]+):/gm, '"$1":').replace(/\/\/[^\n]*/gm, ""));
} else {
data = eval("(" + sData + ")");
}
resource.addrLoad = data['load'];
resource.addrExec = data['exec'];
if (a = data['bytes']) {
resource.aBytes = a;
}
else if (a = data['words']) {
/*
* Convert all words into bytes
*/
resource.aBytes = new Array(a.length * 2);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
}
}
else if (a = data['longs']) {
/*
* Convert all dwords (longs) into bytes
*/
resource.aBytes = new Array(a.length * 4);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
resource.aBytes[ib++] = (a[i] >> 16) & 0xff;
resource.aBytes[ib++] = (a[i] >> 24) & 0xff;
}
}
else if (a = data['data']) {
resource.aData = a;
}
else {
resource.aBytes = data;
}
if (resource.aBytes) {
if (!resource.aBytes.length) {
Component.error("Empty resource: " + sURL);
resource = null;
}
else if (resource.aBytes.length == 1) {
Component.error(resource.aBytes[0]);
resource = null;
}
}
resource.aSymbols = data['symbols'];
} catch (e) {
Component.error("Resource data error (" + sURL + "): " + e.message);
resource = null;
}
}
else {
/*
* Parse the data manually; we assume it's a series of hex byte-values separated by whitespace.
*/
var ab = [];
var sHexData = sData.replace(/\n/gm, " ").replace(/ +$/, "");
var asHexData = sHexData.split(" ");
for (i = 0; i < asHexData.length; i++) {
var n = parseInt(asHexData[i], 16);
if (isNaN(n)) {
Component.error("Resource data error (" + sURL + "): invalid hex byte (" + asHexData[i] + ")");
break;
}
ab.push(n & 0xff);
}
if (i == asHexData.length) resource.aBytes = ab;
}
return resource;
}
/**
* sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
*
* Send a report (eg, bug report) to the server.
*
* @param {string} sApp (eg, "PCjs")
* @param {string} sVer (eg, "1.02")
* @param {string} sURL (eg, "/devices/pc/machine/5150/mda/64kb/machine.xml")
* @param {string} sUser (ie, the user key, if any)
* @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.*
* @param {string} sReport (eg, unparsed state data)
* @param {string} [sHostName] (default is http://SITEHOST)
*/
static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
{
var dataPost = {};
dataPost[ReportAPI.QUERY.APP] = sApp;
dataPost[ReportAPI.QUERY.VER] = sVer;
dataPost[ReportAPI.QUERY.URL] = sURL;
dataPost[ReportAPI.QUERY.USER] = sUser;
dataPost[ReportAPI.QUERY.TYPE] = sType;
dataPost[ReportAPI.QUERY.DATA] = sReport;
var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT;
Web.getResource(sReportURL, dataPost, true);
}
/**
* getHost()
*
* @return {string}
*/
static getHost()
{
return ("http://" + (window? window.location.host : SITEHOST));
}
/**
* getHostURL()
*
* @return {string|null}
*/
static getHostURL()
{
return (window? window.location.href : null);
}
/**
* getHostProtocol()
*
* @return {string}
*/
static getHostProtocol()
{
return (window? window.location.protocol : "file:");
}
/**
* getUserAgent()
*
* @return {string}
*/
static getUserAgent()
{
return (window? window.navigator.userAgent : "");
}
/**
* hasLocalStorage
*
* true if localStorage support exists, is enabled, and works; false otherwise
*
* @return {boolean}
*/
static hasLocalStorage()
{
if (Web.fLocalStorage == null) {
var f = false;
if (window) {
try {
window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest);
f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest);
window.localStorage.removeItem(Web.sLocalStorageTest);
} catch (e) {
Web.logLocalStorageError(e);
f = false;
}
}
Web.fLocalStorage = f;
}
return Web.fLocalStorage;
}
/**
* logLocalStorageError(e)
*
* @param {Error} e is an exception
*/
static logLocalStorageError(e)
{
Web.log(e.message, "localStorage error");
}
/**
* getLocalStorageItem(sKey)
*
* Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available
*
* @param {string} sKey
* @return {string|null|undefined} sValue
*/
static getLocalStorageItem(sKey)
{
var sValue;
if (window) {
try {
sValue = window.localStorage.getItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
return sValue;
}
/**
* setLocalStorageItem(sKey, sValue)
*
* @param {string} sKey
* @param {string} sValue
* @return {boolean} true if localStorage is available, false if not
*/
static setLocalStorageItem(sKey, sValue)
{
try {
window.localStorage.setItem(sKey, sValue);
return true;
} catch (e) {
Web.logLocalStorageError(e);
}
return false;
}
/**
* removeLocalStorageItem(sKey)
*
* @param {string} sKey
*/
static removeLocalStorageItem(sKey)
{
try {
window.localStorage.removeItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
/**
* getLocalStorageKeys()
*
* @return {Array}
*/
static getLocalStorageKeys()
{
var a = [];
try {
for (var i = 0, c = window.localStorage.length; i < c; i++) {
a.push(window.localStorage.key(i));
}
} catch (e) {
Web.logLocalStorageError(e);
}
return a;
}
/**
* reloadPage()
*/
static reloadPage()
{
if (window) window.location.reload();
}
/**
* isUserAgent(s)
*
* Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can
* use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE").
*
* 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating
* the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx);
* they say "public websites should rely on feature detection, rather than browser detection, in order to design
* their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent
* that tries to fool apps into thinking the browser is more like WebKit or Gecko:
*
* Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
*
* That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained
* some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because
* that's a sledgehammer solution which restores the old user-agent string but also disables other features like
* HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or
* "Trident".
*
* UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't
* any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll
* need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture.
*
* @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values
* @return {boolean} is true if the string was found, false if not
*/
static isUserAgent(s)
{
if (window) {
var userAgent = Web.getUserAgent();
/*
* Here's one case where we have to be careful with Component, because when isUserAgent() is called by
* the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call.
*
* Web.log("agent: " + userAgent);
*
* And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure
* Compiler (v20130823) failing to detect the entire expression as a boolean.
*/
return s == "iOS" && !!userAgent.match(/(iPod|iPhone|iPad)/) && !!userAgent.match(/AppleWebKit/) || s == "MSIE" && !!userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0);
}
return false;
}
/**
* isMobile()
*
* Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation:
*
* https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
*
* @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not
*/
static isMobile()
{
return Web.isUserAgent("Mobi");
}
/**
* getURLParm(sParm)
*
* First looks for sParm exactly as specified, then looks for the lower-case version.
*
* @param {string} sParm
* @return {string|undefined}
*/
static getURLParm(sParm)
{
if (!Web.parmsURL) {
Web.parmsURL = Web.parseURLParms();
}
return Web.parmsURL[sParm] || Web.parmsURL[sParm.toLowerCase()];
}
/**
* parseURLParms(sParms)
*
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
* @return {Object} containing properties for each parameter found
*/
static parseURLParms(sParms)
{
var aParms = {};
if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used
if (!sParms) {
/*
* Note that window.location.href returns the entire URL, whereas window.location.search
* returns only the parameters, if any (starting with the '?', which we skip over with a substr() call).
*/
sParms = window.location.search.substr(1);
}
var match;
var pl = /\+/g; // RegExp for replacing addition symbol with a space
var search = /([^&=]+)=?([^&]*)/g;
var decode = function(s)
{
return decodeURIComponent(s.replace(pl, " "));
};
while ((match = search.exec(sParms))) {
aParms[decode(match[1])] = decode(match[2]);
}
}
return aParms;
}
/**
* downloadFile(sData, sType, fBase64, sFileName)
*
* @param {string} sData
* @param {string} sType
* @param {boolean} [fBase64]
* @param {string} [sFileName]
*/
static downloadFile(sData, sType, fBase64, sFileName)
{
var link = null, sAlert;
var sURI = "data:application/" + sType + (fBase64? ";base64" : "") + ",";
if (!Web.isUserAgent("Firefox")) {
sURI += (fBase64? sData : encodeURI(sData));
} else {
sURI += (fBase64? sData : encodeURIComponent(sData));
}
if (sFileName) {
link = document.createElement('a');
if (typeof link.download != 'string') link = null;
}
if (link) {
link.href = sURI;
link.download = sFileName;
document.body.appendChild(link); // Firefox allegedly requires the link to be in the body
link.click();
document.body.removeChild(link);
sAlert = 'Check your Downloads folder for ' + sFileName + '.';
} else {
window.open(sURI);
sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.';
}
return sAlert;
}
/**
* onCountRepeat(n, fnRepeat, fnComplete, msDelay)
*
* Call fnRepeat() n times with an msDelay millisecond delay between calls,
* then call fnComplete() when n has been exhausted OR fnRepeat() returns false.
*
* @param {number} n
* @param {function()} fnRepeat
* @param {function()} fnComplete
* @param {number} [msDelay]
*/
static onCountRepeat(n, fnRepeat, fnComplete, msDelay)
{
var fnTimeout = function doCountRepeat()
{
n -= 1;
if (n >= 0) {
if (!fnRepeat()) n = 0;
}
if (n > 0) {
setTimeout(fnTimeout, msDelay || 0);
return;
}
fnComplete();
};
fnTimeout();
}
/**
* onClickRepeat(e, msDelay, msRepeat, fn)
*
* Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter,
* as long as HTML control Object e has an active "down" event and fn() returns true.
*
* @param {Object} e
* @param {number} msDelay
* @param {number} msRepeat
* @param {function(boolean)} fn is passed false on the first call, true on all repeated calls
*/
static onClickRepeat(e, msDelay, msRepeat, fn)
{
var ms = 0, timer = null, fIgnoreMouseEvents = false;
var fnRepeat = function doClickRepeat()
{
if (fn(ms === msRepeat)) {
timer = setTimeout(fnRepeat, ms);
ms = msRepeat;
}
};
e.onmousedown = function()
{
// Web.log("onMouseDown()");
if (!fIgnoreMouseEvents) {
if (!timer) {
ms = msDelay;
fnRepeat();
}
}
};
e.ontouchstart = function()
{
// Web.log("onTouchStart()");
if (!timer) {
ms = msDelay;
fnRepeat();
}
};
e.onmouseup = e.onmouseout = function()
{
// Web.log("onMouseUp()/onMouseOut()");
if (timer) {
clearTimeout(timer);
timer = null;
}
};
e.ontouchend = e.ontouchcancel = function()
{
// Web.log("onTouchEnd()/onTouchCancel()");
if (timer) {
clearTimeout(timer);
timer = null;
}
/*
* Devices that generate ontouch* events ALSO generate onmouse* events,
* and generally do so immediately after all the touch events are complete,
* so unless we want double the action, we need to ignore mouse events.
*/
fIgnoreMouseEvents = true;
};
}
/**
* onPageEvent(sName, fn)
*
* For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but
* instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively.
*
* The only components that should still use onPageEvent() are THIS component (see the bottom of this file)
* and components that need to capture other events (eg, the 'onresize' event in the Video component).
*
* This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers
* for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'],
* etc, themselves. However, that's less of a concern now, because assuming everyone else is now using
* onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one.
*
* NOTE: It's risky to refer to obscure event handlers with "dot" names, because the Closure Compiler may
* erroneously replace them (eg, window.onpageshow is a good example).
*
* @param {string} sFunc
* @param {function()} fn
*/
static onPageEvent(sFunc, fn)
{
if (window) {
var fnPrev = window[sFunc];
if (typeof fnPrev !== 'function') {
window[sFunc] = fn;
} else {
/*
* TODO: Determine whether there's any value in receiving/sending the Event object that the
* browser provides when it generates the original event.
*/
window[sFunc] = function onWindowEvent()
{
if (fnPrev) fnPrev();
fn();
};
}
}
};
/**
* onInit(fn)
*
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
*
* @param {function()} fn
*/
static onInit(fn)
{
Web.aPageEventHandlers['init'].push(fn);
};
/**
* onShow(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler.
*/
static onShow(fn)
{
Web.aPageEventHandlers['show'].push(fn);
};
/**
* onExit(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler.
*/
static onExit(fn)
{
Web.aPageEventHandlers['exit'].push(fn);
};
/**
* doPageEvent(afn)
*
* @param {Array.<function()>} afn
*/
static doPageEvent(afn)
{
if (Web.fPageEventsEnabled) {
try {
for (var i = 0; i < afn.length; i++) {
afn[i]();
}
} catch (e) {
Web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks.");
}
}
};
/**
* enablePageEvents(fEnable)
*
* @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)
*/
static enablePageEvents(fEnable)
{
if (!Web.fPageEventsEnabled && fEnable) {
Web.fPageEventsEnabled = true;
if (Web.fPageLoaded) Web.sendPageEvent('init');
if (Web.fPageShowed) Web.sendPageEvent('show');
return;
}
Web.fPageEventsEnabled = fEnable;
}
/**
* sendPageEvent(sEvent)
*
* This allows us to manually trigger page events.
*
* @param {string} sEvent (one of 'init', 'show' or 'exit')
*/
static sendPageEvent(sEvent)
{
if (Web.aPageEventHandlers[sEvent]) {
Web.doPageEvent(Web.aPageEventHandlers[sEvent]);
}
}
}
Web.parmsURL = null; // initialized on first call to parseURLParms()
Web.aPageEventHandlers = {
'init': [], // list of window 'onload' handlers
'show': [], // list of window 'onpageshow' handlers
'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible)
};
Web.fPageLoaded = false; // set once the page's first 'onload' event has occurred
Web.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred
Web.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents()
/**
* fLocalStorage
*
* true if localStorage support exists, is enabled, and works; "falsey" otherwise
*
* @type {boolean|null}
*/
Web.fLocalStorage = null;
/**
* TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it.
*
* @const {string}
*/
Web.sLocalStorageTest = "PCjs.localStorage";
Web.onPageEvent('onload', function onPageLoad() {
Web.fPageLoaded = true;
Web.doPageEvent(Web.aPageEventHandlers['init']);
});
Web.onPageEvent('onpageshow', function onPageShow() {
Web.fPageShowed = true;
Web.doPageEvent(Web.aPageEventHandlers['show']);
});
Web.onPageEvent(Web.isUserAgent("Opera") || Web.isUserAgent("iOS")? 'onunload' : 'onbeforeunload', function onPageUnload() {
Web.doPageEvent(Web.aPageEventHandlers['exit']);
});
/**
* @copyright http://pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2017
*/
/*
* All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile
* everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about
* the JSDoc types supported by the Closure Compiler:
*
* https://developers.google.com/closure/compiler/docs/js-for-compiler#types
*
* I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like
* "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace
* all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc.
*
* I prefer sticking with traditional C-style idioms, in part because they are more portable. That
* does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code
* I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need.
*
* UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults.
* And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need
* for those kinds of expressions will be reduced as I also start adopting some ES6 features, like
* default parameters.
*/
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*
* the named property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class Component {
/**
* Component(type, parms, bitsMessage)
*
* A Component object requires:
*
* type: a user-defined type name (eg, "CPU")
*
* and accepts any or all of the following (parms) properties:
*
* id: component ID (default is "")
* name: component name (default is ""; if blank, toString() will use the type name only)
* comment: component comment string (default is undefined)
*
* Component subclasses will usually have additional (parms) properties.
*
* @param {string} type
* @param {Object} [parms]
* @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0)
*/
constructor(type, parms, bitsMessage)
{
this.type = type;
if (!parms) parms = {'id': "", 'name': ""};
this.id = parms['id'] || "";
this.name = parms['name'];
this.comment = parms['comment'];
this.parms = parms;
/*
* The following Component properties need to be accessible by other machines and/or command scripts;
* well, OK, or we could have exported some new functions to walk the contents of these properties, as we
* did with findMachineComponent(), but this works just as well.
*
* Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it
* resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the
* Compiler wants us to change all the other references to bracket notation as well.
*/
this.exports = this['exports'] = {};
this.bindings = this['bindings'] = {};
var i = this.id.indexOf('.');
if (i < 0) {
this.idComponent = this.id;
} else {
this.idMachine = this.id.substr(0, i);
this.idComponent = this.id.substr(i + 1);
}
/*
* Gather all the various component flags (booleans) into a single "flags" object, and encourage
* subclasses to do the same, to reduce the property clutter we have to wade through while debugging.
*/
this.flags = {
ready: false,
busy: false,
busyCancel: false,
powered: false,
unloading: false,
error: false
};
this.fnReady = null;
this.clearError();
this.bitsMessage = bitsMessage || 0;
/** @type {Object|null} controlPrint is the HTML control, if any, that we can print to */
this.controlPrint = null;
this.cmp = null;
this.bus = null;
this.cpu = null;
this.dbg = null;
/*
* TODO: Consider adding another parameter to the Component() constructor that allows components to tell
* us if they support single or multiple instances per machine. For example, there can be multiple SerialPort
* components per machine, but only one CPU component (some machines also support an FPU, but that component
* is considered separate from the CPU).
*
* It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly
* includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive
* CPU controls.
*/
Component.add(this);
}
/**
* Component.add(component)
*
* @param {Component} component
*/
static add(component)
{
/*
* This just generates a lot of useless noise, handy in the early days, not so much these days....
*
* if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")");
*/
Component.components.push(component);
}
/**
* Component.addMachine(idMachine)
*
* @param {string} idMachine
*/
static addMachine(idMachine)
{
Component.machines[idMachine] = {};
}
/**
* Component.addMachineResource(idMachine, sName, data)
*
* @param {string} idMachine
* @param {string|null} sName (name of the resource)
* @param {*} data
*/
static addMachineResource(idMachine, sName, data)
{
/*
* I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used,
* so addMachine() is never called, so resources do not need to be recorded.
*/
if (Component.machines[idMachine] && sName) {
Component.machines[idMachine][sName] = data;
}
}
/**
* Component.getMachineResources(idMachine)
*
* @param {string} idMachine
* @return {Object|undefined}
*/
static getMachineResources(idMachine)
{
return Component.machines[idMachine];
}
/**
* Component.getTime()
*
* @return {number} the current time, in milliseconds
*/
static getTime()
{
return Date.now() || +new Date();
}
/**
* Component.log(s, type)
*
* For diagnostic output only.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
if (!COMPILED) {
if (s) {
var sElapsed = "", sMsg = (type? (type + ": ") : "") + s;
if (typeof Usr != "undefined") {
if (Component.msStart === undefined) {
Component.msStart = Component.getTime();
}
sElapsed = (Component.getTime() - Component.msStart) + "ms: ";
}
if (window && window.console) console.log(sElapsed + sMsg.replace(/\n/g, " "));
}
}
}
/**
* Component.assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds.
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @param {boolean} f is the expression we are asserting to be true
* @param {string} [s] is description of the assertion on failure
*/
static assert(f, s)
{
if (DEBUG) {
if (!f) {
if (!s) s = "assertion failure";
Component.log(s);
throw new Error(s);
}
}
}
/**
* Component.println(s, type, id)
*
* For non-diagnostic messages, which components may override to control the destination/appearance of their output.
*
* Components that inherit from this class should use the instance method, this.println(), rather than Component.println(),
* because if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the class
* method would improperly affect any other machines loaded on the same page).
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
static println(s, type, id)
{
if (!COMPILED) {
Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type);
}
}
/**
* Component.notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, so we alert() as well.
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
static notice(s, fPrintOnly, id)
{
if (!COMPILED) {
Component.println(s, "notice", id);
}
if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s);
}
/**
* Component.warning(s)
*
* @param {string} s describes the warning
*/
static warning(s)
{
if (!COMPILED) {
Component.println(s, "warning");
}
Component.alertUser(s);
}
/**
* Component.error(s)
*
* @param {string} s describes the error; an alert() is displayed as well
*/
static error(s)
{
if (!COMPILED) {
Component.println(s, "error");
}
Component.alertUser(s);
}
/**
* Component.alertUser(sMessage)
*
* @param {string} sMessage
*/
static alertUser(sMessage)
{
if (window) {
window.alert(sMessage);
} else {
Component.log(sMessage);
}
};
/**
* Component.confirmUser(sPrompt)
*
* @param {string} sPrompt
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
*/
static confirmUser(sPrompt)
{
var fResponse = false;
if (window) {
fResponse = window.confirm(sPrompt);
}
return fResponse;
}
/**
* Component.promptUser()
*
* @param {string} sPrompt
* @param {string} [sDefault]
* @returns {string|null}
*/
static promptUser(sPrompt, sDefault)
{
var sResponse = null;
if (window) {
sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault);
}
return sResponse;
}
/**
* Component.getComponents(idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} [idRelated] of related component
* @return {Array} of components
*/
static getComponents(idRelated)
{
var i;
var aComponents = [];
/*
* getComponentByID(id, idRelated)
*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0)
idRelated = idRelated.substr(0, i + 1);
else
idRelated = "";
}
for (i = 0; i < Component.components.length; i++) {
var component = Component.components[i];
if (!idRelated || !component.id.indexOf(idRelated)) {
aComponents.push(component);
}
}
return aComponents;
}
/**
* Component.getComponentByID(id, idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} id of the desired component
* @param {string} [idRelated] of related component
* @return {Component|null}
*/
static getComponentByID(id, idRelated)
{
if (id !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated && (i = idRelated.indexOf('.')) > 0) {
id = idRelated.substr(0, i + 1) + id;
}
for (i = 0; i < Component.components.length; i++) {
if (Component.components[i].id === id) {
return Component.components[i];
}
}
if (Component.components.length) {
Component.log("Component ID '" + id + "' not found", "warning");
}
}
return null;
}
/**
* Component.getComponentByType(sType, idRelated, componentPrev)
*
* @param {string} sType of the desired component
* @param {string} [idRelated] of related component
* @param {Component|null} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
static getComponentByType(sType, idRelated, componentPrev)
{
if (sType !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0) {
idRelated = idRelated.substr(0, i + 1);
} else {
idRelated = "";
}
}
for (i = 0; i < Component.components.length; i++) {
if (componentPrev) {
if (componentPrev == Component.components[i]) componentPrev = null;
continue;
}
if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) {
return Component.components[i];
}
}
Component.log("Component type '" + sType + "' not found", "warning");
}
return null;
}
/**
* Component.getComponentParms(element)
*
* @param {Object} element from the DOM
*/
static getComponentParms(element)
{
var parms = null;
var sParms = element.getAttribute("data-value");
if (sParms) {
try {
parms = eval("(" + sParms + ")"); // jshint ignore:line
/*
* We can no longer invoke removeAttribute() because some components (eg, Panel) need
* to run their initXXX() code more than once, to avoid initialization-order dependencies.
*
* if (!DEBUG) {
* element.removeAttribute("data-value");
* }
*/
} catch(e) {
Component.error(e.message + " (" + sParms + ")");
}
}
return parms;
}
/**
* Component.bindExternalControl(component, sControl, sBinding, sType)
*
* @param {Component} component
* @param {string} sControl
* @param {string} sBinding
* @param {string} [sType] is the external component type
*/
static bindExternalControl(component, sControl, sBinding, sType)
{
if (sControl) {
if (sType === undefined) sType = "Panel";
var target = Component.getComponentByType(sType, component.id);
if (target) {
var eBinding = target.bindings[sControl];
if (eBinding) {
component.setBinding(null, sBinding, eBinding);
}
}
}
}
/**
* Component.bindComponentControls(component, element, sAppClass)
*
* @param {Component} component
* @param {Object} element from the DOM
* @param {string} sAppClass
*/
static bindComponentControls(component, element, sAppClass)
{
var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control");
for (var iControl = 0; iControl < aeControls.length; iControl++) {
var aeChildNodes = aeControls[iControl].childNodes;
for (var iNode = 0; iNode < aeChildNodes.length; iNode++) {
var control = aeChildNodes[iNode];
if (control.nodeType !== 1 /* document.ELEMENT_NODE */) {
continue;
}
var sClass = control.getAttribute("class");
if (!sClass) continue;
var aClasses = sClass.split(" ");
for (var iClass = 0; iClass < aClasses.length; iClass++) {
var parms;
sClass = aClasses[iClass];
switch (sClass) {
case sAppClass + "-binding":
parms = Component.getComponentParms(control);
if (parms && parms['binding']) {
component.setBinding(parms['type'], parms['binding'], control, parms['value']);
} else if (!parms || parms['type'] != "description") {
Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning");
}
iClass = aClasses.length;
break;
default:
// if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning");
break;
}
}
}
}
}
/**
* Component.getElementsByClass(element, sClass, sObjClass)
*
* This is a cross-browser helper function, since not all browser's support getElementsByClassName()
*
* TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above,
* to keep all the browser-related code together.
*
* @param {Object} element from the DOM
* @param {string} sClass
* @param {string} [sObjClass]
* @return {Array|NodeList}
*/
static getElementsByClass(element, sClass, sObjClass)
{
if (sObjClass) sClass += '-' + sObjClass + "-object";
/*
* Use the browser's built-in getElementsByClassName() if it appears to be available
* (for example, it's not available in IE8, but it should be available in IE9 and up)
*/
if (element.getElementsByClassName) {
return element.getElementsByClassName(sClass);
}
var i, j, ae = [];
var aeAll = element.getElementsByTagName("*");
var re = new RegExp('(^| )' + sClass + '( |$)');
for (i = 0, j = aeAll.length; i < j; i++) {
if (re.test(aeAll[i].className)) {
ae.push(aeAll[i]);
}
}
if (!ae.length) {
Component.log('No elements of class "' + sClass + '" found');
}
return ae;
}
/**
* Component.getScriptCommands(sScript)
*
* This is a simple parser that breaks sScript into an array of commands, where each command
* is an array of tokens, where tokens are sequences of characters separated by any of: tab, space,
* carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is
* used, all characters up to the next matching quote become part of the token, allowing any of the
* other separators to be part of the token. CR, LF and semicolon also serve to terminate a command,
* with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire
* script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does,
* and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well;
* see convertMD() in markout.js, where the aCommandDefs array is built).
*
* Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash
* characters, since the entire script string is injected into a JavaScript function call, so any
* backslash sequence that JavaScript supports is automatically converted:
*
* \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX
* ^J ^M ^K ^I ^H ^L
*
* To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX
* is the ASCII code in hex. For ESC, that would \x1B.
*
* @param {string} sScript
* @return {Array}
*/
static getScriptCommands(sScript)
{
var cch = sScript.length;
var aCommands = [], aTokens = [], sToken = "", chQuote = null;
for (var i = 0; i < cch; i++) {
var ch = sScript[i];
if (ch == '"' || ch == "'") {
if (chQuote && ch != chQuote) {
sToken += ch;
continue;
}
if (!chQuote) {
chQuote = ch;
} else {
chQuote = null;
}
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
continue;
}
if (!chQuote) {
if (ch == '\r' || ch == '\n') {
ch = ';';
}
if (ch == ' ' || ch == '\t' || ch == ';') {
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
if (ch == ';' && aTokens.length) {
aCommands.push(aTokens);
aTokens = [];
}
continue;
}
}
sToken += ch;
}
if (sToken) {
aTokens.push(sToken);
}
if (aTokens.length) {
aCommands.push(aTokens);
}
return aCommands;
}
/**
* Component.processScript(idMachine, sScript)
*
* @param {string} idMachine
* @param {string} sScript
* @return {boolean}
*/
static processScript(idMachine, sScript)
{
var fSuccess = false;
idMachine += ".machine";
if (typeof sScript == "string" && !Component.commands[idMachine]) {
fSuccess = true;
Component.commands[idMachine] = Component.getScriptCommands(sScript);
if (!Component.processCommands(idMachine)) {
fSuccess = false;
}
}
return fSuccess;
}
/**
* Component.processCommands(idMachine)
*
* @param {string} idMachine
* @return {boolean}
*/
static processCommands(idMachine)
{
var fSuccess = true;
var aCommands = Component.commands[idMachine];
// var dbg = Component.getComponentByType("Debugger", idMachine);
while (aCommands && aCommands.length) {
var aTokens = aCommands.splice(0, 1)[0];
var sCommand = aTokens[0];
/*
* It's possible to route this output to the Debugger window with dbg.println()
* instead, but it's a bit too confusing mingling script output in a window that
* already mingles Debugger and machine output.
*/
Component.println('script: ' + aTokens.join(' '));
var fnCallReady = null;
if (Component.asyncCommands.indexOf(sCommand) >= 0) {
fnCallReady = function processNextCommand() {
return function() {
Component.processCommands(idMachine);
}
}();
}
var fnCommand = Component.globalCommands[sCommand];
if (fnCommand) {
if (!fnCallReady) {
fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]);
} else {
if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break;
}
}
else {
fSuccess = false;
var component = Component.getComponentByType(aTokens[1], idMachine);
if (component) {
fnCommand = Component.componentCommands[sCommand];
if (fnCommand) {
fSuccess = fnCommand(component, aTokens[2], aTokens[3]);
}
else {
var exports = component['exports'];
if (exports) {
fnCommand = exports[sCommand];
if (fnCommand) {
fSuccess = true;
if (!fnCallReady) {
fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]);
} else {
if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break;
}
}
}
}
}
}
if (!fSuccess) {
Component.alertUser("Script error: " + sCommand + (fnCommand? " failed" : " unrecognized"));
break;
}
}
if (aCommands && !aCommands.length) {
delete Component.commands[idMachine];
}
return fSuccess;
}
/**
* Component.scriptAlert(sMessage)
*
* @param {string} sMessage
* @return {boolean}
*/
static scriptAlert(sMessage)
{
Component.alertUser(sMessage);
return true;
}
/**
* Component.scriptSelect(component, sBinding, sValue)
*
* @param {Component} component
* @param {string} sBinding
* @param {string} sValue
* @return {boolean}
*/
static scriptSelect(component, sBinding, sValue)
{
var fSuccess = false;
var aBindings = component['bindings'];
var control = aBindings[sBinding];
if (control) {
for (var i = 0; i < control.options.length; i++) {
if (control.options[i].textContent == sValue) {
if (control.selectedIndex != i) {
control.selectedIndex = i;
}
fSuccess = true;
break;
}
}
}
return fSuccess;
}
/**
* Component.scriptSleep(fnCallback, sDelay)
*
* @param {function()} fnCallback
* @param {string} sDelay (in milliseconds)
* @return {boolean}
*/
static scriptSleep(fnCallback, sDelay)
{
setTimeout(fnCallback, +sDelay);
return false;
}
/**
* toString()
*
* @this {Component}
* @return {string}
*/
toString()
{
return (this.name? this.name : (this.id || this.type));
}
/**
* getMachineNum()
*
* @this {Component}
* @return {number} unique machine number
*/
getMachineNum()
{
var nMachine = 1;
if (this.idMachine) {
var aDigits = this.idMachine.match(/\d+/);
if (aDigits !== null)
nMachine = parseInt(aDigits[0], 10);
}
return nMachine;
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* Component's setBinding() method is intended to be overridden by subclasses.
*
* @this {Component}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
switch (sBinding) {
case "clear":
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
control.onclick = (function(component) {
return function clearPanel() {
if (component.bindings['print']) {
component.bindings['print'].value = "";
}
};
}(this));
}
return true;
case "print":
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
/*
* HACK: Save this particular HTML element so that the Debugger can access it, too
*/
this.controlPrint = control;
/*
* This was added for Firefox (Safari automatically clears the <textarea> on a page reload,
* but Firefox does not).
*/
control.value = "";
this.println = (function(control) {
return function printPanel(s, type) {
s = (type !== undefined? (type + ": ") : "") + (s || "");
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (COMPILED) {
if (control.value.length > 8192) {
control.value = control.value.substr(control.value.length - 4096);
}
}
control.value += s + "\n";
control.scrollTop = control.scrollHeight;
if (!COMPILED && window && window.console) console.log(s);
};
}(control));
/**
* Override this.notice() with a replacement function that eliminates the Component.alertUser() call
*
* @this {Component}
* @param {string} s
* @param {boolean} [fPrintOnly]
* @param {string} [id]
* @return {boolean}
*/
this.notice = function noticePanel(s, fPrintOnly, id) {
this.println(s, this.idComponent);
return true;
};
}
return true;
default:
return false;
}
}
/**
* log(s, type)
*
* For diagnostic output only.
*
* WARNING: Even though this function's body is completely wrapped in DEBUG, that won't prevent the Closure Compiler
* from including it, so all calls must still be prefixed with "if (DEBUG) ....". For this reason, the class method,
* Component.log(), is preferred, because the compiler IS smart enough to remove those calls.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
log(s, type)
{
if (!COMPILED) {
Component.log(s, type || this.id || this.type);
}
}
/**
* assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* WARNING: Make sure you preface all calls to this.assert() with "if (DEBUG)", because unlike Component.assert(),
* the Closure Compiler can't be sure that this instance method hasn't been overridden, so it refuses to treat it as
* dead code in non-DEBUG builds.
*
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @this {Component}
* @param {boolean|number} f is the expression asserted to be true
* @param {string} [s] is a description of the assertion to be displayed or logged on failure
*/
assert(f, s)
{
if (DEBUG) {
if (!f) {
s = "assertion failure in " + (this.id || this.type) + (s? ": " + s : "");
if (DEBUGGER && this.dbg) {
this.dbg.stopCPU();
/*
* Why do we throw an Error only to immediately catch and ignore it? Simply to give
* any IDE the opportunity to inspect the application's state. Even when the IDE has
* control, you should still be able to invoke Debugger commands from the IDE's REPL,
* using the '$' global function that the Debugger constructor defines; eg:
*
* $('r')
* $('dw 0:0')
* $('h')
* ...
*
* If you have no desire to stop on assertions, consider this a no-op. However, another
* potential benefit of creating an Error object is that, for browsers like Chrome, we get
* a stack trace, too.
*/
try {
throw new Error(s);
} catch(e) {
this.println(e.stack || e.message);
}
return;
}
this.log(s);
throw new Error(s);
}
}
}
/**
* println(s, type)
*
* For non-diagnostic messages, which components may override to control the destination/appearance of their output.
*
* Components using this.println() should wait until after their constructor has run to display any messages, because
* if a Control Panel has been loaded, its override will not take effect until its own constructor has run.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
println(s, type, id)
{
Component.println(s, type, id || this.id);
}
/**
* status(s)
*
* status() is like println() but it also includes information about the component (ie, the component ID),
* which is why there is no corresponding Component.status() function.
*
* @param {string} s is the message text
*/
status(s)
{
this.println(this.idComponent + ": " + s);
}
/**
* notice(s, fPrintOnly)
*
* notice() is like println() but implies a need for user notification, so we alert() as well; however, if this.println()
* is overridden, this.notice will be replaced with a similar override, on the assumption that the override is taking care
* of alerting the user.
*
* @this {Component}
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
* @return {boolean}
*/
notice(s, fPrintOnly, id)
{
if (!fPrintOnly) {
/*
* See if the associated computer, if any, is "unloading"....
*/
var computer = Component.getComponentByType("Computer", this.id);
if (computer && computer.flags.unloading) {
console.log("ignoring notice during unload: " + s);
return false;
}
}
Component.notice(s, fPrintOnly, id || this.type);
return true;
}
/**
* setError(s)
*
* Set a fatal error condition
*
* @this {Component}
* @param {string} s describes a fatal error condition
*/
setError(s)
{
this.flags.error = true;
this.notice(s); // TODO: Any cases where we should still prefix this string with "Fatal error: "?
}
/**
* clearError()
*
* Clear any fatal error condition
*
* @this {Component}
*/
clearError() {
this.flags.error = false;
}
/**
* isError()
*
* Report any fatal error condition
*
* @this {Component}
* @return {boolean} true if a fatal error condition exists, false if not
*/
isError()
{
if (this.flags.error) {
this.println(this.toString() + " error");
return true;
}
return false;
}
/**
* isReady(fnReady)
*
* Return the "ready" state of the component; if the component is not ready, it will queue the optional
* notification function, otherwise it will immediately call the notification function, if any, without queuing it.
*
* NOTE: Since only the Computer component actually cares about the "readiness" of other components, the so-called
* "queue" of notification functions supports exactly one function. This keeps things nice and simple.
*
* @this {Component}
* @param {function()} [fnReady]
* @return {boolean} true if the component is in a "ready" state, false if not
*/
isReady(fnReady)
{
if (fnReady) {
if (this.flags.ready) {
fnReady();
} else {
if (MAXDEBUG) this.log("NOT ready");
this.fnReady = fnReady;
}
}
return this.flags.ready;
}
/**
* setReady(fReady)
*
* Set the "ready" state of the component to true, and call any queued notification functions.
*
* @this {Component}
* @param {boolean} [fReady] is assumed to indicate "ready" unless EXPLICITLY set to false
*/
setReady(fReady)
{
if (!this.flags.error) {
this.flags.ready = (fReady !== false);
if (this.flags.ready) {
if (MAXDEBUG /* || this.name */) this.log("ready");
var fnReady = this.fnReady;
this.fnReady = null;
if (fnReady) fnReady();
}
}
}
/**
* isBusy(fCancel)
*
* Return the "busy" state of the component
*
* @this {Component}
* @param {boolean} [fCancel] is set to true to cancel a "busy" state
* @return {boolean} true if "busy", false if not
*/
isBusy(fCancel)
{
if (this.flags.busy) {
if (fCancel) {
this.flags.busyCancel = true;
} else if (fCancel === undefined) {
this.println(this.toString() + " busy");
}
}
return this.flags.busy;
}
/**
* setBusy(fBusy)
*
* Update the current busy state; if a busyCancel request is pending, it will be honored now.
*
* @this {Component}
* @param {boolean} fBusy
* @return {boolean}
*/
setBusy(fBusy)
{
if (this.flags.busyCancel) {
this.flags.busy = false;
this.flags.busyCancel = false;
return false;
}
if (this.flags.error) {
this.println(this.toString() + " error");
return false;
}
this.flags.busy = fBusy;
return this.flags.busy;
}
/**
* powerUp(fSave)
*
* @this {Component}
* @param {Object|null} data
* @param {boolean} [fRepower] is true if this is "repower" notification
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
this.flags.powered = true;
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Component}
* @param {boolean} fSave
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
if (fShutdown) this.flags.powered = false;
return true;
}
/**
* messageEnabled(bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
*
* @this {Component}
* @param {number} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @return {boolean} true if all specified message enabled, false if not
*/
messageEnabled(bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (this === this.dbg) {
bitsMessage |= 0;
} else {
bitsMessage = bitsMessage || this.bitsMessage;
}
var bitsEnabled = this.dbg.bitsMessage & bitsMessage;
return (!!bitsMessage && bitsEnabled === bitsMessage || !!(bitsEnabled & this.dbg.bitsWarning));
}
return false;
}
/**
* printMessage(sMessage, bitsMessage, fAddress)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed regardless.
*
* @this {Component}
* @param {string} sMessage is any caller-defined message string
* @param {number|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @param {boolean} [fAddress] is true to display the current address
*/
printMessage(sMessage, bitsMessage, fAddress)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true || this.messageEnabled(bitsMessage | 0)) {
this.dbg.message(sMessage, fAddress);
}
}
}
/**
* printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed as long as MESSAGE.PORT is enabled.
*
* @this {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|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
*/
printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true) {
bitsMessage = 0;
} else if (bitsMessage == null) {
bitsMessage = this.bitsMessage;
}
this.dbg.messageIO(this, port, bOut, addrFrom, name, bIn, bitsMessage);
}
}
}
/*
* Every component created on the current page is recorded in this array (see Component.add()),
* enabling any component to locate another component by ID (see Component.getComponentByID())
* or by type (see Component.getComponentByType()).
*
* Every machine on the page are now recorded as well, by their machine ID. We then record the
* various resources used by that machine.
*
* Includes a fallback for non-browser-based environments (ie, Node). TODO: This will need to be
* tailored to Node, probably using the global object instead of the window object, if we ever want
* to support multi-machine configs in that environment.
*/
if (window) {
if (!window['PCjs']) window['PCjs'] = {};
if (!window['PCjs']['Machines']) window['PCjs']['Machines'] = {};
if (!window['PCjs']['Components']) window['PCjs']['Components'] = [];
if (!window['PCjs']['Commands']) window['PCjs']['Commands'] = {};
}
Component.machines = window? window['PCjs']['Machines'] : {};
Component.components = window? window['PCjs']['Components'] : [];
Component.commands = window? window['PCjs']['Commands'] : {};
Component.asyncCommands = [
'hold', 'sleep', 'wait'
];
Component.globalCommands = {
'alert': Component.scriptAlert,
'sleep': Component.scriptSleep
};
Component.componentCommands = {
'select': Component.scriptSelect
};
/*
* The following polyfills provide ES5 functionality that's missing in older browsers (eg, IE8),
* allowing PCjs apps to run without slamming into exceptions; however, due to the lack of HTML5 canvas
* support in those browsers, all you're likely to see are "soft" errors (eg, "Missing <canvas> support").
*
* Perhaps we can implement a text-only faux video display for a fun retro-browser experience someday.
*
* TODO: Come up with a better place to put these polyfills. We will likely have more if we decide to
* make the leap from ES5 to ES6 features.
*/
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
*/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj, start) {
for (var i = (start || 0), j = this.length; i < j; i++) {
if (this[i] === obj) { return i; }
}
return -1;
}
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
*/
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function(obj) {
if (typeof this != "function") {
// Closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind: non-callable object");
}
var args = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fnNOP = /** @constructor */ (function() {});
var fnBound = function() {
return fToBind.apply(this instanceof fnNOP && obj? this : obj, args.concat(Array.prototype.slice.call(arguments)));
};
fnNOP.prototype = this.prototype;
fnBound.prototype = new fnNOP();
return fnBound;
};
}
/**
* @copyright http://pcjs.org/modules/pc8080/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @define {string}
*/
var APPCLASS = "pc8080"; // this @define is the default application class (eg, "pcx86", "c1pjs")
/**
* @define {string}
*/
var APPNAME = "PC8080"; // this @define is the default application name (eg, "PCx86", "C1Pjs")
/**
* @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 Memory8080.afnTypedArray), so support can be enabled dynamically now.
*
* See the Memory component for details.
*/
var TYPEDARRAYS = (typeof ArrayBuffer !== 'undefined');
/*
* Combine all the shared globals and machine-specific globals into one machine-specific global object,
* which all machine components should start using; eg: "if (PC8080.DEBUG) ..." instead of "if (DEBUG) ...".
*/
var PC8080 = {
APPCLASS: APPCLASS,
APPNAME: APPNAME,
APPVERSION: APPVERSION, // shared
BYTEARRAYS: BYTEARRAYS,
COMPILED: COMPILED, // shared
CSSCLASS: CSSCLASS, // shared
DEBUG: DEBUG, // shared
DEBUGGER: DEBUGGER,
MAXDEBUG: MAXDEBUG, // shared
PRIVATE: PRIVATE, // shared
TYPEDARRAYS: TYPEDARRAYS,
SITEHOST: SITEHOST, // shared
XMLVERSION: XMLVERSION // shared
};
/**
* @copyright http://pcjs.org/modules/pc8080/lib/cpudef.js (C) Jeff Parsons 2012-2017
*/
var CPUDef8080 = {
/*
* CPU model numbers (supported)
*/
MODEL_8080: 8080,
/*
* 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: {
CF: 0x0001, // bit 0: Carry Flag
BIT1: 0x0002, // bit 1: reserved, always set
PF: 0x0004, // bit 2: Parity Flag
BIT3: 0x0008, // bit 3: reserved, always clear
AF: 0x0010, // bit 4: Auxiliary Carry Flag
BIT5: 0x0020, // bit 5: reserved, always clear
ZF: 0x0040, // bit 6: Zero Flag
SF: 0x0080, // bit 7: Sign Flag
ALL: 0x00D5, // all "arithmetic" flags (CF, PF, AF, ZF, SF)
MASK: 0x00FF, //
IF: 0x0200 // bit 9: Interrupt Flag (set if interrupts enabled; Intel calls this the INTE bit)
},
PARITY: [ // 256-byte array with a 1 wherever the number of set bits of the array index is EVEN
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1
],
/*
* Interrupt-related flags (stored in intFlags)
*/
INTFLAG: {
NONE: 0x0000,
INTR: 0x00ff, // mask for 8 bits, representing interrupt levels 0-7
HALT: 0x0100 // halt requested; see opHLT()
},
/*
* Opcode definitions
*/
OPCODE: {
HLT: 0x76, // Halt
ACI: 0xCE, // Add with Carry Immediate (affects PS.ALL)
CALL: 0xCD, // Call
RST0: 0xC7
// to be continued....
}
};
/*
* These are the internal PS bits (outside of PS.MASK) that getPS() and setPS() can get and set,
* but which cannot be seen with any of the documented instructions.
*/
CPUDef8080.PS.INTERNAL = (CPUDef8080.PS.IF);
/*
* PS "arithmetic" flags are NOT stored in regPS; they are maintained across separate result registers,
* hence the RESULT designation.
*/
CPUDef8080.PS.RESULT = (CPUDef8080.PS.CF | CPUDef8080.PS.PF | CPUDef8080.PS.AF | CPUDef8080.PS.ZF | CPUDef8080.PS.SF);
/*
* These are the "always set" PS bits for the 8080.
*/
CPUDef8080.PS.SET = (CPUDef8080.PS.BIT1);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/messages.js (C) Jeff Parsons 2012-2017
*/
var Messages8080 = {
CPU: 0x00000001,
BUS: 0x00000040,
MEM: 0x00000080,
PORT: 0x00000100,
NVR: 0x00004000,
CHIPSET: 0x00008000,
KEYBOARD: 0x00010000,
KEYS: 0x00020000,
VIDEO: 0x00040000,
FDC: 0x00080000,
DISK: 0x00200000,
SERIAL: 0x00800000,
SPEAKER: 0x02000000,
COMPUTER: 0x04000000,
LOG: 0x10000000,
WARN: 0x20000000,
BUFFER: 0x40000000,
HALT: 0x80000000|0
};
/*
* 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).
*/
Messages8080.CATEGORIES = {
"cpu": Messages8080.CPU,
"bus": Messages8080.BUS,
"mem": Messages8080.MEM,
"port": Messages8080.PORT,
"nvr": Messages8080.NVR,
"chipset": Messages8080.CHIPSET,
"keyboard": Messages8080.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages()
"key": Messages8080.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects
"video": Messages8080.VIDEO,
"fdc": Messages8080.FDC,
"disk": Messages8080.DISK,
"serial": Messages8080.SERIAL,
"speaker": Messages8080.SPEAKER,
"computer": Messages8080.COMPUTER,
"log": Messages8080.LOG,
"warn": Messages8080.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.
*
* Similarly, "m buffer on" turns on message buffering, deferring the display of all messages
* until "m buffer off" is issued.
*/
"buffer": Messages8080.BUFFER,
"halt": Messages8080.HALT
};
/**
* @copyright http://pcjs.org/modules/pc8080/lib/panel.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Panel8080 extends Component {
/**
* Panel8080(parmsPanel)
*
* The Panel8080 component has no required (parmsPanel) properties.
*
* @this {Panel8080}
* @param {Object} parmsPanel
*/
constructor(parmsPanel)
{
super("Panel", parmsPanel);
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* Most panel layouts don't have bindings of their own, so we pass along all binding requests to the
* Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any component
* that doesn't recognize the specified binding should simply ignore it.
*
* @this {Panel8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
if (this.cmp && this.cmp.setBinding(sHTMLType, sBinding, control, sValue)) return true;
if (this.cpu && this.cpu.setBinding(sHTMLType, sBinding, control, sValue)) return true;
if (this.kbd && this.kbd.setBinding(sHTMLType, sBinding, control, sValue)) return true;
if (DEBUGGER && this.dbg && this.dbg.setBinding(sHTMLType, sBinding, control, sValue)) return true;
return super.setBinding(sHTMLType, sBinding, control, sValue);
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {Panel8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard"));
}
/**
* powerUp(data, fRepower)
*
* @this {Panel8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) Panel8080.init();
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Panel8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return true;
}
/**
* updateStatus(fForce)
*
* Update function for Panels containing elements with high-frequency display requirements.
*
* For older (and slower) DOM-based display elements, those are sill being managed by the CPUState component,
* so it has its own updateStatus() handler.
*
* The Computer's updateStatus() handler is currently responsible for calling both our handler and the CPU's handler.
*
* @this {Panel8080}
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
*/
updateStatus(fForce)
{
}
/**
* Panel8080.init()
*
* This function operates on every HTML element of class "panel", extracting the
* JSON-encoded parameters for the Panel8080 constructor from the element's "data-value"
* attribute, invoking the constructor to create a Panel8080 component, and then binding
* any associated HTML controls to the new component.
*
* NOTE: Unlike most other component init() functions, this one is designed to be
* called multiple times: once at load time, so that we can bind our print()
* function to the panel's output control ASAP, and again when the Computer component
* is verifying that all components are ready and invoking their powerUp() functions.
*
* Our powerUp() method gives us a second opportunity to notify any components that
* that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they
* might want to use.
*/
static init()
{
var fReady = false;
var aePanels = Component.getElementsByClass(document, PC8080.APPCLASS, "panel");
for (var iPanel=0; iPanel < aePanels.length; iPanel++) {
var ePanel = aePanels[iPanel];
var parmsPanel = Component.getComponentParms(ePanel);
var panel = Component.getComponentByID(parmsPanel['id']);
if (!panel) {
fReady = true;
panel = new Panel8080(parmsPanel);
}
Component.bindComponentControls(panel, ePanel, PC8080.APPCLASS);
if (fReady) panel.setReady();
}
}
}
/*
* Initialize every Panel module on the page.
*/
Web.onInit(Panel8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/bus.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Bus8080 extends Component {
/**
* Bus8080(cpu, dbg)
*
* The Bus8080 component manages physical memory and I/O address spaces.
*
* The Bus8080 component has no UI elements, so it does not require an init() handler,
* but it still inherits from the Component class and must be allocated like any
* other device component. It's currently allocated by the Computer's init() handler,
* which then calls the initBus() method of all the other components.
*
* For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped
* devices), the address space must still be allocated through the Bus8080 component via
* addMemory(). If the component needs something more than simple read/write storage,
* it must provide a custom controller.
*
* All port (I/O) operations are defined by external handlers; they register with us,
* and we manage those registrations and provide support for I/O breakpoints, but the
* only default I/O behavior we provide is ignoring writes to any unregistered output
* ports and returning 0xff from any unregistered input ports.
*
* @this {Bus8080}
* @param {Object} parmsBus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
constructor(parmsBus, cpu, dbg)
{
super("Bus", parmsBus);
this.cpu = cpu;
this.dbg = dbg;
this.nBusWidth = parmsBus['busWidth'] || 16;
/*
* Compute all Bus8080 memory block parameters, based on the width of the bus. The entire
* address space is divided into blocks, using a block size that is (hopefully) appropriate to
* the bus width. The following table summarizes our simplistic calculations.
*
* Bus Width Block Shift Block Size
* --------- ----------- ----------
* 16 bits (64Kb address space): 10 1Kb (64 maximum blocks)
* 18 bits (256Kb address space): 11 2Kb (128 maximum blocks)
* 20 bits (1Mb address space): 12 4Kb (256 maximum blocks)
* 22 bits (4Mb address space): 13 8Kb (512 maximum blocks)
* 24 bits (16Mb address space): 14 16Kb (1K maximum blocks)
* 32 bits (4Gb address space); 15 32Kb (128K maximum blocks)
*
* The coarser block granularities (ie, 16Kb and 32Kb) may cause problems for certain RAM and/or ROM
* allocations that are contiguous but are allocated out of order, or that have different controller
* requirements. Your choices, for the moment, are either to ensure the allocations are performed in
* order, or to choose smaller nBlockShift values (at the expense of a generating a larger block array).
*/
this.addrTotal = Math.pow(2, this.nBusWidth);
this.nBusLimit = this.nBusMask = (this.addrTotal - 1) | 0;
this.nBlockShift = (this.nBusWidth >> 1) + 2;
if (this.nBlockShift < 10) this.nBlockShift = 10;
if (this.nBlockShift > 15) this.nBlockShift = 15;
this.nBlockSize = 1 << this.nBlockShift;
this.nBlockLen = this.nBlockSize >> 2;
this.nBlockLimit = this.nBlockSize - 1;
this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0;
this.nBlockMask = this.nBlockTotal - 1;
/*
* Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by
* port, of sub-arrays which contain:
*
* [0]: registered function to call for every I/O access
*
* The registered function is called with the port address, and if the access was triggered by the CPU,
* the instruction pointer (IP) at the point of access.
*
* WARNING: Unlike the (old) read and write memory notification functions, these support only one
* pair of input/output functions per port. A more sophisticated architecture could support a list
* of chained functions across multiple components, but I doubt that will be necessary here.
*
* UPDATE: The Debugger now piggy-backs on these arrays to indicate ports for which it wants notification
* of I/O. In those cases, the registered component/function elements may or may not be set, but the
* following additional element will be set:
*
* [1]: true to break on I/O, false to ignore I/O
*
* The false case is important if fPortInputBreakAll and/or fPortOutputBreakAll is set, because it allows the
* Debugger to selectively ignore specific ports.
*/
this.aPortInputNotify = [];
this.aPortOutputNotify = [];
this.fPortInputBreakAll = this.fPortOutputBreakAll = false;
/*
* By default, all I/O ports are 1 byte wide; ports that are wider must add themselves to one or both of
* these lists, using addPortInputWidth() and/or addPortOutputWidth().
*/
this.aPortInputWidth = [];
this.aPortOutputWidth = [];
/*
* Allocate empty Memory blocks to span the entire physical address space.
*/
this.initMemory();
this.setReady();
}
/**
* initMemory()
*
* Allocate enough (empty) Memory blocks to span the entire physical address space.
*
* @this {Bus8080}
*/
initMemory()
{
var block = new Memory8080();
block.copyBreakpoints(this.dbg);
this.aMemBlocks = new Array(this.nBlockTotal);
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
this.aMemBlocks[iBlock] = block;
}
}
/**
* reset()
*
* @this {Bus8080}
*/
reset()
{
}
/**
* powerUp(data, fRepower)
*
* We don't need a powerDown() handler, because for largely historical reasons, our state is saved by saveMemory(),
* which called by the CPU.
*
* However, we do need a powerUp() handler, because on resumable machines, the Computer's onReset() function calls
* everyone's powerUp() handler rather than their reset() handler.
*
* TODO: Perhaps Computer should be smarter: if there's no powerUp() handler, then fallback to the reset() handler.
* In that case, however, we'd either need to remove the powerUp() stub in Component, or detect the existence of the stub.
*
* @this {Bus8080}
* @param {Object|null} data (always null because we supply no powerDown() handler)
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) this.reset();
return true;
}
/**
* addMemory(addr, size, type)
*
* Adds new Memory blocks to the specified address range. Any Memory blocks previously
* added to that range must first be removed via removeMemory(); otherwise, you'll get
* an allocation conflict error. This helps prevent address calculation errors, redundant
* allocations, etc.
*
* We've relaxed some of the original requirements (ie, that addresses must start at a
* block-granular address, or that sizes must be equal to exactly one or more blocks),
* because machines with large block sizes can make it impossible to load certain ROMs at
* their required addresses. Every allocation still allocates a whole number of blocks.
*
* Even so, Bus8080 memory management does NOT provide a general-purpose heap. Most memory
* allocations occur during machine initialization and never change. In particular, there
* is NO support for removing partial-block allocations.
*
* Each Memory block keeps track of a start address (addr) and length (used), indicating
* the used space within the block; any free space that precedes or follows that used space
* can be allocated later, by simply extending the beginning or ending of the previously used
* space. However, any holes that might have existed between the original allocation and an
* extension are subsumed by the extension.
*
* @this {Bus8080}
* @param {number} addr is the starting physical address of the request
* @param {number} size of the request, in bytes
* @param {number} type is one of the Memory8080.TYPE constants
* @return {boolean} true if successful, false if not
*/
addMemory(addr, size, type)
{
var addrNext = addr;
var sizeLeft = size;
var iBlock = addrNext >>> this.nBlockShift;
while (sizeLeft > 0 && iBlock < this.aMemBlocks.length) {
var block = this.aMemBlocks[iBlock];
var addrBlock = iBlock * this.nBlockSize;
var sizeBlock = this.nBlockSize - (addrNext - addrBlock);
if (sizeBlock > sizeLeft) sizeBlock = sizeLeft;
if (block && block.size) {
if (block.type == type) {
/*
* Where there is already a similar block with a non-zero size, we allow the allocation only if:
*
* 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or
* 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block)
*/
if (addrNext + sizeLeft <= block.addr) {
block.used += (block.addr - addrNext);
block.addr = addrNext;
return true;
}
if (addrNext >= block.addr + block.used) {
var sizeAvail = block.size - (addrNext - addrBlock);
if (sizeAvail > sizeLeft) sizeAvail = sizeLeft;
block.used = addrNext - block.addr + sizeAvail;
addrNext = addrBlock + this.nBlockSize;
sizeLeft -= sizeAvail;
iBlock++;
continue;
}
}
return this.reportError(Bus8080.ERROR.ADD_MEM_INUSE, addrNext, sizeLeft);
}
var blockNew = new Memory8080(addrNext, sizeBlock, this.nBlockSize, type);
blockNew.copyBreakpoints(this.dbg, block);
this.aMemBlocks[iBlock++] = blockNew;
addrNext = addrBlock + this.nBlockSize;
sizeLeft -= sizeBlock;
}
if (sizeLeft <= 0) {
this.status(Math.floor(size / 1024) + "Kb " + Memory8080.TYPE.NAMES[type] + " at " + Str.toHexWord(addr));
return true;
}
return this.reportError(Bus8080.ERROR.ADD_MEM_BADRANGE, addr, size);
}
/**
* cleanMemory(addr, size)
*
* @this {Bus8080}
* @param {number} addr
* @param {number} size
* @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process
*/
cleanMemory(addr, size)
{
var fClean = true;
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aMemBlocks.length) {
if (this.aMemBlocks[iBlock].fDirty) {
this.aMemBlocks[iBlock].fDirty = fClean = false;
this.aMemBlocks[iBlock].fDirtyEver = true;
}
size -= this.nBlockSize;
iBlock++;
}
return fClean;
}
/**
* scanMemory(info, addr, size)
*
* Returns a BusInfo8080 object for the specified address range.
*
* @this {Bus8080}
* @param {Object} [info] previous BusInfo8080, if any
* @param {number} [addr] starting address of range (0 if none provided)
* @param {number} [size] size of range, in bytes (up to end of address space if none provided)
* @return {Object} updated info (or new info if no previous info provided)
*/
scanMemory(info, addr, size)
{
if (addr == null) addr = 0;
if (size == null) size = (this.addrTotal - addr) | 0;
if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []};
var iBlock = addr >>> this.nBlockShift;
var iBlockMax = ((addr + size - 1) >>> this.nBlockShift);
info.cbTotal = 0;
info.cBlocks = 0;
while (iBlock <= iBlockMax) {
var block = this.aMemBlocks[iBlock];
info.cbTotal += block.size;
if (block.size) {
info.aBlocks.push(Usr.initBitFields(Bus8080.BlockInfo, iBlock, 0, 0, block.type));
info.cBlocks++
}
iBlock++;
}
return info;
}
/**
* getWidth()
*
* @this {Bus8080}
* @return {number}
*/
getWidth()
{
return this.nBusWidth;
}
/**
* removeMemory(addr, size)
*
* Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes.
*
* TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface.
*
* @this {Bus8080}
* @param {number} addr
* @param {number} size
* @return {boolean} true if successful, false if not
*/
removeMemory(addr, size)
{
if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) {
var iBlock = addr >>> this.nBlockShift;
while (size > 0) {
var blockOld = this.aMemBlocks[iBlock];
var blockNew = new Memory8080(addr);
blockNew.copyBreakpoints(this.dbg, blockOld);
this.aMemBlocks[iBlock++] = blockNew;
addr = iBlock * this.nBlockSize;
size -= this.nBlockSize;
}
return true;
}
return this.reportError(Bus8080.ERROR.REM_MEM_BADRANGE, addr, size);
}
/**
* getMemoryBlocks(addr, size)
*
* @this {Bus8080}
* @param {number} addr is the starting physical address
* @param {number} size of the request, in bytes
* @return {Array} of Memory blocks
*/
getMemoryBlocks(addr, size)
{
var aBlocks = [];
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aMemBlocks.length) {
aBlocks.push(this.aMemBlocks[iBlock++]);
size -= this.nBlockSize;
}
return aBlocks;
}
/**
* setMemoryBlocks(addr, size, aBlocks, type)
*
* If no type is specified, then specified address range uses all the provided blocks as-is;
* this form of setMemoryBlocks() is used for complete physical aliases.
*
* Otherwise, new blocks are allocated with the specified type; the underlying memory from the
* provided blocks is still used, but the new blocks may have different access to that memory.
*
* @this {Bus8080}
* @param {number} addr is the starting physical address
* @param {number} size of the request, in bytes
* @param {Array} aBlocks as returned by getMemoryBlocks()
* @param {number} [type] is one of the Memory8080.TYPE constants
*/
setMemoryBlocks(addr, size, aBlocks, type)
{
var i = 0;
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aMemBlocks.length) {
var block = aBlocks[i++];
if (!block) break;
if (type !== undefined) {
var blockNew = new Memory8080(addr);
blockNew.clone(block, type, this.dbg);
block = blockNew;
}
this.aMemBlocks[iBlock++] = block;
size -= this.nBlockSize;
}
}
/**
* getByte(addr)
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @return {number} byte (8-bit) value at that address
*/
getByte(addr)
{
return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByte(addr & this.nBlockLimit, addr);
}
/**
* getByteDirect(addr)
*
* This is useful for the Debugger and other components that want to bypass getByte() breakpoint detection.
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @return {number} byte (8-bit) value at that address
*/
getByteDirect(addr)
{
return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByteDirect(addr & this.nBlockLimit, addr);
}
/**
* getShort(addr)
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @return {number} word (16-bit) value at that address
*/
getShort(addr)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
if (off != this.nBlockLimit) {
return this.aMemBlocks[iBlock].readShort(off, addr);
}
return this.aMemBlocks[iBlock++].readByte(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByte(0, addr + 1) << 8);
}
/**
* getShortDirect(addr)
*
* This is useful for the Debugger and other components that want to bypass getShort() breakpoint detection.
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @return {number} word (16-bit) value at that address
*/
getShortDirect(addr)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
if (off != this.nBlockLimit) {
return this.aMemBlocks[iBlock].readShortDirect(off, addr);
}
return this.aMemBlocks[iBlock++].readByteDirect(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByteDirect(0, addr + 1) << 8);
}
/**
* setByte(addr, b)
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe)
*/
setByte(addr, b)
{
this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByte(addr & this.nBlockLimit, b & 0xff, addr);
}
/**
* setByteDirect(addr, b)
*
* This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only
* memory protection (for example, this is an interface the ROM component could use to initialize ROM contents).
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe)
*/
setByteDirect(addr, b)
{
this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr);
}
/**
* setShort(addr, w)
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe)
*/
setShort(addr, w)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
if (off != this.nBlockLimit) {
this.aMemBlocks[iBlock].writeShort(off, w & 0xffff, addr);
return;
}
this.aMemBlocks[iBlock++].writeByte(off, w & 0xff, addr);
this.aMemBlocks[iBlock & this.nBlockMask].writeByte(0, (w >> 8) & 0xff, addr + 1);
}
/**
* setShortDirect(addr, w)
*
* This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only
* memory protection (for example, this is an interface the ROM component could use to initialize ROM contents).
*
* @this {Bus8080}
* @param {number} addr is a physical address
* @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe)
*/
setShortDirect(addr, w)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
if (off != this.nBlockLimit) {
this.aMemBlocks[iBlock].writeShortDirect(off, w & 0xffff, addr);
return;
}
this.aMemBlocks[iBlock++].writeByteDirect(off, w & 0xff, addr);
this.aMemBlocks[iBlock & this.nBlockMask].writeByteDirect(0, (w >> 8) & 0xff, addr + 1);
}
/**
* addMemBreak(addr, fWrite)
*
* @this {Bus8080}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
addMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var iBlock = addr >>> this.nBlockShift;
this.aMemBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite);
}
}
/**
* removeMemBreak(addr, fWrite)
*
* @this {Bus8080}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
removeMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var iBlock = addr >>> this.nBlockShift;
this.aMemBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite);
}
}
/**
* saveMemory(fAll)
*
* The only memory blocks we save are those marked as dirty, but most likely all of RAM will have been marked dirty,
* and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed
* what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least
* all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them).
*
* All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so:
*
* [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...]
*
* In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit
* integer (because they are formed using bitwise operator rather than floating-point math operators), so don't be
* surprised to see negative numbers in the data.
*
* The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays
* will look like:
*
* [count0, dw0, count1, dw1, ...]
*
* where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates
* that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State
* helper methods compress() and decompress() to create and expand the compressed data arrays.
*
* @this {Bus8080}
* @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags)
* @return {Array} a
*/
saveMemory(fAll)
{
var i = 0;
var a = [];
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
var block = this.aMemBlocks[iBlock];
/*
* We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of
* the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag,
* it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine.
*/
if (fAll && block.type != Memory8080.TYPE.ROM || block.fDirty || block.fDirtyEver) {
a[i++] = iBlock;
a[i++] = State.compress(block.save());
}
}
return a;
}
/**
* restoreMemory(a)
*
* This restores the contents of all Memory blocks; called by CPUState.restore().
*
* In theory, we ONLY have to save/restore block contents. Other block attributes,
* like the type, the memory controller (if any), and the active memory access functions,
* should already be restored, since every component (re)allocates all the memory blocks
* it was using when it's restored. And since the CPU is guaranteed to be the last
* component to be restored, all those blocks (and their attributes) should be in place now.
*
* See saveMemory() for more information on how the memory block contents are saved.
*
* @this {Bus8080}
* @param {Array} a
* @return {boolean} true if successful, false if not
*/
restoreMemory(a)
{
var i;
for (i = 0; i < a.length - 1; i += 2) {
var iBlock = a[i];
var adw = a[i+1];
if (adw && adw.length < this.nBlockLen) {
adw = State.decompress(adw, this.nBlockLen);
}
var block = this.aMemBlocks[iBlock];
if (!block || !block.restore(adw)) {
/*
* Either the block to restore hasn't been allocated, indicating a change in the machine
* configuration since it was last saved (the most likely explanation) or there's some internal
* inconsistency (eg, the block size is wrong).
*/
Component.error("Unable to restore memory block " + iBlock);
return false;
}
}
return true;
}
/**
* addPortInputBreak(port)
*
* @this {Bus8080}
* @param {number} [port]
* @return {boolean} true if break on port input enabled, false if disabled
*/
addPortInputBreak(port)
{
if (port === undefined) {
this.fPortInputBreakAll = !this.fPortInputBreakAll;
return this.fPortInputBreakAll;
}
if (this.aPortInputNotify[port] === undefined) {
this.aPortInputNotify[port] = [null, false];
}
this.aPortInputNotify[port][1] = !this.aPortInputNotify[port][1];
return this.aPortInputNotify[port][1];
}
/**
* addPortInputNotify(start, end, fn)
*
* Add a port input-notification handler to the list of such handlers.
*
* @this {Bus8080}
* @param {number} start port address
* @param {number} end port address
* @param {function(number,number)} fn is called with the port and IP values at the time of the input
*/
addPortInputNotify(start, end, fn)
{
if (fn !== undefined) {
for (var port = start; port <= end; port++) {
if (this.aPortInputNotify[port] !== undefined) {
Component.warning("Input port " + Str.toHexWord(port) + " already registered");
continue;
}
this.aPortInputNotify[port] = [fn, false];
if (MAXDEBUG) this.log("addPortInputNotify(" + Str.toHexWord(port) + ")");
}
}
}
/**
* addPortInputTable(component, table, offset)
*
* Add port input-notification handlers from the specified table (a batch version of addPortInputNotify)
*
* @this {Bus8080}
* @param {Component} component
* @param {Object} table
* @param {number} [offset] is an optional port offset
*/
addPortInputTable(component, table, offset)
{
if (offset === undefined) offset = 0;
if (table) {
for (var port in table) {
this.addPortInputNotify(+port + offset, +port + offset, table[port].bind(component));
}
}
}
/**
* addPortInputWidth(port, size)
*
* By default, all input ports are 1 byte wide; ports that are wider must call this function.
*
* @this {Bus8080}
* @param {number} port
* @param {number} size (1, 2 or 4)
*/
addPortInputWidth(port, size)
{
this.aPortInputWidth[port] = size;
}
/**
* checkPortInputNotify(port, size, addrIP)
*
* @this {Bus8080}
* @param {number} port
* @param {number} size (1, 2 or 4)
* @param {number} [addrIP] is the IP value at the time of the input
* @return {number} simulated port data
*
* NOTE: It seems that parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS)
* assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below.
*/
checkPortInputNotify(port, size, addrIP)
{
var data = 0, shift = 0;
while (size > 0) {
var aNotify = this.aPortInputNotify[port];
var sizePort = this.aPortInputWidth[port] || 1;
var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1));
var dataPort = maskPort;
/*
* TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O
* to a 32-bit port). We probably should pass the size through to the aNotify[0] handler,
* and let it decide what to do, but I don't feel like changing all the I/O handlers right now.
* The good news, at least, is that the 8-bit handlers would not have to do anything special.
* This assert will warn us if this is a pressing need.
*/
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;
}
return data;
}
/**
* addPortOutputBreak(port)
*
* @this {Bus8080}
* @param {number} [port]
* @return {boolean} true if break on port output enabled, false if disabled
*/
addPortOutputBreak(port)
{
if (port === undefined) {
this.fPortOutputBreakAll = !this.fPortOutputBreakAll;
return this.fPortOutputBreakAll;
}
if (this.aPortOutputNotify[port] === undefined) {
this.aPortOutputNotify[port] = [null, false];
}
this.aPortOutputNotify[port][1] = !this.aPortOutputNotify[port][1];
return this.aPortOutputNotify[port][1];
}
/**
* addPortOutputNotify(start, end, fn)
*
* Add a port output-notification handler to the list of such handlers.
*
* @this {Bus8080}
* @param {number} start port address
* @param {number} end port address
* @param {function(number,number)} fn is called with the port and IP values at the time of the output
*/
addPortOutputNotify(start, end, fn)
{
if (fn !== undefined) {
for (var port = start; port <= end; port++) {
if (this.aPortOutputNotify[port] !== undefined) {
Component.warning("Output port " + Str.toHexWord(port) + " already registered");
continue;
}
this.aPortOutputNotify[port] = [fn, false];
if (MAXDEBUG) this.log("addPortOutputNotify(" + Str.toHexWord(port) + ")");
}
}
}
/**
* addPortOutputTable(component, table, offset)
*
* Add port output-notification handlers from the specified table (a batch version of addPortOutputNotify)
*
* @this {Bus8080}
* @param {Component} component
* @param {Object} table
* @param {number} [offset] is an optional port offset
*/
addPortOutputTable(component, table, offset)
{
if (offset === undefined) offset = 0;
if (table) {
for (var port in table) {
this.addPortOutputNotify(+port + offset, +port + offset, table[port].bind(component));
}
}
}
/**
* addPortOutputWidth(port, size)
*
* By default, all output ports are 1 byte wide; ports that are wider must call this function.
*
* @this {Bus8080}
* @param {number} port
* @param {number} size (1, 2 or 4)
*/
addPortOutputWidth(port, size)
{
this.aPortOutputWidth[port] = size;
}
/**
* checkPortOutputNotify(port, size, data, addrIP)
*
* @this {Bus8080}
* @param {number} port
* @param {number} size
* @param {number} data
* @param {number} [addrIP] is the IP value at the time of the output
*/
checkPortOutputNotify(port, size, data, addrIP)
{
var shift = 0;
while (size > 0) {
var aNotify = this.aPortOutputNotify[port];
var sizePort = this.aPortOutputWidth[port] || 1;
var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1));
var dataPort = (data >>>= shift) & maskPort;
/*
* TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O
* to a 32-bit port). We probably should pass the size through to the aNotify[0] handler,
* and let it decide what to do, but I don't feel like changing all the I/O handlers right now.
* The good news, at least, is that the 8-bit handlers would not have to do anything special.
* This assert will warn us if this is a pressing need.
*/
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;
}
}
/**
* removePortInputNotify(start, end)
*
* Remove port input-notification handler(s) (to be ENABLED later if needed)
*
* @this {Bus8080}
* @param {number} start address
* @param {number} end address
*
removePortInputNotify(start, end)
{
for (var port = start; port < end; port++) {
if (this.aPortInputNotify[port]) {
delete this.aPortInputNotify[port];
}
}
}
*/
/**
* removePortOutputNotify(start, end)
*
* Remove port output-notification handler(s) (to be ENABLED later if needed)
*
* @this {Bus8080}
* @param {number} start address
* @param {number} end address
*
removePortOutputNotify(start, end)
{
for (var port = start; port < end; port++) {
if (this.aPortOutputNotify[port]) {
delete this.aPortOutputNotify[port];
}
}
}
*/
/**
* reportError(op, addr, size, fQuiet)
*
* @this {Bus8080}
* @param {number} op
* @param {number} addr
* @param {number} size
* @param {boolean} [fQuiet] (true if any error should be quietly logged)
* @return {boolean} false
*/
reportError(op, addr, size, fQuiet)
{
var sError = "Memory block error (" + op + ": " + Str.toHex(addr) + "," + Str.toHex(size) + ")";
if (fQuiet) {
if (this.dbg) {
this.dbg.message(sError);
} else {
this.log(sError);
}
} else {
Component.error(sError);
}
return false;
}
}
Bus8080.ERROR = {
ADD_MEM_INUSE: 1,
ADD_MEM_BADRANGE: 2,
SET_MEM_BADRANGE: 4,
REM_MEM_BADRANGE: 5
};
/*
* Data types used by scanMemory()
*/
/**
* @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
* }}
*/
Bus8080.BlockInfo = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
/**
* BusInfo8080 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.<BlockInfo>
* }}
*/
var BusInfo8080;
/**
* @copyright http://pcjs.org/modules/pc8080/lib/memory.js (C) Jeff Parsons 2012-2017
*/
/**
* @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);
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Memory8080 {
/**
* Memory8080(addr, used, size, type)
*
* The Bus component allocates Memory8080 objects so that each has a memory buffer with a
* block-granular starting address and an address range equal to bus.nBlockSize; however,
* the size of any given Memory8080 object's underlying buffer can be either zero or bus.nBlockSize;
* memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone.
*
* The Bus allocates empty blocks for the entire address space during initialization, so that
* any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM
* components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate
* as many new blockSize Memory8080 objects as the ranges require. Partial Memory8080 blocks could
* also be supported in theory, but in practice, they're not.
*
* Because Memory8080 blocks now allow us to have a "sparse" address space, we could choose to
* take the memory hit of allocating 4K arrays per block, where each element stores only one byte,
* instead of the more frugal but slightly slower approach of allocating arrays of 32-bit dwords
* (LONGARRAYS) and shifting/masking bytes/words to/from dwords; in theory, byte accesses would
* be faster and word accesses somewhat less faster.
*
* However, preliminary testing of that feature (BYTEARRAYS) did not yield significantly faster
* performance, so it is OFF by default to minimize our memory consumption. Using TYPEDARRAYS
* would seem best, but as discussed in defines.js, it's off by default, because it doesn't perform
* as well as LONGARRAYS; the other advantage of TYPEDARRAYS is that it should theoretically use
* about 1/2 the memory of LONGARRAYS (32-bit elements vs 64-bit numbers), but I value speed over
* size at this point. Also, not all JavaScript implementations support TYPEDARRAYS (IE9 is probably
* the only real outlier: it lacks typed arrays but otherwise has all the necessary HTML5 support).
*
* WARNING: Since Memory8080 blocks are low-level objects that have no UI requirements, they
* do not inherit from the Component class, so if you want to use any Component class methods,
* such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger
* is available).
*
* @this {Memory8080}
* @param {number|null} [addr] of lowest used address in block
* @param {number} [used] portion of block in bytes (0 for none); must be a multiple of 4
* @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4
* @param {number} [type] is one of the Memory8080.TYPE constants (default is Memory8080.TYPE.NONE)
*/
constructor(addr, used, size, type)
{
var i;
this.id = (Memory8080.idBlock += 2);
this.adw = null;
this.offset = 0;
this.addr = addr;
this.used = used;
this.size = size || 0;
this.type = type || Memory8080.TYPE.NONE;
this.fReadOnly = (type == Memory8080.TYPE.ROM);
this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize
/*
* TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory()
* to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer.
* But the benefit to saveMemory() is minimal, and the Video component has other options; for example, it now
* uses a custom memory controller for all EGA/VGA video modes, which performs its own dirty block tracking,
* and that could easily be extended to the older MDA/CGA video modes, which still use conventional memory blocks.
* Alternatively, we could restrict the use of dirty block tracking to certain memory types (eg, VIDEO memory).
*
* However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance,
* so I think the overhead of our block-based architecture is swamping the impact of these micro-updates.
*/
this.fDirty = this.fDirtyEver = false;
/*
* For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers.
*/
if (!size) {
this.setAccess();
return;
}
/*
* This is the normal case: allocate a buffer that provides 8 bits of data per address;
* no controller is required because our default memory access functions (see afnMemory)
* know how to deal with this simple 1-1 mapping of addresses to bytes and words.
*
* TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG
* mode; pseudo-random might be best, to help make any bugs reproducible.
*/
if (TYPEDARRAYS) {
this.buffer = new ArrayBuffer(size);
this.dv = new DataView(this.buffer, 0, size);
/*
* If littleEndian is true, we can use ab[], aw[] and adw[] directly; well, we can use them
* whenever the offset is a multiple of 1, 2 or 4, respectively. Otherwise, we must fallback to
* dv.getUint8()/dv.setUint8(), dv.getUint16()/dv.setUint16() and dv.getInt32()/dv.setInt32().
*/
this.ab = new Uint8Array(this.buffer, 0, size);
this.aw = new Uint16Array(this.buffer, 0, size >> 1);
this.adw = new Int32Array(this.buffer, 0, size >> 2);
this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE);
} else {
if (BYTEARRAYS) {
this.ab = new Array(size);
} else {
/*
* NOTE: This is the default mode of operation (!TYPEDARRAYS && !BYTEARRAYS), because it
* seems to provide the best performance; and although in theory, that performance might
* come at twice the overhead of TYPEDARRAYS, it's increasingly likely that the JavaScript
* runtime will notice that all we ever store are 32-bit values, and optimize accordingly.
*/
this.adw = new Array(size >> 2);
for (i = 0; i < this.adw.length; i++) this.adw[i] = 0;
}
this.setAccess(Memory8080.afnMemory);
}
}
/**
* init(addr)
*
* Quick reinitializer when reusing a Memory8080 block.
*
* @this {Memory8080}
* @param {number} addr
*/
init(addr)
{
this.addr = addr;
}
/**
* clone(mem, type)
*
* Converts the current Memory8080 block (this) into a clone of the given Memory8080 block (mem),
* and optionally overrides the current block's type with the specified type.
*
* @this {Memory8080}
* @param {Memory8080} mem
* @param {number} [type]
* @param {Debugger8080} [dbg]
*/
clone(mem, type, dbg)
{
/*
* Original memory block IDs are even; cloned memory block IDs are odd;
* the original ID of the current block is lost, but that's OK, since it was presumably
* produced merely to become a clone.
*/
this.id = mem.id | 0x1;
this.used = mem.used;
this.size = mem.size;
if (type) {
this.type = type;
this.fReadOnly = (type == Memory8080.TYPE.ROM);
}
if (TYPEDARRAYS) {
this.buffer = mem.buffer;
this.dv = mem.dv;
this.ab = mem.ab;
this.aw = mem.aw;
this.adw = mem.adw;
this.setAccess(littleEndian? Memory8080.afnArrayLE : Memory8080.afnArrayBE);
} else {
if (BYTEARRAYS) {
this.ab = mem.ab;
} else {
this.adw = mem.adw;
}
this.setAccess(Memory8080.afnMemory);
}
this.copyBreakpoints(dbg, mem);
}
/**
* save()
*
* This gets the contents of a Memory8080 block as an array of 32-bit values; used by Bus8080.saveMemory(),
* which in turn is called by CPUState.save().
*
* Memory8080 blocks with custom memory controllers do NOT save their contents; that's the responsibility
* of the controller component.
*
* @this {Memory8080}
* @return {Array|Int32Array|null}
*/
save()
{
var adw, i;
if (BYTEARRAYS) {
adw = new Array(this.size >> 2);
var off = 0;
for (i = 0; i < adw.length; i++) {
adw[i] = this.ab[off] | (this.ab[off + 1] << 8) | (this.ab[off + 2] << 16) | (this.ab[off + 3] << 24);
off += 4;
}
}
else if (TYPEDARRAYS) {
/*
* It might be tempting to just return a copy of Int32Array(this.buffer, 0, this.size >> 2),
* but we can't be sure of the "endianness" of an Int32Array -- which would be OK if the array
* was always saved/restored on the same machine, but there's no guarantee of that, either.
* So we use getInt32() and require little-endian values.
*
* Moreover, an Int32Array isn't treated by JSON.stringify() and JSON.parse() exactly like
* a normal array; it's serialized as an Object rather than an Array, so it lacks a "length"
* property and causes problems for State.store() and State.parse().
*/
adw = new Array(this.size >> 2);
for (i = 0; i < adw.length; i++) {
adw[i] = this.dv.getInt32(i << 2, true);
}
}
else {
adw = this.adw;
}
return adw;
}
/**
* restore(adw)
*
* This restores the contents of a Memory8080 block from an array of 32-bit values;
* used by Bus8080.restoreMemory(), which is called by CPUState.restore(), after all other
* components have been restored and thus all Memory8080 blocks have been allocated
* by their respective components.
*
* @this {Memory8080}
* @param {Array|null} adw
* @return {boolean} true if successful, false if block size mismatch
*/
restore(adw)
{
/*
* At this point, it's a consistency error for adw to be null; it's happened once already,
* when there was a restore bug in the Video component that added the frame buffer at the video
* card's "spec'ed" address instead of the programmed address, so there were no controller-owned
* memory blocks installed at the programmed address, and so we arrived here at a block with
* no controller AND no data.
*/
if (adw && this.size == adw.length << 2) {
var i;
if (BYTEARRAYS) {
var off = 0;
for (i = 0; i < adw.length; i++) {
this.ab[off] = adw[i] & 0xff;
this.ab[off + 1] = (adw[i] >> 8) & 0xff;
this.ab[off + 2] = (adw[i] >> 16) & 0xff;
this.ab[off + 3] = (adw[i] >> 24) & 0xff;
off += 4;
}
} else if (TYPEDARRAYS) {
for (i = 0; i < adw.length; i++) {
this.dv.setInt32(i << 2, adw[i], true);
}
} else {
this.adw = adw;
}
this.fDirty = true;
return true;
}
return false;
}
/**
* setAccess(afn, fDirect)
*
* If no function table is specified, a default is selected based on the Memory8080 type.
*
* @this {Memory8080}
* @param {Array.<function()>} [afn] function table
* @param {boolean} [fDirect] (true to update direct access functions as well; default is true)
*/
setAccess(afn, fDirect)
{
if (!afn) {
afn = Memory8080.afnNone;
}
this.setReadAccess(afn, fDirect);
this.setWriteAccess(afn, fDirect);
}
/**
* setReadAccess(afn, fDirect)
*
* @this {Memory8080}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setReadAccess(afn, fDirect)
{
if (!fDirect || !this.cReadBreakpoints) {
this.readByte = afn[0] || this.readNone;
this.readShort = afn[2] || this.readShortDefault;
}
if (fDirect || fDirect === undefined) {
this.readByteDirect = afn[0] || this.readNone;
this.readShortDirect = afn[2] || this.readShortDefault;
}
}
/**
* setWriteAccess(afn, fDirect)
*
* @this {Memory8080}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setWriteAccess(afn, fDirect)
{
if (!fDirect || !this.cWriteBreakpoints) {
this.writeByte = !this.fReadOnly && afn[1] || this.writeNone;
this.writeShort = !this.fReadOnly && afn[3] || this.writeShortDefault;
}
if (fDirect || fDirect === undefined) {
this.writeByteDirect = afn[1] || this.writeNone;
this.writeShortDirect = afn[3] || this.writeShortDefault;
}
}
/**
* resetReadAccess()
*
* @this {Memory8080}
*/
resetReadAccess()
{
this.readByte = this.readByteDirect;
this.readShort = this.readShortDirect;
}
/**
* resetWriteAccess()
*
* @this {Memory8080}
*/
resetWriteAccess()
{
this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect;
this.writeShort = this.fReadOnly? this.writeShortDefault : this.writeShortDirect;
}
/**
* printAddr(sMessage)
*
* @this {Memory8080}
* @param {string} sMessage
*/
printAddr(sMessage)
{
if (DEBUG && this.dbg && this.dbg.messageEnabled(Messages8080.MEM)) {
this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('%' + Str.toHex(this.addr)) : '#' + this.id), true);
}
}
/**
* addBreakpoint(off, fWrite)
*
* @this {Memory8080}
* @param {number} off
* @param {boolean} fWrite
*/
addBreakpoint(off, fWrite)
{
if (!fWrite) {
if (this.cReadBreakpoints++ === 0) {
this.setReadAccess(Memory8080.afnChecked, false);
}
if (DEBUG) this.printAddr("read breakpoint added to memory block");
}
else {
if (this.cWriteBreakpoints++ === 0) {
this.setWriteAccess(Memory8080.afnChecked, false);
}
if (DEBUG) this.printAddr("write breakpoint added to memory block");
}
}
/**
* removeBreakpoint(off, fWrite)
*
* @this {Memory8080}
* @param {number} off
* @param {boolean} fWrite
*/
removeBreakpoint(off, fWrite)
{
if (!fWrite) {
if (--this.cReadBreakpoints === 0) {
this.resetReadAccess();
if (DEBUG) this.printAddr("all read breakpoints removed from memory block");
}
}
else {
if (--this.cWriteBreakpoints === 0) {
this.resetWriteAccess();
if (DEBUG) this.printAddr("all write breakpoints removed from memory block");
}
}
}
/**
* copyBreakpoints(dbg, mem)
*
* @this {Memory8080}
* @param {Debugger8080} [dbg]
* @param {Memory8080} [mem] (outgoing Memory8080 block to copy breakpoints from, if any)
*/
copyBreakpoints(dbg, mem)
{
this.dbg = dbg;
this.cReadBreakpoints = this.cWriteBreakpoints = 0;
if (mem) {
if ((this.cReadBreakpoints = mem.cReadBreakpoints)) {
this.setReadAccess(Memory8080.afnChecked, false);
}
if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) {
this.setWriteAccess(Memory8080.afnChecked, false);
}
}
}
/**
* readNone(off)
*
* Previously, this always returned 0x00, but the initial memory probe by the COMPAQ DeskPro 386 ROM BIOS
* writes 0x0000 to the first word of every 64Kb block in the nearly 16Mb address space it supports, and
* if it reads back 0x0000, it will initially think that LOTS of RAM exists, only to be disappointed later
* when it performs a more exhaustive memory test, generating unwanted error messages in the process.
*
* TODO: Determine if we should have separate readByteNone(), readShortNone() and readLongNone() functions
* to return 0xff, 0xffff and 0xffffffff|0, respectively. This seems sufficient for now, as it seems unlikely
* that a system would require nonexistent memory locations to return ALL bits set.
*
* Also, I'm reluctant to address that potential issue by simply returning -1, because to date, the above
* Memory8080 interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively.
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readNone(off, addr)
{
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) {
this.dbg.message("attempt to read invalid block %" + Str.toHex(this.addr), true);
}
return 0xff;
}
/**
* writeNone(off, v, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} v (could be either a byte or word value, since we use the same handler for both kinds of accesses)
* @param {number} addr
*/
writeNone(off, v, addr)
{
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages8080.CPU | Messages8080.MEM) /* && !off */) {
this.dbg.message("attempt to write " + Str.toHexWord(v) + " to invalid block %" + Str.toHex(this.addr), true);
}
}
/**
* readShortDefault(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readShortDefault(off, addr)
{
return this.readByte(off++, addr++) | (this.readByte(off, addr) << 8);
}
/**
* writeShortDefault(off, w, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeShortDefault(off, w, addr)
{
this.writeByte(off++, w & 0xff, addr++);
this.writeByte(off, w >> 8, addr);
}
/**
* readByteMemory(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteMemory(off, addr)
{
if (BYTEARRAYS) {
return this.ab[off];
}
return ((this.adw[off >> 2] >>> ((off & 0x3) << 3)) & 0xff);
}
/**
* readShortMemory(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readShortMemory(off, addr)
{
if (BYTEARRAYS) {
return this.ab[off] | (this.ab[off + 1] << 8);
}
var w;
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
var dw = (this.adw[idw] >> nShift);
if (nShift < 24) {
w = dw & 0xffff;
} else {
w = (dw & 0xff) | ((this.adw[idw + 1] & 0xff) << 8);
}
return w;
}
/**
* writeByteMemory(off, b, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteMemory(off, b, addr)
{
if (BYTEARRAYS) {
this.ab[off] = b;
} else {
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
this.adw[idw] = (this.adw[idw] & ~(0xff << nShift)) | (b << nShift);
}
this.fDirty = true;
}
/**
* writeShortMemory(off, w, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeShortMemory(off, w, addr)
{
if (BYTEARRAYS) {
this.ab[off] = (w & 0xff);
this.ab[off + 1] = (w >> 8);
} else {
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
if (nShift < 24) {
this.adw[idw] = (this.adw[idw] & ~(0xffff << nShift)) | (w << nShift);
} else {
this.adw[idw] = (this.adw[idw] & 0x00ffffff) | (w << 24);
idw++;
this.adw[idw] = (this.adw[idw] & (0xffffff00|0)) | (w >> 8);
}
}
this.fDirty = true;
}
/**
* readByteChecked(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteChecked(off, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryRead(this.addr + off);
}
return this.readByteDirect(off, addr);
}
/**
* readShortChecked(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readShortChecked(off, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryRead(this.addr + off, 2);
}
return this.readShortDirect(off, addr);
}
/**
* writeByteChecked(off, b, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @param {number} b
*/
writeByteChecked(off, b, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryWrite(this.addr + off);
}
if (this.fReadOnly) this.writeNone(off, b, addr); else this.writeByteDirect(off, b, addr);
}
/**
* writeShortChecked(off, w, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @param {number} w
*/
writeShortChecked(off, w, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryWrite(this.addr + off, 2)
}
if (this.fReadOnly) this.writeNone(off, w, addr); else this.writeShortDirect(off, w, addr);
}
/**
* readByteBE(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteBE(off, addr)
{
return this.ab[off];
}
/**
* readByteLE(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteLE(off, addr)
{
return this.ab[off];
}
/**
* readShortBE(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readShortBE(off, addr)
{
return this.dv.getUint16(off, true);
}
/**
* readShortLE(off, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readShortLE(off, addr)
{
/*
* TODO: It remains to be seen if there's any advantage to checking the offset for an aligned read
* vs. always reading the bytes separately; it seems a safe bet for longs, but it's less clear for shorts.
*/
return (off & 0x1)? (this.ab[off] | (this.ab[off+1] << 8)) : this.aw[off >> 1];
}
/**
* writeByteBE(off, b, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteBE(off, b, addr)
{
this.ab[off] = b;
this.fDirty = true;
}
/**
* writeByteLE(off, b, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @param {number} b
*/
writeByteLE(off, b, addr)
{
this.ab[off] = b;
this.fDirty = true;
}
/**
* writeShortBE(off, w, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @param {number} w
*/
writeShortBE(off, w, addr)
{
this.dv.setUint16(off, w, true);
this.fDirty = true;
}
/**
* writeShortLE(off, w, addr)
*
* @this {Memory8080}
* @param {number} off
* @param {number} addr
* @param {number} w
*/
writeShortLE(off, w, addr)
{
/*
* TODO: It remains to be seen if there's any advantage to checking the offset for an aligned write
* vs. always writing the bytes separately; it seems a safe bet for longs, but it's less clear for shorts.
*/
if (off & 0x1) {
this.ab[off] = w;
this.ab[off+1] = w >> 8;
} else {
this.aw[off >> 1] = w;
}
this.fDirty = true;
}
/**
* adjustEndian(dw)
*
* @param {number} dw
* @return {number}
*/
static adjustEndian(dw)
{
if (TYPEDARRAYS && !littleEndian) {
dw = (dw << 24) | ((dw << 8) & 0x00ff0000) | ((dw >> 8) & 0x0000ff00) | (dw >>> 24);
}
return dw;
}
}
/*
* 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 Memory8080 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.
*/
Memory8080.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)
*/
Memory8080.idBlock = 0;
/*
* This is the effective definition of afnNone, but we need not fully define it, because setAccess()
* uses these defaults when any of the 4 handlers (ie, 2 byte handlers and 2 short handlers) are undefined.
*
Memory8080.afnNone = [
Memory8080.prototype.readNone,
Memory8080.prototype.writeNone,
Memory8080.prototype.readShortDefault,
Memory8080.prototype.writeShortDefault
];
*/
Memory8080.afnNone = [];
Memory8080.afnMemory = [
Memory8080.prototype.readByteMemory,
Memory8080.prototype.writeByteMemory,
Memory8080.prototype.readShortMemory,
Memory8080.prototype.writeShortMemory
];
Memory8080.afnChecked = [
Memory8080.prototype.readByteChecked,
Memory8080.prototype.writeByteChecked,
Memory8080.prototype.readShortChecked,
Memory8080.prototype.writeShortChecked
];
if (TYPEDARRAYS) {
Memory8080.afnArrayBE = [
Memory8080.prototype.readByteBE,
Memory8080.prototype.writeByteBE,
Memory8080.prototype.readShortBE,
Memory8080.prototype.writeShortBE
];
Memory8080.afnArrayLE = [
Memory8080.prototype.readByteLE,
Memory8080.prototype.writeByteLE,
Memory8080.prototype.readShortLE,
Memory8080.prototype.writeShortLE
];
}
/**
* @copyright http://pcjs.org/modules/pc8080/lib/cpu.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class CPU8080 extends Component {
/**
* CPU8080(parmsCPU, nCyclesDefault)
*
* The CPU8080 class supports the following (parmsCPU) properties:
*
* cycles: the machine's base cycles per second; the CPUState8080 constructor
* will provide us with a default (based on the CPU model) to use as a fallback.
*
* multiplier: base cycle multiplier; default is 1.
*
* autoStart: true to automatically start, false to not, or null if "it depends";
* null is the default, which means do not autostart UNLESS there is no Debugger
* and no "Run" button (ie, no way to manually start the machine).
*
* csStart: the number of cycles that runCPU() must wait before generating
* checksum records; -1 if disabled. checksum records are a diagnostic aid
* used to help compare one CPU run to another.
*
* csInterval: the number of cycles that runCPU() must execute before generating
* a checksum record; -1 if disabled.
*
* csStop: the number of cycles to stop generating checksum records.
*
* This component is primarily responsible for interfacing the CPU with the outside
* world (eg, Panel and Debugger components), and managing overall CPU operation.
*
* It is extended by the CPUState8080 component, where the simulation control logic resides.
*
* @this {CPU8080}
* @param {Object} parmsCPU
* @param {number} nCyclesDefault
*/
constructor(parmsCPU, nCyclesDefault)
{
super("CPU", parmsCPU, Messages8080.CPU);
var nCycles = parmsCPU['cycles'] || nCyclesDefault;
var nMultiplier = parmsCPU['multiplier'] || 1;
this.nCyclesPerSecond = nCycles;
/*
* nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for
* the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier
* until we've exceeded the host's speed limit and then starts the multiplier over at 1.
*/
this.nCyclesMultiplier = nMultiplier;
this.mhzDefault = Math.round(this.nCyclesPerSecond / 10000) / 100;
/*
* TODO: Take care of this with an initial setSpeed() call instead?
*/
this.mhzTarget = this.mhzDefault * this.nCyclesMultiplier;
/*
* We add a number of flags to the set initialized by Component
*/
this.flags.running = false;
this.flags.starting = false;
this.flags.autoStart = parmsCPU['autoStart'];
/*
* TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both)
*/
this.flags.displayLiveRegs = false;
/*
* Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum
* is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval
* ("csInterval") set to a positive value.
*
* As above, any of these parameters can also be set with the Debugger's execution options
* command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000
* and call resetChecksum().
*/
this.flags.checksum = false;
this.nChecksum = this.nCyclesChecksumNext = 0;
this.nCyclesChecksumStart = parmsCPU["csStart"];
this.nCyclesChecksumInterval = parmsCPU["csInterval"];
this.nCyclesChecksumStop = parmsCPU["csStop"];
/*
* Array of countdown timers managed by addTimer() and setTimer().
*/
this.aTimers = [];
this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); };
this.setReady();
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {CPU8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPU8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.dbg = dbg;
for (var i = 0; i < CPU8080.BUTTONS.length; i++) {
var control = this.bindings[CPU8080.BUTTONS[i]];
if (control) this.cmp.setBinding(null, CPU8080.BUTTONS[i], control);
}
/*
* Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts.
*/
this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet"));
/*
* We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override.
*/
var sAutoStart = cmp.getMachineParm('autoStart');
if (sAutoStart != null) {
this.flags.autoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart));
}
this.setReady();
}
/**
* reset()
*
* @this {CPU8080}
*/
reset()
{
}
/**
* save()
*
* This is a placeholder for save support (overridden by the CPUState8080 component).
*
* @this {CPU8080}
* @return {Object|null}
*/
save()
{
return null;
}
/**
* restore(data)
*
* This is a placeholder for restore support (overridden by the CPUState8080 component).
*
* @this {CPU8080}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
return false;
}
/**
* powerUp(data, fRepower)
*
* @this {CPU8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data || !this.restore) {
this.reset();
} else {
this.resetCycles();
if (!this.restore(data)) return false;
this.resetChecksum();
}
/*
* Give the Debugger a chance to do/print something once we've powered up
*/
if (DEBUGGER && this.dbg) {
this.dbg.init();
} else {
/*
* The Computer (this.cmp) knows if there's a Control Panel (this.cmp.panel), and the Control Panel
* knows if there's a "print" control (this.cmp.panel.controlPrint), and if there IS a "print" control
* but no debugger, the machine is probably misconfigured (most likely, the page simply neglected to
* load the Debugger component).
*
* However, we don't actually need to check all that; it's always safe use println(), regardless whether
* a Control Panel with a "print" control is present or not.
*/
this.println("No debugger detected");
}
}
/*
* The Computer component (which is responsible for all powerDown and powerUp notifications)
* is now responsible for managing a component's fPowered flag, not us.
*
* this.flags.powered = true;
*/
this.updateCPU();
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {CPU8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
/*
* The Computer component (which is responsible for all powerDown and powerUp notifications)
* is now responsible for managing a component's fPowered flag, not us.
*
* this.flags.powered = false;
*/
return fSave? this.save() : true;
}
/**
* autoStart()
*
* @this {CPU8080}
* @return {boolean} true if started, false if not
*/
autoStart()
{
/*
* Start running automatically on power-up, assuming there's no Debugger and no "Run" button
*/
if (this.flags.autoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) {
/*
* We used to also set fUpdateFocus when calling runCPU(), on the assumption that in the "auto-starting"
* context, a machine without focus is like a day without sunshine, but in reality, focus should only be
* forced when the user takes some other machine-related action.
*/
this.runCPU();
return true;
}
return false;
}
/**
* isPowered()
*
* @this {CPU8080}
* @return {boolean}
*/
isPowered()
{
if (!this.flags.powered) {
this.println(this.toString() + " not powered");
return false;
}
return true;
}
/**
* isRunning()
*
* @this {CPU8080}
* @return {boolean}
*/
isRunning()
{
return this.flags.running;
}
/**
* getChecksum()
*
* This will be implemented by the CPUState8080 component.
*
* @this {CPU8080}
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
*/
getChecksum()
{
return 0;
}
/**
* resetChecksum()
*
* If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the
* cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever
* the CPU is reset or restored.
*
* @this {CPU8080}
* @return {boolean} true if checksum generation enabled, false if not
*/
resetChecksum()
{
if (this.nCyclesChecksumStart === undefined) this.nCyclesChecksumStart = 0;
if (this.nCyclesChecksumInterval === undefined) this.nCyclesChecksumInterval = -1;
if (this.nCyclesChecksumStop === undefined) this.nCyclesChecksumStop = -1;
this.flags.checksum = (this.nCyclesChecksumStart >= 0 && this.nCyclesChecksumInterval > 0);
if (this.flags.checksum) {
this.nChecksum = 0;
this.nCyclesChecksumNext = this.nCyclesChecksumStart - this.nTotalCycles;
/*
* this.nCyclesChecksumNext = this.nCyclesChecksumStart + this.nCyclesChecksumInterval -
* (this.nTotalCycles % this.nCyclesChecksumInterval);
*/
return true;
}
return false;
}
/**
* updateChecksum(nCycles)
*
* When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum
* number of cycles (1), effectively limiting execution to a single instruction, and then we're called with
* the exact number cycles that were actually executed. This should give us instruction-granular checksums
* at precise intervals that are 100% repeatable.
*
* @this {CPU8080}
* @param {number} nCycles
*/
updateChecksum(nCycles)
{
if (this.flags.checksum) {
/*
* Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum
*/
var fDisplay = false;
this.nChecksum = (this.nChecksum + this.getChecksum())|0;
this.nCyclesChecksumNext -= nCycles;
if (this.nCyclesChecksumNext <= 0) {
this.nCyclesChecksumNext += this.nCyclesChecksumInterval;
fDisplay = true;
}
if (this.nCyclesChecksumStop >= 0) {
if (this.nCyclesChecksumStop <= this.getCycles()) {
this.nCyclesChecksumInterval = this.nCyclesChecksumStop = -1;
this.resetChecksum();
this.stopCPU();
fDisplay = true;
}
}
if (fDisplay) this.displayChecksum();
}
}
/**
* displayChecksum()
*
* When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all
* checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU
* properties).
*
* @this {CPU8080}
*/
displayChecksum()
{
this.println(this.getCycles() + " cycles: " + "checksum=" + Str.toHex(this.nChecksum));
}
/**
* displayValue(sLabel, nValue, cch)
*
* This is principally for displaying register values, but in reality, it can be used to display any
* numeric (hex) value bound to the given label.
*
* @this {CPU8080}
* @param {string} sLabel
* @param {number} nValue
* @param {number} cch
*/
displayValue(sLabel, nValue, cch)
{
if (this.bindings[sLabel]) {
if (nValue === undefined) {
this.setError("Value for " + sLabel + " is invalid");
this.stopCPU();
}
var sVal;
if (!this.flags.running || this.flags.displayLiveRegs) {
sVal = Str.toHex(nValue, cch);
} else {
sVal = "--------".substr(0, cch);
}
/*
* TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if
* we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary
* string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being
* too obsessive.
*/
if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal;
}
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {CPU8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var cpu = this;
var fBound = false;
switch (sBinding) {
case "power":
case "reset":
/*
* The "power" and "reset" buttons are functions of the entire computer, not just the CPU,
* but it's not always convenient to stick a power button in the Computer component definition,
* so we record those bindings here and pass them on to the Computer component in initBus().
*/
this.bindings[sBinding] = control;
fBound = true;
break;
case "run":
this.bindings[sBinding] = control;
control.onclick = function onClickRun() {
if (!cpu.cmp || !cpu.cmp.checkPower()) return;
/*
* We no longer pass true to these runCPU()/stopCPU() calls, on the theory that if the "run"
* control is visible, then the computer is probably sufficiently visible as well; the problem
* with setting fUpdateFocus to true is that it can jerk the web page around in annoying ways.
*/
if (!cpu.flags.running)
cpu.runCPU();
else
cpu.stopCPU();
};
fBound = true;
break;
case "speed":
this.bindings[sBinding] = control;
fBound = true;
break;
case "setSpeed":
this.bindings[sBinding] = control;
control.onclick = function onClickSetSpeed() {
cpu.setSpeed(cpu.nCyclesMultiplier << 1, true);
};
control.textContent = this.getSpeedTarget();
fBound = true;
break;
default:
break;
}
return fBound;
}
/**
* setBurstCycles(nCycles)
*
* This function is used by the ChipSet component whenever a very low timer count is set,
* in anticipation of the timer requiring an update sooner than the normal nCyclesPerYield
* period in runCPU() would normally provide.
*
* NOTE: In this context, "timer" refers to a timer chip (eg, an Intel 8253) being emulated by
* by the ChipSet component, not the timers managed by the CPU (eg, addTimer(), setTimer(), etc).
*
* @this {CPU8080}
* @param {number} nCycles is the target number of cycles to drop the current burst to
* @return {boolean}
*/
setBurstCycles(nCycles)
{
if (this.flags.running) {
var nDelta = this.nStepCycles - nCycles;
/*
* NOTE: If nDelta is negative, we will actually be increasing nStepCycles and nBurstCycles.
* Which is OK, but if we're also taking snapshots of the cycle counts, to make sure that instruction
* costs are being properly assessed, then we need to update nSnapCycles as well.
*
* TODO: If the delta is negative, we could simply ignore the request, but we must first carefully
* consider the impact on the ChipSet timers, if any.
*/
// if (DEBUG) this.nSnapCycles -= nDelta;
this.nStepCycles -= nDelta;
this.nBurstCycles -= nDelta;
return true;
}
return false;
}
/**
* addCycles(nCycles, fEndStep)
*
* @this {CPU8080}
* @param {number} nCycles
* @param {boolean} [fEndStep]
*/
addCycles(nCycles, fEndStep)
{
this.nTotalCycles += nCycles;
if (fEndStep) {
this.nBurstCycles = this.nStepCycles = 0;
}
}
/**
* calcCycles(fRecalc)
*
* Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst
* is driven by YIELDS_PER_SECOND (eg, 30).
*
* At the end of each burst, we subtract burst cycles from the yield cycle "threshold" counter.
* Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time to the time
* we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time remaining,
* we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU().
*
* @this {CPU8080}
* @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent
* speed calculation (see calcSpeed).
*/
calcCycles(fRecalc)
{
/*
* Calculate "per" yield values.
*/
var vMultiplier = 1;
if (fRecalc) {
if (this.nCyclesMultiplier > 1 && this.mhz) {
vMultiplier = (this.mhz / this.mhzDefault);
}
}
this.msPerYield = Math.round(1000 / CPU8080.YIELDS_PER_SECOND);
this.nCyclesPerYield = Math.floor(this.nCyclesPerSecond / CPU8080.YIELDS_PER_SECOND * vMultiplier);
/*
* And initialize "next" yield values to the "per" values.
*/
if (!fRecalc) {
this.nCyclesNextYield = this.nCyclesPerYield;
}
this.nCyclesRecalc = 0;
}
/**
* getCycles(fScaled)
*
* getCycles() returns the number of cycles executed so far. Note that we can be called after
* runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(),
* so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles),
* less the number of cycles it has yet to run (nStepCycles).
*
* nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also
* have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and
* nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on
* getCycles() returning steadily increasing values should also be prepared for a reset at any time.
*
* @this {CPU8080}
* @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1
* @return {number}
*/
getCycles(fScaled)
{
var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles;
if (fScaled && this.nCyclesMultiplier > 1 && this.mhz > this.mhzDefault) {
/*
* We could scale the current cycle count by the current effective speed (this.mhz); eg:
*
* nCycles = Math.round(nCycles / (this.mhz / this.mhzDefault));
*
* but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller
* fluctuations after each burst of instructions that runCPU() executes.
*
* Alternatively, we can scale the cycle count by the multiplier, which is good in that the
* multiplier doesn't vary once the user changes it, but a potential downside is that the
* multiplier might be set too high, resulting in a target speed that's higher than the effective
* speed is able to reach.
*
* Also, if multipliers were always limited to a power-of-two, then this could be calculated
* with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger
* interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's
* XML file).
*/
nCycles = Math.round(nCycles / this.nCyclesMultiplier);
}
return nCycles;
}
/**
* getCyclesPerSecond()
*
* This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine)
*
* @this {CPU8080}
* @return {number}
*/
getCyclesPerSecond()
{
return this.nCyclesPerSecond;
}
/**
* resetCycles()
*
* Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp().
* It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(),
* which in turn assumes that all the cycle counts have been initialized to sensible values.
*
* @this {CPU8080}
*/
resetCycles()
{
this.mhz = 0;
this.nYieldsSinceStatusUpdate = 0;
this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = 0;
this.resetChecksum();
this.setSpeed(1);
}
/**
* getSpeed()
*
* @this {CPU8080}
* @return {number} the current speed multiplier
*/
getSpeed()
{
return this.nCyclesMultiplier;
}
/**
* getSpeedCurrent()
*
* @this {CPU8080}
* @return {string} the current speed, in mhz, as a string formatted to two decimal places
*/
getSpeedCurrent()
{
/*
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
*/
return ((this.flags.running && this.mhz)? (this.mhz.toFixed(2) + "Mhz") : "Stopped");
}
/**
* getSpeedTarget()
*
* @this {CPU8080}
* @return {string} the target speed, in mhz, as a string formatted to two decimal places
*/
getSpeedTarget()
{
/*
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
*/
return this.mhzTarget.toFixed(2) + "Mhz";
}
/**
* setSpeed(nMultiplier, fUpdateFocus)
*
* NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point.
*
* @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset,
* so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls
* setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting).
*
* @this {CPU8080}
* @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high)
* @param {boolean} [fUpdateFocus] is true to update Computer focus
* @return {boolean} true if successful, false if not
*/
setSpeed(nMultiplier, fUpdateFocus)
{
var fSuccess = false;
if (nMultiplier !== undefined) {
/*
* If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1).
*/
if (this.mhz / this.mhzTarget < 0.8) {
nMultiplier = 1;
} else {
fSuccess = true;
}
this.nCyclesMultiplier = nMultiplier;
var mhz = this.mhzDefault * this.nCyclesMultiplier;
if (this.mhzTarget != mhz) {
this.mhzTarget = mhz;
var sSpeed = this.getSpeedTarget();
var controlSpeed = this.bindings["setSpeed"];
if (controlSpeed) controlSpeed.textContent = sSpeed;
this.println("target speed: " + sSpeed);
}
if (fUpdateFocus && this.cmp) this.cmp.updateFocus();
}
this.addCycles(this.nRunCycles);
this.nRunCycles = 0;
this.msStartRun = Component.getTime();
this.msEndThisRun = 0;
this.calcCycles();
return fSuccess;
}
/**
* calcSpeed(nCycles, msElapsed)
*
* @this {CPU8080}
* @param {number} nCycles
* @param {number} msElapsed
*/
calcSpeed(nCycles, msElapsed)
{
if (msElapsed) {
this.mhz = Math.round(nCycles / (msElapsed * 10)) / 100;
if (msElapsed >= 86400000) {
this.nTotalCycles = 0;
this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow
}
}
}
/**
* calcStartTime()
*
* @this {CPU8080}
*/
calcStartTime()
{
if (this.nCyclesRecalc >= this.nCyclesPerSecond) {
this.calcCycles(true);
}
this.nCyclesThisRun = 0;
this.msStartThisRun = Component.getTime();
/*
* Try to detect situations where the browser may have throttled us, such as when the user switches
* to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks
* to roughly one per second.
*
* Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled,
* but there can still be enough of a lag between the callbacks that CPU speed will be noticeably
* erratic if we don't compensate for it here.
*
* We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the
* previous run and includes any requested sleep time) is comparable to the current msStartThisRun;
* if the delta is significant, we compensate by bumping msStartRun forward by that delta.
*
* This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called
* whenever the CPU starts running again -- zeroes msEndThisRun.
*
* This also won't do anything about other internal delays; for example, Debugger message() calls.
* By the time the message() function has called yieldCPU(), the cost of the message has already been
* incurred, so it will be end up being charged against the instruction(s) that triggered it.
*
* TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun
* "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability
* to hit its target speed, since you would expect any instruction that displays a message to be an
* EXTREMELY slow instruction.
*/
if (this.msEndThisRun) {
var msDelta = this.msStartThisRun - this.msEndThisRun;
if (msDelta > this.msPerYield) {
if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms");
this.msStartRun += msDelta;
/*
* Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just
* in case, I make absolutely sure it cannot happen, since doing so could result in negative
* speed calculations.
*/
if (this.msStartRun > this.msStartThisRun) {
this.msStartRun = this.msStartThisRun;
}
}
}
}
/**
* calcRemainingTime()
*
* @this {CPU8080}
* @return {number}
*/
calcRemainingTime()
{
this.msEndThisRun = Component.getTime();
var msYield = this.msPerYield;
if (this.nCyclesThisRun) {
/*
* Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU
* now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then
* the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed,
* and so applying that percentage to msPerYield should give us a better estimate of work vs. time.
*/
msYield = Math.round(msYield * this.nCyclesThisRun / this.nCyclesPerYield);
}
var msElapsedThisRun = this.msEndThisRun - this.msStartThisRun;
var msRemainsThisRun = msYield - msElapsedThisRun;
/*
* We could pass only "this run" results to calcSpeed():
*
* nCycles = this.nCyclesThisRun;
* msElapsed = msElapsedThisRun;
*
* but it seems preferable to use longer time periods and hopefully get a more accurate speed.
*
* Also, if msRemainsThisRun >= 0 && this.nCyclesMultiplier == 1, we could pass these results instead:
*
* nCycles = this.nCyclesThisRun;
* msElapsed = this.msPerYield;
*
* to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations.
*/
var nCycles = this.nRunCycles;
var msElapsed = this.msEndThisRun - this.msStartRun;
if (MAXDEBUG && msRemainsThisRun < 0 && this.nCyclesMultiplier > 1) {
this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)");
}
this.calcSpeed(nCycles, msElapsed);
if (msRemainsThisRun < 0 || this.mhz < this.mhzTarget) {
/*
* Try "throwing out" the effects of large anomalies, by moving the overall run start time up;
* ordinarily, this should only happen when the someone is using an external Debugger or some other
* tool or feature that is interfering with our overall execution.
*/
if (msRemainsThisRun < -1000) {
this.msStartRun -= msRemainsThisRun;
}
/*
* If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate
* nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the
* simulation is at least usable.
*/
msRemainsThisRun = 0;
}
/*
* Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(),
* it'll be ready to decide if calcCycles() should be called again.
*/
this.nCyclesRecalc += this.nCyclesThisRun;
if (DEBUG && this.messageEnabled(Messages8080.LOG) && msRemainsThisRun) {
this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.msEndThisRun + "ms");
}
this.msEndThisRun += msRemainsThisRun;
return msRemainsThisRun;
}
/**
* addTimer(callBack)
*
* Components that want to have timers that periodically fire after some number of milliseconds call
* addTimer() to create the timer, and then setTimer() every time they want to arm it. There is currently
* no removeTimer() because these are generally used for the entire lifetime of a component.
*
* Internally, each timer entry is a preallocated Array with two entries: a cycle countdown in element [0]
* and a callback function in element [1]. A timer is initially dormant; dormant timers have a countdown
* value of -1 (although any negative number will suffice) and active timers have a non-negative value.
*
* Why not use JavaScript's setTimeout() instead? Good question. For a good answer, see setTimer() below.
*
* @this {CPU8080}
* @param {function()} callBack
* @return {number} timer index
*/
addTimer(callBack)
{
var iTimer = this.aTimers.length;
this.aTimers.push([-1, callBack]);
return iTimer;
}
/**
* setTimer(iTimer, ms, fReset)
*
* Using the timer index from a previous addTimer() call, this sets that timer to fire after the
* specified number of milliseconds.
*
* This is preferred over JavaScript's setTimeout(), because all our timers are effectively paused when
* the CPU is paused (eg, when the Debugger halts execution). Moreover, setTimeout() handlers only run after
* runCPU() yields, which is far too granular for some components (eg, when the SerialPort tries to simulate
* interrupts at 9600 baud).
*
* Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components
* use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness,
* that's currently not the case. TODO: Fix.
*
* @this {CPU8080}
* @param {number} iTimer
* @param {number} ms (converted into a cycle countdown internally)
* @param {boolean} [fReset] (true if the timer should be reset even if already armed)
* @return {number} (number of cycles used to arm timer, or -1 if error)
*/
setTimer(iTimer, ms, fReset)
{
var nCycles = -1;
if (iTimer >= 0 && iTimer < this.aTimers.length) {
if (fReset || this.aTimers[iTimer][0] < 0) {
nCycles = this.getMSCycles(ms);
/*
* We must now confront the following problem: if the CPU is currently executing a burst of cycles,
* the number of cycles it has executed in that burst so far must NOT be charged against the cycle
* timeout we're about to set. The simplest way to resolve that is to immediately call endBurst()
* and bias the above cycle timeout by the number of cycles that the burst executed.
*/
this.aTimers[iTimer][0] = nCycles + this.endBurst();
}
}
return nCycles;
}
/**
* getMSCycles(ms)
*
* @this {CPU8080}
* @param {number} ms
* @return {number} number of corresponding cycles
*/
getMSCycles(ms)
{
return (this.nCyclesPerSecond * this.nCyclesMultiplier) / 1000 * ms;
}
/**
* getBurstCycles(nCycles)
*
* Used by runCPU() to get min(nCycles,[timer cycle counts])
*
* @this {CPU8080}
* @param {number} nCycles (number of cycles about to execute)
* @return {number} (either nCycles or less if a timer needs to fire)
*/
getBurstCycles(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
if (timer[0] < 0) continue;
if (nCycles > timer[0]) {
nCycles = timer[0];
}
}
return nCycles;
}
/**
* updateTimers(nCycles)
*
* Used by runCPU() to reduce all active timer countdown values by the number of cycles just executed;
* this is the function that actually "fires" any timer(s) whose countdown has reached (or dropped below)
* zero, invoking their callback function.
*
* @this {CPU8080}
* @param {number} nCycles (number of cycles actually executed)
*/
updateTimers(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
if (timer[0] < 0) continue;
timer[0] -= nCycles;
if (timer[0] <= 0) {
timer[0] = -1; // zero is technically an "active" value, so ensure the timer is dormant now
timer[1](); // safe to invoke the callback function now
}
}
}
/**
* endBurst(fReset)
*
* @this {CPU8080}
* @param {boolean} [fReset]
* @return {number} (number of cycles executed in the most recent burst)
*/
endBurst(fReset)
{
var nCycles = this.nBurstCycles -= this.nStepCycles;
this.nStepCycles = 0;
if (fReset) this.nBurstCycles = 0;
return nCycles;
}
/**
* runCPU(fUpdateFocus)
*
* @this {CPU8080}
* @param {boolean} [fUpdateFocus] is true to update Computer focus
*/
runCPU(fUpdateFocus)
{
if (!this.setBusy(true)) {
this.updateCPU();
if (this.cmp) this.cmp.stop(Component.getTime(), this.getCycles());
return;
}
this.startCPU(fUpdateFocus);
/*
* calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally
* recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached.
*/
this.calcStartTime();
try {
do {
/*
* nCycles is how many cycles we WANT to run on each iteration of stepCPU(), and may be as
* HIGH as nCyclesPerYield, but it may be significantly less. getBurstCycles() will adjust
* nCycles downward if any CPU timers need to fire during the next burst.
*/
var nCycles = this.getBurstCycles(this.flags.checksum? 1 : this.nCyclesPerYield);
/*
* Execute the burst.
*/
this.stepCPU(nCycles);
/*
* Terminate the burst, returning the number of cycles that stepCPU() actually ran.
*/
nCycles = this.endBurst(true);
/*
* Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU first started).
*/
this.nCyclesThisRun += nCycles;
this.nRunCycles += nCycles;
this.updateChecksum(nCycles);
/*
* Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero.
*/
this.updateTimers(nCycles);
this.nCyclesNextYield -= nCycles;
if (this.nCyclesNextYield <= 0) {
this.nCyclesNextYield += this.nCyclesPerYield;
if (++this.nYieldsSinceStatusUpdate >= CPU8080.YIELDS_PER_STATUS) {
if (this.cmp) this.cmp.updateStatus();
this.nYieldsSinceStatusUpdate = 0;
}
break;
}
} while (this.flags.running);
}
catch (e) {
this.stopCPU();
this.updateCPU();
if (this.cmp) this.cmp.stop(Component.getTime(), this.getCycles());
this.setBusy(false);
this.setError(e.stack || e.message);
return;
}
setTimeout(this.onRunTimeout, this.calcRemainingTime());
}
/**
* startCPU(fUpdateFocus)
*
* WARNING: Other components must use runCPU() to get the CPU running; this is a runCPU() helper function only.
*
* @param {boolean} [fUpdateFocus]
*/
startCPU(fUpdateFocus)
{
if (!this.flags.running) {
/*
* setSpeed() without a speed parameter leaves the selected speed in place, but also resets the
* cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number
* of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc
* threshold counter.
*/
this.setSpeed();
if (this.cmp) this.cmp.start(this.msStartRun, this.getCycles());
this.flags.running = true;
this.flags.starting = true;
if (this.chipset) this.chipset.start();
var controlRun = this.bindings["run"];
if (controlRun) controlRun.textContent = "Halt";
if (this.cmp) {
this.cmp.updateStatus(true);
if (fUpdateFocus) this.cmp.updateFocus(true);
}
}
}
/**
* stepCPU(nMinCycles)
*
* This will be implemented by the CPUState8080 component.
*
* @this {CPU8080}
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
* @return {number} of cycles executed; 0 indicates that the last instruction was not executed
*/
stepCPU(nMinCycles)
{
return 0;
}
/**
* stopCPU(fComplete)
*
* For use by any component that wants to stop the CPU.
*
* This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU();
* it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....)
*
* @this {CPU8080}
* @param {boolean} [fComplete]
*/
stopCPU(fComplete)
{
this.isBusy(true);
this.endBurst();
this.addCycles(this.nRunCycles);
this.nRunCycles = 0;
if (this.flags.running) {
this.flags.running = false;
if (this.chipset) this.chipset.stop();
var controlRun = this.bindings["run"];
if (controlRun) controlRun.textContent = "Run";
}
this.flags.complete = fComplete;
}
/**
* updateCPU(fForce)
*
* This used to be performed at the end of every stepCPU(), but runCPU() -- which relies upon
* stepCPU() -- needed to have more control over when these updates are performed. However, for
* other callers of stepCPU(), such as the Debugger, the combination of stepCPU() + updateCPU()
* provides the old behavior.
*
* @this {CPU8080}
* @param {boolean} [fForce] (true to force a video update; used by the Debugger)
*/
updateCPU(fForce)
{
if (this.cmp) {
this.cmp.updateVideo(fForce);
this.cmp.updateStatus(fForce);
}
}
/**
* yieldCPU()
*
* Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU
* remains in a "running" state.
*
* @this {CPU8080}
*/
yieldCPU()
{
this.endBurst(); // this will break us out of stepCPU()
this.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU()
// if (DEBUG) this.nSnapCycles = this.nBurstCycles;
/*
* The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks
* odd for those messages to show CPU state changes but for the CPU's own status display to not (ditto
* for the Video display), so I've added this call to try to keep things looking synchronized.
*/
this.updateCPU();
}
}
/*
* 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.nCyclesPerYield: (this.nCyclesPerSecond / CPU8080.YIELDS_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.nCyclesNextYield: this.nCyclesPerYield
*/
CPU8080.YIELDS_PER_SECOND = 30; // just a gut feeling for the MINIMUM number of yields per second
CPU8080.YIELDS_PER_STATUS = 15; // every 15 yields (ie, twice per second), perform CPU status updates
CPU8080.BUTTONS = ["power", "reset"];
/**
* @copyright http://pcjs.org/modules/pc8080/lib/cpustate.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class CPUState8080 extends CPU8080 {
/**
* CPUState8080(parmsCPU)
*
* The CPUState8080 class uses the following (parmsCPU) properties:
*
* model: a number (eg, 8080) that should match one of the CPUDef8080.MODEL_* values
*
* This extends the CPU class and passes any remaining parmsCPU properties to the CPU class
* constructor, along with a default speed (cycles per second) based on the specified (or default)
* CPU model number.
*
* The CPUState8080 class was initially written to simulate a 8080 microprocessor, although over time
* it may evolved to support other microprocessors (eg, the Zilog Z80).
*
* @this {CPUState8080}
* @param {Object} parmsCPU
*/
constructor(parmsCPU)
{
var nCyclesDefault = 0;
var model = +parmsCPU['model'] || CPUDef8080.MODEL_8080;
switch(model) {
case CPUDef8080.MODEL_8080:
default:
nCyclesDefault = 1000000;
break;
}
super(parmsCPU, nCyclesDefault);
this.model = model;
/*
* 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.complete = this.flags.debugCheck = false;
/*
* If there are no live registers to display, then updateStatus() can skip a bit....
*/
this.cLiveRegs = 0;
/*
* Array of halt handlers, if any (see addHaltCheck)
*/
this.afnHalt = [];
this.addrReset = 0x0000;
/*
* This initial resetRegs() call is important to create all the registers, so that if/when we call restore(),
* it will have something to fill in.
*/
this.resetRegs();
}
/**
* addHaltCheck(fn)
*
* Records a function that will be called during HLT opcode processing.
*
* @this {CPUState8080}
* @param {function(number)} fn
*/
addHaltCheck(fn)
{
this.afnHalt.push(fn);
}
/**
* initProcessor()
*
* Interestingly, if I dynamically generate aOps as an array of functions bound to "this", using the bind()
* method, overall performance is worse. You would think that eliminating the need to use the call() method
* on every opcode function invocation would be helpful, but it's not. I'm not sure exactly why yet; perhaps
* a Closure Compiler optimization is defeated when generating the function array at run-time instead of at
* compile-time.
*
* @this {CPUState8080}
*/
initProcessor()
{
this.aOps = CPUDef8080.aOps8080;
}
/**
* reset()
*
* @this {CPUState8080}
*/
reset()
{
if (this.flags.running) this.stopCPU();
this.resetRegs();
this.resetCycles();
this.clearError(); // clear any fatal error/exception that setError() may have flagged
super.reset();
}
/**
* resetRegs()
*
* @this {CPUState8080}
*/
resetRegs()
{
this.regA = 0;
this.regB = 0;
this.regC = 0;
this.regD = 0;
this.regE = 0;
this.regH = 0;
this.regL = 0;
this.setSP(0);
this.setPC(this.addrReset);
/*
* This resets the Processor Status flags (regPS), along with all the internal "result registers".
*/
this.setPS(0);
/*
* intFlags contains some internal states we use to indicate whether a hardware interrupt (INTFLAG.INTR) or
* Trap software interrupt (INTR.TRAP) has been requested, as well as when we're in a "HLT" state (INTFLAG.HALT)
* that requires us to wait for a hardware interrupt (INTFLAG.INTR) before continuing execution.
*/
this.intFlags = CPUDef8080.INTFLAG.NONE;
}
/**
* setReset(addr)
*
* @this {CPUState8080}
* @param {number} addr
*/
setReset(addr)
{
this.addrReset = addr;
this.setPC(addr);
}
/**
* getChecksum()
*
* @this {CPUState8080}
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
*/
getChecksum()
{
var sum = (this.regA + this.regB + this.regC + this.regD + this.regE + this.regH + this.regL)|0;
sum = (sum + this.getSP() + this.getPC() + this.getPS())|0;
return sum;
}
/**
* save()
*
* This implements save support for the CPUState8080 component.
*
* @this {CPUState8080}
* @return {Object|null}
*/
save()
{
var state = new State(this);
state.set(0, [this.regA, this.regB, this.regC, this.regD, this.regE, this.regH, this.regL, this.getSP(), this.getPC(), this.getPS()]);
state.set(1, [this.intFlags, this.nTotalCycles, this.getSpeed()]);
state.set(2, this.bus.saveMemory());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the CPUState8080 component.
*
* @this {CPUState8080}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
var a = data[0];
this.regA = a[0];
this.regB = a[1];
this.regC = a[2];
this.regD = a[3];
this.regE = a[4];
this.regH = a[5];
this.regL = a[6];
this.setSP(a[7]);
this.setPC(a[8]);
this.setPS(a[9]);
a = data[1];
this.intFlags = a[0];
this.nTotalCycles = a[1];
this.setSpeed(a[3]);
return this.bus.restoreMemory(data[2]);
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {CPUState8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "AX")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var fBound = false;
switch (sBinding) {
case "A":
case "B":
case "C":
case "BC":
case "D":
case "E":
case "DE":
case "H":
case "L":
case "HL":
case "SP":
case "PC":
case "PS":
case "IF":
case "SF":
case "ZF":
case "AF":
case "PF":
case "CF":
this.bindings[sBinding] = control;
this.cLiveRegs++;
fBound = true;
break;
default:
fBound = super.setBinding(sHTMLType, sBinding, control);
break;
}
return fBound;
}
/**
* getBC()
*
* @this {CPUState8080}
* @return {number}
*/
getBC()
{
return (this.regB << 8) | this.regC;
}
/**
* setBC(w)
*
* @this {CPUState8080}
* @param {number} w
*/
setBC(w)
{
this.regB = (w >> 8) & 0xff;
this.regC = w & 0xff;
}
/**
* getDE()
*
* @this {CPUState8080}
* @return {number}
*/
getDE()
{
return (this.regD << 8) | this.regE;
}
/**
* setDE(w)
*
* @this {CPUState8080}
* @param {number} w
*/
setDE(w)
{
this.regD = (w >> 8) & 0xff;
this.regE = w & 0xff;
}
/**
* getHL()
*
* @this {CPUState8080}
* @return {number}
*/
getHL()
{
return (this.regH << 8) | this.regL;
}
/**
* setHL(w)
*
* @this {CPUState8080}
* @param {number} w
*/
setHL(w)
{
this.regH = (w >> 8) & 0xff;
this.regL = w & 0xff;
}
/**
* getSP()
*
* @this {CPUState8080}
* @return {number}
*/
getSP()
{
return this.regSP;
}
/**
* setSP(off)
*
* @this {CPUState8080}
* @param {number} off
*/
setSP(off)
{
this.regSP = off & 0xffff;
}
/**
* getPC()
*
* @this {CPUState8080}
* @return {number}
*/
getPC()
{
return this.regPC;
}
/**
* offPC()
*
* @this {CPUState8080}
* @param {number} off
* @return {number}
*/
offPC(off)
{
return (this.regPC + off) & 0xffff;
}
/**
* setPC(off)
*
* @this {CPUState8080}
* @param {number} off
*/
setPC(off)
{
this.regPC = off & 0xffff;
}
/**
* clearCF()
*
* @this {CPUState8080}
*/
clearCF()
{
this.resultZeroCarry &= 0xff;
}
/**
* getCF()
*
* @this {CPUState8080}
* @return {number} 0 or 1 (CPUDef8080.PS.CF)
*/
getCF()
{
return (this.resultZeroCarry & 0x100)? CPUDef8080.PS.CF : 0;
}
/**
* setCF()
*
* @this {CPUState8080}
*/
setCF()
{
this.resultZeroCarry |= 0x100;
}
/**
* updateCF(CF)
*
* @this {CPUState8080}
* @param {number} CF (0x000 or 0x100)
*/
updateCF(CF)
{
this.resultZeroCarry = (this.resultZeroCarry & 0xff) | CF;
}
/**
* clearPF()
*
* @this {CPUState8080}
*/
clearPF()
{
if (this.getPF()) this.resultParitySign ^= 0x1;
}
/**
* getPF()
*
* @this {CPUState8080}
* @return {number} 0 or CPUDef8080.PS.PF
*/
getPF()
{
return (CPUDef8080.PARITY[this.resultParitySign & 0xff])? CPUDef8080.PS.PF : 0;
}
/**
* setPF()
*
* @this {CPUState8080}
*/
setPF()
{
if (!this.getPF()) this.resultParitySign ^= 0x1;
}
/**
* clearAF()
*
* @this {CPUState8080}
*/
clearAF()
{
this.resultAuxOverflow = (this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10);
}
/**
* getAF()
*
* @this {CPUState8080}
* @return {number} 0 or CPUDef8080.PS.AF
*/
getAF()
{
return ((this.resultParitySign ^ this.resultAuxOverflow) & 0x10)? CPUDef8080.PS.AF : 0;
}
/**
* setAF()
*
* @this {CPUState8080}
*/
setAF()
{
this.resultAuxOverflow = (~this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10);
}
/**
* clearZF()
*
* @this {CPUState8080}
*/
clearZF()
{
this.resultZeroCarry |= 0xff;
}
/**
* getZF()
*
* @this {CPUState8080}
* @return {number} 0 or CPUDef8080.PS.ZF
*/
getZF()
{
return (this.resultZeroCarry & 0xff)? 0 : CPUDef8080.PS.ZF;
}
/**
* setZF()
*
* @this {CPUState8080}
*/
setZF()
{
this.resultZeroCarry &= ~0xff;
}
/**
* clearSF()
*
* @this {CPUState8080}
*/
clearSF()
{
if (this.getSF()) this.resultParitySign ^= 0xc0;
}
/**
* getSF()
*
* @this {CPUState8080}
* @return {number} 0 or CPUDef8080.PS.SF
*/
getSF()
{
return (this.resultParitySign & 0x80)? CPUDef8080.PS.SF : 0;
}
/**
* setSF()
*
* @this {CPUState8080}
*/
setSF()
{
if (!this.getSF()) this.resultParitySign ^= 0xc0;
}
/**
* clearIF()
*
* @this {CPUState8080}
*/
clearIF()
{
this.regPS &= ~CPUDef8080.PS.IF;
}
/**
* getIF()
*
* @this {CPUState8080}
* @return {number} 0 or CPUDef8080.PS.IF
*/
getIF()
{
return (this.regPS & CPUDef8080.PS.IF);
}
/**
* setIF()
*
* @this {CPUState8080}
*/
setIF()
{
this.regPS |= CPUDef8080.PS.IF;
}
/**
* getPS()
*
* @this {CPUState8080}
* @return {number}
*/
getPS()
{
return (this.regPS & ~CPUDef8080.PS.RESULT) | (this.getSF() | this.getZF() | this.getAF() | this.getPF() | this.getCF());
}
/**
* setPS(regPS)
*
* @this {CPUState8080}
* @param {number} regPS
*/
setPS(regPS)
{
this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = 0;
if (regPS & CPUDef8080.PS.CF) this.resultZeroCarry |= 0x100;
if (!(regPS & CPUDef8080.PS.PF)) this.resultParitySign |= 0x01;
if (regPS & CPUDef8080.PS.AF) this.resultAuxOverflow |= 0x10;
if (!(regPS & CPUDef8080.PS.ZF)) this.resultZeroCarry |= 0xff;
if (regPS & CPUDef8080.PS.SF) this.resultParitySign ^= 0xc0;
this.regPS = (this.regPS & ~(CPUDef8080.PS.RESULT | CPUDef8080.PS.INTERNAL)) | (regPS & CPUDef8080.PS.INTERNAL) | CPUDef8080.PS.SET;
}
/**
* getPSW()
*
* @this {CPUState8080}
* @return {number}
*/
getPSW()
{
return (this.getPS() & CPUDef8080.PS.MASK) | (this.regA << 8);
}
/**
* setPSW(w)
*
* @this {CPUState8080}
* @param {number} w
*/
setPSW(w)
{
this.setPS((w & CPUDef8080.PS.MASK) | (this.regPS & ~CPUDef8080.PS.MASK));
this.regA = w >> 8;
}
/**
* addByte(src)
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA + src
*/
addByte(src)
{
this.resultAuxOverflow = this.regA ^ src;
return this.resultParitySign = (this.resultZeroCarry = this.regA + src) & 0xff;
}
/**
* addByteCarry(src)
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA + src + carry
*/
addByteCarry(src)
{
this.resultAuxOverflow = this.regA ^ src;
return this.resultParitySign = (this.resultZeroCarry = this.regA + src + ((this.resultZeroCarry & 0x100)? 1 : 0)) & 0xff;
}
/**
* andByte(src)
*
* Ordinarily, one would expect the Auxiliary Carry flag (AF) to be clear after this operation,
* but apparently the 8080 will set AF if bit 3 in either operand is set.
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA & src
*/
andByte(src)
{
this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = this.regA & src;
if ((this.regA | src) & 0x8) this.resultAuxOverflow ^= 0x10; // set AF by inverting bit 4 in resultAuxOverflow
return this.resultZeroCarry;
}
/**
* decByte(b)
*
* We perform this operation using 8-bit two's complement arithmetic, by negating and then adding
* the implied src of 1. This appears to mimic how the 8080 manages the Auxiliary Carry flag (AF).
*
* @this {CPUState8080}
* @param {number} b
* @return {number}
*/
decByte(b)
{
this.resultAuxOverflow = b ^ 0xff;
b = this.resultParitySign = (b + 0xff) & 0xff;
this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b;
return b;
}
/**
* incByte(b)
*
* @this {CPUState8080}
* @param {number} b
* @return {number}
*/
incByte(b)
{
this.resultAuxOverflow = b;
b = this.resultParitySign = (b + 1) & 0xff;
this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b;
return b;
}
/**
* orByte(src)
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA | src
*/
orByte(src)
{
return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA | src;
}
/**
* subByte(src)
*
* We perform this operation using 8-bit two's complement arithmetic, by inverting src, adding
* src + 1, and then inverting the resulting carry (resultZeroCarry ^ 0x100). This appears to mimic
* how the 8080 manages the Auxiliary Carry flag (AF).
*
* This function is also used as a cmpByte() function; compare instructions simply ignore the
* return value.
*
* Example: A=66, SUI $10
*
* If we created the two's complement of 0x10 by negating it, there would just be one addition:
*
* 0110 0110 (0x66)
* + 1111 0000 (0xF0) (ie, -0x10)
* ---------
* 1 0101 0110 (0x56)
*
* But in order to mimic the 8080's AF flag, we must perform the two's complement of src in two steps,
* inverting it before the add, and then incrementing after the add; eg:
*
* 0110 0110 (0x66)
* + 1110 1111 (0xEF) (ie, ~0x10)
* ---------
* 1 0101 0101 (0x55)
* + 0000 0001 (0x01)
* ---------
* 1 0101 0110 (0x56)
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA - src
*/
subByte(src)
{
src ^= 0xff;
this.resultAuxOverflow = this.regA ^ src;
return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + 1) ^ 0x100) & 0xff;
}
/**
* subByteBorrow(src)
*
* We perform this operation using 8-bit two's complement arithmetic, using logic similar to subByte(),
* but changing the final increment to a conditional increment, because if the Carry flag (CF) is set, then
* we don't need to perform the increment at all.
*
* This mimics the behavior of subByte() when the Carry flag (CF) is clear, and hopefully also mimics how the
* 8080 manages the Auxiliary Carry flag (AF) when the Carry flag (CF) is set.
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA - src - carry
*/
subByteBorrow(src)
{
src ^= 0xff;
this.resultAuxOverflow = this.regA ^ src;
return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + ((this.resultZeroCarry & 0x100)? 0 : 1)) ^ 0x100) & 0xff;
}
/**
* xorByte(src)
*
* @this {CPUState8080}
* @param {number} src
* @return {number} regA ^ src
*/
xorByte(src)
{
return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA ^ src;
}
/**
* getByte(addr)
*
* @this {CPUState8080}
* @param {number} addr is a linear address
* @return {number} byte (8-bit) value at that address
*/
getByte(addr)
{
return this.bus.getByte(addr);
}
/**
* getWord(addr)
*
* @this {CPUState8080}
* @param {number} addr is a linear address
* @return {number} word (16-bit) value at that address
*/
getWord(addr)
{
return this.bus.getShort(addr);
}
/**
* setByte(addr, b)
*
* @this {CPUState8080}
* @param {number} addr is a linear address
* @param {number} b is the byte (8-bit) value to write (which we truncate to 8 bits; required by opSTOSb)
*/
setByte(addr, b)
{
this.bus.setByte(addr, b);
}
/**
* setWord(addr, w)
*
* @this {CPUState8080}
* @param {number} addr is a linear address
* @param {number} w is the word (16-bit) value to write (which we truncate to 16 bits to be safe)
*/
setWord(addr, w)
{
this.bus.setShort(addr, w);
}
/**
* getPCByte()
*
* @this {CPUState8080}
* @return {number} byte at the current PC; PC advanced by 1
*/
getPCByte()
{
var b = this.getByte(this.regPC);
this.setPC(this.regPC + 1);
return b;
}
/**
* getPCWord()
*
* @this {CPUState8080}
* @return {number} word at the current PC; PC advanced by 2
*/
getPCWord()
{
var w = this.getWord(this.regPC);
this.setPC(this.regPC + 2);
return w;
}
/**
* popWord()
*
* @this {CPUState8080}
* @return {number} word popped from the current SP; SP increased by 2
*/
popWord()
{
var w = this.getWord(this.regSP);
this.setSP(this.regSP + 2);
return w;
}
/**
* pushWord(w)
*
* @this {CPUState8080}
* @param {number} w is the word (16-bit) value to push at current SP; SP decreased by 2
*/
pushWord(w)
{
this.setSP(this.regSP - 2);
this.setWord(this.regSP, w);
}
/**
* checkINTR()
*
* @this {CPUState8080}
* @return {boolean} true if execution may proceed, false if not
*/
checkINTR()
{
/*
* If the Debugger is single-stepping, this.nStepCycles will always be zero, which we take
* advantage of here to avoid processing interrupts. The Debugger will have to issue a "g"
* command (or "p" command on a call instruction) if you want interrupts to be processed.
*/
if (this.nStepCycles) {
if ((this.intFlags & CPUDef8080.INTFLAG.INTR) && this.getIF()) {
for (var nLevel = 0; nLevel < 8; nLevel++) {
if (this.intFlags & (1 << nLevel)) break;
}
this.clearINTR(nLevel);
this.clearIF();
this.intFlags &= ~CPUDef8080.INTFLAG.HALT;
this.aOps[CPUDef8080.OPCODE.RST0 | (nLevel << 3)].call(this);
}
}
if (this.intFlags & CPUDef8080.INTFLAG.HALT) {
/*
* As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead, opHLT()
* calls requestHALT(), which sets INTFLAG.HALT and signals to stepCPU() that it's free to end the
* current burst AND that it should not execute any more instructions until checkINTR() indicates
* that a hardware interrupt has been requested.
*/
this.endBurst();
return false;
}
return true;
}
/**
* clearINTR(nLevel)
*
* Clear the corresponding interrupt level.
*
* nLevel can either be a valid interrupt level (0-7), or -1 to clear all pending interrupts
* (eg, in the event of a system-wide reset).
*
* @this {CPUState8080}
* @param {number} nLevel (0-7, or -1 for all)
*/
clearINTR(nLevel)
{
var bitsClear = nLevel < 0? 0xff : (1 << nLevel);
this.intFlags &= ~bitsClear;
}
/**
* requestHALT()
*
* @this {CPUState8080}
*/
requestHALT()
{
this.intFlags |= CPUDef8080.INTFLAG.HALT;
this.endBurst();
}
/**
* requestINTR(nLevel)
*
* Request the corresponding interrupt level.
*
* Each interrupt level (0-7) has its own intFlags bit (0-7). If the Interrupt Flag (IF) is also
* set, then we know that checkINTR() will want to issue the interrupt, so we end the current burst
* by setting nStepCycles to zero. But before we do, we subtract nStepCycles from nBurstCycles,
* so that the calculation of how many cycles were actually executed on this burst is correct.
*
* @this {CPUState8080}
* @param {number} nLevel (0-7)
*/
requestINTR(nLevel)
{
this.intFlags |= (1 << nLevel);
if (this.getIF()) {
this.endBurst();
}
}
/**
* updateReg(sReg, nValue, cch)
*
* This function helps updateStatus() by massaging the register names and values according to
* CPU type before passing the call to displayValue(); in the "old days", updateStatus() called
* displayValue() directly (although then it was called displayReg()).
*
* @this {CPUState8080}
* @param {string} sReg
* @param {number} nValue
* @param {number} [cch] (default is 2 hex digits)
*/
updateReg(sReg, nValue, cch)
{
this.displayValue(sReg, nValue, cch || 2);
}
/**
* updateStatus(fForce)
*
* This provides periodic Control Panel updates (eg, a few times per second; see YIELDS_PER_STATUS).
* this is where we take care of any DOM updates (eg, register values) while the CPU is running.
*
* Any high-frequency updates should be performed in updateVideo(), which should avoid DOM updates,
* since updateVideo() can be called up to 60 times per second.
*
* @this {CPUState8080}
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
*/
updateStatus(fForce)
{
if (this.cLiveRegs) {
if (fForce || !this.flags.running || this.flags.displayLiveRegs) {
this.updateReg("A", this.regA);
this.updateReg("B", this.regB);
this.updateReg("C", this.regC);
this.updateReg("BC", this.getBC(), 4);
this.updateReg("D", this.regD);
this.updateReg("E", this.regE);
this.updateReg("DE", this.getDE(), 4);
this.updateReg("H", this.regH);
this.updateReg("L", this.regL);
this.updateReg("HL", this.getHL(), 4);
this.updateReg("SP", this.getSP(), 4);
this.updateReg("PC", this.getPC(), 4);
var regPS = this.getPS();
this.updateReg("PS", regPS, 4);
this.updateReg("IF", (regPS & CPUDef8080.PS.IF)? 1 : 0, 1);
this.updateReg("SF", (regPS & CPUDef8080.PS.SF)? 1 : 0, 1);
this.updateReg("ZF", (regPS & CPUDef8080.PS.ZF)? 1 : 0, 1);
this.updateReg("AF", (regPS & CPUDef8080.PS.AF)? 1 : 0, 1);
this.updateReg("PF", (regPS & CPUDef8080.PS.PF)? 1 : 0, 1);
this.updateReg("CF", (regPS & CPUDef8080.PS.CF)? 1 : 0, 1);
}
}
var controlSpeed = this.bindings["speed"];
if (controlSpeed) controlSpeed.textContent = this.getSpeedCurrent();
}
/**
* stepCPU(nMinCycles)
*
* NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger
* operation that's completely independent of Trap status. The CPU can go in and out of Trap mode,
* in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective,
* they're all one continuous stream of instructions that can be stepped or run at will. Moreover,
* stepping vs. running should never change the behavior of the simulation.
*
* @this {CPUState8080}
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
* @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint
* was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive
* number indicates successful completion of that many cycles (which should always be >= nMinCycles).
*/
stepCPU(nMinCycles)
{
/*
* The Debugger uses fComplete to determine if the instruction completed (true) or was interrupted
* by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript
* exceptions, which stepCPU() expects the caller to catch using its own exception handler.
*
* The CPU relies on the use of stopCPU() rather than fComplete, because the CPU never single-steps
* (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the
* Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set,
* so stopCPU() would have no effect as far as the Debugger is concerned.
*/
this.flags.complete = true;
/*
* fDebugCheck is true if we need to "check" every instruction with the Debugger.
*/
var fDebugCheck = this.flags.debugCheck = (DEBUGGER && this.dbg && this.dbg.checksEnabled());
/*
* nDebugState is checked only when fDebugCheck is true, and its sole purpose is to tell the first call
* to checkInstruction() that it can skip breakpoint checks, and that will be true ONLY when fStarting is
* true OR nMinCycles is zero (the latter means the Debugger is single-stepping).
*
* Once we snap fStarting, we clear it, because technically, we've moved beyond "starting" and have
* officially "started" now.
*/
var nDebugState = (!nMinCycles)? -1 : (this.flags.starting? 0 : 1);
this.flags.starting = false;
/*
* We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other
* functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check
* any other criteria to determine whether we should continue stepping or not.
*/
this.nBurstCycles = this.nStepCycles = nMinCycles;
/*
* NOTE: If checkINTR() returns false, INTFLAG.HALT must be set, so no instructions should be executed.
*/
if (this.checkINTR()) {
do {
if (DEBUGGER && fDebugCheck) {
if (this.dbg.checkInstruction(this.regPC, nDebugState)) {
this.stopCPU();
break;
}
nDebugState = 1;
}
this.aOps[this.getPCByte()].call(this);
} while (this.nStepCycles > 0);
}
return (this.flags.complete? this.nBurstCycles - this.nStepCycles : (this.flags.complete === undefined? 0 : -1));
}
/**
* CPUState8080.init()
*
* This function operates on every HTML element of class "cpu", extracting the
* JSON-encoded parameters for the CPUState8080 constructor from the element's "data-value"
* attribute, invoking the constructor (which in turn invokes the CPU constructor)
* to create a CPUState8080 component, and then binding any associated HTML controls to the
* new component.
*/
static init()
{
var aeCPUs = Component.getElementsByClass(document, PC8080.APPCLASS, "cpu");
for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) {
var eCPU = aeCPUs[iCPU];
var parmsCPU = Component.getComponentParms(eCPU);
var cpu = new CPUState8080(parmsCPU);
Component.bindComponentControls(cpu, eCPU, PC8080.APPCLASS);
}
}
}
/*
* Initialize every CPU module on the page
*/
Web.onInit(CPUState8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/cpuops.js (C) Jeff Parsons 2012-2017
*/
/**
* op=0x00 (NOP)
*
* @this {CPUState8080}
*/
CPUDef8080.opNOP = function()
{
this.nStepCycles -= 4;
};
/**
* op=0x01 (LXI B,d16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLXIB = function()
{
this.setBC(this.getPCWord());
this.nStepCycles -= 10;
};
/**
* op=0x02 (STAX B)
*
* @this {CPUState8080}
*/
CPUDef8080.opSTAXB = function()
{
this.setByte(this.getBC(), this.regA);
this.nStepCycles -= 7;
};
/**
* op=0x03 (INX B)
*
* @this {CPUState8080}
*/
CPUDef8080.opINXB = function()
{
this.setBC(this.getBC() + 1);
this.nStepCycles -= 5;
};
/**
* op=0x04 (INR B)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRB = function()
{
this.regB = this.incByte(this.regB);
this.nStepCycles -= 5;
};
/**
* op=0x05 (DCR B)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRB = function()
{
this.regB = this.decByte(this.regB);
this.nStepCycles -= 5;
};
/**
* op=0x06 (MVI B,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIB = function()
{
this.regB = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x07 (RLC)
*
* @this {CPUState8080}
*/
CPUDef8080.opRLC = function()
{
var carry = this.regA << 1;
this.regA = (carry & 0xff) | (carry >> 8);
this.updateCF(carry & 0x100);
this.nStepCycles -= 4;
};
/**
* op=0x09 (DAD B)
*
* @this {CPUState8080}
*/
CPUDef8080.opDADB = function()
{
var w;
this.setHL(w = this.getHL() + this.getBC());
this.updateCF((w >> 8) & 0x100);
this.nStepCycles -= 10;
};
/**
* op=0x0A (LDAX B)
*
* @this {CPUState8080}
*/
CPUDef8080.opLDAXB = function()
{
this.regA = this.getByte(this.getBC());
this.nStepCycles -= 7;
};
/**
* op=0x0B (DCX B)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCXB = function()
{
this.setBC(this.getBC() - 1);
this.nStepCycles -= 5;
};
/**
* op=0x0C (INR C)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRC = function()
{
this.regC = this.incByte(this.regC);
this.nStepCycles -= 5;
};
/**
* op=0x0D (DCR C)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRC = function()
{
this.regC = this.decByte(this.regC);
this.nStepCycles -= 5;
};
/**
* op=0x0E (MVI C,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIC = function()
{
this.regC = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x0F (RRC)
*
* @this {CPUState8080}
*/
CPUDef8080.opRRC = function()
{
var carry = (this.regA << 8) & 0x100;
this.regA = (carry | this.regA) >> 1;
this.updateCF(carry);
this.nStepCycles -= 4;
};
/**
* op=0x11 (LXI D,d16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLXID = function()
{
this.setDE(this.getPCWord());
this.nStepCycles -= 10;
};
/**
* op=0x12 (STAX D)
*
* @this {CPUState8080}
*/
CPUDef8080.opSTAXD = function()
{
this.setByte(this.getDE(), this.regA);
this.nStepCycles -= 7;
};
/**
* op=0x13 (INX D)
*
* @this {CPUState8080}
*/
CPUDef8080.opINXD = function()
{
this.setDE(this.getDE() + 1);
this.nStepCycles -= 5;
};
/**
* op=0x14 (INR D)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRD = function()
{
this.regD = this.incByte(this.regD);
this.nStepCycles -= 5;
};
/**
* op=0x15 (DCR D)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRD = function()
{
this.regD = this.decByte(this.regD);
this.nStepCycles -= 5;
};
/**
* op=0x16 (MVI D,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVID = function()
{
this.regD = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x17 (RAL)
*
* @this {CPUState8080}
*/
CPUDef8080.opRAL = function()
{
var carry = this.regA << 1;
this.regA = (carry & 0xff) | this.getCF();
this.updateCF(carry & 0x100);
this.nStepCycles -= 4;
};
/**
* op=0x19 (DAD D)
*
* @this {CPUState8080}
*/
CPUDef8080.opDADD = function()
{
var w;
this.setHL(w = this.getHL() + this.getDE());
this.updateCF((w >> 8) & 0x100);
this.nStepCycles -= 10;
};
/**
* op=0x1A (LDAX D)
*
* @this {CPUState8080}
*/
CPUDef8080.opLDAXD = function()
{
this.regA = this.getByte(this.getDE());
this.nStepCycles -= 7;
};
/**
* op=0x1B (DCX D)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCXD = function()
{
this.setDE(this.getDE() - 1);
this.nStepCycles -= 5;
};
/**
* op=0x1C (INR E)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRE = function()
{
this.regE = this.incByte(this.regE);
this.nStepCycles -= 5;
};
/**
* op=0x1D (DCR E)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRE = function()
{
this.regE = this.decByte(this.regE);
this.nStepCycles -= 5;
};
/**
* op=0x1E (MVI E,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIE = function()
{
this.regE = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x1F (RAR)
*
* @this {CPUState8080}
*/
CPUDef8080.opRAR = function()
{
var carry = (this.regA << 8);
this.regA = ((this.getCF() << 8) | this.regA) >> 1;
this.updateCF(carry & 0x100);
this.nStepCycles -= 4;
};
/**
* op=0x21 (LXI H,d16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLXIH = function()
{
this.setHL(this.getPCWord());
this.nStepCycles -= 10;
};
/**
* op=0x22 (SHLD a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opSHLD = function()
{
this.setWord(this.getPCWord(), this.getHL());
this.nStepCycles -= 16;
};
/**
* op=0x23 (INX H)
*
* @this {CPUState8080}
*/
CPUDef8080.opINXH = function()
{
this.setHL(this.getHL() + 1);
this.nStepCycles -= 5;
};
/**
* op=0x24 (INR H)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRH = function()
{
this.regH = this.incByte(this.regH);
this.nStepCycles -= 5;
};
/**
* op=0x25 (DCR H)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRH = function()
{
this.regH = this.decByte(this.regH);
this.nStepCycles -= 5;
};
/**
* op=0x26 (MVI H,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIH = function()
{
this.regH = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x27 (DAA)
*
* @this {CPUState8080}
*/
CPUDef8080.opDAA = function()
{
var src = 0;
var CF = this.getCF();
var AF = this.getAF();
if (AF || (this.regA & 0x0F) > 9) {
src |= 0x06;
}
if (CF || this.regA >= 0x9A) {
src |= 0x60;
CF = CPUDef8080.PS.CF;
}
this.regA = this.addByte(src);
this.updateCF(CF? 0x100 : 0);
this.nStepCycles -= 4;
};
/**
* op=0x29 (DAD H)
*
* @this {CPUState8080}
*/
CPUDef8080.opDADH = function()
{
var w;
this.setHL(w = this.getHL() + this.getHL());
this.updateCF((w >> 8) & 0x100);
this.nStepCycles -= 10;
};
/**
* op=0x2A (LHLD a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLHLD = function()
{
this.setHL(this.getWord(this.getPCWord()));
this.nStepCycles -= 16;
};
/**
* op=0x2B (DCX H)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCXH = function()
{
this.setHL(this.getHL() - 1);
this.nStepCycles -= 5;
};
/**
* op=0x2C (INR L)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRL = function()
{
this.regL = this.incByte(this.regL);
this.nStepCycles -= 5;
};
/**
* op=0x2D (DCR L)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRL = function()
{
this.regL = this.decByte(this.regL);
this.nStepCycles -= 5;
};
/**
* op=0x2E (MVI L,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIL = function()
{
this.regL = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x2F (CMA)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMA = function()
{
this.regA = ~this.regA & 0xff;
this.nStepCycles -= 4;
};
/**
* op=0x31 (LXI SP,d16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLXISP = function()
{
this.setSP(this.getPCWord());
this.nStepCycles -= 10;
};
/**
* op=0x32 (STA a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opSTA = function()
{
this.setByte(this.getPCWord(), this.regA);
this.nStepCycles -= 13;
};
/**
* op=0x33 (INX SP)
*
* @this {CPUState8080}
*/
CPUDef8080.opINXSP = function()
{
this.setSP(this.getSP() + 1);
this.nStepCycles -= 5;
};
/**
* op=0x34 (INR M)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRM = function()
{
var addr = this.getHL();
this.setByte(addr, this.incByte(this.getByte(addr)));
this.nStepCycles -= 10;
};
/**
* op=0x35 (DCR M)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRM = function()
{
var addr = this.getHL();
this.setByte(addr, this.decByte(this.getByte(addr)));
this.nStepCycles -= 10;
};
/**
* op=0x36 (MVI M,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIM = function()
{
this.setByte(this.getHL(), this.getPCByte());
this.nStepCycles -= 10;
};
/**
* op=0x37 (STC)
*
* @this {CPUState8080}
*/
CPUDef8080.opSTC = function()
{
this.setCF();
this.nStepCycles -= 4;
};
/**
* op=0x39 (DAD SP)
*
* @this {CPUState8080}
*/
CPUDef8080.opDADSP = function()
{
var w;
this.setHL(w = this.getHL() + this.getSP());
this.updateCF((w >> 8) & 0x100);
this.nStepCycles -= 10;
};
/**
* op=0x3A (LDA a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opLDA = function()
{
this.regA = this.getByte(this.getPCWord());
this.nStepCycles -= 13;
};
/**
* op=0x3B (DCX SP)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCXSP = function()
{
this.setSP(this.getSP() - 1);
this.nStepCycles -= 5;
};
/**
* op=0x3C (INR A)
*
* @this {CPUState8080}
*/
CPUDef8080.opINRA = function()
{
this.regA = this.incByte(this.regA);
this.nStepCycles -= 5;
};
/**
* op=0x3D (DCR A)
*
* @this {CPUState8080}
*/
CPUDef8080.opDCRA = function()
{
this.regA = this.decByte(this.regA);
this.nStepCycles -= 5;
};
/**
* op=0x3E (MVI A,d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opMVIA = function()
{
this.regA = this.getPCByte();
this.nStepCycles -= 7;
};
/**
* op=0x3F (CMC)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMC = function()
{
this.updateCF(this.getCF()? 0 : 0x100);
this.nStepCycles -= 4;
};
/**
* op=0x40 (MOV B,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBB = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x41 (MOV B,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBC = function()
{
this.regB = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x42 (MOV B,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBD = function()
{
this.regB = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x43 (MOV B,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBE = function()
{
this.regB = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x44 (MOV B,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBH = function()
{
this.regB = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x45 (MOV B,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBL = function()
{
this.regB = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x46 (MOV B,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBM = function()
{
this.regB = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x47 (MOV B,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVBA = function()
{
this.regB = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x48 (MOV C,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCB = function()
{
this.regC = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x49 (MOV C,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCC = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x4A (MOV C,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCD = function()
{
this.regC = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x4B (MOV C,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCE = function()
{
this.regC = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x4C (MOV C,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCH = function()
{
this.regC = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x4D (MOV C,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCL = function()
{
this.regC = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x4E (MOV C,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCM = function()
{
this.regC = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x4F (MOV C,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVCA = function()
{
this.regC = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x50 (MOV D,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDB = function()
{
this.regD = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x51 (MOV D,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDC = function()
{
this.regD = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x52 (MOV D,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDD = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x53 (MOV D,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDE = function()
{
this.regD = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x54 (MOV D,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDH = function()
{
this.regD = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x55 (MOV D,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDL = function()
{
this.regD = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x56 (MOV D,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDM = function()
{
this.regD = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x57 (MOV D,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVDA = function()
{
this.regD = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x58 (MOV E,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEB = function()
{
this.regE = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x59 (MOV E,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEC = function()
{
this.regE = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x5A (MOV E,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVED = function()
{
this.regE = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x5B (MOV E,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEE = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x5C (MOV E,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEH = function()
{
this.regE = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x5D (MOV E,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEL = function()
{
this.regE = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x5E (MOV E,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEM = function()
{
this.regE = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x5F (MOV E,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVEA = function()
{
this.regE = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x60 (MOV H,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHB = function()
{
this.regH = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x61 (MOV H,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHC = function()
{
this.regH = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x62 (MOV H,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHD = function()
{
this.regH = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x63 (MOV H,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHE = function()
{
this.regH = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x64 (MOV H,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHH = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x65 (MOV H,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHL = function()
{
this.regH = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x66 (MOV H,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHM = function()
{
this.regH = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x67 (MOV H,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVHA = function()
{
this.regH = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x68 (MOV L,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLB = function()
{
this.regL = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x69 (MOV L,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLC = function()
{
this.regL = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x6A (MOV L,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLD = function()
{
this.regL = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x6B (MOV L,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLE = function()
{
this.regL = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x6C (MOV L,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLH = function()
{
this.regL = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x6D (MOV L,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLL = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x6E (MOV L,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLM = function()
{
this.regL = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x6F (MOV L,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVLA = function()
{
this.regL = this.regA;
this.nStepCycles -= 5;
};
/**
* op=0x70 (MOV M,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVMB = function()
{
this.setByte(this.getHL(), this.regB);
this.nStepCycles -= 7;
};
/**
* op=0x71 (MOV M,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVMC = function()
{
this.setByte(this.getHL(), this.regC);
this.nStepCycles -= 7;
};
/**
* op=0x72 (MOV M,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVMD = function()
{
this.setByte(this.getHL(), this.regD);
this.nStepCycles -= 7;
};
/**
* op=0x73 (MOV M,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVME = function()
{
this.setByte(this.getHL(), this.regE);
this.nStepCycles -= 7;
};
/**
* op=0x74 (MOV M,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVMH = function()
{
this.setByte(this.getHL(), this.regH);
this.nStepCycles -= 7;
};
/**
* op=0x75 (MOV M,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVML = function()
{
this.setByte(this.getHL(), this.regL);
this.nStepCycles -= 7;
};
/**
* op=0x76 (HLT)
*
* @this {CPUState8080}
*/
CPUDef8080.opHLT = function()
{
var addr = this.getPC() - 1;
/*
* If any HLT check functions are installed, call them, and if any of them return true, then
* immediately stop HLT processing.
*/
if (this.afnHalt.length) {
for (var i = 0; i < this.afnHalt.length; i++) {
if (this.afnHalt[i](addr)) return;
}
}
this.nStepCycles -= 7;
/*
* The CPU is never REALLY halted by a HLT instruction; instead, we call requestHALT(), which
* signals to stepCPU() that it should end the current burst AND that it should not execute any
* more instructions until checkINTR() indicates a hardware interrupt has been requested.
*/
this.requestHALT();
/*
* If a Debugger is present and the HALT message category is enabled, then we REALLY halt the CPU,
* on the theory that whoever's using the Debugger would like to see HLTs.
*/
if (DEBUGGER && this.dbg && this.messageEnabled(Messages8080.HALT)) {
this.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT
this.dbg.stopCPU();
return;
}
/*
* We also REALLY halt the machine if interrupts have been disabled, since that means it's dead
* in the water (we have no NMI generation mechanism at the moment).
*/
if (!this.getIF()) {
if (DEBUGGER && this.dbg) this.setPC(addr);
this.stopCPU();
}
};
/**
* op=0x77 (MOV M,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVMA = function()
{
this.setByte(this.getHL(), this.regA);
this.nStepCycles -= 7;
};
/**
* op=0x78 (MOV A,B)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAB = function()
{
this.regA = this.regB;
this.nStepCycles -= 5;
};
/**
* op=0x79 (MOV A,C)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAC = function()
{
this.regA = this.regC;
this.nStepCycles -= 5;
};
/**
* op=0x7A (MOV A,D)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAD = function()
{
this.regA = this.regD;
this.nStepCycles -= 5;
};
/**
* op=0x7B (MOV A,E)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAE = function()
{
this.regA = this.regE;
this.nStepCycles -= 5;
};
/**
* op=0x7C (MOV A,H)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAH = function()
{
this.regA = this.regH;
this.nStepCycles -= 5;
};
/**
* op=0x7D (MOV A,L)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAL = function()
{
this.regA = this.regL;
this.nStepCycles -= 5;
};
/**
* op=0x7E (MOV A,M)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAM = function()
{
this.regA = this.getByte(this.getHL());
this.nStepCycles -= 7;
};
/**
* op=0x7F (MOV A,A)
*
* @this {CPUState8080}
*/
CPUDef8080.opMOVAA = function()
{
this.nStepCycles -= 5;
};
/**
* op=0x80 (ADD B)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDB = function()
{
this.regA = this.addByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0x81 (ADD C)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDC = function()
{
this.regA = this.addByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0x82 (ADD D)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDD = function()
{
this.regA = this.addByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0x83 (ADD E)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDE = function()
{
this.regA = this.addByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0x84 (ADD H)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDH = function()
{
this.regA = this.addByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0x85 (ADD L)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDL = function()
{
this.regA = this.addByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0x86 (ADD M)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDM = function()
{
this.regA = this.addByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0x87 (ADD A)
*
* @this {CPUState8080}
*/
CPUDef8080.opADDA = function()
{
this.regA = this.addByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0x88 (ADC B)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCB = function()
{
this.regA = this.addByteCarry(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0x89 (ADC C)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCC = function()
{
this.regA = this.addByteCarry(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0x8A (ADC D)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCD = function()
{
this.regA = this.addByteCarry(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0x8B (ADC E)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCE = function()
{
this.regA = this.addByteCarry(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0x8C (ADC H)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCH = function()
{
this.regA = this.addByteCarry(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0x8D (ADC L)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCL = function()
{
this.regA = this.addByteCarry(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0x8E (ADC M)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCM = function()
{
this.regA = this.addByteCarry(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0x8F (ADC A)
*
* @this {CPUState8080}
*/
CPUDef8080.opADCA = function()
{
this.regA = this.addByteCarry(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0x90 (SUB B)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBB = function()
{
this.regA = this.subByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0x91 (SUB C)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBC = function()
{
this.regA = this.subByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0x92 (SUB D)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBD = function()
{
this.regA = this.subByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0x93 (SUB E)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBE = function()
{
this.regA = this.subByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0x94 (SUB H)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBH = function()
{
this.regA = this.subByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0x95 (SUB L)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBL = function()
{
this.regA = this.subByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0x96 (SUB M)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBM = function()
{
this.regA = this.subByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0x97 (SUB A)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUBA = function()
{
this.regA = this.subByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0x98 (SBB B)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBB = function()
{
this.regA = this.subByteBorrow(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0x99 (SBB C)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBC = function()
{
this.regA = this.subByteBorrow(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0x9A (SBB D)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBD = function()
{
this.regA = this.subByteBorrow(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0x9B (SBB E)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBE = function()
{
this.regA = this.subByteBorrow(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0x9C (SBB H)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBH = function()
{
this.regA = this.subByteBorrow(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0x9D (SBB L)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBL = function()
{
this.regA = this.subByteBorrow(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0x9E (SBB M)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBM = function()
{
this.regA = this.subByteBorrow(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0x9F (SBB A)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBBA = function()
{
this.regA = this.subByteBorrow(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0xA0 (ANA B)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAB = function()
{
this.regA = this.andByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0xA1 (ANA C)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAC = function()
{
this.regA = this.andByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0xA2 (ANA D)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAD = function()
{
this.regA = this.andByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0xA3 (ANA E)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAE = function()
{
this.regA = this.andByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0xA4 (ANA H)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAH = function()
{
this.regA = this.andByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0xA5 (ANA L)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAL = function()
{
this.regA = this.andByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0xA6 (ANA M)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAM = function()
{
this.regA = this.andByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0xA7 (ANA A)
*
* @this {CPUState8080}
*/
CPUDef8080.opANAA = function()
{
this.regA = this.andByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0xA8 (XRA B)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAB = function()
{
this.regA = this.xorByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0xA9 (XRA C)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAC = function()
{
this.regA = this.xorByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0xAA (XRA D)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAD = function()
{
this.regA = this.xorByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0xAB (XRA E)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAE = function()
{
this.regA = this.xorByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0xAC (XRA H)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAH = function()
{
this.regA = this.xorByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0xAD (XRA L)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAL = function()
{
this.regA = this.xorByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0xAE (XRA M)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAM = function()
{
this.regA = this.xorByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0xAF (XRA A)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRAA = function()
{
this.regA = this.xorByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0xB0 (ORA B)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAB = function()
{
this.regA = this.orByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0xB1 (ORA C)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAC = function()
{
this.regA = this.orByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0xB2 (ORA D)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAD = function()
{
this.regA = this.orByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0xB3 (ORA E)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAE = function()
{
this.regA = this.orByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0xB4 (ORA H)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAH = function()
{
this.regA = this.orByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0xB5 (ORA L)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAL = function()
{
this.regA = this.orByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0xB6 (ORA M)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAM = function()
{
this.regA = this.orByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0xB7 (ORA A)
*
* @this {CPUState8080}
*/
CPUDef8080.opORAA = function()
{
this.regA = this.orByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0xB8 (CMP B)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPB = function()
{
this.subByte(this.regB);
this.nStepCycles -= 4;
};
/**
* op=0xB9 (CMP C)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPC = function()
{
this.subByte(this.regC);
this.nStepCycles -= 4;
};
/**
* op=0xBA (CMP D)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPD = function()
{
this.subByte(this.regD);
this.nStepCycles -= 4;
};
/**
* op=0xBB (CMP E)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPE = function()
{
this.subByte(this.regE);
this.nStepCycles -= 4;
};
/**
* op=0xBC (CMP H)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPH = function()
{
this.subByte(this.regH);
this.nStepCycles -= 4;
};
/**
* op=0xBD (CMP L)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPL = function()
{
this.subByte(this.regL);
this.nStepCycles -= 4;
};
/**
* op=0xBE (CMP M)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPM = function()
{
this.subByte(this.getByte(this.getHL()));
this.nStepCycles -= 7;
};
/**
* op=0xBF (CMP A)
*
* @this {CPUState8080}
*/
CPUDef8080.opCMPA = function()
{
this.subByte(this.regA);
this.nStepCycles -= 4;
};
/**
* op=0xC0 (RNZ)
*
* @this {CPUState8080}
*/
CPUDef8080.opRNZ = function()
{
if (!this.getZF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xC1 (POP B)
*
* @this {CPUState8080}
*/
CPUDef8080.opPOPB = function()
{
this.setBC(this.popWord());
this.nStepCycles -= 10;
};
/**
* op=0xC2 (JNZ a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJNZ = function()
{
var w = this.getPCWord();
if (!this.getZF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xC3 (JMP a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJMP = function()
{
this.setPC(this.getPCWord());
this.nStepCycles -= 10;
};
/**
* op=0xC4 (CNZ a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCNZ = function()
{
var w = this.getPCWord();
if (!this.getZF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xC5 (PUSH B)
*
* @this {CPUState8080}
*/
CPUDef8080.opPUSHB = function()
{
this.pushWord(this.getBC());
this.nStepCycles -= 11;
};
/**
* op=0xC6 (ADI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opADI = function()
{
this.regA = this.addByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xC7 (RST 0)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST0 = function()
{
this.pushWord(this.getPC());
this.setPC(0);
this.nStepCycles -= 11;
};
/**
* op=0xC8 (RZ)
*
* @this {CPUState8080}
*/
CPUDef8080.opRZ = function()
{
if (this.getZF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xC9 (RET)
*
* @this {CPUState8080}
*/
CPUDef8080.opRET = function()
{
this.setPC(this.popWord());
this.nStepCycles -= 10;
};
/**
* op=0xCA (JZ a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJZ = function()
{
var w = this.getPCWord();
if (this.getZF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xCC (CZ a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCZ = function()
{
var w = this.getPCWord();
if (this.getZF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xCD (CALL a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCALL = function()
{
var w = this.getPCWord();
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 17;
};
/**
* op=0xCE (ACI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opACI = function()
{
this.regA = this.addByteCarry(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xCF (RST 1)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST1 = function()
{
this.pushWord(this.getPC());
this.setPC(0x08);
this.nStepCycles -= 11;
};
/**
* op=0xD0 (RNC)
*
* @this {CPUState8080}
*/
CPUDef8080.opRNC = function()
{
if (!this.getCF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xD1 (POP D)
*
* @this {CPUState8080}
*/
CPUDef8080.opPOPD = function()
{
this.setDE(this.popWord());
this.nStepCycles -= 10;
};
/**
* op=0xD2 (JNC a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJNC = function()
{
var w = this.getPCWord();
if (!this.getCF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xD3 (OUT d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opOUT = function()
{
var port = this.getPCByte();
this.bus.checkPortOutputNotify(port, 1, this.regA, this.offPC(-2));
this.nStepCycles -= 10;
};
/**
* op=0xD4 (CNC a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCNC = function()
{
var w = this.getPCWord();
if (!this.getCF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xD5 (PUSH D)
*
* @this {CPUState8080}
*/
CPUDef8080.opPUSHD = function()
{
this.pushWord(this.getDE());
this.nStepCycles -= 11;
};
/**
* op=0xD6 (SUI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opSUI = function()
{
this.regA = this.subByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xD7 (RST 2)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST2 = function()
{
this.pushWord(this.getPC());
this.setPC(0x10);
this.nStepCycles -= 11;
};
/**
* op=0xD8 (RC)
*
* @this {CPUState8080}
*/
CPUDef8080.opRC = function()
{
if (this.getCF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xDA (JC a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJC = function()
{
var w = this.getPCWord();
if (this.getCF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xDB (IN d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opIN = function()
{
var port = this.getPCByte();
this.regA = this.bus.checkPortInputNotify(port, 1, this.offPC(-2)) & 0xff;
this.nStepCycles -= 10;
};
/**
* op=0xDC (CC a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCC = function()
{
var w = this.getPCWord();
if (this.getCF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xDE (SBI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opSBI = function()
{
this.regA = this.subByteBorrow(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xDF (RST 3)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST3 = function()
{
this.pushWord(this.getPC());
this.setPC(0x18);
this.nStepCycles -= 11;
};
/**
* op=0xE0 (RPO)
*
* @this {CPUState8080}
*/
CPUDef8080.opRPO = function()
{
if (!this.getPF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xE1 (POP H)
*
* @this {CPUState8080}
*/
CPUDef8080.opPOPH = function()
{
this.setHL(this.popWord());
this.nStepCycles -= 10;
};
/**
* op=0xE2 (JPO a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJPO = function()
{
var w = this.getPCWord();
if (!this.getPF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xE3 (XTHL)
*
* @this {CPUState8080}
*/
CPUDef8080.opXTHL = function()
{
var w = this.popWord();
this.pushWord(this.getHL());
this.setHL(w);
this.nStepCycles -= 18;
};
/**
* op=0xE4 (CPO a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCPO = function()
{
var w = this.getPCWord();
if (!this.getPF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xE5 (PUSH H)
*
* @this {CPUState8080}
*/
CPUDef8080.opPUSHH = function()
{
this.pushWord(this.getHL());
this.nStepCycles -= 11;
};
/**
* op=0xE6 (ANI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opANI = function()
{
this.regA = this.andByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xE7 (RST 4)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST4 = function()
{
this.pushWord(this.getPC());
this.setPC(0x20);
this.nStepCycles -= 11;
};
/**
* op=0xE8 (RPE)
*
* @this {CPUState8080}
*/
CPUDef8080.opRPE = function()
{
if (this.getPF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xE9 (PCHL)
*
* @this {CPUState8080}
*/
CPUDef8080.opPCHL = function()
{
this.setPC(this.getHL());
this.nStepCycles -= 5;
};
/**
* op=0xEA (JPE a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJPE = function()
{
var w = this.getPCWord();
if (this.getPF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xEB (XCHG)
*
* @this {CPUState8080}
*/
CPUDef8080.opXCHG = function()
{
var w = this.getHL();
this.setHL(this.getDE());
this.setDE(w);
this.nStepCycles -= 5;
};
/**
* op=0xEC (CPE a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCPE = function()
{
var w = this.getPCWord();
if (this.getPF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xEE (XRI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opXRI = function()
{
this.regA = this.xorByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xEF (RST 5)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST5 = function()
{
this.pushWord(this.getPC());
this.setPC(0x28);
this.nStepCycles -= 11;
};
/**
* op=0xF0 (RP)
*
* @this {CPUState8080}
*/
CPUDef8080.opRP = function()
{
if (!this.getSF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xF1 (POP PSW)
*
* @this {CPUState8080}
*/
CPUDef8080.opPOPSW = function()
{
this.setPSW(this.popWord());
this.nStepCycles -= 10;
};
/**
* op=0xF2 (JP a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJP = function()
{
var w = this.getPCWord();
if (!this.getSF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xF3 (DI)
*
* @this {CPUState8080}
*/
CPUDef8080.opDI = function()
{
this.clearIF();
this.nStepCycles -= 4;
};
/**
* op=0xF4 (CP a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCP = function()
{
var w = this.getPCWord();
if (!this.getSF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xF5 (PUSH PSW)
*
* @this {CPUState8080}
*/
CPUDef8080.opPUPSW = function()
{
this.pushWord(this.getPSW());
this.nStepCycles -= 11;
};
/**
* op=0xF6 (ORI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opORI = function()
{
this.regA = this.orByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xF7 (RST 6)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST6 = function()
{
this.pushWord(this.getPC());
this.setPC(0x30);
this.nStepCycles -= 11;
};
/**
* op=0xF8 (RM)
*
* @this {CPUState8080}
*/
CPUDef8080.opRM = function()
{
if (this.getSF()) {
this.setPC(this.popWord());
this.nStepCycles -= 6;
}
this.nStepCycles -= 5;
};
/**
* op=0xF9 (SPHL)
*
* @this {CPUState8080}
*/
CPUDef8080.opSPHL = function()
{
this.setSP(this.getHL());
this.nStepCycles -= 5;
};
/**
* op=0xFA (JM a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opJM = function()
{
var w = this.getPCWord();
if (this.getSF()) this.setPC(w);
this.nStepCycles -= 10;
};
/**
* op=0xFB (EI)
*
* @this {CPUState8080}
*/
CPUDef8080.opEI = function()
{
this.setIF();
this.nStepCycles -= 4;
this.checkINTR();
};
/**
* op=0xFC (CM a16)
*
* @this {CPUState8080}
*/
CPUDef8080.opCM = function()
{
var w = this.getPCWord();
if (this.getSF()) {
this.pushWord(this.getPC());
this.setPC(w);
this.nStepCycles -= 6;
}
this.nStepCycles -= 11;
};
/**
* op=0xFE (CPI d8)
*
* @this {CPUState8080}
*/
CPUDef8080.opCPI = function()
{
this.subByte(this.getPCByte());
this.nStepCycles -= 7;
};
/**
* op=0xFF (RST 7)
*
* @this {CPUState8080}
*/
CPUDef8080.opRST7 = function()
{
this.pushWord(this.getPC());
this.setPC(0x38);
this.nStepCycles -= 11;
};
/*
* This 256-entry array of opcode functions is at the heart of the CPU engine: stepCPU(n).
*
* It might be worth trying a switch() statement instead, to see how the performance compares,
* but I suspect that would vary quite a bit across JavaScript engines; for now, I'm putting my
* money on array lookup.
*/
CPUDef8080.aOps8080 = [
/* 0x00-0x03 */ CPUDef8080.opNOP, CPUDef8080.opLXIB, CPUDef8080.opSTAXB, CPUDef8080.opINXB,
/* 0x04-0x07 */ CPUDef8080.opINRB, CPUDef8080.opDCRB, CPUDef8080.opMVIB, CPUDef8080.opRLC,
/* 0x08-0x0B */ CPUDef8080.opNOP, CPUDef8080.opDADB, CPUDef8080.opLDAXB, CPUDef8080.opDCXB,
/* 0x0C-0x0F */ CPUDef8080.opINRC, CPUDef8080.opDCRC, CPUDef8080.opMVIC, CPUDef8080.opRRC,
/* 0x10-0x13 */ CPUDef8080.opNOP, CPUDef8080.opLXID, CPUDef8080.opSTAXD, CPUDef8080.opINXD,
/* 0x14-0x17 */ CPUDef8080.opINRD, CPUDef8080.opDCRD, CPUDef8080.opMVID, CPUDef8080.opRAL,
/* 0x18-0x1B */ CPUDef8080.opNOP, CPUDef8080.opDADD, CPUDef8080.opLDAXD, CPUDef8080.opDCXD,
/* 0x1C-0x1F */ CPUDef8080.opINRE, CPUDef8080.opDCRE, CPUDef8080.opMVIE, CPUDef8080.opRAR,
/* 0x20-0x23 */ CPUDef8080.opNOP, CPUDef8080.opLXIH, CPUDef8080.opSHLD, CPUDef8080.opINXH,
/* 0x24-0x27 */ CPUDef8080.opINRH, CPUDef8080.opDCRH, CPUDef8080.opMVIH, CPUDef8080.opDAA,
/* 0x28-0x2B */ CPUDef8080.opNOP, CPUDef8080.opDADH, CPUDef8080.opLHLD, CPUDef8080.opDCXH,
/* 0x2C-0x2F */ CPUDef8080.opINRL, CPUDef8080.opDCRL, CPUDef8080.opMVIL, CPUDef8080.opCMA,
/* 0x30-0x33 */ CPUDef8080.opNOP, CPUDef8080.opLXISP, CPUDef8080.opSTA, CPUDef8080.opINXSP,
/* 0x34-0x37 */ CPUDef8080.opINRM, CPUDef8080.opDCRM, CPUDef8080.opMVIM, CPUDef8080.opSTC,
/* 0x38-0x3B */ CPUDef8080.opNOP, CPUDef8080.opDADSP, CPUDef8080.opLDA, CPUDef8080.opDCXSP,
/* 0x3C-0x3F */ CPUDef8080.opINRA, CPUDef8080.opDCRA, CPUDef8080.opMVIA, CPUDef8080.opCMC,
/* 0x40-0x43 */ CPUDef8080.opMOVBB, CPUDef8080.opMOVBC, CPUDef8080.opMOVBD, CPUDef8080.opMOVBE,
/* 0x44-0x47 */ CPUDef8080.opMOVBH, CPUDef8080.opMOVBL, CPUDef8080.opMOVBM, CPUDef8080.opMOVBA,
/* 0x48-0x4B */ CPUDef8080.opMOVCB, CPUDef8080.opMOVCC, CPUDef8080.opMOVCD, CPUDef8080.opMOVCE,
/* 0x4C-0x4F */ CPUDef8080.opMOVCH, CPUDef8080.opMOVCL, CPUDef8080.opMOVCM, CPUDef8080.opMOVCA,
/* 0x50-0x53 */ CPUDef8080.opMOVDB, CPUDef8080.opMOVDC, CPUDef8080.opMOVDD, CPUDef8080.opMOVDE,
/* 0x54-0x57 */ CPUDef8080.opMOVDH, CPUDef8080.opMOVDL, CPUDef8080.opMOVDM, CPUDef8080.opMOVDA,
/* 0x58-0x5B */ CPUDef8080.opMOVEB, CPUDef8080.opMOVEC, CPUDef8080.opMOVED, CPUDef8080.opMOVEE,
/* 0x5C-0x5F */ CPUDef8080.opMOVEH, CPUDef8080.opMOVEL, CPUDef8080.opMOVEM, CPUDef8080.opMOVEA,
/* 0x60-0x63 */ CPUDef8080.opMOVHB, CPUDef8080.opMOVHC, CPUDef8080.opMOVHD, CPUDef8080.opMOVHE,
/* 0x64-0x67 */ CPUDef8080.opMOVHH, CPUDef8080.opMOVHL, CPUDef8080.opMOVHM, CPUDef8080.opMOVHA,
/* 0x68-0x6B */ CPUDef8080.opMOVLB, CPUDef8080.opMOVLC, CPUDef8080.opMOVLD, CPUDef8080.opMOVLE,
/* 0x6C-0x6F */ CPUDef8080.opMOVLH, CPUDef8080.opMOVLL, CPUDef8080.opMOVLM, CPUDef8080.opMOVLA,
/* 0x70-0x73 */ CPUDef8080.opMOVMB, CPUDef8080.opMOVMC, CPUDef8080.opMOVMD, CPUDef8080.opMOVME,
/* 0x74-0x77 */ CPUDef8080.opMOVMH, CPUDef8080.opMOVML, CPUDef8080.opHLT, CPUDef8080.opMOVMA,
/* 0x78-0x7B */ CPUDef8080.opMOVAB, CPUDef8080.opMOVAC, CPUDef8080.opMOVAD, CPUDef8080.opMOVAE,
/* 0x7C-0x7F */ CPUDef8080.opMOVAH, CPUDef8080.opMOVAL, CPUDef8080.opMOVAM, CPUDef8080.opMOVAA,
/* 0x80-0x83 */ CPUDef8080.opADDB, CPUDef8080.opADDC, CPUDef8080.opADDD, CPUDef8080.opADDE,
/* 0x84-0x87 */ CPUDef8080.opADDH, CPUDef8080.opADDL, CPUDef8080.opADDM, CPUDef8080.opADDA,
/* 0x88-0x8B */ CPUDef8080.opADCB, CPUDef8080.opADCC, CPUDef8080.opADCD, CPUDef8080.opADCE,
/* 0x8C-0x8F */ CPUDef8080.opADCH, CPUDef8080.opADCL, CPUDef8080.opADCM, CPUDef8080.opADCA,
/* 0x90-0x93 */ CPUDef8080.opSUBB, CPUDef8080.opSUBC, CPUDef8080.opSUBD, CPUDef8080.opSUBE,
/* 0x94-0x97 */ CPUDef8080.opSUBH, CPUDef8080.opSUBL, CPUDef8080.opSUBM, CPUDef8080.opSUBA,
/* 0x98-0x9B */ CPUDef8080.opSBBB, CPUDef8080.opSBBC, CPUDef8080.opSBBD, CPUDef8080.opSBBE,
/* 0x9C-0x9F */ CPUDef8080.opSBBH, CPUDef8080.opSBBL, CPUDef8080.opSBBM, CPUDef8080.opSBBA,
/* 0xA0-0xA3 */ CPUDef8080.opANAB, CPUDef8080.opANAC, CPUDef8080.opANAD, CPUDef8080.opANAE,
/* 0xA4-0xA7 */ CPUDef8080.opANAH, CPUDef8080.opANAL, CPUDef8080.opANAM, CPUDef8080.opANAA,
/* 0xA8-0xAB */ CPUDef8080.opXRAB, CPUDef8080.opXRAC, CPUDef8080.opXRAD, CPUDef8080.opXRAE,
/* 0xAC-0xAF */ CPUDef8080.opXRAH, CPUDef8080.opXRAL, CPUDef8080.opXRAM, CPUDef8080.opXRAA,
/* 0xB0-0xB3 */ CPUDef8080.opORAB, CPUDef8080.opORAC, CPUDef8080.opORAD, CPUDef8080.opORAE,
/* 0xB4-0xB7 */ CPUDef8080.opORAH, CPUDef8080.opORAL, CPUDef8080.opORAM, CPUDef8080.opORAA,
/* 0xB8-0xBB */ CPUDef8080.opCMPB, CPUDef8080.opCMPC, CPUDef8080.opCMPD, CPUDef8080.opCMPE,
/* 0xBC-0xBF */ CPUDef8080.opCMPH, CPUDef8080.opCMPL, CPUDef8080.opCMPM, CPUDef8080.opCMPA,
/* 0xC0-0xC3 */ CPUDef8080.opRNZ, CPUDef8080.opPOPB, CPUDef8080.opJNZ, CPUDef8080.opJMP,
/* 0xC4-0xC7 */ CPUDef8080.opCNZ, CPUDef8080.opPUSHB, CPUDef8080.opADI, CPUDef8080.opRST0,
/* 0xC8-0xCB */ CPUDef8080.opRZ, CPUDef8080.opRET, CPUDef8080.opJZ, CPUDef8080.opJMP,
/* 0xCC-0xCF */ CPUDef8080.opCZ, CPUDef8080.opCALL, CPUDef8080.opACI, CPUDef8080.opRST1,
/* 0xD0-0xD3 */ CPUDef8080.opRNC, CPUDef8080.opPOPD, CPUDef8080.opJNC, CPUDef8080.opOUT,
/* 0xD4-0xD7 */ CPUDef8080.opCNC, CPUDef8080.opPUSHD, CPUDef8080.opSUI, CPUDef8080.opRST2,
/* 0xD8-0xDB */ CPUDef8080.opRC, CPUDef8080.opRET, CPUDef8080.opJC, CPUDef8080.opIN,
/* 0xDC-0xDF */ CPUDef8080.opCC, CPUDef8080.opCALL, CPUDef8080.opSBI, CPUDef8080.opRST3,
/* 0xE0-0xE3 */ CPUDef8080.opRPO, CPUDef8080.opPOPH, CPUDef8080.opJPO, CPUDef8080.opXTHL,
/* 0xE4-0xE7 */ CPUDef8080.opCPO, CPUDef8080.opPUSHH, CPUDef8080.opANI, CPUDef8080.opRST4,
/* 0xE8-0xEB */ CPUDef8080.opRPE, CPUDef8080.opPCHL, CPUDef8080.opJPE, CPUDef8080.opXCHG,
/* 0xEC-0xEF */ CPUDef8080.opCPE, CPUDef8080.opCALL, CPUDef8080.opXRI, CPUDef8080.opRST5,
/* 0xF0-0xF3 */ CPUDef8080.opRP, CPUDef8080.opPOPSW, CPUDef8080.opJP, CPUDef8080.opDI,
/* 0xF4-0xF7 */ CPUDef8080.opCP, CPUDef8080.opPUPSW, CPUDef8080.opORI, CPUDef8080.opRST6,
/* 0xF8-0xFB */ CPUDef8080.opRM, CPUDef8080.opSPHL, CPUDef8080.opJM, CPUDef8080.opEI,
/* 0xFC-0xFF */ CPUDef8080.opCM, CPUDef8080.opCALL, CPUDef8080.opCPI, CPUDef8080.opRST7
];
/**
* @copyright http://pcjs.org/modules/pc8080/lib/chipset.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class ChipSet8080 extends Component {
/**
* ChipSet8080(parmsChipSet)
*
* The ChipSet8080 component has the following component-specific (parmsChipSet) properties:
*
* model: eg, "SI1978" (should be a member of ChipSet8080.MODELS)
* swDIP: eg, "00000000", where swDIP[0] is DIP0, swDIP[1] is DIP1, etc.
*
* @this {ChipSet8080}
* @param {Object} parmsChipSet
*/
constructor(parmsChipSet)
{
super("ChipSet", parmsChipSet, Messages8080.CHIPSET);
var model = parmsChipSet['model'];
if (model && !ChipSet8080.MODELS[model]) {
Component.notice("Unrecognized ChipSet model: " + model);
}
this.config = ChipSet8080.MODELS[model] || {};
this.bSwitches = this.parseDIPSwitches(parmsChipSet['swDIP']);
/*
* Here, I'm finally getting around to trying the Web Audio API. Fortunately, based on what little I know about
* sound generation, using the API to make the same noises as the IBM PC speaker seems straightforward.
*
* To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false.
*
* From:
*
* http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html
*
* "Similar to how HTML5 canvas requires a context on which lines and curves are drawn, Web Audio requires an audio context
* on which sounds are played and manipulated. This context will be the parent object of further audio objects to come....
* Your audio context is typically created when your page initializes and should be long-lived. You can play multiple sounds
* coming from multiple sources within the same context, so it is unnecessary to create more than one audio context per page."
*/
this.fSpeaker = false;
if (parmsChipSet['sound']) {
this.classAudio = this.contextAudio = null;
if (window) {
this.classAudio = window['AudioContext'] || window['webkitAudioContext'];
}
if (this.classAudio) {
this.contextAudio = new this.classAudio();
} else {
if (DEBUG) this.log("AudioContext not available");
}
}
this.setReady();
}
/**
* parseDIPSwitches(sBits, bDefault)
*
* @this {ChipSet8080}
* @param {string} sBits describing switch settings
* @param {number} [bDefault]
* @return {number|undefined}
*/
parseDIPSwitches(sBits, bDefault)
{
var b = bDefault;
if (sBits) {
/*
* NOTE: We can't use parseInt() with a base of 2, because both bit order and bit sense are reversed.
*/
b = 0;
var bit = 0x1;
for (var i = 0; i < sBits.length; i++) {
if (sBits.charAt(i) == "0") b |= bit;
bit <<= 1;
}
}
return b;
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {ChipSet8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "sw1")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {ChipSet8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.cmp = cmp;
this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard"));
this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort"));
this.video = /** @type {Video8080} */ (cmp.getMachineComponent("Video"));
bus.addPortInputTable(this, this.config.portsInput);
bus.addPortOutputTable(this, this.config.portsOutput);
if (DEBUGGER) {
if (dbg) {
var chipset = this;
dbg.messageDump(Messages8080.NVR, function onDumpNVR() {
chipset.dumpNVR();
});
}
}
}
/**
* powerUp(data, fRepower)
*
* @this {ChipSet8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {ChipSet8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* dumpNVR()
*
* @this {ChipSet8080}
*/
dumpNVR()
{
if (DEBUGGER) {
var sDump = "";
for (var iWord = 0; iWord < this.aNVRWords.length; iWord++) {
if (sDump) {
sDump += (iWord && (iWord % 10)? ", " : ",\n");
}
sDump += Str.toHexWord(this.aNVRWords[iWord]);
}
this.dbg.println(sDump);
}
}
/**
* reset()
*
* @this {ChipSet8080}
*/
reset()
{
if (this.config.INIT && !this.restore(this.config.INIT)) {
this.notice("reset error");
}
}
/**
* save()
*
* This implements save support for the ChipSet component.
*
* @this {ChipSet8080}
* @return {Object}
*/
save()
{
var state = new State(this);
switch(this.config.MODEL) {
case ChipSet8080.SI1978.MODEL:
state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.wShiftData, this.bShiftCount, this.bSound1, this.bSound2]);
break;
case ChipSet8080.VT100.MODEL:
state.set(0, [this.bBrightness, this.bFlags]);
state.set(1, [this.bDC011Cols, this.bDC011Rate]);
state.set(2, [this.bDC012Scroll, this.bDC012Blink, this.bDC012Reverse, this.bDC012Attr]);
state.set(3, [this.dNVRAddr, this.wNVRData, this.bNVRLatch, this.bNVROut, this.aNVRWords]);
break;
}
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the ChipSet component.
*
* @this {ChipSet8080}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var a;
if (data && (a = data[0]) && a.length) {
switch(this.config.MODEL) {
case ChipSet8080.SI1978.MODEL:
this.bStatus0 = a[0];
this.bStatus1 = a[1];
this.bStatus2 = a[2];
this.wShiftData = a[3];
this.bShiftCount = a[4];
this.bSound1 = a[5];
this.bSound2 = a[6];
return true;
case ChipSet8080.VT100.MODEL:
this.bBrightness = a[0];
this.bFlags = a[1];
a = data[1];
this.bDC011Cols = a[0];
this.bDC011Rate = a[1];
a = data[2];
this.bDC012Scroll = a[0];
this.bDC012Blink = a[1];
this.bDC012Reverse = a[2];
this.bDC012Attr = a[3];
a = data[3];
this.dNVRAddr = a[0]; // 20-bit address
this.wNVRData = a[1]; // 14-bit word
this.bNVRLatch = a[2]; // 1 byte
this.bNVROut = a[3]; // 1 bit
this.aNVRWords = a[4]; // 100 14-bit words
return true;
}
}
return false;
}
/**
* start()
*
* Notification from the CPU that it's starting.
*
* @this {ChipSet8080}
*/
start()
{
/*
* Currently, all we (may) do with this notification is allow the speaker to make noise.
*/
}
/**
* stop()
*
* Notification from the CPU that it's stopping.
*
* @this {ChipSet8080}
*/
stop()
{
/*
* Currently, all we (may) do with this notification is prevent the speaker from making noise.
*/
}
/**
* updateStatus0(bit, fSet)
*
* @this {ChipSet8080}
* @param {number} bit
* @param {boolean} fSet
*/
updateStatus0(bit, fSet)
{
this.bStatus0 &= ~bit;
if (fSet) this.bStatus0 |= bit;
}
/**
* updateStatus1(bit, fSet)
*
* @this {ChipSet8080}
* @param {number} bit
* @param {boolean} fSet
*/
updateStatus1(bit, fSet)
{
this.bStatus1 &= ~bit;
if (fSet) this.bStatus1 |= bit;
}
/**
* updateStatus2(bit, fSet)
*
* @this {ChipSet8080}
* @param {number} bit
* @param {boolean} fSet
*/
updateStatus2(bit, fSet)
{
this.bStatus2 &= ~bit;
if (fSet) this.bStatus2 |= bit;
}
/**
* inSIStatus0(port, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x00)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
inSIStatus0(port, addrFrom)
{
var b = this.bStatus0;
this.printMessageIO(port, null, addrFrom, "STATUS0", b, true);
return b;
}
/**
* inSIStatus1(port, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x01)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
inSIStatus1(port, addrFrom)
{
var b = this.bStatus1;
this.printMessageIO(port, null, addrFrom, "STATUS1", b, true);
return b;
}
/**
* inSIStatus2(port, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x02)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
inSIStatus2(port, addrFrom)
{
var b = this.bStatus2;
this.printMessageIO(port, null, addrFrom, "STATUS2", b, true);
return b;
}
/**
* inSIShiftResult(port, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x03)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
inSIShiftResult(port, addrFrom)
{
var b = (this.wShiftData >> (8 - this.bShiftCount)) & 0xff;
this.printMessageIO(port, null, addrFrom, "SHIFT.RESULT", b, true);
return b;
}
/**
* outSIShiftCount(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x02)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outSIShiftCount(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SHIFT.COUNT", null, true);
this.bShiftCount = b;
}
/**
* outSISound1(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x03)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outSISound1(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SOUND1", null, true);
this.bSound1 = b;
}
/**
* outSIShiftData(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x04)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outSIShiftData(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SHIFT.DATA", null, true);
this.wShiftData = (b << 8) | (this.wShiftData >> 8);
}
/**
* outSISound2(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x05)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outSISound2(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SOUND2", null, true);
this.bSound2 = b;
}
/**
* outSIWatchdog(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x06)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outSIWatchdog(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "WATCHDOG", null, true);
}
/**
* getVT100LBA(iBit)
*
* Returns the state of the requested (simulated) LBA bit.
*
* NOTE: This is currently only used to obtain LBA7, which we approximate with the slightly faster approach
* of masking bit 6 of the CPU cycle count (see the DC011 discussion above). This will result in a shorter LBA7
* period than if we divided the cycle count by 88, but a shorter LBA7 period is probably helpful in terms of
* overall performance.
*
* @param {number} iBit
* @return {number}
*/
getVT100LBA(iBit)
{
return (this.cpu.getCycles() & (1 << (iBit - 1))) << 1;
}
/**
* getNVRAddr()
*
* @return {number}
*/
getNVRAddr()
{
var i;
var tens = 0, ones = 0;
var addr = ~this.dNVRAddr;
for (i = 0; i < 10; i++) {
if (addr & 0x1) tens = 9-i;
addr >>= 1;
}
for (i = 0; i < 10; i++) {
if (addr & 0x1) ones = 9-i;
addr >>= 1;
}
addr = tens*10 + ones;
return addr;
}
/**
* doNVRCommand()
*/
doNVRCommand()
{
var addr, data;
var bit = this.bNVRLatch & 0x1;
var bCmd = (this.bNVRLatch >> 1) & 0x7;
switch(bCmd) {
case ChipSet8080.VT100.NVR.CMD.STANDBY:
break;
case ChipSet8080.VT100.NVR.CMD.ACCEPT_ADDR:
this.dNVRAddr = (this.dNVRAddr << 1) | bit;
break;
case ChipSet8080.VT100.NVR.CMD.ERASE:
addr = this.getNVRAddr();
this.aNVRWords[addr] = ChipSet8080.VT100.NVR.WORDMASK;
this.printMessage("doNVRCommand(): erase data at addr " + Str.toHexWord(addr));
break;
case ChipSet8080.VT100.NVR.CMD.ACCEPT_DATA:
this.wNVRData = (this.wNVRData << 1) | bit;
break;
case ChipSet8080.VT100.NVR.CMD.WRITE:
addr = this.getNVRAddr();
data = this.wNVRData & ChipSet8080.VT100.NVR.WORDMASK;
this.aNVRWords[addr] = data;
this.printMessage("doNVRCommand(): write data " + Str.toHexWord(data) + " to addr " + Str.toHexWord(addr));
break;
case ChipSet8080.VT100.NVR.CMD.READ:
addr = this.getNVRAddr();
data = this.aNVRWords[addr];
/*
* If we don't explicitly initialize aNVRWords[], pretend any uninitialized words contains WORDMASK.
*/
if (data == null) data = ChipSet8080.VT100.NVR.WORDMASK;
this.wNVRData = data;
this.printMessage("doNVRCommand(): read data " + Str.toHexWord(data) + " from addr " + Str.toHexWord(addr));
break;
case ChipSet8080.VT100.NVR.CMD.SHIFT_OUT:
this.wNVRData <<= 1;
/*
* Since WORDMASK is 0x3fff, this will mask the shifted data with 0x4000, which is the bit we want to isolate.
*/
this.bNVROut = this.wNVRData & (ChipSet8080.VT100.NVR.WORDMASK + 1);
break;
default:
this.printMessage("doNVRCommand(): unrecognized command " + Str.toHexByte(bCmd));
break;
}
}
/**
* inVT100Flags(port, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x42)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
inVT100Flags(port, addrFrom)
{
var b = this.bFlags;
/*
* The NVR_CLK bit is driven by LBA7 (ie, bit 7 from Line Buffer Address generation); see the DC011 discussion above.
*/
b &= ~ChipSet8080.VT100.FLAGS.NVR_CLK;
if (this.getVT100LBA(7)) {
b |= ChipSet8080.VT100.FLAGS.NVR_CLK;
if (b != this.bFlags) {
this.doNVRCommand();
}
}
b &= ~ChipSet8080.VT100.FLAGS.NVR_DATA;
if (this.bNVROut) {
b |= ChipSet8080.VT100.FLAGS.NVR_DATA;
}
b &= ~ChipSet8080.VT100.FLAGS.KBD_XMIT;
if (this.kbd && this.kbd.isVT100TransmitterReady()) {
b |= ChipSet8080.VT100.FLAGS.KBD_XMIT;
}
b &= ~ChipSet8080.VT100.FLAGS.UART_XMIT;
if (this.serial && this.serial.isTransmitterReady()) {
b |= ChipSet8080.VT100.FLAGS.UART_XMIT;
}
this.bFlags = b;
this.printMessageIO(port, null, addrFrom, "FLAGS", b);
return b;
}
/**
* outVT100Brightness(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x42)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outVT100Brightness(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "BRIGHTNESS");
this.bBrightness = b;
}
/**
* outVT100NVRLatch(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0x62)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outVT100NVRLatch(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "NVR.LATCH");
this.bNVRLatch = b;
}
/**
* outVT100DC012(port, b, addrFrom)
*
* TODO: Consider whether we should disable any interrupts (eg, vertical retrace) until
* this port is initialized at runtime.
*
* @this {ChipSet8080}
* @param {number} port (0xA2)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outVT100DC012(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "DC012");
var bOpt = b & 0x3;
var bCmd = (b >> 2) & 0x3;
switch(bCmd) {
case 0x0:
this.bDC012Scroll = (this.bDC012Scroll & ~0x3) | bOpt;
break;
case 0x1:
this.bDC012Scroll = (this.bDC012Scroll & ~0xC) | (bOpt << 2);
if (this.video) this.video.updateScrollOffset(this.bDC012Scroll);
break;
case 0x2:
switch(bOpt) {
case 0x0:
this.bDC012Blink = ~this.bDC012Blink;
break;
case 0x1:
// TODO: Clear vertical frequency interrupt?
break;
case 0x2:
case 0x3:
this.bDC012Reverse = 0x3 - bOpt;
break;
}
break;
case 0x3:
this.bDC012Attr = bOpt;
break;
}
}
/**
* outVT100DC011(port, b, addrFrom)
*
* @this {ChipSet8080}
* @param {number} port (0xC2)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outVT100DC011(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "DC011");
if (b & ChipSet8080.VT100.DC011.RATE60) {
b &= ChipSet8080.VT100.DC011.RATE50;
if (this.bDC011Rate != b) {
this.bDC011Rate = b;
if (this.video) {
this.video.updateRate(this.bDC011Rate == ChipSet8080.VT100.DC011.RATE50? 50 : 60);
}
}
} else {
b &= ChipSet8080.VT100.DC011.COLS132;
if (this.bDC011Cols != b) {
this.bDC011Cols = b;
if (this.video) {
var nCols = (this.bDC011Cols == ChipSet8080.VT100.DC011.COLS132? 132 : 80);
var nRows = (nCols > 80 && (this.bFlags & ChipSet8080.VT100.FLAGS.NO_AVO)? 14 : 24);
this.video.updateDimensions(nCols, nRows);
}
}
}
}
/**
* ChipSet8080.init()
*
* This function operates on every HTML element of class "chipset", extracting the
* JSON-encoded parameters for the ChipSet constructor from the element's "data-value"
* attribute, invoking the constructor to create a ChipSet component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeChipSet = Component.getElementsByClass(document, PC8080.APPCLASS, "chipset");
for (var iChip = 0; iChip < aeChipSet.length; iChip++) {
var eChipSet = aeChipSet[iChip];
var parmsChipSet = Component.getComponentParms(eChipSet);
var chipset = new ChipSet8080(parmsChipSet);
Component.bindComponentControls(chipset, eChipSet, PC8080.APPCLASS);
}
}
}
/*
* NOTE: The STATUS1 port could have been handled entirely by the Keyboard component, but it was just as easy
* to create a simple ChipSet interface, updateStatus1(), that the Keyboard calls whenever it wants to simulate a
* button press or release. It's a six-of-one, half-a-dozen-of-another choice, since technically, Space Invaders
* doesn't have a keyboard.
*/
ChipSet8080.SI1978 = {
MODEL: 1978.1,
STATUS0: { // NOTE: STATUS0 not used by the SI1978 ROMs; refer to STATUS1 instead
PORT: 0,
DIP4: 0x01, // self-test request at power up?
FIRE: 0x10, // 1 = fire
LEFT: 0x20, // 1 = left
RIGHT: 0x40, // 1 = right
PORT7: 0x80, // some connection to (undocumented) port 7
ALWAYS_SET: 0x0E // always set
},
STATUS1: {
PORT: 1,
CREDIT: 0x01, // credit (coin slot)
P2: 0x02, // 1 = 2P start
P1: 0x04, // 1 = 1P start
P1_FIRE: 0x10, // 1 = fire (P1 fire if cocktail machine?)
P1_LEFT: 0x20, // 1 = left (P1 left if cocktail machine?)
P1_RIGHT: 0x40, // 1 = right (P1 right if cocktail machine?)
ALWAYS_SET: 0x08 // always set
},
STATUS2: {
PORT: 2,
DIP3_5: 0x03, // 00 = 3 ships, 01 = 4 ships, 10 = 5 ships, 11 = 6 ships
TILT: 0x04, // 1 = tilt detected
DIP6: 0x08, // 0 = extra ship at 1500, 1 = extra ship at 1000
P2_FIRE: 0x10, // 1 = P2 fire (cocktail machines only?)
P2_LEFT: 0x20, // 1 = P2 left (cocktail machines only?)
P2_RIGHT: 0x40, // 1 = P2 right (cocktail machines only?)
DIP7: 0x80, // 0 = display coin info on demo ("attract") screen
ALWAYS_SET: 0x00
},
SHIFT_RESULT: { // bits 0-7 of barrel shifter result
PORT: 3
},
SHIFT_COUNT: {
PORT: 2,
MASK: 0x07
},
SOUND1: {
PORT: 3,
UFO: 0x01,
SHOT: 0x02,
PDEATH: 0x04,
IDEATH: 0x08,
EXPLAY: 0x10,
AMP_ENABLE: 0x20
},
SHIFT_DATA: {
PORT: 4
},
SOUND2: {
PORT: 5,
FLEET1: 0x01,
FLEET2: 0x02,
FLEET3: 0x04,
FLEET4: 0x08,
UFO_HIT: 0x10
}
};
/*
* One of the many chips in the VT100 is an 8224, which operates at 24.8832MHz. That frequency is divided by 9
* to yield a 361.69ns clock period for the 8080 CPU, which means (in theory) that the CPU is running at 2.76Mhz.
*
* Hence the CPU component in the VT100's machine.xml should be defined as:
*
* <cpu id="cpu8080" model="8080" cycles="2764800"/>
*
* WARNING: The choice of clock speed has an effect on other simulated VT100 circuits; see the DC011 Timing Chip
* discussion below, along with the getVT100LBA() function.
*
* For reference, here is a list of all the VT100 I/O ports, from /devices/pc8080/machine/vt100/debugger/README.md,
* which in turn comes from p. 4-17 of the VT100 Technical Manual (July 1982):
*
* READ OR WRITE
* 00H PUSART data bus
* 01H PUSART command port
*
* WRITE ONLY (Decoded with I/O WR L)
* 02H Baud rate generator
* 42H Brightness D/A latch
* 62H NVR latch
* 82H Keyboard UART data input [used to update the Keyboard Status Byte -JP]
* A2H Video processor DC012
* C2H Video processor DC011
* E2H Graphics port
*
* READ ONLY (Decoded with I/O RD L)
* 22H Modem buffer
* 42H Flags buffer
* 82H Keyboard UART data output
*
* Most of these are handled by the ChipSet component, since it exists as sort of a "catch-all" component,
* but some are more appropriately handled by other components; eg, port 0x82 is handled by the Keyboard component,
* so it's defined there instead of here.
*/
ChipSet8080.VT100 = {
MODEL: 100.0,
FLAGS: {
PORT: 0x42, // read-only
UART_XMIT: 0x01, // PUSART transmit buffer empty if SET
NO_AVO: 0x02, // AVO present if CLEAR
NO_GFX: 0x04, // VT125 graphics board present if CLEAR
OPTION: 0x08, // OPTION present if SET
NO_EVEN: 0x10, // EVEN FIELD active if CLEAR
NVR_DATA: 0x20, // NVR DATA if SET
NVR_CLK: 0x40, // NVR CLOCK if SET
KBD_XMIT: 0x80 // KBD transmit buffer empty if SET
},
BRIGHTNESS: {
PORT: 0x42, // write-only
INIT: 0x00 // for lack of a better guess
},
/*
* DC011 is referred to as a Timing Chip.
*
* As p. 4-55 (105) of the VT100 Technical Manual (July 1982) explains:
*
* The DCO11 is a custom designed bipolar circuit that provides most of the timing signals required by the
* video processor. Internal counters divide the output of a 24.0734 MHz oscillator (located elsewhere on the
* terminal controller module) into the lower frequencies that define dot, character, scan, and frame timing.
* The counters are programmable through various input pins to control the number of characters per line,
* the frequency at which the screen is refreshed, and whether the display is interlaced or noninterlaced.
* These parameters can be controlled through SET-UP mode or by the host.
*
* Table 4-6-1: Video Mode Selection (Write Address 0xC2)
*
* D5 D4 Configuration
* -- -- -------------
* 0 0 80-column mode, interlaced
* 0 1 132-column mode, interlaced
* 1 0 60Hz, non-interlaced
* 1 1 50Hz, non-interlaced
*
* On p. 4-56, the DC011 Block Diagram shows 8 outputs labeled LBA0 through LBA7. From p. 4-61:
*
* Several of the LBAs are used as general purpose clocks in the VT100. LBA3 and LBA4 are used to generate
* timing for the keyboard. These signals satisfy the keyboard's requirement of two square-waves, one twice the
* frequency of the other, even though every 16th transition is delayed (the second stage of the horizontal
* counter divides by 17, not 16). LBA7 is used by the nonvolatile RAM.
*
* And on p. 4-62, timings are provided for the LBA0 through LBA7; in particular:
*
* LBA6: 16.82353us (when LBA6 is low, for a period of 33.64706us)
* LBA7: 31.77778us (when LBA7 is high, for a period of 63.55556us)
*
* If we assume that the CPU cycle count increments once every 361.69ns, it will increment roughly 88 times every
* time LBA7 toggles. So we can divide the CPU cycle count by 88 and set LBA to the low bit of that truncated
* result. An even faster (but less accurate) solution would be to mask bit 6 of the CPU cycle count, which will
* doesn't change until the count has been incremented 64 times. See getVT100LBA() for the chosen implementation.
*/
DC011: { // generates Line Buffer Addresses (LBAs) for the Video Processor
PORT: 0xC2, // write-only
COLS80: 0x00,
COLS132: 0x10,
RATE60: 0x20,
RATE50: 0x30,
INITCOLS: 0x00, // ie, COLS80
INITRATE: 0x20 // ie, RATE60
},
/*
* DC012 is referred to as a Control Chip.
*
* As p. 4-67 (117) of the VT100 Technical Manual (July 1982) explains:
*
* The DCO12 performs three main functions.
*
* 1. Scan count generation. This involves two counters, a multiplexer to switch between the counters,
* double-height logic, scroll and line attribute latches, and various logic controlling switching between
* the two counters. This is the biggest part of the chip. It includes all scrolling, double-height logic,
* and feeds into the underline and hold request circuits.
*
* 2. Generation of HOLD REQUEST. This uses information from the scan counters and the scrolling logic to
* decide when to generate HOLD REQUEST.
*
* 3. Video modifications: dot stretching, blanking, addition of attributes to video outputs, and multiple
* intensity levels.
*
* The input decoder accepts a 4-bit command from the microprocessor when VID WR 2 L is asserted. Table 4-6-2
* lists the commands.
*
* D3 D2 D1 D0 Function
* -- -- -- -- --------
* 0 0 0 0 Load low order scroll latch = 00
* 0 0 0 1 Load low order scroll latch = 01
* 0 0 1 0 Load low order scroll latch = 10
* 0 0 1 1 Load low order scroll latch = 11
*
* 0 1 0 0 Load high order scroll latch = 00
* 0 1 0 1 Load high order scroll latch = 01
* 0 1 1 0 Load high order scroll latch = 10
* 0 1 1 1 Load high order scroll latch = 11 (not used)
*
* 1 0 0 0 Toggle blink flip-flop
* 1 0 0 1 Clear vertical frequency interrupt
*
* 1 0 1 0 Set reverse field on
* 1 0 1 1 Set reverse field off
*
* 1 1 0 0 Set basic attribute to underline*
* 1 1 0 1 Set basic attribute to reverse video*
* 1 1 1 0 Reserved for future specification*
* 1 1 1 1 Reserved for future specification*
*
* *These functions also clear blink flip-flop.
*/
DC012: { // generates scan counts for the Video Processor
PORT: 0xA2, // write-only
SCROLL_LO: 0x00,
INITSCROLL: 0x00,
INITBLINK: 0x00,
INITREVERSE:0x00,
INITATTR: 0x00
},
/*
* ER1400 Non-Volatile RAM (NVR) Chip Definitions
*/
NVR: {
LATCH: {
PORT: 0x62 // write-only
},
CMD: {
ACCEPT_DATA: 0x0,
ACCEPT_ADDR: 0x1,
SHIFT_OUT: 0x2,
WRITE: 0x4,
ERASE: 0x5,
READ: 0x6,
STANDBY: 0x7
},
WORDMASK: 0x3fff // NVR words are 14-bit
/*
* The Technical Manual, p. 4-18, also notes that "Early VT100s can disable the receiver interrupt by
* programming D4 in the NVR latch. However, this is never used by the VT100."
*/
}
};
/*
* Supported models and their configurations
*/
ChipSet8080.MODELS = {
"SI1978": ChipSet8080.SI1978,
"VT100": ChipSet8080.VT100
};
ChipSet8080.SI1978.INIT = [
[
ChipSet8080.SI1978.STATUS0.ALWAYS_SET,
ChipSet8080.SI1978.STATUS1.ALWAYS_SET,
ChipSet8080.SI1978.STATUS2.ALWAYS_SET,
0, 0, 0, 0
]
];
ChipSet8080.VT100.INIT = [
[
ChipSet8080.VT100.BRIGHTNESS.INIT,
ChipSet8080.VT100.FLAGS.NO_AVO | ChipSet8080.VT100.FLAGS.NO_GFX
],
[
ChipSet8080.VT100.DC011.INITCOLS,
ChipSet8080.VT100.DC011.INITRATE
],
[
ChipSet8080.VT100.DC012.INITSCROLL,
ChipSet8080.VT100.DC012.INITBLINK,
ChipSet8080.VT100.DC012.INITREVERSE,
ChipSet8080.VT100.DC012.INITATTR
],
[
0, 0, 0, 0,
[
/*
* The following array contains the data we use to initialize all (100) words of NVR (Non-Volatile RAM).
*
* I used to initialize every word to 0x3ff, as if the NVR had been freshly erased, but that causes the
* firmware to (attempt to) beep and then display an error code (2). As the DEC Technical Manual says:
*
* If the NVR fails, the bell sounds several times to inform the operator, and then default settings
* stored in the ROM allow the terminal to work.
*
* but I think what they meant to say is that default settings are stored in the RAM copy of NVR. So then
* I went into SET-UP, pressed SHIFT-S to save those settings back to NVR, and then used the PC8080 debugger
* "d nvr" command to dump the NVR contents. The results are below.
*
* The first dump actually contains only two modifications to the factory defaults: enabling ONLINE instead
* of LOCAL operation, and turning ANSI support ON. The second dump is unmodified (the TRUE factory defaults).
*
* By making selective changes, you can discern where the bits for certain features are stored. For example,
* smooth-scrolling is apparently controlled by bit 7 of the word at offset 0x2B (and is ON by default in
* the factory settings). And it's likely that the word at offset 0x32 (ie, the last word that's not zero)
* is the NVR checksum.
*/
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E00,
0x2E08, 0x2E8E, 0x2E00, 0x2ED0, 0x2E70, 0x2E00, 0x2E20, 0x2E00, 0x2EE0, 0x2EE0,
0x2E7D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
],
[
/*
* The TRUE factory defaults (not currently used for anything; they're just here for reference, wasting space....)
*/
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80,
0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E80, 0x2E00,
0x2E08, 0x2E8E, 0x2E20, 0x2ED0, 0x2E50, 0x2E00, 0x2E20, 0x2E00, 0x2EE0, 0x2EE0,
0x2E69, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
]
]
];
/*
* Port notification tables
*/
ChipSet8080.SI1978.portsInput = {
0x00: ChipSet8080.prototype.inSIStatus0,
0x01: ChipSet8080.prototype.inSIStatus1,
0x02: ChipSet8080.prototype.inSIStatus2,
0x03: ChipSet8080.prototype.inSIShiftResult
};
ChipSet8080.SI1978.portsOutput = {
0x02: ChipSet8080.prototype.outSIShiftCount,
0x03: ChipSet8080.prototype.outSISound1,
0x04: ChipSet8080.prototype.outSIShiftData,
0x05: ChipSet8080.prototype.outSISound2,
0x06: ChipSet8080.prototype.outSIWatchdog
};
ChipSet8080.VT100.portsInput = {
0x42: ChipSet8080.prototype.inVT100Flags
};
ChipSet8080.VT100.portsOutput = {
0x42: ChipSet8080.prototype.outVT100Brightness,
0x62: ChipSet8080.prototype.outVT100NVRLatch,
0xA2: ChipSet8080.prototype.outVT100DC012,
0xC2: ChipSet8080.prototype.outVT100DC011
};
/*
* Initialize every ChipSet module on the page.
*/
Web.onInit(ChipSet8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/rom.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class ROM8080 extends Component {
/**
* ROM8080(parmsROM)
*
* The ROM8080 component expects the following (parmsROM) properties:
*
* addr: physical address of ROM
* size: amount of ROM, in bytes
* alias: physical alias address (null if none)
* file: name of ROM data file
*
* NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND the
* ROM data file has finished loading (see doneLoad()).
*
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM you received
* is the ROM you expected.
*
* @this {ROM8080}
* @param {Object} parmsROM
*/
constructor(parmsROM)
{
super("ROM", parmsROM);
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);
});
}
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {ROM8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.copyROM();
}
/**
* powerUp(data, fRepower)
*
* @this {ROM8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (this.aSymbols) {
if (this.dbg) {
this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols);
}
/*
* Our only role in the handling of symbols is to hand them off to the Debugger at our
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
*/
delete this.aSymbols;
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* Since we have nothing to do on powerDown(), and no state to return, we could simply omit
* this function. But it doesn't hurt anything, and maybe we'll use our state to save something
* useful down the road, like user-defined symbols (ie, symbols that the Debugger may have
* created, above and beyond those symbols we automatically loaded, if any, along with the ROM).
*
* @this {ROM8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return true;
}
/**
* doneLoad(sURL, sROMData, nErrorCode)
*
* @this {ROM8080}
* @param {string} sURL
* @param {string} sROMData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, sROMData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load system ROM (error " + nErrorCode + ": " + sURL + ")");
return;
}
Component.addMachineResource(this.idMachine, sURL, sROMData);
if (sROMData.charAt(0) == "[" || sROMData.charAt(0) == "{") {
try {
/*
* The most likely source of any exception will be here: parsing the JSON-encoded ROM data.
*/
var rom = eval("(" + sROMData + ")");
var ab = rom['bytes'];
/*
* Resource 'longs' should always be 32-bit DWORD values, whereas 'data' bit lengths
* will vary according to the machine architecture for which the resource was designed.
*/
var adw = rom['longs'] || rom['data'];
if (ab) {
this.abROM = ab;
}
else if (adw) {
/*
* Convert all the DWORDs into BYTEs, so that subsequent code only has to deal with abROM.
*/
this.abROM = new Array(adw.length * 4);
for (var idw = 0, ib = 0; idw < adw.length; idw++) {
this.abROM[ib++] = adw[idw] & 0xff;
this.abROM[ib++] = (adw[idw] >> 8) & 0xff;
this.abROM[ib++] = (adw[idw] >> 16) & 0xff;
this.abROM[ib++] = (adw[idw] >> 24) & 0xff;
}
}
else {
this.abROM = rom;
}
this.aSymbols = rom['symbols'];
if (!this.abROM.length) {
Component.error("Empty ROM: " + sURL);
return;
}
else if (this.abROM.length == 1) {
Component.error(this.abROM[0]);
return;
}
} catch (e) {
this.notice("ROM data error: " + e.message);
return;
}
}
else {
/*
* Parse the ROM data manually; we assume it's in "simplified" hex form (a series of hex byte-values
* separated by whitespace).
*/
var sHexData = sROMData.replace(/\n/gm, " ").replace(/ +$/, "");
var asHexData = sHexData.split(" ");
this.abROM = new Array(asHexData.length);
for (var i = 0; i < asHexData.length; i++) {
this.abROM[i] = Str.parseInt(asHexData[i], 16);
}
}
this.copyROM();
}
/**
* copyROM()
*
* This function is called by both initBus() and doneLoad(), but it cannot copy the the ROM data into place
* until after initBus() has received the Bus component AND doneLoad() has received the abROM data. When both
* those criteria are satisfied, the component becomes "ready".
*
* @this {ROM8080}
*/
copyROM()
{
if (!this.isReady()) {
if (!this.sFilePath) {
this.setReady();
}
else if (this.abROM && this.bus) {
/*
* If no explicit size was specified, then use whatever the actual size is.
*/
if (!this.sizeROM) {
this.sizeROM = this.abROM.length;
}
if (this.abROM.length != this.sizeROM) {
/*
* Note that setError() sets the component's fError flag, which in turn prevents setReady() from
* marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a
* good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be
* times when we'd like to forge ahead anyway.
*/
this.setError("ROM size (" + Str.toHexLong(this.abROM.length) + ") does not match specified size (" + Str.toHexLong(this.sizeROM) + ")");
}
else if (this.addROM(this.addrROM)) {
var aliases = [];
if (typeof this.addrAlias == "number") {
aliases.push(this.addrAlias);
} else if (this.addrAlias != null && this.addrAlias.length) {
aliases = this.addrAlias;
}
for (var i = 0; i < aliases.length; i++) {
this.cloneROM(aliases[i]);
}
/*
* We used to hang onto the original ROM data so that we could restore any bytes the CPU overwrote,
* using memory write-notification handlers, but with the introduction of read-only memory blocks, that's
* no longer necessary.
*
* TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs.
* That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless
* whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger,
* and Debugger users should know better.
*/
delete this.abROM;
}
this.setReady();
}
}
}
/**
* addROM(addr)
*
* @this {ROM8080}
* @param {number} addr
* @return {boolean}
*/
addROM(addr)
{
if (this.bus.addMemory(addr, this.sizeROM, Memory8080.TYPE.ROM)) {
if (DEBUG) this.log("addROM(): copying ROM to " + Str.toHexLong(addr) + " (" + Str.toHexLong(this.abROM.length) + " bytes)");
var i;
for (i = 0; i < this.abROM.length; i++) {
this.bus.setByteDirect(addr + i, this.abROM[i]);
}
return true;
}
/*
* We don't need to report an error here, because addMemory() already takes care of that.
*/
return false;
}
/**
* cloneROM(addr)
*
* For ROMs with one or more alias addresses, we used to call addROM() for each address. However,
* that obviously wasted memory, since each alias was an independent copy, and if you used the
* Debugger to edit the ROM in one location, the changes would not appear in the other location(s).
*
* Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods
* to manually get and set the blocks of any memory range, it is now possible to create true aliases.
*
* @this {ROM8080}
* @param {number} addr
*/
cloneROM(addr)
{
var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM);
this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks);
}
/**
* ROM8080.init()
*
* This function operates on every HTML element of class "rom", extracting the
* JSON-encoded parameters for the ROM8080 constructor from the element's "data-value"
* attribute, invoking the constructor to create a ROM8080 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeROM = Component.getElementsByClass(document, PC8080.APPCLASS, "rom");
for (var iROM = 0; iROM < aeROM.length; iROM++) {
var eROM = aeROM[iROM];
var parmsROM = Component.getComponentParms(eROM);
var rom = new ROM8080(parmsROM);
Component.bindComponentControls(rom, eROM, PC8080.APPCLASS);
}
}
}
/*
* 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().
*/
/*
* Initialize all the ROM8080 modules on the page.
*/
Web.onInit(ROM8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/ram.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class RAM8080 extends Component {
/**
* RAM8080(parmsRAM)
*
* The RAM8080 component expects the following (parmsRAM) properties:
*
* addr: starting physical address of RAM (default is 0)
* size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings)
* file: name of optional data file to load into RAM (default is "")
* load: optional file load address (overrides any load address specified in the data file; default is null)
* exec: optional file exec address (overrides any exec address specified in the data file; default is null)
*
* NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the
* Computer component calls powerUp().
*
* @this {RAM8080}
* @param {Object} parmsRAM
*/
constructor(parmsRAM)
{
super("RAM", parmsRAM);
this.abInit = null;
this.aSymbols = null;
this.addrRAM = parmsRAM['addr'];
this.sizeRAM = parmsRAM['size'];
this.addrLoad = parmsRAM['load'];
this.addrExec = parmsRAM['exec'];
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
this.fAllocated = false;
this.sFilePath = parmsRAM['file'];
this.sFileName = Str.getBaseName(this.sFilePath);
if (this.sFilePath) {
var sFileURL = this.sFilePath;
if (DEBUG) this.log('load("' + sFileURL + '")');
/*
* If the selected data file has a ".json" extension, then we assume it's pre-converted
* JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension.
* Otherwise, we ask our server-side converter to return the file in a JSON-compatible format.
*/
var sFileExt = Str.getExtension(this.sFileName);
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
}
var ram = this;
Web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) {
ram.doneLoad(sURL, sResponse, nErrorCode);
});
}
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {RAM8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.initRAM();
}
/**
* powerUp(data, fRepower)
*
* @this {RAM8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
/*
* The Computer powers up the CPU last, at which point CPUState state is restored,
* which includes the Bus state, and since we use the Bus to allocate all our memory,
* memory contents are already restored for us, so we don't need the usual restore
* logic.
*/
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {RAM8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
/*
* The Computer powers down the CPU first, at which point CPUState state is saved,
* which includes the Bus state, and since we use the Bus component to allocate all
* our memory, memory contents are already saved for us, so we don't need the usual
* save logic.
*/
return true;
}
/**
* doneLoad(sURL, sData, nErrorCode)
*
* @this {RAM8080}
* @param {string} sURL
* @param {string} sData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, sData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")");
return;
}
Component.addMachineResource(this.idMachine, sURL, sData);
var resource = Web.parseMemoryResource(sURL, sData);
if (resource) {
this.abInit = resource.aBytes;
this.aSymbols = resource.aSymbols;
if (this.addrLoad == null) this.addrLoad = resource.addrLoad;
if (this.addrExec == null) this.addrExec = resource.addrExec;
} else {
this.sFilePath = null;
}
this.initRAM();
}
/**
* initRAM()
*
* This function is called by both initBus() and doneLoad(), but it cannot copy the initial data into place
* until after initBus() has received the Bus component AND doneLoad() has received the data. When both those
* criteria are satisfied, the component becomes "ready".
*
* @this {RAM8080}
*/
initRAM()
{
if (!this.fAllocated && this.sizeRAM) {
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory8080.TYPE.RAM)) {
this.fAllocated = true;
}
}
if (!this.isReady()) {
if (!this.fAllocated) {
Component.error("No RAM allocated");
}
else if (this.sFilePath) {
/*
* Too early...
*/
if (!this.abInit || !this.bus) return;
var addr = this.addrRAM;
if (this.addrLoad !== null) addr = this.addrLoad;
for (var i = 0; i < this.abInit.length; i++) {
this.bus.setByteDirect(addr + i, this.abInit[i]);
}
if (this.addrExec !== null) {
/*
* Here's where we enable our "Fake CP/M" support, triggered by the user loading a "writable" ROM image
* at offset 0x100. Fake CP/M support works by installing HLT opcodes at well-known CP/M addresses
* (namely, 0x0000, which is the CP/M reset vector, and 0x0005, which is the CP/M system call vector) and
* then telling the CPU to call us whenever a HLT occurs, so we can check PC for one of these addresses.
*/
if (this.addrExec == RAM8080.CPM.INIT) {
for (i = 0; i < RAM8080.CPM.VECTORS.length; i++) {
this.bus.setByteDirect(RAM8080.CPM.VECTORS[i], CPUDef8080.OPCODE.HLT);
}
this.cpu.addHaltCheck(function(rom) {
return function(addr) {
return rom.checkCPMVector(addr)
};
}(this));
}
this.cpu.setReset(this.addrExec);
}
/*
* TODO: Consider an option to retain this data and give the user a way of restoring the initial contents.
*/
delete this.abInit;
}
this.setReady();
}
}
/**
* reset()
*
* @this {RAM8080}
*/
reset()
{
/*
* If you want to zero RAM on reset, then this would be a good place to do it.
*/
}
/**
* checkCPMVector(addr)
*
* @this {RAM8080}
* @param {number} addr (of the HLT opcode)
* @return {boolean} true if special processing performed, false if not
*/
checkCPMVector(addr)
{
var i = RAM8080.CPM.VECTORS.indexOf(addr);
if (i >= 0) {
var fCPM = false;
var cpu = this.cpu;
var dbg = this.dbg;
if (addr == RAM8080.CPM.BDOS.VECTOR) {
fCPM = true;
switch(cpu.regC) {
case RAM8080.CPM.BDOS.FUNC.CON_WRITE:
this.writeCPMString(this.getCPMChar(cpu.regE));
break;
case RAM8080.CPM.BDOS.FUNC.STR_WRITE:
this.writeCPMString(this.getCPMString(cpu.getDE(), '$'));
break;
default:
fCPM = false;
break;
}
}
if (fCPM) {
CPUDef8080.opRET.call(cpu); // for recognized calls, automatically return
}
else if (dbg) {
this.println("\nCP/M vector " + Str.toHexWord(addr));
cpu.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT
dbg.stopCPU();
}
return true;
}
return false;
}
/**
* getCPMChar(ch)
*
* @this {RAM8080}
* @param {number} ch
* @return {string}
*/
getCPMChar(ch)
{
return String.fromCharCode(ch);
}
/**
* getCPMString(addr, chEnd)
*
* @this {RAM8080}
* @param {number} addr (of a string)
* @param {string|number} [chEnd] (terminating character, default is 0)
* @return {string}
*/
getCPMString(addr, chEnd)
{
var s = "";
var cchMax = 255;
var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0;
while (cchMax--) {
var b = this.cpu.getByte(addr++);
if (b == bEnd) break;
s += String.fromCharCode(b);
}
return s;
}
/**
* writeCPMString(s)
*
* @this {RAM8080}
* @param {string} s
*/
writeCPMString(s)
{
s = s.replace(/\r/g, '');
if (this.controlPrint) {
this.controlPrint.value += s;
this.controlPrint.scrollTop = this.controlPrint.scrollHeight;
}
}
/**
* RAM8080.init()
*
* This function operates on every HTML element of class "ram", extracting the
* JSON-encoded parameters for the RAM8080 constructor from the element's "data-value"
* attribute, invoking the constructor to create a RAM8080 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeRAM = Component.getElementsByClass(document, PC8080.APPCLASS, "ram");
for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) {
var eRAM = aeRAM[iRAM];
var parmsRAM = Component.getComponentParms(eRAM);
var ram = new RAM8080(parmsRAM);
Component.bindComponentControls(ram, eRAM, PC8080.APPCLASS);
}
}
}
RAM8080.CPM = {
BIOS: {
VECTOR: 0x0000
},
BDOS: {
VECTOR: 0x0005,
FUNC: { // function number (specified in regC)
RESET: 0x00,
CON_READ: 0x01, // output: A = L = ASCII character
CON_WRITE: 0x02, // input: E = ASCII character
AUX_READ: 0x03, // output: A = L = ASCII character
AUX_WRITE: 0x04, // input: E = ASCII character
PRN_WRITE: 0x05, // input: E = ASCII character
MEM_SIZE: 0x06, // output: base address of CCP (Console Command Processor), but which register? (perhaps moot if this was CP/M 1.3 only...)
CON_IO: 0x06, // input: E = ASCII character (or 0xFF to return ASCII character in A)
GET_IOBYTE: 0x07,
SET_IOBYTE: 0x08,
STR_WRITE: 0x09 // input: DE = address of string
}
},
INIT: 0x100
};
RAM8080.CPM.VECTORS = [RAM8080.CPM.BIOS.VECTOR, RAM8080.CPM.BDOS.VECTOR];
/*
* Initialize all the RAM8080 modules on the page.
*/
Web.onInit(RAM8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/keyboard.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Keyboard8080 extends Component {
/**
* Keyboard8080(parmsKbd)
*
* The Keyboard8080 component has the following component-specific (parmsKbd) properties:
*
* model: eg, "VT100" (should be a member of Keyboard8080.MODELS)
*
* @this {Keyboard8080}
* @param {Object} parmsKbd
*/
constructor(parmsKbd)
{
super("Keyboard", parmsKbd, Messages8080.KEYBOARD);
var model = parmsKbd['model'];
if (model && !Keyboard8080.MODELS[model]) {
Component.notice("Unrecognized Keyboard8080 model: " + model);
}
this.config = Keyboard8080.MODELS[model] || {};
this.reset();
this.setReady();
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {Keyboard8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
/*
* There's a special binding that the Video component uses ("screen") to effectively bind its
* screen to the entire keyboard, in Video.powerUp(); ie:
*
* video.kbd.setBinding("canvas", "screen", video.canvasScreen);
* or:
* video.kbd.setBinding("textarea", "screen", video.textareaScreen);
*
* However, it's also possible for the keyboard XML definition to define a control that serves
* a similar purpose; eg:
*
* <control type="text" binding="kbd" width="2em">Keyboard</control>
*
* The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on
* certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will
* still be enabled (there's currently no way to configure the Video component to not bind its screen,
* but we could certainly add one if the need ever arose).
*/
var kbd = this;
var id = sHTMLType + '-' + sBinding;
if (this.bindings[id] === undefined) {
if (sHTMLType == "led" && this.config.LEDCODES[sBinding]) {
this.bindings[id] = control;
return true;
}
switch (sBinding) {
case "kbd":
case "screen":
/*
* Recording the binding ID prevents multiple controls (or components) from attempting to erroneously
* bind a control to the same ID, but in the case of a "dual display" configuration, we actually want
* to allow BOTH video components to call setBinding() for "screen", so that it doesn't matter which
* display the user gives focus to.
*
* this.bindings[id] = control;
*/
if (Web.isUserAgent("iOS")) {
/*
* For iOS devices, it's best to deal only with keypress events. The main reason is that we don't
* get shift-key events, so we have no way of distinguishing between certain keys, such as ':' and
* ';', unless we are monitoring key presses. Another reason is that, under certain poorly documented
* conditions, an iOS keyup event will not contain any keyCode; this is most easily reproduced with
* the iOS simulator and a physical keyboard (not the pop-up keyboard). When this happens, we think
* the key is stuck. Finally, certain other problems that we have tried to resolve when using a physical
* keyboard (eg, keeping the physical and virtual CAPS-LOCK states in sync) simply don't exist in the
* iOS environment.
*
* So, with all that mind, it seems best to have a separate iOS keypress handler and forego keydown
* and keyup events entirely. The iOS keypress handler must also perform some additional checks, such
* as watching for keys that can only be typed on the emulated device when a shift key is down, and
* simulating "fake" shift-key down and up events.
*
* Perhaps we can eventually standardize on this alternate keypress-centric approach for ALL devices,
* but until then, it's safer to have these two code paths.
*
* UPDATE: So much for the best laid plans. iOS won't deliver BACKSPACE events to the keypress handler,
* so we have to deal with keydown/keyup events after all.
*/
control.onkeypress = function oniOSKeyPress(event)
{
return kbd.oniOSKeyPress(event);
};
control.onkeydown = function oniOSKeyDown(event)
{
return kbd.oniOSKeyDown(event, true);
};
control.onkeyup = function oniOSKeyUp(event)
{
return kbd.oniOSKeyDown(event, false);
};
}
else {
control.onkeydown = function onKeyDown(event)
{
return kbd.onKeyDown(event, true);
};
control.onkeyup = function onKeyUp(event)
{
return kbd.onKeyDown(event, false);
};
control.onkeypress = function onKeyPress(event)
{
return kbd.onKeyPress(event);
};
}
control.onpaste = function onKeyPaste(event)
{
return kbd.onPaste(event);
};
return true;
default:
if (this.config.SOFTCODES && this.config.SOFTCODES[sBinding] !== undefined) {
this.bindings[id] = control;
control.onclick = function(kbd, keyCode) {
return function onKeyboardBindingDown(event) {
/*
* iOS usability improvement: calling preventDefault() prevents rapid clicks from
* also being (mis)interpreted as a desire to "zoom" in on the machine.
*/
if (event.preventDefault) event.preventDefault();
/*
* TODO: Add some additional SOFTCODES configuration info that will tell us which soft
* keys (eg, CTRL) should be treated as toggles, instead of hard-coding that knowledge below.
*
* Moreover, if a *real* CTRL or CAPS-LOCK key is pressed or released, it would be nice
* to update the state of these on-screen controls, too (ie, not just when the controls are
* clicked).
*/
var fDown = true, bit = 0;
if (keyCode == Keys.KEYCODE.CTRL) {
bit = Keyboard8080.STATE.CTRL;
}
else if (keyCode == Keys.KEYCODE.CAPS_LOCK) {
bit = Keyboard8080.STATE.CAPS_LOCK;
}
if (bit) {
control.style.fontWeight = "normal";
fDown = !(kbd.bitsState & bit);
if (fDown) control.style.fontWeight = "bold";
kbd.checkModifierKeys(keyCode, fDown);
}
kbd.onSoftKeyDown(keyCode, fDown, !bit);
if (kbd.cmp) kbd.cmp.updateFocus();
};
}(this, this.config.SOFTCODES[sBinding]);
//
// var fnUp = function (kbd, keyCode) {
// return function onKeyboardBindingUp(event) {
// kbd.onSoftKeyDown(keyCode, false);
// /*
// * Give focus back to the machine (since clicking the button takes focus away).
// *
// * if (kbd.cmp) kbd.cmp.updateFocus();
// *
// * iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down
// * (assuming it was already down).
// */
// };
// }(this, this.config.SOFTCODES[sBinding]);
//
// if ('ontouchstart' in window) {
// control.ontouchstart = fnDown;
// control.ontouchend = fnUp;
// } else {
// control.onmousedown = fnDown;
// control.onmouseup = control.onmouseout = fnUp;
// }
//
// UPDATE: Since the only controls that we explicitly bind to SOFTCODES are buttons, I'm simplifying
// the above code with a conventional "onclick" handler. The only corresponding change I had to make
// to the onclick (formerly fnDown) function was to set fAutoRelease on its call to onSoftKeyDown(),
// since we're no longer attempting to detect when the control (ie, the button) is actually released.
//
// This change also resolves a problem I ran into with the Epiphany (WebKit-based) web browser running
// on the "elementary" (Ubuntu-based) OS, where clicks on the SET-UP button were ignored; perhaps its
// buttons don't generate mouse and/or touch events. Anyway, an argument for keeping things simple.
//
return true;
}
break;
}
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {Keyboard8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.cpu = cpu;
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
var kbd = this;
this.timerReleaseKeys = this.cpu.addTimer(function() {
kbd.checkSoftKeysToRelease();
});
this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet"));
this.serial = /** @type {SerialPort8080} */ (cmp.getMachineComponent("SerialPort"));
bus.addPortInputTable(this, this.config.portsInput);
bus.addPortOutputTable(this, this.config.portsOutput);
}
/**
* powerUp(data, fRepower)
*
* @this {Keyboard8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Keyboard8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {Keyboard8080}
*/
reset()
{
/*
* As keyDown events are encountered, a corresponding "softCode" is looked up. If one is found,
* then an entry for the key is added to the aKeysActive array. Each "key" entry in aKeysActive contains:
*
* softCode: number or string representing the key pressed
* msDown: timestamp of the most recent "down" event
* fAutoRelease: true to auto-release the key after MINPRESSTIME (set when "up" occurs too quickly)
*
* When the key is finally released (or auto-released), its entry is removed from the array.
*/
this.aKeysActive = [];
/*
* The current (assumed) physical (and simulated) states of the various shift/lock keys.
*
* TODO: Determine how (or whether) we can query the browser's initial shift/lock key states.
*/
this.bitsState = 0;
if (this.config.INIT && !this.restore(this.config.INIT)) {
this.notice("reset error");
}
}
/**
* save()
*
* This implements save support for the Keyboard component.
*
* @this {Keyboard8080}
* @return {Object}
*/
save()
{
var state = new State(this);
switch(this.config.MODEL) {
case Keyboard8080.SI1978.MODEL:
break;
case Keyboard8080.VT100.MODEL:
state.set(0, [this.bVT100Status, this.bVT100Address, this.fVT100UARTBusy, this.nVT100UARTCycleSnap, -1]);
break;
}
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the Keyboard component.
*
* @this {Keyboard8080}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var a;
if (data && (a = data[0]) && a.length) {
switch(this.config.MODEL) {
case Keyboard8080.SI1978.MODEL:
return true;
case Keyboard8080.VT100.MODEL:
this.bVT100Status = a[0];
this.updateLEDs(this.bVT100Status & Keyboard8080.VT100.STATUS.LEDS);
this.bVT100Address = a[1];
this.fVT100UARTBusy = a[2];
this.nVT100UARTCycleSnap = a[3];
this.iKeyNext = a[4];
return true;
}
}
return false;
}
/**
* setLED(control, f, color)
*
* TODO: Add support for user-definable LED colors
*
* @this {Keyboard8080}
* @param {Object} control is an HTML control DOM object
* @param {boolean|number} f is true if the LED represented by control should be "on", false if "off"
* @param {number} color (ie, 0xff0000 for RED, or 0x00ff00 for GREEN)
*/
setLED(control, f, color)
{
control.style.backgroundColor = (f? ('#' + Str.toHex(color, 6)) : "#000000");
}
/**
* updateLEDs(bLEDs)
*
* @this {Keyboard8080}
* @param {number} [bLEDs]
*/
updateLEDs(bLEDs)
{
var id, control;
if (bLEDs != null) {
this.bLEDs = bLEDs;
} else {
bLEDs = this.bLEDs;
}
for (var sBinding in this.config.LEDCODES) {
id = "led-" + sBinding;
control = this.bindings[id];
if (control) {
var bitLED = this.config.LEDCODES[sBinding];
var fOn = !!(bLEDs & bitLED);
if (bitLED & (bitLED-1)) {
fOn = !(bLEDs & ~bitLED);
}
this.setLED(control, fOn, 0xff0000);
}
}
id = "led-caps-lock";
control = this.bindings[id];
if (control) {
this.setLED(control, (this.bitsState & Keyboard8080.STATE.CAPS_LOCK), 0x00ff00);
}
}
/**
* checkModifierKeys(keyCode, fDown, fRight)
*
* @this {Keyboard8080}
* @param {number} keyCode (ie, either a keycode or string ID)
* @param {boolean} fDown (true if key going down, false if key going up)
* @param {boolean} [fRight] (true if key is on the right, false if not or unknown or n/a)
* @return {boolean} (fDown updated as needed for CAPS-LOCK weirdness)
*/
checkModifierKeys(keyCode, fDown, fRight)
{
var bit = 0;
switch(keyCode) {
case Keys.KEYCODE.SHIFT:
bit = fRight? Keyboard8080.STATE.RSHIFT : Keyboard8080.STATE.SHIFT;
break;
case Keys.KEYCODE.CTRL:
bit = fRight? Keyboard8080.STATE.RCTRL : Keyboard8080.STATE.CTRL;
break;
case Keys.KEYCODE.ALT:
bit = fRight? Keyboard8080.STATE.RALT : Keyboard8080.STATE.ALT;
break;
case Keys.KEYCODE.CMD:
bit = fRight? Keyboard8080.STATE.RCMD : Keyboard8080.STATE.CMD;
break;
case Keys.KEYCODE.CAPS_LOCK:
bit = Keyboard8080.STATE.CAPS_LOCK;
/*
* WARNING: You have an entered a browser weirdness zone. In Chrome, pressing-and-releasing
* CAPS-LOCK generates a "down" event when it turns the lock on and an "up" event when it turns
* the lock off. Firefox, OTOH, generates only "down" events, so we have to "manufacture"
* the fDown parameter ourselves -- which means we also have to propagate it back to the caller.
*
* And, while this isn't necessary for Chrome, it doesn't appear to hurt anything in Chrome, so
* we're not going to bother making it browser-specific.
*/
fDown = !(this.bitsState & bit);
break;
}
if (bit) {
if (fDown) {
this.bitsState |= bit;
} else {
this.bitsState &= ~bit;
}
}
return fDown;
}
/**
* getSoftCode(keyCode)
*
* Returns a number if the keyCode exists in the KEYMAP, or a string if the keyCode has a string ID.
*
* @this {Keyboard8080}
* @return {string|number|null}
*/
getSoftCode(keyCode)
{
keyCode = this.config.ALTCODES[keyCode] || keyCode;
if (this.config.KEYMAP[keyCode]) {
return keyCode;
}
for (var sSoftCode in this.config.SOFTCODES) {
if (this.config.SOFTCODES[sSoftCode] === keyCode) {
return sSoftCode;
}
}
return null;
}
/**
* onKeyDown(event, fDown)
*
* @this {Keyboard8080}
* @param {Object} event
* @param {boolean} fDown is true for a keyDown event, false for up
* @return {boolean} true to pass the event along, false to consume it
*/
onKeyDown(event, fDown)
{
var fPass = true;
var keyCode = event.keyCode;
if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) {
this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + ")", true);
}
/*
* A note about Firefox: it uses different keyCodes for certain keys; there's a logic to the differences
* (they use ASCII codes), but since other browsers didn't follow suit, we must use a mapping table to
* convert their keyCodes to the more traditional values.
*/
keyCode = Keys.FF_KEYCODES[keyCode] || keyCode;
/*
* We now keep track of physical keyboard modifier keys. This makes it possible for new services
* to eventually be implemented (simulateKeysDown() and simulateKeysUp()), to map special ALT-key
* combinations to VT100 keys, etc.
*/
fDown = this.checkModifierKeys(keyCode, fDown, event.location == Keys.LOCATION.RIGHT);
var softCode = this.getSoftCode(keyCode);
if (softCode) {
/*
* Key combinations involving the "meta" key (ie, the Windows or Command key) are meaningless to
* the VT100, so we ignore them. The "meta" key itself is already effectively ignored, because it's
* not acknowledged by getSoftCode(), but we also don't want any of the keys combined with "meta"
* slipping through either.
*/
if (!event.metaKey) {
/*
* The LINE-FEED key is an important key on the VT100, and while we DO map a host function key
* to it (F7), I like the idea of making ALT-ENTER an alias for LINE-FEED as well. Ditto for
* making ALT-DELETE an alias for BACKSPACE (and no, I don't mean ALT-BACKSPACE as an alias for
* DELETE; see my earlier discussion involving BACKSPACE and DELETE).
*
* Of course, as experienced VT100 users know, it's always possible to type CTRL-J for LINE-FEED
* and CTRL-H for BACKSPACE, too. But not all our users are that experienced.
*
* I was also tempted to use CTRL-ENTER or SHIFT-ENTER, but those are composable VT100 key
* sequences, so it's best not to muck with those.
*
* Finally, this hack is complicated by the fact that if the ALT key is released first, we run
* the risk of the remapped key being stuck "down". Hence the new REMAPPED bit, which should
* remain set (as a "proxy" for the ALT bit) as long as a remapped key is down.
*/
var fRemapped = false;
if (this.bitsState & (Keyboard8080.STATE.ALTS | Keyboard8080.STATE.REMAPPED)) {
if (softCode == Keys.KEYCODE.CR) {
softCode = Keys.KEYCODE.F7;
fRemapped = true;
}
else if (softCode == Keys.KEYCODE.BS) {
softCode = Keys.KEYCODE.DEL;
fRemapped = true;
}
if (fRemapped) {
if (fDown) {
this.bitsState |= Keyboard8080.STATE.REMAPPED;
} else {
this.bitsState &= ~Keyboard8080.STATE.REMAPPED;
}
}
}
fPass = this.onSoftKeyDown(softCode, fDown);
/*
* As onKeyPress() explains, the only key presses we're interested in are letters, which provide
* an important clue regarding the CAPS-LOCK state. For all other keys, we call preventDefault(),
* which normally "suppresses" the keyPress event, as well as other unwanted browser behaviors
* (eg, the SPACE key, which browsers interpret as a desire to scroll the entire web page down).
*
* And, even if the key IS a letter, we STILL want to call preventDefault() if a CTRL key is down,
* so that Windows-based browsers (eg, Edge) don't interfere with their stupid CTRL-based shortcuts. ;-)
*
* NOTE: We COULD check event.ctrlKey too, but it's six of one, half a dozen of another.
*/
if (!(softCode >= Keys.ASCII.A && softCode <= Keys.ASCII.Z) || (this.bitsState | Keyboard8080.STATE.CTRLS)) {
if (event.preventDefault) event.preventDefault();
}
}
}
if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) {
this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): softCode=" + softCode + ", pass=" + fPass, true);
}
return fPass;
}
/**
* onKeyPress(event)
*
* For now, our only interest in keyPress events is letters, as a means of detecting the CAPS-LOCK state.
*
* @this {Keyboard8080}
* @param {Object} event
* @return {boolean} true to pass the event along, false to consume it
*/
onKeyPress(event)
{
/*
* A note about Firefox: the KeyboardEvent they pass to a keypress handler doesn't set 'keyCode', so
* we have to fallback to 'charCode' (or 'which'), both of which are deprecated but realistically can't
* really go away.
*
* TODO: Consider "upgrading" this code to use the new 'key' property. Note, however, that it's a string,
* not a number; for example; if the colon key is pressed, 'key' will be ":", whereas 'charCode' and 'which'
* will be 58.
*/
var charCode = event.keyCode || event.charCode;
if (charCode >= Keys.ASCII.A && charCode <= Keys.ASCII.Z) {
if (!(this.bitsState & (Keyboard8080.STATE.SHIFTS | Keyboard8080.STATE.CAPS_LOCK))) {
this.bitsState |= Keyboard8080.STATE.CAPS_LOCK;
this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, true);
this.updateLEDs();
}
}
else if (charCode >= Keys.ASCII.a && charCode <= Keys.ASCII.z) {
if (this.bitsState & Keyboard8080.STATE.CAPS_LOCK) {
this.bitsState &= ~Keyboard8080.STATE.CAPS_LOCK;
this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, false);
this.updateLEDs();
}
}
if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) {
this.printMessage("onKeyPress(" + charCode + ")", true);
}
return true;
}
/**
* oniOSKeyDown(event, fDown)
*
* @this {Keyboard8080}
* @param {Object} event
* @param {boolean} fDown is true for a keyDown event, false for up
* @return {boolean} true to pass the event along, false to consume it
*/
oniOSKeyDown(event, fDown)
{
var fPass = true;
/*
* Because keydown/keyup events on iOS are inherently "fake", they can be delivered so quickly that
* if we generated matching down/up events, then the emulated machine might not see the key transition.
* So we now deliver only down events, with fAutoRelease always set (see below).
*
* Also, because of iOS weirdness discussed in setBinding() when using a physical keyboard, the keyup
* event may not provide a valid keyCode, which is another reason we have no choice but to always deliver
* keys with fAutoRelease set to true.
*/
if (fDown) {
var keyCode = event.keyCode;
var bMapping = this.config.KEYMAP[keyCode];
if (bMapping) {
/*
* If this is a mappable key, but the mapping isn't in the CHARMAP table, then we have to process
* it now; the most common reason is that the key doesn't generate a keypress event (eg, BACKSPACE).
*/
if (!this.indexOfCharMap(bMapping)) {
fPass = this.onSoftKeyDown(keyCode, fDown, true);
if (event.preventDefault) event.preventDefault();
if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) {
this.printMessage("oniOSKey" + (fDown ? "Down" : "Up") + "(" + keyCode + "): pass=" + fPass, true);
}
}
}
}
return fPass;
}
/**
* oniOSKeyPress(event)
*
* @this {Keyboard8080}
* @param {Object} event
* @return {boolean} true to pass the event along, false to consume it
*/
oniOSKeyPress(event)
{
/*
* A note about Firefox: the KeyboardEvent they pass to a keypress handler doesn't set 'keyCode', so
* we have to fallback to 'charCode' (or 'which'), both of which are deprecated but realistically can't
* really go away.
*
* TODO: Consider "upgrading" this code to use the new 'key' property. Note, however, that it's a string,
* not a number; for example; if the colon key is pressed, 'key' will be ":", whereas 'charCode' and 'which'
* will be 58.
*/
var charCode = event.keyCode || event.charCode;
var fShifted = false;
var bMapping = this.config.CHARMAP[charCode];
if (bMapping) {
if (bMapping & 0x80) {
bMapping &= 0x7f;
fShifted = true;
}
/*
* Since the rest of our code was built around keyCodes, not charCodes, we look up the CHARMAP byte
* in the KEYMAP table to find a corresponding keyCode, and that's what we'll use to simulate the key
* press/release.
*/
var softCode = this.indexOfKeyMap(bMapping);
if (softCode) {
if (!fShifted) {
this.onSoftKeyDown(Keys.KEYCODE.SHIFT, false);
} else {
this.onSoftKeyDown(Keys.KEYCODE.SHIFT, true, true);
}
this.onSoftKeyDown(softCode, true, true);
}
}
if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) {
this.printMessage("oniOSKeyPress(" + charCode + ")", true);
}
return true;
}
/**
* onPaste(event)
*
* @this {Keyboard8080}
* @param {Object} event
* @return {boolean} true to pass the event along, false to consume it
*/
onPaste(event)
{
/*
* TODO: In a perfect world, we would have implemented simulateKeysDown() and simulateKeysUp(),
* which would transform any given text into the appropriate keystrokes. But for now, we're going
* to leapfrog all that and try invoking the SerialPort's sendData() function, which if available,
* is nothing more than a call into a connected machine's receiveData() function.
*
* Besides, paste functionality doesn't seem to be consistently implemented across all browsers
* (partly out of security concerns, apparently) so it may not make sense to expend much more
* effort on this right now. If you want to paste a lot of text into a machine, you're better off
* pasting into a machine that's been configured to use a textarea as part of its Control Panel.
* A visible textarea seems to have less issues than the hidden textarea overlaid on top of our
* Video display.
*/
if (this.serial && this.serial.sendData) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
var clipboardData = event.clipboardData || window.clipboardData;
if (clipboardData) {
this.serial.transmitData(clipboardData.getData('Text'));
return false;
}
}
return true;
}
/**
* indexOfKeyMap(bMapping)
*
* @this {Keyboard8080}
* @param {number} bMapping
* @return {number}
*/
indexOfKeyMap(bMapping)
{
for (var keyCode in this.config.KEYMAP) {
if (this.config.KEYMAP[keyCode] == bMapping) return +keyCode;
}
return 0;
}
/**
* indexOfCharMap(bMapping)
*
* @this {Keyboard8080}
* @param {number} bMapping
* @return {number}
*/
indexOfCharMap(bMapping)
{
for (var charCode in this.config.CHARMAP) {
if (this.config.CHARMAP[charCode] == bMapping) return +charCode;
}
return 0;
}
/**
* indexOfSoftKey(softCode)
*
* @this {Keyboard8080}
* @param {number|string} softCode
* @return {number} index of softCode in aKeysActive, or -1 if not found
*/
indexOfSoftKey(softCode)
{
for (var i = 0; i < this.aKeysActive.length; i++) {
if (this.aKeysActive[i].softCode == softCode) return i;
}
return -1;
}
/**
* onSoftKeyDown(softCode, fDown, fAutoRelease)
*
* @this {Keyboard8080}
* @param {number|string} softCode
* @param {boolean} fDown is true for a down event, false for up
* @param {boolean} [fAutoRelease] is true only if we know we want the key to auto-release
* @return {boolean} true to pass the event along, false to consume it
*/
onSoftKeyDown(softCode, fDown, fAutoRelease)
{
var i = this.indexOfSoftKey(softCode);
if (fDown) {
// this.println(softCode + " down");
if (i < 0) {
this.aKeysActive.push({
softCode: softCode,
msDown: Date.now(),
fAutoRelease: fAutoRelease || false
});
} else {
this.aKeysActive[i].msDown = Date.now();
this.aKeysActive[i].fAutoRelease = fAutoRelease || false;
}
if (fAutoRelease) this.checkSoftKeysToRelease(); // prime the pump
} else if (i >= 0) {
// this.println(softCode + " up");
if (!this.aKeysActive[i].fAutoRelease) {
var msDown = this.aKeysActive[i].msDown;
if (msDown) {
var msElapsed = Date.now() - msDown;
if (msElapsed < Keyboard8080.MINPRESSTIME) {
// this.println(softCode + " released after only " + msElapsed + "ms");
this.aKeysActive[i].fAutoRelease = true;
this.checkSoftKeysToRelease();
return true;
}
}
}
this.aKeysActive.splice(i, 1);
} else {
// this.println(softCode + " up with no down?");
}
if (this.chipset) {
var bit = 0;
switch(softCode) {
case '1p':
bit = ChipSet8080.SI1978.STATUS1.P1;
break;
case '2p':
bit = ChipSet8080.SI1978.STATUS1.P2;
break;
case 'coin':
bit = ChipSet8080.SI1978.STATUS1.CREDIT;
break;
case 'left':
bit = ChipSet8080.SI1978.STATUS1.P1_LEFT;
break;
case 'right':
bit = ChipSet8080.SI1978.STATUS1.P1_RIGHT;
break;
case 'fire':
bit = ChipSet8080.SI1978.STATUS1.P1_FIRE;
break;
}
if (bit) {
this.chipset.updateStatus1(bit, fDown);
}
}
return true;
}
/**
* checkSoftKeysToRelease()
*
* @this {Keyboard8080}
*/
checkSoftKeysToRelease()
{
var i = 0;
var msDelayMin = -1;
while (i < this.aKeysActive.length) {
if (this.aKeysActive[i].fAutoRelease) {
var softCode = this.aKeysActive[i].softCode;
var msDown = this.aKeysActive[i].msDown;
var msElapsed = Date.now() - msDown;
var msDelay = Keyboard8080.MINPRESSTIME - msElapsed;
if (msDelay > 0) {
if (msDelayMin < 0 || msDelayMin > msDelay) {
msDelayMin = msDelay;
}
} else {
/*
* Because the key is already in the auto-release state, this next call guarantees that the
* key will be removed from the array; a consequence of that removal, however, is that we must
* reset our array index to zero.
*/
this.onSoftKeyDown(softCode, false);
i = 0;
continue;
}
}
i++;
}
if (msDelayMin >= 0) {
/*
* Replaced the klunky browser setTimeout() call with our own timer service.
*
* var kbd = this;
* setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin);
*/
this.cpu.setTimer(this.timerReleaseKeys, msDelayMin);
}
}
/**
* isVT100TransmitterReady()
*
* Called whenever the VT100 ChipSet circuit needs the Keyboard UART's transmitter status.
*
* From p. 4-32 of the VT100 Technical Manual (July 1982):
*
* The operating clock for the keyboard interface comes from an address line in the video processor (LBA4).
* This signal has an average period of 7.945 microseconds. Each data byte is transmitted with one start bit
* and one stop bit, and each bit lasts 16 clock periods. The total time for each data byte is 160 times 7.945
* or 1.27 milliseconds. Each time the Transmit Buffer Empty flag on the terminal's UART gets set (when the
* current byte is being transmitted), the microprocessor loads another byte into the transmit buffer. In this
* way, the stream of status bytes to the keyboard is continuous.
*
* We used to always return true (after all, what's wrong with an infinitely fast UART?), but unfortunately,
* the VT100 firmware relies on the UART's slow transmission speed to drive cursor blink rate. We have several
* options:
*
* 1) Snapshot the CPU cycle count each time a byte is transmitted (see outVT100UARTStatus()) and then every
* time this is polled, see if the cycle count has exceeded the snapshot value by the necessary threshold;
* if we assume 361.69ns per CPU cycle, there are 22 CPU cycles for every 1 LBA4 cycle, and since transmission
* time is supposed to last for 160 LBA4 cycles, the threshold is 22*160 CPU cycles, or 3520 cycles.
*
* 2) Set a CPU timer using the new setTimer() interface, which can be passed the number of milliseconds to
* wait before firing (in this case, roughly 1.27ms).
*
* 3) Call the ChipSet's getVT100LBA(4) function for the state of the simulated LBA4, and count 160 LBA4
* transitions; however, that would be the worst solution, because there's no guarantee that the firmware's
* UART polling will occur regularly and/or frequently enough for us to catch every LBA4 transition.
*
* I'm going with solution #1 because it's less overhead.
*
* @this {Keyboard8080}
* @return {boolean} (true if ready, false if not)
*/
isVT100TransmitterReady()
{
if (this.fVT100UARTBusy) {
/*
* NOTE: getMSCycles(1.2731488) should work out to 3520 cycles for a CPU clocked at 361.69ns per cycle,
* which is roughly 2.76Mhz. We could just hard-code 3520 instead of calling getMSCycles(), but this helps
* maintain a reasonable blink rate for the cursor even when the user cranks up the CPU speed.
*/
if (this.cpu.getCycles() >= this.nVT100UARTCycleSnap + this.cpu.getMSCycles(1.2731488)) {
this.fVT100UARTBusy = false;
}
}
return !this.fVT100UARTBusy;
}
/**
* inVT100UARTAddress(port, addrFrom)
*
* We take our cue from iKeyNext. If it's -1 (default), we simply return the last value latched
* in bVT100Address. Otherwise, if iKeyNext is a valid index into aKeysActive, we look up the key
* in the VT100.KEYMAP, latch it, and increment iKeyNext. Failing that, we latch Keyboard8080.VT100.KEYLAST
* and reset iKeyNext to -1.
*
* @this {Keyboard8080}
* @param {number} port (0x82)
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
* @return {number} simulated port value
*/
inVT100UARTAddress(port, addrFrom)
{
var b = this.bVT100Address;
if (this.iKeyNext >= 0) {
if (this.iKeyNext < this.aKeysActive.length) {
var key = this.aKeysActive[this.iKeyNext];
if (!MAXDEBUG) {
this.iKeyNext++;
} else {
/*
* In MAXDEBUG builds, this code removes the key as soon as it's been reported, because
* when debugging, it's easy for the window to lose focus and never receive the keyUp event,
* thereby leaving us with a stuck key. However, this may cause more problems than it solves,
* because the VT100's ROM seems to require that key presses persist for more than a single poll.
*/
this.aKeysActive.splice(this.iKeyNext, 1);
}
b = Keyboard8080.VT100.KEYMAP[key.softCode];
if (b & 0x80) {
/*
* TODO: This code is supposed to be accompanied by a SHIFT key; make sure that it is.
*/
b &= 0x7F;
}
} else {
this.iKeyNext = -1;
b = Keyboard8080.VT100.KEYLAST;
}
this.bVT100Address = b;
this.cpu.requestINTR(1);
}
this.printMessageIO(port, null, addrFrom, "KBDUART.ADDRESS", b);
return b;
}
/**
* outVT100UARTStatus(port, b, addrFrom)
*
* @this {Keyboard8080}
* @param {number} port (0x82)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
outVT100UARTStatus(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "KBDUART.STATUS");
this.bVT100Status = b;
this.fVT100UARTBusy = true;
this.nVT100UARTCycleSnap = this.cpu.getCycles();
this.updateLEDs(b & Keyboard8080.VT100.STATUS.LEDS);
if (b & Keyboard8080.VT100.STATUS.START) {
this.iKeyNext = 0;
this.cpu.requestINTR(1);
}
}
/**
* Keyboard8080.init()
*
* This function operates on every HTML element of class "keyboard", extracting the
* JSON-encoded parameters for the Keyboard constructor from the element's "data-value"
* attribute, invoking the constructor to create a Keyboard component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeKbd = Component.getElementsByClass(document, PC8080.APPCLASS, "keyboard");
for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) {
var eKbd = aeKbd[iKbd];
var parmsKbd = Component.getComponentParms(eKbd);
var kbd = new Keyboard8080(parmsKbd);
Component.bindComponentControls(kbd, eKbd, PC8080.APPCLASS);
}
}
}
/*
* Now that we want to keep track of the physical (and simulated) state of modifier keys, I've
* grabbed a copy of the same bit definitions used by /modules/pcx86/lib/keyboard.js, since it's
* only important that we have a set of unique values; what the values are isn't critical.
*/
Keyboard8080.STATE = {
RSHIFT: 0x0001,
SHIFT: 0x0002,
SHIFTS: 0x0003,
RCTRL: 0x0004, // 101-key keyboard only
CTRL: 0x0008,
CTRLS: 0x000C,
RALT: 0x0010, // 101-key keyboard only
ALT: 0x0020,
ALTS: 0x0030,
RCMD: 0x0040, // 101-key keyboard only
CMD: 0x0080, // 101-key keyboard only
CMDS: 0x00C0,
ALL_RIGHT: 0x0055, // RSHIFT | RCTRL | RALT | RCMD
ALL_SHIFT: 0x00FF, // SHIFT | RSHIFT | CTRL | RCTRL | ALT | RALT | CMD | RCMD
INSERT: 0x0100, // TODO: Placeholder (we currently have no notion of any "insert" states)
CAPS_LOCK: 0x0200,
NUM_LOCK: 0x0400,
SCROLL_LOCK: 0x0800,
ALL_LOCKS: 0x0E00, // CAPS_LOCK | NUM_LOCK | SCROLL_LOCK
REMAPPED: 0x1000
};
Keyboard8080.MINPRESSTIME = 50; // minimum milliseconds to wait before auto-releasing keys
/**
* Alternate keyCode mappings to support popular "WASD"-style directional-key mappings.
*
* TODO: ES6 computed property name support may now be in all mainstream browsers, allowing us to use
* a simple object literal for this and all other object initializations.
*/
Keyboard8080.WASDCODES = {};
Keyboard8080.WASDCODES[Keys.ASCII.A] = Keys.KEYCODE.LEFT;
Keyboard8080.WASDCODES[Keys.ASCII.D] = Keys.KEYCODE.RIGHT;
Keyboard8080.WASDCODES[Keys.ASCII.L] = Keys.KEYCODE.SPACE;
/*
* Supported keyboard configurations.
*
* A word (or two) about SOFTCODES. Their main purpose is to provide a naming convention for machine-specific
* controls, without tying us to any particular keyboard mapping. They are used in two main ways.
*
* First, if we have a binding to the machine's "screen", there will, at a minimum, be onkeydown and onkeyup
* handlers attached to the screen, and those handlers will need to iterate through the SOFTCODES table, looking
* for key codes that we care about and converting them to corresponding soft codes. Some machines, like
* Space Invaders, will then act directly upon the soft code (eg, converting it to a machine-specific status bit).
*
* Second, a machine may have other bindings (eg, buttons) to one or more of these soft codes, and those bindings
* will need to know which key codes they're supposed to generate. Some machines, like the VT100, will then use
* another table (KEYMAP) to convert key codes into a machine-specific "key addresses".
*/
Keyboard8080.SI1978 = {
MODEL: 1978.1,
KEYMAP: {},
CHARMAP: {},
ALTCODES: Keyboard8080.WASDCODES,
LEDCODES: {},
SOFTCODES: {
'1p': Keys.KEYCODE.ONE,
'2p': Keys.KEYCODE.TWO,
'coin': Keys.KEYCODE.THREE,
'left': Keys.KEYCODE.LEFT,
'right': Keys.KEYCODE.RIGHT,
'fire': Keys.KEYCODE.SPACE
}
};
Keyboard8080.VT100 = {
MODEL: 100.0,
KEYMAP: {
/*
* Map of keydown keyCodes to VT100 key addresses (7-bit values representing key positions on the VT100).
*
* NOTE: The VT100 keyboard has both BACKSPACE and DELETE keys, whereas modern keyboards generally only
* have DELETE. And sadly, when you press DELETE, your modern keyboard and/or modern browser is reporting
* it as keyCode 8: the code for BACKSPACE, aka CTRL-H. You have to press a modified DELETE key to get
* the actual DELETE keyCode of 127.
*
* We resolve this below by mapping KEYCODE.BS (8) to VT100 keyCode DELETE (0x03) and KEYCODE.DEL (127)
* to VT100 keyCode BACKSPACE (0x33). So, DELETE is BACKSPACE and BACKSPACE is DELETE. Fortunately, this
* confusion is all internal, because your physical key is (or should be) labeled DELETE, so the fact that
* the browser is converting it to BACKSPACE and that we're converting BACKSPACE back into DELETE is
* something most people don't need to worry their heads about.
*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
[Keys.KEYCODE.BS]: 0x03,
[Keys.ASCII.P]: 0x05,
[Keys.ASCII.O]: 0x06,
[Keys.ASCII.Y]: 0x07,
[Keys.ASCII.T]: 0x08,
[Keys.ASCII.W]: 0x09,
[Keys.ASCII.Q]: 0x0A,
[Keys.KEYCODE.RIGHT]: 0x10,
[Keys.KEYCODE.RBRACK]: 0x14,
[Keys.KEYCODE.LBRACK]: 0x15,
[Keys.ASCII.I]: 0x16,
[Keys.ASCII.U]: 0x17,
[Keys.ASCII.R]: 0x18,
[Keys.ASCII.E]: 0x19,
[Keys.KEYCODE.ONE]: 0x1A,
[Keys.KEYCODE.LEFT]: 0x20,
[Keys.KEYCODE.DOWN]: 0x22,
[Keys.KEYCODE.F6]: 0x23, // aka BREAK
[Keys.KEYCODE.PAUSE]: 0x23, // aka BREAK
[Keys.KEYCODE.BQUOTE]: 0x24,
[Keys.KEYCODE.DASH]: 0x25,
[Keys.KEYCODE.NINE]: 0x26,
[Keys.KEYCODE.SEVEN]: 0x27,
[Keys.KEYCODE.FOUR]: 0x28,
[Keys.KEYCODE.THREE]: 0x29,
[Keys.KEYCODE.ESC]: 0x2A,
[Keys.KEYCODE.UP]: 0x30,
[Keys.KEYCODE.F3]: 0x31, // aka PF3
[Keys.KEYCODE.F1]: 0x32, // aka PF1
[Keys.KEYCODE.DEL]: 0x33,
[Keys.KEYCODE.EQUALS]: 0x34,
[Keys.KEYCODE.ZERO]: 0x35,
[Keys.KEYCODE.EIGHT]: 0x36,
[Keys.KEYCODE.SIX]: 0x37,
[Keys.KEYCODE.FIVE]: 0x38,
[Keys.KEYCODE.TWO]: 0x39,
[Keys.KEYCODE.TAB]: 0x3A,
[Keys.KEYCODE.NUM_7]: 0x40,
[Keys.KEYCODE.F4]: 0x41, // aka PF4
[Keys.KEYCODE.F2]: 0x42, // aka PF2
[Keys.KEYCODE.NUM_0]: 0x43,
[Keys.KEYCODE.F7]: 0x44, // aka LINE-FEED
[Keys.KEYCODE.BSLASH]: 0x45,
[Keys.ASCII.L]: 0x46,
[Keys.ASCII.K]: 0x47,
[Keys.ASCII.G]: 0x48,
[Keys.ASCII.F]: 0x49,
[Keys.ASCII.A]: 0x4A,
[Keys.KEYCODE.NUM_8]: 0x50,
[Keys.KEYCODE.NUM_CR]: 0x51,
[Keys.KEYCODE.NUM_2]: 0x52,
[Keys.KEYCODE.NUM_1]: 0x53,
[Keys.KEYCODE.QUOTE]: 0x55,
[Keys.KEYCODE.SEMI]: 0x56,
[Keys.ASCII.J]: 0x57,
[Keys.ASCII.H]: 0x58,
[Keys.ASCII.D]: 0x59,
[Keys.ASCII.S]: 0x5A,
[Keys.KEYCODE.NUM_DEL]: 0x60, // keypad period
[Keys.KEYCODE.F5]: 0x61, // aka KEYPAD COMMA
[Keys.KEYCODE.NUM_5]: 0x62,
[Keys.KEYCODE.NUM_4]: 0x63,
[Keys.KEYCODE.CR]: 0x64, // TODO: Figure out why the Technical Manual lists CR at both 0x04 and 0x64
[Keys.KEYCODE.PERIOD]: 0x65,
[Keys.KEYCODE.COMMA]: 0x66,
[Keys.ASCII.N]: 0x67,
[Keys.ASCII.B]: 0x68,
[Keys.ASCII.X]: 0x69,
[Keys.KEYCODE.F8]: 0x6A, // aka NO-SCROLL
[Keys.KEYCODE.NUM_9]: 0x70,
[Keys.KEYCODE.NUM_3]: 0x71,
[Keys.KEYCODE.NUM_6]: 0x72,
[Keys.KEYCODE.NUM_SUB]: 0x73, // aka KEYPAD MINUS
[Keys.KEYCODE.SLASH]: 0x75,
[Keys.ASCII.M]: 0x76,
[Keys.ASCII[' ']]: 0x77,
[Keys.ASCII.V]: 0x78,
[Keys.ASCII.C]: 0x79,
[Keys.ASCII.Z]: 0x7A,
[Keys.KEYCODE.F9]: 0x7B, // aka SET-UP
[Keys.KEYCODE.CTRL]: 0x7C,
[Keys.KEYCODE.SHIFT]: 0x7D, // either shift key (doesn't matter)
[Keys.KEYCODE.CAPS_LOCK]:0x7E
},
CHARMAP: {
/*
* Map of keypress charCodes to VT100 key addresses (7-bit values representing key positions on the VT100);
* the 8th bit (0x80) is set for keys that need to be shifted.
*
* This is currently used only with the iOS keypress handler, which processes character codes rather than
* keyboard codes. As a result, this table is not as complete as the KEYMAP table, since certain keys are
* not delivered as key presses (eg, BACKSPACE) and/or are simply not present on the iOS keyboard (eg, ESC,
* arrow keys). Also, SPACE had to be removed from the CHARMAP table as well, because otherwise it causes
* the entire page to scroll down (you have to wonder who thought THAT was a good idea).
*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
[Keys.ASCII.p]: 0x05,
[Keys.ASCII.o]: 0x06,
[Keys.ASCII.y]: 0x07,
[Keys.ASCII.t]: 0x08,
[Keys.ASCII.w]: 0x09,
[Keys.ASCII.q]: 0x0A,
[Keys.ASCII[']']]: 0x14,
[Keys.ASCII['[']]: 0x15,
[Keys.ASCII.i]: 0x16,
[Keys.ASCII.u]: 0x17,
[Keys.ASCII.r]: 0x18,
[Keys.ASCII.e]: 0x19,
[Keys.ASCII['1']]: 0x1A,
[Keys.ASCII['`']]: 0x24,
[Keys.ASCII['-']]: 0x25,
[Keys.ASCII['9']]: 0x26,
[Keys.ASCII['7']]: 0x27,
[Keys.ASCII['4']]: 0x28,
[Keys.ASCII['3']]: 0x29,
[Keys.ASCII['=']]: 0x34,
[Keys.ASCII['0']]: 0x35,
[Keys.ASCII['8']]: 0x36,
[Keys.ASCII['6']]: 0x37,
[Keys.ASCII['5']]: 0x38,
[Keys.ASCII['2']]: 0x39,
[Keys.ASCII['\\']]: 0x45,
[Keys.ASCII.l]: 0x46,
[Keys.ASCII.k]: 0x47,
[Keys.ASCII.g]: 0x48,
[Keys.ASCII.f]: 0x49,
[Keys.ASCII.a]: 0x4A,
[Keys.ASCII["'"]]: 0x55,
[Keys.ASCII[';']]: 0x56,
[Keys.ASCII.j]: 0x57,
[Keys.ASCII.h]: 0x58,
[Keys.ASCII.d]: 0x59,
[Keys.ASCII.s]: 0x5A,
[Keys.KEYCODE.CR]: 0x64, // TODO: Figure out why the Technical Manual lists CR at both 0x04 and 0x64
[Keys.ASCII['.']]: 0x65,
[Keys.ASCII[',']]: 0x66,
[Keys.ASCII.n]: 0x67,
[Keys.ASCII.b]: 0x68,
[Keys.ASCII.x]: 0x69,
[Keys.ASCII['/']]: 0x75,
[Keys.ASCII.m]: 0x76,
// [Keys.ASCII[' ']]: 0x77, // as noted above, we need to process SPACE at keydown rather than keypress
[Keys.ASCII.v]: 0x78,
[Keys.ASCII.c]: 0x79,
[Keys.ASCII.z]: 0x7A,
[Keys.ASCII.P]: 0x85,
[Keys.ASCII.O]: 0x86,
[Keys.ASCII.Y]: 0x87,
[Keys.ASCII.T]: 0x88,
[Keys.ASCII.W]: 0x89,
[Keys.ASCII.Q]: 0x8A,
[Keys.ASCII['}']]: 0x94,
[Keys.ASCII['{']]: 0x95,
[Keys.ASCII.I]: 0x96,
[Keys.ASCII.U]: 0x97,
[Keys.ASCII.R]: 0x98,
[Keys.ASCII.E]: 0x99,
[Keys.ASCII['!']]: 0x9A,
[Keys.ASCII['~']]: 0xA4,
[Keys.ASCII['_']]: 0xA5,
[Keys.ASCII['(']]: 0xA6,
[Keys.ASCII['&']]: 0xA7,
[Keys.ASCII['$']]: 0xA8,
[Keys.ASCII['#']]: 0xA9,
[Keys.ASCII['+']]: 0xB4,
[Keys.ASCII[')']]: 0xB5,
[Keys.ASCII['*']]: 0xB6,
[Keys.ASCII['^']]: 0xB7,
[Keys.ASCII['%']]: 0xB8,
[Keys.ASCII['@']]: 0xB9,
[Keys.ASCII['|']]: 0xC5,
[Keys.ASCII.L]: 0xC6,
[Keys.ASCII.K]: 0xC7,
[Keys.ASCII.G]: 0xC8,
[Keys.ASCII.F]: 0xC9,
[Keys.ASCII.A]: 0xCA,
[Keys.ASCII['"']]: 0xD5,
[Keys.ASCII[':']]: 0xD6,
[Keys.ASCII.J]: 0xD7,
[Keys.ASCII.H]: 0xD8,
[Keys.ASCII.D]: 0xD9,
[Keys.ASCII.S]: 0xDA,
[Keys.ASCII['>']]: 0xE5,
[Keys.ASCII['<']]: 0xE6,
[Keys.ASCII.N]: 0xE7,
[Keys.ASCII.B]: 0xE8,
[Keys.ASCII.X]: 0xE9,
[Keys.ASCII['?']]: 0xF5,
[Keys.ASCII.M]: 0xF6,
[Keys.ASCII.V]: 0xF8,
[Keys.ASCII.C]: 0xF9,
[Keys.ASCII.Z]: 0xFA
},
ALTCODES: {},
LEDCODES: {},
SOFTCODES: {
'caps-lock': Keys.KEYCODE.CAPS_LOCK,
'ctrl': Keys.KEYCODE.CTRL,
'esc': Keys.KEYCODE.ESC,
'tab': Keys.KEYCODE.TAB,
'num-comma': Keys.KEYCODE.F5, // since modern keypads don't typically have a comma...
'break': Keys.KEYCODE.F6,
'line-feed': Keys.KEYCODE.F7,
'no-scroll': Keys.KEYCODE.F8,
'setup': Keys.KEYCODE.F9
},
/*
* Reading port 0x82 returns a key address from the VT100 keyboard's UART data output.
*
* Every time a keyboard scan is initiated (by setting the START bit of the status byte),
* our internal address index (iKeyNext) is set to zero, and an interrupt is generated for
* each entry in the aKeysActive array, along with a final interrupt for KEYLAST.
*/
ADDRESS: {
PORT: 0x82,
INIT: 0x7F
},
/*
* Writing port 0x82 updates the VT100's keyboard status byte via the keyboard's UART data input.
*/
STATUS: {
PORT: 0x82, // write-only
LED4: 0x01,
LED3: 0x02,
LED2: 0x04,
LED1: 0x08,
LOCKED: 0x10,
LOCAL: 0x20,
LEDS: 0x3F, // all LEDs
START: 0x40, // set to initiate a scan
/*
* From p. 4-38 of the VT100 Technical Manual (July 1982):
*
* A bit (CLICK) in the keyboard status word controls the bell.... When a single status word contains
* the bell bit, flip-flop E3 toggles and turns on E1, generating a click. If the bell bit is set for
* many words in succession, the UART latch holds the data output constant..., allowing the circuit to
* produce an 800 hertz tone. Bell is generated by setting the bell bit for 0.25 seconds. Each cycle of
* the tone is at a reduced amplitude compared with the single keyclick.... The overall effect of the
* tone burst on the ear is that of a beep.
*/
CLICK: 0x80,
INIT: 0x00
},
KEYLAST: 0x7F // special end-of-scan key address (all valid key addresses are < KEYLAST)
};
Keyboard8080.VT100.LEDCODES = {
'l4': Keyboard8080.VT100.STATUS.LED4,
'l3': Keyboard8080.VT100.STATUS.LED3,
'l2': Keyboard8080.VT100.STATUS.LED2,
'l1': Keyboard8080.VT100.STATUS.LED1,
'locked': Keyboard8080.VT100.STATUS.LOCKED,
'local': Keyboard8080.VT100.STATUS.LOCAL,
'online': ~Keyboard8080.VT100.STATUS.LOCAL,
'caps-lock':Keyboard8080.STATE.CAPS_LOCK
};
/*
* Supported models and their configurations
*/
Keyboard8080.MODELS = {
"SI1978": Keyboard8080.SI1978,
"VT100": Keyboard8080.VT100
};
Keyboard8080.VT100.INIT = [
[
Keyboard8080.VT100.STATUS.INIT, // bVT100Status
Keyboard8080.VT100.ADDRESS.INIT, // bVT100Address
false, // fVT100UARTBusy
0, // nVT100UARTCycleSnap
-1 // iKeyNext
]
];
/*
* Port notification tables
*/
Keyboard8080.VT100.portsInput = {
0x82: Keyboard8080.prototype.inVT100UARTAddress
};
Keyboard8080.VT100.portsOutput = {
0x82: Keyboard8080.prototype.outVT100UARTStatus
};
/*
* Initialize every Keyboard module on the page.
*/
Web.onInit(Keyboard8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/video.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Video8080 extends Component {
/**
* Video8080(parmsVideo, canvas, context, textarea, container)
*
* The Video8080 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)
* bufferRAM: true to use existing RAM (default is false)
* bufferFormat: if defined, one of the recognized formats in Video8080.FORMATS (eg, "vt100")
* 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)
*
* In addition, if a text-only display is being emulated, define the following properties:
*
* fontROM: URL of font ROM
* fontColor: default is white
* cellWidth: number (eg, 10 for VT100)
* cellHeight: number (eg, 10 for VT100)
*
* 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 the proper location.
*
* @this {Video8080}
* @param {Object} parmsVideo
* @param {Object} [canvas]
* @param {Object} [context]
* @param {Object} [textarea]
* @param {Object} [container]
*/
constructor(parmsVideo, canvas, context, textarea, container)
{
super("Video", parmsVideo, Messages8080.VIDEO);
var video = this;
this.fGecko = Web.isUserAgent("Gecko/");
var i, sEvent, asWebPrefixes = ['', 'moz', 'ms', 'webkit'];
this.cxScreen = parmsVideo['screenWidth'];
this.cyScreen = parmsVideo['screenHeight'];
this.addrBuffer = parmsVideo['bufferAddr'];
this.fUseRAM = parmsVideo['bufferRAM'];
var sFormat = parmsVideo['bufferFormat'];
this.nFormat = sFormat && Video8080.FORMATS[sFormat.toUpperCase()] || Video8080.FORMAT.UNKNOWN;
this.nColsBuffer = parmsVideo['bufferCols'];
this.nRowsBuffer = parmsVideo['bufferRows'];
this.cxCellDefault = this.cxCell = parmsVideo['cellWidth'] || 1;
this.cyCellDefault = this.cyCell = parmsVideo['cellHeight'] || 1;
this.abFontData = null;
this.fDotStretcher = false;
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.rateInterrupt = parmsVideo['interruptRate'];
this.rateRefresh = parmsVideo['refreshRate'] || 60;
this.canvasScreen = canvas;
this.contextScreen = context;
this.textareaScreen = textarea;
this.inputScreen = textarea || canvas || null;
/*
* These variables are here in case we want/need to add support for borders later...
*/
this.xScreenOffset = this.yScreenOffset = 0;
this.cxScreenOffset = this.cxScreen;
this.cyScreenOffset = this.cyScreen;
this.cxScreenCell = (this.cxScreen / this.nColsBuffer)|0;
this.cyScreenCell = (this.cyScreen / this.nRowsBuffer)|0;
/*
* Now that we've finished using nRowsBuffer to help define the screen size, we add one more
* row for text modes, to account for the VT100's scroll line buffer (used for smooth scrolling).
*/
if (this.cyCell > 1) {
this.nRowsBuffer++;
this.bScrollOffset = 0;
this.fSkipSingleCellUpdate = false;
}
/*
* 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 = Web.getURLParm('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;
/*
* TODO: Consider also disallowing any rotateScreen value if bufferRotate was already set; setting
* both is most likely a mistake, but who knows, maybe someone wants to use both for 180-degree rotation?
*/
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);
}
}
/*
* 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);
};
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;
}
}
}
}
this.sFontROM = parmsVideo['fontROM'];
if (this.sFontROM) {
var sFileExt = Str.getExtension(this.sFontROM);
if (sFileExt != "json") {
this.sFontROM = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFontROM + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES;
}
Web.getResource(this.sFontROM, null, true, function(sURL, sResponse, nErrorCode) {
video.doneLoad(sURL, sResponse, nErrorCode);
});
}
this.ledBindings = {};
}
/**
* initBuffers()
*
* @this {Video8080}
* @return {boolean}
*/
initBuffers()
{
/*
* Allocate off-screen buffers now
*/
this.cxBuffer = this.nColsBuffer * this.cxCell;
this.cyBuffer = this.nRowsBuffer * this.cyCell;
var cxBuffer = this.cxBuffer;
var cyBuffer = this.cyBuffer;
if (this.rotateBuffer) {
cxBuffer = this.cyBuffer;
cyBuffer = this.cxBuffer;
}
this.sizeBuffer = 0;
if (!this.fUseRAM) {
this.sizeBuffer = ((this.cxBuffer * this.nBitsPerPixel) >> 3) * this.cyBuffer;
if (!this.bus.addMemory(this.addrBuffer, this.sizeBuffer, Memory8080.TYPE.VIDEO)) {
return false;
}
}
/*
* imageBuffer is only used for graphics modes. For text modes, we create a canvas
* for each font and draw characters by drawing from the font canvas to the target canvas.
*/
if (this.sizeBuffer) {
this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer);
this.nPixelsPerCell = (16 / this.nBitsPerPixel)|0;
this.initCellCache(this.sizeBuffer >> 1);
} else {
/*
* We add an extra column per row to store the visible line length at the start of every row.
*/
this.initCellCache((this.nColsBuffer + 1) * this.nRowsBuffer);
}
this.canvasBuffer = document.createElement("canvas");
this.canvasBuffer.width = cxBuffer;
this.canvasBuffer.height = cyBuffer;
this.contextBuffer = this.canvasBuffer.getContext("2d");
this.aFonts = {};
this.initColors();
if (this.nFormat == Video8080.FORMAT.VT100) {
/*
* Beyond fonts, VT100 support requires that we maintain a number of additional properties:
*
* rateMonitor: must be either 50 or 60 (defaults to 60); we don't emulate the monitor refresh rate,
* but we do need to keep track of which rate has been selected, because that affects the number of
* "fill lines" present at the top of the VT100's frame buffer: 2 lines for 60Hz, 5 lines for 50Hz.
*
* The VT100 July 1982 Technical Manual, p. 4-89, shows the following sample frame buffer layout:
*
* 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
* --------------------------------------------------------------
* 0x2000: 7F 70 03 7F F2 D0 7F 70 06 7F 70 0C 7F 70 0F 7F
* 0x2010: 70 03 .. .. .. .. .. .. .. .. .. .. .. .. .. ..
* ...
* 0x22D0: 'D' 'A' 'T' 'A' ' ' 'F' 'O' 'R' ' ' 'F' 'I' 'R' 'S' 'T' ' ' 'L'
* 0x22E0: 'I' 'N' 'E' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
* ...
* 0x2320: 7F F3 23 'D' 'A' 'T' 'A' ' ' 'F' 'O' 'R' ' ' 'S' 'E' 'C' 'O'
* 0x2330: 'N' 'D' ' ' 'L' 'I' 'N' 'E' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
* ...
* 0x2BE0: ' ' ' ' 'E' 'N' 'D' ' ' 'O' 'F' ' ' 'L' 'A' 'S' 'T' ' ' 'L' 'I'
* 0x2BF0: 'N' 'E' 7F 70 06 .. .. .. .. .. .. .. .. .. .. ..
* 0x2C00: [AVO SCREEN RAM, IF ANY, BEGINS HERE]
*
* ERRATA: The manual claims that if you change the byte at 0x2002 from 03 to 09, the number of "fill
* lines" will change from 2 to 5 (for 50Hz operation), but it shows 06 instead of 0C at location 0x200B;
* if you follow the links, it's pretty clear that byte has to be 0C to yield 5 "fill lines". Since the
* address following the terminator at 0x2006 points to itself, it never makes sense for that terminator
* to be used EXCEPT at the end of the frame buffer.
*
* As an alternative to tracking the monitor refresh rate, we could hard-code some knowledge about how
* the VT100's 8080 code uses memory, and simply ignore lines below address 0x22D0. But the VT100 Video
* Processor makes no such assumption, and it would also break our test code in createFonts(), which
* builds a contiguous screen of test data starting at the default frame buffer address (0x2000).
*/
this.rateMonitor = 60;
/*
* The default character-selectable attribute (reverse video vs. underline) is controlled by fUnderline.
*/
this.fUnderline = false;
this.abLineBuffer = new Array(this.nColsBuffer);
}
return true;
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {Video8080}
* @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
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var video = this;
/*
* TODO: A more general-purpose binding mechanism would be nice someday....
*/
if (sHTMLType == "led" || sHTMLType == "rled") {
this.ledBindings[sBinding] = control;
return true;
}
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;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {Video8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
/*
* Allocate the frame buffer (as needed) along with all other buffers.
*/
this.initBuffers();
/*
* If we have an associated keyboard, then ensure that the keyboard will be notified
* whenever the canvas gets focus and receives input.
*/
this.kbd = /** @type {Keyboard8080} */ (cmp.getMachineComponent("Keyboard"));
if (this.kbd) {
for (var s in this.ledBindings) {
this.kbd.setBinding("led", s, this.ledBindings[s]);
}
if (this.canvasScreen) {
this.kbd.setBinding(this.textareaScreen? "textarea" : "canvas", "screen", this.inputScreen);
}
}
var video = this;
this.timerUpdateNext = this.cpu.addTimer(function() {
video.updateScreen();
});
this.cpu.setTimer(this.timerUpdateNext, this.getRefreshTime());
this.nUpdates = 0;
if (!this.sFontROM) this.setReady();
}
/**
* doneLoad(sURL, sFontData, nErrorCode)
*
* @this {Video8080}
* @param {string} sURL
* @param {string} sFontData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, sFontData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load font ROM (error " + nErrorCode + ": " + sURL + ")");
return;
}
Component.addMachineResource(this.idMachine, sURL, sFontData);
try {
/*
* The most likely source of any exception will be here: parsing the JSON-encoded data.
*/
var ab = eval("(" + sFontData + ")");
var abFontData = ab['bytes'] || ab;
if (!abFontData || !abFontData.length) {
Component.error("Empty font ROM: " + sURL);
return;
}
else if (abFontData.length == 1) {
Component.error(abFontData[0]);
return;
}
/*
* Minimal font data validation, just to make sure we're not getting garbage from the server.
*/
if (abFontData.length == 2048) {
this.abFontData = abFontData;
this.createFonts();
}
else {
this.notice("Unrecognized font data length (" + abFontData.length + ")");
return;
}
} catch (e) {
this.notice("Font ROM data error: " + e.message);
return;
}
/*
* If we're still here, then we're ready!
*
* UPDATE: Per issue #21, I'm issuing setReady() *only* if a valid contextScreen exists *or* a Debugger is attached.
*
* TODO: Consider a more general-purpose solution for deciding whether or not the user wants to run in a "headless" mode.
*/
if (this.contextScreen || this.dbg) this.setReady();
}
/**
* createFonts()
*
* @this {Video8080}
* @return {boolean}
*/
createFonts()
{
/*
* We retain abFontData in case we have to rebuild the fonts (eg, when we switch from 80 to 132 columns)
*/
if (this.abFontData) {
this.fDotStretcher = (this.nFormat == Video8080.FORMAT.VT100);
this.aFonts[Video8080.VT100.FONT.NORML] = [
this.createFontVariation(this.cxCell, this.cyCell),
this.createFontVariation(this.cxCell, this.cyCell, this.fUnderline)
];
this.aFonts[Video8080.VT100.FONT.DWIDE] = [
this.createFontVariation(this.cxCell*2, this.cyCell),
this.createFontVariation(this.cxCell*2, this.cyCell, this.fUnderline)
];
this.aFonts[Video8080.VT100.FONT.DHIGH] = this.aFonts[Video8080.VT100.FONT.DHIGH_BOT] = [
this.createFontVariation(this.cxCell*2, this.cyCell*2),
this.createFontVariation(this.cxCell*2, this.cyCell*2, this.fUnderline)
];
return true;
}
return false;
}
/**
* createFontVariation(cxCell, cyCell, fUnderline)
*
* This creates a 16x16 character grid for the requested font variation. Variations include:
*
* 1) no variation (cell size is this.cxCell x this.cyCell)
* 2) double-wide characters (cell size is this.cxCell*2 x this.cyCell)
* 3) double-high double-wide characters (cell size is this.cxCell*2 x this.cyCell*2)
* 4) any of the above with either reverse video or underline enabled (default is neither)
*
* @this {Video8080}
* @param {number} cxCell is the target width of each character in the grid
* @param {number} cyCell is the target height of each character in the grid
* @param {boolean} [fUnderline] (null for unmodified font, false for reverse video, true for underline)
* @return {Object}
*/
createFontVariation(cxCell, cyCell, fUnderline)
{
/*
* On a VT100, cxCell,cyCell is initially 10,10, but may change to 9,10 for 132-column mode.
*/
/*
* Create a font canvas that is both 16 times the target character width and the target character height,
* ensuring that it will accommodate 16x16 characters (for a maximum of 256). Note that the VT100 font ROM
* defines only 128 characters, so that canvas will contain only 16x8 entries.
*/
var nFontBytesPerChar = this.cxCellDefault <= 8? 8 : 16;
var nFontByteOffset = nFontBytesPerChar > 8? 15 : 0;
var nChars = this.abFontData.length / nFontBytesPerChar;
/*
* The absence of a boolean for fUnderline means that both fReverse and fUnderline are "falsey". The presence
* of a boolean means that fReverse will be true OR fUnderline will be true, but NOT both.
*/
var fReverse = (fUnderline === false);
var font = {cxCell: cxCell, cyCell: cyCell};
font.canvas = document.createElement("canvas");
font.canvas.width = cxCell * 16;
font.canvas.height = cyCell * (nChars / 16);
font.context = font.canvas.getContext("2d");
var imageChar = font.context.createImageData(cxCell, cyCell);
for (var iChar = 0; iChar < nChars; iChar++) {
for (var y = 0, yDst = y; y < this.cyCell; y++) {
var offFontData = iChar * nFontBytesPerChar + ((nFontByteOffset + y) & (nFontBytesPerChar - 1));
var bits = (fUnderline && y == 8? 0xff : this.abFontData[offFontData]);
for (var nRows = 0; nRows < (cyCell / this.cyCell); nRows++) {
var bitPrev = 0;
for (var x = 0, xDst = x; x < this.cxCell; x++) {
/*
* While x goes from 0 to cxCell-1, obviously we will run out of bits after x is 7;
* since the final bit must be replicated all the way to the right edge of the cell
* (so that line-drawing characters seamlessly connect), we ensure that the effective
* shift count remains stuck at 7 once it reaches 7.
*/
var bitReal = bits & (0x80 >> (x > 7? 7 : x));
var bit = (this.fDotStretcher && !bitReal && bitPrev)? bitPrev : bitReal;
for (var nCols = 0; nCols < (cxCell / this.cxCell); nCols++) {
if (fReverse) bit = !bit;
this.setPixel(imageChar, xDst, yDst, bit? 1 : 0);
xDst++;
}
bitPrev = bitReal;
}
yDst++;
}
}
/*
* (iChar >> 4) performs the integer equivalent of Math.floor(iChar / 16), and (iChar & 0xf) is the equivalent of (iChar % 16).
*/
font.context.putImageData(imageChar, (iChar & 0xf) * cxCell, (iChar >> 4) * cyCell);
}
return font;
}
/**
* powerUp(data, fRepower)
*
* @this {Video8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (data) {
if (!this.restore(data)) return false;
}
}
/*
* Because the VT100 frame buffer can be located anywhere in RAM (above 0x2000), we must defer this
* test code until the powerUp() notification handler is called, when all RAM has (hopefully) been allocated.
*
* NOTE: The following test screen was useful for early testing, but a *real* VT100 doesn't display a test screen,
* so this code is no longer enabled by default. Remove MAXDEBUG if you want to see it again.
*/
if (MAXDEBUG && this.nFormat == Video8080.FORMAT.VT100) {
/*
* Build a test screen in the VT100 frame buffer; we'll mimic the "SET-UP A" screen, since it uses
* all the font variations. The process involves iterating over 0-based row numbers -2 (or -5 if 50Hz
* operation is selected) through 24, checking aLineData for a matching row number, and converting the
* corresponding string(s) to appropriate byte values. Negative row numbers correspond to "fill lines"
* and do not require a row entry. If multiple strings are present for a given row, we invert the
* default character attribute for subsequent strings. An empty array ends the screen build process.
*/
var aLineData = {
0: [Video8080.VT100.FONT.DHIGH, 'SET-UP A'],
2: [Video8080.VT100.FONT.DWIDE, 'TO EXIT PRESS "SET-UP"'],
22: [Video8080.VT100.FONT.NORML, ' T T T T T T T T T'],
23: [Video8080.VT100.FONT.NORML, '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890'],
24: []
};
var addr = this.addrBuffer;
var addrNext = -1, font = -1;
var b, nFill = (this.rateMonitor == 60? 2 : 5);
for (var iRow = -nFill; iRow < this.nRowsBuffer; iRow++) {
var lineData = aLineData[iRow];
if (addrNext >= 0) {
var fBreak = false;
addrNext = addr + 2;
if (!lineData) {
if (font == Video8080.VT100.FONT.DHIGH) {
lineData = aLineData[iRow-1];
font = Video8080.VT100.FONT.DHIGH_BOT;
}
}
else {
if (lineData.length) {
font = lineData[0];
} else {
addrNext = addr - 1;
fBreak = true;
}
}
b = (font & Video8080.VT100.LINEATTR.FONTMASK) | ((addrNext >> 8) & Video8080.VT100.LINEATTR.ADDRMASK) | Video8080.VT100.LINEATTR.ADDRBIAS;
this.bus.setByteDirect(addr++, b);
this.bus.setByteDirect(addr++, addrNext & 0xff);
if (fBreak) break;
}
if (lineData) {
var attr = 0;
for (var j = 1; j < lineData.length; j++) {
var s = lineData[j];
for (var k = 0; k < s.length; k++) {
this.bus.setByteDirect(addr++, s.charCodeAt(k) | attr);
}
attr ^= 0x80;
}
}
this.bus.setByteDirect(addr++, Video8080.VT100.LINETERM);
addrNext = addr;
}
/*
* NOTE: By calling updateVT100() directly, we are bypassing any checks that might block the update.
*/
this.updateVT100();
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Video8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return !fSave || this.save();
}
/**
* save()
*
* This implements save support for the Video8080 component.
*
* @this {Video8080}
* @return {Object|null}
*/
save()
{
var state = new State(this);
state.set(0, []);
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the Video8080 component.
*
* @this {Video8080}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
return true;
}
/**
* updateDimensions(nCols, nRows)
*
* Called from the ChipSet component whenever the screen dimensions have been dynamically altered.
*
* @this {Video8080}
* @param {number} nCols (should be either 80 or 132; 80 is the default)
* @param {number} nRows (should be either 24 or 14; 24 is the default)
*/
updateDimensions(nCols, nRows)
{
this.printMessage("updateDimensions(" + nCols + "," + nRows + ")");
this.nColsBuffer = nCols;
/*
* Even when the number of effective rows is 14 (or 15 counting the scroll line buffer), we want
* to leave the number of rows at 24 (or 25 counting the scroll line buffer), because the VT100 doesn't
* actually change character height (only character width).
*
* this.nRowsBuffer = nRows+1; // +1 for scroll line buffer
*/
this.cxCell = this.cxCellDefault;
if (nCols > 80) this.cxCell--; // VT100 font cells are 9x10 instead of 10x10 in 132-column mode
if (this.initBuffers()) {
this.createFonts();
}
}
/**
* updateRate(nRate)
*
* Called from the ChipSet component whenever the monitor refresh rate has been dynamically altered.
*
* @this {Video8080}
* @param {number} nRate (should be either 50 or 60; 60 is the default)
*/
updateRate(nRate)
{
this.printMessage("updateRate(" + nRate + ")");
this.rateMonitor = nRate;
}
/**
* updateScrollOffset(bScroll)
*
* Called from the ChipSet component whenever the screen scroll offset has been dynamically altered.
*
* @this {Video8080}
* @param {number} bScroll
*/
updateScrollOffset(bScroll)
{
this.printMessage("updateScrollOffset(" + bScroll + ")");
if (this.bScrollOffset !== bScroll) {
this.bScrollOffset = bScroll;
/*
* WARNING: If we immediately redraw the screen on the first wrap of the scroll offset back to zero,
* we end up "slamming" the screen's contents back down again, because it seems that the frame buffer
* contents haven't actually been scrolled yet. So we redraw now ONLY if bScroll is non-zero, lest
* we ruin the smooth-scroll effect.
*
* And this change, while necessary, is not sufficient, because another intervening updateScreen()
* call could still occur before the frame buffer contents are actually scrolled; and ordinarily, if the
* buffer hasn't changed, updateScreen() would do nothing, but alas, if the cursor happens to get toggled
* in the interim, updateScreen() will want to update exactly ONE cell.
*
* So we deal with that by setting the fSkipSingleCellUpdate flag. Now of course, there's no guarantee
* that the next update of only ONE cell will always be a cursor update, but even if it isn't, skipping
* that update doesn't seem like a huge cause for concern.
*/
if (bScroll) {
this.updateScreen(true);
} else {
this.fSkipSingleCellUpdate = true;
}
}
}
/**
* doFullScreen()
*
* @this {Video8080}
* @return {boolean} true if request successful, false if not (eg, failed OR not supported)
*/
doFullScreen()
{
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 {Video8080}
* @param {boolean|null} fFullScreen (null if there was a full-screen error)
*/
notifyFullScreen(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 + ")");
}
/**
* setFocus()
*
* @this {Video8080}
*/
setFocus()
{
if (this.inputScreen) this.inputScreen.focus();
}
/**
* getRefreshTime()
*
* @this {Video8080}
* @return {number} (number of milliseconds per refresh)
*/
getRefreshTime()
{
return 1000 / Math.max(this.rateRefresh, this.rateInterrupt);
}
/**
* initCellCache(nCells)
*
* Initializes the contents of our internal cell cache.
*
* @this {Video8080}
* @param {number} nCells
*/
initCellCache(nCells)
{
this.nCellCache = nCells;
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 {Video8080}
*/
initColors()
{
var rgbBlack = [0x00, 0x00, 0x00, 0xff];
var rgbWhite = [0xff, 0xff, 0xff, 0xff];
this.nColors = (1 << this.nBitsPerPixel);
this.aRGB = new Array(this.nColors + Video8080.COLORS.OVERLAY_TOTAL);
this.aRGB[0] = rgbBlack;
this.aRGB[1] = rgbWhite;
if (this.nFormat == Video8080.FORMAT.SI1978) {
var rgbGreen = [0x00, 0xff, 0x00, 0xff];
//noinspection UnnecessaryLocalVariableJS
var rgbYellow = [0xff, 0xff, 0x00, 0xff];
this.aRGB[this.nColors + Video8080.COLORS.OVERLAY_TOP] = rgbYellow;
this.aRGB[this.nColors + Video8080.COLORS.OVERLAY_BOTTOM] = rgbGreen;
}
}
/**
* setPixel(image, x, y, bPixel)
*
* @this {Video8080}
* @param {Object} image
* @param {number} x
* @param {number} y
* @param {number} bPixel (ie, an index into aRGB)
*/
setPixel(image, x, y, bPixel)
{
var index;
if (!this.rotateBuffer) {
index = (x + y * image.width);
} else {
index = (image.height - x - 1) * image.width + y;
}
if (bPixel && this.nFormat == Video8080.FORMAT.SI1978) {
if (x >= 208 && x < 236) {
bPixel = this.nColors + Video8080.COLORS.OVERLAY_TOP;
}
else if (x >= 28 && x < 72) {
bPixel = this.nColors + Video8080.COLORS.OVERLAY_BOTTOM;
}
}
var rgb = this.aRGB[bPixel];
index *= rgb.length;
image.data[index] = rgb[0];
image.data[index+1] = rgb[1];
image.data[index+2] = rgb[2];
image.data[index+3] = rgb[3];
}
/**
* updateChar(idFont, col, row, data, context)
*
* Updates a particular character cell (row,col) in the associated window.
*
* @this {Video8080}
* @param {number} idFont
* @param {number} col
* @param {number} row
* @param {number} data
* @param {Object} [context]
*/
updateChar(idFont, col, row, data, context)
{
var bChar = data & 0x7f;
var font = this.aFonts[idFont][(data & 0x80)? 1 : 0];
if (!font) return;
var xSrc = (bChar & 0xf) * font.cxCell;
var ySrc = (bChar >> 4) * font.cyCell;
var xDst, yDst, cxDst, cyDst;
var cxSrc = font.cxCell;
var cySrc = font.cyCell;
if (context) {
xDst = col * this.cxCell;
yDst = row * this.cyCell;
cxDst = this.cxCell;
cyDst = this.cyCell;
} else {
xDst = col * this.cxScreenCell;
yDst = row * this.cyScreenCell;
cxDst = this.cxScreenCell;
cyDst = this.cyScreenCell;
}
/*
* If font.cxCell > this.cxCell, then we assume the caller wants to draw a double-wide character,
* so we will double xDst and cxDst.
*/
if (font.cxCell > this.cxCell) {
xDst *= 2;
cxDst *= 2;
}
/*
* If font.cyCell > this.cyCell, then we rely on idFont to indicate whether the top half or bottom half
* of the character should be drawn.
*/
if (font.cyCell > this.cyCell) {
if (idFont == Video8080.VT100.FONT.DHIGH_BOT) ySrc += this.cyCell;
cySrc = this.cyCell;
}
if (context) {
context.drawImage(font.canvas, xSrc, ySrc, cxSrc, cySrc, xDst, yDst, cxDst, cyDst);
} else {
xDst += this.xScreenOffset;
yDst += this.yScreenOffset;
this.contextScreen.drawImage(font.canvas, xSrc, ySrc, cxSrc, cySrc, xDst, yDst, cxDst, cyDst);
}
}
/**
* updateVT100(fForced)
*
* @this {Video8080}
* @param {boolean} [fForced]
*/
updateVT100(fForced)
{
var addrNext = this.addrBuffer, fontNext = -1;
var nRows = 0;
var nFill = (this.rateMonitor == 60? 2 : 5);
var iCell = 0, cUpdated = 0, iCellUpdated = -1;
while (nRows < this.nRowsBuffer) {
/*
* Populate the line buffer
*/
var nCols = 0;
var addr = addrNext;
var font = fontNext;
var nColsVisible = this.nColsBuffer;
if (font != Video8080.VT100.FONT.NORML) nColsVisible >>= 1;
while (true) {
var data = this.bus.getByteDirect(addr++);
if ((data & Video8080.VT100.LINETERM) == Video8080.VT100.LINETERM) {
var b = this.bus.getByteDirect(addr++);
fontNext = b & Video8080.VT100.LINEATTR.FONTMASK;
addrNext = ((b & Video8080.VT100.LINEATTR.ADDRMASK) << 8) | this.bus.getByteDirect(addr);
addrNext += (b & Video8080.VT100.LINEATTR.ADDRBIAS)? Video8080.VT100.ADDRBIAS_LO : Video8080.VT100.ADDRBIAS_HI;
break;
}
if (nCols < nColsVisible) {
this.abLineBuffer[nCols++] = data;
} else {
break; // ideally, we would wait for a LINETERM byte, but it's not safe to loop without limit
}
}
/*
* Skip the first few "fill lines"
*/
if (nFill) {
nFill--;
continue;
}
/*
* Pad the line buffer as needed
*/
while (nCols < this.abLineBuffer.length) {
this.abLineBuffer[nCols++] = 0; // character code 0 is a empty font character
}
/*
* Display the line buffer; ordinarily, the font number would be valid after processing the "fill lines",
* but if the buffer isn't initialized yet, those lines might be missing, so the font number might not be set.
*/
if (font >= 0) {
/*
* Cell cache logic is complicated by the fact that a line may be single-width one frame and double-width
* the next. So we store the visible line length at the start of each row in the cache, which must match if
* the cache can be considered valid for the current line.
*/
var fLineCacheValid = this.fCellCacheValid && (this.aCellCache[iCell] == nColsVisible);
this.aCellCache[iCell++] = nColsVisible;
for (var iCol = 0; iCol < nCols; iCol++) {
data = this.abLineBuffer[iCol];
if (!fLineCacheValid || data !== this.aCellCache[iCell]) {
this.aCellCache[iCellUpdated = iCell] = data;
this.updateChar(font, iCol, nRows, data, this.contextBuffer);
cUpdated++;
}
iCell++;
}
}
nRows++;
}
this.fCellCacheValid = true;
if (!fForced && this.fSkipSingleCellUpdate && cUpdated == 1) {
/*
* We're going to blow off this update, since it comes on the heels of a smooth-scroll that *may*
* not be completely finished yet, and at the same time, we're going to zap the only updated cell
* cache entry, to guarantee that it's redrawn on the next update.
*/
/*
* TODO: If I change the RECV rate to 19200 and enable smooth scrolling, I sometimes see a spurious
* "H" on the bottom line after a long series of "HELLO WORLD!\r\n" tests. Dumping video memory shows
* "HELLO WORLD!" on 23 lines and an "H" on the 24th line, so it's really there. But strangely, if
* I then press SET-UP two times, the restored screen does NOT have the spurious "H". So somehow the
* firmware knows what should and shouldn't be on-screen.
*
* Possible VT100 firmware bug? I'm not sure. Anyway, this DEBUG-only code is here to help trap
* that scenario, until I figure it out.
*/
if (DEBUG && (this.aCellCache[iCellUpdated] & 0x7f) == 0x48) {
console.log("spurious character?");
}
this.aCellCache[iCellUpdated] = -1;
cUpdated = 0;
}
this.fSkipSingleCellUpdate = false;
if ((cUpdated || fForced) && this.contextBuffer) {
/*
* We must subtract cyCell from cyBuffer to avoid displaying the extra "scroll line" that we normally
* buffer, in support of smooth scrolling. Speaking of which, we must also add bScrollOffset to ySrc
* (well, ySrc is always relative to zero, so no add is actually required).
*/
this.contextScreen.drawImage(
this.canvasBuffer,
0, // xSrc
this.bScrollOffset, // ySrc
this.cxBuffer, // cxSrc
this.cyBuffer - this.cyCell, // cySrc
this.xScreenOffset, // xDst
this.yScreenOffset, // yDst
this.cxScreenOffset, // cxDst
this.cyScreenOffset // cyDst
);
}
}
/**
* updateScreen(fForced)
*
* 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 initCellCache() 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 {Video8080}
* @param {boolean} [fForced]
*/
updateScreen(fForced)
{
var fUpdate = true;
if (!fForced) {
if (this.rateInterrupt) {
/*
* TODO: Incorporate these hard-coded interrupt vector numbers into configuration blocks.
*/
if (this.rateInterrupt == 120) {
if (!(this.nUpdates & 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);
fUpdate = false;
}
} else {
this.cpu.requestINTR(4);
}
}
/*
* Since this is not a forced update, if our cell cache is valid AND we allocated our own buffer AND the buffer
* is clean, then there's nothing to do.
*/
if (fUpdate && this.fCellCacheValid && this.sizeBuffer) {
if (this.bus.cleanMemory(this.addrBuffer, this.sizeBuffer)) {
fUpdate = false;
}
}
this.cpu.setTimer(this.timerUpdateNext, this.getRefreshTime());
this.nUpdates++;
}
if (!fUpdate) {
return;
}
if (this.cxCell > 1) {
this.updateScreenText(fForced);
} else {
this.updateScreenGraphics(fForced);
}
}
/**
* updateScreenText(fForced)
*
* @this {Video8080}
* @param {boolean} [fForced]
*/
updateScreenText(fForced)
{
switch(this.nFormat) {
case Video8080.FORMAT.VT100:
this.updateVT100(fForced);
break;
}
}
/**
* updateScreenGraphics(fForced)
*
* @this {Video8080}
* @param {boolean} [fForced]
*/
updateScreenGraphics(fForced)
{
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);
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);
/*
* As originally noted in /modules/pcx86/lib/video.js, I would prefer to draw only the dirty portion of
* canvasBuffer, but there usually isn't a 1-1 pixel mapping between canvasBuffer and contextScreen, so
* if we draw interior rectangles, we can end up with subpixel artifacts along the edges of those rectangles.
*/
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.canvasBuffer.width, this.canvasBuffer.height, 0, 0, this.cxScreen, this.cyScreen);
}
}
/**
* Video8080.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.
*/
static init()
{
var aeVideo = Component.getElementsByClass(document, PC8080.APPCLASS, "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 = "<br/>Missing &lt;canvas&gt; 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'] || Web.getURLParm('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");
/*
* One of the problems on iOS devices is that after a soft-key control is clicked, we need to give
* focus back to the above textarea, usually by calling cmp.updateFocus(), but in doing so, iOS may
* also "zoom" the page rather jarringly. While it's a simple matter to completely disable zooming,
* by fiddling with the page's viewport, that prevents the user from intentionally zooming. A bit of
* Googling reveals that another way to prevent those jarring unintentional zooms is to simply set the
* font-size of the text control to 16px. So that's what we do.
*/
eTextArea.style.fontSize = "16px";
}
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 Video8080(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, PC8080.APPCLASS);
}
}
}
Video8080.COLORS = {
OVERLAY_TOP: 0,
OVERLAY_BOTTOM: 1,
OVERLAY_TOTAL: 2
};
Video8080.FORMAT = {
UNKNOWN: 0,
SI1978: 1,
VT100: 2
};
Video8080.FORMATS = {
"SI1978": Video8080.FORMAT.SI1978,
"VT100": Video8080.FORMAT.VT100
};
Video8080.VT100 = {
/*
* The following font IDs are nothing more than all the possible LINEATTR values masked with FONTMASK;
* also, note that double-high implies double-wide; the VT100 doesn't support a double-high single-wide font.
*/
FONT: {
NORML: 0x60, // normal font (eg, 10x10)
DWIDE: 0x40, // double-wide, single-high font (eg, 20x10)
DHIGH: 0x20, // technically, this means display only the TOP half of the double-high font (eg, 20x20)
DHIGH_BOT: 0x00 // technically, this means display only the BOTTOM half of the double-high font (eg, 20x20)
},
LINETERM: 0x7F,
LINEATTR: {
ADDRMASK: 0x0F,
ADDRBIAS: 0x10, // 0x10 == ADDRBIAS_LO, 0x00 = ADDRBIAS_HI
FONTMASK: 0x60,
SCROLL: 0x80
},
ADDRBIAS_LO: 0x2000,
ADDRBIAS_HI: 0x4000
};
/*
* Initialize every Video module on the page.
*/
Web.onInit(Video8080.init);
/**
* @copyright http://pcjs.org/modules/pc8080/lib/serial.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class SerialPort8080 extends Component {
/**
* SerialPort8080(parmsSerial)
*
* The SerialPort8080 component has the following component-specific (parmsSerial) properties:
*
* adapter: 0 if not defined
*
* binding: name of a control (based on its "binding" attribute) to bind to this port's I/O
*
* tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to
* the above binding); default is 0 (no conversion)
*
* In the future, we may support 'port' and 'irq' properties that allow the machine to define a
* non-standard serial port configuration, instead of only our pre-defined 'adapter' configurations.
*
* NOTE: Since the XSL file defines 'adapter' as a number, not a string, there's no need to use
* parseInt(), and as an added benefit, we don't need to worry about whether a hex or decimal format
* was used.
*
* @this {SerialPort8080}
* @param {Object} parmsSerial
*/
constructor(parmsSerial)
{
super("SerialPort", parmsSerial, Messages8080.SERIAL);
this.iAdapter = +parmsSerial['adapter'];
switch (this.iAdapter) {
case 0:
this.portBase = 0;
this.nIRQ = 2;
break;
default:
Component.warning("Unrecognized serial adapter #" + this.iAdapter);
return;
}
/**
* consoleOutput becomes a string that records serial port output if the 'binding' property is set to the
* reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output
* or the string length reaches a threshold (currently, 1024 characters).
*
* @type {string|null}
*/
this.consoleOutput = null;
/**
* controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()).
*
* @type {Object}
*/
this.controlIOBuffer = null;
/*
* If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters
* being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate
* number of spaces.
*
* charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably
* isn't generally useful; I use it internally to preformat serial output.
*/
this.tabSize = parmsSerial['tabSize'];
this.charBOL = parmsSerial['charBOL'];
this.iLogicalCol = 0;
/*
* fAutoXOFF enables some experimental auto-XOFF/XON processing. It assumes if the VT100 firmware
* issues an XOFF, receiveByte() should stop accepting more data until the firmware issues an XOFF.
*
* The downside is that this doesn't really do anything to stem the flow of incoming data; it just
* prevents the VT100's internal buffer from overflowing. TODO: Eliminate the need for this hack
* and add some *real* flow-control interfaces between connected SerialPort components.
*/
this.fAutoXOFF = true;
this.fAutoStop = false;
this.fNullModem = true;
var sBinding = parmsSerial['binding'];
if (sBinding == "console") {
this.consoleOutput = "";
} else {
/*
* NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing.
*/
Component.bindExternalControl(this, sBinding, SerialPort8080.sIOBuffer);
}
/*
* No connection until initConnection() is called.
*/
this.sDataReceived = "";
this.connection = this.sendData = this.updateStatus = null;
/*
* Export all functions required by initConnection().
*/
this['exports'] = {
'connect': this.initConnection,
'receiveData': this.receiveData,
'receiveStatus': this.receiveStatus
};
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {SerialPort8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "buffer")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var serial = this;
switch (sBinding) {
case SerialPort8080.sIOBuffer:
this.bindings[sBinding] = this.controlIOBuffer = control;
/*
* By establishing an onkeypress handler here, we make it possible for DOS commands like
* "CTTY COM1" to more or less work (use "CTTY CON" to restore control to the DOS console).
*/
control.onkeydown = function onKeyDown(event) {
/*
* This is required in addition to onkeypress, because it's the only way to prevent
* BACKSPACE (keyCode 8) from being interpreted by the browser as a "Back" operation;
* moreover, not all browsers generate an onkeypress notification for BACKSPACE.
*
* A related problem exists for Ctrl-key combinations in most Windows-based browsers
* (eg, IE, Edge, Chrome for Windows, etc), because keys like Ctrl-C and Ctrl-S have
* special meanings (eg, Copy, Save). To the extent the browser will allow it, we
* attempt to disable that default behavior when this control receives an onkeydown
* event for one of those keys (probably the only event the browser generates for them).
*/
event = event || window.event;
var keyCode = event.keyCode;
if (keyCode === 0x08 || event.ctrlKey && keyCode >= 0x41 && keyCode <= 0x5A) {
if (event.preventDefault) event.preventDefault();
if (keyCode > 0x40) keyCode -= 0x40;
serial.receiveByte(keyCode);
}
return true;
};
control.onkeypress = function onKeyPress(event) {
/*
* Browser-independent keyCode extraction; refer to onKeyPress() and the other key event
* handlers in keyboard.js.
*/
event = event || window.event;
var keyCode = event.which || event.keyCode;
serial.receiveByte(keyCode);
/*
* Since we're going to remove the "readonly" attribute from the <textarea> control
* (so that the soft keyboard activates on iOS), instead of calling preventDefault() for
* selected keys (eg, the SPACE key, whose default behavior is to scroll the page), we must
* now call it for *all* keys, so that the keyCode isn't added to the control immediately,
* on top of whatever the machine is echoing back, resulting in double characters.
*/
if (event.preventDefault) event.preventDefault();
return true;
};
/*
* Now that we've added an onkeypress handler that calls preventDefault() for ALL keys, the control
* itself no longer needs the "readonly" attribute; we primarily need to remove it for iOS browsers,
* so that the soft keyboard will activate, but it shouldn't hurt to remove the attribute for all browsers.
*/
control.removeAttribute("readonly");
return true;
default:
if (sValue) {
/*
* Instead of just having a dedicated "test" control, we now treat any unrecognized control with
* a "value" attribute as a test control. The only caveat is that such controls must have binding IDs
* that do not conflict with predefined controls (which, of course, is the only way you can get here).
*/
this.bindings[sBinding] = control;
/*
* Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash
* characters, by virtue of the eval() function that all our component parameter strings pass through;
* eval() treats strings like "source code", so any backslash sequence that JavaScript supports is
* automatically converted.
*
* The complete list of backslash sequences supported by JavaScript:
*
* \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX
* ^J ^M ^K ^I ^H ^L
*
* To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX
* is the ASCII code in hex. For ESC, that would \x1B.
*/
control.onclick = function onClickTest(event) {
serial.receiveData(sValue);
/*
* Give focus back to the machine (since clicking the button takes focus away).
*
* if (serial.cmp) serial.cmp.updateFocus();
*
* iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down
* (assuming it was already down).
*/
return true;
};
return true;
}
break;
}
return false;
}
/**
* echoByte(b)
*
* @this {SerialPort8080}
* @param {number} b
* @return {boolean} true if echo, false if not
*/
echoByte(b)
{
var fEchoed = false;
if (this.controlIOBuffer) {
if (b == 0x08) {
this.controlIOBuffer.value = this.controlIOBuffer.value.slice(0, -1);
/*
* TODO: Back up the correct number of columns if the character erased was a tab.
*/
if (this.iLogicalCol > 0) this.iLogicalCol--;
}
else {
var s = Str.toASCIICode(b);
var nChars = s.length;
if (b == 0x09) {
var tabSize = this.tabSize || 8;
nChars = tabSize - (this.iLogicalCol % tabSize);
if (this.tabSize) s = Str.pad("", nChars);
}
else if (b == 0x0D) {
this.iLogicalCol = nChars = 0;
s = "\n";
}
if (this.charBOL && !this.iLogicalCol && nChars) s = String.fromCharCode(this.charBOL) + s;
this.controlIOBuffer.value += s;
this.controlIOBuffer.scrollTop = this.controlIOBuffer.scrollHeight;
this.iLogicalCol += nChars;
}
fEchoed = true;
}
else if (this.consoleOutput != null) {
if (b == 0x0A || this.consoleOutput.length >= 1024) {
this.println(this.consoleOutput);
this.consoleOutput = "";
}
if (b != 0x0A) {
this.consoleOutput += String.fromCharCode(b);
}
fEchoed = true;
}
return fEchoed;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {SerialPort8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
var serial = this;
this.timerReceiveNext = this.cpu.addTimer(function() {
serial.receiveData();
});
this.timerTransmitNext = this.cpu.addTimer(function() {
serial.transmitData();
});
this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet"));
bus.addPortInputTable(this, SerialPort8080.aPortInput, this.portBase);
bus.addPortOutputTable(this, SerialPort8080.aPortOutput, this.portBase);
this.setReady();
}
/**
* initConnection(fNullModem)
*
* If a machine 'connection' parameter exists of the form "{sourcePort}->{targetMachine}.{targetPort}",
* and "{sourcePort}" matches our idComponent, then look for a component with id "{targetMachine}.{targetPort}".
*
* If the target component is found, then verify that it has exported functions with the following names:
*
* receiveData(data): called when we have data to transmit; aliased internally to sendData(data)
* receiveStatus(pins): called when our control signals have changed; aliased internally to updateStatus(pins)
*
* For now, we're not going to worry about communication in the other direction, because when the target component
* performs its own initConnection(), it will find our receiveData() and receiveStatus() functions, at which point
* communication in both directions should be established, and the circle of life complete.
*
* For added robustness, if the target machine initializes much more slowly than we do, and our connection attempt
* fails, that's OK, because when it finally initializes, its initConnection() will call our initConnection();
* if we've already initialized, no harm done.
*
* @this {SerialPort8080}
* @param {boolean} [fNullModem] (caller's null-modem setting, to ensure our settings are in agreement)
*/
initConnection(fNullModem)
{
if (!this.connection) {
var sConnection = this.cmp.getMachineParm("connection");
if (sConnection) {
var asParts = sConnection.split('->');
if (asParts.length == 2) {
var sSourceID = Str.trim(asParts[0]);
if (sSourceID != this.idComponent) return; // this connection string is intended for another instance
var sTargetID = Str.trim(asParts[1]);
this.connection = Component.getComponentByID(sTargetID);
if (this.connection) {
var exports = this.connection['exports'];
if (exports) {
var fnConnect = exports['connect'];
if (fnConnect) fnConnect.call(this.connection, this.fNullModem);
this.sendData = exports['receiveData'];
if (this.sendData) {
this.fNullModem = fNullModem;
this.updateStatus = exports['receiveStatus'];
this.status("Connected " + this.idMachine + '.' + sSourceID + " to " + sTargetID);
return;
}
}
}
}
/*
* Changed from notice() to status() because sometimes a connection fails simply because one of us is a laggard.
*/
this.status("Unable to establish connection: " + sConnection);
}
}
}
/**
* powerUp(data, fRepower)
*
* @this {SerialPort8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* This is as late as we can currently wait to make our first inter-machine connection attempt;
* even so, the target machine's initialization process may still be ongoing, so any connection
* may be not fully resolved until the target machine performs its own initConnection(), which will
* in turn invoke our initConnection() again.
*/
this.initConnection(this.fNullModem);
if (!data || !this.restore) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {SerialPort8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {SerialPort8080}
*/
reset()
{
this.initState();
}
/**
* save()
*
* This implements save support for the SerialPort8080 component.
*
* @this {SerialPort8080}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.saveRegisters());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the SerialPort8080 component.
*
* @this {SerialPort8080}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
return this.initState(data[0]);
}
/**
* initState(data)
*
* @this {SerialPort8080}
* @param {Array} [data]
* @return {boolean} true if successful, false if failure
*/
initState(data)
{
var i = 0;
if (data === undefined) {
data = SerialPort8080.UART8251.INIT;
}
this.fReady = data[i++];
this.bDataIn = data[i++];
this.bDataOut = data[i++];
this.bStatus = data[i++];
this.bMode = data[i++];
this.bCommand = data[i++];
this.bBaudRates = data[i];
return true;
}
/**
* saveRegisters()
*
* @this {SerialPort8080}
* @return {Array}
*/
saveRegisters()
{
var i = 0;
var data = [];
data[i++] = this.fReady;
data[i++] = this.bDataIn;
data[i++] = this.bDataOut;
data[i++] = this.bStatus;
data[i++] = this.bMode;
data[i++] = this.bCommand;
data[i] = this.bBaudRates;
return data;
}
/**
* getBaudTimeout(maskRate)
*
* @this {SerialPort8080}
* @param {number} maskRate (either SerialPort8080.UART8251.BAUDRATES.RECV_RATE or SerialPort8080.UART8251.BAUDRATES.XMIT_RATE)
* @return {number} (number of milliseconds per byte)
*/
getBaudTimeout(maskRate)
{
var indexRate = (this.bBaudRates & maskRate);
if (!(maskRate & 0xf)) indexRate >>= 4;
var nBaud = SerialPort8080.UART8251.BAUDTABLE[indexRate];
var nBits = ((this.bMode & SerialPort8080.UART8251.MODE.DATA_BITS) >> 2) + 6; // includes an extra +1 for start bit
if (this.bMode & SerialPort8080.UART8251.MODE.PARITY_ENABLE) nBits++;
nBits += ((((this.bMode & SerialPort8080.UART8251.MODE.STOP_BITS) >> 6) + 1) >> 1);
var nBytesPerSecond = Math.round(nBaud / nBits);
return 1000 / nBytesPerSecond;
}
/**
* receiveByte(b)
*
* @this {SerialPort8080}
* @param {number} b
* @return {boolean}
*/
receiveByte(b)
{
if (MAXDEBUG) this.echoByte(b);
this.printMessage("receiveByte(" + Str.toHexByte(b) + "), status=" + Str.toHexByte(this.bStatus));
if (!this.fAutoStop && !(this.bStatus & SerialPort8080.UART8251.STATUS.RECV_FULL)) {
this.bDataIn = b;
this.bStatus |= SerialPort8080.UART8251.STATUS.RECV_FULL;
this.cpu.requestINTR(this.nIRQ);
return true;
}
return false;
}
/**
* receiveData(data)
*
* Helper for clocking received data at the expected RECV_RATE.
*
* When we're cramming test data down the terminal's throat, that data will typically be in the form
* of a string. When we're called by another component, data will typically be a number (ie, byte). If no
* data is specified at all, then all we do is "clock" any remaining data into the receiver.
*
* @this {SerialPort8080}
* @param {number|string|undefined} [data]
* @return {boolean} true if received, false if not
*/
receiveData(data)
{
if (data != null) {
if (typeof data != "number") {
this.sDataReceived = data;
} else {
this.sDataReceived += String.fromCharCode(data);
}
}
if (this.sDataReceived) {
if (this.receiveByte(this.sDataReceived.charCodeAt(0))) {
this.sDataReceived = this.sDataReceived.substr(1);
}
if (this.sDataReceived && this.cpu) {
this.cpu.setTimer(this.timerReceiveNext, this.getBaudTimeout(SerialPort8080.UART8251.BAUDRATES.RECV_RATE));
}
}
return true; // for now, return true regardless, since we're buffering everything anyway
}
/**
* receiveStatus(pins)
*
* NOTE: Prior to the addition of this interface, the DSR bit was initialized set and remained set for the life
* of the machine. It is entirely appropriate that this is the only way the bit can be changed, because it represents
* an external control signal.
*
* @this {SerialPort8080}
* @param {number} pins
*/
receiveStatus(pins)
{
this.bStatus &= ~SerialPort8080.UART8251.STATUS.DSR;
if (pins & RS232.DSR.MASK) this.bStatus |= SerialPort8080.UART8251.STATUS.DSR;
}
/**
* transmitByte(b)
*
* @this {SerialPort8080}
* @param {number} b
* @return {boolean} true if transmitted, false if not
*/
transmitByte(b)
{
var fTransmitted = false;
this.printMessage("transmitByte(" + Str.toHexByte(b) + ")");
if (this.fAutoXOFF) {
if (b == 0x13) { // XOFF
this.fAutoStop = true;
return false;
}
if (b == 0x11) { // XON
this.fAutoStop = false;
return false;
}
}
if (this.sendData) {
if (this.sendData.call(this.connection, b)) {
fTransmitted = true;
}
}
if (this.echoByte(b)) {
fTransmitted = true;
}
return fTransmitted;
}
/**
* transmitData(sData)
*
* Helper for clocking transmitted data at the expected XMIT_RATE.
*
* When timerTransmitNext fires, we have honored the programmed XMIT_RATE period, so we can
* set XMIT_READY (and XMIT_EMPTY), which signals the firmware that another byte can be transmitted.
*
* @this {SerialPort8080}
* @param {string} [sData]
* @return {boolean} true if successful, false if not
*/
transmitData(sData)
{
this.bStatus |= (SerialPort8080.UART8251.STATUS.XMIT_READY | SerialPort8080.UART8251.STATUS.XMIT_EMPTY);
if (sData) {
return this.sendData? this.sendData.call(this.connection, sData) : false;
}
return true;
}
/**
* isTransmitterReady()
*
* Called whenever a ChipSet circuit needs the SerialPort8080 UART's transmitter status.
*
* @this {SerialPort8080}
* @return {boolean} (true if ready, false if not)
*/
isTransmitterReady()
{
return !!(this.bStatus & SerialPort8080.UART8251.STATUS.XMIT_READY);
}
/**
* inData(port, addrFrom)
*
* @this {SerialPort8080}
* @param {number} port (0x0)
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
* @return {number} simulated port value
*/
inData(port, addrFrom)
{
var b = this.bDataIn;
this.printMessageIO(port, null, addrFrom, "DATA", b);
this.bStatus &= ~SerialPort8080.UART8251.STATUS.RECV_FULL;
return b;
}
/**
* inControl(port, addrFrom)
*
* @this {SerialPort8080}
* @param {number} port (0x1)
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
* @return {number} simulated port value
*/
inControl(port, addrFrom)
{
var b = this.bStatus;
this.printMessageIO(port, null, addrFrom, "STATUS", b);
return b;
}
/**
* outData(port, bOut, addrFrom)
*
* @this {SerialPort8080}
* @param {number} port (0x0)
* @param {number} bOut
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
*/
outData(port, bOut, addrFrom)
{
this.printMessageIO(port, bOut, addrFrom, "DATA");
this.bDataOut = bOut;
this.bStatus &= ~(SerialPort8080.UART8251.STATUS.XMIT_READY | SerialPort8080.UART8251.STATUS.XMIT_EMPTY);
/*
* If we're transmitting to a virtual device that has no measurable delay, this code may clear XMIT_READY
* too quickly:
*
* if (this.transmitByte(bOut)) {
* this.bStatus |= (SerialPort8080.UART8251.STATUS.XMIT_READY | SerialPort8080.UART8251.STATUS.XMIT_EMPTY);
* }
*
* A better solution is to arm a timer based on the XMIT_RATE baud rate, and clear the above bits when that
* timer fires. Consequently, we no longer care what transmitByte() reports.
*/
this.transmitByte(bOut);
if (this.cpu) {
this.cpu.setTimer(this.timerTransmitNext, this.getBaudTimeout(SerialPort8080.UART8251.BAUDRATES.XMIT_RATE));
}
}
/**
* outControl(port, bOut, addrFrom)
*
* Writes to the CONTROL port (0x1) are either MODE or COMMAND bytes. If the device has just
* been powered or reset, it is in a "not ready" state and is waiting for a MODE byte. Once it
* has received that initial byte, the device is marked "ready", and all further bytes are
* interpreted as COMMAND bytes (until/unless a COMMAND byte with the INTERNAL_RESET bit is set).
*
* @this {SerialPort8080}
* @param {number} port (0x1)
* @param {number} bOut
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
*/
outControl(port, bOut, addrFrom)
{
this.printMessageIO(port, bOut, addrFrom, "CONTROL");
if (!this.fReady) {
this.bMode = bOut;
this.fReady = true;
} else {
/*
* Whenever DTR or RTS changes, we also want to notify any connected machine, via updateStatus().
*/
if (this.updateStatus) {
var delta = (bOut ^ this.bCommand);
if (delta & (SerialPort8080.UART8251.COMMAND.RTS | SerialPort8080.UART8251.COMMAND.DTR)) {
var pins = 0;
if (this.fNullModem) {
pins |= (bOut & SerialPort8080.UART8251.COMMAND.RTS)? RS232.CTS.MASK : 0;
pins |= (bOut & SerialPort8080.UART8251.COMMAND.DTR)? (RS232.DSR.MASK | RS232.CD.MASK): 0;
} else {
pins |= (bOut & SerialPort8080.UART8251.COMMAND.RTS)? RS232.RTS.MASK : 0;
pins |= (bOut & SerialPort8080.UART8251.COMMAND.DTR)? RS232.DTR.MASK : 0;
}
this.updateStatus.call(this.connection, pins);
}
}
this.bCommand = bOut;
if (this.bCommand & SerialPort8080.UART8251.COMMAND.INTERNAL_RESET) {
this.fReady = false;
}
}
}
/**
* outBaudRates(port, bOut, addrFrom)
*
* @this {SerialPort8080}
* @param {number} port (0x2)
* @param {number} bOut
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
*/
outBaudRates(port, bOut, addrFrom)
{
this.printMessageIO(port, bOut, addrFrom, "BAUDRATES");
this.bBaudRates = bOut;
}
/**
* SerialPort8080.init()
*
* This function operates on every HTML element of class "serial", extracting the
* JSON-encoded parameters for the SerialPort8080 constructor from the element's "data-value"
* attribute, invoking the constructor to create a SerialPort8080 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeSerial = Component.getElementsByClass(document, PC8080.APPCLASS, "serial");
for (var iSerial = 0; iSerial < aeSerial.length; iSerial++) {
var eSerial = aeSerial[iSerial];
var parmsSerial = Component.getComponentParms(eSerial);
var serial = new SerialPort8080(parmsSerial);
Component.bindComponentControls(serial, eSerial, PC8080.APPCLASS);
}
}
}
/*
* class SerialPort8080
* property {number} iAdapter
* property {number} portBase
* property {number} nIRQ
* property {Object} controlIOBuffer is a DOM element bound to the port (for rudimentary output; see transmitByte())
*
* NOTE: This class declaration started as a way of informing the code inspector of the controlIOBuffer property,
* which remained undefined until a setBinding() call set it later, but I've since decided that explicitly
* initializing such properties in the constructor is a better way to go -- even though it's more code -- because
* JavaScript compilers are supposed to be happier when the underlying object structures aren't constantly changing.
*
* Besides, I'm not sure I want to get into documenting every property this way, for this or any/every other class,
* let alone getting into which ones should be considered private or protected, because PCjs isn't really a library
* for third-party apps.
*/
SerialPort8080.UART8251 = {
/*
* Format of MODE byte written to CONTROL port 0x1
*/
MODE: {
BAUD_FACTOR: 0x03, // 00=SYNC, 01=1x, 10=16x, 11=64x
DATA_BITS: 0x0C, // 00=5, 01=6, 10=7, 11=8
PARITY_ENABLE: 0x10,
EVEN_PARITY: 0x20,
STOP_BITS: 0xC0, // 00=invalid, 01=1, 10=1.5, 11=2
INIT: 0x8E // 16x baud rate, 8 data bits, no parity, 1.5 stop bits
},
/*
* Format of COMMAND byte written to CONTROL port 0x1
*/
COMMAND: {
XMIT_ENABLE: 0x01,
DTR: 0x02, // Data Terminal Ready
RECV_ENABLE: 0x04,
SEND_BREAK: 0x08,
ERROR_RESET: 0x10,
RTS: 0x20, // Request To Send
INTERNAL_RESET: 0x40,
HUNT_MODE: 0x80,
INIT: 0x27 // XMIT_ENABLE | DTR | RECV_ENABLE | RTS
},
/*
* Format of STATUS byte read from CONTROL port 0x1
*/
STATUS: {
XMIT_READY: 0x01,
RECV_FULL: 0x02,
XMIT_EMPTY: 0x04,
PARITY_ERROR: 0x08,
OVERRUN_ERROR: 0x10,
FRAMING_ERROR: 0x20,
BREAK_DETECT: 0x40,
DSR: 0x80, // Data Set Ready
INIT: 0x85 // XMIT_READY | XMIT_EMPTY | DSR
},
/*
* Format of BAUDRATES byte written to port 0x2
*
* Each nibble is an index (0x0-0xF) into a set of internal CPU clock divisors that yield the
* following baud rates:
*
* Index Divisor Baud Rate
* ----- ------- ---------
* 0x0 3456 50
* 0x1 2304 75
* 0x2 1571 110
* 0x3 1285 134.5
* 0x4 1152 150
* 0x5 864 200
* 0x6 576 300
* 0x7 288 600
* 0x8 144 1200
* 0x9 96 1800
* 0xA 86 2000
* 0xB 72 2400
* 0xC 48 3600
* 0xD 36 4800
* 0xE 18 9600 (default)
* 0xF 9 19200
*
* NOTE: This is a VT100-specific port and baud rate table.
*/
BAUDRATES: {
RECV_RATE: 0x0F,
XMIT_RATE: 0xF0,
INIT: 0xEE // default to 9600 (0xE) for both XMIT and RECV
},
BAUDTABLE: [
50, 75, 110, 134.5, 150, 200, 300, 600, 1200, 1800, 2000, 2400, 3600, 4800, 9600, 19200
]
};
SerialPort8080.UART8251.INIT = [
false,
0,
0,
SerialPort8080.UART8251.STATUS.INIT,
SerialPort8080.UART8251.MODE.INIT,
SerialPort8080.UART8251.COMMAND.INIT,
SerialPort8080.UART8251.BAUDRATES.INIT
];
/*
* Internal name used for the I/O buffer control, if any, that we bind to a SerialPort8080.
*
* Alternatively, if SerialPort8080 wants to use another component's control (eg, the Panel's
* "print" control), it can specify the name of that control with the 'binding' property.
*
* For that binding to succeed, we also need to know the target component; for now, that's
* been hard-coded to "Panel", in part because that's one of the few components we can rely
* upon initializing before we do, but it would be a simple matter to include a component type
* or ID as part of the 'binding' property as well, if we need more flexibility later.
*/
SerialPort8080.sIOBuffer = "buffer";
/*
* Port input notification table
*/
SerialPort8080.aPortInput = {
0x0: SerialPort8080.prototype.inData,
0x1: SerialPort8080.prototype.inControl
};
/*
* Port output notification table
*/
SerialPort8080.aPortOutput = {
0x0: SerialPort8080.prototype.outData,
0x1: SerialPort8080.prototype.outControl,
0x2: SerialPort8080.prototype.outBaudRates
};
/*
* Initialize every SerialPort8080 module on the page.
*/
Web.onInit(SerialPort8080.init);
/**
* @copyright http://pcjs.org/modules/shared/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* Debugger Address Object
*
* This is the basic structure; other debuggers may extend it.
*
* 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.<string>|undefined)
* }}
*/
var DbgAddr;
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*
* the named property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class Debugger extends Component
{
/**
* Debugger(parmsDbg)
*
* The Debugger component supports the following optional (parmsDbg) properties:
*
* base: the base to use for most numeric input/output (default is 16)
*
* The Debugger component is a shared component containing a subset of functionality used by
* the other CPU-specific Debuggers (eg, DebuggerX86). Over time, the goal is to factor out as
* much common debugging support as possible from those components into this one.
*
* @param {Object} parmsDbg
*/
constructor(parmsDbg)
{
if (DEBUGGER) {
super("Debugger", parmsDbg);
/*
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = +parmsDbg['base'] || 16;
/*
* Default number of bits of integer precision; it can be overridden by the Debugger
* but there is no command to adjust it.
*/
this.nBits = 32;
this.achGroup = ['{','}'];
this.achAddress = ['[',']'];
/*
* 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;
/*
* fAssemble is true when "assemble mode" is active, false when not.
*/
this.fAssemble = false;
/*
* 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 = [];
/*
* aVariables is an object with properties that grow 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 non-digit, followed by zero or more symbol characters and/or digits)
* and the property value is the variable's numeric value. See doVar() and setVariable() for
* details.
*
* Note that parseValue() parses variables before numbers, so any variable that looks like a
* unprefixed hex value (eg, "a5" as opposed to "0xa5") will trump the numeric value. Unprefixed
* hex values are a convenience of parseValue(), which always calls Str.parseInt() with a default
* base of 16; however, that default be overridden with a variety of explicit prefixes or suffixes
* (eg, a leading "0o" to indicate octal, a trailing period to indicate decimal, etc.)
*
* See Str.parseInt() for more details about supported numbers.
*/
this.aVariables = {};
} // endif DEBUGGER
}
/**
* getRegIndex(sReg, off)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off)
{
return -1;
}
/**
* getRegValue(iReg)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
return undefined;
}
/**
* parseAddrReference(s, sAddr)
*
* Returns the given string with the given address reference replaced with the contents of that address.
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} s
* @param {string} sAddr
* @return {string}
*/
parseAddrReference(s, sAddr)
{
return s.replace('[' + sAddr + ']', "unimplemented");
}
/**
* getNextCommand()
*
* @this {Debugger}
* @return {string}
*/
getNextCommand()
{
var sCmd;
if (this.iPrevCmd > 0) {
sCmd = this.aPrevCmds[--this.iPrevCmd];
} else {
sCmd = "";
this.iPrevCmd = -1;
}
return sCmd;
}
/**
* getPrevCommand()
*
* @this {Debugger}
* @return {string|null}
*/
getPrevCommand()
{
var sCmd = null;
if (this.iPrevCmd < this.aPrevCmds.length - 1) {
sCmd = this.aPrevCmds[++this.iPrevCmd];
}
return sCmd;
}
/**
* 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.<string>}
*/
parseCommand(sCmd, fSave, chSep)
{
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
sCmd = "end";
} else {
sCmd = this.aPrevCmds[this.iPrevCmd+1];
}
} else {
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
this.iPrevCmd = 0;
}
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
this.aPrevCmds.splice(0, 0, sCmd);
this.iPrevCmd = 0;
}
this.iPrevCmd--;
}
}
var a = [];
if (sCmd) {
/*
* With the introduction of breakpoint commands (ie, quoted command sequences
* associated with a breakpoint), we can no longer perform simplistic splitting.
*
* a = sCmd.split(chSep || ';');
* for (var i = 0; i < a.length; i++) a[i] = Str.trim(a[i]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
* DOUBLE double-quotes with single quotes.
*/
sCmd = sCmd.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;
}
/**
* evalAND(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.AND().
*
* Performs the bitwise "and" (AND) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst & src)
*/
evalAND(dst, src)
{
/*
* We AND the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 AND 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst & src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) & ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst & src) >>> 0);
}
/**
* evalIOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.IOR().
*
* Performs the logical "inclusive-or" (OR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst | src)
*/
evalIOR(dst, src)
{
/*
* We OR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 OR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) | ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst | src) >>> 0);
}
/**
* evalXOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.XOR().
*
* Performs the logical "exclusive-or" (XOR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst ^ src)
*/
evalXOR(dst, src)
{
/*
* We XOR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 XOR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) ^ ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst ^ src) >>> 0);
}
/**
* evalMUL(dst, src)
*
* I could have adapted the code from /modules/pdp10/lib/cpuops.js:PDP10.doMUL(), but it was simpler to
* write this base method and let the PDP-10 Debugger override it with a call to the *actual* doMUL() method.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst * src)
*/
evalMUL(dst, src)
{
return dst * src;
}
/**
* truncate(v, nBits, fUnsigned)
*
* @this {Debugger}
* @param {number} v
* @param {number} [nBits]
* @param {boolean} [fUnsigned]
* @return {number}
*/
truncate(v, nBits, fUnsigned)
{
var limit, vNew = v;
nBits = nBits || this.nBits;
if (fUnsigned) {
if (nBits == 32) {
vNew = v >>> 0;
}
else if (nBits < 32) {
vNew = v & ((1 << nBits) - 1);
}
else {
limit = Math.pow(2, nBits);
if (v < 0 || v >= limit) {
vNew = v % limit;
if (vNew < 0) vNew += limit;
}
}
}
else {
if (nBits <= 32) {
vNew = (v << (32 - nBits)) >> (32 - nBits);
}
else {
limit = Math.pow(2, nBits - 1);
if (v >= limit) {
vNew = (v % limit);
if (((v / limit)|0) & 1) vNew -= limit;
} else if (v < -limit) {
vNew = (v % limit);
if ((((-v - 1) / limit) | 0) & 1) {
if (vNew) vNew += limit;
}
else {
if (!vNew) vNew -= limit;
}
}
}
}
if (v != vNew) {
if (MAXDEBUG) this.println("warning: value " + v + " truncated to " + vNew);
v = vNew;
}
return v;
}
/**
* evalOps(aVals, aOps, cOps)
*
* Some of our clients want a specific number of bits of integer precision. If that precision is
* greater than 32, some of the operations below will fail; for example, JavaScript bitwise operators
* always truncate the result to 32 bits, so beware when using shift operations. Similarly, it would
* be wrong to always "|0" the final result, which is why we rely on truncate() now.
*
* Note that JavaScript integer precision is limited to 52 bits. For example, in Node, if you set a
* variable to 0x80000001:
*
* foo=0x80000001|0
*
* then calculate foo*foo and display the result in binary using "(foo*foo).toString(2)":
*
* '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.<number>} aVals
* @param {Array.<string>} aOps
* @param {number} [cOps] (default is -1 for all)
* @return {boolean} true if successful, false if error
*/
evalOps(aVals, aOps, 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 = this.evalMUL(val1, val2);
break;
case '/':
if (!val2) return false;
valNew = Math.trunc(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 = this.evalAND(val1, val2);
break;
case '!': // alias for MACRO-10 to perform a bitwise inclusive-or (OR)
case '|':
valNew = this.evalIOR(val1, val2);
break;
case '^!': // since MACRO-10 uses '^' for base overrides, '^!' is used for bitwise exclusive-or (XOR)
valNew = this.evalXOR(val1, val2);
break;
case '&&':
valNew = (val1 && val2? 1 : 0);
break;
case '||':
valNew = (val1 || val2? 1 : 0);
break;
case ',,':
valNew = this.truncate(val1, 18, true) * Math.pow(2, 18) + this.truncate(val2, 18, true);
break;
case '_':
case '^_':
valNew = val1;
/*
* While we always try to avoid assuming any particular number of bits of precision, the 'B' shift
* operator (which we've converted to '^_') is unique to the MACRO-10 environment, which imposes the
* following restrictions on the shift count.
*/
if (chOp == '^_') val2 = 35 - (val2 & 0xff);
if (val2) {
/*
* Since binary shifting is a logical (not arithmetic) operation, and since shifting by division only
* works properly with positive numbers, we call truncate() to produce an unsigned value.
*/
valNew = this.truncate(valNew, 0, true);
if (val2 > 0) {
valNew *= Math.pow(2, val2);
} else {
valNew = Math.trunc(valNew / Math.pow(2, -val2));
}
}
break;
default:
return false;
}
aVals.push(this.truncate(valNew));
}
return true;
}
/**
* parseArray(asValues, iValue, iLimit, nBase, aUndefined)
*
* parseExpression() takes a complete expression and divides it into array elements, where even elements
* are values (which may be empty if two or more operators appear consecutively) and odd elements are operators.
*
* For example, if the original expression was "2*{3+{4/2}}", parseExpression() would call parseArray() with:
*
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
* - - - - - - - - - - -- -- -- -- --
* 2 * { 3 + { 4 / 2 } }
*
* This function takes care of recursively processing grouped expressions, by processing subsets of the array,
* as well as handling certain base overrides (eg, temporarily switching to base-10 for binary shift suffixes).
*
* @param {Array.<string>} asValues
* @param {number} iValue
* @param {number} iLimit
* @param {number} nBase
* @param {Array|undefined} [aUndefined]
* @return {number|undefined}
*/
parseArray(asValues, iValue, iLimit, nBase, aUndefined)
{
var value;
var sValue, sOp;
var fError = false;
var nUnary = 0;
var aVals = [], aOps = [];
var nBasePrev = this.nBase;
this.nBase = nBase;
while (iValue < iLimit) {
var v;
sValue = asValues[iValue++].trim();
sOp = (iValue < iLimit? asValues[iValue++] : "");
if (sValue) {
v = this.parseValue(sValue, null, aUndefined, nUnary);
} else {
if (sOp == '{') {
var cOpen = 1;
var iStart = iValue;
while (iValue < iLimit) {
sValue = asValues[iValue++].trim();
sOp = (iValue < asValues.length? asValues[iValue++] : "");
if (sOp == '{') {
cOpen++;
} else if (sOp == '}') {
if (!--cOpen) break;
}
}
v = this.parseArray(asValues, iStart, iValue-1, this.nBase, aUndefined);
if (v != null && nUnary) {
v = this.parseUnary(v, nUnary);
}
sValue = (iValue < iLimit? asValues[iValue++].trim() : "");
sOp = (iValue < iLimit? asValues[iValue++] : "");
}
else {
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* another operator and is easily ignored (although perhaps it should still trigger a reset of nBase
* and nUnary -- TBD).
*/
if (sOp == ' ') {
continue;
}
if (sOp == '^B') {
this.nBase = 2;
continue;
}
if (sOp == '^O') {
this.nBase = 8;
continue;
}
if (sOp == '^D') {
this.nBase = 10;
continue;
}
if (!(nUnary & (0xC0000000|0))) {
if (sOp == '+') {
continue;
}
if (sOp == '-') {
nUnary = (nUnary << 2) | 1;
continue;
}
if (sOp == '~' || sOp == '^-') {
nUnary = (nUnary << 2) | 2;
continue;
}
if (sOp == '^L') {
nUnary = (nUnary << 2) | 3;
continue;
}
}
fError = true;
break;
}
}
if (v === undefined) {
if (aUndefined) {
aUndefined.push(sValue);
v = 0;
} else {
fError = true;
aUndefined = [];
break;
}
}
aVals.push(this.truncate(v));
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* a value, and since we don't want to misinterpret the next operator as a unary operator, we look
* ahead and grab the next operator if it's not preceded by a value.
*/
if (sOp == ' ') {
if (iValue < asValues.length - 1 && !asValues[iValue]) {
iValue++;
sOp = asValues[iValue++]
} else {
fError = true;
break;
}
}
if (!sOp) break;
var aBinOp = (this.achGroup[0] == '<'? Debugger.aDECOpPrecedence : Debugger.aBinOpPrecedence);
if (!aBinOp[sOp]) {
fError = true;
break;
}
if (aOps.length && aBinOp[sOp] <= aBinOp[aOps[aOps.length - 1]]) {
this.evalOps(aVals, aOps, 1);
}
aOps.push(sOp);
/*
* The MACRO-10 binary shifting operator assumes a base-10 shift count, regardless of the current
* base, so we must override the current base to ensure the count is parsed correctly.
*/
this.nBase = (sOp == '^_')? 10 : nBase;
nUnary = 0;
}
if (fError || !this.evalOps(aVals, aOps) || aVals.length != 1) {
fError = true;
}
if (!fError) {
value = aVals.pop();
} else if (!aUndefined) {
this.println("parse error (" + (sValue || sOp) + ")");
}
this.nBase = nBasePrev;
return value;
}
/**
* parseASCII(sExp, chDelim, nBits, cchMax)
*
* @this {Debugger}
* @param {string} sExp
* @param {string} chDelim
* @param {number} nBits
* @param {number} cchMax
* @return {string|undefined}
*/
parseASCII(sExp, chDelim, nBits, cchMax)
{
var i;
while ((i = sExp.indexOf(chDelim)) >= 0) {
var v = 0;
var j = i + 1;
var cch = cchMax;
while (j < sExp.length) {
var ch = sExp[j++];
if (ch == chDelim) {
cch = -1;
break;
}
if (!cch) break;
cch--;
var c = ch.charCodeAt(0);
if (nBits == 7) {
c &= 0x7F;
} else {
c = (c - 0x20) & 0x3F;
}
v = this.truncate(v * Math.pow(2, nBits) + c, nBits * cchMax, true);
}
if (cch >= 0) {
this.println("parse error (" + chDelim + sExp + chDelim + ")");
return undefined;
} else {
sExp = sExp.substr(0, i) + this.toStrBase(v, -1) + sExp.substr(j);
}
}
return sExp;
}
/**
* parseExpression(sExp, fQuiet)
*
* 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. Only selected unary
* operators are supported (eg, negate and complement); no ternary operators like '?:' are supported.
*
* fQuiet can be used to pass an array that collects any undefined variables that parseExpression()
* encounters; the value of an undefined variable is zero. This mode was added for components that need
* to support expressions containing "fixups" (ie, values that must be determined later).
*
* @this {Debugger}
* @param {string|undefined} sExp
* @param {Array|undefined|boolean} [fQuiet]
* @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values
*/
parseExpression(sExp, fQuiet)
{
var value = undefined;
var fPrint = (fQuiet === false);
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sExp) {
/*
* The default delimiting characters for grouped expressions are braces; they can be changed by altering
* achGroup, but when that happens, instead of changing our regular expressions and operator tables,
* we simply replace all achGroup characters with braces in the given expression.
*
* Why not use parentheses for grouped expressions? Because some debuggers use parseReference() to perform
* parenthetical value replacements in message strings, and they don't want parentheses taking on a different
* meaning. And for some machines, like the PDP-10, the convention is to use parentheses for other things,
* like indexed addressing, and to use angle brackets for grouped expressions.
*/
if (this.achGroup[0] != '{') {
sExp = sExp.split(this.achGroup[0]).join('{').split(this.achGroup[1]).join('}');
}
/*
* Quoted ASCII characters can have a numeric value, too, which must be converted now, to avoid any
* conflicts with the operators below.
*/
sExp = this.parseASCII(sExp, '"', 7, 5); // MACRO-10 packs up to 5 7-bit ASCII codes into a value
if (!sExp) return value;
sExp = this.parseASCII(sExp, "'", 6, 6); // MACRO-10 packs up to 6 6-bit ASCII (SIXBIT) codes into a value
if (!sExp) return value;
/*
* 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".
*
* Although I started listing the operators in the RegExp in "precedential" order, that's not important;
* what IS important is listing operators than contain shorter operators first. For example, bitwise
* shift operators must be listed BEFORE the logical less-than or greater-than operators. The aBinOp tables
* (aBinOpPrecedence and aDECOpPrecedence) are what determine precedence, not the RegExp.
*
* Also, to better accommodate MACRO-10 syntax, I've replaced the single '^' for XOR with '^!', and I've
* added '!' as an alias for '|' (bitwise inclusive-or), '^-' as an alias for '~' (one's complement operator),
* and '_' as a shift operator (+/- values specify a left/right shift, and the count is not limited to 32).
*
* And to avoid conflicts with MACRO-10 syntax, I've replaced the original mod operator ('%') with '^/'.
*
* The MACRO-10 binary shifting suffix ('B') is a bit more problematic, since a capital B can also appear
* inside symbols, or inside hex values. So if the default base is NOT 16, then I pre-scan for that suffix
* and replace all non-symbolic occurrences with an internal shift operator ('^_').
*
* Note that Str.parseInt(), which parseValue() relies on, supports both the MACRO-10 base prefix overrides
* and the binary shifting suffix ('B'), but since that suffix can also be a bracketed expression, we have to
* support it here as well.
*
* MACRO-10 supports only a subset of all the PCjs operators; for example, MACRO-10 doesn't support any of
* the boolean logical/compare operators. But unless we run into conflicts, I prefer sticking with this
* common set of operators.
*
* All whitespace in the expression is collapsed to single spaces, and space has been added to the list
* of "operators", but its sole function is as a separator, not as an operator. parseArray() will ignore
* single spaces as long as they are preceded and/or followed by a "real" operator. It would be dangerous
* to remove spaces entirely, because if an operator-less expression like "A B" was passed in, we would want
* that to generate an error; if we converted it to "AB", evaluation might inadvertently succeed.
*/
var regExp = /({|}|\|\||&&|\||\^!|\^B|\^O|\^D|\^L|\^-|~|\^_|_|&|!=|!|==|>=|>>>|>>|>|<=|<<|<|-|\+|\^\/|\/|\*|,,| )/;
if (this.nBase != 16) {
sExp = sExp.replace(/(^|[^A-Z0-9$%.])([0-9]+)B/, "$1$2^_").replace(/\s+/g, ' ');
}
var asValues = sExp.split(regExp);
value = this.parseArray(asValues, 0, asValues.length, this.nBase, aUndefined);
if (value !== undefined && fPrint) {
this.printValue(null, value);
}
}
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.
*
* @this {Debugger}
* @param {string} s
* @return {string|undefined}
*/
parseReference(s)
{
var a;
var chOpen = this.achGroup[0];
var chClose = this.achGroup[1];
var chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
var chInnerEscape = (chOpen == '['? '\\' : '');
var reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
var value = this.parseExpression(a[1]);
if (value === undefined) return undefined;
var sSearch = chOpen + a[1] + chClose;
var sReplace = value != null? this.toStrBase(value) : "undefined";
/*
* Note that by default, the String replace() method only replaces the FIRST occurrence,
* and there MIGHT be more than one occurrence of the expression we just parsed, so we could
* do this instead:
*
* s = s.split(sSearch).join(sReplace);
*
* However, that's knd of an expensive (slow) solution, and it's not strictly necessary, since
* any additional identical expressions will be picked up on a subsequent iteration through this loop.
*/
s = s.replace(sSearch, sReplace);
}
if (this.achAddress.length) {
chOpen = this.achAddress[0];
chClose = this.achAddress[1];
chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
chInnerEscape = (chOpen == '['? '\\' : '');
reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
s = this.parseAddrReference(s, a[1]);
}
}
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}
*/
parseSysVars(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;
}
/**
* parseUnary(value, nUnary)
*
* nUnary is actually a small "stack" of unary operations encoded in successive pairs of bits.
* As parseExpression() encounters each unary operator, nUnary is shifted left 2 bits, and the
* new unary operator is encoded in bits 0 and 1 (0b00 is none, 0b01 is negate, 0b10 is complement,
* and 0b11 is reserved). Here, we process the bits in reverse order (hence the stack-like nature),
* ensuring that we process the unary operators associated with this value right-to-left.
*
* Since bitwise operators see only 32 bits, more than 16 unary operators cannot be supported
* using this method. We'll let parseExpression() worry about that; if it ever happens in practice,
* then we'll have to switch to a more "expensive" approach (eg, an actual array of unary operators).
*
* @this {Debugger}
* @param {number} value
* @param {number} nUnary
* @return {number}
*/
parseUnary(value, nUnary)
{
while (nUnary) {
switch(nUnary & 0o3) {
case 1:
value = -this.truncate(value);
break;
case 2:
value = this.evalXOR(value, -1); // this is easier than adding an evalNOT()...
break;
case 3:
var bit = 35; // simple left-to-right zero-bit-counting loop...
while (bit >= 0 && !this.evalAND(value, Math.pow(2, bit))) bit--;
value = 35 - bit;
break;
}
nUnary >>>= 2;
}
return value;
}
/**
* parseValue(sValue, sName, fQuiet, nUnary)
*
* @this {Debugger}
* @param {string|undefined} sValue
* @param {string|null} [sName] is the name of the value, if any
* @param {Array|undefined|boolean} [fQuiet]
* @param {number} [nUnary] (0 for none, 1 for negate, 2 for complement, 3 for leading zeros)
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
*/
parseValue(sValue, sName, fQuiet, nUnary = 0)
{
var value;
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sValue != null) {
var iReg = this.getRegIndex(sValue);
if (iReg >= 0) {
value = this.getRegValue(iReg);
} else {
value = this.getVariable(sValue);
if (value != null) {
var sUndefined = this.getVariableFixup(sValue);
if (sUndefined) {
if (aUndefined) {
aUndefined.push(sUndefined);
} else {
var valueUndefined = this.parseExpression(sUndefined, fQuiet);
if (valueUndefined !== undefined) {
value += valueUndefined;
} else {
if (!fQuiet) {
this.println("undefined " + (sName || "value") + ": " + sValue + " (" + sUndefined + ")");
}
value = undefined;
}
}
}
} else {
/*
* A feature of MACRO-10 is that any single-digit number is automatically interpreted as base-10.
*/
value = Str.parseInt(sValue, sValue.length > 1 || this.nBase > 10? this.nBase : 10);
}
}
if (value != null) {
value = this.truncate(this.parseUnary(value, nUnary));
} else {
if (!fQuiet) {
this.println("invalid " + (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
*/
printValue(sVar, value)
{
var sValue;
var fDefined = false;
if (value !== undefined) {
fDefined = true;
if (this.nBase == 8) {
sValue = this.toStrBase(value, this.nBits, 8, 1) + " " + value + '.';
} else {
sValue = this.toStrBase(value, this.nBits, 16, 1) + " " + this.toStrBase(value, this.nBits, 8, 1) + " " + this.toStrBase(value, this.nBits, 2, this.nBits <= 32? 8 : 6) + " " + value + '.';
}
if (value >= 0x20 && value < 0x7F) {
sValue += " '" + String.fromCharCode(value) + "'";
}
}
sVar = (sVar != null? (sVar + ": ") : "");
this.println(sVar + sValue);
return fDefined;
}
/**
* resetVariables()
*
* @this {Debugger}
* @return {Object}
*/
resetVariables()
{
var a = this.aVariables;
this.aVariables = {};
return a;
}
/**
* restoreVariables(a)
*
* @this {Debugger}
* @param {Object} a (from previous resetVariables() call)
*/
restoreVariables(a)
{
this.aVariables = a;
}
/**
* printVariable(sVar)
*
* @this {Debugger}
* @param {string} [sVar]
* @return {boolean} true if all value(s) defined, false if not
*/
printVariable(sVar)
{
var cVariables = 0;
if (this.aVariables) {
if (sVar) {
return this.printValue(sVar, this.aVariables[sVar] && this.aVariables[sVar].value);
}
var aVars = Object.keys(this.aVariables);
aVars.sort();
for (var i = 0; i < aVars.length; i++) {
this.printValue(aVars[i], this.aVariables[aVars[i]].value);
cVariables++;
}
}
return cVariables > 0;
}
/**
* delVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
*/
delVariable(sVar)
{
delete this.aVariables[sVar];
}
/**
* getVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {number|undefined}
*/
getVariable(sVar)
{
if (this.aVariables[sVar]) {
return this.aVariables[sVar].value;
}
sVar = sVar.substr(0, 6);
return this.aVariables[sVar] && this.aVariables[sVar].value;
}
/**
* getVariableFixup(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {string|undefined}
*/
getVariableFixup(sVar)
{
return this.aVariables[sVar] && this.aVariables[sVar].sUndefined;
}
/**
* isVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {boolean}
*/
isVariable(sVar)
{
return this.aVariables[sVar] !== undefined;
}
/**
* setVariable(sVar, value, sUndefined)
*
* @this {Debugger}
* @param {string} sVar
* @param {number} value
* @param {string|undefined} [sUndefined]
*/
setVariable(sVar, value, sUndefined)
{
this.aVariables[sVar] = {value, sUndefined};
}
/**
* toStrBase(n, nBits, nBase, nGrouping)
*
* Use this instead of Str's toOct()/toDec()/toHex() to convert numbers to the Debugger's default base.
*
* @this {Debugger}
* @param {number|null|undefined} n
* @param {number} [nBits] (-1 to strip leading zeros, 0 to allow a variable number of digits)
* @param {number} [nBase]
* @param {number} [nGrouping] (if nBase is 2, this is a grouping; otherwise, it's a prefix condition)
* @return {string}
*/
toStrBase(n, nBits = 0, nBase = 0, nGrouping = 0)
{
var s;
switch(nBase || this.nBase) {
case 2:
s = Str.toBin(n, nBits > 0? nBits : 0, nGrouping);
break;
case 8:
s = Str.toOct(n, nBits > 0? ((nBits + 2)/3)|0 : 0, !!nGrouping);
break;
case 10:
/*
* The multiplier is actually Math.log(2)/Math.log(10), but an approximation is more than adequate.
*/
s = Str.toDec(n, nBits > 0? Math.ceil(nBits * 0.3) : 0);
break;
case 16:
default:
s = Str.toHex(n, nBits > 0? ((nBits + 3) >> 2) : 0, !!nGrouping);
break;
}
return (nBits < 0? Str.stripLeadingZeros(s) : s);
}
}
if (DEBUGGER) {
/*
* These are our operator precedence tables. Operators toward the bottom (with higher values) have
* higher precedence. aBinOpPrecedence was our original table; we had to add aDECOpPrecedence because
* the precedence of operators in DEC's MACRO-10 expressions differ. Having separate tables also allows
* us to remove operators that shouldn't be supported, but unless some operator creates a problem,
* I prefer to keep as much commonality between the tables as possible.
*
* Missing from these tables are the (limited) set of unary operators we support (negate and complement),
* since this is only a BINARY operator precedence, not a general-purpose precedence table. Assume that
* all unary operators take precedence over all binary operators.
*/
Debugger.aBinOpPrecedence = {
'||': 5, // logical OR
'&&': 6, // logical AND
'!': 7, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 7, // bitwise OR
'^!': 8, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 9, // bitwise AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
Debugger.aDECOpPrecedence = {
',,': 1, // high-word,,low-word
'||': 5, // logical OR
'&&': 6, // logical AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'!': 15, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 15, // bitwise OR
'^!': 15, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 15, // bitwise AND
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
/*
* Assorted constants
*/
Debugger.TWO_POW32 = Math.pow(2, 32);
} // endif DEBUGGER
/**
* @copyright http://pcjs.org/modules/pc8080/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* Debugger8080 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.<string>|undefined)
* }}
*/
var DbgAddr8080;
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Debugger8080 extends Debugger {
/**
* Debugger8080(parmsDbg)
*
* The Debugger8080 component supports the following optional (parmsDbg) properties:
*
* commands: string containing zero or more commands, separated by ';'
*
* messages: string containing zero or more message categories to enable;
* multiple categories must be separated by '|' or ';'. Parsed by messageInit().
*
* The Debugger8080 component is an optional component that implements a variety of user
* commands for controlling the CPU, dumping and editing memory, etc.
*
* @this {Debugger8080}
* @param {Object} parmsDbg
*/
constructor(parmsDbg)
{
if (DEBUGGER) {
super(parmsDbg);
this.style = Debugger8080.STYLE_8080;
/*
* Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode
* or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn,
* update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done.
*
* For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically
* cleared when they're hit.
*/
this.dbgAddrNextCode = this.newAddr();
this.dbgAddrNextData = this.newAddr();
this.dbgAddrAssemble = this.newAddr();
/*
* aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space,
* where each object contains the following properties:
*
* sModule
* addr (physical address, if any; eg, symbols for a ROM)
* len
* aSymbols
* aOffsets
*
* See addSymbols() for more details, since that's how callers add sets of symbols to the table.
*/
this.aSymbolTable = [];
/*
* clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses
* to halt on whenever attempting to execute an instruction at the corresponding address,
* and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write,
* respectively, occurs at the corresponding address.
*
* NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609,
* the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead
* property was already defined. To eliminate whatever was confusing the Closure Compiler, I've
* explicitly initialized all the properties that clearBreakpoints() (re)initializes.
*/
this.aBreakExec = this.aBreakRead = this.aBreakWrite = [];
this.clearBreakpoints();
/*
* 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 Debugger8080 message support
*/
this.afnDumpers = {};
this.messageInit(parmsDbg['messages']);
this.sInitCommands = parmsDbg['commands'];
/*
* Make it easier to access Debugger8080 commands from an external REPL (eg, the WebStorm
* "live" console window); eg:
*
* pc8080('r')
* pc8080('dw 0:0')
* pc8080('h')
* ...
*/
var dbg = this;
if (window) {
if (window[PC8080.APPCLASS] === undefined) {
window[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); };
}
} else {
if (global[PC8080.APPCLASS] === undefined) {
global[PC8080.APPCLASS] = function(s) { return dbg.doCommands(s); };
}
}
} // endif DEBUGGER
}
/**
* initBus(bus, cpu, dbg)
*
* @this {Debugger8080}
* @param {Computer8080} cmp
* @param {Bus8080} bus
* @param {CPUState8080} cpu
* @param {Debugger8080} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.cmp = cmp;
/*
* Re-initialize Debugger message support if necessary
*/
var sMessages = cmp.getMachineParm('messages');
if (sMessages) this.messageInit(sMessages);
this.aaOpDescs = Debugger8080.aaOpDescs;
this.messageDump(Messages8080.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); });
this.setReady();
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {Debugger8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var dbg = this;
switch (sBinding) {
case "debugInput":
this.bindings[sBinding] = control;
this.controlDebug = control;
/*
* For halted machines, this is fine, but for auto-start machines, it can be annoying.
*
* control.focus();
*/
control.onkeydown = function onKeyDownDebugInput(event) {
var sCmd;
if (event.keyCode == Keys.KEYCODE.CR) {
sCmd = control.value;
control.value = "";
dbg.doCommands(sCmd, true);
}
else if (event.keyCode == Keys.KEYCODE.ESC) {
control.value = sCmd = "";
}
else {
if (event.keyCode == Keys.KEYCODE.UP) {
sCmd = dbg.getPrevCommand();
}
else if (event.keyCode == Keys.KEYCODE.DOWN) {
sCmd = dbg.getNextCommand();
}
if (sCmd != null) {
var cch = sCmd.length;
control.value = sCmd;
control.setSelectionRange(cch, cch);
}
}
if (sCmd != null && event.preventDefault) event.preventDefault();
};
return true;
case "debugEnter":
this.bindings[sBinding] = control;
Web.onClickRepeat(
control,
500, 100,
function onClickDebugEnter(fRepeat) {
if (dbg.controlDebug) {
var sCmds = dbg.controlDebug.value;
dbg.controlDebug.value = "";
dbg.doCommands(sCmds, true);
return true;
}
if (DEBUG) dbg.log("no debugger input buffer");
return false;
}
);
return true;
case "step":
this.bindings[sBinding] = control;
Web.onClickRepeat(
control,
500, 100,
function onClickStep(fRepeat) {
var fCompleted = false;
if (!dbg.isBusy(true)) {
dbg.setBusy(true);
fCompleted = dbg.stepCPU(fRepeat? 1 : 0);
dbg.setBusy(false);
}
return fCompleted;
}
);
return true;
default:
break;
}
return false;
}
/**
* updateFocus()
*
* @this {Debugger8080}
*/
updateFocus()
{
if (this.controlDebug) this.controlDebug.focus();
}
/**
* getAddr(dbgAddr, fWrite, nb)
*
* @this {Debugger8080}
* @param {DbgAddr8080|null|undefined} dbgAddr
* @param {boolean} [fWrite]
* @param {number} [nb] number of bytes to check (1 or 2); default is 1
* @return {number} is the corresponding linear address, or CPUDef8080.ADDR_INVALID
*/
getAddr(dbgAddr, fWrite, nb)
{
var addr = dbgAddr && dbgAddr.addr;
if (addr == null) {
addr = CPUDef8080.ADDR_INVALID;
}
return addr;
}
/**
* getByte(dbgAddr, inc)
*
* We must route all our memory requests through the CPU now, in case paging is enabled.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} [inc]
* @return {number}
*/
getByte(dbgAddr, inc)
{
var b = 0xff;
var addr = this.getAddr(dbgAddr, false, 1);
if (addr !== CPUDef8080.ADDR_INVALID) {
b = this.bus.getByteDirect(addr);
if (inc) this.incAddr(dbgAddr, inc);
}
return b;
}
/**
* getWord(dbgAddr, fAdvance)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {boolean} [fAdvance]
* @return {number}
*/
getWord(dbgAddr, fAdvance)
{
return this.getShort(dbgAddr, fAdvance? 2 : 0);
}
/**
* getShort(dbgAddr, inc)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} [inc]
* @return {number}
*/
getShort(dbgAddr, inc)
{
var w = 0xffff;
var addr = this.getAddr(dbgAddr, false, 2);
if (addr !== CPUDef8080.ADDR_INVALID) {
w = this.bus.getShortDirect(addr);
if (inc) this.incAddr(dbgAddr, inc);
}
return w;
}
/**
* setByte(dbgAddr, b, inc)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} b
* @param {number} [inc]
*/
setByte(dbgAddr, b, inc)
{
var addr = this.getAddr(dbgAddr, true, 1);
if (addr !== CPUDef8080.ADDR_INVALID) {
this.bus.setByteDirect(addr, b);
if (inc) this.incAddr(dbgAddr, inc);
this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target
}
}
/**
* setShort(dbgAddr, w, inc)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} w
* @param {number} [inc]
*/
setShort(dbgAddr, w, inc)
{
var addr = this.getAddr(dbgAddr, true, 2);
if (addr !== CPUDef8080.ADDR_INVALID) {
this.bus.setShortDirect(addr, w);
if (inc) this.incAddr(dbgAddr, inc);
this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target
}
}
/**
* newAddr(addr)
*
* Returns a NEW DbgAddr8080 object, initialized with specified values and/or defaults.
*
* @this {Debugger8080}
* @param {number} [addr]
* @return {DbgAddr8080}
*/
newAddr(addr)
{
return {addr: addr, fTemporary: false};
}
/**
* setAddr(dbgAddr, addr)
*
* Updates an EXISTING DbgAddr8080 object, initialized with specified values and/or defaults.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} addr
* @return {DbgAddr8080}
*/
setAddr(dbgAddr, addr)
{
dbgAddr.addr = addr;
dbgAddr.fTemporary = false;
return dbgAddr;
}
/**
* packAddr(dbgAddr)
*
* Packs a DbgAddr8080 object into an Array suitable for saving in a machine state object.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @return {Array}
*/
packAddr(dbgAddr)
{
return [dbgAddr.addr, dbgAddr.fTemporary];
}
/**
* unpackAddr(aAddr)
*
* Unpacks a DbgAddr8080 object from an Array created by packAddr() and restored from a saved machine state.
*
* @this {Debugger8080}
* @param {Array} aAddr
* @return {DbgAddr8080}
*/
unpackAddr(aAddr)
{
return {addr: aAddr[0], fTemporary: aAddr[1]};
}
/**
* parseAddr(sAddr, fCode, fNoChecks)
*
* Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's
* done later, by getAddr(), which returns CPUDef8080.ADDR_INVALID for invalid segments, out-of-range offsets,
* etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an
* invalid address is passed through to the Bus memory interfaces, the address will simply be masked with
* Bus8080.nBusLimit; in the case of CPUDef8080.ADDR_INVALID, that will generally refer to the top of the physical
* address space.
*
* @this {Debugger8080}
* @param {string|undefined} sAddr
* @param {boolean} [fCode] (true if target is code, false if target is data)
* @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later)
* @return {DbgAddr8080|null|undefined}
*/
parseAddr(sAddr, fCode, fNoChecks)
{
var dbgAddr;
var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData);
var addr = dbgAddrNext.addr;
if (sAddr !== undefined) {
sAddr = this.parseReference(sAddr) || sAddr;
dbgAddr = this.findSymbolAddr(sAddr);
if (dbgAddr) return dbgAddr;
addr = this.parseExpression(sAddr);
}
if (addr != null) {
dbgAddr = this.newAddr(addr);
}
return dbgAddr;
}
/**
* parseAddrOptions(dbdAddr, sOptions)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {string} [sOptions]
*/
parseAddrOptions(dbgAddr, sOptions)
{
if (sOptions) {
var a = sOptions.match(/(['"])(.*?)\1/);
if (a) {
dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]);
}
}
}
/**
* incAddr(dbgAddr, inc)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} [inc] contains value to increment dbgAddr by (default is 1)
*/
incAddr(dbgAddr, inc)
{
if (dbgAddr.addr != null) {
dbgAddr.addr += (inc || 1);
}
}
/**
* toHexOffset(off)
*
* @this {Debugger8080}
* @param {number|null|undefined} [off]
* @return {string} the hex representation of off
*/
toHexOffset(off)
{
return Str.toHex(off, 4);
}
/**
* toHexAddr(dbgAddr)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @return {string} the hex representation of the address
*/
toHexAddr(dbgAddr)
{
return this.toHexOffset(dbgAddr.addr);
}
/**
* getSZ(dbgAddr, cchMax)
*
* Gets zero-terminated (aka "ASCIIZ") string from dbgAddr. It also stops at the first '$', in case this is
* a '$'-terminated string -- mainly because I'm lazy and didn't feel like writing a separate get() function.
* Yes, a zero-terminated string containing a '$' will be prematurely terminated, and no, I don't care.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {number} [cchMax] (default is 256)
* @return {string} (and dbgAddr advanced past the terminating zero)
*/
getSZ(dbgAddr, cchMax)
{
var s = "";
cchMax = cchMax || 256;
while (s.length < cchMax) {
var b = this.getByte(dbgAddr, 1);
if (!b || b == 0x24 || b >= 127) break;
s += (b >= 32? String.fromCharCode(b) : '.');
}
return s;
}
/**
* dumpBlocks(aBlocks, sAddr)
*
* @this {Debugger8080}
* @param {Array} aBlocks
* @param {string} [sAddr] (optional block address)
*/
dumpBlocks(aBlocks, sAddr)
{
var addr = 0, i = 0, n = aBlocks.length;
if (sAddr) {
addr = this.getAddr(this.parseAddr(sAddr));
if (addr === CPUDef8080.ADDR_INVALID) {
this.println("invalid address: " + sAddr);
return;
}
i = addr >>> this.bus.nBlockShift;
n = 1;
}
this.println("blockid physical blockaddr used size type");
this.println("-------- --------- ---------- ------ ------ ----");
var typePrev = -1, cPrev = 0;
while (n--) {
var block = aBlocks[i];
if (block.type == typePrev) {
if (!cPrev++) this.println("...");
} else {
typePrev = block.type;
var sType = Memory8080.TYPE.NAMES[typePrev];
if (block) {
this.println(Str.toHex(block.id) + " %" + Str.toHex(i << this.bus.nBlockShift) + " %%" + Str.toHex(block.addr) + " " + Str.toHexWord(block.used) + " " + Str.toHexWord(block.size) + " " + sType);
}
if (typePrev != Memory8080.TYPE.NONE) typePrev = -1;
cPrev = 0;
}
addr += this.bus.nBlockSize;
i++;
}
}
/**
* dumpBus(asArgs)
*
* Dumps Bus allocations.
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs (asArgs[0] is an optional block address)
*/
dumpBus(asArgs)
{
this.dumpBlocks(this.bus.aMemBlocks, asArgs[0]);
}
/**
* dumpHistory(sPrev, sLines)
*
* If sLines is not a number, it can be a instruction filter. However, for the moment, the only
* supported filter is "call", which filters the history buffer for all CALL and RET instructions
* from the specified previous point forward.
*
* @this {Debugger8080}
* @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10)
* @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10)
*/
dumpHistory(sPrev, sLines)
{
var sMore = "";
var cHistory = 0;
var iHistory = this.iOpcodeHistory;
var aHistory = this.aOpcodeHistory;
if (aHistory.length) {
var nPrev = +sPrev || this.nextHistory;
var nLines = +sLines || 10;
if (isNaN(nPrev)) {
nPrev = nLines;
} else {
sMore = "more ";
}
if (nPrev > aHistory.length) {
this.println("note: only " + aHistory.length + " available");
nPrev = aHistory.length;
}
iHistory -= nPrev;
if (iHistory < 0) {
/*
* If the dbgAddr of the last aHistory element contains a valid selector, wrap around.
*/
if (aHistory[aHistory.length - 1].addr == null) {
nPrev = iHistory + nPrev;
iHistory = 0;
} else {
iHistory += aHistory.length;
}
}
var aFilters = [];
if (sLines == "call") {
nLines = 100000;
aFilters = ["CALL"];
}
if (sPrev !== undefined) {
this.println(nPrev + " instructions earlier:");
}
/*
* TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive)
* faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically
* increase the amount of memory used to store instruction history (eg, storing copies of all the instruction
* bytes alongside the execution addresses).
*
* For now, we're living dangerously, so that our history dumps actually work.
*
* this.nSuppressBreaks++;
*
* If you re-enable this protection, be sure to re-enable the decrement below, too.
*/
while (nLines > 0 && iHistory != this.iOpcodeHistory) {
var dbgAddr = aHistory[iHistory++];
if (dbgAddr.addr == null) break;
/*
* We must create a new dbgAddr from the address in aHistory, because dbgAddr was
* a reference, not a copy, and we don't want getInstruction() modifying the original.
*/
var dbgAddrNew = this.newAddr(dbgAddr.addr);
var sComment = "history";
var nSequence = nPrev--;
if (DEBUG && dbgAddr.cycleCount != null) {
sComment = "cycles";
nSequence = dbgAddr.cycleCount;
}
var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence);
if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) {
this.println(sInstruction);
}
/*
* If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction()
* will have automatically disassembled additional bytes, so skip additional history entries.
*/
if (dbgAddrNew.cOverrides) {
iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides;
}
if (iHistory >= aHistory.length) iHistory = 0;
this.nextHistory = nPrev;
cHistory++;
nLines--;
}
/*
* See comments above.
*
* this.nSuppressBreaks--;
*/
}
if (!cHistory) {
this.println("no " + sMore + "history available");
this.nextHistory = undefined;
}
}
/**
* messageInit(sEnable)
*
* @this {Debugger8080}
* @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|'
*/
messageInit(sEnable)
{
this.dbg = this;
this.bitsMessage = this.bitsWarning = Messages8080.WARN;
this.sMessagePrev = null;
this.aMessageBuffer = [];
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard".
*/
var aEnable = this.parseCommand(sEnable.replace("keys","key").replace("kbd","keyboard"), false, '|');
if (aEnable.length) {
for (var m in Messages8080.CATEGORIES) {
if (Usr.indexOf(aEnable, m) >= 0) {
this.bitsMessage |= Messages8080.CATEGORIES[m];
this.println(m + " messages enabled");
}
}
}
}
/**
* messageDump(bitMessage, fnDumper)
*
* @this {Debugger8080}
* @param {number} bitMessage is one Messages category flag
* @param {function(Array.<string>)} fnDumper is a function the Debugger can use to dump data for that category
* @return {boolean} true if successfully registered, false if not
*/
messageDump(bitMessage, fnDumper)
{
for (var m in Messages8080.CATEGORIES) {
if (bitMessage == Messages8080.CATEGORIES[m]) {
this.afnDumpers[m] = fnDumper;
return true;
}
}
return false;
}
/**
* getRegIndex(sReg, off)
*
* @this {Debugger8080}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off)
{
var i;
sReg = sReg.toUpperCase();
if (off == null) {
i = Usr.indexOf(Debugger8080.REGS, sReg);
} else {
i = Usr.indexOf(Debugger8080.REGS, sReg.substr(off, 2));
if (i < 0) i = Usr.indexOf(Debugger8080.REGS, sReg.substr(off, 1));
}
return i;
}
/**
* getRegString(iReg)
*
* @this {Debugger8080}
* @param {number} iReg
* @return {string}
*/
getRegString(iReg)
{
var cch = 0;
var n = this.getRegValue(iReg);
if (n !== undefined) {
switch(iReg) {
case Debugger8080.REG_A:
case Debugger8080.REG_B:
case Debugger8080.REG_C:
case Debugger8080.REG_D:
case Debugger8080.REG_E:
case Debugger8080.REG_H:
case Debugger8080.REG_L:
case Debugger8080.REG_M:
cch = 2;
break;
case Debugger8080.REG_BC:
case Debugger8080.REG_DE:
case Debugger8080.REG_HL:
case Debugger8080.REG_SP:
case Debugger8080.REG_PC:
case Debugger8080.REG_PS:
case Debugger8080.REG_PSW:
cch = 4;
break;
}
}
return cch? Str.toHex(n, cch) : "??";
}
/**
* getRegValue(iReg)
*
* @this {Debugger8080}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
var n;
if (iReg >= 0) {
var cpu = this.cpu;
switch(iReg) {
case Debugger8080.REG_A:
n = cpu.regA;
break;
case Debugger8080.REG_B:
n = cpu.regB;
break;
case Debugger8080.REG_C:
n = cpu.regC;
break;
case Debugger8080.REG_BC:
n = cpu.getBC();
break;
case Debugger8080.REG_D:
n = cpu.regD;
break;
case Debugger8080.REG_E:
n = cpu.regE;
break;
case Debugger8080.REG_DE:
n = cpu.getDE();
break;
case Debugger8080.REG_H:
n = cpu.regH;
break;
case Debugger8080.REG_L:
n = cpu.regL;
break;
case Debugger8080.REG_HL:
n = cpu.getHL();
break;
case Debugger8080.REG_M:
n = cpu.getByte(cpu.getHL());
break;
case Debugger8080.REG_SP:
n = cpu.getSP();
break;
case Debugger8080.REG_PC:
n = cpu.getPC();
break;
case Debugger8080.REG_PS:
n = cpu.getPS();
break;
case Debugger8080.REG_PSW:
n = cpu.getPSW();
break;
default:
break;
}
}
return n;
}
/**
* replaceRegs(s)
*
* @this {Debugger8080}
* @param {string} s
* @return {string}
*/
replaceRegs(s)
{
/*
* Replace any references first; this means that register references inside the reference
* do NOT need to be prefixed with '@'.
*/
s = this.parseReference(s) || s;
/*
* Replace every @XX (or @XXX), where XX (or XXX) is a register, with the register's value.
*/
var i = 0;
var b, sChar, sAddr, dbgAddr, sReplace;
while ((i = s.indexOf('@', i)) >= 0) {
var iReg = this.getRegIndex(s, i + 1);
if (iReg >= 0) {
s = s.substr(0, i) + this.getRegString(iReg) + s.substr(i + 1 + Debugger8080.REGS[iReg].length);
}
i++;
}
/*
* Replace every #XX, where XX is a hex byte value, with the corresponding ASCII character (if printable).
*/
i = 0;
while ((i = s.indexOf('#', i)) >= 0) {
sChar = s.substr(i+1, 2);
b = Str.parseInt(sChar, 16);
if (b != null && b >= 32 && b < 128) {
sReplace = sChar + " '" + String.fromCharCode(b) + "'";
s = s.replace('#' + sChar, sReplace);
i += sReplace.length;
continue;
}
i++;
}
/*
* Replace every $XXXX:XXXX, where XXXX:XXXX is a segmented address, with the zero-terminated string at that address.
*/
i = 0;
while ((i = s.indexOf('$', i)) >= 0) {
sAddr = s.substr(i+1, 9);
dbgAddr = this.parseAddr(sAddr);
if (dbgAddr) {
sReplace = sAddr + ' "' + this.getSZ(dbgAddr) + '"';
s = s.replace('$' + sAddr, sReplace);
i += sReplace.length;
continue;
}
i++;
}
/*
* Replace every ^XXXX:XXXX, where XXXX:XXXX is a segmented address, with the FCB filename stored at that address.
*/
i = 0;
while ((i = s.indexOf('^', i)) >= 0) {
sAddr = s.substr(i+1, 9);
dbgAddr = this.parseAddr(sAddr);
if (dbgAddr) {
this.incAddr(dbgAddr);
sReplace = sAddr + ' "' + this.getSZ(dbgAddr, 11) + '"';
s = s.replace('^' + sAddr, sReplace);
i += sReplace.length;
continue;
}
i++;
}
return s;
}
/**
* message(sMessage, fAddress)
*
* @this {Debugger8080}
* @param {string} sMessage is any caller-defined message string
* @param {boolean} [fAddress] is true to display the current CS:IP
*/
message(sMessage, fAddress)
{
if (fAddress) {
sMessage += " at " + this.toHexAddr(this.newAddr(this.cpu.getPC()));
}
if (this.bitsMessage & Messages8080.BUFFER) {
this.aMessageBuffer.push(sMessage);
return;
}
if (this.sMessagePrev && sMessage == this.sMessagePrev) return;
this.sMessagePrev = sMessage;
if (this.bitsMessage & Messages8080.HALT) {
this.stopCPU();
sMessage += " (cpu halted)";
}
this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)"
/*
* We have no idea what the frequency of println() calls might be; all we know is that they easily
* screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every
* message, to effectively end the current burst and start fresh.
*
* TODO: See CPU8080.calcStartTime() for a discussion of why we might want to call yieldCPU() *before*
* we display the message.
*/
if (this.cpu) this.cpu.yieldCPU();
}
/**
* messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage)
*
* Most (if not all) port handlers should provide a name for their respective ports, so if no name is provided,
* we assume this is an unknown port, and display a message by default.
*
* @this {Debugger8080}
* @param {Component} component
* @param {number} port
* @param {number|null} bOut if an output operation
* @param {number|null} [addrFrom]
* @param {string|null} [name] of the port, if any
* @param {number|null} [bIn] is the input value, if known, on an input operation
* @param {number} [bitsMessage] is one or more Messages category flag(s)
*/
messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage)
{
bitsMessage |= Messages8080.PORT;
if (name == null || (this.bitsMessage & bitsMessage) == bitsMessage) {
this.message(component.idComponent + '.' + (bOut != null? "outPort" : "inPort") + '(' + Str.toHexWord(port) + ',' + (name? name : "unknown") + (bOut != null? ',' + Str.toHexByte(bOut) : "") + ')' + (bIn != null? (": " + Str.toHexByte(bIn)) : "") + (addrFrom != null? (" at " + this.toHexOffset(addrFrom)) : ""));
}
}
/**
* init()
*
* @this {Debugger8080}
*/
init()
{
this.println("Type ? for help with PC8080 Debugger commands");
this.updateStatus();
if (this.sInitCommands) {
var sCmds = this.sInitCommands;
this.sInitCommands = null;
this.doCommands(sCmds);
}
}
/**
* historyInit(fQuiet)
*
* This function is intended to be called by the constructor, reset(), addBreakpoint(), findBreakpoint()
* and any other function that changes the checksEnabled() criteria used to decide whether checkInstruction()
* should be called.
*
* That is, if the history arrays need to be allocated and haven't already been allocated, then allocate them,
* and if the arrays are no longer needed, then deallocate them.
*
* @this {Debugger8080}
* @param {boolean} [fQuiet]
*/
historyInit(fQuiet)
{
var i;
if (!this.checksEnabled()) {
if (this.aOpcodeHistory && this.aOpcodeHistory.length && !fQuiet) {
this.println("instruction history buffer freed");
}
this.iOpcodeHistory = 0;
this.aOpcodeHistory = [];
this.aaOpcodeCounts = [];
return;
}
if (!this.aOpcodeHistory || !this.aOpcodeHistory.length) {
this.aOpcodeHistory = new Array(Debugger8080.HISTORY_LIMIT);
for (i = 0; i < this.aOpcodeHistory.length; i++) {
/*
* Preallocate dummy Addr (Array) objects in every history slot, so that
* checkInstruction() doesn't need to call newAddr() on every slot update.
*/
this.aOpcodeHistory[i] = this.newAddr();
}
this.iOpcodeHistory = 0;
if (!fQuiet) {
this.println("instruction history buffer allocated");
}
}
if (!this.aaOpcodeCounts || !this.aaOpcodeCounts.length) {
this.aaOpcodeCounts = new Array(256);
for (i = 0; i < this.aaOpcodeCounts.length; i++) {
this.aaOpcodeCounts[i] = [i, 0];
}
}
}
/**
* runCPU(fUpdateFocus)
*
* @this {Debugger8080}
* @param {boolean} [fUpdateFocus] is true to update focus
* @return {boolean} true if run request successful, false if not
*/
runCPU(fUpdateFocus)
{
if (!this.isCPUAvail()) return false;
this.cpu.runCPU(fUpdateFocus);
return true;
}
/**
* stepCPU(nCycles, fRegs, fUpdateCPU)
*
* @this {Debugger8080}
* @param {number} nCycles (0 for one instruction without checking breakpoints)
* @param {boolean} [fRegs] is true to display registers after step (default is false)
* @param {boolean} [fUpdateCPU] is false to disable calls to updateCPU() (default is true)
* @return {boolean}
*/
stepCPU(nCycles, fRegs, fUpdateCPU)
{
if (!this.isCPUAvail()) return false;
this.nCycles = 0;
if (!nCycles) {
/*
* When single-stepping, the CPU won't call checkInstruction(), which is good for
* avoiding breakpoints, but bad for instruction data collection if checks are enabled.
* So we call checkInstruction() ourselves.
*/
if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0);
}
try {
var nCyclesStep = this.cpu.stepCPU(nCycles);
if (nCyclesStep > 0) {
this.nCycles += nCyclesStep;
this.cpu.addCycles(nCyclesStep, true);
this.cpu.updateChecksum(nCyclesStep);
this.cOpcodes++;
}
}
catch(exception) {
if (typeof exception != "number") {
var e = exception;
this.nCycles = 0;
this.cpu.setError(e.stack || e.message);
}
}
/*
* Because we called cpu.stepCPU() and not cpu.runCPU(), we must nudge the cpu's update code,
* and then update our own state. Normally, the only time fUpdateCPU will be false is when doTrace()
* is calling us in a loop, in which case it will perform its own updateCPU() when it's done.
*/
if (fUpdateCPU !== false) this.cpu.updateCPU();
this.updateStatus(fRegs || false);
return (this.nCycles > 0);
}
/**
* stopCPU()
*
* @this {Debugger8080}
* @param {boolean} [fComplete]
*/
stopCPU(fComplete)
{
if (this.cpu) this.cpu.stopCPU(fComplete);
}
/**
* updateStatus(fRegs)
*
* @this {Debugger8080}
* @param {boolean} [fRegs] (default is true)
*/
updateStatus(fRegs)
{
if (fRegs === undefined) fRegs = true;
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
/*
* this.nStep used to be a simple boolean, but now it's 0 (or undefined)
* if inactive, 1 if stepping over an instruction without a register dump, or 2
* if stepping over an instruction with a register dump.
*/
if (!fRegs || this.nStep == 1)
this.doUnassemble();
else {
this.doRegisters();
}
}
/**
* isCPUAvail()
*
* Make sure the CPU is ready (finished initializing), not busy (already running), and not in an error state.
*
* @this {Debugger8080}
* @return {boolean}
*/
isCPUAvail()
{
if (!this.cpu)
return false;
if (!this.cpu.isReady())
return false;
if (!this.cpu.isPowered())
return false;
if (this.cpu.isBusy())
return false;
return !this.cpu.isError();
}
/**
* powerUp(data, fRepower)
*
* @this {Debugger8080}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* Because Debugger save/restore support is somewhat limited (and didn't always exist),
* we deviate from the typical save/restore design pattern: instead of reset OR restore,
* we always reset and then perform a (potentially limited) restore.
*/
this.reset(true);
// this.println(data? "resuming" : "powering up");
if (data && this.restore) {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Debugger8080}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean}
*/
powerDown(fSave, fShutdown)
{
if (fShutdown) this.println(fSave? "suspending" : "shutting down");
return fSave? this.save() : true;
}
/**
* reset(fQuiet)
*
* This is a notification handler, called by the Computer, to inform us of a reset.
*
* @this {Debugger8080}
* @param {boolean} fQuiet (true only when called from our own powerUp handler)
*/
reset(fQuiet)
{
this.historyInit();
this.cOpcodes = this.cOpcodesStart = 0;
this.sMessagePrev = null;
this.nCycles = 0;
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
/*
* fRunning is set by start() and cleared by stop(). In addition, we clear
* it here, so that if the CPU is reset while running, we can prevent stop()
* from unnecessarily dumping the CPU state.
*/
this.flags.running = false;
this.clearTempBreakpoint();
if (!fQuiet) this.updateStatus();
}
/**
* save()
*
* This implements (very rudimentary) save support for the Debugger component.
*
* @this {Debugger8080}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.packAddr(this.dbgAddrNextCode));
state.set(1, this.packAddr(this.dbgAddrAssemble));
state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]);
state.set(3, this.aSymbolTable);
return state.data();
}
/**
* restore(data)
*
* This implements (very rudimentary) restore support for the Debugger component.
*
* @this {Debugger8080}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var i = 0;
if (data[2] !== undefined) {
this.dbgAddrNextCode = this.unpackAddr(data[i++]);
this.dbgAddrAssemble = this.unpackAddr(data[i++]);
this.aPrevCmds = data[i][0];
if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds];
this.fAssemble = data[i][1];
this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state
}
if (data[3]) this.aSymbolTable = data[3];
return true;
}
/**
* start(ms, nCycles)
*
* This is a notification handler, called by the Computer, to inform us the CPU has started.
*
* @this {Debugger8080}
* @param {number} ms
* @param {number} nCycles
*/
start(ms, nCycles)
{
if (!this.nStep) this.println("running");
this.flags.running = true;
this.msStart = ms;
this.nCyclesStart = nCycles;
}
/**
* stop(ms, nCycles)
*
* This is a notification handler, called by the Computer, to inform us the CPU has now stopped.
*
* @this {Debugger8080}
* @param {number} ms
* @param {number} nCycles
*/
stop(ms, nCycles)
{
if (this.flags.running) {
this.flags.running = false;
this.nCycles = nCycles - this.nCyclesStart;
if (!this.nStep) {
var sStopped = "stopped";
if (this.nCycles) {
var msTotal = ms - this.msStart;
var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0);
sStopped += " (";
if (this.checksEnabled()) {
sStopped += this.cOpcodes + " opcodes, ";
/*
* $ops displays progress by calculating cOpcodes - cOpcodesStart, so before
* zeroing cOpcodes, we should subtract cOpcodes from cOpcodesStart (since we're
* effectively subtracting cOpcodes from cOpcodes as well).
*/
this.cOpcodesStart -= this.cOpcodes;
this.cOpcodes = 0;
}
sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)";
} else {
if (this.messageEnabled(Messages8080.HALT)) {
/*
* It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault()
* for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful
* message (another helpful tip would be to simply turn off the "halt" message category).
*/
sStopped += " (use the 't' command to execute blocked faults)";
}
}
this.println(sStopped);
}
this.updateStatus(true);
this.updateFocus();
this.clearTempBreakpoint(this.cpu.getPC());
}
}
/**
* checksEnabled(fRelease)
*
* This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked.
*
* Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints
* no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate
* "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've
* simplified the test below.
*
* @this {Debugger8080}
* @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria)
* @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not
*/
checksEnabled(fRelease)
{
return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakIns));
}
/**
* checkInstruction(addr, nState)
*
* This "check" function is called by the CPU to inform us about the next instruction to be executed,
* giving us an opportunity to look for "exec" breakpoints and update opcode frequencies and instruction history.
*
* @this {Debugger8080}
* @param {number} addr
* @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running
* @return {boolean} true if breakpoint hit, false if not
*/
checkInstruction(addr, nState)
{
var cpu = this.cpu;
if (nState > 0) {
if (this.nBreakIns && !--this.nBreakIns) {
return true;
}
if (this.checkBreakpoint(addr, 1, this.aBreakExec)) {
return true;
}
}
/*
* The rest of the instruction tracking logic can only be performed if historyInit() has allocated the
* necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than
* adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction();
* well, OK, and a few other things now, like enabling Messages8080.INT messages.
*/
if (nState >= 0 && this.aaOpcodeCounts.length) {
this.cOpcodes++;
var bOpcode = this.bus.getByteDirect(addr);
if (bOpcode != null) {
this.aaOpcodeCounts[bOpcode][1]++;
var dbgAddr = this.aOpcodeHistory[this.iOpcodeHistory];
this.setAddr(dbgAddr, cpu.getPC());
if (DEBUG) dbgAddr.cycleCount = cpu.getCycles();
if (++this.iOpcodeHistory == this.aOpcodeHistory.length) this.iOpcodeHistory = 0;
}
}
return false;
}
/**
* checkMemoryRead(addr, nb)
*
* This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an
* opportunity to track the read if we want, and look for a matching "read" breakpoint, if any.
*
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read.
*
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
*
* @this {Debugger8080}
* @param {number} addr
* @param {number} [nb] (# of bytes; default is 1)
* @return {boolean} true if breakpoint hit, false if not
*/
checkMemoryRead(addr, nb)
{
if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) {
this.stopCPU(true);
return true;
}
return false;
}
/**
* checkMemoryWrite(addr, nb)
*
* This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an
* opportunity to track the write if we want, and look for a matching "write" breakpoint, if any.
*
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write.
*
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
*
* @this {Debugger8080}
* @param {number} addr
* @param {number} [nb] (# of bytes; default is 1)
* @return {boolean} true if breakpoint hit, false if not
*/
checkMemoryWrite(addr, nb)
{
if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) {
this.stopCPU(true);
return true;
}
return false;
}
/**
* checkPortInput(port, size, data)
*
* This "check" function is called by the Bus component to inform us that port input occurred.
*
* @this {Debugger8080}
* @param {number} port
* @param {number} size
* @param {number} data
* @return {boolean} true if breakpoint hit, false if not
*/
checkPortInput(port, size, data)
{
/*
* We trust that the Bus component won't call us unless we told it to, so we halt unconditionally
*/
this.println("break on input from port " + Str.toHexWord(port) + ": " + Str.toHex(data));
this.stopCPU(true);
return true;
}
/**
* checkPortOutput(port, size, data)
*
* This "check" function is called by the Bus component to inform us that port output occurred.
*
* @this {Debugger8080}
* @param {number} port
* @param {number} size
* @param {number} data
* @return {boolean} true if breakpoint hit, false if not
*/
checkPortOutput(port, size, data)
{
/*
* We trust that the Bus component won't call us unless we told it to, so we halt unconditionally
*/
this.println("break on output to port " + Str.toHexWord(port) + ": " + Str.toHex(data));
this.stopCPU(true);
return true;
}
/**
* clearBreakpoints()
*
* @this {Debugger8080}
*/
clearBreakpoints()
{
var i, dbgAddr;
this.aBreakExec = ["bp"];
if (this.aBreakRead !== undefined) {
for (i = 1; i < this.aBreakRead.length; i++) {
dbgAddr = this.aBreakRead[i];
this.bus.removeMemBreak(this.getAddr(dbgAddr), false);
}
}
this.aBreakRead = ["br"];
if (this.aBreakWrite !== undefined) {
for (i = 1; i < this.aBreakWrite.length; i++) {
dbgAddr = this.aBreakWrite[i];
this.bus.removeMemBreak(this.getAddr(dbgAddr), true);
}
}
this.aBreakWrite = ["bw"];
/*
* nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup requires
* reading a segment descriptor via getSegment(), and that triggers more memory reads, which triggers
* more breakpoint checks.
*/
this.nSuppressBreaks = 0;
}
/**
* addBreakpoint(aBreak, dbgAddr, fTemporary)
*
* In case you haven't already figured this out, all our breakpoint commands use the address
* to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers;
* see doBreak() for details.
*
* This has a few implications, one being that you CANNOT set more than one kind of breakpoint
* on a single address. In practice, that's rarely a problem, because you can almost always set
* a different breakpoint on a neighboring address.
*
* Also, there is one exception to the "one address, one breakpoint" rule, and that involves
* temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command
* may create to step over a chunk of code). Those breakpoints automatically clear themselves,
* so there usually isn't any need to refer to them using breakpoint commands.
*
* TODO: Consider supporting the more "traditional" breakpoint index syntax; the current
* address-based syntax was implemented solely for expediency and consistency. At the same time,
* also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific
* breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc.
*
* @this {Debugger8080}
* @param {Array} aBreak
* @param {DbgAddr8080} dbgAddr
* @param {boolean} [fTemporary]
* @return {boolean} true if breakpoint added, false if already exists
*/
addBreakpoint(aBreak, dbgAddr, fTemporary)
{
var fSuccess = true;
// this.nSuppressBreaks++;
/*
* Instead of complaining that a breakpoint already exists (as we used to do), we now
* allow breakpoints to be re-set; this makes it easier to update any commands that may
* be associated with the breakpoint.
*
* The only exception: we DO allow a temporary breakpoint at an address where there may
* already be a breakpoint, so that you can easily step ("p" or "g") over such addresses.
*/
if (!fTemporary) {
this.findBreakpoint(aBreak, dbgAddr, true, false, true);
}
if (aBreak != this.aBreakExec) {
var addr = this.getAddr(dbgAddr);
if (addr === CPUDef8080.ADDR_INVALID) {
this.println("invalid address: " + this.toHexAddr(dbgAddr));
fSuccess = false;
} else {
this.bus.addMemBreak(addr, aBreak == this.aBreakWrite);
}
}
if (fSuccess) {
aBreak.push(dbgAddr);
if (fTemporary) {
dbgAddr.fTemporary = true;
}
else {
this.printBreakpoint(aBreak, aBreak.length-1, "set");
this.historyInit();
}
}
// this.nSuppressBreaks--;
return fSuccess;
}
/**
* findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
*
* @this {Debugger8080}
* @param {Array} aBreak
* @param {DbgAddr8080} dbgAddr
* @param {boolean} [fRemove]
* @param {boolean} [fTemporary]
* @param {boolean} [fQuiet]
* @return {boolean} true if found, false if not
*/
findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
{
var fFound = false;
var addr = this.getAddr(dbgAddr);
for (var i = 1; i < aBreak.length; i++) {
var dbgAddrBreak = aBreak[i];
if (addr == this.getAddr(dbgAddrBreak)) {
if (!fTemporary || dbgAddrBreak.fTemporary) {
fFound = true;
if (fRemove) {
if (!dbgAddrBreak.fTemporary && !fQuiet) {
this.printBreakpoint(aBreak, i, "cleared");
}
aBreak.splice(i, 1);
if (aBreak != this.aBreakExec) {
this.bus.removeMemBreak(addr, aBreak == this.aBreakWrite);
}
/*
* We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this
* was a temporary breakpoint.
*/
if (!dbgAddrBreak.fTemporary) {
this.historyInit();
}
break;
}
if (!fQuiet) this.printBreakpoint(aBreak, i, "exists");
break;
}
}
}
return fFound;
}
/**
* listBreakpoints(aBreak)
*
* @this {Debugger8080}
* @param {Array} aBreak
* @return {number} of breakpoints listed, 0 if none
*/
listBreakpoints(aBreak)
{
for (var i = 1; i < aBreak.length; i++) {
this.printBreakpoint(aBreak, i);
}
return aBreak.length - 1;
}
/**
* printBreakpoint(aBreak, i, sAction)
*
* @this {Debugger8080}
* @param {Array} aBreak
* @param {number} i
* @param {string} [sAction]
*/
printBreakpoint(aBreak, i, sAction)
{
var dbgAddr = aBreak[i];
this.println(aBreak[0] + ' ' + this.toHexAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : '')));
}
/**
* setTempBreakpoint(dbgAddr)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr of new temp breakpoint
*/
setTempBreakpoint(dbgAddr)
{
this.addBreakpoint(this.aBreakExec, dbgAddr, true);
}
/**
* clearTempBreakpoint(addr)
*
* @this {Debugger8080}
* @param {number|undefined} [addr] clear all temp breakpoints if no address specified
*/
clearTempBreakpoint(addr)
{
if (addr !== undefined) {
this.checkBreakpoint(addr, 1, this.aBreakExec, true);
this.nStep = 0;
} else {
for (var i = 1; i < this.aBreakExec.length; i++) {
var dbgAddrBreak = this.aBreakExec[i];
if (dbgAddrBreak.fTemporary) {
if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break;
i = 0;
}
}
}
}
/**
* checkBreakpoint(addr, nb, aBreak, fTemporary)
*
* @this {Debugger8080}
* @param {number} addr
* @param {number} nb (# of bytes)
* @param {Array} aBreak
* @param {boolean} [fTemporary]
* @return {boolean} true if breakpoint has been hit, false if not
*/
checkBreakpoint(addr, nb, aBreak, fTemporary)
{
/*
* Time to check for execution breakpoints; note that this should be done BEFORE updating frequency
* or history data (see checkInstruction), since we might not actually execute the current instruction.
*/
var fBreak = false;
if (!this.nSuppressBreaks++) {
for (var i = 1; !fBreak && i < aBreak.length; i++) {
var dbgAddrBreak = aBreak[i];
if (fTemporary && !dbgAddrBreak.fTemporary) continue;
/*
* We used to calculate the linear address of the breakpoint at the time the
* breakpoint was added, so that a breakpoint set in one mode (eg, in real-mode)
* would still work as intended if the mode changed later (eg, to protected-mode).
*
* However, that created difficulties setting protected-mode breakpoints in segments
* that might not be defined yet, or that could move in physical memory.
*
* If you want to create a real-mode breakpoint that will break regardless of mode,
* use the physical address of the real-mode memory location instead.
*/
var addrBreak = this.getAddr(dbgAddrBreak);
for (var n = 0; n < nb; n++) {
if (addr + n == addrBreak) {
var a;
fBreak = true;
if (dbgAddrBreak.fTemporary) {
this.findBreakpoint(aBreak, dbgAddrBreak, true, true);
fTemporary = true;
}
if (a = dbgAddrBreak.aCmds) {
/*
* When one or more commands are attached to a breakpoint, we don't halt by default.
* Instead, we set fBreak to true only if, at the completion of all the commands, the
* CPU is halted; in other words, you should include "h" as one of the breakpoint commands
* if you want the breakpoint to stop execution.
*
* Another useful command is "if", which will return false if the expression is false,
* at which point we'll jump ahead to the next "else" command, and if there isn't an "else",
* we abort.
*/
fBreak = false;
for (var j = 0; j < a.length; j++) {
if (!this.doCommand(a[j], true)) {
if (a[j].indexOf("if")) {
fBreak = true; // the failed command wasn't "if", so abort
break;
}
var k = j + 1;
for (; k < a.length; k++) {
if (!a[k].indexOf("else")) break;
j++;
}
if (k == a.length) { // couldn't find an "else" after the "if", so abort
fBreak = true;
break;
}
/*
* If we're still here, we'll execute the "else" command (which is just a no-op),
* followed by any remaining commands.
*/
}
}
if (!this.cpu.isRunning()) fBreak = true;
}
if (fBreak) {
if (!fTemporary) this.printBreakpoint(aBreak, i, "hit");
break;
}
}
}
}
}
this.nSuppressBreaks--;
return fBreak;
}
/**
* getInstruction(dbgAddr, sComment, nSequence)
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {string} [sComment] is an associated comment
* @param {number|null} [nSequence] is an associated sequence number, undefined if none
* @return {string} (and dbgAddr is updated to the next instruction)
*/
getInstruction(dbgAddr, sComment, nSequence)
{
var dbgAddrIns = this.newAddr(dbgAddr.addr);
var bOpcode = this.getByte(dbgAddr, 1);
var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086;
var aOpDesc = this.aaOpDescs[bOpcode];
var iIns = aOpDesc[0];
var sOperands = "";
var sOpcode = asOpcodes[iIns];
var cOperands = aOpDesc.length - 1;
var typeSizeDefault = Debugger8080.TYPE_NONE, type;
for (var iOperand = 1; iOperand <= cOperands; iOperand++) {
var disp, off, cch;
var sOperand = "";
type = aOpDesc[iOperand];
if (type === undefined) continue;
if ((type & Debugger8080.TYPE_OPT) && this.style == Debugger8080.STYLE_8080) continue;
var typeMode = type & Debugger8080.TYPE_MODE;
if (!typeMode) continue;
var typeSize = type & Debugger8080.TYPE_SIZE;
if (!typeSize) {
type |= typeSizeDefault;
} else {
typeSizeDefault = typeSize;
}
var typeOther = type & Debugger8080.TYPE_OTHER;
if (!typeOther) {
type |= (iOperand == 1? Debugger8080.TYPE_OUT : Debugger8080.TYPE_IN);
}
if (typeMode & Debugger8080.TYPE_IMM) {
sOperand = this.getImmOperand(type, dbgAddr);
}
else if (typeMode & Debugger8080.TYPE_REG) {
sOperand = this.getRegOperand((type & Debugger8080.TYPE_IREG) >> 8, type, dbgAddr);
}
else if (typeMode & Debugger8080.TYPE_INT) {
sOperand = ((bOpcode >> 3) & 0x7).toString();
}
if (!sOperand || !sOperand.length) {
sOperands = "INVALID";
break;
}
if (sOperands.length > 0) sOperands += ',';
sOperands += (sOperand || "???");
}
var sBytes = "";
var sLine = this.toHexAddr(dbgAddrIns) + ' ';
if (dbgAddrIns.addr !== CPUDef8080.ADDR_INVALID && dbgAddr.addr !== CPUDef8080.ADDR_INVALID) {
do {
sBytes += Str.toHex(this.getByte(dbgAddrIns, 1), 2);
if (dbgAddrIns.addr == null) break;
} while (dbgAddrIns.addr != dbgAddr.addr);
}
sLine += Str.pad(sBytes, 10);
sLine += (type & Debugger8080.TYPE_UNDOC)? '*' : ' ';
sLine += Str.pad(sOpcode, 7);
if (sOperands) sLine += ' ' + sOperands;
if (sComment) {
sLine = Str.pad(sLine, 40) + ';' + sComment;
if (!this.cpu.flags.checksum) {
sLine += (nSequence != null? '=' + nSequence.toString() : "");
} else {
var nCycles = this.cpu.getCycles();
sLine += "cycles=" + nCycles.toString() + " cs=" + Str.toHex(this.cpu.nChecksum);
}
}
return sLine;
}
/**
* getImmOperand(type, dbgAddr)
*
* @this {Debugger8080}
* @param {number} type
* @param {DbgAddr8080} dbgAddr
* @return {string} operand
*/
getImmOperand(type, dbgAddr)
{
var sOperand = ' ';
var typeSize = type & Debugger8080.TYPE_SIZE;
switch (typeSize) {
case Debugger8080.TYPE_BYTE:
sOperand = Str.toHex(this.getByte(dbgAddr, 1), 2);
break;
case Debugger8080.TYPE_SBYTE:
sOperand = Str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4);
break;
case Debugger8080.TYPE_WORD:
sOperand = Str.toHex(this.getShort(dbgAddr, 2), 4);
break;
default:
return "imm(" + Str.toHexWord(type) + ')';
}
if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) {
sOperand = '[' + sOperand + ']';
} else if (!(type & Debugger8080.TYPE_REG)) {
sOperand = (this.style == Debugger8080.STYLE_8080? '$' : "0x") + sOperand;
}
return sOperand;
}
/**
* getRegOperand(iReg, type, dbgAddr)
*
* @this {Debugger8080}
* @param {number} iReg
* @param {number} type
* @param {DbgAddr8080} dbgAddr
* @return {string} operand
*/
getRegOperand(iReg, type, dbgAddr)
{
/*
* Although this breaks with 8080 assembler conventions, I'm going to experiment with some different
* mnemonics; specifically, "[HL]" instead of "M". This is also more in keeping with how getImmOperand()
* displays memory references (ie, by enclosing them in brackets).
*/
var sOperand = Debugger8080.REGS[iReg];
if (this.style == Debugger8080.STYLE_8086 && (type & Debugger8080.TYPE_MEM)) {
if (iReg == Debugger8080.REG_M) {
sOperand = "HL";
}
sOperand = '[' + sOperand + ']';
}
return sOperand;
}
/**
* parseInstruction(sOp, sOperand, addr)
*
* TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a working implementation.
*
* @this {Debugger8080}
* @param {string} sOp
* @param {string|undefined} sOperand
* @param {DbgAddr8080} dbgAddr of memory where this instruction is being assembled
* @return {Array.<number>} of opcode bytes; if the instruction can't be parsed, the array will be empty
*/
parseInstruction(sOp, sOperand, dbgAddr)
{
var aOpBytes = [];
this.println("not supported yet");
return aOpBytes;
}
/**
* getFlagOutput(sFlag)
*
* @this {Debugger8080}
* @param {string} sFlag
* @return {string} value of flag
*/
getFlagOutput(sFlag)
{
var b;
switch (sFlag) {
case "IF":
b = this.cpu.getIF();
break;
case "SF":
b = this.cpu.getSF();
break;
case "ZF":
b = this.cpu.getZF();
break;
case "AF":
b = this.cpu.getAF();
break;
case "PF":
b = this.cpu.getPF();
break;
case "CF":
b = this.cpu.getCF();
break;
default:
b = 0;
break;
}
return sFlag.charAt(0) + (b? '1' : '0') + ' ';
}
/**
* getRegOutput(iReg)
*
* @this {Debugger8080}
* @param {number} iReg
* @return {string}
*/
getRegOutput(iReg)
{
var sReg = Debugger8080.REGS[iReg];
return sReg + '=' + this.getRegString(iReg) + ' ';
}
/**
* getRegDump()
*
* Sample 8080 register dump:
*
* A=00 BC=0000 DE=0000 HL=0000 SP=0000 I0 S0 Z0 A0 P0 C0
* 0000 00 NOP
*
* @this {Debugger8080}
* @return {string}
*/
getRegDump()
{
var s;
s = this.getRegOutput(Debugger8080.REG_A) +
this.getRegOutput(Debugger8080.REG_BC) +
this.getRegOutput(Debugger8080.REG_DE) +
this.getRegOutput(Debugger8080.REG_HL) +
this.getRegOutput(Debugger8080.REG_SP) +
this.getFlagOutput("IF") + this.getFlagOutput("SF") + this.getFlagOutput("ZF") +
this.getFlagOutput("AF") + this.getFlagOutput("PF") + this.getFlagOutput("CF");
return s;
}
/**
* comparePairs(p1, p2)
*
* @this {Debugger8080}
* @param {number|string|Array|Object} p1
* @param {number|string|Array|Object} p2
* @return {number}
*/
comparePairs(p1, p2)
{
return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0;
}
/**
* addSymbols(sModule, addr, len, aSymbols)
*
* As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist
* of all the symbols (in upper-case), and the values of those properties are objects containing any or all of
* the following properties:
*
* 'v': the value of an absolute (unsized) value
* 'b': either 1, 2, 4 or undefined if an unsized value
* 's': either a hard-coded segment or undefined
* 'o': the offset of the symbol within the associated address space
* 'l': the original-case version of the symbol, present only if it wasn't originally upper-case
* 'a': annotation for the specified offset; eg, the original assembly language, with optional comment
*
* To that list of properties, we also add:
*
* 'p': the physical address (calculated whenever both 's' and 'o' properties are defined)
*
* Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values
* for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original
* annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for
* converting them back to individual double-quotes.
*
* For example:
* {
* 'HF_PORT': {
* 'v':800
* },
* 'HDISK_INT': {
* 'b':4, 's':0, 'o':52
* },
* 'ORG_VECTOR': {
* 'b':4, 's':0, 'o':76
* },
* 'CMD_BLOCK': {
* 'b':1, 's':64, 'o':66
* },
* 'DISK_SETUP': {
* 'o':3
* },
* '.40': {
* 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR"
* }
* }
*
* If a symbol only has an offset, then that offset value can be assigned to the symbol property directly:
*
* 'DISK_SETUP': 3
*
* The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol.
* Such entries are identified by a period followed by a unique number (usually the offset of the entry), and
* they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading
* period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols.
*
* We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which
* look like:
*
* [addr, len, aSymbols, aOffsets]
*
* There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any,
* at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous
* symbol with a matching offset ('o') property.
*
* To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs
* (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space.
*
* We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert();
* it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're
* hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates
* properly.
*
* @this {Debugger8080}
* @param {string|null} sModule
* @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM)
* @param {number} len (the size of the region, in bytes)
* @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below)
*/
addSymbols(sModule, addr, len, aSymbols)
{
var dbgAddr = {};
var aOffsets = [];
for (var sSymbol in aSymbols) {
var symbol = aSymbols[sSymbol];
if (typeof symbol == "number") {
aSymbols[sSymbol] = symbol = {'o': symbol};
}
var offSymbol = symbol['o'];
var sAnnotation = symbol['a'];
if (offSymbol !== undefined) {
Usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs);
}
if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\"");
}
var symbolTable = {
sModule: sModule,
addr: addr,
len: len,
aSymbols: aSymbols,
aOffsets: aOffsets
};
this.aSymbolTable.push(symbolTable);
}
/**
* dumpSymbols()
*
* TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever
* order they appeared in the original MAP file.
*
* @this {Debugger8080}
*/
dumpSymbols()
{
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
for (var sSymbol in symbolTable.aSymbols) {
if (sSymbol.charAt(0) == '.') continue;
var symbol = symbolTable.aSymbols[sSymbol];
var offSymbol = symbol['o'];
if (offSymbol === undefined) continue;
var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l'];
if (sSymbolOrig) sSymbol = sSymbolOrig;
this.println(this.toHexOffset(offSymbol) + ' ' + sSymbol);
}
}
}
/**
* findSymbol(dbgAddr, fNearest)
*
* Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found).
*
* If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of
* entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @param {boolean} [fNearest]
* @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment
*/
findSymbol(dbgAddr, fNearest)
{
var aSymbol = [];
var addrSymbol = this.getAddr(dbgAddr) >>> 0;
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
var addr = symbolTable.addr >>> 0;
var len = symbolTable.len;
if (addrSymbol >= addr && addrSymbol < addr + len) {
var offSymbol = addrSymbol - addr;
var result = Usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs);
if (result >= 0) {
this.returnSymbol(iTable, result, aSymbol);
}
else if (fNearest) {
result = ~result;
this.returnSymbol(iTable, result-1, aSymbol);
this.returnSymbol(iTable, result, aSymbol);
}
break;
}
}
return aSymbol;
}
/**
* findSymbolAddr(sSymbol)
*
* Search aSymbolTable for sSymbol, and if found, return a dbgAddr (same as parseAddr())
*
* @this {Debugger8080}
* @param {string} sSymbol
* @return {DbgAddr8080|undefined}
*/
findSymbolAddr(sSymbol)
{
var dbgAddr;
if (sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) {
var sUpperCase = sSymbol.toUpperCase();
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
var symbol = symbolTable.aSymbols[sUpperCase];
if (symbol !== undefined) {
var offSymbol = symbol['o'];
if (offSymbol !== undefined) {
/*
* We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an
* explicitly-defined segment, we return the segment associated with the entire group; for
* a ROM, that segment is normally "addrROM >>> 4". Down the road, we may want/need to
* support a special symbol entry (eg, ".ORG") that defines an alternate origin.
*/
dbgAddr = this.newAddr(offSymbol);
}
/*
* The symbol matched, but it wasn't for an address (no 'o' offset), and there's no point
* looking any farther, since each symbol appears only once, so we indicate it's an unknown symbol.
*/
break;
}
}
}
return dbgAddr;
}
/**
* returnSymbol(iTable, iOffset, aSymbol)
*
* Helper function for findSymbol().
*
* @param {number} iTable
* @param {number} iOffset
* @param {Array} aSymbol is updated with the specified symbol, if it exists
*/
returnSymbol(iTable, iOffset, aSymbol)
{
var symbol = {};
var aOffsets = this.aSymbolTable[iTable].aOffsets;
var offset = 0, sSymbol = null;
if (iOffset >= 0 && iOffset < aOffsets.length) {
offset = aOffsets[iOffset][0];
sSymbol = aOffsets[iOffset][1];
}
if (sSymbol) {
symbol = this.aSymbolTable[iTable].aSymbols[sSymbol];
sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol));
}
aSymbol.push(sSymbol);
aSymbol.push(offset);
aSymbol.push(symbol['a']);
aSymbol.push(symbol['c']);
}
/**
* doHelp()
*
* @this {Debugger8080}
*/
doHelp()
{
var s = "commands:";
for (var sCommand in Debugger8080.COMMANDS) {
s += '\n' + Str.pad(sCommand, 9) + Debugger8080.COMMANDS[sCommand];
}
if (!this.checksEnabled()) s += "\nnote: frequency/history disabled if no exec breakpoints";
this.println(s);
}
/**
* doAssemble(asArgs)
*
* This always receives the complete argument array, where the order of the arguments is:
*
* [0]: the assemble command (assumed to be "a")
* [1]: the target address (eg, "200")
* [2]: the operation code, aka instruction name (eg, "adc")
* [3]: the operation mode operand, if any (eg, "14", "[1234]", etc)
*
* The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present.
* As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands
* until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a"
* and the next available target address to the argument array.
*
* Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg:
*
* a ff00 cld
* a ff01 ldx 28
* ...
*
* without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage
* of automatic target address advancement (see dbgAddrAssemble).
*
* NOTE: As the previous example implies, you can even assemble new instructions into ROM address space;
* as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU.
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs is the complete argument array, beginning with the "a" command in asArgs[0]
*/
doAssemble(asArgs)
{
var dbgAddr = this.parseAddr(asArgs[1], true);
if (!dbgAddr) return;
this.dbgAddrAssemble = dbgAddr;
if (asArgs[2] === undefined) {
this.println("begin assemble at " + this.toHexAddr(dbgAddr));
this.fAssemble = true;
this.cpu.updateCPU();
return;
}
var aOpBytes = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr);
if (aOpBytes.length) {
for (var i = 0; i < aOpBytes.length; i++) {
this.setByte(dbgAddr, aOpBytes[i], 1);
}
/*
* Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced.
*/
this.println(this.getInstruction(this.dbgAddrAssemble));
}
}
/**
* doBreak(sCmd, sAddr, sOptions)
*
* As the "help" output below indicates, the following breakpoint commands are supported:
*
* bp [a] set exec breakpoint on linear addr [a]
* br [a] set read breakpoint on linear addr [a]
* bw [a] set write breakpoint on linear addr [a]
* bc [a] clear breakpoint on linear addr [a] (use "*" for all breakpoints)
* bl list breakpoints
*
* to which we have recently added the following I/O breakpoint commands:
*
* bi [p] toggle input breakpoint on port [p] (use "*" for all input ports)
* bo [p] toggle output breakpoint on port [p] (use "*" for all output ports)
*
* These two new commands operate as toggles so that if "*" is used to trap all input (or output),
* you can also use these commands to NOT trap specific ports.
*
* bn [n] break after [n] instructions
*
* TODO: Update the "bl" command to include any/all I/O breakpoints, and the "bc" command to
* clear them. Because "bi" and "bo" commands are piggy-backing on Bus functions, those breakpoints
* are currently outside the realm of what the "bl" and "bc" commands are aware of.
*
* @this {Debugger8080}
* @param {string} sCmd
* @param {string|undefined} [sAddr]
* @param {string} [sOptions] (the rest of the breakpoint command-line)
*/
doBreak(sCmd, sAddr, sOptions)
{
if (sAddr == '?') {
this.println("breakpoint commands:");
this.println("\tbi [p]\ttoggle break on input port [p]");
this.println("\tbo [p]\ttoggle break on output port [p]");
this.println("\tbp [a]\tset exec breakpoint at addr [a]");
this.println("\tbr [a]\tset read breakpoint at addr [a]");
this.println("\tbw [a]\tset write breakpoint at addr [a]");
this.println("\tbc [a]\tclear breakpoint at addr [a]");
this.println("\tbl\tlist all breakpoints");
this.println("\tbn [n]\tbreak after [n] instruction(s)");
return;
}
var sParm = sCmd.charAt(1);
if (sParm == 'l') {
var cBreaks = 0;
cBreaks += this.listBreakpoints(this.aBreakExec);
cBreaks += this.listBreakpoints(this.aBreakRead);
cBreaks += this.listBreakpoints(this.aBreakWrite);
if (!cBreaks) this.println("no breakpoints");
return;
}
if (sParm == 'n') {
this.nBreakIns = this.parseValue(sAddr);
this.println("break after " + this.nBreakIns + " instruction(s)");
return;
}
if (sAddr === undefined) {
this.println("missing breakpoint address");
return;
}
var dbgAddr = this.newAddr();
if (sAddr != '*') {
dbgAddr = this.parseAddr(sAddr, true, true);
if (!dbgAddr) return;
}
sAddr = Str.toHexWord(dbgAddr.addr);
if (sParm == 'c') {
if (dbgAddr.addr == null) {
this.clearBreakpoints();
this.println("all breakpoints cleared");
return;
}
if (this.findBreakpoint(this.aBreakExec, dbgAddr, true))
return;
if (this.findBreakpoint(this.aBreakRead, dbgAddr, true))
return;
if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true))
return;
this.println("breakpoint missing: " + this.toHexAddr(dbgAddr));
return;
}
if (sParm == 'i') {
this.println("breakpoint " + (this.bus.addPortInputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (input)");
return;
}
if (sParm == 'o') {
this.println("breakpoint " + (this.bus.addPortOutputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (output)");
return;
}
if (dbgAddr.addr == null) return;
this.parseAddrOptions(dbgAddr, sOptions);
if (sParm == 'p') {
this.addBreakpoint(this.aBreakExec, dbgAddr);
return;
}
if (sParm == 'r') {
this.addBreakpoint(this.aBreakRead, dbgAddr);
return;
}
if (sParm == 'w') {
this.addBreakpoint(this.aBreakWrite, dbgAddr);
return;
}
this.println("unknown breakpoint command: " + sParm);
}
/**
* doClear(sCmd)
*
* @this {Debugger8080}
* @param {string} [sCmd] (eg, "cls" or "clear")
*/
doClear(sCmd)
{
/*
* TODO: There should be a clear() component method that the Control Panel overrides to perform this function.
*/
if (this.controlPrint) this.controlPrint.value = "";
}
/**
* doDump(asArgs)
*
* The length parameter is interpreted as a number of bytes, in hex, which we convert to the appropriate number
* of lines, because we always display whole lines. If the length is omitted/undefined, it defaults to 0x80 (128.)
* bytes, which normally translates to 8 lines.
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes])
*/
doDump(asArgs)
{
var m;
var sCmd = asArgs[0];
var sAddr = asArgs[1];
var sLen = asArgs[2];
var sBytes = asArgs[3];
if (sAddr == '?') {
var sDumpers = "";
for (m in Messages8080.CATEGORIES) {
if (this.afnDumpers[m]) {
if (sDumpers) sDumpers += ',';
sDumpers = sDumpers + m;
}
}
sDumpers += ",state,symbols";
this.println("dump memory commands:");
this.println("\tdb [a] [#] dump # bytes at address a");
this.println("\tdw [a] [#] dump # words at address a");
this.println("\tdd [a] [#] dump # dwords at address a");
this.println("\tdh [#] [#] dump # instructions from history");
if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers);
return;
}
if (sAddr == "state") {
var sState = this.cmp.powerOff(true);
if (sLen == "console") {
/*
* Console buffers are notoriously small, and even the following code, which breaks the
* data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful.
*
* var nPart = +sBytes;
* if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000);
*
* So, the best way to capture a large machine state is to use the new "Save Machine" link
* that downloads a machine's entire state. Alternatively, run your own local server and use
* server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML
* control to the computer.powerOff() and computer.saveServerState() functions.
*/
console.log(sState);
} else {
this.doClear();
if (sState) this.println(sState);
}
return;
}
if (sAddr == "symbols") {
this.dumpSymbols();
return;
}
if (sCmd == "d") {
for (m in Messages8080.CATEGORIES) {
if (asArgs[1] == m) {
var fnDumper = this.afnDumpers[m];
if (fnDumper) {
asArgs.shift();
asArgs.shift();
fnDumper(asArgs);
} else {
this.println("no dump registered for " + sAddr);
}
return;
}
}
if (!sAddr) sCmd = this.sCmdDumpPrev || "db";
} else {
this.sCmdDumpPrev = sCmd;
}
if (sCmd == "dh") {
this.dumpHistory(sAddr, sLen);
return;
}
var dbgAddr = this.parseAddr(sAddr);
if (!dbgAddr) return;
var len = 0; // 0 is not a default; it triggers the appropriate default below
if (sLen) {
if (sLen.charAt(0) == 'l') {
sLen = sLen.substr(1) || sBytes;
}
len = this.parseValue(sLen) >>> 0; // negative lengths not allowed
if (len > 0x10000) len = 0x10000; // prevent bad user (or variable) input from producing excessive output
}
var sDump = "";
var size = (sCmd == "dd"? 4 : (sCmd == "dw"? 2 : 1));
var cb = (size * len) || 128;
var cLines = ((cb + 15) >> 4) || 1;
while (cLines-- && cb > 0) {
var data = 0, iByte = 0, i;
var sData = "", sChars = "";
sAddr = this.toHexAddr(dbgAddr);
for (i = 16; i > 0 && cb > 0; i--) {
var b = this.getByte(dbgAddr, 1);
data |= (b << (iByte++ << 3));
if (iByte == size) {
sData += Str.toHex(data, size * 2);
sData += (size == 1? (i == 9? '-' : ' ') : " ");
data = iByte = 0;
}
sChars += (b >= 32 && b < 128? String.fromCharCode(b) : '.');
cb--;
}
if (sDump) sDump += '\n';
sDump += sAddr + " " + sData + ((i == 0)? (' ' + sChars) : "");
}
if (sDump) this.println(sDump);
this.dbgAddrNextData = dbgAddr;
}
/**
* doEdit(asArgs)
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs
*/
doEdit(asArgs)
{
var size = 1;
var mask = 0xff;
var fnGet = this.getByte;
var fnSet = this.setByte;
if (asArgs[0] == "ew") {
size = 2;
mask = 0xffff;
fnGet = this.getShort;
fnSet = this.setShort;
}
var cch = size << 1;
var sAddr = asArgs[1];
if (sAddr == null) {
this.println("edit memory commands:");
this.println("\teb [a] [...] edit bytes at address a");
this.println("\tew [a] [...] edit words at address a");
return;
}
var dbgAddr = this.parseAddr(sAddr);
if (!dbgAddr) return;
for (var i = 2; i < asArgs.length; i++) {
var vNew = this.parseExpression(asArgs[i]);
if (vNew === undefined) {
this.println("unrecognized value: " + asArgs[i]);
break;
}
if (vNew & ~mask) {
this.println("warning: " + Str.toHex(vNew) + " exceeds " + size + "-byte value");
}
var vOld = fnGet.call(this, dbgAddr);
this.println("changing " + this.toHexAddr(dbgAddr) + " from " + Str.toHex(vOld, cch, true) + " to " + Str.toHex(vNew, cch, true));
fnSet.call(this, dbgAddr, vNew, size);
}
}
/**
* doFreqs(sParm)
*
* @this {Debugger8080}
* @param {string|undefined} sParm
*/
doFreqs(sParm)
{
if (sParm == '?') {
this.println("frequency commands:");
this.println("\tclear\tclear all frequency counts");
return;
}
var i;
var cData = 0;
if (this.aaOpcodeCounts) {
if (sParm == "clear") {
for (i = 0; i < this.aaOpcodeCounts.length; i++)
this.aaOpcodeCounts[i] = [i, 0];
this.println("frequency data cleared");
cData++;
}
else if (sParm !== undefined) {
this.println("unknown frequency command: " + sParm);
cData++;
}
else {
var aaSortedOpcodeCounts = this.aaOpcodeCounts.slice();
aaSortedOpcodeCounts.sort(function(p, q) {
return q[1] - p[1];
});
var asOpcodes = this.style != Debugger8080.STYLE_8086? Debugger8080.INS_NAMES : Debugger8080.INS_NAMES_8086;
for (i = 0; i < aaSortedOpcodeCounts.length; i++) {
var bOpcode = aaSortedOpcodeCounts[i][0];
var cFreq = aaSortedOpcodeCounts[i][1];
if (cFreq) {
this.println((asOpcodes[this.aaOpDescs[bOpcode][0]] + " ").substr(0, 5) + " (" + Str.toHexByte(bOpcode) + "): " + cFreq + " times");
cData++;
}
}
}
}
if (!cData) {
this.println("no frequency data available");
}
}
/**
* doHalt(fQuiet)
*
* @this {Debugger8080}
* @param {boolean} [fQuiet]
*/
doHalt(fQuiet)
{
var sMsg;
if (this.flags.running) {
sMsg = "halting";
this.stopCPU();
} else {
if (this.isBusy(true)) return;
sMsg = "already halted";
}
if (!fQuiet) this.println(sMsg);
}
/**
* doIf(sCmd, fQuiet)
*
* NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate
* an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period
* (eg, "10.") if you really intend decimal.
*
* Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes
* "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true).
*
* @this {Debugger8080}
* @param {string} sCmd
* @param {boolean} [fQuiet]
* @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error)
*/
doIf(sCmd, fQuiet)
{
sCmd = Str.trim(sCmd);
if (!this.parseExpression(sCmd)) {
if (!fQuiet) this.println("false: " + sCmd);
return false;
}
if (!fQuiet) this.println("true: " + sCmd);
return true;
}
/**
* doInfo(asArgs)
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs
* @return {boolean} true only if the instruction info command ("n") is supported
*/
doInfo(asArgs)
{
if (DEBUG) {
this.println("msPerYield: " + this.cpu.msPerYield);
this.println("nCyclesPerYield: " + this.cpu.nCyclesPerYield);
return true;
}
return false;
}
/**
* doInput(sPort)
*
* Simulate a 1-byte port input operation.
*
* @this {Debugger8080}
* @param {string|undefined} sPort
*/
doInput(sPort)
{
if (!sPort || sPort == '?') {
this.println("input commands:");
this.println("\ti [p]\tread port [p]");
/*
* TODO: Regarding this warning, consider adding an "unchecked" version of
* bus.checkPortInputNotify(), since all Debugger memory accesses are unchecked, too.
*
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),
* but changing them all to be non-destructive would take time, and situations where you
* actually want to affect the hardware state are just as likely as not....
*/
this.println("warning: port accesses can affect hardware state");
return;
}
var port = this.parseValue(sPort);
if (port !== undefined) {
var bIn = this.bus.checkPortInputNotify(port, 1);
this.println(Str.toHexWord(port) + ": " + Str.toHexByte(bIn));
}
}
/**
* doInt(sLevel)
*
* @this {Debugger8080}
* @param {string} sLevel
* @return {boolean} true if success, false if error
*/
doInt(sLevel)
{
if (!this.cpu.getIF()) {
this.println("interrupts disabled (use rif=1 to enable)");
return false;
}
var nLevel = this.parseExpression(sLevel);
if (nLevel == null) return false;
this.println("requesting interrupt level " + nLevel);
this.cpu.requestINTR(nLevel);
return true;
}
/**
* doVar(sCmd)
*
* The command must be of the form "{variable} = [{expression}]", where expression may contain constants,
* operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if
* any, is deleted.
*
* Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}"
* prints the value of the specified variable.
*
* @this {Debugger8080}
* @param {string} sCmd
* @return {boolean} true if valid "var" assignment, false if not
*/
doVar(sCmd)
{
var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i);
if (a) {
if (!a[1]) {
if (!this.printVariable()) this.println("no variables");
return true; // it's not considered an error to print an empty list of variables
}
if (!a[2]) {
return this.printVariable(a[1]);
}
if (!a[3]) {
this.delVariable(a[1]);
return true; // it's not considered an error to delete a variable that didn't exist
}
var v = this.parseExpression(a[3]);
if (v !== undefined) {
this.setVariable(a[1], v);
return true;
}
return false;
}
this.println("invalid assignment:" + sCmd);
return false;
}
/**
* doList(sAddr, fPrint)
*
* @this {Debugger8080}
* @param {string} sAddr
* @param {boolean} [fPrint]
* @return {string|null}
*/
doList(sAddr, fPrint)
{
var sSymbol = null;
var dbgAddr = this.parseAddr(sAddr, true);
if (dbgAddr) {
var addr = this.getAddr(dbgAddr);
var aSymbol = this.findSymbol(dbgAddr, true);
if (aSymbol.length) {
var nDelta, sDelta, s;
if (aSymbol[0]) {
sDelta = "";
nDelta = dbgAddr.addr - aSymbol[1];
if (nDelta) sDelta = " + " + Str.toHexWord(nDelta);
s = aSymbol[0] + " (" + this.toHexOffset(aSymbol[1]) + ')' + sDelta;
if (fPrint) this.println(s);
sSymbol = s;
}
if (aSymbol.length > 4 && aSymbol[4]) {
sDelta = "";
nDelta = aSymbol[5] - dbgAddr.addr;
if (nDelta) sDelta = " - " + Str.toHexWord(nDelta);
s = aSymbol[4] + " (" + this.toHexOffset(aSymbol[5]) + ')' + sDelta;
if (fPrint) this.println(s);
if (!sSymbol) sSymbol = s;
}
} else {
if (fPrint) this.println("no symbols");
}
}
return sSymbol;
}
/**
* doMessages(asArgs)
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs
*/
doMessages(asArgs)
{
var m;
var fCriteria = null;
var sCategory = asArgs[1];
if (sCategory == '?') sCategory = undefined;
if (sCategory !== undefined) {
var bitsMessage = 0;
if (sCategory == "all") {
bitsMessage = (0xffffffff|0) & ~(Messages8080.HALT | Messages8080.KEYS | Messages8080.LOG);
sCategory = null;
} else if (sCategory == "on") {
fCriteria = true;
sCategory = null;
} else if (sCategory == "off") {
fCriteria = false;
sCategory = null;
} else {
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard".
*/
if (sCategory == "keys") sCategory = "key";
if (sCategory == "kbd") sCategory = "keyboard";
for (m in Messages8080.CATEGORIES) {
if (sCategory == m) {
bitsMessage = Messages8080.CATEGORIES[m];
fCriteria = !!(this.bitsMessage & bitsMessage);
break;
}
}
if (!bitsMessage) {
this.println("unknown message category: " + sCategory);
return;
}
}
if (bitsMessage) {
if (asArgs[2] == "on") {
this.bitsMessage |= bitsMessage;
fCriteria = true;
}
else if (asArgs[2] == "off") {
this.bitsMessage &= ~bitsMessage;
fCriteria = false;
if (bitsMessage == Messages8080.BUFFER) {
for (var i = 0; i < this.aMessageBuffer.length; i++) {
this.println(this.aMessageBuffer[i]);
}
this.aMessageBuffer = [];
}
}
}
}
/*
* Display those message categories that match the current criteria (on or off)
*/
var n = 0;
var sCategories = "";
for (m in Messages8080.CATEGORIES) {
if (!sCategory || sCategory == m) {
var bitMessage = Messages8080.CATEGORIES[m];
var fEnabled = !!(this.bitsMessage & bitMessage);
if (fCriteria !== null && fCriteria != fEnabled) continue;
if (sCategories) sCategories += ',';
if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys".
*/
if (m == "key") m = "keys";
sCategories += m;
}
}
if (sCategory === undefined) {
this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off");
}
this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none"));
this.historyInit(); // call this just in case Messages8080.INT was turned on
}
/**
* doOptions(asArgs)
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs
*/
doOptions(asArgs)
{
switch (asArgs[1]) {
case "8080":
this.style = Debugger8080.STYLE_8080;
break;
case "8086":
this.style = Debugger8080.STYLE_8086;
break;
case "cs":
var nCycles;
if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion
switch (asArgs[2]) {
case "int":
this.cpu.nCyclesChecksumInterval = nCycles;
break;
case "start":
this.cpu.nCyclesChecksumStart = nCycles;
break;
case "stop":
this.cpu.nCyclesChecksumStop = nCycles;
break;
default:
this.println("unknown cs option");
return;
}
if (nCycles !== undefined) {
this.cpu.resetChecksum();
}
this.println("checksums " + (this.cpu.flags.checksum? "enabled" : "disabled"));
return;
case "sp":
if (asArgs[2] !== undefined) {
if (!this.cpu.setSpeed(+asArgs[2])) {
this.println("warning: using 1x multiplier, previous target not reached");
}
}
this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)");
return;
case "?":
this.println("debugger options:");
this.println("\t8080\t\tselect 8080-style mnemonics");
this.println("\t8086\t\tselect 8086-style mnemonics");
this.println("\tcs int #\tset checksum cycle interval to #");
this.println("\tcs start #\tset checksum cycle start count to #");
this.println("\tcs stop #\tset checksum cycle stop count to #");
this.println("\tsp #\t\tset speed multiplier to #");
break;
default:
if (asArgs[1]) {
this.println("unknown option: " + asArgs[1]);
return;
}
break;
}
this.println(this.style + "-style mnemonics enabled");
}
/**
* doOutput(sPort, sByte)
*
* Simulate a 1-byte port output operation.
*
* @this {Debugger8080}
* @param {string|undefined} sPort
* @param {string|undefined} sByte (string representation of 1 byte)
*/
doOutput(sPort, sByte)
{
if (!sPort || sPort == '?') {
this.println("output commands:");
this.println("\to [p] [b]\twrite byte [b] to port [p]");
/*
* TODO: Regarding this warning, consider adding an "unchecked" version of
* bus.checkPortOutputNotify(), since all Debugger memory accesses are unchecked, too.
*
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),
* but changing them all to be non-destructive would take time, and situations where you
* actually want to affect the hardware state are just as likely as not....
*/
this.println("warning: port accesses can affect hardware state");
return;
}
var port = this.parseValue(sPort, "port #");
var bOut = this.parseValue(sByte);
if (port !== undefined && bOut !== undefined) {
this.bus.checkPortOutputNotify(port, 1, bOut);
this.println(Str.toHexWord(port) + ": " + Str.toHexByte(bOut));
}
}
/**
* doRegisters(asArgs, fInstruction)
*
* @this {Debugger8080}
* @param {Array.<string>} [asArgs]
* @param {boolean} [fInstruction] (true to include the current instruction; default is true)
*/
doRegisters(asArgs, fInstruction)
{
if (asArgs && asArgs[1] == '?') {
this.println("register commands:");
this.println("\tr\tdump registers");
this.println("\trx [#]\tset flag or register x to [#]");
return;
}
var cpu = this.cpu;
if (fInstruction == null) fInstruction = true;
if (asArgs != null && asArgs.length > 1) {
var sReg = asArgs[1];
var sValue = null;
var i = sReg.indexOf('=');
if (i > 0) {
sValue = sReg.substr(i + 1);
sReg = sReg.substr(0, i);
}
else if (asArgs.length > 2) {
sValue = asArgs[2];
}
else {
this.println("missing value for " + asArgs[1]);
return;
}
var w = this.parseExpression(sValue);
if (w === undefined) return;
var sRegMatch = sReg.toUpperCase();
switch (sRegMatch) {
case "A":
cpu.regA = w & 0xff;
break;
case "B":
cpu.regB = w & 0xff;
break;
case "BC":
cpu.regB = ((w >> 8) & 0xff);
/* falls through */
case "C":
cpu.regC = w & 0xff;
break;
case "D":
cpu.regD = w & 0xff;
break;
case "DE":
cpu.regD = ((w >> 8) & 0xff);
/* falls through */
case "E":
cpu.regE = w & 0xff;
break;
case "H":
cpu.regH = w & 0xff;
break;
case "HL":
cpu.regH = ((w >> 8) & 0xff);
/* falls through */
case "L":
cpu.regL = w & 0xff;
break;
case "SP":
cpu.setSP(w);
break;
case "PC":
cpu.setPC(w);
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
break;
case "PS":
cpu.setPS(w);
break;
case "PSW":
cpu.setPSW(w);
break;
case "CF":
if (w) cpu.setCF(); else cpu.clearCF();
break;
case "PF":
if (w) cpu.setPF(); else cpu.clearPF();
break;
case "AF":
if (w) cpu.setAF(); else cpu.clearAF();
break;
case "ZF":
if (w) cpu.setZF(); else cpu.clearZF();
break;
case "SF":
if (w) cpu.setSF(); else cpu.clearSF();
break;
case "IF":
if (w) cpu.setIF(); else cpu.clearIF();
break;
default:
this.println("unknown register: " + sReg);
return;
}
cpu.updateCPU();
this.println("updated registers:");
}
this.println(this.getRegDump());
if (fInstruction) {
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
this.doUnassemble(this.toHexAddr(this.dbgAddrNextCode));
}
}
/**
* doRun(sCmd, sAddr, sOptions, fQuiet)
*
* @this {Debugger8080}
* @param {string} sCmd
* @param {string|undefined} [sAddr]
* @param {string} [sOptions] (the rest of the breakpoint command-line)
* @param {boolean} [fQuiet]
*/
doRun(sCmd, sAddr, sOptions, fQuiet)
{
if (sCmd == "gt") {
this.fIgnoreNextCheckFault = true;
}
if (sAddr !== undefined) {
var dbgAddr = this.parseAddr(sAddr, true);
if (!dbgAddr) return;
this.parseAddrOptions(dbgAddr, sOptions);
this.setTempBreakpoint(dbgAddr);
}
if (!this.runCPU(true)) {
if (!fQuiet) this.println("cpu busy or unavailable, run command ignored");
}
}
/**
* doPrint(sCmd)
*
* NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that
* you can take advantage of all the special replacement options used for software interrupt logging.
*
* @this {Debugger8080}
* @param {string} sCmd
*/
doPrint(sCmd)
{
sCmd = Str.trim(sCmd);
var a = sCmd.match(/^(['"])(.*?)\1$/);
if (!a) {
this.parseExpression(sCmd, false);
} else {
this.println(this.replaceRegs(a[2]));
}
}
/**
* doStep(sCmd)
*
* @this {Debugger8080}
* @param {string} [sCmd] "p" or "pr"
*/
doStep(sCmd)
{
var fCallStep = true;
var fRegs = (sCmd == "pr"? 1 : 0);
/*
* Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants
* a subsequent register dump ("pr") or not ("p").
*/
var nStep = 1 + fRegs;
if (!this.nStep) {
var dbgAddr = this.newAddr(this.cpu.getPC());
var bOpcode = this.getByte(dbgAddr);
switch (bOpcode) {
case CPUDef8080.OPCODE.CALL:
if (fCallStep) {
this.nStep = nStep;
this.incAddr(dbgAddr, 3);
}
break;
default:
break;
}
if (this.nStep) {
this.setTempBreakpoint(dbgAddr);
if (!this.runCPU()) {
if (this.cmp) this.cmp.updateFocus();
this.nStep = 0;
}
/*
* A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(),
* which will clear nStep, so there's your assurance that nStep will be reset. Now we may have
* stopped for reasons unrelated to the temporary breakpoint, but that's OK.
*/
} else {
this.doTrace(fRegs? "tr" : "t");
}
} else {
this.println("step in progress");
}
}
/**
* getCall(dbgAddr)
*
* Given a possible return address (typically from the stack), look for a matching CALL (or INT) that
* immediately precedes that address.
*
* @this {Debugger8080}
* @param {DbgAddr8080} dbgAddr
* @return {string|null} CALL instruction at or near dbgAddr, or null if none
*/
getCall(dbgAddr)
{
var sCall = null;
var addr = dbgAddr.addr;
var addrOrig = addr;
for (var n = 1; n <= 6 && !!addr; n++) {
if (n > 2) {
dbgAddr.addr = addr;
var s = this.getInstruction(dbgAddr);
if (s.indexOf("CALL") >= 0) {
/*
* Verify that the length of this CALL (or INT), when added to the address of the CALL (or INT),
* matches the original return address. We do this by getting the string index of the opcode bytes,
* subtracting that from the string index of the next space, and dividing that difference by two,
* to yield the length of the CALL (or INT) instruction, in bytes.
*/
var i = s.indexOf(' ');
var j = s.indexOf(' ', i+1);
if (addr + (j - i - 1)/2 == addrOrig) {
sCall = s;
break;
}
}
}
addr--;
}
dbgAddr.addr = addrOrig;
return sCall;
}
/**
* doStackTrace(sCmd, sAddr)
*
* Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info.
*
* @this {Debugger8080}
* @param {string} [sCmd]
* @param {string} [sAddr] (not used yet)
*/
doStackTrace(sCmd, sAddr)
{
if (sAddr == '?') {
this.println("stack trace commands:");
this.println("\tk\tshow frame addresses");
this.println("\tks\tshow symbol information");
return;
}
var nFrames = 10, cFrames = 0;
var dbgAddrCall = this.newAddr();
var dbgAddrStack = this.newAddr(this.cpu.getSP());
this.println("stack trace for " + this.toHexAddr(dbgAddrStack));
while (cFrames < nFrames) {
var sCall = null, sCallPrev = null, cTests = 256;
while ((dbgAddrStack.addr >>> 0) < 0x10000) {
dbgAddrCall.addr = this.getWord(dbgAddrStack, true);
/*
* Because we're using the auto-increment feature of getWord(), and because that will automatically
* wrap the offset around the end of the segment, we must also check the addr property to detect the wrap.
*/
if (dbgAddrStack.addr == null || !cTests--) break;
sCall = this.getCall(dbgAddrCall);
if (sCall) break;
}
/*
* The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always)
* indicative of a false positive, in which case the previous call is probably bogus as well, but
* at least we won't duplicate that mistake. Of course, there are always exceptions, recursion
* being one of them, but it's rare that we're debugging recursive code.
*/
if (!sCall || sCall == sCallPrev) break;
var sSymbol = null;
if (sCmd == "ks") {
var a = sCall.match(/[0-9A-F]+$/);
if (a) sSymbol = this.doList(a[0]);
}
sCall = Str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toHexAddr(dbgAddrStack)); // + " return=" + this.toHexAddr(dbgAddrCall));
this.println(sCall);
sCallPrev = sCall;
cFrames++;
}
if (!cFrames) this.println("no return addresses found");
}
/**
* doTrace(sCmd, sCount)
*
* The "t" and "tr" commands interpret the count as a number of instructions, and since
* we call the Debugger's stepCPU() for each iteration, a single instruction includes
* any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only
* difference between "t" and "tr": the former displays only the next instruction, while
* the latter also displays the (updated) registers.
*
* The "tc" command interprets the count as a number of cycles rather than instructions,
* allowing you to quickly execute large chunks of instructions with a single command; it
* doesn't display anything until the the chunk has finished.
*
* However, generally a more useful command is "bn", which allows you to break after some
* number of instructions have been executed (as opposed to some number of cycles).
*
* @this {Debugger8080}
* @param {string} [sCmd] ("t", "tc", or "tr")
* @param {string} [sCount] # of instructions to step
*/
doTrace(sCmd, sCount)
{
var dbg = this;
var fRegs = (sCmd != "t");
var nCount = this.parseValue(sCount, null, true) || 1;
var nCycles = (nCount == 1? 0 : 1);
if (sCmd == "tc") {
nCycles = nCount;
nCount = 1;
}
Web.onCountRepeat(
nCount,
function onCountStep() {
return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false);
},
function onCountStepComplete() {
/*
* We explicitly called stepCPU() with fUpdateCPU === false, because repeatedly
* calling updateCPU() can be very slow, especially when fDisplayLiveRegs is true,
* so once the repeat count has been exhausted, we must perform a final updateCPU().
*/
dbg.cpu.updateCPU();
dbg.setBusy(false);
}
);
}
/**
* doUnassemble(sAddr, sAddrEnd, n)
*
* @this {Debugger8080}
* @param {string} [sAddr]
* @param {string} [sAddrEnd]
* @param {number} [n]
*/
doUnassemble(sAddr, sAddrEnd, n)
{
var dbgAddr = this.parseAddr(sAddr, true);
if (!dbgAddr) return;
if (n === undefined) n = 1;
var cb = 0x100;
if (sAddrEnd !== undefined) {
var dbgAddrEnd = this.parseAddr(sAddrEnd, true);
if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return;
cb = dbgAddrEnd.addr - dbgAddr.addr;
if (!DEBUG && cb > 0x100) {
/*
* Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to
* prevent the user from wedging the browser by dumping too many lines, but also a recognition
* that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway.
*/
this.println("range too large");
return;
}
n = -1;
}
var cLines = 0;
var sInstruction;
while (cb > 0 && n--) {
var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null;
var sComment = (nSequence != null? "cycles" : null);
var aSymbol = this.findSymbol(dbgAddr);
var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it
if (aSymbol[0] && n) {
if (!cLines && n || aSymbol[0].indexOf('+') < 0) {
var sLabel = aSymbol[0] + ':';
if (aSymbol[2]) sLabel += ' ' + aSymbol[2];
this.println(sLabel);
}
}
if (aSymbol[3]) {
sComment = aSymbol[3];
nSequence = null;
}
sInstruction = this.getInstruction(dbgAddr, sComment, nSequence);
this.println(sInstruction);
this.dbgAddrNextCode = dbgAddr;
cb -= dbgAddr.addr - addr;
cLines++;
}
}
/**
* parseCommand(sCmd, fSave, chSep)
*
* @this {Debugger8080}
* @param {string|undefined} sCmd
* @param {boolean} [fSave] is true to save the command, false if not
* @param {string} [chSep] is the command separator character (default is ';')
* @return {Array.<string>}
*/
parseCommand(sCmd, fSave, chSep)
{
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
sCmd = "end";
} else {
sCmd = this.aPrevCmds[this.iPrevCmd+1];
}
} else {
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
this.iPrevCmd = 0;
}
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
this.aPrevCmds.splice(0, 0, sCmd);
this.iPrevCmd = 0;
}
this.iPrevCmd--;
}
}
var a = [];
if (sCmd) {
/*
* With the introduction of breakpoint commands (ie, quoted command sequences
* associated with a breakpoint), we can no longer perform simplistic splitting.
*
* a = sCmd.split(chSep || ';');
* for (var i = 0; i < a.length; i++) a[i] = str.trim(a[i]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
* DOUBLE double-quotes with single quotes.
*/
sCmd = sCmd.toLowerCase().replace(/""/g, "'");
var iPrev = 0;
var chQuote = null;
chSep = chSep || ';';
/*
* NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking
* advantage of the fact that charAt() with an invalid index returns an empty string,
* allowing us to use the same substring() call to capture the final portion of sCmd.
*
* In a sense, it allows us to pretend that the string ends with a zero terminator.
*/
for (var i = 0; i <= sCmd.length; i++) {
var ch = sCmd.charAt(i);
if (ch == '"' || ch == "'") {
if (!chQuote) {
chQuote = ch;
} else if (ch == chQuote) {
chQuote = null;
}
}
else if (ch == chSep && !chQuote || !ch) {
/*
* Recall that substring() accepts starting (inclusive) and ending (exclusive)
* indexes, whereas substr() accepts a starting index and a length. We need the former.
*/
a.push(Str.trim(sCmd.substring(iPrev, i)));
iPrev = i + 1;
}
}
}
return a;
}
/**
* shiftArgs(asArgs)
*
* Used with any command (eg, "r") that allows but doesn't require whitespace between command and first argument.
*
* @this {Debugger8080}
* @param {Array.<string>} asArgs
* @return {Array.<string>}
*/
shiftArgs(asArgs)
{
if (asArgs && asArgs.length) {
var s0 = asArgs[0];
var ch0 = s0.charAt(0);
for (var i = 1; i < s0.length; i++) {
var ch = s0.charAt(i);
if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') {
asArgs[0] = s0.substr(i);
asArgs.unshift(s0.substr(0, i));
break;
}
}
}
return asArgs;
}
/**
* doCommand(sCmd, fQuiet)
*
* @this {Debugger8080}
* @param {string} sCmd
* @param {boolean} [fQuiet]
* @return {boolean} true if command processed, false if unrecognized
*/
doCommand(sCmd, fQuiet)
{
var result = true;
try {
if (!sCmd.length || sCmd == "end") {
if (this.fAssemble) {
this.println("ended assemble at " + this.toHexAddr(this.dbgAddrAssemble));
this.dbgAddrNextCode = this.dbgAddrAssemble;
this.fAssemble = false;
}
sCmd = "";
}
else if (!fQuiet) {
var sPrompt = ">> ";
this.println(sPrompt + sCmd);
}
var ch = sCmd.charAt(0);
if (ch == '"' || ch == "'") return true;
/*
* Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat.
*/
this.sMessagePrev = null;
/*
* I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally.
*/
if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) {
if (this.fAssemble) {
sCmd = "a " + this.toHexAddr(this.dbgAddrAssemble) + ' ' + sCmd;
}
var asArgs = this.shiftArgs(sCmd.replace(/ +/g, ' ').split(' '));
switch (asArgs[0].charAt(0)) {
case 'a':
this.doAssemble(asArgs);
break;
case 'b':
this.doBreak(asArgs[0], asArgs[1], sCmd);
break;
case 'c':
this.doClear(asArgs[0]);
break;
case 'd':
if (!COMPILED && sCmd == "debug") {
window.DEBUG = true;
this.println("DEBUG checks on");
break;
}
this.doDump(asArgs);
break;
case 'e':
if (asArgs[0] == "else") break;
this.doEdit(asArgs);
break;
case 'f':
this.doFreqs(asArgs[1]);
break;
case 'g':
this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet);
break;
case 'h':
this.doHalt(fQuiet);
break;
case 'i':
if (asArgs[0] == "if") {
if (!this.doIf(sCmd.substr(2), fQuiet)) {
result = false;
}
break;
}
if (asArgs[0] == "int") {
if (!this.doInt(asArgs[1])) {
result = false;
}
break;
}
this.doInput(asArgs[1]);
break;
case 'k':
this.doStackTrace(asArgs[0], asArgs[1]);
break;
case 'l':
if (asArgs[0] == "ln") {
this.doList(asArgs[1], true);
break;
}
break;
case 'm':
this.doMessages(asArgs);
break;
case 'o':
this.doOutput(asArgs[1], asArgs[2]);
break;
case 'p':
if (asArgs[0] == "print") {
this.doPrint(sCmd.substr(5));
break;
}
this.doStep(asArgs[0]);
break;
case 'r':
if (sCmd == "reset") {
if (this.cmp) this.cmp.reset();
break;
}
this.doRegisters(asArgs);
break;
case 's':
this.doOptions(asArgs);
break;
case 't':
this.doTrace(asArgs[0], asArgs[1]);
break;
case 'u':
this.doUnassemble(asArgs[1], asArgs[2], 8);
break;
case 'v':
if (asArgs[0] == "var") {
if (!this.doVar(sCmd.substr(3))) {
result = false;
}
break;
}
this.println((PC8080.APPNAME || "PC8080") + " version " + (XMLVERSION || PC8080.APPVERSION) + " (" + this.cpu.model + (PC8080.COMPILED? ",RELEASE" : (PC8080.DEBUG? ",DEBUG" : ",NODEBUG")) + (PC8080.TYPEDARRAYS? ",TYPEDARRAYS" : (PC8080.BYTEARRAYS? ",BYTEARRAYS" : ",LONGARRAYS")) + ')');
this.println(Web.getUserAgent());
break;
case '?':
if (asArgs[1]) {
this.doPrint(sCmd.substr(1));
break;
}
this.doHelp();
break;
case 'n':
if (!COMPILED && sCmd == "nodebug") {
window.DEBUG = false;
this.println("DEBUG checks off");
break;
}
if (this.doInfo(asArgs)) break;
/* falls through */
default:
this.println("unknown command: " + sCmd);
result = false;
break;
}
}
} catch(e) {
this.println("debugger error: " + (e.stack || e.message));
result = false;
}
return result;
}
/**
* doCommands(sCmds, fSave)
*
* @this {Debugger8080}
* @param {string} sCmds
* @param {boolean} [fSave]
* @return {boolean} true if all commands processed, false if not
*/
doCommands(sCmds, fSave)
{
var a = this.parseCommand(sCmds, fSave);
for (var s in a) {
if (!this.doCommand(a[+s])) return false;
}
return true;
}
/**
* Debugger8080.init()
*
* This function operates on every HTML element of class "debugger", extracting the
* JSON-encoded parameters for the Debugger constructor from the element's "data-value"
* attribute, invoking the constructor to create a Debugger component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeDbg = Component.getElementsByClass(document, PC8080.APPCLASS, "debugger");
for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) {
var eDbg = aeDbg[iDbg];
var parmsDbg = Component.getComponentParms(eDbg);
var dbg = new Debugger8080(parmsDbg);
Component.bindComponentControls(dbg, eDbg, PC8080.APPCLASS);
}
}
}
if (DEBUGGER) {
/*
* NOTE: Every Debugger8080 property from here to the first prototype function definition (initBus()) is
* considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but
* 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_SIZE
* at one point, and if all the definitions had been preceded by an "@const", that mistake would have been
* caught at compile-time.
*/
Debugger8080.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",
'int [#]': "request interrupt",
'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"
};
Debugger8080.STYLE_8080 = 8080;
Debugger8080.STYLE_8086 = 8086;
/*
* CPU instruction ordinals
*/
Debugger8080.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.
*/
Debugger8080.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"
];
Debugger8080.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"
];
Debugger8080.REG_B = 0x00;
Debugger8080.REG_C = 0x01;
Debugger8080.REG_D = 0x02;
Debugger8080.REG_E = 0x03;
Debugger8080.REG_H = 0x04;
Debugger8080.REG_L = 0x05;
Debugger8080.REG_M = 0x06;
Debugger8080.REG_A = 0x07;
Debugger8080.REG_BC = 0x08;
Debugger8080.REG_DE = 0x09;
Debugger8080.REG_HL = 0x0A;
Debugger8080.REG_SP = 0x0B;
Debugger8080.REG_PC = 0x0C;
Debugger8080.REG_PS = 0x0D;
Debugger8080.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.
*/
Debugger8080.REGS = [
"B", "C", "D", "E", "H", "L", "M", "A", "BC", "DE", "HL", "SP", "PC", "PS", "PSW"
];
/*
* Operand type descriptor masks and definitions
*/
Debugger8080.TYPE_SIZE = 0x000F; // size field
Debugger8080.TYPE_MODE = 0x00F0; // mode field
Debugger8080.TYPE_IREG = 0x0F00; // implied register field
Debugger8080.TYPE_OTHER = 0xF000; // "other" field
/*
* TYPE_SIZE values
*/
Debugger8080.TYPE_NONE = 0x0000; // (all other TYPE fields ignored)
Debugger8080.TYPE_BYTE = 0x0001; // byte, regardless of operand size
Debugger8080.TYPE_SBYTE = 0x0002; // byte sign-extended to word
Debugger8080.TYPE_WORD = 0x0003; // word (16-bit value)
/*
* TYPE_MODE values
*/
Debugger8080.TYPE_REG = 0x0010; // register
Debugger8080.TYPE_IMM = 0x0020; // immediate data
Debugger8080.TYPE_ADDR = 0x0033; // immediate (word) address
Debugger8080.TYPE_MEM = 0x0040; // memory reference
Debugger8080.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.
*/
Debugger8080.TYPE_A = (Debugger8080.REG_A << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_B = (Debugger8080.REG_B << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_C = (Debugger8080.REG_C << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_D = (Debugger8080.REG_D << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_E = (Debugger8080.REG_E << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_H = (Debugger8080.REG_H << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_L = (Debugger8080.REG_L << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE);
Debugger8080.TYPE_M = (Debugger8080.REG_M << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_BYTE | Debugger8080.TYPE_MEM);
Debugger8080.TYPE_BC = (Debugger8080.REG_BC << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
Debugger8080.TYPE_DE = (Debugger8080.REG_DE << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
Debugger8080.TYPE_HL = (Debugger8080.REG_HL << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
Debugger8080.TYPE_SP = (Debugger8080.REG_SP << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
Debugger8080.TYPE_PC = (Debugger8080.REG_PC << 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
Debugger8080.TYPE_PSW = (Debugger8080.REG_PSW<< 8 | Debugger8080.TYPE_REG | Debugger8080.TYPE_WORD);
/*
* TYPE_OTHER bit definitions
*/
Debugger8080.TYPE_IN = 0x1000; // operand is input
Debugger8080.TYPE_OUT = 0x2000; // operand is output
Debugger8080.TYPE_BOTH = (Debugger8080.TYPE_IN | Debugger8080.TYPE_OUT);
Debugger8080.TYPE_OPT = 0x4000; // optional operand (ie, normally omitted in 8080 assembly language)
Debugger8080.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.
*/
Debugger8080.aaOpDescs = [
/* 0x00 */ [Debugger8080.INS.NOP],
/* 0x01 */ [Debugger8080.INS.LXI, Debugger8080.TYPE_BC, Debugger8080.TYPE_IMM],
/* 0x02 */ [Debugger8080.INS.STAX, Debugger8080.TYPE_BC | Debugger8080.TYPE_MEM, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT],
/* 0x03 */ [Debugger8080.INS.INX, Debugger8080.TYPE_BC],
/* 0x04 */ [Debugger8080.INS.INR, Debugger8080.TYPE_B],
/* 0x05 */ [Debugger8080.INS.DCR, Debugger8080.TYPE_B],
/* 0x06 */ [Debugger8080.INS.MVI, Debugger8080.TYPE_B, Debugger8080.TYPE_IMM],
/* 0x07 */ [Debugger8080.INS.RLC],
/* 0x08 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x09 */ [Debugger8080.INS.DAD, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_BC],
/* 0x0A */ [Debugger8080.INS.LDAX, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_BC | Debugger8080.TYPE_MEM],
/* 0x0B */ [Debugger8080.INS.DCX, Debugger8080.TYPE_BC],
/* 0x0C */ [Debugger8080.INS.INR, Debugger8080.TYPE_C],
/* 0x0D */ [Debugger8080.INS.DCR, Debugger8080.TYPE_C],
/* 0x0E */ [Debugger8080.INS.MVI, Debugger8080.TYPE_C, Debugger8080.TYPE_IMM],
/* 0x0F */ [Debugger8080.INS.RRC],
/* 0x10 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x11 */ [Debugger8080.INS.LXI, Debugger8080.TYPE_DE, Debugger8080.TYPE_IMM],
/* 0x12 */ [Debugger8080.INS.STAX, Debugger8080.TYPE_DE | Debugger8080.TYPE_MEM, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT],
/* 0x13 */ [Debugger8080.INS.INX, Debugger8080.TYPE_DE],
/* 0x14 */ [Debugger8080.INS.INR, Debugger8080.TYPE_D],
/* 0x15 */ [Debugger8080.INS.DCR, Debugger8080.TYPE_D],
/* 0x16 */ [Debugger8080.INS.MVI, Debugger8080.TYPE_D, Debugger8080.TYPE_IMM],
/* 0x17 */ [Debugger8080.INS.RAL],
/* 0x18 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x19 */ [Debugger8080.INS.DAD, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_DE],
/* 0x1A */ [Debugger8080.INS.LDAX, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_DE | Debugger8080.TYPE_MEM],
/* 0x1B */ [Debugger8080.INS.DCX, Debugger8080.TYPE_DE],
/* 0x1C */ [Debugger8080.INS.INR, Debugger8080.TYPE_E],
/* 0x1D */ [Debugger8080.INS.DCR, Debugger8080.TYPE_E],
/* 0x1E */ [Debugger8080.INS.MVI, Debugger8080.TYPE_E, Debugger8080.TYPE_IMM],
/* 0x1F */ [Debugger8080.INS.RAR],
/* 0x20 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x21 */ [Debugger8080.INS.LXI, Debugger8080.TYPE_HL, Debugger8080.TYPE_IMM],
/* 0x22 */ [Debugger8080.INS.SHLD, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_MEM, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT],
/* 0x23 */ [Debugger8080.INS.INX, Debugger8080.TYPE_HL],
/* 0x24 */ [Debugger8080.INS.INR, Debugger8080.TYPE_H],
/* 0x25 */ [Debugger8080.INS.DCR, Debugger8080.TYPE_H],
/* 0x26 */ [Debugger8080.INS.MVI, Debugger8080.TYPE_H, Debugger8080.TYPE_IMM],
/* 0x27 */ [Debugger8080.INS.DAA],
/* 0x28 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x29 */ [Debugger8080.INS.DAD, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_HL],
/* 0x2A */ [Debugger8080.INS.LHLD, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_MEM],
/* 0x2B */ [Debugger8080.INS.DCX, Debugger8080.TYPE_HL],
/* 0x2C */ [Debugger8080.INS.INR, Debugger8080.TYPE_L],
/* 0x2D */ [Debugger8080.INS.DCR, Debugger8080.TYPE_L],
/* 0x2E */ [Debugger8080.INS.MVI, Debugger8080.TYPE_L, Debugger8080.TYPE_IMM],
/* 0x2F */ [Debugger8080.INS.CMA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT],
/* 0x30 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x31 */ [Debugger8080.INS.LXI, Debugger8080.TYPE_SP, Debugger8080.TYPE_IMM],
/* 0x32 */ [Debugger8080.INS.STA, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_MEM, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT],
/* 0x33 */ [Debugger8080.INS.INX, Debugger8080.TYPE_SP],
/* 0x34 */ [Debugger8080.INS.INR, Debugger8080.TYPE_M],
/* 0x35 */ [Debugger8080.INS.DCR, Debugger8080.TYPE_M],
/* 0x36 */ [Debugger8080.INS.MVI, Debugger8080.TYPE_M, Debugger8080.TYPE_IMM],
/* 0x37 */ [Debugger8080.INS.STC],
/* 0x38 */ [Debugger8080.INS.NOP, Debugger8080.TYPE_UNDOC],
/* 0x39 */ [Debugger8080.INS.DAD, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_SP],
/* 0x3A */ [Debugger8080.INS.LDA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_MEM],
/* 0x3B */ [Debugger8080.INS.DCX, Debugger8080.TYPE_SP],
/* 0x3C */ [Debugger8080.INS.INR, Debugger8080.TYPE_A],
/* 0x3D */ [Debugger8080.INS.DCR, Debugger8080.TYPE_A],
/* 0x3E */ [Debugger8080.INS.MVI, Debugger8080.TYPE_A, Debugger8080.TYPE_IMM],
/* 0x3F */ [Debugger8080.INS.CMC],
/* 0x40 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_B],
/* 0x41 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_C],
/* 0x42 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_D],
/* 0x43 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_E],
/* 0x44 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_H],
/* 0x45 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_L],
/* 0x46 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_M],
/* 0x47 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_B, Debugger8080.TYPE_A],
/* 0x48 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_B],
/* 0x49 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_C],
/* 0x4A */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_D],
/* 0x4B */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_E],
/* 0x4C */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_H],
/* 0x4D */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_L],
/* 0x4E */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_M],
/* 0x4F */ [Debugger8080.INS.MOV, Debugger8080.TYPE_C, Debugger8080.TYPE_A],
/* 0x50 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_B],
/* 0x51 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_C],
/* 0x52 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_D],
/* 0x53 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_E],
/* 0x54 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_H],
/* 0x55 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_L],
/* 0x56 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_M],
/* 0x57 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_D, Debugger8080.TYPE_A],
/* 0x58 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_B],
/* 0x59 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_C],
/* 0x5A */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_D],
/* 0x5B */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_E],
/* 0x5C */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_H],
/* 0x5D */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_L],
/* 0x5E */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_M],
/* 0x5F */ [Debugger8080.INS.MOV, Debugger8080.TYPE_E, Debugger8080.TYPE_A],
/* 0x60 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_B],
/* 0x61 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_C],
/* 0x62 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_D],
/* 0x63 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_E],
/* 0x64 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_H],
/* 0x65 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_L],
/* 0x66 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_M],
/* 0x67 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_H, Debugger8080.TYPE_A],
/* 0x68 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_B],
/* 0x69 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_C],
/* 0x6A */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_D],
/* 0x6B */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_E],
/* 0x6C */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_H],
/* 0x6D */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_L],
/* 0x6E */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_M],
/* 0x6F */ [Debugger8080.INS.MOV, Debugger8080.TYPE_L, Debugger8080.TYPE_A],
/* 0x70 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_B],
/* 0x71 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_C],
/* 0x72 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_D],
/* 0x73 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_E],
/* 0x74 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_H],
/* 0x75 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_L],
/* 0x76 */ [Debugger8080.INS.HLT],
/* 0x77 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_M, Debugger8080.TYPE_A],
/* 0x78 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_B],
/* 0x79 */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_C],
/* 0x7A */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_D],
/* 0x7B */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_E],
/* 0x7C */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_H],
/* 0x7D */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_L],
/* 0x7E */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_M],
/* 0x7F */ [Debugger8080.INS.MOV, Debugger8080.TYPE_A, Debugger8080.TYPE_A],
/* 0x80 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0x81 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0x82 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0x83 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0x84 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0x85 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0x86 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0x87 */ [Debugger8080.INS.ADD, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0x88 */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0x89 */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0x8A */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0x8B */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0x8C */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0x8D */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0x8E */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0x8F */ [Debugger8080.INS.ADC, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0x90 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0x91 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0x92 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0x93 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0x94 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0x95 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0x96 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0x97 */ [Debugger8080.INS.SUB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0x98 */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0x99 */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0x9A */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0x9B */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0x9C */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0x9D */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0x9E */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0x9F */ [Debugger8080.INS.SBB, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0xA0 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0xA1 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0xA2 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0xA3 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0xA4 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0xA5 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0xA6 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0xA7 */ [Debugger8080.INS.ANA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0xA8 */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0xA9 */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0xAA */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0xAB */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0xAC */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0xAD */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0xAE */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0xAF */ [Debugger8080.INS.XRA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0xB0 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0xB1 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0xB2 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0xB3 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0xB4 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0xB5 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0xB6 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0xB7 */ [Debugger8080.INS.ORA, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0xB8 */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_B],
/* 0xB9 */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_C],
/* 0xBA */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_D],
/* 0xBB */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_E],
/* 0xBC */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_H],
/* 0xBD */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_L],
/* 0xBE */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_M],
/* 0xBF */ [Debugger8080.INS.CMP, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_A],
/* 0xC0 */ [Debugger8080.INS.RNZ],
/* 0xC1 */ [Debugger8080.INS.POP, Debugger8080.TYPE_BC],
/* 0xC2 */ [Debugger8080.INS.JNZ, Debugger8080.TYPE_ADDR],
/* 0xC3 */ [Debugger8080.INS.JMP, Debugger8080.TYPE_ADDR],
/* 0xC4 */ [Debugger8080.INS.CNZ, Debugger8080.TYPE_ADDR],
/* 0xC5 */ [Debugger8080.INS.PUSH, Debugger8080.TYPE_BC],
/* 0xC6 */ [Debugger8080.INS.ADI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xC7 */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xC8 */ [Debugger8080.INS.RZ],
/* 0xC9 */ [Debugger8080.INS.RET],
/* 0xCA */ [Debugger8080.INS.JZ, Debugger8080.TYPE_ADDR],
/* 0xCB */ [Debugger8080.INS.JMP, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_UNDOC],
/* 0xCC */ [Debugger8080.INS.CZ, Debugger8080.TYPE_ADDR],
/* 0xCD */ [Debugger8080.INS.CALL, Debugger8080.TYPE_ADDR],
/* 0xCE */ [Debugger8080.INS.ACI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xCF */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xD0 */ [Debugger8080.INS.RNC],
/* 0xD1 */ [Debugger8080.INS.POP, Debugger8080.TYPE_DE],
/* 0xD2 */ [Debugger8080.INS.JNC, Debugger8080.TYPE_ADDR],
/* 0xD3 */ [Debugger8080.INS.OUT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE,Debugger8080.TYPE_A | Debugger8080.TYPE_OPT],
/* 0xD4 */ [Debugger8080.INS.CNC, Debugger8080.TYPE_ADDR],
/* 0xD5 */ [Debugger8080.INS.PUSH, Debugger8080.TYPE_DE],
/* 0xD6 */ [Debugger8080.INS.SUI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xD7 */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xD8 */ [Debugger8080.INS.RC],
/* 0xD9 */ [Debugger8080.INS.RET, Debugger8080.TYPE_UNDOC],
/* 0xDA */ [Debugger8080.INS.JC, Debugger8080.TYPE_ADDR],
/* 0xDB */ [Debugger8080.INS.IN, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xDC */ [Debugger8080.INS.CC, Debugger8080.TYPE_ADDR],
/* 0xDD */ [Debugger8080.INS.CALL, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_UNDOC],
/* 0xDE */ [Debugger8080.INS.SBI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xDF */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xE0 */ [Debugger8080.INS.RPO],
/* 0xE1 */ [Debugger8080.INS.POP, Debugger8080.TYPE_HL],
/* 0xE2 */ [Debugger8080.INS.JPO, Debugger8080.TYPE_ADDR],
/* 0xE3 */ [Debugger8080.INS.XTHL, Debugger8080.TYPE_SP | Debugger8080.TYPE_MEM| Debugger8080.TYPE_OPT, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT],
/* 0xE4 */ [Debugger8080.INS.CPO, Debugger8080.TYPE_ADDR],
/* 0xE5 */ [Debugger8080.INS.PUSH, Debugger8080.TYPE_HL],
/* 0xE6 */ [Debugger8080.INS.ANI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xE7 */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xE8 */ [Debugger8080.INS.RPE],
/* 0xE9 */ [Debugger8080.INS.PCHL, Debugger8080.TYPE_HL],
/* 0xEA */ [Debugger8080.INS.JPE, Debugger8080.TYPE_ADDR],
/* 0xEB */ [Debugger8080.INS.XCHG, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT, Debugger8080.TYPE_DE | Debugger8080.TYPE_OPT],
/* 0xEC */ [Debugger8080.INS.CPE, Debugger8080.TYPE_ADDR],
/* 0xED */ [Debugger8080.INS.CALL, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_UNDOC],
/* 0xEE */ [Debugger8080.INS.XRI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xEF */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xF0 */ [Debugger8080.INS.RP],
/* 0xF1 */ [Debugger8080.INS.POP, Debugger8080.TYPE_PSW],
/* 0xF2 */ [Debugger8080.INS.JP, Debugger8080.TYPE_ADDR],
/* 0xF3 */ [Debugger8080.INS.DI],
/* 0xF4 */ [Debugger8080.INS.CP, Debugger8080.TYPE_ADDR],
/* 0xF5 */ [Debugger8080.INS.PUSH, Debugger8080.TYPE_PSW],
/* 0xF6 */ [Debugger8080.INS.ORI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xF7 */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT],
/* 0xF8 */ [Debugger8080.INS.RM],
/* 0xF9 */ [Debugger8080.INS.SPHL, Debugger8080.TYPE_SP | Debugger8080.TYPE_OPT, Debugger8080.TYPE_HL | Debugger8080.TYPE_OPT],
/* 0xFA */ [Debugger8080.INS.JM, Debugger8080.TYPE_ADDR],
/* 0xFB */ [Debugger8080.INS.EI],
/* 0xFC */ [Debugger8080.INS.CM, Debugger8080.TYPE_ADDR],
/* 0xFD */ [Debugger8080.INS.CALL, Debugger8080.TYPE_ADDR | Debugger8080.TYPE_UNDOC],
/* 0xFE */ [Debugger8080.INS.CPI, Debugger8080.TYPE_A | Debugger8080.TYPE_OPT, Debugger8080.TYPE_IMM | Debugger8080.TYPE_BYTE],
/* 0xFF */ [Debugger8080.INS.RST, Debugger8080.TYPE_INT]
];
Debugger8080.HISTORY_LIMIT = DEBUG? 100000 : 1000;
/*
* Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-))
*/
Web.onInit(Debugger8080.init);
} // endif DEBUGGER
/**
* @copyright http://pcjs.org/modules/pc8080/lib/computer.js (C) Jeff Parsons 2012-2017
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Computer8080 extends Component {
/**
* Computer8080(parmsComputer, parmsMachine, fSuspended)
*
* The Computer8080 component has no required (parmsComputer) properties, but it does
* support the following:
*
* autoPower: true to automatically power the computer (default), false to wait;
* false is honored only if a "power" button binding exists.
*
* busWidth: number of memory address lines (address bits) on the computer's "bus";
* 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing,
* while 24 is required for 80286 protected-mode addressing. This value is passed
* directly through to the Bus component; see that component for more details.
*
* resume: one of the Computer8080.RESUME constants, which are as follows:
* '0' if resume disabled (default)
* '1' if enabled without prompting
* '2' if enabled with prompting
* '3' if enabled with prompting and auto-delete
* or a string containing the path of a predefined JSON-encoded state
*
* state: the path to JSON-encoded state file (see details regarding 'state' below)
*
* The parmsMachine object, if provided, may contain any of:
*
* autoMount: if set, this should override any 'autoMount' property in the FDC's
* parmsFDC object.
*
* autoPower: if set, this should override any 'autoPower' property in the Computer8080's
* parmsComputer object.
*
* messages: if set, this should override any 'messages' property in the Debugger's
* parmsDbg object.
*
* state: if set, this should override any 'state' property in the Computer8080's
* parmsComputer object.
*
* url: the location of the machine XML file
*
* If a predefined state is supplied AND it's successfully loaded, then resume behavior
* defaults to '1' (ie, resume enabled without prompting).
*
* This component insures that all components are ready before "powering" them.
*
* Different components become ready at different times, and initialization order (ie,
* the order the scripts are combined on the page) only partially determines readiness.
* This is because components like ROM and Video must finish loading their resource files
* before they are ready. Other components become ready after we call their initBus()
* function, because they have a Bus or CPU dependency, such as access to memory management
* functions. And other components, like CPU and Panel, are ready as soon as their
* constructor finishes.
*
* Once a component has indicated it's ready, we call its powerUp() notification
* function (if it has one--it's optional). We call the CPU's powerUp() function last,
* so that the CPU is assured that all other components are ready and "powered".
*
* @this {Computer8080}
* @param {Object} parmsComputer
* @param {Object} [parmsMachine]
* @param {boolean} [fSuspended]
*/
constructor(parmsComputer, parmsMachine, fSuspended)
{
super("Computer", parmsComputer, Messages8080.COMPUTER);
this.flags.powered = false;
this.setMachineParms(parmsMachine);
this.fAutoPower = this.getMachineParm('autoPower', parmsComputer);
/*
* nPowerChange is 0 while the power state is stable, 1 while power is transitioning
* to "on", and -1 while power is transitioning to "off".
*/
this.nPowerChange = 0;
/*
* TODO: Deprecate 'buswidth' (it should have always used camelCase)
*/
this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth'];
this.resume = Computer8080.RESUME_NONE;
this.sStateData = null;
this.fStateData = false; // remembers if sStateData was loaded
this.fServerState = false;
this.url = this.getMachineParm('url') || "";
/*
* Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be
* non-zero, convert to base 36, and chop off the leading digit and "decimal" point.
*/
this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12);
this.sUserID = this.queryUserID();
/*
* Find the appropriate CPU (and Debugger and Control Panel, if any)
*
* CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here,
* where we know getComponentByType() will only return an CPUState object or null), wrap the expression
* in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide".
*/
this.cpu = /** @type {CPUState8080} */ (Component.getComponentByType("CPU", this.id));
if (!this.cpu) {
Component.error("Unable to find CPU component");
return;
}
this.dbg = /** @type {Debugger8080} */ (Component.getComponentByType("Debugger", this.id));
/*
* Enumerate all Video components for future updateVideo() calls.
*/
this.aVideo = [];
for (var video = null; (video = this.getMachineComponent("Video", video));) {
this.aVideo.push(video);
}
/*
* Initialize the Bus component
*/
this.bus = new Bus8080({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg);
/*
* Iterate through all the components and connect them to the Control Panel, if any
*/
var iComponent, component;
var aComponents = Component.getComponents(this.id);
this.panel = /** @type {Panel8080} */ (Component.getComponentByType("Panel", this.id));
if (this.panel && this.panel.controlPrint) {
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
/*
* I can think of many "cleaner" ways for the Control Panel component to pass its
* notice(), println(), etc, overrides on to all the other components, but it's just
* too darn convenient to slam those overrides into the components directly.
*/
component.notice = this.panel.notice;
component.println = this.panel.println;
component.controlPrint = this.panel.controlPrint;
}
}
this.println(PC8080.APPNAME + " v" + (XMLVERSION || PC8080.APPVERSION) + "\n" + COPYRIGHT + "\n" + LICENSE);
if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS);
/*
* Iterate through all the components again and call their initBus() handler, if any
*/
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg);
}
var sStatePath = null;
var sResume = this.getMachineParm('resume', parmsComputer);
if (sResume !== undefined) {
/*
* Decide whether the 'resume' property is a number or the path of a state file to resume.
*/
if (sResume.length > 1) {
sStatePath = this.sResumePath = sResume;
} else {
this.resume = parseInt(sResume, 10);
}
}
/*
* The Computer 'state' property allows a state file to be specified independent of the 'resume' feature;
* previously, you could only use 'resume' to load a state file -- which we still support, but loading a state
* file that way prevents the machine's state from being saved, since we always resume from the 'resume' file.
*
* The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists.
* So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data,
* but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've
* already loaded.
*
* However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that
* OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in
* localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off).
*/
var fAllowResume;
var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state'];
if (sState) {
sStatePath = this.sStatePath = sState;
if (!fAllowResume) {
this.fServerState = true;
this.resume = Computer8080.RESUME_NONE;
}
if (this.resume) {
this.stateComputer = new State(this, PC8080.APPVERSION);
if (this.stateComputer.load()) {
sStatePath = null;
} else {
delete this.stateComputer;
}
}
}
/*
* If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set,
* then we have the option of resuming from a server-side state, assuming a valid USERID.
*/
if (!sStatePath && this.resume) {
sStatePath = this.getServerStatePath();
if (sStatePath) this.fServerState = true;
}
if (!sStatePath) {
this.setReady();
} else {
var cmp = this;
Web.getResource(sStatePath, null, true, function(sURL, sResource, nErrorCode) {
cmp.doneLoad(sURL, sResource, nErrorCode);
});
}
if (!this.bindings["power"]) this.fAutoPower = true;
/*
* Power on the computer, giving every component the opportunity to reset or restore itself.
*/
if (!fSuspended && this.fAutoPower) this.wait(this.powerOn);
}
/**
* getMachineID()
*
* @this {Computer8080}
* @return {string}
*/
getMachineID()
{
return this.sMachineID;
}
/**
* setMachineParms(parmsMachine)
*
* If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any).
*
* @this {Computer8080}
* @param {Object} [parmsMachine]
*/
setMachineParms(parmsMachine)
{
if (!parmsMachine) {
var sParms;
if (typeof resources == 'object' && (sParms = resources['parms'])) {
try {
parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")"));
} catch(e) {
Component.error(e.message + " (" + sParms + ")");
}
}
}
this.parmsMachine = parmsMachine;
}
/**
* getMachineParm(sParm, parmsComponent)
*
* If the machine parameter doesn't exist, we check for a matching component parameter (if parmsComponent is provided),
* and failing that, we check the bundled resources (if any).
*
* At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, then we return
* 'state' back to the caller (ie, the name of the resource), so that the caller will then attempt to load the 'state'
* resource to obtain the actual state.
*
* @this {Computer8080}
* @param {string} sParm
* @param {Object} [parmsComponent]
* @return {string|undefined}
*/
getMachineParm(sParm, parmsComponent)
{
/*
* When using getURLParm(), the check is allowed be a bit looser, because URL parameters are
* user-supplied, whereas most other parameters are developer-supplied. Granted, a developer
* may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'),
* but there are limits to my paranoia.
*/
var sParmLC = sParm.toLowerCase();
var value = Web.getURLParm(sParm) || Web.getURLParm(sParmLC);
if (value === undefined && this.parmsMachine) {
value = this.parmsMachine[sParm];
}
if (value === undefined && parmsComponent) {
value = parmsComponent[sParm];
}
if (value === undefined && typeof resources == 'object' && resources[sParm]) {
value = sParm;
}
return value;
}
/**
* saveMachineParms()
*
* @this {Computer8080}
* @return {string|null}
*/
saveMachineParms()
{
return this.parmsMachine? JSON.stringify(this.parmsMachine) : null;
}
/**
* getUserID()
*
* @this {Computer8080}
* @return {string}
*/
getUserID()
{
return this.sUserID || "";
}
/**
* doneLoad(sURL, sStateData, nErrorCode)
*
* @this {Computer8080}
* @param {string} sURL
* @param {string} sStateData
* @param {number} nErrorCode
*/
doneLoad(sURL, sStateData, nErrorCode)
{
if (!nErrorCode) {
this.sStateData = sStateData;
this.fStateData = true;
if (DEBUG && this.messageEnabled()) {
this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx"));
}
} else {
this.sResumePath = null;
this.fServerState = false;
this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + Str.trim(sStateData) : '') + ')');
}
this.setReady();
}
/**
* wait(fn, parms)
*
* wait() waits until every component is ready (including ourselves, the last component we check), then calls the
* specified Computer method.
*
* TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases;
* sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that
* takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists
* that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped
* function declaration, instead of something stricter like:
*
* param {function(this:Computer, (number|Array|undefined)): undefined} fn
*
* @this {Computer8080}
* @param {function(...)} fn
* @param {number|Array} [parms] optional parameters
*/
wait(fn, parms)
{
var computer = this;
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) {
var component = (iComponent < aComponents.length ? aComponents[iComponent] : this);
if (!component.isReady()) {
component.isReady(function onComponentReady() {
computer.wait(fn, parms);
});
return;
}
}
if (DEBUG && this.messageEnabled()) this.printMessage("Computer8080.wait(ready)");
fn.call(this, parms);
}
/**
* validateState(stateComputer)
*
* NOTE: We clear() stateValidate only when there's no stateComputer.
*
* @this {Computer8080}
* @param {State|null} [stateComputer]
* @return {boolean} true if state passes validation, false if not
*/
validateState(stateComputer)
{
var fValid = true;
var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE);
if (stateValidate.load() && stateValidate.parse()) {
var sTimestampValidate = stateValidate.get(Computer8080.STATE_TIMESTAMP);
var sTimestampComputer = stateComputer? stateComputer.get(Computer8080.STATE_TIMESTAMP) : "unknown";
if (sTimestampValidate != sTimestampComputer) {
this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits");
fValid = false;
if (!stateComputer) stateValidate.clear();
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")");
}
}
}
return fValid;
}
/**
* powerOn(resume)
*
* Power every component "up", applying any previously available state information.
*
* @this {Computer8080}
* @param {number} [resume] is a valid RESUME value; default is this.resume
*/
powerOn(resume)
{
if (resume === undefined) {
resume = this.resume || (this.sStateData? Computer8080.RESUME_AUTO : Computer8080.RESUME_NONE);
}
if (DEBUG && this.messageEnabled()) {
this.printMessage("Computer8080.powerOn(" + (resume == Computer8080.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")");
}
if (this.nPowerChange) {
return;
}
this.nPowerChange++;
var fRepower = false;
var fRestore = false;
this.fRestoreError = false;
var stateComputer = this.stateComputer || new State(this, PC8080.APPVERSION);
if (resume == Computer8080.RESUME_REPOWER) {
fRepower = true;
}
else if (resume > Computer8080.RESUME_NONE) {
if (stateComputer.load(this.sStateData)) {
/*
* Since we're resuming something (either a predefined state or a state from localStorage), let's
* create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn().
* Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad
* may have happened the last time around.
*/
this.stateFailSafe = new State(this, PC8080.APPVERSION, Computer8080.STATE_FAILSAFE);
if (this.stateFailSafe.load()) {
this.powerReport(stateComputer);
/*
* We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it
* all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to
* restore, the state will be removed.
*/
resume = Computer8080.RESUME_PROMPT;
/*
* To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail
* with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'").
*/
this.stateFailSafe.unload();
}
this.stateFailSafe.set(Computer8080.STATE_TIMESTAMP, Usr.getTimestamp());
this.stateFailSafe.store();
var fValidate = this.resume && !this.fServerState;
if (resume == Computer8080.RESUME_AUTO || Component.confirmUser("Click OK to restore the previous " + PC8080.APPNAME + " machine state, or CANCEL to reset the machine.")) {
fRestore = stateComputer.parse();
if (fRestore) {
var sCode = /** @type {string} */ (stateComputer.get(UserAPI.RES.CODE));
var sData = /** @type {string} */ (stateComputer.get(UserAPI.RES.DATA));
if (sCode) {
if (sCode == UserAPI.CODE.OK) {
stateComputer.load(sData);
} else {
/*
* A missing (or not yet created) state file is no cause for alarm, but other errors might be
*/
if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) {
this.notice("Error: " + sData);
if (sData == UserAPI.FAIL.VERIFY) this.resetUserID();
} else {
this.println(sCode + ": " + sData);
}
/*
* Try falling back to the state that we should have saved in localStorage, as a backup to the
* server-side state.
*/
stateComputer.unload(); // discard the invalid server-side state first
if (stateComputer.load()) {
fRestore = stateComputer.parse();
fValidate = true;
} else {
fRestore = false; // hmmm, there was nothing in localStorage either
}
}
}
}
/*
* If the load/parse was successful, and it was from localStorage (not sStateData),
* then we should to try verify that localStorage snapshot is current. One reason it may
* NOT be current is if localStorage was full and we got a quota error during the last
* powerOff().
*/
if (fValidate) this.validateState(fRestore? stateComputer : null);
} else {
/*
* RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above.
*/
if (resume == Computer8080.RESUME_PROMPT) stateComputer.clear();
}
} else {
/*
* If there's no state, then there should also be no validation timestamp; if there is, then once again,
* we're probably dealing with a quota error.
*/
this.validateState();
}
delete this.sStateData;
delete this.stateComputer;
}
/*
* Start powering all components, including any data they may need to restore their state;
* we restore power to the CPU last.
*/
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component !== this && component != this.cpu) {
fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore);
}
}
/*
* Assuming this is not a repower, we must perform another wait, because some components may
* have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it
* to mount one or more additional disk images).
*/
var aParms = [stateComputer, resume, fRestore];
if (resume != Computer8080.RESUME_REPOWER) {
this.wait(this.donePowerOn, aParms);
return;
}
this.donePowerOn(aParms);
}
/**
* powerRestore(component, stateComputer, fRepower, fRestore)
*
* @this {Computer8080}
* @param {Component} component
* @param {State} stateComputer
* @param {boolean} fRepower
* @param {boolean} fRestore
* @return {boolean} true if restore should continue, false if not
*/
powerRestore(component, stateComputer, fRepower, fRestore)
{
if (!component.flags.powered) {
component.flags.powered = true;
if (component.powerUp) {
var data = null;
if (fRestore) {
data = stateComputer.get(component.id);
if (!data) {
/*
* This is a hack that makes it possible for a machine whose ID has been
* supplemented with a suffix (a single letter or digit) to find object IDs
* in states created from a machine without the suffix.
*
* For example, if a state file was created from a machine with ID "ibm5160"
* but the current machine is "ibm5160a", this attempts a second lookup with
* "ibm5160", enabling us to find objects that match the original machine ID
* (eg, "ibm5160.romEGA").
*/
data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.'));
}
}
/*
* State.get() will return whatever was originally passed to State.set() (eg, an
* Object or a string), but components are supposed to store only Objects, so if a
* string comes back, something went wrong. By explicitly eliminating "string" data,
* the Closure Compiler stops complaining that we might be passing strings to our
* powerUp() functions (even though we know we're not).
*
* TODO: Determine if there's some way to coerce the Closure Compiler into treating
* data as Object or null, without having to include this runtime check. An assert
* would be a good idea, but this is overkill.
*/
if (typeof data === "string") data = null;
/*
* If computer is null, this is simply a repower notification, which most components
* don't do anything with. Exceptions include: CPU (since it may be halted) and Video
* (since its screen may be "turned off").
*/
if (!component.powerUp(data, fRepower) && data) {
Component.error("Unable to restore state for " + component.type);
/*
* If this is a resume error for a machine that also has a predefined state
* AND we're not restoring from that state, then throw away the current state,
* prevent any new state from being created, and then force a reload, which will
* hopefully restore us to the functioning predefined state.
*
* TODO: Considering doing this in ALL cases, not just in situations where a
* 'state' exists but we're not actually resuming from it.
*/
if (this.sStatePath && !this.fStateData) {
stateComputer.clear();
this.resume = Computer8080.RESUME_NONE;
Web.reloadPage();
} else {
/*
* In all other cases, we set fRestoreError, which should trigger a call to
* powerReport() and then delete the offending state.
*/
this.fRestoreError = true;
}
/*
* Any failure triggers an automatic to call powerUp() again, without any state,
* in the hopes that the component can recover by performing a reset.
*/
component.powerUp(null);
/*
* We also disable the rest of the restore operation, because it's not clear
* the remaining state information can be trusted; the machine is already in an
* inconsistent state, so we're not likely to make things worse, and the only
* alternative (starting over and performing a state-less reset) isn't likely to make
* the user any happier. But, we'll see... we need some experience with the code.
*/
fRestore = false;
}
}
if (!fRepower && component.comment) {
var asComments = component.comment.split("|");
for (var i = 0; i < asComments.length; i++) {
component.status(asComments[i]);
}
}
}
return fRestore;
}
/**
* donePowerOn(aParms)
*
* This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time.
*
* @this {Computer8080}
* @param {Array} aParms containing [stateComputer, resume, fRestore]
*/
donePowerOn(aParms)
{
var stateComputer = aParms[0];
var fRepower = (aParms[1] < 0);
var fRestore = aParms[2];
if (DEBUG && this.flags.powered && this.messageEnabled()) {
this.printMessage("Computer8080.donePowerOn(): redundant");
}
this.fInitialized = true;
this.flags.powered = true;
var controlPower = this.bindings["power"];
if (controlPower) controlPower.textContent = "Shutdown";
/*
* Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU;
* the CPU should begin executing immediately, unless a debugger is attached.
*/
if (this.cpu) {
/*
* TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)?
*/
this.powerRestore(this.cpu, stateComputer, fRepower, fRestore);
this.cpu.autoStart();
}
/*
* If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new
* state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same.
*/
if (this.fRestoreError) {
this.powerReport(stateComputer);
stateComputer.clear();
}
if (!fRepower && this.stateFailSafe) {
this.stateFailSafe.clear();
delete this.stateFailSafe;
}
this.nPowerChange = 0;
}
/**
* checkPower()
*
* @this {Computer8080}
* @return {boolean} true if the computer is fully powered, false otherwise
*/
checkPower()
{
if (this.flags.powered) return true;
var component = null, iComponent;
var aComponents = Component.getComponents(this.id);
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component !== this && !component.flags.ready) break;
}
if (iComponent == aComponents.length) {
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component !== this && !component.flags.powered) break;
}
}
if (iComponent == aComponents.length) component = this;
var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + ".";
Component.alertUser(s);
return false;
}
/**
* powerReport(stateComputer)
*
* @this {Computer8080}
* @param {State} stateComputer
*/
powerReport(stateComputer)
{
if (Component.confirmUser("There may be a problem with your " + PC8080.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PC8080.APPNAME + " machine state to http://" + SITEHOST + ".")) {
Web.sendReport(PC8080.APPNAME, PC8080.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString());
}
}
/**
* powerOff(fSave, fShutdown)
*
* Power every component "down" and optionally save the machine state.
*
* There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when
* the user switches away while it's still being restored, causing Disk getResource() calls to fail. The
* Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't
* notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact
* it was only partially successful.
*
* Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to
* deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport()
* and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new
* state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future
* restores won't remount them either.
*
* We could perhaps solve this by having the Disk component notify us in those situations, set a new flag
* (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false,
* that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to
* do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons,
* making sure those failures don't interfere with the save process in the same way.
*
* As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted,
* which doesn't seem like a huge problem.
*
* @this {Computer8080}
* @param {boolean} [fSave] is true to request a saved state
* @param {boolean} [fShutdown] is true if the machine is being shut down
* @return {string|null} string representing the saved state (or null if error)
*/
powerOff(fSave, fShutdown)
{
var data;
var sState = "none";
if (DEBUG && this.messageEnabled()) {
this.printMessage("Computer8080.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")");
}
if (this.nPowerChange) {
return null;
}
this.nPowerChange--;
var stateComputer = new State(this, PC8080.APPVERSION);
var stateValidate = new State(this, PC8080.APPVERSION, Computer8080.STATE_VALIDATE);
var sTimestamp = Usr.getTimestamp();
stateValidate.set(Computer8080.STATE_TIMESTAMP, sTimestamp);
stateComputer.set(Computer8080.STATE_TIMESTAMP, sTimestamp);
stateComputer.set(Computer8080.STATE_VERSION, APPVERSION);
stateComputer.set(Computer8080.STATE_HOSTURL, Web.getHostURL());
stateComputer.set(Computer8080.STATE_BROWSER, Web.getUserAgent());
/*
* Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything
* after they're no longer ready.
*/
if (this.cpu && this.cpu.powerDown) {
if (fShutdown) this.cpu.stopCPU();
data = this.cpu.powerDown(fSave, fShutdown);
if (typeof data === "object") stateComputer.set(this.cpu.id, data);
if (fShutdown) {
this.cpu.flags.powered = false;
if (data === false) sState = null;
}
}
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.flags.powered) {
if (component.powerDown) {
data = component.powerDown(fSave, fShutdown);
if (typeof data === "object") stateComputer.set(component.id, data);
}
if (fShutdown) {
component.flags.powered = false;
if (data === false) sState = null;
}
}
}
if (sState) {
if (fShutdown) {
var fClear = false;
var fClearAll = false;
if (fSave) {
if (this.sUserID) {
this.saveServerState(this.sUserID, stateComputer.toString());
}
if (!stateValidate.store() || !stateComputer.store()) {
sState = null;
/*
* New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away.
* Dedicated server-side storage is the only way we'll ever be able to reliably preserve a
* particular machine's state. Historically, attempting to limp along with whatever localStorage
* is left just generates the same useless and annoying warnings over and over.
*/
fClear = fClearAll = true;
}
}
else {
/*
* I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the
* current machine is "resumable", because there are situations where I have two configurations
* for the same machine -- one resumable and one not -- and I don't want the latter throwing away the
* state of the former.
*
* So this code is here now strictly for callers to delete the state of a "resumable" machine, not as
* some paranoid clean-up operation.
*
* An undocumented feature of this operation is that if your configuration uses the special 'resume="3"'
* value, and you click the "Reset" button, and then you click OK to reset the everything, this will
* actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed).
*/
if (this.resume) {
fClear = true;
fClearAll = (this.resume == Computer8080.RESUME_DELETE);
}
}
if (fClear) {
stateComputer.clear(fClearAll);
}
} else {
sState = stateComputer.toString();
}
}
if (fShutdown) {
this.flags.powered = false;
var controlPower = this.bindings["power"];
if (controlPower) controlPower.textContent = "Power";
}
this.nPowerChange = 0;
return sState;
}
/**
* reset()
*
* Notify all (other) components with a reset() method that the Computer is being reset.
*
* NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we
* allocated the Bus object ourselves, after all the other components were allocated, it ends
* up near the end of Component's list of components. Hence the special case for this.bus below.
*
* @this {Computer8080}
*/
reset()
{
if (this.bus && this.bus.reset) {
/*
* TODO: Why does WebStorm think that this.bus.type is undefined? The base class (Component)
* constructor defines it.
*/
this.printMessage("Resetting " + this.bus.type);
this.bus.reset();
}
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component !== this && component !== this.bus && component.reset) {
this.printMessage("Resetting " + component.type);
component.reset();
}
}
}
/**
* start(ms, nCycles)
*
* Notify all (other) components with a start() method that the CPU has started.
*
* Note that we're called by runCPU(), which is why we exclude the CPU component,
* as well as ourselves.
*
* @this {Computer8080}
* @param {number} ms
* @param {number} nCycles
*/
start(ms, nCycles)
{
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.type == "CPU" || component === this) continue;
if (component.start) {
component.start(ms, nCycles);
}
}
}
/**
* stop(ms, nCycles)
*
* Notify all (other) components with a stop() method that the CPU has stopped.
*
* Note that we're called by runCPU(), which is why we exclude the CPU component,
* as well as ourselves.
*
* @this {Computer8080}
* @param {number} ms
* @param {number} nCycles
*/
stop(ms, nCycles)
{
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.type == "CPU" || component === this) continue;
if (component.stop) {
component.stop(ms, nCycles);
}
}
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {Computer8080}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
var computer = this;
switch (sBinding) {
case "power":
this.bindings[sBinding] = control;
control.onclick = function onClickPower() {
computer.onPower();
};
return true;
case "reset":
this.bindings[sBinding] = control;
control.onclick = function onClickReset() {
computer.onReset();
};
return true;
/*
* Technically, this binding should now be called "saveState", to clearly distinguish it from
* the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving
* an entire machine includes everything needed to start/restore the machine; eg, the machine
* XML configuration file(s) *and* the JSON-encoded machine state.
*/
case "save":
/*
* Since this feature depends on the server supporting the PCjs User API (see userapi.js),
* and since pcjs.org is no longer running a Node web server, we disable the feature for that
* particular host.
*/
if (Str.endsWith(Web.getHost(), "pcjs.org")) {
if (DEBUG) this.log("Remote user API not available");
/*
* We could also simply hide the control; eg:
*
* control.style.display = "none";
*
* but removing the control altogether seems better.
*/
control.parentNode.removeChild(/** @type {Node} */ (control));
return false;
}
this.bindings[sBinding] = control;
control.onclick = function onClickSave() {
var sUserID = computer.queryUserID(true);
if (sUserID) {
/*
* I modified the test to include a check for sStatePath so that I could save new states
* for machines with existing states; otherwise, I'd have no (easy) way of capturing and
* updating their state. Making the machine (even temporarily) resumable would have been
* one work-around, but it's not appropriate for some machines, as their state is simply
* too large (for localStorage anyway, which is the default storage solution).
*/
var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath);
var sState = computer.powerOff(fSave);
if (fSave) {
computer.saveServerState(sUserID, sState);
} else {
computer.notice("Resume disabled, machine state not saved");
}
}
/*
* This seemed like a handy alternative, but it turned out to be a no-go, at least for large states:
*
* var sState = computer.powerOff(true);
* if (sState) {
* sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState);
* window.open(sState);
* }
*
* Perhaps if I embedded the data in a link on the current page instead; eg:
*
* $('<a href="' + sState + '" download="state.json">Download</a>').appendTo('#container');
*/
};
return true;
default:
break;
}
return false;
}
/**
* resetUserID()
*/
resetUserID()
{
Web.setLocalStorageItem(Computer8080.STATE_USERID, "");
this.sUserID = null;
}
/**
* queryUserID(fPrompt)
*
* @param {boolean} [fPrompt]
* @returns {string|null|undefined}
*/
queryUserID(fPrompt)
{
var sUserID = this.sUserID;
if (!sUserID) {
sUserID = Web.getLocalStorageItem(Computer8080.STATE_USERID);
if (sUserID !== undefined) {
if (!sUserID && fPrompt) {
/*
* NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is
* merely a precaution, because ordinarily, setBinding() should have already determined if we are
* running from pcjs.org and disabled any "Save" button.
*/
sUserID = Component.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below.");
if (sUserID) {
sUserID = this.verifyUserID(sUserID);
if (!sUserID) this.notice("The user ID is invalid.");
}
}
} else if (fPrompt) {
this.notice("Browser local storage is not available");
}
}
return sUserID;
}
/**
* verifyUserID(sUserID)
*
* @this {Computer8080}
* @param {string} sUserID
* @return {string} validated user ID, or null if error
*/
verifyUserID(sUserID)
{
this.sUserID = null;
var fMessages = DEBUG && this.messageEnabled();
if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")");
var sRequest = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID;
var response = Web.getResource(sRequest);
var nErrorCode = response[0];
var sResponse = response[1];
if (!nErrorCode && sResponse) {
try {
response = eval("(" + sResponse + ")");
if (response.code && response.code == UserAPI.CODE.OK) {
Web.setLocalStorageItem(Computer8080.STATE_USERID, response.data);
if (fMessages) this.printMessage(Computer8080.STATE_USERID + " updated: " + response.data);
this.sUserID = response.data;
} else {
if (fMessages) this.printMessage(response.code + ": " + response.data);
}
} catch (e) {
Component.error(e.message + " (" + sResponse + ")");
}
} else {
if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")");
}
return this.sUserID;
}
/**
* getServerStatePath()
*
* @this {Computer8080}
* @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage)
*/
getServerStatePath()
{
var sStatePath = null;
if (this.sUserID) {
if (DEBUG && this.messageEnabled()) {
this.printMessage(Computer8080.STATE_USERID + " for load: " + this.sUserID);
}
sStatePath = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PC8080.APPVERSION);
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage(Computer8080.STATE_USERID + " unavailable");
}
}
return sStatePath;
}
/**
* saveServerState(sUserID, sState)
*
* @param {string} sUserID
* @param {string|null} sState
*/
saveServerState(sUserID, sState)
{
/*
* We must pass fSync == true, because (as I understand it) browsers will blow off any async
* requests when a page is being closed. Since our request is synchronous, storeServerState()
* should also return a result, but there's not much we can do with it, since browsers ALSO
* tend to blow off alerts() and the like when closing down.
*/
if (sState) {
if (DEBUG && this.messageEnabled()) {
this.printMessage("size of server state: " + sState.length + " bytes");
}
var response = this.storeServerState(sUserID, sState, true);
if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) {
this.notice("Machine state saved to server");
} else if (sState) {
var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE;
if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) {
sError = "Error: " + sError;
} else {
sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError;
}
this.notice(sError);
this.resetUserID();
}
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage("no state to store");
}
}
}
/**
* storeServerState(sUserID, sState, fSync)
*
* @this {Computer8080}
* @param {string} sUserID
* @param {string} sState
* @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async)
* @return {*} server response if fSync is true and a response was received; otherwise null
*/
storeServerState(sUserID, sState, fSync)
{
if (DEBUG && this.messageEnabled()) {
this.printMessage(Computer8080.STATE_USERID + " for store: " + sUserID);
}
/*
* TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event,
* and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through).
*/
var dataPost = {};
dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE;
dataPost[UserAPI.QUERY.USER] = sUserID;
dataPost[UserAPI.QUERY.STATE] = State.key(this, PC8080.APPVERSION);
dataPost[UserAPI.QUERY.DATA] = sState;
var sRequest = Web.getHost() + UserAPI.ENDPOINT;
if (!fSync) {
Web.getResource(sRequest, dataPost, true);
} else {
var response = Web.getResource(sRequest, dataPost);
var sResponse = response[0];
if (response[1]) {
if (sResponse) {
var i = sResponse.indexOf('\n');
if (i > 0) sResponse = sResponse.substr(0, i);
if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7);
}
sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}';
}
if (DEBUG && this.messageEnabled()) this.printMessage(sResponse);
return JSON.parse(sResponse);
}
return null;
}
/**
* onPower()
*
* This handles UI requests to toggle the computer's power (eg, see the "power" button binding).
*
* @this {Computer8080}
*/
onPower()
{
if (!this.nPowerChange) {
if (!this.flags.powered) {
this.wait(this.powerOn);
} else {
this.powerOff(false, true);
}
}
}
/**
* onReset()
*
* This handles UI requests to reset the computer's state (eg, see the "reset" button binding).
*
* @this {Computer8080}
*/
onReset()
{
/*
* I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset";
* ditto if the power state is currently being changed.
*/
if (!this.flags.powered || this.nPowerChange) return;
/*
* If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset
* operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to
* discard a machine's state, including any disk changes. The traditional "reset" operation is still available
* for non-resumable machines.
*
* TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really
* wants to clutter the UI with confusing options. ;-)
*/
if (this.resume && !this.sResumePath) {
/*
* I used to bypass the prompt if this.resume == Computer8080.RESUME_AUTO, setting fSave to true automatically,
* but that gives the user no means of resetting a resumable machine that contains errors in its resume state.
*/
var fSave = (/* this.resume == Computer8080.RESUME_AUTO || */ Component.confirmUser("Click OK to save changes to this " + PC8080.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded."));
this.powerOff(fSave, true);
/*
* Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn()
* and rely on all the components to reset themselves to their default state. The components with
* the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether
* or not to unload/reload all their original auto-mounted disk images.
*
* However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because
* we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource()
* again); alternatively, we could avoid throwing that state away, but it seems better to save the memory.
*
* TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer.
*/
if (!fSave && this.sStatePath) {
Web.reloadPage();
return;
}
if (!fSave) this.fReload = true;
this.powerOn(Computer8080.RESUME_NONE);
this.fReload = false;
} else {
this.reset();
if (this.cpu) this.cpu.autoStart();
}
}
/**
* getMachineComponent(sType, componentPrev)
*
* @this {Computer8080}
* @param {string} sType
* @param {Component|null} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
getMachineComponent(sType, componentPrev)
{
var componentLast = componentPrev;
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (componentPrev) {
if (componentPrev == component) componentPrev = null;
continue;
}
if (component.type == sType) return component;
}
if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning");
return null;
}
/**
* updateFocus(fScroll)
*
* NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button),
* the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS,
* where the display is more constrained, so we no longer do it by default (fScroll must be true).
*
* @this {Computer8080}
* @param {boolean} [fScroll] (true if you really want the control scrolled into view)
*/
updateFocus(fScroll)
{
if (this.aVideo.length) {
/*
* This seems to be recommended work-around to prevent the browser from scrolling the focused element
* into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent
* is to ensure that keyboard input is fielded properly.
*/
var x = 0, y = 0;
if (!fScroll && window) {
x = window.scrollX;
y = window.scrollY;
}
/*
* TODO: We need a mechanism to determine the "active" display, instead of hard-coding this to aVideo[0].
*/
this.aVideo[0].setFocus();
if (!fScroll && window) {
window.scrollTo(x, y);
}
}
}
/**
* updateStatus(fForce)
*
* If any DOM controls were bound to the CPU, then we need to call its updateStatus() handler; if there are no
* such bindings, then cpu.updateStatus() does nothing.
*
* Similarly, if there's a Panel, then we need to call its updateStatus() handler, in case it created its own canvas
* and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateStatus() also does nothing.
*
* In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or*
* neither. In theory, there could be BOTH, but that would be unusual.
*
* TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to
* separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately
* knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate,
* but Panel is an optional component; generally, only machines that include Debugger also include Panel.
*
* @this {Computer8080}
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
*/
updateStatus(fForce)
{
/*
* fForce is generally set to true whenever the CPU is transitioning to/from a running state, in which case
* cpu.updateStatus() will definitely want to hide/show register contents; however, at other times, when the
* CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance.
*
* So fForce serves as a hint to help cpu.updateStatus() make a more informed decision. panel.updateStatus()
* currently doesn't care, on the theory that canvas updates should be significantly faster than DOM updates,
* but we still pass fForce on.
*/
if (this.cpu) this.cpu.updateStatus(fForce);
if (this.panel) this.panel.updateStatus(fForce);
}
/**
* updateVideo(fForced)
*
* Any high-frequency updates should be performed here (avoid updating DOM elements).
*
* @this {Computer8080}
* @param {boolean} [fForced]
*/
updateVideo(fForced)
{
for (var i = 0; i < this.aVideo.length; i++) {
this.aVideo[i].updateScreen(fForced);
}
}
/**
* Computer8080.init()
*
* For every machine represented by an HTML element of class "pc8080-machine", this function
* locates the HTML element of class "computer", extracting the JSON-encoded parameters for the
* Computer constructor from the element's "data-value" attribute, invoking the constructor to
* create a Computer component, and then binding any associated HTML controls to the new component.
*/
static init()
{
/*
* In non-COMPILED builds, embedMachine() may have set XMLVERSION.
*/
if (!COMPILED && XMLVERSION) PC8080.APPVERSION = XMLVERSION;
var aeMachines = Component.getElementsByClass(document, PC8080.APPCLASS + "-machine");
for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) {
var eMachine = aeMachines[iMachine];
var parmsMachine = Component.getComponentParms(eMachine);
var aeComputers = Component.getElementsByClass(eMachine, PC8080.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
/*
* We set fSuspended in the Computer constructor because we want to "power up" the
* computer ourselves, after any/all bindings are in place.
*/
var computer = new Computer8080(parmsComputer, parmsMachine, true);
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onInit(" + computer.flags.powered + ")");
}
/*
* Bind any "power", "reset" and "save" buttons. An "erase" button was also considered,
* but "reset" now provides a way to force the machine to start from scratch again, so "erase"
* may be redundant now.
*/
Component.bindComponentControls(computer, eComputer, PC8080.APPCLASS);
/*
* Power on the computer, giving every component the opportunity to reset or restore itself.
*/
if (computer.fAutoPower) computer.wait(computer.powerOn);
}
}
}
/**
* Computer8080.show()
*
* When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything,
* without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the
* computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process
* should be very quick, essentially just marking all components as powered again (so that, for example, the Video
* component will start drawing again) and firing the CPU up again.
*/
static show()
{
var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id']));
if (computer) {
computer.flags.unloading = false;
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")");
}
/*
* Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs
* AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet.
* So, practically speaking, the first show() callback isn't all that useful.
*/
if (computer.fInitialized && !computer.flags.powered) {
/**
* Repower the computer, notifying every component to continue running as-is.
*/
computer.powerOn(Computer8080.RESUME_REPOWER);
}
}
}
}
/**
* Computer8080.exit()
*
* The Computer is currently the only component that uses an "exit" handler, which web.onExit() defines as
* either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state,
* using our powerOff() function, before the page goes away.
*
* It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire
* page (and therefore this entire application) is retained in its current state by the browser (well, some
* browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing
* the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state.
*
* In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words,
* an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere
* presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns.
*
* However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower
* everything, without either resetting or restoring. Hence, the Computer8080.show() function, which calls powerOn()
* with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser
* didn't change anything. This "repower" process should be very quick, essentially just marking all components as
* powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again.
*
* Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use
* "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and
* localStorage means the simulation should be restored and become operational almost immediately.
*/
static exit()
{
var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
var computer = /** @type {Computer8080} */ (Component.getComponentByType("Computer", parmsComputer['id']));
if (computer) {
/*
* Added a new flag that Component functions (eg, notice()) should check before alerting the user.
*/
computer.flags.unloading = true;
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onExit(" + computer.flags.powered + ")");
}
if (computer.flags.powered) {
/**
* Power off the computer, giving every component an opportunity to save its state,
* but only if 'resume' has been set AND there is no valid resume path (because if a valid resume
* path exists, we'll always load our state from there, and not from whatever we save here).
*/
computer.powerOff(!!(computer.resume && !computer.sResumePath), true);
}
}
}
}
}
Computer8080.STATE_FAILSAFE = "failsafe";
Computer8080.STATE_VALIDATE = "validate";
Computer8080.STATE_TIMESTAMP = "timestamp";
Computer8080.STATE_VERSION = "version";
Computer8080.STATE_HOSTURL = "url";
Computer8080.STATE_BROWSER = "browser";
Computer8080.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.
*/
Computer8080.RESUME_REPOWER = -1; // resume without changing any state (for internal use only)
Computer8080.RESUME_NONE = 0; // default (no resume)
Computer8080.RESUME_AUTO = 1; // automatically save/restore state
Computer8080.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded)
Computer8080.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented)
/*
* Initialize every Computer on the page.
*/
Web.onInit(Computer8080.init);
Web.onShow(Computer8080.show);
Web.onExit(Computer8080.exit);
/**
* @copyright http://pcjs.org/modules/shared/lib/state.js (C) Jeff Parsons 2012-2017
*/
class State {
/**
* 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).
*
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
* From that point on, care must be taken to insure that any new version that's incompatible with
* previous localStorage data be released with a version number that is at least 1 greater,
* since we're tagging the localStorage data with the integer portion of the version string.
*
* @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
*/
constructor(component, sVersion, sSuffix)
{
this.id = component.id;
this.dbg = component.dbg;
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
this.key = State.key(component, sVersion, sSuffix);
this.unload(component.parms);
}
/**
* set(id, data)
*
* @this {State}
* @param {number|string} id
* @param {Object|string} data
*/
set(id, data)
{
try {
this.state[id] = data;
} catch(e) {
Component.log(e.message);
}
}
/**
* get(id)
*
* @this {State}
* @param {number|string} id
* @return {Object|string|null}
*/
get(id)
{
return this.state[id] || null;
}
/**
* data()
*
* @this {State}
* @return {Object}
*/
data()
{
return this.state;
}
/**
* load(json)
*
* 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 {string|null} [json]
* @return {boolean} true if state exists in localStorage, false if not
*/
load(json)
{
if (json) {
this.json = json;
this.fLoaded = true;
this.fParsed = false;
return true;
}
if (this.fLoaded) {
/*
* This is assumed to be a redundant load().
*/
return true;
}
if (Web.hasLocalStorage()) {
var s = Web.getLocalStorageItem(this.key);
if (s) {
this.json = s;
this.fLoaded = true;
if (DEBUG) Component.log("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()
{
var fSuccess = true;
if (!this.fParsed) {
try {
this.state = JSON.parse(this.json);
this.fParsed = true;
} catch (e) {
Component.error(e.message || e);
fSuccess = false;
}
}
return fSuccess;
}
/**
* store()
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
store()
{
var fSuccess = true;
if (Web.hasLocalStorage()) {
var s = JSON.stringify(this.state);
if (Web.setLocalStorageItem(this.key, s)) {
if (DEBUG) Component.log("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()
*
* @this {State}
* @return {string} JSON-encoded state
*/
toString()
{
return this.state? JSON.stringify(this.state) : this.json;
}
/**
* 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(parms)
{
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
if (parms) this.set("parms", parms);
}
/**
* 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(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) Component.log("localStorage(" + sKey + ") removed");
aKeys.splice(i, 1);
i = 0;
}
}
}
/**
* 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
*/
static key(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.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compress(aSrc)
{
if (aSrc) {
var iSrc = 0;
var iComp = 0;
var aComp = [];
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
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.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompress(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;
}
}
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.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compressEvenOdd(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.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompressEvenOdd(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.
*/
if (iDst == nLength) iDst = 1;
}
return aDst;
}
}
/**
* @copyright http://pcjs.org/modules/shared/lib/embed.js (C) Jeff Parsons 2012-2017
*/
/*
* We now support asynchronous XML and XSL file loads; simply set fAsync (below) to true.
*
* NOTE: For that support to work, we have to keep track of the number of machines on the page
* (ie, how many embedMachine() calls were issued), reduce the count as each machine XML file
* is fully transformed into HTML, and when the count finally returns to zero, notify all the
* machine component init() handlers.
*
* Also, to prevent those init() handlers from running prematurely, we must disable all page
* notification events at the start of the embedding process (Web.enablePageEvents(false)) and
* re-enable them at the end (Web.enablePageEvents(true)).
*/
var fAsync = true;
var cAsyncMachines = 0;
/**
* loadXML(sFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* This is the preferred way to load all XML and XSL files. It uses getResource()
* to load them as strings, which parseXML() can massage before parsing/transforming them.
*
* For example, since I've been unable to get the XSLT document() function to work inside any
* XSL document loaded by JavaScript's XSLT processor, that has prevented me from dynamically
* loading any XML machine file that uses the "ref" attribute to refer to and incorporate
* another XML document.
*
* To solve that, I've added an fResolve parameter that tells parseXML() to fetch any
* referenced documents ITSELF and insert them into the XML string prior to parsing, instead
* of relying on the XSLT template to pull them in. That fetching is handled by resolveXML(),
* which iterates over the XML until all "refs" have been resolved (including any nested
* references).
*
* Also, XSL files with a <!DOCTYPE [...]> cause MSIE's Microsoft.XMLDOM.loadXML() function
* to choke, so I strip that out prior to parsing as well.
*
* TODO: Figure out why the XSLT document() function works great when the web browser loads an
* XML file (and the associated XSL file) itself, but does not work when loading documents via
* JavaScript XSLT support. Is it broken, is it a security issue, or am I just calling it wrong?
*
* @param {string} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var doneLoadXML = function(sURLName, sXML, nErrorCode) {
if (nErrorCode) {
if (!sXML) sXML = "unable to load " + sXMLFile + " (" + nErrorCode + ")";
done(sXML, null);
return;
}
parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done);
};
display("Loading " + sXMLFile + "...");
Web.getResource(sXMLFile, null, fAsync, doneLoadXML);
}
/**
* parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* Generates an XML document from an XML string. This function also provides a work-around for XSLT's
* lack of support for the document() function (at least on some browsers), by replacing every reference
* tag (ie, a tag with a "ref" attribute) with the contents of the referenced file.
*
* @param {string} sXML
* @param {string|null} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes; default is false
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var buildXML = function(sXML, sError) {
if (sError) {
done(sError, null);
return;
}
if (idMachine) {
/*
* A more sensible place to record the machine XML would be embedMachine(), like we do for the
* XSL file, but since we're about to modify the original machine XML, it's best to record it now.
*/
Component.addMachineResource(idMachine, sXMLFile, sXML);
var sURL = sXMLFile;
if (sURL && sURL.indexOf('/') < 0 && window.location.pathname.slice(-1) == '/') {
sURL = window.location.pathname + sURL;
}
/*
* We embed the URL of the XML file both as a separate "xml" attribute for easy access from the
* XSL file, and as part of the "parms" attribute for easy access from machines (see getMachineParm()).
*/
if (!sParms) {
sParms = '{';
} else if (sParms.slice(-1) == '}') {
sParms = sParms.slice(0, -1);
if (sParms.length > 1) sParms += ',';
} else { // sParms must just be a "state" file, so encode it as a "state" property
sParms = '{state:"' + sParms + '",';
}
sParms += 'url:"' + sURL + '"}';
/*
* Note that while we no longer generate a machine XML file with a "state" attribute (because it's
* encoded inside the "parms" attribute), the XSL file must still cope with "state" attributes inside
* other XML files; for example, manifest XML files like /apps/pc/1981/visicalc/manifest.xml contain
* machine elements with "state" attributes that must still be passed down to the computer element
* "the old fashioned way".
*
* Until/unless that changes, components.xsl cannot be simplified as much as I might have hoped.
*/
if (typeof resources == 'object') sURL = null; // turn off URL inclusion if we have embedded resources
sParms = sParms.replace(/\$/g, "$$$$");
sXML = sXML.replace(/(<machine[^>]*\sid=)(['"]).*?\2/, "$1$2" + idMachine + "$2" + (sParms? " parms='" + sParms + "'" : "") + (sURL? ' url="' + sURL + '"' : ''));
}
if (!fResolve) {
/*
* I'm trying to switch to a shared components.xsl (at least for all PC-class machines),
* but in the interim, that means hacking the XSL file on the fly to reflect the actual class.
*/
sXML = sXML.replace(/(<xsl:variable name="APPNAME">).*?(<\/xsl:variable>)/, "$1" + sAppName + "$2");
sXML = sXML.replace(/(<xsl:variable name="APPCLASS">).*?(<\/xsl:variable>)/, "$1" + sAppClass + "$2");
/*
* Non-COMPILED kludge to replace the version number template in the XSL file (which we assume we're reading,
* since fResolve is false) with whatever XMLVERSION we extracted from the XML file (see corresponding kludge below).
*
* ES6 ALERT: Template strings.
*/
if (!COMPILED && XMLVERSION) {
sXML = sXML.replace(/<xsl:variable name="APPVERSION">1.x.x<\/xsl:variable>/, `<xsl:variable name="APPVERSION">${XMLVERSION}</xsl:variable>`);
}
}
/*
* If the resource we requested is not really an XML file (or the file didn't exist and the server simply returned
* a message like "Cannot GET /devices/pc/machine/5150/cga/64kb/donkey/machine.xml"), we'd like to display a more
* meaningful message, because the XML DOM parsers will blithely return a document that contains nothing useful; eg:
*
* This page contains the following errors:error on line 1 at column 1:
* Document is empty Below is a rendering of the page up to the first error.
*
* Supposedly, the IE XML DOM parser will throw an exception, but I haven't tested that, and unless all other
* browsers do that, that's not helpful.
*
* The best I can do at this stage (assuming Web.getResource() didn't drop any error information on the floor)
* is verify that the requested resource "looks like" valid XML (in other words, it begins with a '<').
*/
var xmlDoc = null;
if (sXML.charAt(0) == '<') {
try {
/*
* Another hack for MSIE, which fails to load XSL documents containing a <!DOCTYPE [...]> tag.
*
* This is also why the XSLTProcessor 'transformToFragment' method in Microsoft Edge silently failed,
* so I had pull this hack out of the "ActiveXObject" code. And rather than add yet-another Microsoft
* browser check, I'm going to try doing this across the board, and hope that none of the other XSLT
* processors fail *without* the DOCTYPE tag.
*/
if (!fResolve) {
sXML = sXML.replace(/<!DOCTYPE(.|[\r\n])*]>\s*/g, "");
}
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
/** @namespace window.ActiveXObject */
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc['loadXML'](sXML);
} else {
/** @namespace window.DOMParser */
xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml");
}
} catch(e) {
xmlDoc = null;
sXML = e.message;
}
} else {
sXML = "unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML);
}
done(sXML, xmlDoc);
};
if (sXML) {
if (PRIVATE) sXML = sXML.replace(/\/library.xml/, "/private/library.xml");
if (fResolve) {
resolveXML(sXML, display, buildXML);
return;
}
buildXML(sXML, null);
return;
}
done("no data" + (sXMLFile? " for file: " + sXMLFile : ""), null);
}
/**
* resolveXML(sXML, display, done)
*
* Replaces every tag with a "ref" attribute with the contents of the corresponding file.
*
* TODO: Fix some of the limitations of this code, such as: 1) requiring the "ref" attribute
* to appear as the tag's first attribute, 2) requiring the "ref" attribute to be double-quoted,
* and 3) requiring the "ref" tag to be self-closing.
*
* @param {string} sXML
* @param {function(string)} display
* @param {function(string,(string|null))} done (the first string contains the resolved XML data, the second is for any error message)
*/
function resolveXML(sXML, display, done)
{
var matchRef;
var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g;
if ((matchRef = reRef.exec(sXML))) {
var sRefFile = matchRef[2];
var doneReadXML = function(sURLName, sXMLRef, nErrorCode) {
if (nErrorCode || !sXMLRef) {
done(sXML, "unable to resolve XML reference: " + matchRef[0] + " (" + nErrorCode + ")");
return;
}
/*
* If there are additional attributes in the "referring" XML tag, we want to insert them
* into the "referred" XML tag; attributes that don't exist in the referred tag should be
* appended, and attributes that DO exist should be overwritten.
*/
var sRefAttrs = matchRef[3];
if (sRefAttrs) {
var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>"));
if (aXMLRefTag) {
var sXMLNewTag = aXMLRefTag[0];
/*
* Iterate over all the attributes in the "referring" XML tag (sRefAttrs)
*/
var matchAttr;
var reAttr = /( [a-z]+=)(['"])(.*?)\2/g;
while ((matchAttr = reAttr.exec(sRefAttrs))) {
if (sXMLNewTag.indexOf(matchAttr[1]) < 0) {
/*
* This is the append case
*/
sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">");
} else {
/*
* This is the overwrite case
*/
sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]);
}
}
if (aXMLRefTag[0] != sXMLNewTag) {
sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag);
}
} else {
done(sXML, "missing <" + matchRef[1] + "> in " + sRefFile);
return;
}
}
/*
* Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line:
*
* <?xml version="1.0" encoding="UTF-8"?>\n
*
* I didn't determine exactly what it was doing at this point (probably just changing the \n to \r\n),
* but in any case, relaxing the following replace() solved it.
*/
sXMLRef = sXMLRef.replace(/<\?xml[^>]*>[\r\n]*/, "");
sXML = sXML.replace(matchRef[0], sXMLRef);
resolveXML(sXML, display, done);
};
display("Loading " + sRefFile + "...");
Web.getResource(sRefFile, null, fAsync, doneReadXML);
return;
}
done(sXML, null);
}
/**
* embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
*
* This allows to you embed a machine on a web page, by transforming the machine XML into HTML.
*
* @param {string} sAppName is the app name (eg, "PCx86")
* @param {string} sAppClass is the app class (eg, "pcx86"); also known as the machine class
* @param {string} sVersion is the app version (eg, "1.15.7")
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
{
var eMachine, eWarning, fSuccess = true;
cAsyncMachines++;
Component.addMachine(idMachine);
var doneMachine = function() {
if (!--cAsyncMachines) {
if (fAsync) Web.enablePageEvents(true);
}
};
var displayError = function(sError) {
Component.log(sError);
displayMessage("Error: " + sError);
if (fSuccess) doneMachine();
fSuccess = false;
};
var displayMessage = function(sMessage) {
if (eWarning === undefined) {
/*
* Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like:
*
* <div id="' + sMachineID + '" class="machine-placeholder"><p>Embedded PC</p><p class="machine-warning">...</p></div>
*
* with the "machine-warning" paragraph pre-populated with a warning message that the user will
* see if nothing at all happens. But hopefully, in the normal case (and especially the error case),
* *something* will have happened.
*
* Note that it is the HTMLOut module (in processMachines()) that ultimately decides which scripts to
* include and then generates the embedXXX() call.
*/
var aeWarning = (eMachine && Component.getElementsByClass(eMachine, "machine-warning"));
eWarning = (aeWarning && aeWarning[0]) || eMachine;
}
if (eWarning) eWarning.innerHTML = Str.escapeHTML(sMessage);
};
try {
eMachine = document.getElementById(idMachine);
if (eMachine) {
/*
* If we have a 'css' resource, add it to the page first.
*/
var css;
if (typeof resources == "object" && (css = resources['css'])) {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
if (!sXSLFile) {
/*
* Now that PCjs is an open-source project, we can make the following test more flexible,
* and revert to the internal template if DEBUG *or* internal version (instead of *and*).
*
* Third-party sites that don't use the PCjs server will ALWAYS want to specify a fully-qualified
* path to the XSL file, unless they choose to mirror our folder structure.
*/
var sAppFolder = sAppClass;
if (DEBUG || sVersion == "1.x.x") {
sXSLFile = "/modules/" + sAppFolder + "/templates/components.xsl";
} else {
if (sAppClass.substr(0, 3) == "pdp") sAppFolder = "pdpjs";
sXSLFile = "/versions/" + sAppFolder + "/" + sVersion + "/components.xsl";
}
}
var processXML = function(sXML, xml) {
if (!xml) {
displayError(sXML);
return;
}
/*
* Non-COMPILED kludge to extract the version number from the stylesheet path in the machine XML file;
* we don't need this code in COMPILED (non-DEBUG) releases, because APPVERSION is hard-coded into them.
*/
if (!COMPILED) {
var aMatch = sXML.match(/<\?xml-stylesheet[^>]* href=(['"])[^'"]*?\/([0-9.]*)\/([^'"]*)\1/);
if (aMatch) XMLVERSION = aMatch[2];
}
var transformXML = function(sXSL, xsl) {
if (!xsl) {
displayError(sXSL);
return;
}
/*
* Record the XSL file, in case someone wants to save the entire machine later.
*/
Component.addMachineResource(idMachine, sXSLFile, sXSL);
/*
* The <machine> template in components.xsl now generates a "machine div" that makes
* the div we required the caller of embedMachine() to provide redundant, so instead
* of appending this fragment to the caller's node, we REPLACE the caller's node.
* This works only because because we ALSO inject the caller's "machine div" ID into
* the fragment's ID during parseXML().
*
* eMachine.innerHTML = sFragment;
*
* Also, if the transform function fails, make sure you're using the appropriate
* "components.xsl" and not a "machine.xsl", because the latter will not produce valid
* embeddable HTML (and is the most common cause of failure at this final stage).
*/
displayMessage("Processing " + sXMLFile + "...");
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
var sFragment = xml['transformNode'](xsl);
if (sFragment) {
eMachine.outerHTML = sFragment;
doneMachine();
} else {
displayError("transformNodeToObject failed");
}
}
else if (document.implementation && document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor['importStylesheet'](xsl);
var eFragment = xsltProcessor['transformToFragment'](xml, document);
if (eFragment) {
/*
* This fails in Microsoft Edge...
*
var machine = eFragment.getElementById(idMachine);
if (!machine) {
displayError("machine generation failed: " + idMachine);
} else
*/
if (eMachine.parentNode) {
eMachine.parentNode.replaceChild(eFragment, eMachine);
doneMachine();
} else {
/*
* NOTE: This error can occur if our Node web server, when processing a folder with
* both a manifest.xml with a machine.xml reference AND a README.md containing a
* machine link, generates duplicate embedXXX() calls for the same machine; if the
* first embedXXX() call finds its target, subsequent calls for the same target will
* fail.
*
* Technically, such a folder is in a misconfigured state, but it happens, in part
* because when we switched to the Jekyll web server, we had to add machine links to
* all README.md files where we had previously relied on manifest.xml or machine.xml
* processing. This is because the Jekyll web server currently doesn't process XML
* files, nor is support for that likely to be added any time soon; it was a nice
* feature of the Node web server, but it's not clear that it's worth doing for Jekyll.
*/
displayError("invalid machine element: " + idMachine);
}
} else {
displayError("transformToFragment failed");
}
} else {
/*
* Perhaps I should have performed this test at the outset; on the other hand, I'm
* not aware of any browsers don't support one or both of the above XSLT transformation
* methods, so treat this as a bug.
*/
displayError("unable to transform XML: unsupported browser");
}
};
loadXML(sXSLFile, null, sAppName, sAppClass, null, false, displayMessage, transformXML);
};
if (sXMLFile.charAt(0) != '<') {
loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, true, displayMessage, processXML);
} else {
parseXML(sXMLFile, null, idMachine, sAppName, sAppClass, sParms, false, displayMessage, processXML);
}
} else {
displayError("missing machine element: " + idMachine);
}
} catch(e) {
displayError(e.message);
}
return fSuccess;
}
/**
* embedC1P(idMachine, sXMLFile, sXSLFile)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @return {boolean} true if successful, false if error
*/
function embedC1P(idMachine, sXMLFile, sXSLFile)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("C1Pjs", "c1pjs", APPVERSION, idMachine, sXMLFile, sXSLFile);
}
/**
* embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PCx86", "pcx86", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PC8080", "pc8080", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp10", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp11", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* findMachineComponent(idMachine, sType)
*
* @param {string} idMachine
* @param {string} sType
* @return {Component|null}
*/
function findMachineComponent(idMachine, sType)
{
return Component.getComponentByType(sType, idMachine + ".machine");
}
/**
* processMachineScript(idMachine, sScript)
*
* @param {string} idMachine
* @param {string} sScript
* @return {boolean}
*/
function processMachineScript(idMachine, sScript)
{
return Component.processScript(idMachine, sScript);
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them as global properties.
*
* TODO: Consider making all these functions properties on a single global object (eg, 'PCjs'), to minimize global
* pollution and risk of name collision.
*/
if (APPNAME == "C1Pjs") {
window['embedC1P'] = embedC1P;
}
if (APPNAME == "PCx86") {
window['embedPC'] = embedPCx86; // WARNING: embedPC() deprecated as of v1.23.0
window['embedPCx86'] = embedPCx86;
}
if (APPNAME == "PC8080") {
window['embedPC8080'] = embedPC8080;
}
if (APPNAME == "PDPjs") {
window['embedPDP10'] = embedPDP10;
window['embedPDP11'] = embedPDP11;
}
window['findMachineComponent'] = findMachineComponent;
window['processMachineScript'] = processMachineScript;
window['enableEvents'] = Web.enablePageEvents;
window['sendEvent'] = Web.sendPageEvent;